├── .gitignore ├── README.md ├── go ├── README.md ├── _examples │ ├── README.md │ ├── both_spot_futures.go │ ├── futures_order_test.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── orderbook.go │ └── spot_order_test.go ├── changelog.md ├── channel.go ├── channel_test.go ├── client.go ├── client_test.go ├── constant.go ├── go.mod ├── go.sum ├── go.work ├── model.go ├── model │ ├── future.go │ ├── future_order.go │ ├── spot.go │ └── spot_order.go ├── resp │ ├── future_order.go │ └── order.go ├── response_future.go └── response_spot.go └── python ├── .gitignore ├── README.md ├── examples ├── README.md ├── local_order_book.py ├── order.py └── ticker.py ├── gate_ws ├── __init__.py ├── client.py ├── futures.py └── spot.py ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python,go,macos,pycharm+all 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,go,macos,pycharm+all 4 | 5 | ### Go ### 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | 22 | ### Go Patch ### 23 | /vendor/ 24 | /Godeps/ 25 | 26 | ### macOS ### 27 | # General 28 | .DS_Store 29 | .AppleDouble 30 | .LSOverride 31 | 32 | # Icon must end with two \r 33 | Icon 34 | 35 | 36 | # Thumbnails 37 | ._* 38 | 39 | # Files that might appear in the root of a volume 40 | .DocumentRevisions-V100 41 | .fseventsd 42 | .Spotlight-V100 43 | .TemporaryItems 44 | .Trashes 45 | .VolumeIcon.icns 46 | .com.apple.timemachine.donotpresent 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | 55 | ### PyCharm+all ### 56 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 57 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 58 | 59 | # User-specific stuff 60 | .idea/**/workspace.xml 61 | .idea/**/tasks.xml 62 | .idea/**/usage.statistics.xml 63 | .idea/**/dictionaries 64 | .idea/**/shelf 65 | 66 | # Generated files 67 | .idea/**/contentModel.xml 68 | 69 | # Sensitive or high-churn files 70 | .idea/**/dataSources/ 71 | .idea/**/dataSources.ids 72 | .idea/**/dataSources.local.xml 73 | .idea/**/sqlDataSources.xml 74 | .idea/**/dynamic.xml 75 | .idea/**/uiDesigner.xml 76 | .idea/**/dbnavigator.xml 77 | 78 | # Gradle 79 | .idea/**/gradle.xml 80 | .idea/**/libraries 81 | 82 | # Gradle and Maven with auto-import 83 | # When using Gradle or Maven with auto-import, you should exclude module files, 84 | # since they will be recreated, and may cause churn. Uncomment if using 85 | # auto-import. 86 | # .idea/artifacts 87 | # .idea/compiler.xml 88 | # .idea/jarRepositories.xml 89 | # .idea/modules.xml 90 | # .idea/*.iml 91 | # .idea/modules 92 | # *.iml 93 | # *.ipr 94 | 95 | # CMake 96 | cmake-build-*/ 97 | 98 | # Mongo Explorer plugin 99 | .idea/**/mongoSettings.xml 100 | 101 | # File-based project format 102 | *.iws 103 | 104 | # IntelliJ 105 | out/ 106 | 107 | # mpeltonen/sbt-idea plugin 108 | .idea_modules/ 109 | 110 | # JIRA plugin 111 | atlassian-ide-plugin.xml 112 | 113 | # Cursive Clojure plugin 114 | .idea/replstate.xml 115 | 116 | # Crashlytics plugin (for Android Studio and IntelliJ) 117 | com_crashlytics_export_strings.xml 118 | crashlytics.properties 119 | crashlytics-build.properties 120 | fabric.properties 121 | 122 | # Editor-based Rest Client 123 | .idea/httpRequests 124 | 125 | # Android studio 3.1+ serialized cache file 126 | .idea/caches/build_file_checksums.ser 127 | 128 | ### PyCharm+all Patch ### 129 | # Ignores the whole .idea folder and all .iml files 130 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 131 | 132 | .idea/ 133 | 134 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 135 | 136 | *.iml 137 | modules.xml 138 | .idea/misc.xml 139 | *.ipr 140 | 141 | # Sonarlint plugin 142 | .idea/sonarlint 143 | 144 | ### Python ### 145 | # Byte-compiled / optimized / DLL files 146 | __pycache__/ 147 | *.py[cod] 148 | *$py.class 149 | 150 | # C extensions 151 | 152 | # Distribution / packaging 153 | .Python 154 | build/ 155 | develop-eggs/ 156 | dist/ 157 | downloads/ 158 | eggs/ 159 | .eggs/ 160 | parts/ 161 | sdist/ 162 | var/ 163 | wheels/ 164 | pip-wheel-metadata/ 165 | share/python-wheels/ 166 | *.egg-info/ 167 | .installed.cfg 168 | *.egg 169 | MANIFEST 170 | 171 | # PyInstaller 172 | # Usually these files are written by a python script from a template 173 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 174 | *.manifest 175 | *.spec 176 | 177 | # Installer logs 178 | pip-log.txt 179 | pip-delete-this-directory.txt 180 | 181 | # Unit test / coverage reports 182 | htmlcov/ 183 | .tox/ 184 | .nox/ 185 | .coverage 186 | .coverage.* 187 | .cache 188 | nosetests.xml 189 | coverage.xml 190 | *.cover 191 | *.py,cover 192 | .hypothesis/ 193 | .pytest_cache/ 194 | pytestdebug.log 195 | 196 | # Translations 197 | *.mo 198 | *.pot 199 | 200 | # Django stuff: 201 | *.log 202 | local_settings.py 203 | db.sqlite3 204 | db.sqlite3-journal 205 | 206 | # Flask stuff: 207 | instance/ 208 | .webassets-cache 209 | 210 | # Scrapy stuff: 211 | .scrapy 212 | 213 | # Sphinx documentation 214 | docs/_build/ 215 | doc/_build/ 216 | 217 | # PyBuilder 218 | target/ 219 | 220 | # Jupyter Notebook 221 | .ipynb_checkpoints 222 | 223 | # IPython 224 | profile_default/ 225 | ipython_config.py 226 | 227 | # pyenv 228 | .python-version 229 | 230 | # pipenv 231 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 232 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 233 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 234 | # install all needed dependencies. 235 | #Pipfile.lock 236 | 237 | # poetry 238 | #poetry.lock 239 | 240 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 241 | __pypackages__/ 242 | 243 | # Celery stuff 244 | celerybeat-schedule 245 | celerybeat.pid 246 | 247 | # SageMath parsed files 248 | *.sage.py 249 | 250 | # Environments 251 | # .env 252 | .env/ 253 | .venv/ 254 | env/ 255 | venv/ 256 | ENV/ 257 | env.bak/ 258 | venv.bak/ 259 | pythonenv* 260 | 261 | # Spyder project settings 262 | .spyderproject 263 | .spyproject 264 | 265 | # Rope project settings 266 | .ropeproject 267 | 268 | # mkdocs documentation 269 | /site 270 | 271 | # mypy 272 | .mypy_cache/ 273 | .dmypy.json 274 | dmypy.json 275 | 276 | # Pyre type checker 277 | .pyre/ 278 | 279 | # pytype static type analyzer 280 | .pytype/ 281 | 282 | # operating system-related files 283 | # file properties cache/storage on macOS 284 | *.DS_Store 285 | # thumbnail cache on Windows 286 | Thumbs.db 287 | 288 | # profiling data 289 | .prof 290 | 291 | 292 | # End of https://www.toptal.com/developers/gitignore/api/python,go,macos,pycharm+all 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # gatews - Gate WebSocket SDK 3 | 4 | `gatews` provides new Gate WebSocket V4 implementations. It is intended to 5 | work along with `gateapi-*` series to provide a quick way for developers to 6 | integrate Gate tradings. 7 | 8 | > This repository is meant to replace [WebSocket-API](https://github.com/gateio/WebSocket-API). 9 | > The latter will not accept any new feature requirements. 10 | 11 | Supported languages are: 12 | 13 | - [Python](python) 14 | - [Golang](go) 15 | 16 | Refer to corresponding directory for usage. 17 | 18 | -------------------------------------------------------------------------------- /go/README.md: -------------------------------------------------------------------------------- 1 | # Gate WebSocket Go SDK 2 | 3 | `gatews` provides Gate WebSocket V4 Go implementation, including all channels defined in spot(new) WebSocket. 4 | 5 | Features: 6 | 7 | 1. Fully asynchronous 8 | 2. Reconnect on connection to server lost, and resubscribe on connection recovered 9 | 3. Support connecting to multiple websocket servers 10 | 4. Highly configurable 11 | 12 | ## Installation 13 | 14 | ```shell 15 | go get github.com/gateio/gatews/go 16 | ``` 17 | 18 | ## Getting started 19 | 20 | ```golang 21 | package main 22 | 23 | import ( 24 | "encoding/json" 25 | "log" 26 | "os" 27 | "os/signal" 28 | "syscall" 29 | "time" 30 | 31 | gate "github.com/gateio/gatews/go" 32 | ) 33 | 34 | func main() { 35 | // create WsService with ConnConf, this is recommended, key and secret will be needed by some channels 36 | // ctx and logger could be nil, they'll be initialized by default 37 | ws, err := gate.NewWsService(nil, nil, gate.NewConnConfFromOption(&gate.ConfOptions{ 38 | Key: "YOUR_API_KEY", 39 | Secret: "YOUR_API_SECRET", 40 | MaxRetryConn: 10, // default value is math.MaxInt64, set it when needs 41 | SkipTlsVerify: false, 42 | })) 43 | // we can also do nothing to get a WsService, all parameters will be initialized by default and default url is spot 44 | // but some channels need key and secret for auth, we can also use set function to set key and secret 45 | // ws, err := gate.NewWsService(nil, nil, nil) 46 | // ws.SetKey("YOUR_API_KEY") 47 | // ws.SetSecret("YOUR_API_SECRET") 48 | if err != nil { 49 | log.Printf("NewWsService err:%s", err.Error()) 50 | return 51 | } 52 | 53 | // checkout connection status when needs 54 | go func() { 55 | ticker := time.NewTicker(time.Second) 56 | for { 57 | <-ticker.C 58 | log.Println("connetion status:", ws.Status()) 59 | } 60 | }() 61 | 62 | // create callback functions for receive messages 63 | callOrder := gate.NewCallBack(func(msg *gate.UpdateMsg) { 64 | // parse the message to struct we need 65 | var order []gate.SpotOrderMsg 66 | if err := json.Unmarshal(msg.Result, &order); err != nil { 67 | log.Printf("order Unmarshal err:%s", err.Error()) 68 | } 69 | log.Printf("%+v", order) 70 | }) 71 | 72 | callTrade := gate.NewCallBack(func(msg *gate.UpdateMsg) { 73 | var trade gate.SpotTradeMsg 74 | if err := json.Unmarshal(msg.Result, &trade); err != nil { 75 | log.Printf("trade Unmarshal err:%s", err.Error()) 76 | } 77 | log.Printf("%+v", trade) 78 | }) 79 | 80 | // first, we need set callback function 81 | ws.SetCallBack(gate.ChannelSpotOrder, callOrder) 82 | ws.SetCallBack(gate.ChannelSpotPublicTrade, callTrade) 83 | // second, after set callback function, subscribe to any channel you are interested into 84 | if err := ws.Subscribe(gate.ChannelSpotPublicTrade, []string{"BTC_USDT"}); err != nil { 85 | log.Printf("Subscribe err:%s", err.Error()) 86 | return 87 | } 88 | if err := ws.Subscribe(gate.ChannelSpotBookTicker, []string{"BTC_USDT"}); err != nil { 89 | log.Printf("Subscribe err:%s", err.Error()) 90 | return 91 | } 92 | 93 | // example for maintaining local order book 94 | // LocalOrderBook(context.Background(), ws, []string{"BTC_USDT"}) 95 | 96 | ch := make(chan os.Signal) 97 | signal.Ignore(syscall.SIGPIPE, syscall.SIGALRM) 98 | signal.Notify(ch, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGABRT, syscall.SIGKILL) 99 | <-ch 100 | } 101 | ``` 102 | 103 | ## Example 104 | 105 | We provide some demo applications in the [examples](_examples) directory, which can be run directly. 106 | 107 | ## ChangeLog 108 | 109 | [changelog](changelog.md) 110 | -------------------------------------------------------------------------------- /go/_examples/README.md: -------------------------------------------------------------------------------- 1 | ## example for order book 2 | 3 | ### How to maintain local depth 4 | 1. Subscribe `spot.order_book_update` with specified level and update frequency, e.g. `["BTC_USDT", "1000ms"]` pushes the update in BTC_USDT order book every 1s 5 | 2. Cache WebSocket notifications. Every notification use `U` and `u` to tell the first and last update ID since last notification. 6 | 3. Retrieve base order book using REST API, and make sure the order book ID is recorded(referred as `baseID` below) e.g. `https://api.gateio.ws/api/v4/spot/order_book?currency_pair=BTC_USDT&limit=100&with_id=true` retrieves the base order book of BTC_USDT 7 | 4. Iterate the cached WebSocket notifications, and find the first one which contains the baseID, i.e. `U <= baseId+1` and `u >= baseId+1`, then start consuming from it. Note that sizes in notifications are all absolute values. Use them to replace original sizes in corresponding price. If size equals to 0, delete the price from the order book. 8 | 5. Dump all notifications which satisfy `u < baseID+1`. If `baseID+1 < first notification U`, it means current base order book falls behind notifications. Start from step 3 to retrieve newer base order book. 9 | 6. If any subsequent notification which satisfy `U > baseID+1` is found, it means some updates are lost. Reconstruct local order book from step 3. 10 | 11 | -------------------------------------------------------------------------------- /go/_examples/both_spot_futures.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "time" 7 | 8 | gate "github.com/gateio/gatews/go" 9 | ) 10 | 11 | func main2() { 12 | spotWs, err := gate.NewWsService(nil, nil, gate.NewConnConfFromOption(&gate.ConfOptions{ 13 | URL: gate.BaseUrl, 14 | Key: "YOUR_API_KEY", 15 | Secret: "YOUR_API_SECRET", 16 | MaxRetryConn: 10, 17 | })) 18 | if err != nil { 19 | log.Printf("new spot wsService err:%s", err.Error()) 20 | return 21 | } 22 | 23 | futureWs, err := gate.NewWsService(nil, nil, gate.NewConnConfFromOption(&gate.ConfOptions{ 24 | URL: gate.FuturesUsdtUrl, 25 | Key: "YOUR_API_KEY", 26 | Secret: "YOUR_API_SECRET", 27 | MaxRetryConn: 10, 28 | })) 29 | if err != nil { 30 | log.Printf("new futures wsService err:%s", err.Error()) 31 | return 32 | } 33 | 34 | // create callback functions for receive messages 35 | // spot order book update 36 | callOrderBookUpdate := gate.NewCallBack(func(msg *gate.UpdateMsg) { 37 | // parse the message to struct we need 38 | var update gate.SpotUpdateDepthMsg 39 | if err := json.Unmarshal(msg.Result, &update); err != nil { 40 | log.Printf("order book update Unmarshal err:%s", err.Error()) 41 | } 42 | log.Printf("%+v", update) 43 | }) 44 | 45 | // futures trade 46 | callTrade := gate.NewCallBack(func(msg *gate.UpdateMsg) { 47 | var trades []gate.FuturesTrade 48 | if err := json.Unmarshal(msg.Result, &trades); err != nil { 49 | log.Printf("trade Unmarshal err:%s", err.Error()) 50 | } 51 | log.Printf("%+v", trades) 52 | }) 53 | 54 | // first, set callback 55 | spotWs.SetCallBack(gate.ChannelSpotOrderBookUpdate, callOrderBookUpdate) 56 | futureWs.SetCallBack(gate.ChannelFutureTrade, callTrade) 57 | if err := spotWs.Subscribe(gate.ChannelSpotOrderBookUpdate, []string{"BTC_USDT", "100ms"}); err != nil { 58 | log.Printf("spotWs Subscribe err:%s", err.Error()) 59 | return 60 | } 61 | 62 | if err := futureWs.Subscribe(gate.ChannelFutureTrade, []string{"BTC_USDT"}); err != nil { 63 | log.Printf("futureWs Subscribe err:%s", err.Error()) 64 | return 65 | } 66 | 67 | ch := make(chan bool) 68 | defer close(ch) 69 | 70 | for { 71 | select { 72 | case <-ch: 73 | log.Printf("manual done") 74 | case <-time.After(time.Second * 1000): 75 | log.Printf("auto done") 76 | return 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /go/_examples/futures_order_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | gate "github.com/gateio/gatews/go" 10 | "github.com/gateio/gatews/go/model" 11 | "github.com/gateio/gatews/go/resp" 12 | "github.com/rs/zerolog" 13 | "github.com/rs/zerolog/log" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | var ( 18 | testFuturesOrderParam = &model.FuturesOrder{ 19 | Contract: "BTC_USDT", 20 | Size: 50, 21 | Iceberg: 0, 22 | Price: "30000", 23 | Text: "t-my-custom-id", 24 | } 25 | 26 | testKeyVals = map[string]any{ 27 | "X-Gate-Channel-Id": "T-xxx", 28 | "req_id": "test_req_id", 29 | } 30 | ) 31 | 32 | type futuresTester struct { 33 | svc *gate.WsService 34 | } 35 | 36 | func newFuturesTester() (*futuresTester, error) { 37 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) 38 | 39 | tester, err := gate.NewWsService(nil, nil, gate.NewConnConfFromOption(&gate.ConfOptions{ 40 | // URL: "", 41 | App: "futures", // required 42 | Key: "", // required 43 | Secret: "", // required 44 | MaxRetryConn: 5, 45 | })) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return &futuresTester{tester}, nil 51 | } 52 | 53 | func (s *futuresTester) loginCallback() gate.CallBack { 54 | return gate.NewCallBack(func(msg *gate.UpdateMsg) { 55 | if msg.Data.Errs != nil { 56 | log.Error().Msgf("[Login] label: %s, message: %s", msg.Data.Errs.Label, msg.Data.Errs.Message) 57 | return 58 | } 59 | log.Info().Msgf("[Login] result: %s", msg.Data.Result) 60 | }) 61 | } 62 | 63 | func (s *futuresTester) createOrderCallback() gate.CallBack { 64 | return gate.NewCallBack(func(msg *gate.UpdateMsg) { 65 | if msg.Data.Errs != nil { 66 | log.Error().Msgf("[Create] label: %s, message: %s", msg.Data.Errs.Label, msg.Data.Errs.Message) 67 | return 68 | } 69 | 70 | var order resp.FutureOrder 71 | if err := json.Unmarshal(msg.Data.Result, &order); err != nil { 72 | log.Error().Msgf("[Create] failed to unmarshal response: %v, msg: %v", err, msg) 73 | return 74 | } 75 | 76 | if order.Id == 0 { 77 | return 78 | } 79 | 80 | log.Info().Msgf("[Create] order_id: %v, status: %s", order.Id, order.Status) 81 | }) 82 | } 83 | 84 | func (s *futuresTester) orderAmendCallback() gate.CallBack { 85 | return gate.NewCallBack(func(msg *gate.UpdateMsg) { 86 | if msg.Data.Errs != nil { 87 | log.Error().Msgf("[Amend] label: %s, message: %s", msg.Data.Errs.Label, msg.Data.Errs.Message) 88 | return 89 | } 90 | 91 | var order resp.FutureOrder 92 | if err := json.Unmarshal(msg.Data.Result, &order); err != nil { 93 | log.Error().Msgf("[Amend] failed to unmarshal response: %v, msg: %v", err, msg) 94 | return 95 | } 96 | 97 | log.Info().Msgf("[Amend] order_id: %v, status: %s", order.Id, order.Status) 98 | }) 99 | } 100 | 101 | func (s *futuresTester) orderStatusCallback() gate.CallBack { 102 | return gate.NewCallBack(func(msg *gate.UpdateMsg) { 103 | if msg.Data.Errs != nil { 104 | log.Info().Msgf("[Query] label: %s, message: %s", msg.Data.Errs.Label, msg.Data.Errs.Message) 105 | return 106 | } 107 | 108 | var order resp.FutureOrder 109 | if err := json.Unmarshal(msg.Data.Result, &order); err != nil { 110 | log.Info().Msgf("[Query] failed to unmarshal response: %v, msg: %v", err, msg) 111 | return 112 | } 113 | 114 | log.Info().Msgf("[Query] order: %#v", order) 115 | }) 116 | } 117 | 118 | func (s *futuresTester) orderCancelCallback() gate.CallBack { 119 | return gate.NewCallBack(func(msg *gate.UpdateMsg) { 120 | if msg.Data.Errs != nil { 121 | log.Error().Msgf("[Cancel] failed to cancel order, label: %s, message: %s", msg.Data.Errs.Label, msg.Data.Errs.Message) 122 | return 123 | } 124 | 125 | var orders *resp.FutureOrder 126 | if err := json.Unmarshal(msg.Data.Result, &orders); err != nil { 127 | log.Error().Msgf("[Cancel] failed to unmarshal response: %v, msg: %v", err, msg) 128 | return 129 | } 130 | 131 | log.Info().Msgf("[Cancel] order_id: %v, status: %v", orders.Id, orders.Status) 132 | }) 133 | } 134 | 135 | func TestFuturesCreateOrder(t *testing.T) { 136 | s, err := newFuturesTester() 137 | assert.NoError(t, err) 138 | 139 | s.svc.SetCallBack(gate.ChannelFutureLogin, s.loginCallback()) 140 | s.svc.SetCallBack(gate.ChannelFutureOrderPlace, s.createOrderCallback()) 141 | assert.NoError(t, s.svc.APIRequest(gate.ChannelFutureOrderPlace, testFuturesOrderParam, testKeyVals)) 142 | 143 | time.Sleep(5 * time.Second) 144 | } 145 | 146 | func TestFuturesAmendOrder(t *testing.T) { 147 | s, err := newFuturesTester() 148 | assert.NoError(t, err) 149 | 150 | s.svc.SetCallBack(gate.ChannelFutureLogin, s.loginCallback()) 151 | s.svc.SetCallBack(gate.ChannelFutureOrderAmend, s.orderAmendCallback()) 152 | 153 | order := &model.AmendFuturesOrder{ 154 | OrderId: "order_id", 155 | Settle: "USDT", 156 | Price: "40000", 157 | AmendText: "", 158 | Size: 1, 159 | } 160 | assert.NoError(t, s.svc.APIRequest(gate.ChannelFutureOrderAmend, order, testKeyVals)) 161 | } 162 | 163 | func TestFuturesQueryOrderStatus(t *testing.T) { 164 | s, err := newFuturesTester() 165 | assert.NoError(t, err) 166 | 167 | s.svc.SetCallBack(gate.ChannelFutureLogin, s.loginCallback()) 168 | s.svc.SetCallBack(gate.ChannelFutureOrderStatus, s.orderStatusCallback()) 169 | 170 | order := &model.StatusFuturesOrder{ 171 | Settle: "usdt", 172 | OrderId: "order_id", 173 | } 174 | 175 | assert.NoError(t, s.svc.APIRequest(gate.ChannelFutureOrderStatus, order, testKeyVals)) 176 | 177 | time.Sleep(5 * time.Second) 178 | } 179 | 180 | func TestFuturesCancelOrder(t *testing.T) { 181 | s, err := newFuturesTester() 182 | assert.NoError(t, err) 183 | 184 | s.svc.SetCallBack(gate.ChannelFutureLogin, s.loginCallback()) 185 | s.svc.SetCallBack(gate.ChannelFutureOrderCancel, s.orderCancelCallback()) 186 | 187 | order := &model.CancelFuturesOrder{ 188 | Settle: "usdt", 189 | OrderId: "order_id", 190 | } 191 | assert.NoError(t, s.svc.APIRequest(gate.ChannelFutureOrderCancel, order, testKeyVals)) 192 | 193 | time.Sleep(5 * time.Second) 194 | } 195 | -------------------------------------------------------------------------------- /go/_examples/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gansidui/skiplist v0.0.0-20141121051332-c6a909ce563b 7 | github.com/gateio/gatews/go v0.0.0-00010101000000-000000000000 8 | github.com/rs/zerolog v1.32.0 9 | github.com/shopspring/decimal v1.3.1 10 | github.com/stretchr/testify v1.9.0 11 | github.com/yireyun/go-queue v0.0.0-20220725040158-a4dd64810e1e 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/deckarep/golang-set v1.7.1 // indirect 17 | github.com/gorilla/websocket v1.4.2 // indirect 18 | github.com/mattn/go-colorable v0.1.13 // indirect 19 | github.com/mattn/go-isatty v0.0.19 // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | golang.org/x/sys v0.12.0 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | 25 | replace github.com/gateio/gatews/go => ../ 26 | -------------------------------------------------------------------------------- /go/_examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= 5 | github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= 6 | github.com/gansidui/skiplist v0.0.0-20141121051332-c6a909ce563b h1:MAoeneEI/UCOxABHa7aU2+8dqM89Uaj6tSMFxr1wbe0= 7 | github.com/gansidui/skiplist v0.0.0-20141121051332-c6a909ce563b/go.mod h1:8VKNiVGGPIhJE0qomrfsTKscI5iypji4r9wt4gEUDWE= 8 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 9 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 10 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 11 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 12 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 13 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 14 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 15 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 16 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 20 | github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= 21 | github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 22 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 23 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 24 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 25 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 26 | github.com/yireyun/go-queue v0.0.0-20220725040158-a4dd64810e1e h1:6BhwJMDB4lTjHfNKdf1O/IuhVS3Mw85/mnqpoQlngRk= 27 | github.com/yireyun/go-queue v0.0.0-20220725040158-a4dd64810e1e/go.mod h1:NS8O3p7NiPwC1Yw9xTd9DnDBguxFJG/BL1VPTSfJ5Gw= 28 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 31 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /go/_examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | gate "github.com/gateio/gatews/go" 12 | ) 13 | 14 | func main() { 15 | // create WsService with ConnConf, this is recommended, key and secret will be needed by some channels 16 | // ctx and logger could be nil, they'll be initialized by default 17 | ws, err := gate.NewWsService(nil, nil, gate.NewConnConfFromOption(&gate.ConfOptions{ 18 | Key: "YOUR_API_KEY", 19 | Secret: "YOUR_API_SECRET", 20 | MaxRetryConn: 10, // default value is math.MaxInt64, set it when needs 21 | SkipTlsVerify: false, 22 | })) 23 | // we can also do nothing to get a WsService, all parameters will be initialized by default and default url is spot 24 | // but some channels need key and secret for auth, we can also use set function to set key and secret 25 | // ws, err := gate.NewWsService(nil, nil, nil) 26 | // ws.SetKey("YOUR_API_KEY") 27 | // ws.SetSecret("YOUR_API_SECRET") 28 | if err != nil { 29 | log.Printf("NewWsService err:%s", err.Error()) 30 | return 31 | } 32 | 33 | // checkout connection status when needs 34 | go func() { 35 | ticker := time.NewTicker(time.Second) 36 | for { 37 | <-ticker.C 38 | log.Println("connetion status:", ws.Status()) 39 | } 40 | }() 41 | 42 | // create callback functions for receive messages 43 | callOrder := gate.NewCallBack(func(msg *gate.UpdateMsg) { 44 | if msg.Event != "update" { 45 | return 46 | } 47 | // parse the message to struct we need 48 | var order []gate.SpotOrderMsg 49 | if err := json.Unmarshal(msg.Result, &order); err != nil { 50 | log.Printf("order %s unmarshal err: %v", msg.Result, err) 51 | } 52 | log.Printf("order: %+v", order) 53 | }) 54 | 55 | callTrade := gate.NewCallBack(func(msg *gate.UpdateMsg) { 56 | var trade gate.SpotTradeMsg 57 | if err := json.Unmarshal(msg.Result, &trade); err != nil { 58 | log.Printf("trade %s unmarshal err: %v", msg.Result, err) 59 | } 60 | log.Printf("trade: %+v", trade) 61 | }) 62 | 63 | // first, we need set callback function 64 | ws.SetCallBack(gate.ChannelSpotOrder, callOrder) 65 | ws.SetCallBack(gate.ChannelSpotPublicTrade, callTrade) 66 | // second, after set callback function, subscribe to any channel you are interested into 67 | if err := ws.Subscribe(gate.ChannelSpotOrder, []string{"BTC_USDT"}); err != nil { 68 | log.Printf("Subscribe err:%s", err.Error()) 69 | return 70 | } 71 | if err := ws.Subscribe(gate.ChannelSpotPublicTrade, []string{"BTC_USDT"}); err != nil { 72 | log.Printf("Subscribe err:%s", err.Error()) 73 | return 74 | } 75 | 76 | // example for maintaining local order book 77 | // LocalOrderBook(context.Background(), ws, []string{"BTC_USDT"}) 78 | 79 | ch := make(chan os.Signal) 80 | signal.Ignore(syscall.SIGPIPE, syscall.SIGALRM) 81 | signal.Notify(ch, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGABRT, syscall.SIGKILL) 82 | <-ch 83 | } 84 | -------------------------------------------------------------------------------- /go/_examples/orderbook.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/gansidui/skiplist" 14 | "github.com/shopspring/decimal" 15 | queue2 "github.com/yireyun/go-queue" 16 | 17 | gate "github.com/gateio/gatews/go" 18 | ) 19 | 20 | const ( 21 | MaxLimit = 100 22 | QueueSize = 3000 23 | depthUrl = "https://api.gateio.ws/api/v4/spot/order_book?currency_pair=%s&limit=%d&with_id=true" 24 | ) 25 | 26 | var ( 27 | localOrderBook = sync.Map{} 28 | queue = queue2.NewQueue(QueueSize) 29 | spotMsg = new(sync.Map) 30 | ) 31 | 32 | type OrderBookEntry struct { 33 | Price decimal.Decimal `json:"p"` 34 | Size string `json:"s"` 35 | } 36 | 37 | func (e *OrderBookEntry) Less(other interface{}) bool { 38 | return e.Price.LessThan(other.(*OrderBookEntry).Price) 39 | } 40 | 41 | type OrderBook struct { 42 | ID int64 43 | Asks *skiplist.SkipList 44 | Bids *skiplist.SkipList 45 | } 46 | 47 | type HttpOrderBook struct { 48 | ID int64 `json:"id"` 49 | Asks [][]string `json:"asks"` 50 | Bids [][]string `json:"bids"` 51 | } 52 | 53 | func LocalOrderBook(ctx context.Context, ws *gate.WsService, cps []string) { 54 | callBack := gate.NewCallBack(func(msg *gate.UpdateMsg) { 55 | queue.Put(msg) 56 | if queue.Quantity()+5 >= QueueSize { 57 | log.Printf("LocalOrderBook queue is almost full") 58 | } 59 | }) 60 | 61 | channel := gate.ChannelSpotOrderBookUpdate 62 | 63 | ws.SetCallBack(channel, callBack) 64 | 65 | for _, cp := range cps { 66 | if err := ws.Subscribe(channel, []string{cp, "100ms"}); err != nil { 67 | log.Printf("Subscribe err:%s", err.Error()) 68 | } 69 | if resp, depth, err := getBaseDepth(cp, MaxLimit); err == nil { 70 | localOrderBook.Store(cp, depth) 71 | log.Printf("spot init market %s order book asks:%v, bids:%v", cp, resp.Asks, resp.Bids) 72 | } 73 | } 74 | 75 | for { 76 | select { 77 | case <-ctx.Done(): 78 | return 79 | default: 80 | msg, ok, _ := queue.Get() 81 | if !ok { 82 | continue 83 | } 84 | var depthMsg gate.SpotUpdateDepthMsg 85 | if err := json.Unmarshal(msg.(*gate.UpdateMsg).Result, &depthMsg); err != nil { 86 | log.Printf("order Unmarshal err:%s", err.Error()) 87 | } 88 | if v, ok := spotMsg.Load(depthMsg.CurrencyPair); ok { 89 | if v.(gate.SpotUpdateDepthMsg).LastId+1 != depthMsg.FirstId { 90 | log.Printf("%s order book msg id discontinuous, old id %d-%d, new msg id %d-%d", depthMsg.CurrencyPair, 91 | v.(gate.SpotUpdateDepthMsg).FirstId, v.(gate.SpotUpdateDepthMsg).LastId, depthMsg.FirstId, depthMsg.LastId) 92 | } 93 | } 94 | spotMsg.Store(depthMsg.CurrencyPair, depthMsg) 95 | if err := updateLocalOrderBook(depthMsg); err != nil { 96 | log.Printf("err:%s", err.Error()) 97 | } 98 | } 99 | } 100 | } 101 | 102 | func updateLocalOrderBook(msg gate.SpotUpdateDepthMsg) error { 103 | // log.Printf("updateLocalOrderBook msg:%+v", msg) 104 | 105 | if orderBook, ok := localOrderBook.Load(msg.CurrencyPair); ok { 106 | if orderBook.(*OrderBook).ID+1 >= msg.FirstId && orderBook.(*OrderBook).ID+1 <= msg.LastId { 107 | if err := updateOrderBook(orderBook.(*OrderBook), msg); err != nil { 108 | log.Printf("spot:%s", err.Error()) 109 | if strings.Contains(err.Error(), "overlapping") { 110 | if err = reGetBaseDepth(0, msg); err != nil { 111 | return err 112 | } 113 | } 114 | return err 115 | } 116 | } else if msg.LastId < orderBook.(*OrderBook).ID+1 { 117 | return nil 118 | } else if orderBook.(*OrderBook).ID+1 < msg.FirstId { 119 | log.Printf(">>>>>>>>>>>>>>>>%s depth is fall behind, now:%d, f:%d, l:%d", msg.CurrencyPair, orderBook.(*OrderBook).ID, msg.FirstId, msg.LastId) 120 | log.Printf("reinit %s depth", msg.CurrencyPair) 121 | if err := reGetBaseDepth(orderBook.(*OrderBook).ID, msg); err != nil { 122 | return err 123 | } else { 124 | if orderBook, ok := localOrderBook.Load(msg.CurrencyPair); ok { 125 | if orderBook.(*OrderBook).ID+1 >= msg.FirstId && orderBook.(*OrderBook).ID+1 <= msg.LastId { 126 | if err := updateOrderBook(orderBook.(*OrderBook), msg); err != nil { 127 | log.Printf("after reGetBaseDepth reconsume msg failed, msg:%+v, err:%s", msg, err.Error()) 128 | return err 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } else if msg.CurrencyPair != "" { 135 | log.Printf("init %s depth", msg.CurrencyPair) 136 | if err := reGetBaseDepth(0, msg); err != nil { 137 | return err 138 | } 139 | } 140 | return nil 141 | } 142 | 143 | func reGetBaseDepth(nowID int64, msg gate.SpotUpdateDepthMsg) (err error) { 144 | var depth *OrderBook 145 | var resp HttpOrderBook 146 | for nowID < msg.FirstId { 147 | resp, depth, err = getBaseDepth(msg.CurrencyPair, MaxLimit) 148 | if err != nil { 149 | return err 150 | } 151 | nowID = depth.ID 152 | } 153 | 154 | if depth != nil && depth.ID > 0 { 155 | log.Printf("after reGetBaseDepth resp %+v, ask min %s, bid max %s", resp, 156 | depth.Asks.Front().Value.(*OrderBookEntry).Price.String(), depth.Bids.Back().Value.(*OrderBookEntry).Price.String()) 157 | localOrderBook.Store(msg.CurrencyPair, depth) 158 | } 159 | 160 | return nil 161 | } 162 | 163 | func getBaseDepth(cp string, limit int) (HttpOrderBook, *OrderBook, error) { 164 | url := fmt.Sprintf(depthUrl, cp, limit) 165 | resp, err := http.DefaultClient.Get(url) 166 | if err != nil { 167 | return HttpOrderBook{}, nil, err 168 | } 169 | body, _ := ioutil.ReadAll(resp.Body) 170 | 171 | var baseOrderBook HttpOrderBook 172 | err = json.Unmarshal(body, &baseOrderBook) 173 | if err != nil { 174 | return baseOrderBook, nil, err 175 | } 176 | 177 | asks := skiplist.New() 178 | bids := skiplist.New() 179 | if len(baseOrderBook.Asks) > 0 { 180 | for _, a := range baseOrderBook.Asks { 181 | askEntry, err := fromHttpOrderBook(a) 182 | if err != nil { 183 | return baseOrderBook, nil, err 184 | } 185 | asks.Insert(askEntry) 186 | } 187 | for _, b := range baseOrderBook.Bids { 188 | bidEntry, err := fromHttpOrderBook(b) 189 | if err != nil { 190 | return baseOrderBook, nil, err 191 | } 192 | bids.Insert(bidEntry) 193 | } 194 | } 195 | if asks.Len() > 0 && bids.Len() > 0 { 196 | // reject overlapping 197 | if asks.Front().Value.(*OrderBookEntry).Price.LessThanOrEqual(bids.Back().Value.(*OrderBookEntry).Price) { 198 | return baseOrderBook, nil, fmt.Errorf("overlapping price ask[%s] and bid[%s]", 199 | asks.Front().Value.(*OrderBookEntry).Price.String(), bids.Back().Value.(*OrderBookEntry).Price.String()) 200 | } 201 | } 202 | 203 | return baseOrderBook, &OrderBook{ 204 | ID: baseOrderBook.ID, 205 | Asks: asks, 206 | Bids: bids, 207 | }, nil 208 | } 209 | 210 | func fromHttpOrderBook(apiEntry []string) (*OrderBookEntry, error) { 211 | if len(apiEntry) != 2 { 212 | return nil, fmt.Errorf("invalid http order book entry") 213 | } 214 | price, err := decimal.NewFromString(apiEntry[0]) 215 | if err != nil { 216 | return nil, fmt.Errorf("invalid price %s: %v", apiEntry[0], err) 217 | } 218 | return &OrderBookEntry{Price: price, Size: apiEntry[1]}, nil 219 | } 220 | 221 | func updateOrderBook(orderBook *OrderBook, update gate.SpotUpdateDepthMsg) error { 222 | orderBook.ID = update.LastId 223 | if len(update.Ask) > 0 { 224 | for _, ask := range update.Ask { 225 | askEntry, err := fromHttpOrderBook(ask) 226 | if err != nil { 227 | log.Printf("incorrect http ask entry %v: %v", ask, err) 228 | return err 229 | } 230 | if ask[1] == "0" { 231 | for e := orderBook.Asks.Front(); e != nil; e = e.Next() { 232 | if e.Value.(*OrderBookEntry).Price.String() == ask[0] { 233 | orderBook.Asks.Delete(e.Value) 234 | break 235 | } 236 | } 237 | } else { 238 | if e := orderBook.Asks.Find(askEntry); e != nil { 239 | e.Value.(*OrderBookEntry).Size = ask[1] 240 | } else { 241 | orderBook.Asks.Insert(askEntry) 242 | } 243 | } 244 | } 245 | } 246 | 247 | if len(update.Bid) > 0 { 248 | for _, bid := range update.Bid { 249 | bidEntry, err := fromHttpOrderBook(bid) 250 | if err != nil { 251 | log.Printf("incorrect http bid entry %v: %v", bid, err) 252 | return err 253 | } 254 | if bid[1] == "0" { 255 | for e := orderBook.Bids.Back(); e != nil; e = e.Prev() { 256 | if e.Value.(*OrderBookEntry).Price.String() == bid[0] { 257 | orderBook.Bids.Delete(e.Value) 258 | break 259 | } 260 | } 261 | } else { 262 | if e := orderBook.Bids.Find(bidEntry); e != nil { 263 | e.Value.(*OrderBookEntry).Size = bid[1] 264 | } else { 265 | orderBook.Bids.Insert(bidEntry) 266 | } 267 | } 268 | } 269 | } 270 | 271 | // judge overlapping 272 | if orderBook.Asks.Len() > 0 && orderBook.Bids.Len() > 0 { 273 | // reject overlapping 274 | if orderBook.Asks.Front().Value.(*OrderBookEntry).Price.LessThanOrEqual(orderBook.Bids.Back().Value.(*OrderBookEntry).Price) { 275 | return fmt.Errorf("overlapping price ask[%s] and bid[%s]", 276 | orderBook.Asks.Front().Value.(*OrderBookEntry).Price.String(), orderBook.Bids.Back().Value.(*OrderBookEntry).Price.String()) 277 | } 278 | } 279 | return nil 280 | } 281 | -------------------------------------------------------------------------------- /go/_examples/spot_order_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | gate "github.com/gateio/gatews/go" 10 | "github.com/gateio/gatews/go/model" 11 | "github.com/gateio/gatews/go/resp" 12 | "github.com/rs/zerolog" 13 | "github.com/rs/zerolog/log" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | var testSpotOrderParam = &model.Order{ 18 | CurrencyPair: "BTC_USDT", 19 | Amount: "1", 20 | Account: "spot", 21 | Iceberg: "0", 22 | TimeInForce: "gtc", 23 | Price: "18000", 24 | Text: "t-my-custom-id", 25 | Side: "buy", 26 | } 27 | 28 | type spotOrderTester struct { 29 | svc *gate.WsService 30 | } 31 | 32 | func newSpotOrderTester() (*spotOrderTester, error) { 33 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) 34 | 35 | tester, err := gate.NewWsService(nil, nil, gate.NewConnConfFromOption(&gate.ConfOptions{ 36 | // URL: "", 37 | Key: "", // required 38 | Secret: "", // required 39 | MaxRetryConn: 5, 40 | })) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return &spotOrderTester{tester}, nil 46 | } 47 | 48 | func (s *spotOrderTester) loginCallback() gate.CallBack { 49 | return gate.NewCallBack(func(msg *gate.UpdateMsg) { 50 | if msg.Data.Errs != nil { 51 | log.Error().Msgf("[Login] label: %s, message: %s", msg.Data.Errs.Label, msg.Data.Errs.Message) 52 | return 53 | } 54 | log.Info().Msgf("[Login] result: %s", msg.Data.Result) 55 | }) 56 | } 57 | 58 | func (s *spotOrderTester) createOrderCallback() gate.CallBack { 59 | return gate.NewCallBack(func(msg *gate.UpdateMsg) { 60 | if msg.Data.Errs != nil { 61 | log.Error().Msgf("[Create] label: %s, message: %s", msg.Data.Errs.Label, msg.Data.Errs.Message) 62 | return 63 | } 64 | 65 | var order resp.SpotOrder 66 | if err := json.Unmarshal(msg.Data.Result, &order); err != nil { 67 | log.Error().Msgf("[Create] failed to unmarshal response: %v, msg: %v", err, msg) 68 | return 69 | } 70 | 71 | if order.Id == "" { 72 | return 73 | } 74 | 75 | log.Info().Msgf("[Create] order_id: %s, price: %v, amount: %v, status: %s", order.Id, order.Price, order.Amount, order.Status) 76 | }) 77 | } 78 | 79 | func (s *spotOrderTester) orderAmendCallback() gate.CallBack { 80 | return gate.NewCallBack(func(msg *gate.UpdateMsg) { 81 | 82 | if msg.Data.Errs != nil { 83 | log.Error().Msgf("[Amend] label: %s, message: %s", msg.Data.Errs.Label, msg.Data.Errs.Message) 84 | return 85 | } 86 | 87 | var order resp.SpotOrder 88 | if err := json.Unmarshal(msg.Data.Result, &order); err != nil { 89 | log.Error().Msgf("[Amend] failed to unmarshal response: %v, msg: %v", err, msg) 90 | return 91 | } 92 | 93 | log.Info().Msgf("[Amend] order_id: %s, price: %v, amount: %v, status: %s", order.Id, order.Price, order.Amount, order.Status) 94 | }) 95 | } 96 | 97 | func (s *spotOrderTester) orderStatusCallback() gate.CallBack { 98 | return gate.NewCallBack(func(msg *gate.UpdateMsg) { 99 | if msg.Data.Errs != nil { 100 | log.Error().Msgf("[Query] label: %s, message: %s", msg.Data.Errs.Label, msg.Data.Errs.Message) 101 | return 102 | } 103 | 104 | var order resp.SpotOrder 105 | if err := json.Unmarshal(msg.Data.Result, &order); err != nil { 106 | log.Error().Msgf("[Query] failed to unmarshal response: %v, msg: %v", err, msg) 107 | return 108 | } 109 | 110 | log.Info().Msgf("[Query] order_id: %s, status: %s", order.Id, order.Status) 111 | }) 112 | } 113 | 114 | func (s *spotOrderTester) orderCancelCallback() gate.CallBack { 115 | return gate.NewCallBack(func(msg *gate.UpdateMsg) { 116 | if msg.Data.Errs != nil { 117 | log.Info().Msgf("[Cancel] failed to cancel order, label: %s, message: %s", msg.Data.Errs.Label, msg.Data.Errs.Message) 118 | return 119 | } 120 | 121 | orders := make([]*resp.SpotOrder, 0) 122 | if err := json.Unmarshal(msg.Data.Result, &orders); err != nil { 123 | log.Info().Msgf("[Cancel] failed to unmarshal response: %v, msg: %v", err, msg) 124 | return 125 | } 126 | 127 | log.Info().Msgf("[Cancel] order_id: %s, cancel succeeded: %v", orders[0].Id, orders[0].Succeeded) 128 | }) 129 | } 130 | 131 | func TestSpotCreateOrder(t *testing.T) { 132 | s, err := newSpotOrderTester() 133 | assert.NoError(t, err) 134 | 135 | s.svc.SetCallBack(gate.ChannelSpotLogin, s.loginCallback()) 136 | s.svc.SetCallBack(gate.ChannelSpotOrderPlace, s.createOrderCallback()) 137 | 138 | assert.NoError(t, s.svc.APIRequest(gate.ChannelSpotOrderPlace, testSpotOrderParam, testKeyVals)) 139 | 140 | time.Sleep(5 * time.Second) 141 | } 142 | 143 | func TestSpotAmendOrder(t *testing.T) { 144 | s, err := newSpotOrderTester() 145 | assert.NoError(t, err) 146 | 147 | s.svc.SetCallBack(gate.ChannelSpotLogin, s.loginCallback()) 148 | s.svc.SetCallBack(gate.ChannelSpotOrderAmend, s.orderAmendCallback()) 149 | 150 | // NOTE: Only can chose one of amount or price 151 | order := &model.AmendOrderParam{ 152 | Price: "19000", 153 | OrderId: "order_id", 154 | CurrencyPair: "BTC_USDT", 155 | AmendText: "", 156 | } 157 | assert.NoError(t, s.svc.APIRequest(gate.ChannelSpotOrderAmend, order, testKeyVals)) 158 | 159 | time.Sleep(5 * time.Second) 160 | } 161 | 162 | func TestSpotQueryOrderStatus(t *testing.T) { 163 | s, err := newSpotOrderTester() 164 | assert.NoError(t, err) 165 | 166 | s.svc.SetCallBack(gate.ChannelSpotLogin, s.loginCallback()) 167 | s.svc.SetCallBack(gate.ChannelSpotOrderStatus, s.orderStatusCallback()) 168 | orderStatus := &model.StatusOrderParam{ 169 | CurrencyPair: "BTC_USDT", 170 | OrderId: "order_id", 171 | } 172 | 173 | assert.NoError(t, s.svc.APIRequest(gate.ChannelSpotOrderStatus, orderStatus, testKeyVals)) 174 | 175 | time.Sleep(5 * time.Second) 176 | } 177 | 178 | func TestSpotCancelOrder(t *testing.T) { 179 | s, err := newSpotOrderTester() 180 | assert.NoError(t, err) 181 | 182 | s.svc.SetCallBack(gate.ChannelSpotLogin, s.loginCallback()) 183 | s.svc.SetCallBack(gate.ChannelSpotOrderCancelIds, s.orderCancelCallback()) 184 | 185 | testSpotOrderParam.Id = "order_id" 186 | assert.NoError(t, s.svc.APIRequest(gate.ChannelSpotOrderCancelIds, []*model.Order{testSpotOrderParam}, testKeyVals)) 187 | 188 | time.Sleep(5 * time.Second) 189 | } 190 | -------------------------------------------------------------------------------- /go/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.5.1 4 | 5 | 2024-08-14 6 | 7 | - added recent new fields 8 | 9 | ## v0.5.0 10 | 11 | 2024-04-30 12 | 13 | - support order operations 14 | 15 | ## v0.4.2 16 | 17 | 2023-05-09 18 | 19 | - no longer try websocket ping after disconnect 20 | - support querying websocket connection status 21 | 22 | ## v0.4.1 23 | 24 | 2023-03-16 25 | 26 | - fix subscribe msg concurrent write cause panic 27 | - add `FuturesOrder` response field `stop_profit_price` and `stop_loss_price` 28 | - add `FuturesAutoOrder` response field `me_order_id`, `order_type` and `initial.auto_size` 29 | 30 | ## v0.4.0 31 | 32 | 2022-12-07 33 | 34 | - spot balance add fields `freeze`,`freeze_change` and `change_type` 35 | 36 | ## v0.3.0 37 | 38 | 2022-11-22 39 | 40 | - add common response field `time_ms` for time of message created 41 | 42 | ## v0.2.8 43 | 44 | 2022-10-21 45 | 46 | - futures model `FuturesUserTrade` add fields `fee` and `point_fee` 47 | - avoid saving `ping` and `time` subscribe msg in request history 48 | 49 | ## v0.2.7 50 | 51 | 2022-08-11 52 | 53 | - remove client method `NewConnConf`. Recommend to use `NewConnConfFromOption` instead 54 | - add new config field `PingInterval` to send ping message 55 | - add default ping message to avoid to be closed by server 56 | 57 | ## v0.2.6 58 | 59 | 2022-05-24 60 | 61 | - fix reconnect panic 62 | - update spot and futures models 63 | - add `ShowReconnectMsg` config field to decide to show reconnect success msg 64 | - update some test cases 65 | 66 | ## v0.2.5 67 | 68 | - update future's models, fix fields wrong type 69 | 70 | ## v0.2.4 71 | 72 | 2021-08-12 73 | 74 | - update futures models, fix fields wrong type 75 | - add futures model `FuturesPositions` 76 | 77 | ## v0.2.3 78 | 79 | 2021-08-11 80 | 81 | - Support websocket skip tls verify with `SkipTlsVerify` of ConfOptions 82 | - Update futures models 83 | - Update examples 84 | 85 | ## v0.2.2 86 | 87 | 2021-07-23 88 | 89 | - Add constant `ChannelSpotCrossBalance` support `spot.cross_balances` channel 90 | - SpotUserTradesMsg add field `Text` for orders' text 91 | - Update local order book example 92 | 93 | ## v0.2.1 94 | 95 | 2021-06-24 96 | 97 | - fix futures book ticker and order book update struct 98 | 99 | ## v0.2.0 100 | 101 | 2021-06-24 102 | 103 | - fix futures order book update struct 104 | 105 | ## v0.1.9 106 | 107 | 2021-06-24 108 | 109 | - add futures order book struct 110 | 111 | ## v0.1.8 112 | 113 | 2021-06-22 114 | 115 | - add `WsService` method `GetConnection()` to get the connection 116 | - fix `changelog` date error 117 | 118 | ## v0.1.7 119 | 120 | 2021-06-04 121 | 122 | - fix reconnect msg nil `SubscribeOptions` caused reconnect msg lost 123 | 124 | ## v0.1.6 125 | 126 | 2021-06-04 127 | 128 | - add `io.ErrUnexpectedEOF` error capture, it caused v0.1.5 can't reconnect 129 | - fix reconnect msg repeat add 130 | 131 | ## v0.1.5 132 | 133 | 2021-06-02 134 | 135 | - add `SpotUpdateAllDepthMsg` struct for parse all order book msg 136 | 137 | ## v0.1.4 138 | 139 | 2021-06-02 140 | 141 | - fix overlapping price for local order book example 142 | - update README 143 | 144 | ## v0.1.3 145 | 146 | 2021-05-26 147 | 148 | - Support futures websocket. 149 | - Modify channels name to with flag `Spot` or `Future`. 150 | - Add field `TimestampInMilli` in models `SpotBalancesMsg`, `SpotFundingBalancesMsg`, `SpotMarginBalancesMsg`. Add 151 | field `TimeInMilli` in model `SpotBookTickerMsg` 152 | - Add new method `NewConnConfFromOption` to get a ConnConf flexible. 153 | - Add new method `SubscribeWithOption` to support futures subscribe with id. 154 | - Add example for both spot and futures connection use. 155 | - Fix reconnect websocket failed bug. 156 | - Optimizing code structure. 157 | 158 | ## v0.1.2 159 | 160 | 2021-04-19 161 | 162 | - Fix subscribe repeat bug. 163 | 164 | ## v0.1.1 165 | 166 | 2021-04-16 167 | 168 | - Fix subscribe channel failed bug. 169 | 170 | ## v0.1.0 171 | 172 | 2021-04-12 173 | 174 | - Support spot websocket function. 175 | -------------------------------------------------------------------------------- /go/channel.go: -------------------------------------------------------------------------------- 1 | package gatews 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha512" 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | type SubscribeOptions struct { 17 | ID int64 `json:"id"` 18 | IsReConnect bool `json:"-"` 19 | } 20 | 21 | func (ws *WsService) Subscribe(channel string, payload []string) error { 22 | if (ws.conf.Key == "" || ws.conf.Secret == "") && authChannel[channel] { 23 | return newAuthEmptyErr() 24 | } 25 | 26 | msgCh, ok := ws.msgChs.Load(channel) 27 | if !ok { 28 | msgCh = make(chan *UpdateMsg, 1) 29 | go ws.receiveCallMsg(channel, msgCh.(chan *UpdateMsg)) 30 | } 31 | 32 | return ws.newBaseChannel(channel, payload, msgCh.(chan *UpdateMsg), nil) 33 | } 34 | 35 | func (ws *WsService) SubscribeWithOption(channel string, payload any, op *SubscribeOptions) error { 36 | if (ws.conf.Key == "" || ws.conf.Secret == "") && authChannel[channel] { 37 | return newAuthEmptyErr() 38 | } 39 | 40 | msgCh, ok := ws.msgChs.Load(channel) 41 | if !ok { 42 | msgCh = make(chan *UpdateMsg, 1) 43 | go ws.receiveCallMsg(channel, msgCh.(chan *UpdateMsg)) 44 | } 45 | 46 | return ws.newBaseChannel(channel, payload, msgCh.(chan *UpdateMsg), op) 47 | } 48 | 49 | func (ws *WsService) UnSubscribe(channel string, payload []string) error { 50 | return ws.baseSubscribe(UnSubscribe, channel, payload, nil) 51 | } 52 | 53 | func (ws *WsService) newBaseChannel(channel string, payload any, bch chan *UpdateMsg, op *SubscribeOptions) error { 54 | err := ws.baseSubscribe(Subscribe, channel, payload, op) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if _, ok := ws.msgChs.Load(channel); !ok { 60 | ws.msgChs.Store(channel, bch) 61 | } 62 | 63 | ws.readMsg() 64 | 65 | return nil 66 | } 67 | 68 | func (ws *WsService) baseSubscribe(event, channel string, payload any, op *SubscribeOptions) error { 69 | ts := time.Now().Unix() 70 | hash := hmac.New(sha512.New, []byte(ws.conf.Secret)) 71 | hash.Write([]byte(fmt.Sprintf("channel=%s&event=%s&time=%d", channel, Subscribe, ts))) 72 | req := Request{ 73 | Time: ts, 74 | Channel: channel, 75 | Event: event, 76 | Payload: payload, 77 | Auth: Auth{ 78 | Method: AuthMethodApiKey, 79 | Key: ws.conf.Key, 80 | Secret: hex.EncodeToString(hash.Sum(nil)), 81 | }, 82 | } 83 | // options 84 | if op != nil { 85 | req.Id = &op.ID 86 | } 87 | 88 | byteReq, err := json.Marshal(req) 89 | if err != nil { 90 | ws.Logger.Printf("req Marshal err:%s", err.Error()) 91 | return err 92 | } 93 | ws.mu.Lock() 94 | defer ws.mu.Unlock() 95 | 96 | err = ws.Client.WriteMessage(websocket.TextMessage, byteReq) 97 | if err != nil { 98 | ws.Logger.Printf("wsWrite [%s] err:%s", channel, err.Error()) 99 | return err 100 | } 101 | 102 | if strings.HasSuffix(channel, "ping") { 103 | return nil 104 | } 105 | 106 | if v, ok := ws.conf.subscribeMsg.Load(channel); ok { 107 | if op != nil && op.IsReConnect { 108 | return nil 109 | } 110 | reqs := v.([]requestHistory) 111 | reqs = append(reqs, requestHistory{ 112 | Channel: channel, 113 | Event: event, 114 | Payload: payload, 115 | }) 116 | ws.conf.subscribeMsg.Store(channel, reqs) 117 | } else { 118 | // avoid saving invalid subscribe msg 119 | if strings.HasSuffix(channel, ".ping") || strings.HasSuffix(channel, ".time") { 120 | return nil 121 | } 122 | 123 | ws.conf.subscribeMsg.Store(channel, []requestHistory{{ 124 | Channel: channel, 125 | Event: event, 126 | Payload: payload, 127 | }}) 128 | } 129 | 130 | return nil 131 | } 132 | 133 | // readMsg only run once to read message 134 | func (ws *WsService) readMsg() { 135 | ws.once.Do(func() { 136 | go func() { 137 | defer ws.Client.Close() 138 | 139 | for { 140 | select { 141 | case <-ws.Ctx.Done(): 142 | ws.Logger.Printf("closing reader") 143 | return 144 | 145 | default: 146 | _, rawMsg, err := ws.Client.ReadMessage() 147 | if err != nil { 148 | ws.Logger.Printf("websocket err: %s", err.Error()) 149 | if e := ws.reconnect(); e != nil { 150 | ws.Logger.Printf("reconnect err:%s", err.Error()) 151 | return 152 | } 153 | ws.Logger.Println("reconnect success, continue read message") 154 | continue 155 | } 156 | 157 | var msg UpdateMsg 158 | if err := json.Unmarshal(rawMsg, &msg); err != nil { 159 | continue 160 | } 161 | 162 | channel := msg.GetChannel() 163 | if channel == "" { 164 | ws.Logger.Printf("channel is empty in message %v", msg) 165 | return 166 | } 167 | 168 | if bch, ok := ws.msgChs.Load(channel); ok { 169 | select { 170 | case <-ws.Ctx.Done(): 171 | return 172 | default: 173 | if _, ok := ws.msgChs.Load(channel); ok { 174 | bch.(chan *UpdateMsg) <- &msg 175 | } 176 | } 177 | } 178 | } 179 | } 180 | }() 181 | }) 182 | } 183 | 184 | type CallBack func(*UpdateMsg) 185 | 186 | func NewCallBack(f func(*UpdateMsg)) func(*UpdateMsg) { 187 | return f 188 | } 189 | 190 | func (ws *WsService) SetCallBack(channel string, call CallBack) { 191 | if call == nil { 192 | return 193 | } 194 | ws.calls.Store(channel, call) 195 | } 196 | 197 | func (ws *WsService) receiveCallMsg(channel string, msgCh chan *UpdateMsg) { 198 | // avoid send closed channel error 199 | // defer close(msgCh) 200 | for { 201 | select { 202 | case <-ws.Ctx.Done(): 203 | ws.Logger.Printf("received parent context exit") 204 | return 205 | case msg := <-msgCh: 206 | if call, ok := ws.calls.Load(channel); ok { 207 | call.(CallBack)(msg) 208 | } 209 | } 210 | } 211 | } 212 | 213 | func (ws *WsService) APIRequest(channel string, payload any, keyVals map[string]any) error { 214 | var err error 215 | ws.loginOnce.Do(func() { 216 | err = ws.login() 217 | }) 218 | 219 | if err != nil { 220 | return err 221 | } 222 | 223 | if (ws.conf.Key == "" || ws.conf.Secret == "") && authChannel[channel] { 224 | return newAuthEmptyErr() 225 | } 226 | 227 | msgCh, ok := ws.msgChs.Load(channel) 228 | if !ok { 229 | msgCh = make(chan *UpdateMsg, 1) 230 | go ws.receiveCallMsg(channel, msgCh.(chan *UpdateMsg)) 231 | } 232 | 233 | if _, ok := ws.msgChs.Load(channel); !ok { 234 | ws.msgChs.Store(channel, msgCh) 235 | } 236 | 237 | ws.readMsg() 238 | 239 | return ws.apiRequest(channel, payload, keyVals) 240 | } 241 | 242 | func (ws *WsService) login() error { 243 | if ws.conf.Key == "" || ws.conf.Secret == "" { 244 | return newAuthEmptyErr() 245 | } 246 | channel := ChannelSpotLogin 247 | if ws.conf.App == "futures" { 248 | channel = ChannelFutureLogin 249 | } 250 | msgCh, ok := ws.msgChs.Load(channel) 251 | if !ok { 252 | msgCh = make(chan *UpdateMsg, 1) 253 | go ws.receiveCallMsg(channel, msgCh.(chan *UpdateMsg)) 254 | } 255 | 256 | if _, ok := ws.msgChs.Load(channel); !ok { 257 | ws.msgChs.Store(channel, msgCh) 258 | } 259 | 260 | ws.readMsg() 261 | 262 | return ws.apiRequest(channel, nil, nil) 263 | } 264 | 265 | func (ws *WsService) apiRequest(channel string, payload any, keyVals map[string]any) error { 266 | req := Request{ 267 | Time: time.Now().Unix(), 268 | Channel: channel, 269 | Event: API, 270 | Payload: ws.generateAPIRequest(channel, payload, keyVals), 271 | } 272 | 273 | byteReq, err := json.Marshal(req) 274 | if err != nil { 275 | ws.Logger.Printf("req Marshal err:%s", err.Error()) 276 | return err 277 | } 278 | ws.mu.Lock() 279 | defer ws.mu.Unlock() 280 | 281 | return ws.Client.WriteMessage(websocket.TextMessage, byteReq) 282 | } 283 | 284 | func (ws *WsService) generateAPIRequest(channel string, placeParam any, keyVals map[string]any) any { 285 | reqID := "req_id" 286 | gateChannelID := "T_channel_id" 287 | 288 | if v, ok := keyVals["req_id"]; ok { 289 | reqID, _ = v.(string) 290 | } 291 | 292 | if v, ok := keyVals["X-Gate-Channel-Id"]; ok { 293 | gateChannelID, _ = v.(string) 294 | } 295 | 296 | now := time.Now().Unix() 297 | 298 | reqParam, _ := json.Marshal(placeParam) 299 | 300 | message := fmt.Sprintf("api\n%s\n%s\n%d", channel, reqParam, now) 301 | 302 | return APIReq{ 303 | ApiKey: ws.conf.Key, 304 | Signature: calculateSignature(ws.conf.Secret, message), 305 | Timestamp: strconv.Itoa(int(now)), 306 | ReqId: reqID, 307 | ReqHeader: json.RawMessage(fmt.Sprintf(`{"X-Gate-Channel-Id":"%s"}`, gateChannelID)), 308 | ReqParam: reqParam, 309 | } 310 | } 311 | 312 | func calculateSignature(secret string, message string) string { 313 | h := hmac.New(sha512.New, []byte(secret)) 314 | h.Write([]byte(message)) 315 | return hex.EncodeToString(h.Sum(nil)) 316 | } 317 | -------------------------------------------------------------------------------- /go/channel_test.go: -------------------------------------------------------------------------------- 1 | package gatews 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestNilCallBack(t *testing.T) { 11 | ws, err := NewWsService(nil, nil, nil) 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | ws.SetCallBack(ChannelSpotPublicTrade, nil) 17 | if err := ws.Subscribe(ChannelSpotPublicTrade, []string{"BCH_USDT"}); err != nil { 18 | log.Fatalf("Subscribe err:%s", err.Error()) 19 | return 20 | } 21 | 22 | ch := make(chan bool) 23 | defer close(ch) 24 | 25 | for { 26 | select { 27 | case <-ch: 28 | log.Printf("manual done") 29 | case <-time.After(time.Second * 1000): 30 | log.Printf("auto done") 31 | return 32 | } 33 | } 34 | } 35 | 36 | func TestSubscribeFutures(t *testing.T) { 37 | ws, err := NewWsService(nil, nil, NewConnConfFromOption(&ConfOptions{ 38 | URL: FuturesUsdtUrl, Key: "", Secret: "", MaxRetryConn: 10, 39 | })) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | call := NewCallBack(func(msg *UpdateMsg) { 45 | fmt.Println(string(msg.Result)) 46 | }) 47 | ws.SetCallBack(ChannelFutureCandleStick, call) 48 | if err := ws.Subscribe(ChannelFutureCandleStick, []string{"1m", "BTC_USDT"}); err != nil { 49 | log.Fatalf("Subscribe err:%s", err.Error()) 50 | return 51 | } 52 | 53 | ch := make(chan bool) 54 | defer close(ch) 55 | 56 | for { 57 | select { 58 | case <-ch: 59 | log.Printf("manual done") 60 | case <-time.After(time.Second * 1000): 61 | log.Printf("auto done") 62 | return 63 | } 64 | } 65 | } 66 | 67 | func TestSubscribeFuturesWithOptions(t *testing.T) { 68 | ws, err := NewWsService(nil, nil, NewConnConfFromOption(&ConfOptions{ 69 | URL: FuturesUsdtUrl, 70 | })) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | call := NewCallBack(func(msg *UpdateMsg) { 76 | fmt.Printf("%+v\n", msg) 77 | }) 78 | ws.SetCallBack(ChannelFutureTrade, call) 79 | if err := ws.SubscribeWithOption(ChannelFutureTrade, []string{"BTC_USDT"}, &SubscribeOptions{ 80 | ID: 123456, 81 | }); err != nil { 82 | log.Fatalf("Subscribe err:%s", err.Error()) 83 | return 84 | } 85 | 86 | ch := make(chan bool) 87 | defer close(ch) 88 | 89 | for { 90 | select { 91 | case <-ch: 92 | log.Printf("manual done") 93 | case <-time.After(time.Second * 1000): 94 | log.Printf("auto done") 95 | return 96 | } 97 | } 98 | } 99 | 100 | func TestSubscribeAuthChannel(t *testing.T) { 101 | ws, err := NewWsService(nil, nil, nil) 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | 106 | if err := ws.Subscribe(ChannelSpotOrder, []string{"BCH_USDT"}); err != nil { 107 | log.Fatalf("Subscribe err:%s", err.Error()) 108 | return 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /go/client.go: -------------------------------------------------------------------------------- 1 | package gatews 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "log" 7 | "os" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | mapset "github.com/deckarep/golang-set" 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | type status int 17 | 18 | const ( 19 | disconnected status = iota 20 | connected 21 | reconnecting 22 | ) 23 | 24 | type WsService struct { 25 | mu *sync.Mutex 26 | Logger *log.Logger 27 | Ctx context.Context 28 | Client *websocket.Conn 29 | once *sync.Once 30 | loginOnce *sync.Once 31 | msgChs *sync.Map // business chan 32 | calls *sync.Map 33 | conf *ConnConf 34 | status status 35 | clientMu *sync.Mutex 36 | } 37 | 38 | // ConnConf default URL is spot websocket 39 | type ConnConf struct { 40 | App string 41 | subscribeMsg *sync.Map 42 | URL string 43 | Key string 44 | Secret string 45 | MaxRetryConn int 46 | SkipTlsVerify bool 47 | ShowReconnectMsg bool 48 | PingInterval string 49 | } 50 | 51 | type ConfOptions struct { 52 | App string 53 | URL string 54 | Key string 55 | Secret string 56 | MaxRetryConn int 57 | SkipTlsVerify bool 58 | ShowReconnectMsg bool 59 | PingInterval string 60 | } 61 | 62 | func NewWsService(ctx context.Context, logger *log.Logger, conf *ConnConf) (*WsService, error) { 63 | if logger == nil { 64 | logger = log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile) 65 | } 66 | if ctx == nil { 67 | ctx = context.Background() 68 | } 69 | 70 | defaultConf := getInitConnConf() 71 | if conf != nil { 72 | conf = applyOptionConf(defaultConf, conf) 73 | } else { 74 | conf = defaultConf 75 | } 76 | 77 | stop := false 78 | retry := 0 79 | var conn *websocket.Conn 80 | for !stop { 81 | dialer := websocket.DefaultDialer 82 | if conf.SkipTlsVerify { 83 | dialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 84 | } 85 | c, _, err := dialer.Dial(conf.URL, nil) 86 | if err != nil { 87 | if retry >= conf.MaxRetryConn { 88 | log.Printf("max reconnect time %d reached, give it up", conf.MaxRetryConn) 89 | return nil, err 90 | } 91 | retry++ 92 | log.Printf("failed to connect to server for the %d time, try again later", retry) 93 | time.Sleep(time.Millisecond * (time.Duration(retry) * 500)) 94 | continue 95 | } else { 96 | stop = true 97 | conn = c 98 | } 99 | } 100 | 101 | if retry > 0 { 102 | log.Printf("reconnect succeeded after retrying %d times", retry) 103 | } 104 | 105 | ws := &WsService{ 106 | mu: new(sync.Mutex), 107 | conf: conf, 108 | Logger: logger, 109 | Ctx: ctx, 110 | Client: conn, 111 | calls: new(sync.Map), 112 | msgChs: new(sync.Map), 113 | once: new(sync.Once), 114 | loginOnce: new(sync.Once), 115 | status: connected, 116 | clientMu: new(sync.Mutex), 117 | } 118 | 119 | go ws.activePing() 120 | 121 | return ws, nil 122 | } 123 | 124 | func getInitConnConf() *ConnConf { 125 | return &ConnConf{ 126 | App: "spot", 127 | subscribeMsg: new(sync.Map), 128 | MaxRetryConn: MaxRetryConn, 129 | Key: "", 130 | Secret: "", 131 | URL: BaseUrl, 132 | SkipTlsVerify: false, 133 | ShowReconnectMsg: true, 134 | PingInterval: DefaultPingInterval, 135 | } 136 | } 137 | 138 | func applyOptionConf(defaultConf, userConf *ConnConf) *ConnConf { 139 | if userConf.App == "" { 140 | userConf.App = defaultConf.App 141 | } 142 | 143 | if userConf.URL == "" { 144 | userConf.URL = defaultConf.URL 145 | } 146 | 147 | if userConf.MaxRetryConn == 0 { 148 | userConf.MaxRetryConn = defaultConf.MaxRetryConn 149 | } 150 | 151 | if userConf.PingInterval == "" { 152 | userConf.PingInterval = defaultConf.PingInterval 153 | } 154 | 155 | return userConf 156 | } 157 | 158 | // NewConnConfFromOption conf from options, recommend using this 159 | func NewConnConfFromOption(op *ConfOptions) *ConnConf { 160 | if op.URL == "" { 161 | op.URL = BaseUrl 162 | } 163 | if op.MaxRetryConn == 0 { 164 | op.MaxRetryConn = MaxRetryConn 165 | } 166 | return &ConnConf{ 167 | App: op.App, 168 | subscribeMsg: new(sync.Map), 169 | MaxRetryConn: op.MaxRetryConn, 170 | Key: op.Key, 171 | Secret: op.Secret, 172 | URL: op.URL, 173 | SkipTlsVerify: op.SkipTlsVerify, 174 | ShowReconnectMsg: op.ShowReconnectMsg, 175 | PingInterval: op.PingInterval, 176 | } 177 | } 178 | 179 | func (ws *WsService) GetConnConf() *ConnConf { 180 | return ws.conf 181 | } 182 | 183 | func (ws *WsService) reconnect() error { 184 | // avoid repeated reconnection 185 | if ws.status == reconnecting { 186 | return nil 187 | } 188 | 189 | ws.clientMu.Lock() 190 | defer ws.clientMu.Unlock() 191 | 192 | if ws.Client != nil { 193 | ws.Client.Close() 194 | } 195 | 196 | ws.status = reconnecting 197 | 198 | stop := false 199 | retry := 0 200 | for !stop { 201 | c, _, err := websocket.DefaultDialer.Dial(ws.conf.URL, nil) 202 | if err != nil { 203 | if retry >= ws.conf.MaxRetryConn { 204 | ws.Logger.Printf("max reconnect time %d reached, give it up", ws.conf.MaxRetryConn) 205 | return err 206 | } 207 | retry++ 208 | log.Printf("failed to connect to server for the %d time, try again later", retry) 209 | time.Sleep(time.Millisecond * (time.Duration(retry) * 500)) 210 | continue 211 | } else { 212 | stop = true 213 | ws.Client = c 214 | } 215 | } 216 | 217 | ws.status = connected 218 | 219 | // resubscribe after reconnect 220 | ws.conf.subscribeMsg.Range(func(key, value interface{}) bool { 221 | // key is channel, value is []requestHistory 222 | if _, ok := value.([]requestHistory); ok { 223 | for _, req := range value.([]requestHistory) { 224 | if req.op == nil { 225 | req.op = &SubscribeOptions{ 226 | IsReConnect: true, 227 | } 228 | } else { 229 | req.op.IsReConnect = true 230 | } 231 | if err := ws.baseSubscribe(req.Event, req.Channel, req.Payload, req.op); err != nil { 232 | ws.Logger.Printf("after reconnect, subscribe channel[%s] err:%s", key.(string), err.Error()) 233 | } else { 234 | if ws.conf.ShowReconnectMsg { 235 | ws.Logger.Printf("reconnect channel[%s] with payload[%v] success", key.(string), req.Payload) 236 | } 237 | } 238 | } 239 | } 240 | return true 241 | }) 242 | 243 | return nil 244 | } 245 | 246 | func (ws *WsService) SetKey(key string) { 247 | ws.conf.Key = key 248 | } 249 | 250 | func (ws *WsService) GetKey() string { 251 | return ws.conf.Key 252 | } 253 | 254 | func (ws *WsService) SetSecret(secret string) { 255 | ws.conf.Secret = secret 256 | } 257 | 258 | func (ws *WsService) GetSecret() string { 259 | return ws.conf.Secret 260 | } 261 | 262 | func (ws *WsService) SetMaxRetryConn(max int) { 263 | ws.conf.MaxRetryConn = max 264 | } 265 | 266 | func (ws *WsService) GetMaxRetryConn() int { 267 | return ws.conf.MaxRetryConn 268 | } 269 | 270 | func (ws *WsService) GetChannelMarkets(channel string) []string { 271 | var markets []string 272 | set := mapset.NewSet() 273 | if v, ok := ws.conf.subscribeMsg.Load(channel); ok { 274 | for _, req := range v.([]requestHistory) { 275 | payloads, ok := req.Payload.([]string) 276 | if !ok { 277 | continue 278 | } 279 | 280 | if req.Event == Subscribe { 281 | for _, pl := range payloads { 282 | if strings.Contains(pl, "_") { 283 | set.Add(pl) 284 | } 285 | } 286 | } else { 287 | for _, pl := range payloads { 288 | if strings.Contains(pl, "_") { 289 | set.Remove(pl) 290 | } 291 | } 292 | } 293 | } 294 | 295 | for _, v := range set.ToSlice() { 296 | markets = append(markets, v.(string)) 297 | } 298 | return markets 299 | } 300 | return markets 301 | } 302 | 303 | func (ws *WsService) GetChannels() []string { 304 | var channels []string 305 | ws.calls.Range(func(key, value interface{}) bool { 306 | channels = append(channels, key.(string)) 307 | return true 308 | }) 309 | return channels 310 | } 311 | 312 | func (ws *WsService) GetConnection() *websocket.Conn { 313 | return ws.Client 314 | } 315 | 316 | func (ws *WsService) activePing() { 317 | du, err := time.ParseDuration(ws.conf.PingInterval) 318 | if err != nil { 319 | ws.Logger.Printf("failed to parse ping interval: %s, use default ping interval 10s instead", ws.conf.PingInterval) 320 | du, err = time.ParseDuration(DefaultPingInterval) 321 | if err != nil { 322 | du = time.Second * 10 323 | } 324 | } 325 | 326 | ticker := time.NewTicker(du) 327 | defer ticker.Stop() 328 | 329 | for { 330 | select { 331 | case <-ws.Ctx.Done(): 332 | return 333 | case <-ticker.C: 334 | subscribeMap := map[string]int{} 335 | ws.conf.subscribeMsg.Range(func(key, value interface{}) bool { 336 | splits := strings.Split(key.(string), ".") 337 | if len(splits) == 2 { 338 | subscribeMap[splits[0]] = 1 339 | } 340 | return true 341 | }) 342 | 343 | if ws.status != connected { 344 | continue 345 | } 346 | 347 | for app := range subscribeMap { 348 | channel := app + ".ping" 349 | if err := ws.Subscribe(channel, nil); err != nil { 350 | ws.Logger.Printf("subscribe channel[%s] failed: %v", channel, err) 351 | } 352 | } 353 | } 354 | } 355 | } 356 | 357 | var statusString = map[status]string{ 358 | disconnected: "disconnected", 359 | connected: "connected", 360 | reconnecting: "reconnecting", 361 | } 362 | 363 | func (ws *WsService) Status() string { 364 | return statusString[ws.status] 365 | } 366 | -------------------------------------------------------------------------------- /go/client_test.go: -------------------------------------------------------------------------------- 1 | package gatews 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "testing" 10 | ) 11 | 12 | func TestGetChannelMarkets(t *testing.T) { 13 | ws, err := NewWsService(nil, nil, nil) 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | 18 | if err := ws.Subscribe(ChannelSpotPublicTrade, []string{"BCH_USDT"}); err != nil { 19 | log.Fatalf("Subscribe err:%s", err.Error()) 20 | return 21 | } 22 | if err := ws.Subscribe(ChannelSpotPublicTrade, []string{"BTC_USDT"}); err != nil { 23 | log.Fatalf("Subscribe err:%s", err.Error()) 24 | return 25 | } 26 | if err := ws.Subscribe(ChannelSpotOrderBookUpdate, []string{"BTC_USDT", "100ms"}); err != nil { 27 | log.Fatalf("Subscribe err:%s", err.Error()) 28 | return 29 | } 30 | if err := ws.Subscribe(ChannelSpotOrderBookUpdate, []string{"ETH_USDT", "100ms"}); err != nil { 31 | log.Fatalf("Subscribe err:%s", err.Error()) 32 | return 33 | } 34 | fmt.Println(ChannelSpotPublicTrade, " subscribed markets: ", ws.GetChannelMarkets(ChannelSpotPublicTrade)) 35 | fmt.Println(ChannelSpotOrderBookUpdate, " subscribed markets: ", ws.GetChannelMarkets(ChannelSpotOrderBookUpdate)) 36 | 37 | if err := ws.UnSubscribe(ChannelSpotPublicTrade, []string{"BTC_USDT"}); err != nil { 38 | log.Fatalf("Subscribe err:%s", err.Error()) 39 | return 40 | } 41 | if err := ws.UnSubscribe(ChannelSpotOrderBookUpdate, []string{"BTC_USDT", "100ms"}); err != nil { 42 | log.Fatalf("Subscribe err:%s", err.Error()) 43 | return 44 | } 45 | fmt.Println("after unsubscribe") 46 | fmt.Println(ChannelSpotPublicTrade, " subscribed markets: ", ws.GetChannelMarkets(ChannelSpotPublicTrade)) 47 | fmt.Println(ChannelSpotOrderBookUpdate, " subscribed markets: ", ws.GetChannelMarkets(ChannelSpotOrderBookUpdate)) 48 | } 49 | 50 | func TestGetChannels(t *testing.T) { 51 | ws, err := NewWsService(nil, nil, nil) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | call := NewCallBack(func(msg *UpdateMsg) {}) 57 | ws.SetCallBack(ChannelSpotPublicTrade, call) 58 | if err := ws.Subscribe(ChannelSpotPublicTrade, []string{"BCH_USDT"}); err != nil { 59 | log.Fatalf("Subscribe err:%s", err.Error()) 60 | return 61 | } 62 | if err := ws.Subscribe(ChannelSpotCandleStick, []string{"BTC_USDT", "10ms"}); err != nil { 63 | log.Fatalf("Subscribe err:%s", err.Error()) 64 | return 65 | } 66 | 67 | fmt.Println(ws.GetChannels()) 68 | } 69 | 70 | func TestGetConf(t *testing.T) { 71 | ws, err := NewWsService(nil, nil, NewConnConfFromOption(&ConfOptions{ 72 | URL: "", Key: "KEY", Secret: "SECRET", MaxRetryConn: 10, 73 | })) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | 78 | call := NewCallBack(func(msg *UpdateMsg) {}) 79 | ws.SetCallBack(ChannelSpotPublicTrade, call) 80 | if err := ws.Subscribe(ChannelSpotPublicTrade, []string{"BCH_USDT"}); err != nil { 81 | log.Fatalf("Subscribe err:%s", err.Error()) 82 | return 83 | } 84 | 85 | fmt.Println(ws.GetKey()) 86 | fmt.Println(ws.GetSecret()) 87 | fmt.Println(ws.GetMaxRetryConn()) 88 | } 89 | 90 | func TestGetConfFromOption(t *testing.T) { 91 | ws, err := NewWsService(nil, nil, NewConnConfFromOption(&ConfOptions{ 92 | URL: "", Key: "KEY", Secret: "SECRET", MaxRetryConn: 10, 93 | })) 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | 98 | call := NewCallBack(func(msg *UpdateMsg) {}) 99 | ws.SetCallBack(ChannelSpotPublicTrade, call) 100 | if err := ws.Subscribe(ChannelSpotPublicTrade, []string{"BCH_USDT"}); err != nil { 101 | log.Fatalf("Subscribe err:%s", err.Error()) 102 | return 103 | } 104 | fmt.Println(ws.GetKey()) 105 | fmt.Println(ws.GetSecret()) 106 | fmt.Println(ws.GetMaxRetryConn()) 107 | } 108 | 109 | func TestMultiClients(t *testing.T) { 110 | for i := 0; i < 100; i++ { 111 | go connWs() 112 | } 113 | 114 | ch := make(chan os.Signal) 115 | signal.Ignore(syscall.SIGPIPE, syscall.SIGALRM) 116 | signal.Notify(ch, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGABRT, syscall.SIGKILL) 117 | <-ch 118 | } 119 | 120 | func connWs() { 121 | ws, err := NewWsService(nil, nil, NewConnConfFromOption(&ConfOptions{ 122 | URL: "", MaxRetryConn: 10, ShowReconnectMsg: true, 123 | })) 124 | if err != nil { 125 | log.Fatal(err) 126 | } 127 | 128 | call := NewCallBack(func(msg *UpdateMsg) { 129 | // fmt.Println(string(msg.Result)) 130 | }) 131 | ws.SetCallBack(ChannelSpotBookTicker, call) 132 | if err := ws.Subscribe(ChannelSpotBookTicker, []string{"BTC_USDT"}); err != nil { 133 | log.Fatalf("Subscribe err:%s", err.Error()) 134 | return 135 | } 136 | 137 | ch := make(chan os.Signal) 138 | signal.Ignore(syscall.SIGPIPE, syscall.SIGALRM) 139 | signal.Notify(ch, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGABRT, syscall.SIGKILL) 140 | <-ch 141 | } 142 | -------------------------------------------------------------------------------- /go/constant.go: -------------------------------------------------------------------------------- 1 | package gatews 2 | 3 | import "math" 4 | 5 | const ( 6 | BaseUrl = "wss://api.gateio.ws/ws/v4/" 7 | FuturesBtcUrl = "wss://fx-ws.gateio.ws/v4/ws/btc" 8 | FuturesUsdtUrl = "wss://fx-ws.gateio.ws/v4/ws/usdt" 9 | 10 | AuthMethodApiKey = "api_key" 11 | MaxRetryConn = math.MaxInt64 12 | ) 13 | 14 | // spot channels 15 | const ( 16 | ChannelSpotBalance = "spot.balances" 17 | ChannelSpotCandleStick = "spot.candlesticks" 18 | ChannelSpotOrder = "spot.orders" 19 | ChannelSpotOrderBook = "spot.order_book" 20 | ChannelSpotBookTicker = "spot.book_ticker" 21 | ChannelSpotOrderBookUpdate = "spot.order_book_update" 22 | ChannelSpotTicker = "spot.tickers" 23 | ChannelSpotUserTrade = "spot.usertrades" 24 | ChannelSpotPublicTrade = "spot.trades" 25 | ChannelSpotFundingBalance = "spot.funding_balances" 26 | ChannelSpotMarginBalance = "spot.margin_balances" 27 | ChannelSpotCrossBalance = "spot.cross_balances" 28 | 29 | // order 30 | ChannelSpotLogin = "spot.login" 31 | ChannelSpotOrderAmend = "spot.order_amend" 32 | ChannelSpotOrderCancel = "spot.order_cancel" 33 | ChannelSpotOrderCancelCp = "spot.order_cancel_cp" 34 | ChannelSpotOrderCancelIds = "spot.order_cancel_ids" 35 | ChannelSpotOrderPlace = "spot.order_place" 36 | ChannelSpotOrderStatus = "spot.order_status" 37 | ) 38 | 39 | // future channels 40 | const ( 41 | ChannelFutureTicker = "futures.tickers" 42 | ChannelFutureTrade = "futures.trades" 43 | ChannelFutureOrderBook = "futures.order_book" 44 | ChannelFutureBookTicker = "futures.book_ticker" 45 | ChannelFutureOrderBookUpdate = "futures.order_book_update" 46 | ChannelFutureCandleStick = "futures.candlesticks" 47 | ChannelFutureOrder = "futures.orders" 48 | ChannelFutureUserTrade = "futures.usertrades" 49 | ChannelFutureLiquidates = "futures.liquidates" 50 | ChannelFutureAutoDeleverages = "futures.auto_deleverages" 51 | ChannelFuturePositionCloses = "futures.position_closes" 52 | ChannelFutureBalance = "futures.balances" 53 | ChannelFutureReduceRiskLimits = "futures.reduce_risk_limits" 54 | ChannelFuturePositions = "futures.positions" 55 | ChannelFutureAutoOrders = "futures.autoorders" 56 | 57 | // order 58 | ChannelFutureLogin = "futures.login" 59 | ChannelFutureOrderAmend = "futures.order_amend" 60 | ChannelFutureOrderCancel = "futures.order_cancel" 61 | ChannelFutureOrderCancelCp = "futures.order_cancel_cp" 62 | ChannelFutureOrderPlace = "futures.order_place" 63 | ChannelFutureOrderBatchPlace = "futures.order_batch_place" 64 | ChannelFutureOrderStatus = "futures.order_status" 65 | ChannelFutureOrderList = "futures.order_list" 66 | ) 67 | 68 | var authChannel = map[string]bool{ 69 | // spot 70 | ChannelSpotBalance: true, 71 | ChannelSpotFundingBalance: true, 72 | ChannelSpotMarginBalance: true, 73 | ChannelSpotOrder: true, 74 | ChannelSpotUserTrade: true, 75 | 76 | // future 77 | ChannelFutureOrder: true, 78 | ChannelFutureUserTrade: true, 79 | ChannelFutureLiquidates: true, 80 | ChannelFutureAutoDeleverages: true, 81 | ChannelFuturePositionCloses: true, 82 | ChannelFutureReduceRiskLimits: true, 83 | ChannelFuturePositions: true, 84 | ChannelFutureAutoOrders: true, 85 | ChannelFutureBalance: true, 86 | } 87 | 88 | const ( 89 | Subscribe = "subscribe" 90 | UnSubscribe = "unsubscribe" 91 | API = "api" 92 | 93 | ServiceTypeSpot = 1 94 | ServiceTypeFutures = 2 95 | 96 | DefaultPingInterval = "10s" 97 | ) 98 | -------------------------------------------------------------------------------- /go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gateio/gatews/go 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/deckarep/golang-set v1.7.1 7 | github.com/gorilla/websocket v1.4.2 8 | ) 9 | 10 | // just for test and examples 11 | //require ( 12 | // github.com/gansidui/skiplist v0.0.0-20141121051332-c6a909ce563b // indirect 13 | // github.com/shopspring/decimal v1.3.1 // indirect 14 | // github.com/yireyun/go-queue v0.0.0-20220725040158-a4dd64810e1e // indirect 15 | //) 16 | -------------------------------------------------------------------------------- /go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= 2 | github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= 3 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 4 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | -------------------------------------------------------------------------------- /go/go.work: -------------------------------------------------------------------------------- 1 | go 1.18.0 2 | 3 | use ./_examples 4 | -------------------------------------------------------------------------------- /go/model.go: -------------------------------------------------------------------------------- 1 | package gatews 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type UpdateMsg struct { 9 | Header ResponseHeader `json:"header"` 10 | Time int64 `json:"time"` 11 | TimeMs int64 `json:"time_ms"` 12 | Id *int64 `json:"id,omitempty"` 13 | Channel string `json:"channel"` 14 | Event string `json:"event"` 15 | Error *ServiceError `json:"error,omitempty"` 16 | Result json.RawMessage `json:"result"` 17 | Data struct { 18 | Result json.RawMessage `json:"result"` 19 | Errs *struct { 20 | Label string `json:"label"` 21 | Message string `json:"message"` 22 | } `json:"errs"` 23 | } `json:"data"` 24 | } 25 | 26 | type ResponseHeader struct { 27 | ResponseTime string `json:"response_time"` 28 | Status string `json:"status"` 29 | Channel string `json:"channel"` 30 | Event string `json:"event"` 31 | ClientID string `json:"client_id"` 32 | } 33 | 34 | func (u *UpdateMsg) GetChannel() string { 35 | if u.Channel != "" { 36 | return u.Channel 37 | } 38 | 39 | return u.Header.Channel 40 | } 41 | 42 | type ServiceError struct { 43 | Code int `json:"code"` 44 | Message string `json:"message"` 45 | } 46 | 47 | func (e ServiceError) Error() string { 48 | return e.Message 49 | } 50 | 51 | func newAuthEmptyErr() error { 52 | return fmt.Errorf("auth key or secret empty") 53 | } 54 | 55 | type WSEvent struct { 56 | UpdateMsg 57 | } 58 | 59 | type ChannelEvent struct { 60 | Event string 61 | Market []string 62 | } 63 | 64 | type WebsocketRequest struct { 65 | Market []string 66 | } 67 | 68 | type Request struct { 69 | App string `json:"app,omitempty"` 70 | Time int64 `json:"time"` 71 | Id *int64 `json:"id,omitempty"` 72 | Channel string `json:"channel"` 73 | Event string `json:"event"` 74 | Auth Auth `json:"auth"` 75 | Payload any `json:"payload"` 76 | } 77 | 78 | type Auth struct { 79 | Method string `json:"method"` 80 | Key string `json:"KEY"` 81 | Secret string `json:"SIGN"` 82 | } 83 | 84 | type requestHistory struct { 85 | Channel string `json:"channel"` 86 | Event string `json:"event"` 87 | Payload any `json:"payload"` 88 | op *SubscribeOptions 89 | } 90 | 91 | type APIReq struct { 92 | ApiKey string `json:"api_key"` 93 | Signature string `json:"signature"` 94 | Timestamp string `json:"timestamp"` 95 | ReqId string `json:"req_id"` 96 | ReqHeader json.RawMessage `json:"req_header"` 97 | ReqParam json.RawMessage `json:"req_param"` 98 | } 99 | 100 | type APIResp struct { 101 | ClientID string `json:"client_id"` 102 | ReqID string `json:"req_id"` 103 | RespTimeMs int64 `json:"resp_time_ms"` 104 | Status int `json:"status"` 105 | ReqHeader struct { 106 | XGateChannelID string `json:"x-gate-channel-id"` 107 | } `json:"req_header"` 108 | Data struct { 109 | Error any `json:"error"` 110 | Result any `json:"result"` 111 | } `json:"data"` 112 | } 113 | -------------------------------------------------------------------------------- /go/model/future.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ListFuturesOrders struct { 4 | Contract string `json:"contract,omitempty"` 5 | Status string `json:"status,omitempty"` 6 | LastId string `json:"last_id,omitempty"` 7 | Settle string `json:"settle,omitempty"` 8 | Limit int32 `json:"limit,omitempty"` 9 | Offset int32 `json:"offset,omitempty"` 10 | } 11 | 12 | type CancelFuturesOrder struct { 13 | OrderId string `json:"order_id,omitempty"` 14 | Settle string `json:"settle,omitempty"` 15 | } 16 | 17 | type CancelFuturesCpOrder struct { 18 | Contract string `json:"contract,omitempty"` 19 | Side string `json:"side,omitempty"` 20 | Settle string `json:"settle,omitempty"` 21 | } 22 | 23 | type StatusFuturesOrder struct { 24 | OrderId string `json:"order_id,omitempty"` 25 | Settle string `json:"settle,omitempty"` 26 | } 27 | 28 | type AmendFuturesOrder struct { 29 | OrderId string `json:"order_id,omitempty"` 30 | Settle string `json:"settle,omitempty"` 31 | Price string `json:"price,omitempty"` 32 | AmendText string `json:"amend_text"` 33 | Size int64 `json:"size,omitempty"` 34 | } 35 | 36 | type CancelFuturesOrderIds struct { 37 | OrderIds []string `json:"order_ids,omitempty"` 38 | Settle string `json:"settle,omitempty"` 39 | } 40 | -------------------------------------------------------------------------------- /go/model/future_order.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Futures order details 4 | type FuturesOrder struct { 5 | // Futures order ID 6 | Id int64 `json:"id,omitempty"` 7 | // User ID 8 | User int32 `json:"user,omitempty"` 9 | // Creation time of order 10 | CreateTime float64 `json:"create_time,omitempty"` 11 | // Order finished time. Not returned if order is open 12 | FinishTime float64 `json:"finish_time,omitempty"` 13 | // 结束方式,包括: - filled: 完全成交 - cancelled: 用户撤销 - liquidated: 强制平仓撤销 - ioc: 未立即完全成交,因为tif设置为ioc - auto_deleveraged: 自动减仓撤销 - reduce_only: 增持仓位撤销,因为设置reduce_only或平仓 - position_closed: 因为仓位平掉了,所以挂单被撤掉 - reduce_out: 只减仓被排除的不容易成交的挂单 - stp: 订单发生自成交限制而被撤销 14 | FinishAs string `json:"finish_as,omitempty"` 15 | // Order status - `open`: waiting to be traded - `finished`: finished 16 | Status string `json:"status,omitempty"` 17 | // Futures contract 18 | Contract string `json:"contract"` 19 | // Order size. Specify positive number to make a bid, and negative number to ask 20 | Size int64 `json:"size"` 21 | // Display size for iceberg order. 0 for non-iceberg. Note that you will have to pay the taker fee for the hidden size 22 | Iceberg int64 `json:"iceberg,omitempty"` 23 | // Order price. 0 for market order with `tif` set as `ioc` 24 | Price string `json:"price,omitempty"` 25 | // Set as `true` to close the position, with `size` set to 0 26 | Close bool `json:"close,omitempty"` 27 | // Is the order to close position 28 | IsClose bool `json:"is_close,omitempty"` 29 | // Set as `true` to be reduce-only order 30 | ReduceOnly bool `json:"reduce_only,omitempty"` 31 | // Is the order reduce-only 32 | IsReduceOnly bool `json:"is_reduce_only,omitempty"` 33 | // Is the order for liquidation 34 | IsLiq bool `json:"is_liq,omitempty"` 35 | // Time in force - gtc: GoodTillCancelled - ioc: ImmediateOrCancelled, taker only - poc: PendingOrCancelled, makes a post-only order that always enjoys a maker fee - fok: FillOrKill, fill either completely or none 36 | Tif string `json:"tif,omitempty"` 37 | // Size left to be traded 38 | Left int64 `json:"left,omitempty"` 39 | // Fill price of the order 40 | FillPrice string `json:"fill_price,omitempty"` 41 | // User defined information. If not empty, must follow the rules below: 1. prefixed with `t-` 2. no longer than 28 bytes without `t-` prefix 3. can only include 0-9, A-Z, a-z, underscore(_), hyphen(-) or dot(.) Besides user defined information, reserved contents are listed below, denoting how the order is created: - web: from web - api: from API - app: from mobile phones - auto_deleveraging: from ADL - liquidation: from liquidation - insurance: from insurance 42 | Text string `json:"text,omitempty"` 43 | // Taker fee 44 | Tkfr string `json:"tkfr,omitempty"` 45 | // Maker fee 46 | Mkfr string `json:"mkfr,omitempty"` 47 | // Reference user ID 48 | Refu int32 `json:"refu,omitempty"` 49 | // Set side to close dual-mode position. `close_long` closes the long side; while `close_short` the short one. Note `size` also needs to be set to 0 50 | AutoSize string `json:"auto_size,omitempty"` 51 | // 订单所属的`STP用户组`id,同一个`STP用户组`内用户之间的订单不允许发生自成交。 1. 如果撮合时两个订单的 `stp_id` 非 `0` 且相等,则不成交,而是根据 `taker` 的 `stp_act` 执行相应策略。 2. 没有设置`STP用户组`成交的订单,`stp_id` 默认返回 `0`。 52 | StpId int32 `json:"stp_id,omitempty"` 53 | // Self-Trading Prevention Action,用户可以用该字段设置自定义限制自成交策略。 1. 用户在设置加入`STP用户组`后,可以通过传递 `stp_act` 来限制用户发生自成交的策略,没有传递 `stp_act` 默认按照 `cn` 的策略。 2. 用户在没有设置加入`STP用户组`时,传递 `stp_act` 参数会报错。 3. 用户没有使用 `stp_act` 发生成交的订单,`stp_act` 返回 `-`。 - cn: Cancel newest,取消新订单,保留老订单 - co: Cancel oldest,取消⽼订单,保留新订单 - cb: Cancel both,新旧订单都取消 54 | StpAct string `json:"stp_act,omitempty"` 55 | // 用户修改订单时备注的信息 56 | AmendText string `json:"amend_text,omitempty"` 57 | // 附加信息 58 | BizInfo string `json:"biz_info,omitempty"` 59 | } 60 | -------------------------------------------------------------------------------- /go/model/spot.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type AmendOrderParam struct { 4 | Amount string `json:"amount,omitempty"` // New order amount. `amount` and `price` must specify one of them 5 | Price string `json:"price,omitempty"` // New order price. `amount` and `Price` must specify one of them" 6 | AmendText string `json:"amend_text,omitempty"` // Custom info during amending order 7 | OrderId string `json:"order_id,omitempty" ` 8 | CurrencyPair string `json:"currency_pair,omitempty" ` 9 | Account string `json:"account,omitempty"` 10 | } 11 | 12 | type CancelOrderParam struct { 13 | OrderId string `json:"order_id,omitempty"` 14 | CurrencyPair string `json:"currency_pair,omitempty"` 15 | Account string `json:"account,omitempty"` 16 | } 17 | 18 | type CancelOrderWithCpParam struct { 19 | CurrencyPair string `json:"currency_pair,omitempty"` 20 | Side string `json:"side,omitempty"` 21 | Account string `json:"account,omitempty"` 22 | } 23 | 24 | type StatusOrderParam struct { 25 | OrderId string `json:"order_id,omitempty" ` 26 | CurrencyPair string `json:"currency_pair,omitempty" ` 27 | Account string `json:"account,omitempty"` 28 | } 29 | -------------------------------------------------------------------------------- /go/model/spot_order.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Order struct { 4 | // Order ID 5 | Id string `json:"id,omitempty"` 6 | // User defined information. If not empty, must follow the rules below: 1. prefixed with `t-` 2. no longer than 28 bytes without `t-` prefix 3. can only include 0-9, A-Z, a-z, underscore(_), hyphen(-) or dot(.) Besides user defined information, reserved contents are listed below, denoting how the order is created: - 101: from android - 102: from IOS - 103: from IPAD - 104: from webapp - 3: from web - 2: from apiv2 - apiv4: from apiv4 7 | Text string `json:"text,omitempty"` 8 | // 用户修改订单时备注的信息 9 | AmendText string `json:"amend_text,omitempty"` 10 | // Creation time of order 11 | CreateTime string `json:"create_time,omitempty"` 12 | // Last modification time of order 13 | UpdateTime string `json:"update_time,omitempty"` 14 | // Creation time of order (in milliseconds) 15 | CreateTimeMs int64 `json:"create_time_ms,omitempty"` 16 | // Last modification time of order (in milliseconds) 17 | UpdateTimeMs int64 `json:"update_time_ms,omitempty"` 18 | // Order status - `open`: to be filled - `closed`: filled - `cancelled`: cancelled 19 | Status string `json:"status,omitempty"` 20 | // Currency pair 21 | CurrencyPair string `json:"currency_pair"` 22 | // Order Type - limit : Limit Order - market : Market Order 23 | Type string `json:"type,omitempty"` 24 | // 账户类型,spot - 现货账户,margin - 杠杆账户,cross_margin - 全仓杠杆账户,unified - 统一账户 统一账户(旧)只能设置 `cross_margin` 25 | Account string `json:"account,omitempty"` 26 | // Order side 27 | Side string `json:"side"` 28 | // When `type` is limit, it refers to base currency. For instance, `BTC_USDT` means `BTC` When `type` is `market`, it refers to different currency according to `side` - `side` : `buy` means quote currency, `BTC_USDT` means `USDT` - `side` : `sell` means base currency,`BTC_USDT` means `BTC` 29 | Amount string `json:"amount"` 30 | // Price can't be empty when `type`= `limit` 31 | Price string `json:"price,omitempty"` 32 | // Time in force - gtc: GoodTillCancelled - ioc: ImmediateOrCancelled, taker only - poc: PendingOrCancelled, makes a post-only order that always enjoys a maker fee - fok: FillOrKill, fill either completely or none Only `ioc` and `fok` are supported when `type`=`market` 33 | TimeInForce string `json:"time_in_force,omitempty"` 34 | // Amount to display for the iceberg order. Null or 0 for normal orders. Hiding all amount is not supported. 35 | Iceberg string `json:"iceberg,omitempty"` 36 | // Used in margin or cross margin trading to allow automatic loan of insufficient amount if balance is not enough. 37 | AutoBorrow bool `json:"auto_borrow,omitempty"` 38 | // Enable or disable automatic repayment for automatic borrow loan generated by cross margin order. Default is disabled. Note that: 1. This field is only effective for cross margin orders. Margin account does not support setting auto repayment for orders. 2. `auto_borrow` and `auto_repay` cannot be both set to true in one order. 39 | AutoRepay bool `json:"auto_repay,omitempty"` 40 | // Amount left to fill 41 | Left string `json:"left,omitempty"` 42 | // Total filled in quote currency. Deprecated in favor of `filled_total` 43 | FillPrice string `json:"fill_price,omitempty"` 44 | // Total filled in quote currency 45 | FilledTotal string `json:"filled_total,omitempty"` 46 | // Average fill price 47 | AvgDealPrice string `json:"avg_deal_price,omitempty"` 48 | // Fee deducted 49 | Fee string `json:"fee,omitempty"` 50 | // Fee currency unit 51 | FeeCurrency string `json:"fee_currency,omitempty"` 52 | // Points used to deduct fee 53 | PointFee string `json:"point_fee,omitempty"` 54 | // GT used to deduct fee 55 | GtFee string `json:"gt_fee,omitempty"` 56 | // GT used to deduct maker fee 57 | GtMakerFee string `json:"gt_maker_fee,omitempty"` 58 | // GT used to deduct taker fee 59 | GtTakerFee string `json:"gt_taker_fee,omitempty"` 60 | // Whether GT fee discount is used 61 | GtDiscount bool `json:"gt_discount,omitempty"` 62 | // Rebated fee 63 | RebatedFee string `json:"rebated_fee,omitempty"` 64 | // Rebated fee currency unit 65 | RebatedFeeCurrency string `json:"rebated_fee_currency,omitempty"` 66 | // 订单所属的`STP用户组`id,同一个`STP用户组`内用户之间的订单不允许发生自成交。 1. 如果撮合时两个订单的 `stp_id` 非 `0` 且相等,则不成交,而是根据 `taker` 的 `stp_act` 执行相应策略。 2. 没有设置`STP用户组`成交的订单,`stp_id` 默认返回 `0`。 67 | StpId int32 `json:"stp_id,omitempty"` 68 | // Self-Trading Prevention Action,用户可以用该字段设置自定义限制自成交策略。 1. 用户在设置加入`STP用户组`后,可以通过传递 `stp_act` 来限制用户发生自成交的策略,没有传递 `stp_act` 默认按照 `cn` 的策略。 2. 用户在没有设置加入`STP用户组`时,传递 `stp_act` 参数会报错。 3. 用户没有使用 `stp_act` 发生成交的订单,`stp_act` 返回 `-`。 - cn: Cancel newest,取消新订单,保留老订单 - co: Cancel oldest,取消⽼订单,保留新订单 - cb: Cancel both,新旧订单都取消 69 | StpAct string `json:"stp_act,omitempty"` 70 | // 订单结束方式,包括: - open: 等待处理 - filled: 完全成交 - cancelled: 用户撤销 - ioc: 未立即完全成交,因为 tif 设置为 ioc - stp: 订单发生自成交限制而被撤销 71 | FinishAs string `json:"finish_as,omitempty"` 72 | // 费率折扣 73 | FeeDiscount string `json:"fee_discount,omitempty"` 74 | // 处理模式: 下单时根据action_mode返回不同的字段, 该字段只在请求时有效,响应结果中不包含该字段 `ACK`: 异步模式,只返回订单关键字段 `RESULT`: 无清算信息 `FULL`: 完整模式(默认) 75 | ActionMode string `json:"action_mode,omitempty"` 76 | } 77 | -------------------------------------------------------------------------------- /go/resp/future_order.go: -------------------------------------------------------------------------------- 1 | package resp 2 | 3 | type FutureOrder struct { 4 | Text string `json:"text,omitempty"` 5 | Price string `json:"price,omitempty"` 6 | BizInfo string `json:"biz_info,omitempty"` 7 | Tif string `json:"tif,omitempty"` 8 | AmendText string `json:"amend_text,omitempty"` 9 | Status string `json:"status,omitempty"` 10 | Contract string `json:"contract"` 11 | StpAct string `json:"stp_act,omitempty"` 12 | FinishAs string `json:"finish_as,omitempty"` 13 | FillPrice string `json:"fill_price,omitempty"` 14 | AutoSize string `json:"auto_size,omitempty"` 15 | Id int64 `json:"id,omitempty"` 16 | CreateTime float64 `json:"create_time,omitempty"` 17 | Iceberg int64 `json:"iceberg,omitempty"` 18 | Size int64 `json:"size"` 19 | FinishTime float64 `json:"finish_time,omitempty"` 20 | Left int64 `json:"left"` 21 | Refu int32 `json:"refu,omitempty"` 22 | User int32 `json:"user,omitempty"` 23 | StpId int32 `json:"stp_id,omitempty"` 24 | IsClose bool `json:"is_close,omitempty"` 25 | Close bool `json:"close,omitempty"` 26 | IsLiq bool `json:"is_liq,omitempty"` 27 | IsReduceOnly bool `json:"is_reduce_only,omitempty"` 28 | ReduceOnly bool `json:"reduce_only,omitempty"` 29 | } 30 | -------------------------------------------------------------------------------- /go/resp/order.go: -------------------------------------------------------------------------------- 1 | package resp 2 | 3 | type SpotOrder struct { 4 | Left string `json:"left,omitempty"` 5 | UpdateTime string `json:"update_time,omitempty"` 6 | Amount string `json:"amount"` 7 | CreateTime string `json:"create_time,omitempty"` 8 | Price string `json:"price,omitempty"` 9 | FinishAs string `json:"finish_as,omitempty"` 10 | StpAct string `json:"stp_act,omitempty"` 11 | TimeInForce string `json:"time_in_force,omitempty"` 12 | CurrencyPair string `json:"currency_pair"` 13 | Type string `json:"type,omitempty"` 14 | Account string `json:"account,omitempty"` 15 | Side string `json:"side"` 16 | AmendText string `json:"amend_text,omitempty"` 17 | Text string `json:"text,omitempty"` 18 | Status string `json:"status,omitempty"` 19 | Iceberg string `json:"iceberg,omitempty"` 20 | AvgDealPrice string `json:"avg_deal_price,omitempty"` 21 | FilledTotal string `json:"filled_total,omitempty"` 22 | Id string `json:"id,omitempty"` 23 | FillPrice string `json:"fill_price,omitempty"` 24 | UpdateTimeMs int64 `json:"update_time_ms,omitempty"` 25 | CreateTimeMs int64 `json:"create_time_ms,omitempty"` 26 | StpId int32 `json:"stp_id,omitempty"` 27 | AutoRepay bool `json:"auto_repay,omitempty"` 28 | AutoBorrow bool `json:"auto_borrow,omitempty"` 29 | Succeeded bool `json:"succeeded"` 30 | } 31 | -------------------------------------------------------------------------------- /go/response_future.go: -------------------------------------------------------------------------------- 1 | package gatews 2 | 3 | type FuturesTicker struct { 4 | // Futures contract 5 | Contract string `json:"contract,omitempty"` 6 | // Last trading price 7 | Last string `json:"last,omitempty"` 8 | // Change percentage. 9 | ChangePercentage string `json:"change_percentage,omitempty"` 10 | // Contract total size 11 | TotalSize string `json:"total_size,omitempty"` 12 | // Trade size in recent 24h 13 | Volume24h string `json:"volume_24h,omitempty"` 14 | // Trade volume in recent 24h, in base currency 15 | Volume24hBase string `json:"volume_24h_base,omitempty"` 16 | // Trade volume in recent 24h, in quote currency 17 | Volume24hQuote string `json:"volume_24h_quote,omitempty"` 18 | // Trade volume in recent 24h, in settle currency 19 | Volume24hSettle string `json:"volume_24h_settle,omitempty"` 20 | Volume24Usd string `json:"volume_24_usd"` 21 | Volume24Btc string `json:"volume_24_btc"` 22 | // Recent mark price 23 | MarkPrice string `json:"mark_price,omitempty"` 24 | // Funding rate 25 | FundingRate string `json:"funding_rate,omitempty"` 26 | // Indicative Funding rate in next period 27 | FundingRateIndicative string `json:"funding_rate_indicative,omitempty"` 28 | // Index price 29 | IndexPrice string `json:"index_price,omitempty"` 30 | // Exchange rate of base currency and settlement currency in Quanto contract. Not existed in contract of other types 31 | QuantoBaseRate string `json:"quanto_base_rate,omitempty"` 32 | Low24h string `json:"low_24h"` 33 | High24h string `json:"high_24h"` 34 | } 35 | 36 | type FuturesTrade struct { 37 | // Trade ID 38 | Id int64 `json:"id,omitempty"` 39 | // Trading time 40 | CreateTime int64 `json:"create_time,omitempty"` 41 | // Trading time, with milliseconds set to 3 decimal places. 42 | CreateTimeMs int64 `json:"create_time_ms,omitempty"` 43 | // Futures contract 44 | Contract string `json:"contract,omitempty"` 45 | // Trading size 46 | Size int64 `json:"size,omitempty"` 47 | // Trading price 48 | Price string `json:"price,omitempty"` 49 | } 50 | 51 | type FuturesOrderBookItem struct { 52 | // Price 53 | P string `json:"p,omitempty"` 54 | // Size 55 | S int64 `json:"s,omitempty"` 56 | } 57 | 58 | type FuturesOrderBook struct { 59 | // Order Book ID. Increase by 1 on every order book change. Set `with_id=true` to include this field in response 60 | Id int64 `json:"id,omitempty"` 61 | Contract string `json:"contract"` 62 | Time int64 `json:"t"` 63 | // Asks order depth 64 | Asks []FuturesOrderBookItem `json:"asks"` 65 | // Bids order depth 66 | Bids []FuturesOrderBookItem `json:"bids"` 67 | } 68 | 69 | type FuturesOrderBookAll struct { 70 | Contract string `json:"c"` 71 | Price string `json:"p"` 72 | Id int64 `json:"id"` 73 | Size int64 `json:"s"` 74 | } 75 | 76 | type FuturesBookTicker struct { 77 | TimeMillis int64 `json:"t"` 78 | Contract string `json:"s"` 79 | UpdateId int64 `json:"u"` 80 | BestBidPrice string `json:"b"` 81 | BestBidSize int64 `json:"B"` 82 | BestAskPrice string `json:"a"` 83 | BestAskSize int64 `json:"A"` 84 | } 85 | 86 | type FuturesOrderBookUpdate struct { 87 | TimeMillis int64 `json:"t"` 88 | Contract string `json:"s"` 89 | FirstId int64 `json:"U"` 90 | LastId int64 `json:"u"` 91 | // Asks order depth 92 | Asks []FuturesOrderBookItem `json:"a"` 93 | // Bids order depth 94 | Bids []FuturesOrderBookItem `json:"b"` 95 | } 96 | 97 | type FuturesCandlestick struct { 98 | // Unix timestamp in seconds 99 | T int64 `json:"t,omitempty"` 100 | // size volume. Only returned if `contract` is not prefixed 101 | V int64 `json:"v,omitempty"` 102 | // Close price 103 | C string `json:"c,omitempty"` 104 | // Highest price 105 | H string `json:"h,omitempty"` 106 | // Lowest price 107 | L string `json:"l,omitempty"` 108 | // Open price 109 | O string `json:"o,omitempty"` 110 | // futures contract name 111 | N string `json:"n"` 112 | Amount string `json:"a"` 113 | } 114 | 115 | type FuturesOrder struct { 116 | // Futures order ID 117 | Id int64 `json:"id,omitempty"` 118 | // User ID 119 | User string `json:"user,omitempty"` 120 | // Order creation time 121 | CreateTime int64 `json:"create_time,omitempty"` 122 | CreateTimeMs int64 `json:"create_time_ms,omitempty"` 123 | // Order finished time. Not returned if order is open 124 | FinishTime int64 `json:"finish_time,omitempty"` 125 | FinishTimeMs int64 `json:"finish_time_ms,omitempty"` 126 | // FinishAs indicates how the order was completed: 127 | // - filled: all filled 128 | // - cancelled: manually cancelled 129 | // - liquidated: cancelled due to liquidation 130 | // - ioc: time in force is IOC, finished immediately 131 | // - auto_deleveraged: finished by ADL 132 | // - reduce_only: cancelled due to increase in position while reduce-only set 133 | // - position_closed: cancelled due to position close 134 | // - stp: cancelled due to self trade prevention 135 | // - _new: order created 136 | // - _update: order filled, partially filled, or updated 137 | // - reduce_out: only reduce position, excluding pending orders hard to execute 138 | FinishAs string `json:"finish_as,omitempty"` 139 | // Futures contract 140 | Contract string `json:"contract"` 141 | // Order size. Specify positive number to make a bid, and negative number to ask 142 | Size int64 `json:"size"` 143 | // Display size for iceberg order. 0 for non-iceberg. Note that you would pay the taker fee for the hidden size 144 | Iceberg int64 `json:"iceberg,omitempty"` 145 | // Order price. 0 for market order with `tif` set as `ioc` 146 | Price float64 `json:"price,omitempty"` 147 | // Is the order to close position 148 | IsClose bool `json:"is_close,omitempty"` 149 | // Is the order reduce-only 150 | IsReduceOnly bool `json:"is_reduce_only,omitempty"` 151 | // Is the order for liquidation 152 | IsLiq bool `json:"is_liq,omitempty"` 153 | // Time in force - gtc: GoodTillCancelled - ioc: ImmediateOrCancelled, taker only - poc: PendingOrCancelled, reduce-only 154 | Tif string `json:"tif,omitempty"` 155 | // Size left to be traded 156 | Left int64 `json:"left,omitempty"` 157 | // Fill price of the order 158 | FillPrice float64 `json:"fill_price,omitempty"` 159 | // User defined information. If not empty, must follow the rules below: 1. prefixed with `t-` 2. no longer than 28 bytes without `t-` prefix 3. can only include 0-9, A-Z, a-z, underscore(_), hyphen(-) or dot(.) Besides user defined information, reserved contents are listed below, denoting how the order is created: - web: from web - api: from API - app: from mobile phones - auto_deleveraging: from ADL - liquidation: from liquidation - insurance: from insurance 160 | Text string `json:"text,omitempty"` 161 | // Taker fee 162 | Tkfr float64 `json:"tkfr,omitempty"` 163 | // Maker fee 164 | Mkfr float64 `json:"mkfr,omitempty"` 165 | // Reference user ID 166 | Refu int32 `json:"refu,omitempty"` 167 | Refr float64 `json:"refr"` 168 | 169 | StopProfitPrice string `json:"stop_profit_price"` 170 | StopLossPrice string `json:"stop_loss_price"` 171 | // StpId represents the ID associated with the self-trade prevention mechanism. 172 | StpId int64 `json:"stp_id,omitempty"` 173 | // StpAct represents the self-trade prevention (STP) action: 174 | // - cn: Cancel newest (keep old orders) 175 | // - co: Cancel oldest (keep new orders) 176 | // - cb: Cancel both (cancel both old and new orders) 177 | // If not provided, defaults to 'cn'. Requires STP group membership; otherwise, an error is returned. 178 | StpAct string `json:"stp_act,omitempty"` 179 | // BizInfo represents business-specific information related to the order. The exact content and format can vary depending on the use case. 180 | BizInfo string `json:"biz_info,omitempty"` 181 | // AmendText provides the custom data that the user remarked when amending the order 182 | AmendText string `json:"amend_text,omitempty"` 183 | } 184 | 185 | type FuturesUserTrade struct { 186 | Contract string `json:"contract"` 187 | // Trading time 188 | CreateTime int64 `json:"create_time,omitempty"` 189 | // Trading time, with milliseconds set to 3 decimal places. 190 | CreateTimeMs int64 `json:"create_time_ms,omitempty"` 191 | Id string `json:"id"` 192 | OrderId string `json:"order_id"` 193 | Price string `json:"price"` 194 | Size int64 `json:"size"` 195 | Role string `json:"role"` 196 | Text string `json:"text"` 197 | Fee float64 `json:"fee"` 198 | PointFee float64 `json:"point_fee"` 199 | } 200 | 201 | type FuturesLiquidate struct { 202 | // Liquidation time 203 | Time int64 `json:"time,omitempty"` 204 | // time in milliseconds 205 | TimeMs int64 `json:"time_ms"` 206 | // Futures contract 207 | Contract string `json:"contract,omitempty"` 208 | // Position leverage. Not returned in public endpoints. 209 | Leverage float64 `json:"leverage,omitempty"` 210 | // Position size 211 | Size int64 `json:"size,omitempty"` 212 | // Position margin. Not returned in public endpoints. 213 | Margin float64 `json:"margin,omitempty"` 214 | // Average entry price. Not returned in public endpoints. 215 | EntryPrice float64 `json:"entry_price,omitempty"` 216 | // Liquidation price. Not returned in public endpoints. 217 | LiqPrice float64 `json:"liq_price,omitempty"` 218 | // Mark price. Not returned in public endpoints. 219 | MarkPrice float64 `json:"mark_price,omitempty"` 220 | // Liquidation order ID. Not returned in public endpoints. 221 | OrderId int64 `json:"order_id,omitempty"` 222 | // Liquidation order price 223 | OrderPrice float64 `json:"order_price,omitempty"` 224 | // Liquidation order average taker price 225 | FillPrice float64 `json:"fill_price,omitempty"` 226 | // Liquidation order maker size 227 | Left int64 `json:"left,omitempty"` 228 | // user id 229 | User string `json:"user"` 230 | } 231 | 232 | type FuturesAutoDeleverages struct { 233 | EntryPrice float64 `json:"entry_price"` 234 | FillPrice float64 `json:"fill_price"` 235 | PositionSize int64 `json:"position_size"` 236 | TradeSize int64 `json:"trade_size"` 237 | Time int64 `json:"time"` 238 | TimeMs int64 `json:"time_ms"` 239 | Contract string `json:"contract"` 240 | User string `json:"user"` 241 | } 242 | 243 | type FuturesPositionCloses struct { 244 | Contract string `json:"contract"` 245 | Pnl float64 `json:"pnl"` 246 | Side string `json:"side"` 247 | Text string `json:"text"` 248 | Time int64 `json:"time"` 249 | TimeMs int64 `json:"time_ms"` 250 | User string `json:"user"` 251 | } 252 | 253 | type FuturesBalance struct { 254 | Balance float64 `json:"balance"` 255 | Change float64 `json:"change"` 256 | Text string `json:"text"` 257 | Time int64 `json:"time"` 258 | TimeMs int64 `json:"time_ms"` 259 | User string `json:"user"` 260 | Type string `json:"type"` 261 | Currency string `json:"currency"` 262 | } 263 | 264 | type FuturesReduceRiskLimits struct { 265 | CancelOrders int64 `json:"cancel_orders"` 266 | Contract string `json:"contract"` 267 | LeverageMax float64 `json:"leverage_max"` 268 | LiqPrice float64 `json:"liq_price"` 269 | MaintenanceRate float64 `json:"maintenance_rate"` 270 | RiskLimit float64 `json:"risk_limit"` 271 | Time int64 `json:"time"` 272 | TimeMs int64 `json:"time_ms"` 273 | User string `json:"user"` 274 | } 275 | 276 | type FuturesPositions struct { 277 | Contract string `json:"contract"` 278 | CrossLeverageLimit float64 `json:"cross_leverage_limit"` 279 | EntryPrice float64 `json:"entry_price"` 280 | HistoryPnl float64 `json:"history_pnl"` 281 | HistoryPoint float64 `json:"history_point"` 282 | LastClosePnl float64 `json:"last_close_pnl"` 283 | Leverage float64 `json:"leverage"` 284 | LeverageMax float64 `json:"leverage_max"` 285 | LiqPrice float64 `json:"liq_price"` 286 | MaintenanceRate float64 `json:"maintenance_rate"` 287 | Margin float64 `json:"margin"` 288 | Mode string `json:"mode"` 289 | RealisedPnl float64 `json:"realised_pnl"` 290 | RealisedPoint float64 `json:"realised_point"` 291 | RiskLimit float64 `json:"risk_limit"` 292 | Size int64 `json:"size"` 293 | Time int64 `json:"time"` 294 | TimeMs int64 `json:"time_ms"` 295 | User string `json:"user"` 296 | } 297 | 298 | type FuturesAutoOrder struct { 299 | Initial FuturesInitialOrder `json:"initial"` 300 | Trigger FuturesPriceTrigger `json:"trigger"` 301 | StopTrigger FutureStopTrigger `json:"stop_trigger"` 302 | // Auto order ID 303 | Id int64 `json:"id,omitempty"` 304 | // User ID 305 | User int64 `json:"user,omitempty"` 306 | // Creation time 307 | CreateTime int64 `json:"create_time,omitempty"` 308 | // Finished time 309 | FinishTime int64 `json:"finish_time,omitempty"` 310 | // ID of the newly created order on condition triggered 311 | TradeId int64 `json:"trade_id,omitempty"` 312 | // Order status. 313 | Status string `json:"status,omitempty"` 314 | // Extra messages of how order is finished 315 | Reason string `json:"reason,omitempty"` 316 | Name string `json:"name"` 317 | IsStopOrder bool `json:"is_stop_order"` 318 | FinishAs string `json:"finish_as"` 319 | MeOrderId int64 `json:"me_order_id"` 320 | OrderType string `json:"order_type"` 321 | } 322 | 323 | type FutureStopTrigger struct { 324 | Rule int32 `json:"rule"` 325 | TriggerPrice string `json:"trigger_price"` 326 | OrderPrice string `json:"order_price"` 327 | } 328 | 329 | type FuturesPriceTrigger struct { 330 | // How the order will be triggered - `0`: by price, which means order will be triggered on price condition satisfied - `1`: by price gap, which means order will be triggered on gap of recent two prices of specified `price_type` satisfied. Only `0` is supported currently 331 | StrategyType int32 `json:"strategy_type,omitempty"` 332 | // Price type. 0 - latest deal price, 1 - mark price, 2 - index price 333 | PriceType int32 `json:"price_type,omitempty"` 334 | // Value of price on price triggered, or price gap on price gap triggered 335 | Price string `json:"price,omitempty"` 336 | // Trigger condition type - `1`: calculated price based on `strategy_type` and `price_type` >= `price` - `2`: calculated price based on `strategy_type` and `price_type` <= `price` 337 | Rule int32 `json:"rule,omitempty"` 338 | // How many seconds will the order wait for the condition being triggered. Order will be cancelled on timed out 339 | Expiration int32 `json:"expiration,omitempty"` 340 | } 341 | 342 | type FuturesInitialOrder struct { 343 | // Futures contract 344 | Contract string `json:"contract"` 345 | // Order size. Positive size means to buy, while negative one means to sell. Set to 0 to close the position 346 | Size int64 `json:"size,omitempty"` 347 | // Order price. Set to 0 to use market price 348 | Price string `json:"price"` 349 | // Time in force. If using market price, only `ioc` is supported. - gtc: GoodTillCancelled - ioc: ImmediateOrCancelled 350 | Tif string `json:"tif,omitempty"` 351 | // How the order is created. Possible values are: web, api and app 352 | Text string `json:"text,omitempty"` 353 | Iceberg int64 `json:"iceberg"` 354 | // Is the order reduce-only 355 | IsReduceOnly bool `json:"is_reduce_only,omitempty"` 356 | // Is the order to close position 357 | IsClose bool `json:"is_close,omitempty"` 358 | AutoSize string `json:"auto_size"` 359 | } 360 | -------------------------------------------------------------------------------- /go/response_spot.go: -------------------------------------------------------------------------------- 1 | package gatews 2 | 3 | type SpotBalancesMsg struct { 4 | Timestamp string `json:"timestamp"` 5 | TimestampInMilli string `json:"timestamp_ms"` 6 | User string `json:"user"` 7 | Asset string `json:"currency"` 8 | Change string `json:"change"` 9 | Total string `json:"total"` 10 | Available string `json:"available"` 11 | Freeze string `json:"freeze"` 12 | FreezeChange string `json:"freeze_change"` 13 | ChangeType string `json:"change_type"` 14 | } 15 | 16 | type SpotCandleUpdateMsg struct { 17 | Time string `json:"t"` 18 | Volume string `json:"v"` 19 | Close string `json:"c"` 20 | High string `json:"h"` 21 | Low string `json:"l"` 22 | Open string `json:"o"` 23 | Name string `json:"n"` 24 | Amount string `json:"a"` 25 | WindowClose bool `json:"w"` 26 | } 27 | 28 | // SpotUpdateDepthMsg update order book 29 | type SpotUpdateDepthMsg struct { 30 | TimeInMilli int64 `json:"t"` 31 | Event string `json:"e"` 32 | ETime int64 `json:"E"` 33 | CurrencyPair string `json:"s"` 34 | FirstId int64 `json:"U"` 35 | LastId int64 `json:"u"` 36 | Bid [][]string `json:"b"` 37 | Ask [][]string `json:"a"` 38 | } 39 | 40 | // SpotUpdateAllDepthMsg all order book 41 | type SpotUpdateAllDepthMsg struct { 42 | TimeInMilli int64 `json:"t"` 43 | LastUpdateId int64 `json:"lastUpdateId"` 44 | CurrencyPair string `json:"s"` 45 | Bid [][2]string `json:"bids"` 46 | Ask [][2]string `json:"asks"` 47 | } 48 | 49 | type SpotFundingBalancesMsg struct { 50 | Timestamp string `json:"timestamp"` 51 | TimestampInMilli string `json:"timestamp_ms"` 52 | User string `json:"user"` 53 | Asset string `json:"currency"` 54 | Change string `json:"change"` 55 | Freeze string `json:"freeze"` 56 | Lent string `json:"lent"` 57 | } 58 | 59 | type SpotMarginBalancesMsg struct { 60 | Timestamp string `json:"timestamp"` 61 | TimestampInMilli string `json:"timestamp_ms"` 62 | User string `json:"user"` 63 | Market string `json:"currency_pair"` 64 | Asset string `json:"currency"` 65 | Change string `json:"change"` 66 | Available string `json:"available"` 67 | Freeze string `json:"freeze"` 68 | Borrowed string `json:"borrowed"` 69 | Interest string `json:"interest"` 70 | } 71 | 72 | type SpotBookTickerMsg struct { 73 | TimeInMilli int64 `json:"t"` 74 | LastId int64 `json:"u"` 75 | CurrencyPair string `json:"s"` 76 | Bid string `json:"b"` 77 | BidSize string `json:"B"` 78 | Ask string `json:"a"` 79 | AskSize string `json:"A"` 80 | } 81 | 82 | type SpotTickerMsg struct { 83 | // Currency pair 84 | CurrencyPair string `json:"currency_pair,omitempty"` 85 | // Last trading price 86 | Last string `json:"last,omitempty"` 87 | // Lowest ask 88 | LowestAsk string `json:"lowest_ask,omitempty"` 89 | // Highest bid 90 | HighestBid string `json:"highest_bid,omitempty"` 91 | // Change percentage. 92 | ChangePercentage string `json:"change_percentage,omitempty"` 93 | // Base currency trade volume 94 | BaseVolume string `json:"base_volume,omitempty"` 95 | // Quote currency trade volume 96 | QuoteVolume string `json:"quote_volume,omitempty"` 97 | // Highest price in 24h 98 | High24h string `json:"high_24h,omitempty"` 99 | // Lowest price in 24h 100 | Low24h string `json:"low_24h,omitempty"` 101 | } 102 | 103 | type SpotUserTradesMsg struct { 104 | Id uint64 `json:"id"` 105 | UserId uint64 `json:"user_id"` 106 | OrderId string `json:"order_id"` 107 | CurrencyPair string `json:"currency_pair"` 108 | CreateTime int64 `json:"create_time"` 109 | CreateTimeMs string `json:"create_time_ms"` 110 | Side string `json:"side"` 111 | Amount string `json:"amount"` 112 | Role string `json:"role"` 113 | Price string `json:"price"` 114 | Fee string `json:"fee"` 115 | FeeCurrency string `json:"fee_currency"` 116 | PointFee string `json:"point_fee"` 117 | GtFee string `json:"gt_fee"` 118 | Text string `json:"text"` 119 | AmendText string `json:"amend_text"` 120 | BizInfo string `json:"biz_info"` 121 | } 122 | 123 | type SpotTradeMsg struct { 124 | Id uint64 `json:"id"` 125 | CreateTime int64 `json:"create_time"` 126 | CreateTimeMs string `json:"create_time_ms"` 127 | Side string `json:"side"` 128 | CurrencyPair string `json:"currency_pair"` 129 | Amount string `json:"amount"` 130 | Price string `json:"price"` 131 | } 132 | 133 | type OrderMsg struct { 134 | // SpotOrderMsg ID 135 | Id string `json:"id,omitempty"` 136 | // User defined information. If not empty, must follow the rules below: 1. prefixed with `t-` 2. no longer than 28 bytes without `t-` prefix 3. can only include 0-9, A-Z, a-z, underscore(_), hyphen(-) or dot(.) 137 | Text string `json:"text,omitempty"` 138 | // SpotOrderMsg creation time 139 | CreateTime string `json:"create_time,omitempty"` 140 | // SpotOrderMsg last modification time 141 | UpdateTime string `json:"update_time,omitempty"` 142 | // Currency pair 143 | CurrencyPair string `json:"currency_pair"` 144 | // SpotOrderMsg type. limit - limit order 145 | Type string `json:"type,omitempty"` 146 | // Account type. spot - use spot account; margin - use margin account 147 | Account string `json:"account,omitempty"` 148 | // SpotOrderMsg side 149 | Side string `json:"side"` 150 | // SpotTradeMsg amount 151 | Amount string `json:"amount"` 152 | // SpotOrderMsg price 153 | Price string `json:"price"` 154 | // Time in force - gtc: GoodTillCancelled - ioc: ImmediateOrCancelled, taker only - poc: PendingOrCancelled, makes a post-only order that always enjoys a maker fee 155 | TimeInForce string `json:"time_in_force,omitempty"` 156 | // Amount to display for the iceberg order. Null or 0 for normal orders 157 | Iceberg string `json:"iceberg,omitempty"` 158 | // Used in margin trading(i.e. `account` is `margin`) to allow automatic loan of insufficient part if balance is not enough. 159 | AutoBorrow bool `json:"auto_borrow,omitempty"` 160 | // Amount left to fill 161 | Left string `json:"left,omitempty"` 162 | // Total filled in quote currency. Deprecated in favor of `filled_total` 163 | FillPrice string `json:"fill_price,omitempty"` 164 | // Total filled in quote currency 165 | FilledTotal string `json:"filled_total,omitempty"` 166 | // Average fill price 167 | AvgDealPrice string `json:"avg_deal_price,omitempty"` 168 | // Fee deducted 169 | Fee string `json:"fee,omitempty"` 170 | // Fee currency unit 171 | FeeCurrency string `json:"fee_currency,omitempty"` 172 | // Point used to deduct fee 173 | PointFee string `json:"point_fee,omitempty"` 174 | // GT used to deduct fee 175 | GtFee string `json:"gt_fee,omitempty"` 176 | // Whether GT fee discount is used 177 | GtDiscount bool `json:"gt_discount,omitempty"` 178 | // Rebated fee 179 | RebatedFee string `json:"rebated_fee,omitempty"` 180 | // Rebated fee currency unit 181 | RebatedFeeCurrency string `json:"rebated_fee_currency,omitempty"` 182 | // StpId represents the ID associated with the self-trade prevention mechanism. 183 | StpId int64 `json:"stp_id,omitempty"` 184 | // StpAct represents the self-trade prevention (STP) action: 185 | // - cn: Cancel newest (keep old orders) 186 | // - co: Cancel oldest (keep new orders) 187 | // - cb: Cancel both (cancel both old and new orders) 188 | // If not provided, defaults to 'cn'. Requires STP group membership; otherwise, an error is returned. 189 | StpAct string `json:"stp_act,omitempty"` 190 | // FinishAs indicates how the order was finished: 191 | // - open: processing 192 | // - filled: fully filled 193 | // - cancelled: manually cancelled 194 | // - ioc: finished immediately (IOC) 195 | // - stp: cancelled due to self-trade prevention 196 | FinishAs string `json:"finish_as,omitempty"` 197 | // BizInfo represents business-specific information related to the order. The exact content and format can vary depending on the use case. 198 | BizInfo string `json:"biz_info,omitempty"` 199 | // AmendText provides the custom data that the user remarked when amending the order 200 | AmendText string `json:"amend_text,omitempty"` 201 | } 202 | 203 | type SpotOrderMsg struct { 204 | OrderMsg 205 | CreateTimeMs string `json:"create_time_ms,omitempty"` 206 | UpdateTimeMs string `json:"update_time_ms,omitempty"` 207 | User int64 `json:"user"` 208 | Event string `json:"event"` 209 | } 210 | -------------------------------------------------------------------------------- /python/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python,pycharm+all,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm+all,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### PyCharm+all ### 34 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 35 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 36 | 37 | # User-specific stuff 38 | .idea/**/workspace.xml 39 | .idea/**/tasks.xml 40 | .idea/**/usage.statistics.xml 41 | .idea/**/dictionaries 42 | .idea/**/shelf 43 | 44 | # Generated files 45 | .idea/**/contentModel.xml 46 | 47 | # Sensitive or high-churn files 48 | .idea/**/dataSources/ 49 | .idea/**/dataSources.ids 50 | .idea/**/dataSources.local.xml 51 | .idea/**/sqlDataSources.xml 52 | .idea/**/dynamic.xml 53 | .idea/**/uiDesigner.xml 54 | .idea/**/dbnavigator.xml 55 | 56 | # Gradle 57 | .idea/**/gradle.xml 58 | .idea/**/libraries 59 | 60 | # Gradle and Maven with auto-import 61 | # When using Gradle or Maven with auto-import, you should exclude module files, 62 | # since they will be recreated, and may cause churn. Uncomment if using 63 | # auto-import. 64 | # .idea/artifacts 65 | # .idea/compiler.xml 66 | # .idea/jarRepositories.xml 67 | # .idea/modules.xml 68 | # .idea/*.iml 69 | # .idea/modules 70 | # *.iml 71 | # *.ipr 72 | 73 | # CMake 74 | cmake-build-*/ 75 | 76 | # Mongo Explorer plugin 77 | .idea/**/mongoSettings.xml 78 | 79 | # File-based project format 80 | *.iws 81 | 82 | # IntelliJ 83 | out/ 84 | 85 | # mpeltonen/sbt-idea plugin 86 | .idea_modules/ 87 | 88 | # JIRA plugin 89 | atlassian-ide-plugin.xml 90 | 91 | # Cursive Clojure plugin 92 | .idea/replstate.xml 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | # Editor-based Rest Client 101 | .idea/httpRequests 102 | 103 | # Android studio 3.1+ serialized cache file 104 | .idea/caches/build_file_checksums.ser 105 | 106 | ### PyCharm+all Patch ### 107 | # Ignores the whole .idea folder and all .iml files 108 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 109 | 110 | .idea/ 111 | 112 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 113 | 114 | *.iml 115 | modules.xml 116 | .idea/misc.xml 117 | *.ipr 118 | 119 | # Sonarlint plugin 120 | .idea/sonarlint 121 | 122 | ### Python ### 123 | # Byte-compiled / optimized / DLL files 124 | __pycache__/ 125 | *.py[cod] 126 | *$py.class 127 | 128 | # C extensions 129 | *.so 130 | 131 | # Distribution / packaging 132 | .Python 133 | build/ 134 | develop-eggs/ 135 | dist/ 136 | downloads/ 137 | eggs/ 138 | .eggs/ 139 | parts/ 140 | sdist/ 141 | var/ 142 | wheels/ 143 | pip-wheel-metadata/ 144 | share/python-wheels/ 145 | *.egg-info/ 146 | .installed.cfg 147 | *.egg 148 | MANIFEST 149 | 150 | # PyInstaller 151 | # Usually these files are written by a python script from a template 152 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 153 | *.manifest 154 | *.spec 155 | 156 | # Installer logs 157 | pip-log.txt 158 | pip-delete-this-directory.txt 159 | 160 | # Unit test / coverage reports 161 | htmlcov/ 162 | .tox/ 163 | .nox/ 164 | .coverage 165 | .coverage.* 166 | .cache 167 | nosetests.xml 168 | coverage.xml 169 | *.cover 170 | *.py,cover 171 | .hypothesis/ 172 | .pytest_cache/ 173 | pytestdebug.log 174 | 175 | # Translations 176 | *.mo 177 | *.pot 178 | 179 | # Django stuff: 180 | *.log 181 | local_settings.py 182 | db.sqlite3 183 | db.sqlite3-journal 184 | 185 | # Flask stuff: 186 | instance/ 187 | .webassets-cache 188 | 189 | # Scrapy stuff: 190 | .scrapy 191 | 192 | # Sphinx documentation 193 | docs/_build/ 194 | doc/_build/ 195 | 196 | # PyBuilder 197 | target/ 198 | 199 | # Jupyter Notebook 200 | .ipynb_checkpoints 201 | 202 | # IPython 203 | profile_default/ 204 | ipython_config.py 205 | 206 | # pyenv 207 | .python-version 208 | 209 | # pipenv 210 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 211 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 212 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 213 | # install all needed dependencies. 214 | #Pipfile.lock 215 | 216 | # poetry 217 | #poetry.lock 218 | 219 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 220 | __pypackages__/ 221 | 222 | # Celery stuff 223 | celerybeat-schedule 224 | celerybeat.pid 225 | 226 | # SageMath parsed files 227 | *.sage.py 228 | 229 | # Environments 230 | # .env 231 | .env/ 232 | .venv/ 233 | env/ 234 | venv/ 235 | ENV/ 236 | env.bak/ 237 | venv.bak/ 238 | pythonenv* 239 | 240 | # Spyder project settings 241 | .spyderproject 242 | .spyproject 243 | 244 | # Rope project settings 245 | .ropeproject 246 | 247 | # mkdocs documentation 248 | /site 249 | 250 | # mypy 251 | .mypy_cache/ 252 | .dmypy.json 253 | dmypy.json 254 | 255 | # Pyre type checker 256 | .pyre/ 257 | 258 | # pytype static type analyzer 259 | .pytype/ 260 | 261 | # operating system-related files 262 | # file properties cache/storage on macOS 263 | *.DS_Store 264 | # thumbnail cache on Windows 265 | Thumbs.db 266 | 267 | # profiling data 268 | .prof 269 | 270 | 271 | # End of https://www.toptal.com/developers/gitignore/api/python,pycharm+all,macos -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Gate WebSocket Python SDK 2 | 3 | `gate_ws` provides Gate WebSocket V4 Python implementation, including all channels defined in 4 | spot(new) and futures WebSocket. 5 | 6 | Features: 7 | 8 | 1. Fully asynchronous 9 | 2. Reconnect on connection to server lost, and resubscribe on connection recovered 10 | 3. Support connecting to multiple websocket servers 11 | 4. Highly configurable 12 | 13 | ## Installation 14 | 15 | This package requires Python version 3.6+. Python 2 will NOT be supported. 16 | 17 | ```sh 18 | pip install --user gate-ws 19 | ``` 20 | 21 | ## Getting Started 22 | 23 | ```python 24 | import asyncio 25 | 26 | from gate_ws import Configuration, Connection, WebSocketResponse 27 | from gate_ws.spot import SpotPublicTradeChannel 28 | 29 | 30 | # define your callback function on message received 31 | def print_message(conn: Connection, response: WebSocketResponse): 32 | if response.error: 33 | print('error returned: ', response.error) 34 | conn.close() 35 | return 36 | print(response.result) 37 | 38 | 39 | async def main(): 40 | # initialize default connection, which connects to spot WebSocket V4 41 | # it is recommended to use one conn to initialize multiple channels 42 | conn = Connection(Configuration()) 43 | 44 | # subscribe to any channel you are interested into, with the callback function 45 | channel = SpotPublicTradeChannel(conn, print_message) 46 | channel.subscribe(["GT_USDT"]) 47 | 48 | # start the client 49 | await conn.run() 50 | 51 | 52 | if __name__ == '__main__': 53 | loop = asyncio.get_event_loop() 54 | loop.run_until_complete(main()) 55 | loop.close() 56 | ``` 57 | 58 | ## Application Demos 59 | 60 | We provide some demo applications in the [examples](examples) directory, which can be run directly. 61 | 62 | ## Advanced usage 63 | 64 | 1. Subscribe to private channels 65 | ```python 66 | from gate_ws import Configuration, Connection 67 | from gate_ws.spot import SpotOrderChannel 68 | 69 | 70 | async def main(): 71 | conn = Connection(Configuration(api_key='YOUR_API_KEY', api_secret='YOUR_API_SECRET')) 72 | channel = SpotOrderChannel(conn, lambda c, r: print(r.result)) 73 | channel.subscribe(["GT_USDT"]) 74 | 75 | # start the client 76 | await conn.run() 77 | ``` 78 | 2. Your callback function can also be a coroutine 79 | ```python 80 | import asyncio 81 | 82 | 83 | async def my_callback(conn, response): 84 | await asyncio.sleep(1) 85 | print(response.result) 86 | ``` 87 | 3. You can provide a default callback function for all channels, so that when subscribing to new 88 | channels, no additional callback function are needed. 89 | ```python 90 | from gate_ws import Configuration, Connection 91 | from gate_ws.spot import SpotPublicTradeChannel 92 | 93 | 94 | async def main(): 95 | # provide default callback for all channels 96 | conn = Connection(Configuration(default_callback=lambda c, r: print(r.result))) 97 | 98 | # default callback will be used if callback not provided when initializing channels 99 | channel = SpotPublicTradeChannel(conn) 100 | channel.subscribe(["GT_USDT"]) 101 | 102 | # start the client 103 | await conn.run() 104 | ``` 105 | 4. Subscribe to both spot and futures WebSockets 106 | ```python 107 | import asyncio 108 | 109 | from gate_ws import Configuration, Connection 110 | from gate_ws.spot import SpotPublicTradeChannel 111 | from gate_ws.futures import FuturesPublicTradeChannel 112 | 113 | 114 | async def main(): 115 | # initialize a spot connection, which is the default if no parameters is provided 116 | spot_conn = Connection(Configuration(app='spot')) 117 | # initialize a futures connection 118 | futures_conn = Connection(Configuration(app='futures', settle='usdt', test_net=False)) 119 | 120 | # subscribe to any channel you are interested into, with the callback function 121 | channel = SpotPublicTradeChannel(spot_conn, lambda c, r: print(r.result)) 122 | channel.subscribe(["BTC_USDT"]) 123 | 124 | channel = FuturesPublicTradeChannel(futures_conn, lambda c, r: print(r.result)) 125 | channel.subscribe(["BTC_USDT"]) 126 | 127 | # start both connection 128 | await asyncio.gather(spot_conn.run(), futures_conn.run()) 129 | ``` 130 | 5. You can use your own executor pool to run your callback function 131 | ```python 132 | import concurrent.futures 133 | 134 | from gate_ws import Configuration, Connection 135 | from gate_ws.spot import SpotPublicTradeChannel 136 | 137 | 138 | async def main(): 139 | # use process pool to run your callback function 140 | with concurrent.futures.ProcessPoolExecutor() as pool: 141 | conn = Connection(Configuration(executor_pool=pool)) 142 | 143 | # default callback will be used if callback not provided when subscribing 144 | channel = SpotPublicTradeChannel(conn, lambda c, r: print(r.result)) 145 | channel.subscribe(["GT_USDT"]) 146 | 147 | # start the client 148 | await conn.run() 149 | ``` 150 | -------------------------------------------------------------------------------- /python/examples/README.md: -------------------------------------------------------------------------------- 1 | # Demo applications 2 | 3 | ## Local order book 4 | 5 | [local_order_book.py](local_order_book.py) provides a demo application showing how to maintain a 6 | local spot BTC_USDT order book using Gate.io WebSocket `spot.order_book_update` channel and HTTP 7 | API. 8 | 9 | To run this demo, you need to install the following packages manually: 10 | 11 | ```sh 12 | pip install --user gate-ws sortedcontainers aiohttp asciimatics 13 | ``` 14 | 15 | Then run it directly `python local_order_book.py` 16 | 17 | > Python3.6+ is required. 18 | 19 | This application maintains the local order book through `LocalOrderBook` class which provides a 20 | callback method `ws_callback` for WebSocket connection to call on order book update received. The 21 | animation is shown through `OrderBookFrame` using `asciimatics`. You can use `LocalOrderBook` 22 | instance in your own application, without `OrderBookFrame`, `screen` initialization 23 | and `play_order_book` task. 24 | 25 | Some notes: 26 | 27 | 1. When the demo application starts, you might see the order book did not change for several 28 | seconds. It is a normal case, as the HTTP result is trying to keep up with WebSocket updates' 29 | pace. 30 | 2. You can only run this application in a terminal. Order book maintained this way provides at most 31 | 100 levels. The application will detect the terminal height to display with proper levels. The 32 | longer your terminal, the higher levels will be shown. 33 | 3. Resizing your terminal is not supported when running. 34 | 35 | Here is a pre-recorded clip. 36 | 37 | [![asciicast](https://asciinema.org/a/406646.svg)](https://asciinema.org/a/406646) 38 | -------------------------------------------------------------------------------- /python/examples/local_order_book.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # coding: utf-8 3 | 4 | """ 5 | Example of how to maintain a local spot order book 6 | """ 7 | import asyncio 8 | import itertools 9 | import logging 10 | import sys 11 | import typing 12 | from collections import defaultdict 13 | from datetime import datetime 14 | from decimal import Decimal 15 | 16 | try: 17 | import aiohttp 18 | from asciimatics.parsers import AsciimaticsParser 19 | from asciimatics.scene import Scene 20 | from asciimatics.screen import Screen 21 | from asciimatics.widgets import Frame, Layout, TextBox 22 | from asciimatics.widgets import MultiColumnListBox 23 | from sortedcontainers import SortedList 24 | except ImportError: 25 | sys.stderr.write('aiohttp, sortedcontainers, and asciimatics are required\n') 26 | sys.exit(1) 27 | 28 | from gate_ws import Configuration, Connection, WebSocketResponse 29 | from gate_ws.spot import SpotOrderBookUpdateChannel 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | class SimpleRingBuffer(object): 35 | """Simple ring buffer to cache order book updates 36 | 37 | But can be used in other general scenario too 38 | """ 39 | 40 | def __init__(self, size: int): 41 | self.max = size 42 | self.data = [] 43 | self.cur = 0 44 | 45 | class __Full: 46 | # to avoid warning hints from IDE 47 | max: int 48 | data: typing.List 49 | cur: int 50 | 51 | def append(self, x): 52 | self.data[self.cur] = x 53 | self.cur = (self.cur + 1) % self.max 54 | 55 | def __iter__(self): 56 | for i in itertools.chain(range(self.cur, self.max), range(self.cur)): 57 | yield self.data[i] 58 | 59 | def get(self, idx): 60 | return self.data[(self.cur + idx) % self.max] 61 | 62 | def __getitem__(self, item): 63 | if isinstance(item, int): 64 | return self.get(item) 65 | return (self.data[self.cur:] + self.data[:self.cur]).__getitem__(item) 66 | 67 | def __len__(self): 68 | return self.max 69 | 70 | def __iter__(self): 71 | for i in self.data: 72 | yield i 73 | 74 | def append(self, x): 75 | self.data.append(x) 76 | if len(self.data) == self.max: 77 | self.cur = 0 78 | # Permanently change self's class from non-full to full 79 | self.__class__ = self.__Full 80 | 81 | def get(self, idx): 82 | return self.data[idx] 83 | 84 | def __getitem__(self, item): 85 | return self.data.__getitem__(item) 86 | 87 | def __len__(self): 88 | return len(self.data) 89 | 90 | 91 | class OrderBookEntry(object): 92 | 93 | def __init__(self, price, amount): 94 | self.price: Decimal = Decimal(price) 95 | self.amount: str = amount 96 | 97 | def __eq__(self, other): 98 | return self.price == other.price 99 | 100 | def __str__(self): 101 | return '(%s, %s)' % (self.price, self.amount) 102 | 103 | 104 | class OrderBook(object): 105 | 106 | def __init__(self, cp: str, last_id: id, asks: SortedList, bids: SortedList): 107 | self.cp = cp 108 | self.id = last_id 109 | self.asks = asks 110 | self.bids = bids 111 | 112 | @classmethod 113 | def update_entry(cls, book: SortedList, entry: OrderBookEntry): 114 | if Decimal(entry.amount) == Decimal('0'): 115 | # remove price if amount is 0 116 | try: 117 | book.remove(entry) 118 | except ValueError: 119 | pass 120 | else: 121 | try: 122 | idx = book.index(entry) 123 | except ValueError: 124 | # price not found, insert it 125 | book.add(entry) 126 | else: 127 | # price found, update amount 128 | book[idx].amount = entry.amount 129 | 130 | def __str__(self): 131 | return '\n id: %d\n asks:\n%s\n bids:\n%s' % (self.id, 132 | '\n'.join([' ' * 4 + str(a) for a in self.asks]), 133 | '\n'.join([' ' * 4 + str(b) for b in self.bids])) 134 | 135 | def update(self, ws_update): 136 | if ws_update['u'] < self.id + 1: 137 | # ignore older message 138 | return 139 | if ws_update['U'] > self.id + 1: 140 | raise ValueError("base order book ID %d falls behind update between %d-%d" % 141 | (self.id, ws_update['U'], ws_update['u'])) 142 | # start from the first message which satisfies U <= ob.id+1 <= u 143 | logger.debug("current id %d, update from %s", self.id, ws_update) 144 | for ask in ws_update['a']: 145 | entry = OrderBookEntry(*ask) 146 | self.update_entry(self.asks, entry) 147 | for bid in ws_update['b']: 148 | entry = OrderBookEntry(*bid) 149 | self.update_entry(self.bids, entry) 150 | # update local order book ID 151 | # check order book overlapping 152 | if len(self.asks) > 0 and len(self.bids) > 0: 153 | if self.asks[0].price <= self.bids[0].price: 154 | raise ValueError("price overlapping, min ask price %s not greater than max bid price %s" % ( 155 | self.asks[0].price, self.bids[0].price)) 156 | self.id = ws_update['u'] 157 | 158 | 159 | class LocalOrderBook(object): 160 | 161 | def __init__(self, currency_pair: str): 162 | self.cp = currency_pair 163 | self.q = asyncio.Queue(maxsize=500) 164 | self.buf = SimpleRingBuffer(size=500) 165 | self.ob = OrderBook(self.cp, 0, SortedList(), SortedList()) 166 | 167 | @property 168 | def id(self): 169 | return self.ob.id 170 | 171 | @property 172 | def asks(self): 173 | return self.ob.asks 174 | 175 | @property 176 | def bids(self): 177 | return self.ob.bids 178 | 179 | async def construct_base_order_book(self) -> OrderBook: 180 | while True: 181 | async with aiohttp.ClientSession() as session: 182 | # aiohttp does not allow boolean parameter variable 183 | async with session.get('https://api.gateio.ws/api/v4/spot/order_book', 184 | params={'currency_pair': self.cp, 'limit': 100, 'with_id': 'true'}) as response: 185 | if response.status != 200: 186 | logger.warning("failed to retrieve base order book: ", await response.text()) 187 | await asyncio.sleep(1) 188 | continue 189 | result = await response.json() 190 | assert isinstance(result, dict) 191 | assert result.get('id') 192 | logger.debug("retrieved new base order book with id %d", result.get('id')) 193 | ob = OrderBook(self.cp, result.get('id'), 194 | SortedList([OrderBookEntry(*x) for x in result.get('asks')], key=lambda x: x.price), 195 | # sort bid from high to low 196 | SortedList([OrderBookEntry(*x) for x in result.get('bids')], key=lambda x: -x.price)) 197 | # use cached result to recover our local order book fast 198 | for b in self.buf: 199 | try: 200 | ob.update(b) 201 | except ValueError as e: 202 | logger.warning("failed to update: %s", e) 203 | await asyncio.sleep(0.5) 204 | break 205 | else: 206 | return ob 207 | 208 | async def run(self): 209 | while True: 210 | self.ob = await self.construct_base_order_book() 211 | while True: 212 | result = await self.q.get() 213 | try: 214 | self.ob.update(result) 215 | except ValueError as e: 216 | logger.error("failed to update: %s", e) 217 | # reconstruct order book 218 | break 219 | 220 | def _cache_update(self, ws_update): 221 | if len(self.buf) > 0: 222 | last_id = self.buf[-1]['u'] 223 | if ws_update['u'] < last_id: 224 | # ignore older message 225 | return 226 | if ws_update['U'] != last_id + 1: 227 | # update message not consecutive, reconstruct cache 228 | self.buf = SimpleRingBuffer(size=100) 229 | self.buf.append(ws_update) 230 | 231 | async def ws_callback(self, conn: Connection, response: WebSocketResponse): 232 | if response.error: 233 | # stop the client if error happened 234 | conn.close() 235 | raise response.error 236 | # ignore subscribe success response 237 | if 's' not in response.result or response.result.get('s') != self.cp: 238 | return 239 | result = response.result 240 | logger.debug("received update: %s", result) 241 | assert isinstance(result, dict) 242 | self._cache_update(result) 243 | await self.q.put(result) 244 | 245 | 246 | class OrderBookFrame(Frame): 247 | def __init__(self, screen, order_book: LocalOrderBook): 248 | super(OrderBookFrame, self).__init__(screen, 249 | screen.height, 250 | screen.width, 251 | has_border=False, 252 | name="Order Book") 253 | # Internal state required for doing periodic updates 254 | self._last_frame = 0 255 | self._ob = order_book 256 | self._level = screen.height // 2 - 1 257 | 258 | # Create the basic form layout... 259 | layout = Layout([1], fill_frame=True) 260 | self._header = TextBox(1, as_string=True) 261 | self._header.disabled = True 262 | self._header.custom_colour = "label" 263 | self._asks = MultiColumnListBox( 264 | screen.height // 2, 265 | ["<25%", "<25%", "<25%"], 266 | [], 267 | titles=["Level", "Price", "Amount"], 268 | name="ask_book", 269 | parser=AsciimaticsParser()) 270 | self._bids = MultiColumnListBox( 271 | screen.height // 2, 272 | ["<25%", "<25%", "<25%"], 273 | [], 274 | titles=["Level", "Price", "Amount"], 275 | name="bid_book", 276 | parser=AsciimaticsParser()) 277 | self.add_layout(layout) 278 | layout.add_widget(self._header) 279 | layout.add_widget(self._asks) 280 | layout.add_widget(self._bids) 281 | self.fix() 282 | 283 | # Add my own colour palette 284 | self.palette = defaultdict( 285 | lambda: (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK)) 286 | for key in ["selected_focus_field", "label"]: 287 | self.palette[key] = (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_BLACK) 288 | self.palette["title"] = (Screen.COLOUR_BLACK, Screen.A_NORMAL, Screen.COLOUR_WHITE) 289 | 290 | def _update(self, frame_no): 291 | # Refresh the list view if needed 292 | if frame_no - self._last_frame >= self.frame_update_count or self._last_frame == 0: 293 | self._last_frame = frame_no 294 | 295 | # Create the data to go in the multi-column list 296 | ask_data = [ 297 | (["#%02d" % (self._level - i), str(x.price), x.amount], i) 298 | for i, x in enumerate(reversed(self._ob.asks[:self._level])) 299 | ] 300 | bid_data = [ 301 | (["#%02d" % (i + 1), str(x.price), x.amount], i) for i, x in enumerate(self._ob.bids[:self._level]) 302 | ] 303 | self._asks.options = ask_data 304 | self._bids.options = bid_data 305 | self._header.value = ( 306 | "Currency Pair: {} Time: {}".format( 307 | 'BTC_USDT', 308 | datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S.%f%z") 309 | ) 310 | ) 311 | 312 | # Now redraw as normal 313 | super(OrderBookFrame, self)._update(frame_no) 314 | 315 | @property 316 | def frame_update_count(self): 317 | # Refresh once every 0.5 seconds 318 | return 10 319 | 320 | 321 | async def play_order_book(screen: Screen): 322 | while True: 323 | screen.draw_next_frame() 324 | await asyncio.sleep(0.05) 325 | 326 | 327 | if __name__ == '__main__': 328 | logging.basicConfig(level=logging.ERROR, format="%(asctime)s: %(message)s") 329 | conn = Connection(Configuration()) 330 | demo_cp = 'BTC_USDT' 331 | order_book = LocalOrderBook(demo_cp) 332 | channel = SpotOrderBookUpdateChannel(conn, order_book.ws_callback) 333 | channel.subscribe([demo_cp, "100ms"]) 334 | 335 | loop = asyncio.get_event_loop() 336 | 337 | screen = Screen.open() 338 | screen.set_scenes([Scene([OrderBookFrame(screen, order_book)], -1)]) 339 | loop.create_task(order_book.run()) 340 | loop.create_task(conn.run()) 341 | loop.create_task(play_order_book(screen)) 342 | try: 343 | loop.run_forever() 344 | except KeyboardInterrupt: 345 | for task in asyncio.Task.all_tasks(loop): 346 | task.cancel() 347 | screen.close() 348 | loop.close() 349 | -------------------------------------------------------------------------------- /python/examples/order.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # coding: utf-8 3 | 4 | """ 5 | Example of how to order spot 6 | """ 7 | import asyncio 8 | import logging 9 | 10 | from gate_ws import Configuration, Connection, WebSocketResponse 11 | from gate_ws.spot import SpotOrderCancelChannel, SpotOrderPlaceChannel 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | async def callback(_: Connection, response: WebSocketResponse): 17 | if response.error: 18 | logger.error("failed to api_request: %s", response.error) 19 | 20 | if response.channel == "spot.login": 21 | return 22 | 23 | if response.channel == "spot.order_place": 24 | logger.info("order status: %s", response.result) 25 | 26 | if response.channel == "spot.order_cancel": 27 | logger.info("order cancel: %s", response.result) 28 | 29 | 30 | order_place_param = { 31 | "text": "t-sssd", 32 | "currency_pair": "BCH_USDT", 33 | "type": "limit", 34 | "account": "spot", 35 | "side": "buy", 36 | "iceberg": "0", 37 | "price": "20", 38 | "amount": "0.05", 39 | "time_in_force": "gtc", 40 | "auto_borrow": False, 41 | } 42 | 43 | order_cancel_param = {"currency_pair": "BCH_USDT", "order_id": "1862000415"} 44 | 45 | if __name__ == "__main__": 46 | logging.basicConfig(level=logging.DEBUG, format="%(asctime)s: %(message)s") 47 | cfg = Configuration( 48 | api_key="{API_KEY}", # required 49 | api_secret="{API_SECRET}", # required 50 | ) 51 | 52 | conn = Connection(cfg) 53 | SpotOrderPlaceChannel(conn, callback).api_request( 54 | order_place_param, "header", "req_id" 55 | ) 56 | 57 | SpotOrderCancelChannel(conn, callback).api_request( 58 | order_cancel_param, "header", "req_id" 59 | ) 60 | 61 | loop = asyncio.get_event_loop() 62 | loop.create_task(conn.run()) 63 | 64 | try: 65 | loop.run_forever() 66 | except KeyboardInterrupt: 67 | tasks = asyncio.Task.all_tasks(loop) 68 | for task in tasks: 69 | task.cancel() 70 | group = asyncio.gather(*tasks, return_exceptions=True) 71 | loop.run_until_complete(group) 72 | loop.close() 73 | -------------------------------------------------------------------------------- /python/examples/ticker.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # coding: utf-8 3 | 4 | """ 5 | Example of subscribe tickers 6 | """ 7 | import asyncio 8 | import logging 9 | 10 | from gate_ws import Configuration, Connection, WebSocketResponse 11 | from gate_ws.spot import SpotTickerChannel 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | async def callback(conn: Connection, response: WebSocketResponse): 17 | if response.error: 18 | conn.close() 19 | raise response.error 20 | 21 | result = response.result 22 | logger.debug("received update: %s", result) 23 | assert isinstance(result, dict) 24 | 25 | 26 | if __name__ == "__main__": 27 | logging.basicConfig(level=logging.DEBUG, format="%(asctime)s: %(message)s") 28 | cfg = Configuration() 29 | 30 | conn = Connection(cfg) 31 | channel = SpotTickerChannel(conn, callback) 32 | channel.subscribe(["BTC_USDT"]) 33 | 34 | loop = asyncio.get_event_loop() 35 | loop.create_task(conn.run()) 36 | 37 | try: 38 | loop.run_forever() 39 | except KeyboardInterrupt: 40 | tasks = asyncio.Task.all_tasks(loop) 41 | for task in tasks: 42 | task.cancel() 43 | group = asyncio.gather(*tasks, return_exceptions=True) 44 | loop.run_until_complete(group) 45 | loop.close() 46 | -------------------------------------------------------------------------------- /python/gate_ws/__init__.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # coding: utf-8 3 | 4 | 5 | from gate_ws.client import Configuration 6 | from gate_ws.client import Connection 7 | from gate_ws.client import GateWebsocketError 8 | from gate_ws.client import WebSocketResponse 9 | 10 | __all__ = [Configuration, Connection, GateWebsocketError, WebSocketResponse] 11 | -------------------------------------------------------------------------------- /python/gate_ws/client.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # coding: utf-8 3 | import abc 4 | import asyncio 5 | import hashlib 6 | import hmac 7 | import json 8 | import logging 9 | import ssl 10 | import time 11 | import typing 12 | 13 | import websockets 14 | from websockets.exceptions import WebSocketException 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class GateWebsocketError(Exception): 20 | def __init__(self, code, message): 21 | self.code = code 22 | self.message = message 23 | 24 | def __str__(self): 25 | return "code: %d, message: %s" % (self.code, self.message) 26 | 27 | 28 | class Configuration(object): 29 | def __init__( 30 | self, 31 | app: str = "spot", 32 | settle: str = "usdt", 33 | test_net: bool = False, 34 | host: str = "", 35 | api_key: str = "", 36 | api_secret: str = "", 37 | event_loop=None, 38 | executor_pool=None, 39 | default_callback=None, 40 | ping_interval: int = 5, 41 | max_retry: int = 10, 42 | verify: bool = True, 43 | ): 44 | """Initialize running configuration 45 | 46 | @param app: Which websocket to connect to, spot or futures, default to spot 47 | @param settle: If app is futures, which settle currency to use, btc or usdt 48 | @param test_net: If app is futures, whether use test net 49 | @param host: Websocket host, inferred from app, settle and test_net if not provided 50 | @param api_key: APIv4 Key, must not be empty if subscribing to private channels 51 | @param api_secret: APIv4 Secret, must not be empty if subscribing to private channels 52 | @param event_loop: Event loop to use. default to asyncio default event loop 53 | @param executor_pool: Your callback executor pool. Default to asyncio default event loop if callback is 54 | awaitable, otherwise asyncio default concurrent.futures.Executor executor 55 | @param default_callback: Default callback function for all channels. If channels specific callback is not 56 | provided, it will be called instead 57 | @param ping_interval: Active ping interval to websocket server, default to 5 seconds 58 | @param max_retry: Connection retry times on connection to server lost. Reconnect will be given up if 59 | max_retry reached. No upper limit if negative. Default to 10. 60 | @param verify: enable certificate verification, default to True 61 | """ 62 | self.app = app 63 | self.api_key = api_key 64 | self.api_secret = api_secret 65 | default_host = "wss://api.gateio.ws/ws/v4/" 66 | if app == "futures": 67 | default_host = "wss://fx-ws.gateio.ws/v4/ws/%s" % settle 68 | if test_net: 69 | default_host = "wss://fx-ws-testnet.gateio.ws/v4/ws/%s" % settle 70 | self.host = host or default_host 71 | self.loop = event_loop 72 | self.pool = executor_pool 73 | self.default_callback = default_callback 74 | self.ping_interval = ping_interval 75 | self.max_retry = max_retry 76 | self.verify = verify 77 | 78 | 79 | class WebSocketRequest(object): 80 | def __init__( 81 | self, 82 | cfg: Configuration, 83 | channel: str, 84 | event: str, 85 | payload: str, 86 | require_auth: bool, 87 | ): 88 | self.channel = channel 89 | self.event = event 90 | self.payload = payload 91 | self.require_auth = require_auth 92 | self.cfg = cfg 93 | 94 | def __str__(self): 95 | request = { 96 | "time": int(time.time()), 97 | "channel": self.channel, 98 | "event": self.event, 99 | "payload": self.payload, 100 | } 101 | if self.require_auth: 102 | if not (self.cfg.api_key and self.cfg.api_secret): 103 | raise ValueError("configuration does not provide api key or secret") 104 | message = "channel=%s&event=%s&time=%d" % ( 105 | self.channel, 106 | self.event, 107 | request["time"], 108 | ) 109 | request["auth"] = { 110 | "method": "api_key", 111 | "KEY": self.cfg.api_key, 112 | "SIGN": hmac.new( 113 | self.cfg.api_secret.encode("utf8"), 114 | message.encode("utf8"), 115 | hashlib.sha512, 116 | ).hexdigest(), 117 | } 118 | return json.dumps(request) 119 | 120 | 121 | class ApiRequest(object): 122 | def __init__( 123 | self, 124 | cfg: Configuration, 125 | channel: str, 126 | header: str = "", 127 | req_id: str = "", 128 | payload: object = {}, 129 | ): 130 | self.cfg = cfg 131 | if not (self.cfg.api_key and self.cfg.api_secret): 132 | raise ValueError("configuration does not provide api key or secret") 133 | self.channel = channel 134 | self.header = header 135 | self.req_id = req_id 136 | self.payload = payload 137 | 138 | def gen(self): 139 | data_time = int(time.time()) 140 | param_json = json.dumps(self.payload) 141 | message = "%s\n%s\n%s\n%d" % ("api", self.channel, param_json, data_time) 142 | 143 | data_param = { 144 | "time": data_time, 145 | "channel": self.channel, 146 | "event": "api", 147 | "payload": { 148 | "req_header": {"X-Gate-Channel-Id": self.header}, 149 | "api_key": self.cfg.api_key, 150 | "timestamp": f"{data_time}", 151 | "signature": hmac.new( 152 | self.cfg.api_secret.encode("utf8"), 153 | message.encode("utf8"), 154 | hashlib.sha512, 155 | ).hexdigest(), 156 | "req_id": self.req_id, 157 | "req_param": self.payload, 158 | }, 159 | } 160 | 161 | return json.dumps(data_param) 162 | 163 | 164 | class WebSocketResponse(object): 165 | def __init__(self, body: str): 166 | self.body = body 167 | msg = json.loads(body) 168 | self.channel = msg.get("channel") or (msg.get("header") or {}).get("channel") 169 | if not self.channel: 170 | raise ValueError("no channel found from response message: %s" % body) 171 | 172 | self.timestamp = msg.get("time") 173 | self.event = msg.get("event") 174 | self.result = ( 175 | msg.get("result") 176 | or (msg.get("data") or {}).get("result") 177 | or (msg.get("data") or {}).get("errs") 178 | ) 179 | self.error = None 180 | if msg.get("error"): 181 | self.error = GateWebsocketError( 182 | msg["error"].get("code"), msg["error"].get("message") 183 | ) 184 | 185 | def __str__(self) -> str: 186 | return self.body 187 | 188 | 189 | class Connection(object): 190 | def __init__(self, cfg: Configuration): 191 | self.cfg = cfg 192 | self.channels: typing.Dict[str, typing.Any] = dict() 193 | self.sending_queue = asyncio.Queue() 194 | self.sending_history = list() 195 | self.event_loop: asyncio.AbstractEventLoop = ( 196 | cfg.loop or asyncio.get_event_loop() 197 | ) 198 | self.main_loop = None 199 | 200 | def register(self, channel, callback=None): 201 | if callback: 202 | self.channels[channel] = callback 203 | 204 | def unregister(self, channel): 205 | self.channels.pop(channel, None) 206 | 207 | def send(self, msg): 208 | self.sending_queue.put_nowait(msg) 209 | 210 | async def _active_ping(self, conn: websockets.WebSocketClientProtocol): 211 | while True: 212 | data = json.dumps( 213 | {"time": int(time.time()), "channel": "%s.ping" % self.cfg.app} 214 | ) 215 | await conn.send(data) 216 | await asyncio.sleep(self.cfg.ping_interval) 217 | 218 | async def _write(self, conn: websockets.WebSocketClientProtocol): 219 | if self.sending_history: 220 | for msg in self.sending_history: 221 | if isinstance(msg, WebSocketRequest): 222 | msg = str(msg) 223 | await conn.send(msg) 224 | while True: 225 | msg = await self.sending_queue.get() 226 | self.sending_history.append(msg) 227 | if isinstance(msg, WebSocketRequest): 228 | msg = str(msg) 229 | await conn.send(msg) 230 | 231 | async def _read(self, conn: websockets.WebSocketClientProtocol): 232 | async for msg in conn: 233 | response = WebSocketResponse(msg) 234 | callback = self.channels.get(response.channel, self.cfg.default_callback) 235 | if callback is not None: 236 | if asyncio.iscoroutinefunction(callback): 237 | self.event_loop.create_task(callback(self, response)) 238 | else: 239 | self.event_loop.run_in_executor( 240 | self.cfg.pool, callback, self, response 241 | ) 242 | 243 | def close(self): 244 | if self.main_loop: 245 | self.main_loop.cancel() 246 | 247 | async def run(self): 248 | stopped = False 249 | retried = 0 250 | while not stopped: 251 | try: 252 | ctx = None 253 | if self.cfg.host.startswith("wss://"): 254 | ctx = ssl.create_default_context() 255 | if not self.cfg.verify: 256 | ctx.check_hostname = False 257 | ctx.verify_mode = ssl.CERT_NONE 258 | # compression is not fully supported in server 259 | conn = await websockets.connect( 260 | self.cfg.host, ssl=ctx, compression=None 261 | ) 262 | if retried > 0: 263 | logger.warning( 264 | "reconnect succeeded after retrying %d times", retried + 1 265 | ) 266 | retried = 0 267 | # DNS might be resolved to multiple address, which cause multiple ConnectionRefusedError 268 | # being combined to one OSError 269 | except (WebSocketException, ConnectionRefusedError, OSError) as e: 270 | logger.warning( 271 | "failed to connect to server for the %d time, try again later: %s", 272 | retried + 1, 273 | e, 274 | ) 275 | retried += 1 276 | if 0 < self.cfg.max_retry < retried: 277 | logger.error( 278 | "max reconnect time %d reached, give it up", self.cfg.max_retry 279 | ) 280 | stopped = True 281 | continue 282 | await asyncio.sleep(0.5 * retried) 283 | else: 284 | tasks: typing.List[asyncio.Task] = list() 285 | try: 286 | tasks.append(self.event_loop.create_task(self._write(conn))) 287 | tasks.append(self.event_loop.create_task(self._read(conn))) 288 | tasks.append(self.event_loop.create_task(self._active_ping(conn))) 289 | self.main_loop = asyncio.gather(*tasks) 290 | await self.main_loop 291 | except websockets.ConnectionClosed: 292 | logger.warning("websocket connection lost, retry to reconnect") 293 | except asyncio.CancelledError: 294 | await conn.close() 295 | stopped = True 296 | finally: 297 | # user callback tasks are not our concern 298 | for task in tasks: 299 | task.cancel() 300 | 301 | 302 | class BaseChannel(abc.ABC): 303 | name = "" 304 | require_auth = False 305 | 306 | def __init__(self, conn: Connection, callback=None): 307 | self.conn = conn 308 | self.callback = callback 309 | self.cfg = self.conn.cfg 310 | self.conn.register(self.name, callback) 311 | 312 | def subscribe(self, payload={}): 313 | self.conn.send( 314 | WebSocketRequest( 315 | self.cfg, self.name, "subscribe", payload, self.require_auth 316 | ) 317 | ) 318 | 319 | def unsubscribe(self, payload={}): 320 | self.conn.send( 321 | WebSocketRequest( 322 | self.cfg, self.name, "unsubscribe", payload, self.require_auth 323 | ) 324 | ) 325 | 326 | def api_request(self, payload={}, header="", req_id=""): 327 | self.login(header, req_id) 328 | self.conn.send(ApiRequest(self.cfg, self.name, header, req_id, payload).gen()) 329 | 330 | def login(self, header, req_id): 331 | channel = "spot.login" 332 | if self.cfg.app != "spot": 333 | channel = "futures.login" 334 | self.conn.send(ApiRequest(self.cfg, channel, header, req_id, {}).gen()) 335 | -------------------------------------------------------------------------------- /python/gate_ws/futures.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from gate_ws.client import BaseChannel 5 | 6 | 7 | class FuturesTickerChannel(BaseChannel): 8 | name = "futures.tickers" 9 | 10 | 11 | class FuturesPublicTradeChannel(BaseChannel): 12 | name = "futures.trades" 13 | 14 | 15 | class FuturesCandlesticksChannel(BaseChannel): 16 | name = "futures.candlesticks" 17 | 18 | 19 | class FuturesBookTickerChannel(BaseChannel): 20 | name = "futures.book_ticker" 21 | 22 | 23 | class FuturesOrderBookUpdateChannel(BaseChannel): 24 | name = "futures.order_book_update" 25 | 26 | 27 | class FuturesOrderBookChannel(BaseChannel): 28 | name = "futures.order_book" 29 | 30 | 31 | class FuturesOrderChannel(BaseChannel): 32 | name = "futures.orders" 33 | require_auth = True 34 | 35 | 36 | class FuturesUserTradesChannel(BaseChannel): 37 | name = "futures.usertrades" 38 | require_auth = True 39 | 40 | 41 | class FuturesLiquidatesChannel(BaseChannel): 42 | name = "futures.liquidates" 43 | require_auth = True 44 | 45 | 46 | class FuturesADLChannel(BaseChannel): 47 | name = "futures.auto_deleverages" 48 | require_auth = True 49 | 50 | 51 | class FuturesPositionClosesChannel(BaseChannel): 52 | name = "futures.position_closes" 53 | require_auth = True 54 | 55 | 56 | class FuturesBalanceChannel(BaseChannel): 57 | name = "futures.balances" 58 | require_auth = True 59 | 60 | 61 | class FuturesReduceRiskLimitChannel(BaseChannel): 62 | name = "futures.reduce_risk_limits" 63 | require_auth = True 64 | 65 | 66 | class FuturesPositionsChannel(BaseChannel): 67 | name = "futures.positions" 68 | require_auth = True 69 | 70 | 71 | class FuturesAutoOrdersChannel(BaseChannel): 72 | name = "futures.autoorders" 73 | require_auth = True 74 | 75 | 76 | class FuturesLoginChannel(BaseChannel): 77 | name = "futures.login" 78 | 79 | 80 | class FuturesOrderAmendChannel(BaseChannel): 81 | name = "futures.order_amend" 82 | 83 | 84 | class FuturesOrderCancelChannel(BaseChannel): 85 | name = "futures.order_cancel" 86 | 87 | 88 | class FuturesOrderCancelCpChannel(BaseChannel): 89 | name = "futures.order_cancel_cp" 90 | 91 | 92 | class FuturesOrderPlaceChannel(BaseChannel): 93 | name = "futures.order_place" 94 | 95 | 96 | class FuturesOrderBatchPlaceChannel(BaseChannel): 97 | name = "futures.order_batch_place" 98 | 99 | 100 | class FuturesOrderStatusChannel(BaseChannel): 101 | name = "futures.order_status" 102 | 103 | 104 | class FuturesOrderListChannel(BaseChannel): 105 | name = "futures.order_list" 106 | -------------------------------------------------------------------------------- /python/gate_ws/spot.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from gate_ws.client import BaseChannel 5 | 6 | 7 | class SpotTickerChannel(BaseChannel): 8 | name = "spot.tickers" 9 | 10 | 11 | class SpotPublicTradeChannel(BaseChannel): 12 | name = "spot.trades" 13 | 14 | 15 | class SpotCandlesticksChannel(BaseChannel): 16 | name = "spot.candlesticks" 17 | 18 | 19 | class SpotBookTickerChannel(BaseChannel): 20 | name = "spot.book_ticker" 21 | 22 | 23 | class SpotOrderBookUpdateChannel(BaseChannel): 24 | name = "spot.order_book_update" 25 | 26 | 27 | class SpotOrderBookChannel(BaseChannel): 28 | name = "spot.order_book" 29 | 30 | 31 | class SpotOrderChannel(BaseChannel): 32 | name = "spot.orders" 33 | require_auth = True 34 | 35 | 36 | class SpotUserTradesChannel(BaseChannel): 37 | name = "spot.usertrades" 38 | require_auth = True 39 | 40 | 41 | class SpotBalanceChannel(BaseChannel): 42 | name = "spot.balances" 43 | require_auth = True 44 | 45 | 46 | class SpotMarginBalanceChannel(BaseChannel): 47 | name = "spot.margin_balances" 48 | require_auth = True 49 | 50 | 51 | class SpotFundingBalanceChannel(BaseChannel): 52 | name = "spot.funding_balances" 53 | require_auth = True 54 | 55 | 56 | class SpotCrossMarginBalanceChannel(BaseChannel): 57 | name = "spot.cross_balances" 58 | require_auth = True 59 | 60 | 61 | class SpotLoginChannel(BaseChannel): 62 | name = "spot.login" 63 | 64 | 65 | class SpotOrderAmendChannel(BaseChannel): 66 | name = "spot.order_amend" 67 | 68 | 69 | class SpotOrderCancelChannel(BaseChannel): 70 | name = "spot.order_cancel" 71 | 72 | 73 | class SpotOrderCancelCpChannel(BaseChannel): 74 | name = "spot.order_cancel_cp" 75 | 76 | 77 | class SpotOrderCancelIdsChannel(BaseChannel): 78 | name = "spot.order_cancel_ids" 79 | 80 | 81 | class SpotOrderPlaceChannel(BaseChannel): 82 | name = "spot.order_place" 83 | 84 | 85 | class SpotOrderStatusChannel(BaseChannel): 86 | name = "spot.order_status" 87 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | websockets>=8.1 -------------------------------------------------------------------------------- /python/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | long_description = file: README.md 3 | long_description_content_type = text/markdown 4 | classifiers : 5 | License :: OSI Approved :: MIT License 6 | Development Status :: 3 - Alpha 7 | Framework :: AsyncIO 8 | Programming Language :: Python :: 3 9 | Programming Language :: Python :: 3.6 10 | Programming Language :: Python :: 3.7 11 | Programming Language :: Python :: 3.8 12 | Programming Language :: Python :: 3.9 13 | Programming Language :: Python :: 3.10 14 | maintainer = gateio 15 | maintainer_email = dev@mail.gate.io 16 | 17 | [flake8] 18 | max-line-length = 120 -------------------------------------------------------------------------------- /python/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | VERSION = '0.3.1' 4 | 5 | setup( 6 | name='gate-ws', 7 | version=VERSION, 8 | packages=find_packages(), 9 | url='https://github.com/gateio/gatews', 10 | install_requires=['websockets>=8.1'], 11 | license='MIT License', 12 | author='gateio', 13 | keywords=["Gate WebSocket V4"], 14 | author_email='dev@mail.gate.io', 15 | description='Gate.io WebSocket V4 Python SDK' 16 | ) 17 | --------------------------------------------------------------------------------