├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── lint.yml │ └── vuln.yml ├── .gitignore ├── .golangci.yml ├── LICENSE.md ├── Makefile ├── README.md ├── account.go ├── account_test.go ├── binance.go ├── binance_test.go ├── bytes.go ├── client.go ├── endpoints.go ├── errors.go ├── go.mod ├── go.sum ├── oco.go ├── oco_test.go ├── order.go ├── order_test.go ├── ticker.go ├── ticker_test.go ├── types.go └── ws ├── account.go ├── account_test.go ├── binance.go ├── binance_test.go ├── conn.go ├── endpoints.go ├── ticker.go ├── ticker_test.go └── types.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: "/" 9 | schedule: 10 | interval: daily -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | permissions: {} # no need any permissions 4 | 5 | on: 6 | push: 7 | branches: [master] 8 | pull_request: 9 | branches: [master] 10 | schedule: 11 | - cron: '0 10 * * 1' # run "At 10:00 on Monday" 12 | workflow_call: 13 | inputs: 14 | skipTests: 15 | description: 'Skip tests, useful when there is a dedicated CI job for tests' 16 | default: false 17 | required: false 18 | type: boolean 19 | 20 | jobs: 21 | run: 22 | name: Build 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 5 25 | strategy: 26 | fail-fast: true 27 | matrix: 28 | go: ['stable'] 29 | 30 | steps: 31 | - name: Check out code 32 | uses: actions/checkout@v3 33 | 34 | - name: Install Go 35 | uses: actions/setup-go@v4 36 | with: 37 | go-version: ${{ matrix.go }} 38 | check-latest: true 39 | 40 | - name: Go Format 41 | run: gofmt -s -w . && git diff --exit-code 42 | 43 | - name: Go Vet 44 | run: go vet ./... 45 | 46 | - name: Go Tidy 47 | run: go mod tidy && git diff --exit-code 48 | 49 | - name: Go Mod 50 | run: go mod download 51 | 52 | - name: Go Mod Verify 53 | run: go mod verify 54 | 55 | - name: Go Generate 56 | run: go generate ./... && git diff --exit-code 57 | 58 | - name: Go Build 59 | run: go build -o /dev/null ./... 60 | 61 | - name: Go Compile Tests 62 | if: ${{ inputs.skipTests }} 63 | run: go test -exec /bin/true ./... 64 | 65 | - name: Go Test 66 | if: ${{ !inputs.skipTests }} 67 | run: go test -v -count=1 -race -shuffle=on -coverprofile=coverage.txt ./... 68 | 69 | - name: Go Benchmark 70 | if: ${{ !inputs.skipTests }} 71 | run: go test -v -shuffle=on -run=- -bench=. -benchtime=1x ./... 72 | 73 | - name: Upload Coverage 74 | if: ${{ !inputs.skipTests }} 75 | uses: codecov/codecov-action@v3 76 | continue-on-error: true 77 | with: 78 | token: ${{secrets.CODECOV_TOKEN}} 79 | file: ./coverage.txt 80 | fail_ci_if_error: false -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | permissions: {} # no need any permissions 4 | 5 | on: 6 | push: 7 | branches: [master] 8 | pull_request: 9 | branches: [master] 10 | workflow_call: 11 | 12 | jobs: 13 | run: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 5 17 | strategy: 18 | fail-fast: true 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | 24 | - name: Install Go 25 | uses: actions/setup-go@v4 26 | with: 27 | go-version: 'stable' 28 | check-latest: true 29 | 30 | - name: Lint 31 | uses: golangci/golangci-lint-action@v3.4.0 32 | with: 33 | version: latest 34 | args: --timeout 5m -------------------------------------------------------------------------------- /.github/workflows/vuln.yml: -------------------------------------------------------------------------------- 1 | name: vuln 2 | 3 | permissions: {} # no need any permissions 4 | 5 | on: 6 | push: 7 | branches: [master] 8 | pull_request: 9 | branches: [master] 10 | schedule: 11 | - cron: '0 10 * * 1' # run "At 10:00 on Monday" 12 | workflow_call: 13 | 14 | jobs: 15 | run: 16 | name: Vuln 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 5 19 | strategy: 20 | fail-fast: true 21 | 22 | steps: 23 | - name: Install Go 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version: 'stable' 27 | check-latest: true 28 | 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | 32 | - name: Install govulncheck 33 | run: go install golang.org/x/vuln/cmd/govulncheck@latest 34 | 35 | - name: Run govulncheck 36 | run: govulncheck -test ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### IDEA settings 2 | .idea 3 | 4 | ### Go template 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | .env 22 | 23 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | # default concurrency is a available CPU number 3 | concurrency: 4 4 | 5 | # timeout for analysis, e.g. 30s, 5m, default is 1m 6 | timeout: 10m 7 | 8 | # exit code when at least one issue was found, default is 1 9 | issues-exit-code: 1 10 | 11 | # include test files or not, default is true 12 | tests: true 13 | 14 | # list of build tags, all linters use it. Default is empty list. 15 | # build-tags: 16 | # - mytag 17 | 18 | # which dirs to skip: issues from them won't be reported; 19 | # can use regexp here: generated.*, regexp is applied on full path; 20 | # default value is empty list, but default dirs are skipped independently 21 | # from this option's value (see skip-dirs-use-default). 22 | # "/" will be replaced by current OS file path separator to properly work 23 | # on Windows. 24 | # skip-dirs: 25 | # - src/external_libs 26 | # - autogenerated_by_my_lib 27 | 28 | # default is true. Enables skipping of directories: 29 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 30 | skip-dirs-use-default: true 31 | 32 | # which files to skip: they will be analyzed, but issues from them 33 | # won't be reported. Default value is empty list, but there is 34 | # no need to include all autogenerated files, we confidently recognize 35 | # autogenerated files. If it's not please let us know. 36 | # "/" will be replaced by current OS file path separator to properly work 37 | # on Windows. 38 | # skip-files: 39 | # - ".*\\.my\\.go$" 40 | # - lib/bad.go 41 | 42 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 43 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 44 | # automatic updating of go.mod described above. Instead, it fails when any changes 45 | # to go.mod are needed. This setting is most useful to check that go.mod does 46 | # not need updates, such as in a continuous integration and testing system. 47 | # If invoked with -mod=vendor, the go command assumes that the vendor 48 | # directory holds the correct copies of dependencies and ignores 49 | # the dependency descriptions in go.mod. 50 | # modules-download-mode: readonly|vendor|mod 51 | 52 | # Allow multiple parallel golangci-lint instances running. 53 | # If false (default) - golangci-lint acquires file lock on start. 54 | allow-parallel-runners: false 55 | 56 | linters: 57 | enable: 58 | - errcheck # : Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false] 59 | - gosimple # (megacheck): Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false] 60 | - govet # (vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false] 61 | - ineffassign # : Detects when assignments to existing variables are not used [fast: true, auto-fix: false] 62 | - staticcheck # (megacheck): Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false] 63 | - typecheck # : Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false] 64 | - unused # (megacheck): Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] 65 | - asciicheck # : Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] 66 | - bodyclose # : checks whether HTTP response body is closed successfully [fast: false, auto-fix: false] 67 | - durationcheck # : check for two durations multiplied together [fast: false, auto-fix: false] 68 | - exportloopref # : checks for pointers to enclosing loop variables [fast: false, auto-fix: false] 69 | - gocritic # : Provides many diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: false] 70 | - importas # : Enforces consistent import aliases [fast: false, auto-fix: false] 71 | - misspell # : Finds commonly misspelled English words in comments [fast: true, auto-fix: true] 72 | - nakedret # : Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] 73 | - promlinter # : Check Prometheus metrics naming via promlint [fast: true, auto-fix: false] 74 | - revive # : Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false] 75 | - stylecheck # : Stylecheck is a replacement for golint [fast: false, auto-fix: false] 76 | - unconvert # : Remove unnecessary type conversions [fast: false, auto-fix: false] 77 | - unparam # : Reports unused function parameters [fast: false, auto-fix: false] 78 | - wastedassign # : wastedassign finds wasted assignment statements. [fast: false, auto-fix: false] 79 | disable: 80 | - golint # : Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: false, auto-fix: false] 81 | - forcetypeassert # : finds forced type assertions [fast: true, auto-fix: false] 82 | - goimports # : In addition to fixing imports, goimports also formats your code in the same style as gofmt. [fast: true, auto-fix: true] 83 | - gofumpt # : Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true] 84 | - contextcheck #: check the function whether use a non-inherited context [fast: false, auto-fix: false] 85 | - exhaustive # : check exhaustiveness of enum switch statements [fast: false, auto-fix: false] 86 | - goconst # : Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] 87 | - godot # : Check if comments end in a period [fast: true, auto-fix: true] 88 | - gofmt # : Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] 89 | - gomnd # : An analyzer to detect magic numbers. [fast: true, auto-fix: false] 90 | - gosec # (gas): Inspects source code for security problems [fast: false, auto-fix: false] 91 | - interfacer # : Linter that suggests narrower interface types [fast: false, auto-fix: false] 92 | - makezero # : Finds slice declarations with non-zero initial length [fast: false, auto-fix: false] 93 | - nestif # : Reports deeply nested if statements [fast: true, auto-fix: false] 94 | - nilerr # : Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false] 95 | - noctx # : noctx finds sending http request without context.Context [fast: false, auto-fix: false] 96 | - nolintlint # : Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false] 97 | - prealloc # : Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false] 98 | - predeclared # : find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false] 99 | - rowserrcheck # : checks whether Err of rows is checked successfully [fast: false, auto-fix: false] 100 | - sqlclosecheck # : Checks that sql.Rows and sql.Stmt are closed. [fast: false, auto-fix: false] 101 | - cyclop # : checks function and package cyclomatic complexity [fast: false, auto-fix: false] 102 | - depguard # : Go linter that checks if package imports are in a list of acceptable packages [fast: false, auto-fix: false] 103 | - dogsled # : Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] 104 | - dupl # : Tool for code clone detection [fast: true, auto-fix: false] 105 | - errname # : Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false] 106 | - errorlint # : errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false] 107 | - exhaustivestruct # : Checks if all struct's fields are initialized [fast: false, auto-fix: false] 108 | - forbidigo # : Forbids identifiers [fast: true, auto-fix: false] 109 | - funlen # : Tool for detection of long functions [fast: true, auto-fix: false] 110 | - gci # : Gci control golang package import order and make it always deterministic. [fast: true, auto-fix: true] 111 | - gochecknoglobals # : check that no global variables exist [fast: true, auto-fix: false] 112 | - gochecknoinits # : Checks that no init functions are present in Go code [fast: true, auto-fix: false] 113 | - gocognit # : Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false] 114 | - gocyclo # : Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] 115 | - godox # : Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false] 116 | - goerr113 # : Golang linter to check the errors handling expressions [fast: false, auto-fix: false] 117 | - goheader # : Checks is file header matches to pattern [fast: true, auto-fix: false] 118 | - gomoddirectives # : Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false] 119 | - gomodguard # : Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false] 120 | - goprintffuncname # : Checks that printf-like functions are named with `f` at the end [fast: true, auto-fix: false] 121 | - ifshort # : Checks that your code uses short syntax for if-statements whenever possible [fast: true, auto-fix: false] 122 | - lll # : Reports long lines [fast: true, auto-fix: false] 123 | - maligned # : Tool to detect Go structs that would take less memory if their fields were sorted [fast: false, auto-fix: false] 124 | - nlreturn # : nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false] 125 | - paralleltest # : paralleltest detects missing usage of t.Parallel() method in your Go test [fast: true, auto-fix: false] 126 | - scopelint # : Scopelint checks for unpinned variables in go programs [fast: true, auto-fix: false] 127 | - tagliatelle # : Checks the struct tags. [fast: true, auto-fix: false] 128 | - tenv #: tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 [fast: false, auto-fix: false] 129 | - testpackage # : linter that makes you use a separate _test package [fast: true, auto-fix: false] 130 | - thelper # : thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false] 131 | - tparallel # : tparallel detects inappropriate usage of t.Parallel() method in your Go test codes [fast: false, auto-fix: false] 132 | - varnamelen #: checks that the length of a variable's name matches its scope [fast: false, auto-fix: false] 133 | - whitespace # : Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true] 134 | - wrapcheck # : Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false] 135 | - wsl # : Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false] 136 | 137 | # See also https://gist.github.com/cristaloleg/dc29ca0ef2fb554de28d94c3c6f6dc88 138 | 139 | # output configuration options 140 | output: 141 | # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions 142 | # default is "colored-line-number" 143 | format: line-number 144 | 145 | # print lines of code with issue, default is true 146 | print-issued-lines: false 147 | 148 | # print linter name in the end of issue text, default is true 149 | print-linter-name: true 150 | 151 | # make issues output unique by line, default is true 152 | # uniq-by-line: true 153 | 154 | # add a prefix to the output file references; default is no prefix 155 | # path-prefix: "" 156 | 157 | # sorts results by: filepath, line and column 158 | sort-results: false 159 | 160 | # all available settings of specific linters 161 | linters-settings: 162 | gocritic: 163 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. 164 | # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 165 | enabled-tags: 166 | - diagnostic 167 | - experimental 168 | - opinionated 169 | - performance 170 | - style 171 | disable-checker: 172 | - unnamedResult 173 | - commentedOutCode -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2021 Vladislav Khutorskoy 5 | 6 | Copyright (c) 2017 Eran Yanay 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: fmt ## Run linter for the code 2 | golangci-lint run ./... -v -c .golangci.yml 3 | 4 | fmt: ## gofmt and goimports all go files 5 | gci write -s standard -s default -s "prefix(xenking/binance-api)" --skip-generated . 6 | gofumpt -l -w . 7 | 8 | # Absolutely awesome: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 9 | help: 10 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 11 | 12 | .DEFAULT_GOAL := help -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Binance API 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/xenking/binance-api)](https://pkg.go.dev/github.com/xenking/binance-api) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/xenking/binance-api)](https://goreportcard.com/report/github.com/xenking/binance-api) 5 | ![Build Status](https://github.com/xenking/binance-api/actions/workflows/build.yml/badge.svg) 6 | [![codecov](https://codecov.io/gh/xenking/binance-api/branch/master/graph/badge.svg)](https://codecov.io/gh/xenking/binance-api) 7 | 8 | binance-api is a fast and lightweight Golang implementation for [Binance API](https://github.com/binance/binance-spot-api-docs), providing complete API coverage, and supports both REST API and websockets API 9 | 10 | This library created to help you interact with the Binance API, streaming candlestick charts data, market depth, or use other advanced features binance exposes via API. 11 | 12 | ## Quickstart 13 | ```golang 14 | package main 15 | 16 | import ( 17 | "log" 18 | 19 | "github.com/xenking/binance-api" 20 | ) 21 | 22 | func main() { 23 | client := binance.NewClient("API-KEY", "SECRET") 24 | 25 | err := client.Ping() 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | prices, err := client.Prices() 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | for _, p := range prices { 36 | log.Printf("symbol: %s, price: %s", p.Symbol, p.Price) 37 | } 38 | } 39 | ``` 40 | 41 | 42 | ## Installation 43 | ``` 44 | go get -u github.com/xenking/binance-api 45 | ``` 46 | 47 | ## Getting started 48 | ```golang 49 | // Create default client 50 | client := binance.NewClient("API-KEY", "SECRET") 51 | 52 | // Send ping request 53 | err := client.Ping() 54 | 55 | // Create client with custom request window size 56 | client := binance.NewClient("API-KEY", "SECRET").ReqWindow(5000) 57 | 58 | // Create websocket client 59 | wsClient := ws.NewClient() 60 | 61 | // Connect to Klines websocket 62 | ws, err := wsClient.Klines("ETHBTC", binance.KlineInterval1m) 63 | 64 | // Read ws 65 | msg, err := ws.Read() 66 | ``` 67 | 68 | Full documentation on [pkg.go.dev](https://pkg.go.dev/github.com/xenking/binance-api) 69 | 70 | ## License 71 | This library is under the [MIT License](https://opensource.org/licenses/MIT). See the [LICENSE](LICENSE.md) file for more info. 72 | 73 | -------------------------------------------------------------------------------- /account.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/segmentio/encoding/json" 5 | "github.com/valyala/fasthttp" 6 | ) 7 | 8 | // Trades get for a specific account and symbol 9 | func (c *Client) Trades(req *TradeReq) ([]*Trade, error) { 10 | if req == nil { 11 | return nil, ErrNilRequest 12 | } 13 | if req.Symbol == "" { 14 | return nil, ErrEmptySymbol 15 | } 16 | if req.Limit < 0 || req.Limit > MaxTradesLimit { 17 | req.Limit = DefaultTradesLimit 18 | } 19 | res, err := c.Do(fasthttp.MethodGet, EndpointTrades, req, false, false) 20 | if err != nil { 21 | return nil, err 22 | } 23 | var trades []*Trade 24 | err = json.Unmarshal(res, &trades) 25 | 26 | return trades, err 27 | } 28 | 29 | // HistoricalTrades get for a specific symbol started from order id 30 | func (c *Client) HistoricalTrades(req *HistoricalTradeReq) ([]*Trade, error) { 31 | if req == nil { 32 | return nil, ErrNilRequest 33 | } 34 | if req.Symbol == "" { 35 | return nil, ErrEmptySymbol 36 | } 37 | if req.Limit < 0 || req.Limit > MaxTradesLimit { 38 | req.Limit = DefaultTradesLimit 39 | } 40 | res, err := c.Do(fasthttp.MethodGet, EndpointHistoricalTrades, req, false, false) 41 | if err != nil { 42 | return nil, err 43 | } 44 | var trades []*Trade 45 | err = json.Unmarshal(res, &trades) 46 | 47 | return trades, err 48 | } 49 | 50 | // AggregatedTrades gets compressed, aggregate trades. 51 | // AccountTrades that fill at the time, from the same order, with the same price will have the quantity aggregated 52 | // Remark: If both startTime and endTime are sent, limit should not be sent AND the distance between startTime and endTime must be less than 24 hours. 53 | // Remark: If frondId, startTime, and endTime are not sent, the most recent aggregate trades will be returned. 54 | func (c *Client) AggregatedTrades(req *AggregatedTradeReq) ([]*AggregatedTrade, error) { 55 | if req == nil { 56 | return nil, ErrNilRequest 57 | } 58 | if req.Symbol == "" { 59 | return nil, ErrEmptySymbol 60 | } 61 | if req.Limit < 0 || req.Limit > MaxTradesLimit { 62 | req.Limit = DefaultTradesLimit 63 | } 64 | res, err := c.Do(fasthttp.MethodGet, EndpointAggTrades, req, false, false) 65 | if err != nil { 66 | return nil, err 67 | } 68 | var trades []*AggregatedTrade 69 | err = json.Unmarshal(res, &trades) 70 | 71 | return trades, err 72 | } 73 | 74 | // AccountTrades get trades for a specific account and symbol 75 | func (c *Client) AccountTrades(req *AccountTradesReq) ([]*AccountTrade, error) { 76 | if req == nil { 77 | return nil, ErrNilRequest 78 | } 79 | if req.Limit < 0 || req.Limit > MaxAccountTradesLimit { 80 | req.Limit = MaxAccountTradesLimit 81 | } 82 | res, err := c.Do(fasthttp.MethodGet, EndpointAccountTrades, req, true, false) 83 | if err != nil { 84 | return nil, err 85 | } 86 | var resp []*AccountTrade 87 | err = json.Unmarshal(res, &resp) 88 | 89 | return resp, err 90 | } 91 | 92 | // Account get current account information 93 | func (c *Client) Account() (*AccountInfo, error) { 94 | res, err := c.Do(fasthttp.MethodGet, EndpointAccount, nil, true, false) 95 | if err != nil { 96 | return nil, err 97 | } 98 | resp := &AccountInfo{} 99 | err = json.Unmarshal(res, &resp) 100 | 101 | return resp, err 102 | } 103 | 104 | // OrderRateLimit get the user's current order count usage for all intervals. 105 | func (c *Client) OrderRateLimit() ([]*RateLimit, error) { 106 | res, err := c.Do(fasthttp.MethodGet, EndpointRateLimit, nil, true, false) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | var resp []*RateLimit 112 | err = json.Unmarshal(res, &resp) 113 | 114 | return resp, err 115 | } 116 | 117 | // MyPreventedMatches get orders that were expired due to STP 118 | func (c *Client) MyPreventedMatches(req *AccountTradesReq) ([]*AccountTrade, error) { 119 | if req == nil { 120 | return nil, ErrNilRequest 121 | } 122 | if req.Limit < 0 || req.Limit > MaxAccountTradesLimit { 123 | req.Limit = MaxAccountTradesLimit 124 | } 125 | res, err := c.Do(fasthttp.MethodGet, EndpointMyPreventedMatches, req, true, false) 126 | if err != nil { 127 | return nil, err 128 | } 129 | var resp []*AccountTrade 130 | err = json.Unmarshal(res, &resp) 131 | 132 | return resp, err 133 | } 134 | 135 | // User stream endpoint 136 | 137 | // DataStream starts a new user data stream 138 | func (c *Client) DataStream() (string, error) { 139 | res, err := c.Do(fasthttp.MethodPost, EndpointDataStream, nil, false, true) 140 | if err != nil { 141 | return "", err 142 | } 143 | 144 | resp := &DataStream{} 145 | err = json.Unmarshal(res, &resp) 146 | 147 | return resp.ListenKey, err 148 | } 149 | 150 | // DataStreamKeepAlive pings the data stream key to prevent timeout 151 | func (c *Client) DataStreamKeepAlive(listenKey string) error { 152 | _, err := c.Do(fasthttp.MethodPut, EndpointDataStream, DataStream{ListenKey: listenKey}, false, true) 153 | 154 | return err 155 | } 156 | 157 | // DataStreamClose closes the data stream key 158 | func (c *Client) DataStreamClose(listenKey string) error { 159 | _, err := c.Do(fasthttp.MethodDelete, EndpointDataStream, DataStream{ListenKey: listenKey}, false, true) 160 | 161 | return err 162 | } 163 | -------------------------------------------------------------------------------- /account_test.go: -------------------------------------------------------------------------------- 1 | package binance_test 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/segmentio/encoding/json" 7 | "github.com/xenking/binance-api" 8 | ) 9 | 10 | type mockedAccountTestSuite struct { 11 | mockedTestSuite 12 | } 13 | 14 | func (s *mockedAccountTestSuite) TestHistoricalTrades() { 15 | var expected []*binance.Trade 16 | 17 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 18 | s.Require().IsType(&binance.HistoricalTradeReq{}, data) 19 | expected = []*binance.Trade{ 20 | { 21 | ID: rand.Int63(), 22 | Price: "0.1", 23 | Qty: "1", 24 | QuoteQty: "1", 25 | Time: rand.Int63(), 26 | }, 27 | } 28 | return json.Marshal(expected) 29 | } 30 | _, e := s.client.HistoricalTrades(&binance.HistoricalTradeReq{Symbol: "LTCBTC", Limit: 5}) 31 | s.Require().NoError(e) 32 | } 33 | 34 | func (s *mockedAccountTestSuite) TestAggregatedTrades() { 35 | var expected []*binance.AggregatedTrade 36 | 37 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 38 | s.Require().IsType(&binance.AggregatedTradeReq{}, data) 39 | expected = []*binance.AggregatedTrade{ 40 | { 41 | TradeID: rand.Int63(), 42 | Price: "0.1", 43 | Quantity: "1", 44 | Timestamp: rand.Int63(), 45 | }, 46 | } 47 | return json.Marshal(expected) 48 | } 49 | _, e := s.client.AggregatedTrades(&binance.AggregatedTradeReq{Symbol: "LTCBTC"}) 50 | s.Require().NoError(e) 51 | } 52 | 53 | func (s *mockedAccountTestSuite) TestAccountTrades() { 54 | var expected []*binance.AccountTrade 55 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 56 | s.Require().IsType(&binance.AccountTradesReq{}, data) 57 | req := data.(*binance.AccountTradesReq) 58 | expected = []*binance.AccountTrade{ 59 | { 60 | Symbol: req.Symbol, 61 | OrderID: int64(rand.Uint32()), 62 | QuoteQty: "1", 63 | Price: "0.1", 64 | Qty: "1", 65 | Time: int64(rand.Uint32()), 66 | }, 67 | } 68 | return json.Marshal(expected) 69 | } 70 | 71 | actual, e := s.client.AccountTrades(&binance.AccountTradesReq{ 72 | Symbol: "LTCBTC", 73 | }) 74 | s.Require().NoError(e) 75 | s.Require().EqualValues(expected, actual) 76 | } 77 | 78 | func (s *mockedAccountTestSuite) TestAccount() { 79 | var expected *binance.AccountInfo 80 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 81 | s.Require().Nil(data) 82 | expected = &binance.AccountInfo{ 83 | MakerCommission: 15, 84 | TakerCommission: 15, 85 | BuyerCommission: 0, 86 | SellerCommission: 0, 87 | CanTrade: true, 88 | CanWithdraw: true, 89 | CanDeposit: true, 90 | AccountType: binance.PermissionTypeSpot, 91 | Balances: []*binance.Balance{{ 92 | Asset: "SNM", 93 | Free: "1", 94 | Locked: "", 95 | }}, 96 | } 97 | return json.Marshal(expected) 98 | } 99 | 100 | actual, e := s.client.Account() 101 | s.Require().NoError(e) 102 | s.Require().EqualValues(expected, actual) 103 | } 104 | 105 | func (s *mockedAccountTestSuite) TestDataStream() { 106 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 107 | s.Require().Nil(data) 108 | return json.Marshal(&binance.DataStream{ 109 | ListenKey: "stream-key", 110 | }) 111 | } 112 | key, err := s.client.DataStream() 113 | s.Require().NoError(err) 114 | s.Require().Equal("stream-key", key) 115 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 116 | s.Require().IsType(binance.DataStream{}, data) 117 | return nil, nil 118 | } 119 | s.Require().NoError(s.client.DataStreamKeepAlive(key)) 120 | s.Require().NoError(s.client.DataStreamClose(key)) 121 | } 122 | -------------------------------------------------------------------------------- /binance.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/segmentio/encoding/json" 5 | "github.com/valyala/fasthttp" 6 | ) 7 | 8 | const ( 9 | // BaseHostPort for binance addresses 10 | BaseHostPort = "api.binance.com:443" 11 | BaseHost = "api.binance.com" 12 | DefaultUserAgent = "Binance/client" 13 | ) 14 | 15 | type Client struct { 16 | RestClient 17 | } 18 | 19 | // NewClient creates a new binance client with key and secret 20 | func NewClient(apikey, secret string) *Client { 21 | return &Client{ 22 | RestClient: NewRestClient(apikey, secret), 23 | } 24 | } 25 | 26 | // NewClientHTTP2 creates a new binance client using HTTP/2 protocol with key and secret 27 | func NewClientHTTP2(apikey, secret string) (*Client, error) { 28 | c, err := NewRestClientHTTP2(apikey, secret) 29 | 30 | return &Client{ 31 | RestClient: c, 32 | }, err 33 | } 34 | 35 | func NewCustomClient(restClient RestClient) *Client { 36 | return &Client{ 37 | RestClient: restClient, 38 | } 39 | } 40 | 41 | func (c *Client) ReqWindow(window int) *Client { 42 | c.RestClient.SetWindow(window) 43 | 44 | return c 45 | } 46 | 47 | // General endpoints 48 | 49 | // Time tests connectivity to the Rest API and get the current server time 50 | func (c *Client) Time() (*ServerTime, error) { 51 | res, err := c.Do(fasthttp.MethodGet, EndpointTime, nil, false, false) 52 | if err != nil { 53 | return nil, err 54 | } 55 | serverTime := &ServerTime{} 56 | err = json.Unmarshal(res, serverTime) 57 | 58 | return serverTime, err 59 | } 60 | 61 | // Ping tests connectivity to the Rest API 62 | func (c *Client) Ping() error { 63 | _, err := c.Do(fasthttp.MethodGet, EndpointPing, nil, false, false) 64 | 65 | return err 66 | } 67 | 68 | // ExchangeInfo get current exchange trading rules and symbols information 69 | func (c *Client) ExchangeInfo(req *ExchangeInfoReq) (*ExchangeInfo, error) { 70 | if req == nil { 71 | req = &ExchangeInfoReq{} 72 | } 73 | if len(req.Symbols) > 0 && req.Symbol != "" { 74 | req.Symbol = "" 75 | } 76 | 77 | res, err := c.Do(fasthttp.MethodGet, EndpointExchangeInfo, req, false, false) 78 | if err != nil { 79 | return nil, err 80 | } 81 | resp := &ExchangeInfo{} 82 | err = json.Unmarshal(res, &resp) 83 | 84 | return resp, err 85 | } 86 | 87 | // Depth retrieves the order book for the given symbol 88 | func (c *Client) Depth(req *DepthReq) (*Depth, error) { 89 | if req == nil { 90 | return nil, ErrNilRequest 91 | } 92 | if req.Symbol == "" { 93 | return nil, ErrEmptySymbol 94 | } 95 | if req.Limit < 0 || req.Limit > MaxDepthLimit { 96 | req.Limit = DefaultDepthLimit 97 | } 98 | res, err := c.Do(fasthttp.MethodGet, EndpointDepth, req, false, false) 99 | if err != nil { 100 | return nil, err 101 | } 102 | depth := &Depth{} 103 | err = json.Unmarshal(res, &depth) 104 | 105 | return depth, err 106 | } 107 | 108 | // Klines returns kline/candlestick bars for a symbol. Kline are uniquely identified by their open time 109 | func (c *Client) Klines(req *KlinesReq) ([]*Kline, error) { 110 | if req == nil { 111 | return nil, ErrNilRequest 112 | } 113 | if req.Symbol == "" { 114 | return nil, ErrEmptySymbol 115 | } 116 | if req.Interval == "" { 117 | req.Interval = KlineInterval5min 118 | } 119 | if req.Limit < 0 || req.Limit > MaxKlinesLimit { 120 | req.Limit = DefaultKlinesLimit 121 | } 122 | res, err := c.Do(fasthttp.MethodGet, EndpointKlines, req, false, false) 123 | if err != nil { 124 | return nil, err 125 | } 126 | var klines []*Kline 127 | err = json.Unmarshal(res, &klines) 128 | 129 | return klines, err 130 | } 131 | 132 | // UIKlines returns kline/candlestick bars for a symbol. UIKlines is optimized for presentation of candlestick charts. 133 | func (c *Client) UIKlines(req *KlinesReq) ([]*Kline, error) { 134 | if req == nil { 135 | return nil, ErrNilRequest 136 | } 137 | if req.Symbol == "" { 138 | return nil, ErrEmptySymbol 139 | } 140 | if req.Interval == "" { 141 | req.Interval = KlineInterval5min 142 | } 143 | if req.Limit < 0 || req.Limit > MaxKlinesLimit { 144 | req.Limit = DefaultKlinesLimit 145 | } 146 | res, err := c.Do(fasthttp.MethodGet, EndpointUIKlines, req, false, false) 147 | if err != nil { 148 | return nil, err 149 | } 150 | var klines []*Kline 151 | err = json.Unmarshal(res, &klines) 152 | 153 | return klines, err 154 | } 155 | 156 | // AvgPrice returns current average price for a symbol. 157 | func (c *Client) AvgPrice(req *AvgPriceReq) (*AvgPrice, error) { 158 | if req == nil { 159 | return nil, ErrNilRequest 160 | } 161 | if req.Symbol == "" { 162 | return nil, ErrEmptySymbol 163 | } 164 | res, err := c.Do(fasthttp.MethodGet, EndpointAvgPrice, req, false, false) 165 | if err != nil { 166 | return nil, err 167 | } 168 | price := &AvgPrice{} 169 | err = json.Unmarshal(res, price) 170 | 171 | return price, err 172 | } 173 | 174 | // Prices calculates the latest price for all symbols 175 | func (c *Client) Prices(req *TickerPricesReq) ([]*SymbolPrice, error) { 176 | res, err := c.Do(fasthttp.MethodGet, EndpointTickerPrice, req, false, false) 177 | if err != nil { 178 | return nil, err 179 | } 180 | var prices []*SymbolPrice 181 | err = json.Unmarshal(res, &prices) 182 | 183 | return prices, err 184 | } 185 | 186 | // Price calculates the latest price for a symbol 187 | func (c *Client) Price(req *TickerPriceReq) (*SymbolPrice, error) { 188 | if req == nil { 189 | return nil, ErrNilRequest 190 | } 191 | if req.Symbol == "" { 192 | return nil, ErrEmptySymbol 193 | } 194 | res, err := c.Do(fasthttp.MethodGet, EndpointTickerPrice, req, false, false) 195 | if err != nil { 196 | return nil, err 197 | } 198 | price := &SymbolPrice{} 199 | err = json.Unmarshal(res, price) 200 | 201 | return price, err 202 | } 203 | -------------------------------------------------------------------------------- /binance_test.go: -------------------------------------------------------------------------------- 1 | package binance_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/stretchr/testify/suite" 8 | "github.com/xenking/binance-api" 9 | ) 10 | 11 | func TestClient(t *testing.T) { 12 | client := binance.NewClient("", "") 13 | 14 | suite.Run(t, &clientTestSuite{newBaseTestSuite(client)}) 15 | suite.Run(t, &tickerTestSuite{newBaseTestSuite(client)}) 16 | } 17 | 18 | func TestClientHTTP2(t *testing.T) { 19 | client, err := binance.NewClientHTTP2("", "") 20 | require.New(t).NoError(err) 21 | 22 | suite.Run(t, &clientTestSuite{newBaseTestSuite(client)}) 23 | suite.Run(t, &tickerTestSuite{newBaseTestSuite(client)}) 24 | } 25 | 26 | func TestMockClient(t *testing.T) { 27 | suite.Run(t, new(mockedAccountTestSuite)) 28 | suite.Run(t, new(mockedOrderTestSuite)) 29 | suite.Run(t, new(mockedOCOTestSuite)) 30 | } 31 | 32 | type baseTestSuite struct { 33 | suite.Suite 34 | client *binance.Client 35 | } 36 | 37 | func newBaseTestSuite(client binance.RestClient) baseTestSuite { 38 | return baseTestSuite{ 39 | client: binance.NewCustomClient(client), 40 | } 41 | } 42 | 43 | func (s *baseTestSuite) SetupSuite() { 44 | s.client = binance.NewClient("", "") 45 | } 46 | 47 | type clientTestSuite struct { 48 | baseTestSuite 49 | } 50 | 51 | func (s *clientTestSuite) TestTime() { 52 | _, e := s.client.Time() 53 | s.Require().NoError(e) 54 | } 55 | 56 | func (s *clientTestSuite) TestPing() { 57 | s.Require().NoError(s.client.Ping()) 58 | } 59 | 60 | func (s *clientTestSuite) TestExchangeInfo() { 61 | info, err := s.client.ExchangeInfo(nil) 62 | s.Require().NoError(err) 63 | s.Require().NotNil(info) 64 | s.Require().NotEmpty(info.Symbols) 65 | } 66 | 67 | func (s *clientTestSuite) TestExchangeInfoSymbol() { 68 | info, err := s.client.ExchangeInfo(&binance.ExchangeInfoReq{Symbol: "LTCBTC"}) 69 | s.Require().NoError(err) 70 | s.Require().NotNil(info) 71 | s.Require().Len(info.Symbols, 1) 72 | } 73 | 74 | func (s *clientTestSuite) TestExchangeInfoSymbols() { 75 | info, err := s.client.ExchangeInfo(&binance.ExchangeInfoReq{Symbols: []string{"LTCBTC", "ETHBTC"}}) 76 | s.Require().NoError(err) 77 | s.Require().NotNil(info) 78 | s.Require().Len(info.Symbols, 2) 79 | } 80 | 81 | func (s *clientTestSuite) TestExchangeInfoPermissions() { 82 | info, err := s.client.ExchangeInfo(&binance.ExchangeInfoReq{Permissions: []binance.PermissionType{binance.PermissionTypeSpot}}) 83 | s.Require().NoError(err) 84 | s.Require().NotNil(info) 85 | s.Require().NotEmpty(info.Symbols) 86 | } 87 | 88 | func (s *clientTestSuite) TestDepth() { 89 | _, e := s.client.Depth(&binance.DepthReq{Symbol: "LTCBTC", Limit: 5}) 90 | s.Require().NoError(e) 91 | } 92 | 93 | func (s *clientTestSuite) TestKlines() { 94 | resp, e := s.client.Klines(&binance.KlinesReq{Symbol: "LTCBTC", Interval: binance.KlineInterval1hour, Limit: 5}) 95 | s.Require().NoError(e) 96 | s.Require().Len(resp, 5) 97 | } 98 | 99 | func (s *clientTestSuite) TestTrades() { 100 | _, e := s.client.Trades(&binance.TradeReq{Symbol: "LTCBTC"}) 101 | s.Require().NoError(e) 102 | } 103 | 104 | func (s *clientTestSuite) TestAvgPrice() { 105 | _, e := s.client.AvgPrice(&binance.AvgPriceReq{Symbol: "LTCBTC"}) 106 | s.Require().NoError(e) 107 | } 108 | 109 | func (s *clientTestSuite) TestPrices() { 110 | _, e := s.client.Prices(nil) 111 | s.Require().NoError(e) 112 | 113 | resp, e := s.client.Prices(&binance.TickerPricesReq{Symbols: []string{"LTCBTC", "ETHBTC"}}) 114 | s.Require().NoError(e) 115 | s.Require().Len(resp, 2) 116 | } 117 | 118 | func (s *clientTestSuite) TestPrice() { 119 | _, e := s.client.Price(&binance.TickerPriceReq{Symbol: "LTCBTC"}) 120 | s.Require().NoError(e) 121 | } 122 | 123 | type mockedClient struct { 124 | Response func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) 125 | window int 126 | } 127 | 128 | func (m *mockedClient) UsedWeight() map[string]int64 { 129 | panic("not used") 130 | } 131 | 132 | func (m *mockedClient) OrderCount() map[string]int64 { 133 | panic("not used") 134 | } 135 | 136 | func (m *mockedClient) RetryAfter() int64 { 137 | panic("not used") 138 | } 139 | 140 | func (m *mockedClient) SetWindow(w int) { 141 | m.window = w 142 | } 143 | 144 | func (m *mockedClient) Do(method, endpoint string, data interface{}, sign, stream bool) ([]byte, error) { 145 | return m.Response(method, endpoint, data, sign, stream) 146 | } 147 | 148 | type mockedTestSuite struct { 149 | suite.Suite 150 | client *binance.Client 151 | mock *mockedClient 152 | } 153 | 154 | func (s *mockedTestSuite) SetupSuite() { 155 | s.mock = &mockedClient{} 156 | s.client = binance.NewCustomClient(s.mock) 157 | } 158 | 159 | func (s *mockedTestSuite) SetupTest() { 160 | s.mock.Response = nil 161 | } 162 | -------------------------------------------------------------------------------- /bytes.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "unsafe" 5 | ) 6 | 7 | // b2s converts byte slice to a string without memory allocation. 8 | // See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ . 9 | func b2s(b []byte) string { 10 | return unsafe.String(unsafe.SliceData(b), len(b)) 11 | } 12 | 13 | // s2b converts string to a byte slice without memory allocation. 14 | func s2b(s string) []byte { 15 | return unsafe.Slice(unsafe.StringData(s), len(s)) 16 | } 17 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "crypto/tls" 8 | "encoding/hex" 9 | "hash" 10 | "net/url" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | "sync/atomic" 16 | "time" 17 | 18 | "github.com/go-faster/errors" 19 | "github.com/google/go-querystring/query" 20 | "github.com/segmentio/encoding/json" 21 | "github.com/valyala/fasthttp" 22 | "github.com/xenking/bytebufferpool" 23 | "github.com/xenking/http2" 24 | ) 25 | 26 | type RestClient interface { 27 | Do(method, endpoint string, data interface{}, sign, stream bool) ([]byte, error) 28 | 29 | SetWindow(window int) 30 | UsedWeight() map[string]int64 31 | OrderCount() map[string]int64 32 | RetryAfter() int64 33 | } 34 | 35 | const DefaultResponseWindow = 5000 36 | 37 | func NewRestClient(key, secret string) RestClient { 38 | return &restClient{ 39 | apikey: key, 40 | hmac: hmac.New(sha256.New, s2b(secret)), 41 | client: newHTTPClient(), 42 | window: DefaultResponseWindow, 43 | } 44 | } 45 | 46 | func NewRestClientHTTP2(key, secret string) (RestClient, error) { 47 | c, err := newHTTP2Client() 48 | 49 | return &restClient{ 50 | apikey: key, 51 | hmac: hmac.New(sha256.New, s2b(secret)), 52 | client: c, 53 | window: DefaultResponseWindow, 54 | }, err 55 | } 56 | 57 | type RestClientConfig struct { 58 | APIKey string 59 | APISecret string 60 | HTTPClient *fasthttp.HostClient 61 | ResponseWindow int 62 | } 63 | 64 | func (c RestClientConfig) defaults() RestClientConfig { 65 | if c.HTTPClient == nil { 66 | c.HTTPClient = newHTTPClient() 67 | } 68 | if c.ResponseWindow == 0 { 69 | c.ResponseWindow = DefaultResponseWindow 70 | } 71 | 72 | return c 73 | } 74 | 75 | func NewCustomRestClient(config RestClientConfig) RestClient { 76 | c := config.defaults() 77 | 78 | return &restClient{ 79 | apikey: c.APIKey, 80 | hmac: hmac.New(sha256.New, s2b(c.APISecret)), 81 | client: c.HTTPClient, 82 | window: c.ResponseWindow, 83 | } 84 | } 85 | 86 | // restClient represents the actual HTTP RestClient, that is being used to interact with binance API server 87 | type restClient struct { 88 | apikey string 89 | hmac hash.Hash 90 | client *fasthttp.HostClient 91 | window int 92 | usedWeight sync.Map 93 | orderCount sync.Map 94 | retryAfter int64 95 | } 96 | 97 | const ( 98 | DefaultSchema = "https" 99 | HeaderTypeJSON = "application/json" 100 | HeaderTypeForm = "application/x-www-form-urlencoded" 101 | HeaderAccept = "Accept" 102 | HeaderAPIKey = "X-MBX-APIKEY" 103 | ) 104 | 105 | var ( 106 | HeaderUsedWeight = []byte("X-Mbx-Used-Weight-") 107 | HeaderOrderCount = []byte("X-Mbx-Order-Count-") 108 | HeaderRetryAfter = []byte("Retry-After") 109 | ) 110 | 111 | func newHTTP2Client() (*fasthttp.HostClient, error) { 112 | hc := newHTTPClient() 113 | 114 | if err := http2.ConfigureClient(hc, http2.ClientOpts{}); err != nil { 115 | return nil, errors.Wrapf(err, "%s doesn't support http/2", hc.Addr) 116 | } 117 | 118 | return hc, nil 119 | } 120 | 121 | // newHTTPClient create fasthttp.HostClient with default settings 122 | func newHTTPClient() *fasthttp.HostClient { 123 | return &fasthttp.HostClient{ 124 | NoDefaultUserAgentHeader: true, // Don't send: User-Agent: fasthttp 125 | DisableHeaderNamesNormalizing: false, 126 | DisablePathNormalizing: false, 127 | IsTLS: true, 128 | Name: DefaultUserAgent, 129 | Addr: BaseHostPort, 130 | TLSConfig: &tls.Config{ServerName: BaseHost}, 131 | } 132 | } 133 | 134 | // Do invoke the given API command with the given data 135 | // sign indicates whether the api call should be done with signed payload 136 | // stream indicates if the request is stream related 137 | func (c *restClient) Do(method, endpoint string, data interface{}, sign, stream bool) ([]byte, error) { 138 | // Convert the given data to urlencoded format 139 | values, err := query.Values(data) 140 | if err != nil { 141 | return nil, err 142 | } 143 | pb := encodeValues(values) 144 | // Signed requests require the additional timestamp, window size and signature of the payload 145 | // Remark: This is done only to routes with actual data 146 | if sign { 147 | buf := bytebufferpool.Get() 148 | pb = append(pb, "×tamp="...) 149 | pb = append(pb, strconv.AppendInt(buf.B, time.Now().UnixMilli(), 10)...) 150 | 151 | buf.Reset() 152 | pb = append(pb, "&recvWindow="...) 153 | pb = append(pb, strconv.AppendInt(buf.B, int64(c.window), 10)...) 154 | _, err = c.hmac.Write(pb) 155 | if err != nil { 156 | return nil, err 157 | } 158 | pb = append(pb, "&signature="...) 159 | sum := c.hmac.Sum(nil) 160 | enc := make([]byte, len(sum)*2) 161 | hex.Encode(enc, sum) 162 | pb = append(pb, enc...) 163 | c.hmac.Reset() 164 | bytebufferpool.Put(buf) 165 | } 166 | 167 | var b strings.Builder 168 | b.WriteString(endpoint) 169 | 170 | // Construct the http request 171 | // Remark: GET requests payload is as a query parameters 172 | // POST requests payload is given as a body 173 | req := fasthttp.AcquireRequest() 174 | 175 | if method == fasthttp.MethodGet { 176 | b.Grow(len(pb) + 1) 177 | b.WriteByte('?') 178 | b.Write(pb) 179 | } else { 180 | req.Header.SetContentType(HeaderTypeForm) 181 | req.SetBody(pb) 182 | } 183 | req.SetRequestURI(b.String()) 184 | req.Header.SetHost(BaseHost) 185 | req.URI().SetScheme(DefaultSchema) 186 | req.Header.SetMethod(method) 187 | 188 | if sign || stream { 189 | req.Header.Add(HeaderAPIKey, c.apikey) 190 | } 191 | 192 | req.Header.Add(HeaderAccept, HeaderTypeJSON) 193 | resp := fasthttp.AcquireResponse() 194 | 195 | err = c.client.Do(req, resp) 196 | if err != nil { 197 | return nil, err 198 | } 199 | fasthttp.ReleaseRequest(req) 200 | 201 | body := append([]byte{}, resp.Body()...) 202 | 203 | pb = append(pb[:0], resp.Header.Header()...) 204 | status := resp.StatusCode() 205 | fasthttp.ReleaseResponse(resp) 206 | 207 | if h := getHeader(pb, HeaderUsedWeight); h != nil { 208 | interval, val, parseErr := parseInterval(h) 209 | if parseErr == nil { 210 | c.usedWeight.Store(interval, val) 211 | } 212 | } 213 | if h := getHeader(pb, HeaderOrderCount); h != nil { 214 | interval, val, parseErr := parseInterval(h) 215 | if parseErr == nil { 216 | c.orderCount.Store(interval, val) 217 | } 218 | } 219 | 220 | if status != fasthttp.StatusOK { 221 | if h := getHeader(pb, HeaderRetryAfter); len(h) > 2 { 222 | retry, parseErr := fasthttp.ParseUint(h[2:]) 223 | if parseErr == nil { 224 | atomic.StoreInt64(&c.retryAfter, int64(retry)) 225 | } 226 | } 227 | 228 | apiErr := &APIError{} 229 | err = json.Unmarshal(body, apiErr) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | return nil, apiErr 235 | } 236 | 237 | return body, err 238 | } 239 | 240 | // SetWindow to specify response time window in milliseconds 241 | func (c *restClient) SetWindow(window int) { 242 | c.window = window 243 | } 244 | 245 | func (c *restClient) UsedWeight() map[string]int64 { 246 | res := make(map[string]int64) 247 | c.usedWeight.Range(func(k, v interface{}) bool { 248 | key, ok1 := k.(string) 249 | value, ok2 := v.(int) 250 | if ok1 && ok2 { 251 | res[key] = int64(value) 252 | } 253 | 254 | return true 255 | }) 256 | 257 | return res 258 | } 259 | 260 | func (c *restClient) OrderCount() map[string]int64 { 261 | res := make(map[string]int64) 262 | c.usedWeight.Range(func(k, v interface{}) bool { 263 | key, ok1 := k.(string) 264 | value, ok2 := v.(int) 265 | if ok1 && ok2 { 266 | res[key] = int64(value) 267 | } 268 | 269 | return true 270 | }) 271 | 272 | return res 273 | } 274 | 275 | func (c *restClient) RetryAfter() int64 { 276 | return atomic.LoadInt64(&c.retryAfter) 277 | } 278 | 279 | func encodeValues(v url.Values) []byte { 280 | if v == nil { 281 | return nil 282 | } 283 | var buf []byte 284 | keys := make([]string, 0, len(v)) 285 | for k := range v { 286 | keys = append(keys, k) 287 | } 288 | sort.Strings(keys) 289 | for _, k := range keys { 290 | vs := v[k] 291 | if len(vs) == 0 { 292 | continue 293 | } 294 | if len(buf) > 0 { 295 | buf = append(buf, '&') 296 | } 297 | buf = append(buf, url.QueryEscape(k)...) 298 | buf = append(buf, '=') 299 | if len(vs) == 1 { 300 | buf = append(buf, url.QueryEscape(vs[0])...) 301 | continue 302 | } 303 | vss, _ := json.Marshal(&vs) 304 | buf = append(buf, url.QueryEscape(string(vss))...) 305 | } 306 | return buf 307 | } 308 | 309 | func parseInterval(header []byte) (interval string, value int, err error) { 310 | parseValue := false 311 | for i := 0; i < len(header); i++ { 312 | c := header[i] 313 | switch { 314 | case c == ':', c == ' ': 315 | parseValue = true 316 | 317 | continue 318 | case parseValue: 319 | value, err = fasthttp.ParseUint(header[i:]) 320 | 321 | return 322 | case c >= '0' && c <= '9': 323 | continue 324 | } 325 | interval = string(header[:i+1]) 326 | } 327 | 328 | return 329 | } 330 | 331 | func getHeader(header, search []byte) []byte { 332 | if len(header) == 0 { 333 | return nil 334 | } 335 | if idx := bytes.Index(header, search); idx > 0 { 336 | for i := idx + len(search); i < len(header); i++ { 337 | if header[i] == '\n' { 338 | return header[idx+len(search) : i-1] 339 | } 340 | } 341 | } 342 | 343 | return nil 344 | } 345 | -------------------------------------------------------------------------------- /endpoints.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | // Endpoints with NONE security 4 | const ( 5 | EndpointAggTrades = "/api/v3/aggTrades" 6 | EndpointAvgPrice = "/api/v3/avgPrice" 7 | EndpointDepth = "/api/v3/depth" 8 | EndpointExchangeInfo = "/api/v3/exchangeInfo" 9 | EndpointKlines = "/api/v3/klines" 10 | EndpointPing = "/api/v3/ping" 11 | EndpointTicker = "/api/v3/ticker" 12 | EndpointTicker24h = "/api/v3/ticker/24hr" 13 | EndpointTickerPrice = "/api/v3/ticker/price" 14 | EndpointTickerBook = "/api/v3/ticker/bookTicker" 15 | EndpointTime = "/api/v3/time" 16 | EndpointTrades = "/api/v3/trades" 17 | EndpointUIKlines = "/api/v3/uiKlines" 18 | ) 19 | 20 | // Endpoints with SIGNED security 21 | const ( 22 | EndpointAccountTrades = "/api/v3/myTrades" 23 | EndpointRateLimit = "/api/v3/rateLimit/order" 24 | EndpointMyPreventedMatches = "/api/v3/myPreventedMatches" 25 | EndpointHistoricalTrades = "/api/v3/historicalTrades" 26 | EndpointOrder = "/api/v3/order" 27 | EndpointOrderTest = "/api/v3/order/test" 28 | EndpointCancelReplaceOrder = "/api/v3/order/cancelReplace" 29 | EndpointOrdersAll = "/api/v3/allOrders" 30 | EndpointOpenOrders = "/api/v3/openOrders" 31 | EndpointOCOOrder = "/api/v3/order/oco" 32 | EndpointOCOOrders = "/api/v3/orderList" 33 | EndpointOCOOrdersAll = "/api/v3/allOrderList" 34 | EndpointOpenOCOOrders = "/api/v3/openOrderList" 35 | ) 36 | 37 | const ( 38 | EndpointAccount = "/api/v3/account" 39 | EndpointDataStream = "/api/v3/userDataStream" 40 | ) 41 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/segmentio/encoding/json" 5 | ) 6 | 7 | type ValidationError struct { 8 | msg string 9 | } 10 | 11 | func (e ValidationError) Error() string { 12 | return e.msg 13 | } 14 | 15 | var ( 16 | ErrNilRequest = ValidationError{"nil request"} 17 | ErrEmptySymbol = ValidationError{"symbol is not set"} 18 | ErrEmptyQuantity = ValidationError{"quantity is not set"} 19 | ErrEmptyPrice = ValidationError{"price is not set"} 20 | ErrEmptyStopPrice = ValidationError{"stop price is not set"} 21 | ErrEmptySide = ValidationError{"order side is not set"} 22 | ErrEmptyOrderID = ValidationError{"order id is not set"} 23 | ErrMinStrategyType = ValidationError{"strategy type can not be lower than 1000000"} 24 | ErrEmptyJSONResponse = ValidationError{"empty json response"} 25 | ErrInvalidJSON = ValidationError{"invalid json"} 26 | ErrInvalidTickerWindow = ValidationError{"invalid ticker window"} 27 | ErrInvalidOrderType = ValidationError{"invalid order type"} 28 | // ErrIncorrectAccountEventType represents error when event type can't before determined 29 | ErrIncorrectAccountEventType = ValidationError{"incorrect account event type"} 30 | ) 31 | 32 | type APIError struct { 33 | Code int `json:"code"` 34 | Msg string `json:"msg"` 35 | } 36 | 37 | // Error return error code and message 38 | func (e *APIError) Error() string { 39 | bb, _ := json.Marshal(e) 40 | 41 | return b2s(bb) 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xenking/binance-api 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/go-faster/errors v0.6.1 7 | github.com/gobwas/ws v1.2.1 8 | github.com/google/go-querystring v1.1.0 9 | github.com/segmentio/encoding v0.3.6 10 | github.com/stretchr/testify v1.8.3 11 | github.com/valyala/fasthttp v1.47.0 12 | github.com/xenking/bytebufferpool v1.1.0 13 | github.com/xenking/decimal v1.3.5 14 | github.com/xenking/http2 v0.2.0 15 | ) 16 | 17 | require ( 18 | github.com/andybalholm/brotli v1.0.5 // indirect 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/gobwas/httphead v0.1.0 // indirect 21 | github.com/gobwas/pool v0.2.1 // indirect 22 | github.com/klauspost/compress v1.16.5 // indirect 23 | github.com/kr/pretty v0.2.1 // indirect 24 | github.com/kr/text v0.2.0 // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | github.com/segmentio/asm v1.2.0 // indirect 27 | github.com/valyala/bytebufferpool v1.0.0 // indirect 28 | github.com/valyala/fastrand v1.1.0 // indirect 29 | golang.org/x/sys v0.8.0 // indirect 30 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 31 | gopkg.in/yaml.v3 v3.0.1 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI= 7 | github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY= 8 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 9 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 10 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 11 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 12 | github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= 13 | github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= 14 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 15 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 16 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 17 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 18 | github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= 19 | github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 20 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 21 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 22 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 23 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 24 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 25 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= 29 | github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 30 | github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 31 | github.com/segmentio/encoding v0.3.6 h1:E6lVLyDPseWEulBmCmAKPanDd3jiyGDo5gMcugCRwZQ= 32 | github.com/segmentio/encoding v0.3.6/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= 33 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 34 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 35 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 36 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 37 | github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c= 38 | github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 39 | github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= 40 | github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= 41 | github.com/xenking/bytebufferpool v1.1.0 h1:xbAh59Ihh81vmlK6DsSsBi/Uo8KeIxiOJkAfWNCXscs= 42 | github.com/xenking/bytebufferpool v1.1.0/go.mod h1:GGTH45tL+BHIjyaGfrMWM7UT0ZCaW0a9Y3c/GfW8EDg= 43 | github.com/xenking/decimal v1.3.5 h1:LywO1/UdsET8lTnU4zNhTAcZcC0dv4nrwREk3nTepyw= 44 | github.com/xenking/decimal v1.3.5/go.mod h1:bKaX9swc/dBJqj8Y+IDw2TN9SXat+JfpR17HREpzEsA= 45 | github.com/xenking/http2 v0.2.0 h1:/TKdNq1gaM3pfhFDLqwJrW4Fft6XevY0A5IZrQcFZc0= 46 | github.com/xenking/http2 v0.2.0/go.mod h1:zLUhLTPLJ5t97r1MjFrI6XGPB1jwqFM67mDFzVmscvo= 47 | golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 50 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 51 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 53 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 54 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 55 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 56 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 57 | -------------------------------------------------------------------------------- /oco.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/segmentio/encoding/json" 5 | "github.com/valyala/fasthttp" 6 | ) 7 | 8 | // NewOCO get all account orders; active, canceled, or filled 9 | func (c *Client) NewOCO(req *OCOReq) (*OCOOrder, error) { 10 | switch { 11 | case req == nil: 12 | return nil, ErrNilRequest 13 | case req.Symbol == "": 14 | return nil, ErrEmptySymbol 15 | case req.Quantity == "": 16 | return nil, ErrEmptyQuantity 17 | case req.Price == "": 18 | return nil, ErrEmptyPrice 19 | case req.StopPrice == "": 20 | return nil, ErrEmptyStopPrice 21 | } 22 | 23 | res, err := c.Do(fasthttp.MethodGet, EndpointOCOOrder, req, true, false) 24 | if err != nil { 25 | return nil, err 26 | } 27 | resp := &OCOOrder{} 28 | err = json.Unmarshal(res, resp) 29 | 30 | return resp, err 31 | } 32 | 33 | // CancelOCO cancel an active OCO order 34 | func (c *Client) CancelOCO(req *CancelOCOReq) (*OCOOrder, error) { 35 | if req == nil { 36 | return nil, ErrNilRequest 37 | } 38 | if req.Symbol == "" { 39 | return nil, ErrEmptySymbol 40 | } 41 | if req.OrderListID == 0 && req.ListClientOrderID == "" { 42 | return nil, ErrEmptyOrderID 43 | } 44 | res, err := c.Do(fasthttp.MethodDelete, EndpointOCOOrders, req, true, false) 45 | if err != nil { 46 | return nil, err 47 | } 48 | resp := &OCOOrder{} 49 | err = json.Unmarshal(res, resp) 50 | 51 | return resp, err 52 | } 53 | 54 | // QueryOCO get an OCO order 55 | func (c *Client) QueryOCO(req *QueryOCOReq) (*OCOOrder, error) { 56 | if req == nil { 57 | return nil, ErrNilRequest 58 | } 59 | if req.OrderListID == 0 && req.ListClientOrderID == "" { 60 | return nil, ErrEmptyOrderID 61 | } 62 | res, err := c.Do(fasthttp.MethodGet, EndpointOCOOrders, req, true, false) 63 | if err != nil { 64 | return nil, err 65 | } 66 | resp := &OCOOrder{} 67 | err = json.Unmarshal(res, resp) 68 | 69 | return resp, err 70 | } 71 | 72 | // AllOCO get all account orders; active, canceled, or filled 73 | func (c *Client) AllOCO(req *AllOCOReq) ([]*OCOOrder, error) { 74 | if req != nil && (req.Limit < 0 || req.Limit > MaxOrderLimit) { 75 | req.Limit = DefaultOrderLimit 76 | } 77 | res, err := c.Do(fasthttp.MethodGet, EndpointOCOOrdersAll, req, true, false) 78 | if err != nil { 79 | return nil, err 80 | } 81 | var resp []*OCOOrder 82 | err = json.Unmarshal(res, &resp) 83 | 84 | return resp, err 85 | } 86 | 87 | // OpenOCO get all open orders on a symbol 88 | func (c *Client) OpenOCO() ([]*OCOOrder, error) { 89 | res, err := c.Do(fasthttp.MethodGet, EndpointOpenOCOOrders, nil, true, false) 90 | if err != nil { 91 | return nil, err 92 | } 93 | var resp []*OCOOrder 94 | err = json.Unmarshal(res, &resp) 95 | 96 | return resp, err 97 | } 98 | -------------------------------------------------------------------------------- /oco_test.go: -------------------------------------------------------------------------------- 1 | package binance_test 2 | 3 | type mockedOCOTestSuite struct { 4 | mockedTestSuite 5 | } 6 | -------------------------------------------------------------------------------- /order.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/segmentio/encoding/json" 5 | "github.com/valyala/fasthttp" 6 | ) 7 | 8 | // NewOrder sends in a new order 9 | func (c *Client) NewOrder(req *OrderReq) (*OrderRespAck, error) { 10 | if err := c.validateNewOrderReq(req); err != nil { 11 | return nil, err 12 | } 13 | req.OrderRespType = OrderRespTypeAsk 14 | res, err := c.Do(fasthttp.MethodPost, EndpointOrder, req, true, false) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | resp := &OrderRespAck{} 20 | err = json.Unmarshal(res, resp) 21 | 22 | return resp, err 23 | } 24 | 25 | // NewOrderResult sends in a new order and return created order 26 | func (c *Client) NewOrderResult(req *OrderReq) (*OrderRespResult, error) { 27 | if err := c.validateNewOrderReq(req); err != nil { 28 | return nil, err 29 | } 30 | req.OrderRespType = OrderRespTypeResult 31 | res, err := c.Do(fasthttp.MethodPost, EndpointOrder, req, true, false) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | resp := &OrderRespResult{} 37 | err = json.Unmarshal(res, resp) 38 | 39 | return resp, err 40 | } 41 | 42 | // NewOrderFull sends in a new order and return created full order info 43 | func (c *Client) NewOrderFull(req *OrderReq) (*OrderRespFull, error) { 44 | if err := c.validateNewOrderReq(req); err != nil { 45 | return nil, err 46 | } 47 | req.OrderRespType = OrderRespTypeFull 48 | res, err := c.Do(fasthttp.MethodPost, EndpointOrder, req, true, false) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | resp := &OrderRespFull{} 54 | err = json.Unmarshal(res, resp) 55 | 56 | return resp, err 57 | } 58 | 59 | // NewOrderTest tests new order creation and signature/recvWindow long. Creates and validates a new order but does not send it into the matching engine 60 | func (c *Client) NewOrderTest(req *OrderReq) error { 61 | if err := c.validateNewOrderReq(req); err != nil { 62 | return err 63 | } 64 | _, err := c.Do(fasthttp.MethodPost, EndpointOrderTest, req, true, false) 65 | 66 | return err 67 | } 68 | 69 | // QueryOrder checks an order's status 70 | func (c *Client) QueryOrder(req *QueryOrderReq) (*QueryOrder, error) { 71 | if req == nil { 72 | return nil, ErrNilRequest 73 | } 74 | if req.Symbol == "" { 75 | return nil, ErrEmptySymbol 76 | } 77 | if req.OrderID == 0 && req.OrigClientOrderID == "" { 78 | return nil, ErrEmptyOrderID 79 | } 80 | res, err := c.Do(fasthttp.MethodGet, EndpointOrder, req, true, false) 81 | if err != nil { 82 | return nil, err 83 | } 84 | resp := &QueryOrder{} 85 | err = json.Unmarshal(res, resp) 86 | 87 | return resp, err 88 | } 89 | 90 | // CancelOrder cancel an active order 91 | func (c *Client) CancelOrder(req *CancelOrderReq) (*CancelOrder, error) { 92 | if req == nil { 93 | return nil, ErrNilRequest 94 | } 95 | if req.Symbol == "" { 96 | return nil, ErrEmptySymbol 97 | } 98 | if req.OrderID == 0 && req.OrigClientOrderID == "" { 99 | return nil, ErrEmptyOrderID 100 | } 101 | res, err := c.Do(fasthttp.MethodDelete, EndpointOrder, req, true, false) 102 | if err != nil { 103 | return nil, err 104 | } 105 | resp := &CancelOrder{} 106 | err = json.Unmarshal(res, resp) 107 | 108 | return resp, err 109 | } 110 | 111 | // CancelOpenOrders cancel all open orders on a symbol 112 | func (c *Client) CancelOpenOrders(req *CancelOpenOrdersReq) ([]*CancelOrder, error) { 113 | if req == nil { 114 | return nil, ErrNilRequest 115 | } 116 | if req.Symbol == "" { 117 | return nil, ErrEmptySymbol 118 | } 119 | res, err := c.Do(fasthttp.MethodDelete, EndpointOpenOrders, req, true, false) 120 | if err != nil { 121 | return nil, err 122 | } 123 | var resp []*CancelOrder 124 | err = json.Unmarshal(res, &resp) 125 | 126 | return resp, err 127 | } 128 | 129 | // CancelReplaceOrder cancels an existing order and places a new order on the same symbol 130 | func (c *Client) CancelReplaceOrder(req *CancelReplaceOrderReq) (*CancelReplaceOrder, error) { 131 | if req == nil { 132 | return nil, ErrNilRequest 133 | } 134 | if req.CancelOrderID == 0 && req.CancelOrigClientOrderID == "" { 135 | return nil, ErrEmptyOrderID 136 | } 137 | if err := c.validateNewOrderReq(&req.OrderReq); err != nil { 138 | return nil, err 139 | } 140 | if req.CancelReplaceMode == "" { 141 | req.CancelReplaceMode = CancelReplaceModeStopOnFailure 142 | } 143 | if req.OrderRespType == "" { 144 | req.OrderRespType = OrderRespTypeAsk 145 | } 146 | 147 | res, err := c.Do(fasthttp.MethodPost, EndpointCancelReplaceOrder, req, true, false) 148 | if err != nil { 149 | return nil, err 150 | } 151 | resp := &CancelReplaceOrder{} 152 | err = json.Unmarshal(res, resp) 153 | 154 | return resp, err 155 | } 156 | 157 | // OpenOrders get all open orders on a symbol 158 | func (c *Client) OpenOrders(req *OpenOrdersReq) ([]*QueryOrder, error) { 159 | res, err := c.Do(fasthttp.MethodGet, EndpointOpenOrders, req, true, false) 160 | if err != nil { 161 | return nil, err 162 | } 163 | var resp []*QueryOrder 164 | err = json.Unmarshal(res, &resp) 165 | 166 | return resp, err 167 | } 168 | 169 | // AllOrders get all account orders; active, canceled, or filled 170 | func (c *Client) AllOrders(req *AllOrdersReq) ([]*QueryOrder, error) { 171 | if req == nil { 172 | return nil, ErrNilRequest 173 | } 174 | if req.Symbol == "" { 175 | return nil, ErrEmptySymbol 176 | } 177 | if req.Limit < 0 || req.Limit > MaxOrderLimit { 178 | req.Limit = DefaultOrderLimit 179 | } 180 | res, err := c.Do(fasthttp.MethodGet, EndpointOrdersAll, req, true, false) 181 | if err != nil { 182 | return nil, err 183 | } 184 | var resp []*QueryOrder 185 | err = json.Unmarshal(res, &resp) 186 | 187 | return resp, err 188 | } 189 | 190 | func (c *Client) validateNewOrderReq(req *OrderReq) error { 191 | switch { 192 | case req == nil: 193 | return ErrNilRequest 194 | case req.Symbol == "": 195 | return ErrEmptySymbol 196 | case req.Side == "": 197 | return ErrEmptySide 198 | case req.StrategyType > 0 && req.StrategyType < MinStrategyType: 199 | return ErrMinStrategyType 200 | } 201 | 202 | switch req.Type { 203 | case OrderTypeLimit, OrderTypeLimitMaker: 204 | switch { 205 | case req.Price == "": 206 | return ErrEmptyPrice 207 | case req.Quantity == "": 208 | return ErrEmptyQuantity 209 | case req.TimeInForce == "": 210 | req.TimeInForce = TimeInForceGTC 211 | } 212 | case OrderTypeMarket: 213 | if req.Quantity == "" && req.QuoteQuantity == "" { 214 | return ErrEmptyQuantity 215 | } 216 | case OrderTypeStopLoss, OrderTypeTakeProfit: 217 | switch { 218 | case req.Quantity == "": 219 | return ErrEmptyQuantity 220 | case req.StopPrice == "" && req.TrailingDelta == 0: 221 | return ErrEmptyStopPrice 222 | } 223 | case OrderTypeStopLossLimit, OrderTypeTakeProfitLimit: 224 | switch { 225 | case req.Quantity == "": 226 | return ErrEmptyQuantity 227 | case req.Price == "": 228 | return ErrEmptyPrice 229 | case req.StopPrice == "" && req.TrailingDelta == 0: 230 | return ErrEmptyStopPrice 231 | case req.TimeInForce == "": 232 | req.TimeInForce = TimeInForceGTC 233 | } 234 | default: 235 | return ErrInvalidOrderType 236 | } 237 | 238 | return nil 239 | } 240 | -------------------------------------------------------------------------------- /order_test.go: -------------------------------------------------------------------------------- 1 | package binance_test 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/segmentio/encoding/json" 7 | "github.com/xenking/binance-api" 8 | ) 9 | 10 | type mockedOrderTestSuite struct { 11 | mockedTestSuite 12 | } 13 | 14 | func (s *mockedOrderTestSuite) TestNewOrder() { 15 | var expected *binance.OrderRespAck 16 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 17 | s.Require().IsType(&binance.OrderReq{}, data) 18 | req := data.(*binance.OrderReq) 19 | expected = &binance.OrderRespAck{ 20 | Symbol: req.Symbol, 21 | OrderID: int64(rand.Uint32()), 22 | TransactTime: int64(rand.Uint32()), 23 | } 24 | return json.Marshal(expected) 25 | } 26 | 27 | actual, e := s.client.NewOrder(&binance.OrderReq{ 28 | Symbol: "LTCBTC", 29 | Side: binance.OrderSideSell, 30 | Type: binance.OrderTypeLimit, 31 | Quantity: "1", 32 | Price: "0.1", 33 | }) 34 | s.Require().NoError(e) 35 | s.Require().EqualValues(expected, actual) 36 | } 37 | 38 | func (s *mockedOrderTestSuite) TestNewMarketOrder() { 39 | var expected *binance.OrderRespAck 40 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 41 | s.Require().IsType(&binance.OrderReq{}, data) 42 | req := data.(*binance.OrderReq) 43 | expected = &binance.OrderRespAck{ 44 | Symbol: req.Symbol, 45 | OrderID: int64(rand.Uint32()), 46 | TransactTime: int64(rand.Uint32()), 47 | } 48 | return json.Marshal(expected) 49 | } 50 | 51 | actual, e := s.client.NewOrder(&binance.OrderReq{ 52 | Symbol: "LTCBTC", 53 | Side: binance.OrderSideSell, 54 | Type: binance.OrderTypeMarket, 55 | Quantity: "1", 56 | Price: "0.1", 57 | }) 58 | s.Require().NoError(e) 59 | s.Require().EqualValues(expected, actual) 60 | } 61 | 62 | func (s *mockedOrderTestSuite) TestNewOrderTest() { 63 | var expected *binance.OrderRespAck 64 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 65 | s.Require().IsType(&binance.OrderReq{}, data) 66 | req := data.(*binance.OrderReq) 67 | expected = &binance.OrderRespAck{ 68 | Symbol: req.Symbol, 69 | OrderID: int64(rand.Uint32()), 70 | TransactTime: int64(rand.Uint32()), 71 | } 72 | return json.Marshal(expected) 73 | } 74 | 75 | e := s.client.NewOrderTest(&binance.OrderReq{ 76 | Symbol: "LTCBTC", 77 | Side: binance.OrderSideSell, 78 | Type: binance.OrderTypeLimit, 79 | Quantity: "1", 80 | Price: "0.1", 81 | }) 82 | s.Require().NoError(e) 83 | } 84 | 85 | func (s *mockedOrderTestSuite) TestNewOrderResult() { 86 | var expected *binance.OrderRespResult 87 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 88 | s.Require().IsType(&binance.OrderReq{}, data) 89 | req := data.(*binance.OrderReq) 90 | expected = &binance.OrderRespResult{ 91 | Symbol: req.Symbol, 92 | OrderID: int64(rand.Uint32()), 93 | TransactTime: int64(rand.Uint32()), 94 | Price: req.Price, 95 | OrigQty: req.Quantity, 96 | ExecutedQty: "0", 97 | CummulativeQuoteQty: req.QuoteQuantity, 98 | Status: binance.OrderStatusNew, 99 | TimeInForce: string(req.TimeInForce), 100 | Type: req.Type, 101 | Side: req.Side, 102 | } 103 | return json.Marshal(expected) 104 | } 105 | 106 | actual, e := s.client.NewOrderResult(&binance.OrderReq{ 107 | Symbol: "LTCBTC", 108 | Side: binance.OrderSideSell, 109 | Type: binance.OrderTypeLimit, 110 | TimeInForce: binance.TimeInForceGTC, 111 | Quantity: "1", 112 | Price: "0.1", 113 | }) 114 | s.Require().NoError(e) 115 | s.Require().EqualValues(expected, actual) 116 | } 117 | 118 | func (s *mockedOrderTestSuite) TestNewOrderFull() { 119 | var expected *binance.OrderRespFull 120 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 121 | s.Require().IsType(&binance.OrderReq{}, data) 122 | req := data.(*binance.OrderReq) 123 | expected = &binance.OrderRespFull{ 124 | Symbol: req.Symbol, 125 | OrderID: int64(rand.Uint32()), 126 | TransactTime: int64(rand.Uint32()), 127 | Price: req.Price, 128 | OrigQty: req.Quantity, 129 | ExecutedQty: "0", 130 | CummulativeQuoteQty: req.QuoteQuantity, 131 | Status: binance.OrderStatusNew, 132 | TimeInForce: string(req.TimeInForce), 133 | Type: req.Type, 134 | Side: req.Side, 135 | } 136 | return json.Marshal(expected) 137 | } 138 | 139 | _, e := s.client.NewOrderFull(&binance.OrderReq{ 140 | Symbol: "LTCBTC", 141 | Side: binance.OrderSideSell, 142 | Type: binance.OrderTypeLimit, 143 | TimeInForce: binance.TimeInForceGTC, 144 | Quantity: "1", 145 | Price: "0.1", 146 | }) 147 | s.Require().NoError(e) 148 | } 149 | 150 | func (s *mockedOrderTestSuite) TestQueryCancelOrder() { 151 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 152 | s.Require().IsType(&binance.OrderReq{}, data) 153 | req := data.(*binance.OrderReq) 154 | return json.Marshal(&binance.OrderRespAck{ 155 | Symbol: req.Symbol, 156 | OrderID: int64(rand.Uint32()), 157 | TransactTime: int64(rand.Uint32()), 158 | }) 159 | } 160 | createReq := &binance.OrderReq{ 161 | Symbol: "LTCBTC", 162 | Side: binance.OrderSideSell, 163 | Type: binance.OrderTypeLimit, 164 | TimeInForce: binance.TimeInForceGTC, 165 | Quantity: "1", 166 | Price: "0.1", 167 | } 168 | resp, e := s.client.NewOrder(createReq) 169 | s.Require().NoError(e) 170 | 171 | var expectedQuery *binance.QueryOrder 172 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 173 | s.Require().IsType(&binance.QueryOrderReq{}, data) 174 | req := data.(*binance.QueryOrderReq) 175 | expectedQuery = &binance.QueryOrder{ 176 | Symbol: req.Symbol, 177 | OrderID: req.OrderID, 178 | Price: createReq.Price, 179 | OrigQty: createReq.Quantity, 180 | ExecutedQty: "0", 181 | CummulativeQuoteQty: createReq.Quantity, 182 | Status: binance.OrderStatusNew, 183 | TimeInForce: createReq.TimeInForce, 184 | Type: createReq.Type, 185 | Side: createReq.Side, 186 | Time: int64(rand.Uint32()), 187 | UpdateTime: int64(rand.Uint32()), 188 | OrigQuoteOrderQty: createReq.QuoteQuantity, 189 | } 190 | return json.Marshal(expectedQuery) 191 | } 192 | actualQuery, e := s.client.QueryOrder(&binance.QueryOrderReq{ 193 | Symbol: "LTCBTC", 194 | OrderID: resp.OrderID, 195 | }) 196 | s.Require().NoError(e) 197 | s.Require().EqualValues(expectedQuery, actualQuery) 198 | 199 | var expectedCancel *binance.CancelOrder 200 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 201 | s.Require().IsType(&binance.CancelOrderReq{}, data) 202 | expectedCancel = &binance.CancelOrder{ 203 | Symbol: actualQuery.Symbol, 204 | OrderID: actualQuery.OrderID, 205 | Price: actualQuery.Price, 206 | OrigQty: actualQuery.OrigQty, 207 | ExecutedQty: actualQuery.ExecutedQty, 208 | CummulativeQuoteQty: actualQuery.CummulativeQuoteQty, 209 | Status: actualQuery.Status, 210 | TimeInForce: actualQuery.TimeInForce, 211 | Type: actualQuery.Type, 212 | Side: actualQuery.Side, 213 | } 214 | return json.Marshal(expectedCancel) 215 | } 216 | actualCancel, e := s.client.CancelOrder(&binance.CancelOrderReq{ 217 | Symbol: "LTCBTC", 218 | OrderID: resp.OrderID, 219 | }) 220 | s.Require().NoError(e) 221 | s.Require().EqualValues(expectedCancel, actualCancel) 222 | } 223 | 224 | func (s *mockedOrderTestSuite) TestCancelReplaceOrder() { 225 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 226 | s.Require().IsType(&binance.OrderReq{}, data) 227 | req := data.(*binance.OrderReq) 228 | return json.Marshal(&binance.OrderRespAck{ 229 | Symbol: req.Symbol, 230 | OrderID: int64(rand.Uint32()), 231 | TransactTime: int64(rand.Uint32()), 232 | }) 233 | } 234 | createReq := &binance.OrderReq{ 235 | Symbol: "LTCBTC", 236 | Side: binance.OrderSideSell, 237 | Type: binance.OrderTypeLimit, 238 | TimeInForce: binance.TimeInForceGTC, 239 | Quantity: "1", 240 | Price: "0.1", 241 | } 242 | resp, e := s.client.NewOrder(createReq) 243 | s.Require().NoError(e) 244 | 245 | var expectedQuery *binance.QueryOrder 246 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 247 | s.Require().IsType(&binance.QueryOrderReq{}, data) 248 | req := data.(*binance.QueryOrderReq) 249 | expectedQuery = &binance.QueryOrder{ 250 | Symbol: req.Symbol, 251 | OrderID: req.OrderID, 252 | Price: createReq.Price, 253 | OrigQty: createReq.Quantity, 254 | ExecutedQty: "0", 255 | CummulativeQuoteQty: createReq.Quantity, 256 | Status: binance.OrderStatusNew, 257 | TimeInForce: createReq.TimeInForce, 258 | Type: createReq.Type, 259 | Side: createReq.Side, 260 | Time: int64(rand.Uint32()), 261 | UpdateTime: int64(rand.Uint32()), 262 | OrigQuoteOrderQty: createReq.QuoteQuantity, 263 | } 264 | return json.Marshal(expectedQuery) 265 | } 266 | actualQuery, e := s.client.QueryOrder(&binance.QueryOrderReq{ 267 | Symbol: "LTCBTC", 268 | OrderID: resp.OrderID, 269 | }) 270 | s.Require().NoError(e) 271 | s.Require().EqualValues(expectedQuery, actualQuery) 272 | 273 | req := &binance.CancelReplaceOrderReq{ 274 | OrderReq: *createReq, 275 | CancelOrderID: resp.OrderID, 276 | } 277 | 278 | var expectedCancel *binance.CancelReplaceOrder 279 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 280 | s.Require().IsType(&binance.CancelReplaceOrderReq{}, data) 281 | expectedCancel = &binance.CancelReplaceOrder{ 282 | CancelResult: binance.CancelReplaceResultSuccess, 283 | NewOrderResult: binance.CancelReplaceResultSuccess, 284 | CancelResponse: binance.CancelOrder{ 285 | Symbol: actualQuery.Symbol, 286 | OrderID: actualQuery.OrderID, 287 | Price: actualQuery.Price, 288 | OrigQty: actualQuery.OrigQty, 289 | ExecutedQty: actualQuery.ExecutedQty, 290 | CummulativeQuoteQty: actualQuery.CummulativeQuoteQty, 291 | Status: actualQuery.Status, 292 | TimeInForce: actualQuery.TimeInForce, 293 | Type: actualQuery.Type, 294 | Side: actualQuery.Side, 295 | }, 296 | NewOrderResponse: &binance.OrderRespFull{ 297 | Symbol: req.Symbol, 298 | OrderID: int64(rand.Uint32()), 299 | TransactTime: int64(rand.Uint32()), 300 | Price: req.Price, 301 | OrigQty: req.Quantity, 302 | ExecutedQty: "0", 303 | CummulativeQuoteQty: req.QuoteQuantity, 304 | Status: binance.OrderStatusNew, 305 | TimeInForce: string(req.TimeInForce), 306 | Type: req.Type, 307 | Side: req.Side, 308 | }, 309 | } 310 | return json.Marshal(expectedCancel) 311 | } 312 | actualCancel, e := s.client.CancelReplaceOrder(req) 313 | s.Require().NoError(e) 314 | s.Require().EqualValues(expectedCancel, actualCancel) 315 | } 316 | 317 | func (s *mockedOrderTestSuite) TestOpenOrders() { 318 | var expected []*binance.QueryOrder 319 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 320 | s.Require().IsType(&binance.OpenOrdersReq{}, data) 321 | req := data.(*binance.OpenOrdersReq) 322 | expected = append(expected, &binance.QueryOrder{ 323 | Symbol: req.Symbol, 324 | OrderID: int64(rand.Uint32()), 325 | Price: "0.1", 326 | OrigQty: "1", 327 | ExecutedQty: "0", 328 | CummulativeQuoteQty: "1", 329 | Status: binance.OrderStatusNew, 330 | TimeInForce: binance.TimeInForceGTC, 331 | Type: binance.OrderTypeLimit, 332 | Side: binance.OrderSideSell, 333 | Time: int64(rand.Uint32()), 334 | UpdateTime: int64(rand.Uint32()), 335 | OrigQuoteOrderQty: "1", 336 | }) 337 | return json.Marshal(expected) 338 | } 339 | 340 | actual, e := s.client.OpenOrders(&binance.OpenOrdersReq{Symbol: "LTCBTC"}) 341 | s.Require().NoError(e) 342 | s.Require().EqualValues(expected, actual) 343 | } 344 | 345 | func (s *mockedOrderTestSuite) TestAllOrders() { 346 | var expected []*binance.QueryOrder 347 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 348 | s.Require().IsType(&binance.AllOrdersReq{}, data) 349 | req := data.(*binance.AllOrdersReq) 350 | expected = append(expected, &binance.QueryOrder{ 351 | Symbol: req.Symbol, 352 | OrderID: req.OrderID, 353 | Price: "0.1", 354 | OrigQty: "1", 355 | ExecutedQty: "0", 356 | CummulativeQuoteQty: "1", 357 | Status: binance.OrderStatusNew, 358 | TimeInForce: binance.TimeInForceGTC, 359 | Type: binance.OrderTypeLimit, 360 | Side: binance.OrderSideSell, 361 | Time: int64(rand.Uint32()), 362 | UpdateTime: int64(rand.Uint32()), 363 | OrigQuoteOrderQty: "1", 364 | }) 365 | return json.Marshal(expected) 366 | } 367 | 368 | actual, e := s.client.AllOrders(&binance.AllOrdersReq{Symbol: "LTCBTC"}) 369 | s.Require().NoError(e) 370 | s.Require().EqualValues(expected, actual) 371 | } 372 | 373 | func (s *mockedOrderTestSuite) TestCancelOpenOrders() { 374 | var expected []*binance.CancelOrder 375 | s.mock.Response = func(method, endpoint string, data interface{}, sign bool, stream bool) ([]byte, error) { 376 | s.Require().IsType(&binance.CancelOpenOrdersReq{}, data) 377 | req := data.(*binance.CancelOpenOrdersReq) 378 | expected = append(expected, &binance.CancelOrder{ 379 | Symbol: req.Symbol, 380 | OrderID: int64(rand.Uint32()), 381 | Price: "0.1", 382 | OrigQty: "1", 383 | ExecutedQty: "0", 384 | CummulativeQuoteQty: "1", 385 | Status: binance.OrderStatusNew, 386 | TimeInForce: binance.TimeInForceGTC, 387 | Type: binance.OrderTypeLimit, 388 | Side: binance.OrderSideSell, 389 | }) 390 | return json.Marshal(expected) 391 | } 392 | 393 | actual, e := s.client.CancelOpenOrders(&binance.CancelOpenOrdersReq{Symbol: "LTCBTC"}) 394 | s.Require().NoError(e) 395 | s.Require().EqualValues(expected, actual) 396 | } 397 | -------------------------------------------------------------------------------- /ticker.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/segmentio/encoding/json" 5 | "github.com/valyala/fasthttp" 6 | ) 7 | 8 | // Tickers24h returns 24 hour price change statistics 9 | func (c *Client) Tickers24h(req *Tickers24hReq) ([]*TickerStatFull, error) { 10 | res, err := c.Do(fasthttp.MethodGet, EndpointTicker24h, req, false, false) 11 | if err != nil { 12 | return nil, err 13 | } 14 | var tickerStats []*TickerStatFull 15 | err = json.Unmarshal(res, &tickerStats) 16 | 17 | return tickerStats, err 18 | } 19 | 20 | // Tickers24hMini returns 24 hour price change statistics 21 | func (c *Client) Tickers24hMini(req *Tickers24hReq) ([]*TickerStatMini, error) { 22 | if req == nil { 23 | req = &Tickers24hReq{} 24 | } 25 | req.RespType = TickerRespTypeMini 26 | 27 | res, err := c.Do(fasthttp.MethodGet, EndpointTicker24h, req, false, false) 28 | if err != nil { 29 | return nil, err 30 | } 31 | var tickerStats []*TickerStatMini 32 | err = json.Unmarshal(res, &tickerStats) 33 | 34 | return tickerStats, err 35 | } 36 | 37 | // Ticker24h returns 24 hour price change statistics 38 | func (c *Client) Ticker24h(req *Ticker24hReq) (*TickerStatFull, error) { 39 | if req == nil { 40 | return nil, ErrNilRequest 41 | } 42 | if req.Symbol == "" { 43 | return nil, ErrEmptySymbol 44 | } 45 | res, err := c.Do(fasthttp.MethodGet, EndpointTicker24h, req, false, false) 46 | if err != nil { 47 | return nil, err 48 | } 49 | tickerStats := &TickerStatFull{} 50 | err = json.Unmarshal(res, tickerStats) 51 | 52 | return tickerStats, err 53 | } 54 | 55 | // Ticker24hMini returns 24 hour price change statistics 56 | func (c *Client) Ticker24hMini(req *Ticker24hReq) (*TickerStatMini, error) { 57 | if req == nil { 58 | return nil, ErrNilRequest 59 | } 60 | if req.Symbol == "" { 61 | return nil, ErrEmptySymbol 62 | } 63 | req.RespType = TickerRespTypeMini 64 | res, err := c.Do(fasthttp.MethodGet, EndpointTicker24h, req, false, false) 65 | if err != nil { 66 | return nil, err 67 | } 68 | tickerStats := &TickerStatMini{} 69 | err = json.Unmarshal(res, tickerStats) 70 | 71 | return tickerStats, err 72 | } 73 | 74 | // BookTickers returns best price/qty on the order book for all symbols 75 | func (c *Client) BookTickers(req *BookTickersReq) ([]*BookTicker, error) { 76 | res, err := c.Do(fasthttp.MethodGet, EndpointTickerBook, req, false, false) 77 | if err != nil { 78 | return nil, err 79 | } 80 | var resp []*BookTicker 81 | err = json.Unmarshal(res, &resp) 82 | 83 | return resp, err 84 | } 85 | 86 | // BookTicker returns best price/qty on the order book for all symbols 87 | func (c *Client) BookTicker(req *BookTickerReq) (*BookTicker, error) { 88 | if req == nil { 89 | return nil, ErrNilRequest 90 | } 91 | if req.Symbol == "" { 92 | return nil, ErrEmptySymbol 93 | } 94 | res, err := c.Do(fasthttp.MethodGet, EndpointTickerBook, req, false, false) 95 | if err != nil { 96 | return nil, err 97 | } 98 | resp := &BookTicker{} 99 | err = json.Unmarshal(res, &resp) 100 | 101 | return resp, err 102 | } 103 | 104 | // Tickers returns rolling window price change statistics 105 | func (c *Client) Tickers(req *TickersReq) ([]*TickerStat, error) { 106 | if req == nil { 107 | return nil, ErrNilRequest 108 | } 109 | if len(req.Symbols) == 0 { 110 | return nil, ErrEmptySymbol 111 | } 112 | if req.WindowSize != "" && !req.WindowSize.IsValid() { 113 | return nil, ErrInvalidTickerWindow 114 | } 115 | req.RespType = TickerRespTypeMini 116 | res, err := c.Do(fasthttp.MethodGet, EndpointTicker, req, false, false) 117 | if err != nil { 118 | return nil, err 119 | } 120 | var resp []*TickerStat 121 | err = json.Unmarshal(res, &resp) 122 | 123 | return resp, err 124 | } 125 | 126 | // TickersMini returns rolling window price change statistics 127 | func (c *Client) TickersMini(req *TickersReq) ([]*TickerStatMini, error) { 128 | if req != nil && req.WindowSize != "" && !req.WindowSize.IsValid() { 129 | return nil, ErrInvalidTickerWindow 130 | } 131 | res, err := c.Do(fasthttp.MethodGet, EndpointTicker, req, false, false) 132 | if err != nil { 133 | return nil, err 134 | } 135 | var resp []*TickerStatMini 136 | err = json.Unmarshal(res, &resp) 137 | 138 | return resp, err 139 | } 140 | 141 | // Ticker returns rolling window price change statistics 142 | func (c *Client) Ticker(req *TickerReq) (*TickerStat, error) { 143 | if req == nil { 144 | return nil, ErrNilRequest 145 | } 146 | if req.WindowSize != "" && !req.WindowSize.IsValid() { 147 | return nil, ErrInvalidTickerWindow 148 | } 149 | if req.Symbol == "" { 150 | return nil, ErrEmptySymbol 151 | } 152 | res, err := c.Do(fasthttp.MethodGet, EndpointTicker, req, false, false) 153 | if err != nil { 154 | return nil, err 155 | } 156 | resp := &TickerStat{} 157 | err = json.Unmarshal(res, &resp) 158 | 159 | return resp, err 160 | } 161 | 162 | // TickerMini returns rolling window price change statistics 163 | func (c *Client) TickerMini(req *TickerReq) (*TickerStatMini, error) { 164 | if req == nil { 165 | return nil, ErrNilRequest 166 | } 167 | if req.WindowSize != "" && !req.WindowSize.IsValid() { 168 | return nil, ErrInvalidTickerWindow 169 | } 170 | if req.Symbol == "" { 171 | return nil, ErrEmptySymbol 172 | } 173 | res, err := c.Do(fasthttp.MethodGet, EndpointTicker, req, false, false) 174 | if err != nil { 175 | return nil, err 176 | } 177 | resp := &TickerStatMini{} 178 | err = json.Unmarshal(res, &resp) 179 | 180 | return resp, err 181 | } 182 | -------------------------------------------------------------------------------- /ticker_test.go: -------------------------------------------------------------------------------- 1 | package binance_test 2 | 3 | import "github.com/xenking/binance-api" 4 | 5 | type tickerTestSuite struct { 6 | baseTestSuite 7 | } 8 | 9 | func (s *tickerTestSuite) TestTickers24h() { 10 | _, e := s.client.Tickers24h(nil) 11 | s.Require().NoError(e) 12 | 13 | resp, e := s.client.Tickers24h(&binance.Tickers24hReq{Symbols: []string{"LTCBTC", "ETHBTC"}}) 14 | s.Require().NoError(e) 15 | s.Require().Len(resp, 2) 16 | } 17 | 18 | func (s *tickerTestSuite) TestTickers24hMini() { 19 | _, e := s.client.Tickers24h(nil) 20 | s.Require().NoError(e) 21 | 22 | resp, e := s.client.Tickers24hMini(&binance.Tickers24hReq{Symbols: []string{"LTCBTC", "ETHBTC"}}) 23 | s.Require().NoError(e) 24 | s.Require().Len(resp, 2) 25 | } 26 | 27 | func (s *tickerTestSuite) TestTicker24h() { 28 | _, e := s.client.Ticker24h(&binance.Ticker24hReq{}) 29 | s.Require().ErrorIs(e, binance.ErrEmptySymbol) 30 | 31 | _, e = s.client.Ticker24h(&binance.Ticker24hReq{Symbol: "LTCBTC"}) 32 | s.Require().NoError(e) 33 | } 34 | 35 | func (s *tickerTestSuite) TestTicker24hMini() { 36 | _, e := s.client.Ticker24hMini(&binance.Ticker24hReq{Symbol: "LTCBTC"}) 37 | s.Require().NoError(e) 38 | } 39 | 40 | func (s *tickerTestSuite) TestBookTickers() { 41 | _, e := s.client.BookTickers(nil) 42 | s.Require().NoError(e) 43 | 44 | resp, e := s.client.BookTickers(&binance.BookTickersReq{Symbols: []string{"LTCBTC", "ETHBTC"}}) 45 | s.Require().NoError(e) 46 | s.Require().Len(resp, 2) 47 | } 48 | 49 | func (s *tickerTestSuite) TestBookTicker() { 50 | _, e := s.client.BookTicker(&binance.BookTickerReq{Symbol: "LTCBTC"}) 51 | s.Require().NoError(e) 52 | } 53 | 54 | func (s *tickerTestSuite) TestTickers() { 55 | resp, e := s.client.Tickers(&binance.TickersReq{Symbols: []string{"LTCBTC", "ETHBTC"}}) 56 | s.Require().NoError(e) 57 | s.Require().Len(resp, 2) 58 | } 59 | 60 | func (s *tickerTestSuite) TestTickersMini() { 61 | resp, e := s.client.TickersMini(&binance.TickersReq{Symbols: []string{"LTCBTC", "ETHBTC"}}) 62 | s.Require().NoError(e) 63 | s.Require().Len(resp, 2) 64 | } 65 | 66 | func (s *tickerTestSuite) TestTicker() { 67 | _, e := s.client.Ticker(&binance.TickerReq{Symbol: "LTCBTC"}) 68 | s.Require().NoError(e) 69 | } 70 | 71 | func (s *tickerTestSuite) TestTickerMini() { 72 | _, e := s.client.TickerMini(&binance.TickerReq{Symbol: "LTCBTC"}) 73 | s.Require().NoError(e) 74 | } 75 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/valyala/fasthttp" 7 | "github.com/xenking/decimal" 8 | ) 9 | 10 | // OrderType represents the order type 11 | type OrderType string 12 | 13 | const ( 14 | OrderTypeMarket OrderType = "MARKET" 15 | OrderTypeLimit OrderType = "LIMIT" 16 | OrderTypeStopLoss OrderType = "STOP_LOSS" 17 | OrderTypeStopLossLimit OrderType = "STOP_LOSS_LIMIT" 18 | OrderTypeTakeProfit OrderType = "TAKE_PROFIT" 19 | OrderTypeTakeProfitLimit OrderType = "TAKE_PROFIT_LIMIT" 20 | OrderTypeLimitMaker OrderType = "LIMIT_MAKER" 21 | ) 22 | 23 | type OrderStatus string 24 | 25 | const ( 26 | OrderStatusNew OrderStatus = "NEW" 27 | OrderStatusPartial OrderStatus = "PARTIALLY_FILLED" 28 | OrderStatusFilled OrderStatus = "FILLED" 29 | OrderStatusCanceled OrderStatus = "CANCELED" 30 | OrderStatusPending OrderStatus = "PENDING_CANCEL" 31 | OrderStatusRejected OrderStatus = "REJECTED" 32 | OrderStatusExpired OrderStatus = "EXPIRED" 33 | OrderStatusExpiredInMatch OrderStatus = "EXPIRED_IN_MATCH" 34 | ) 35 | 36 | type OrderFailure string 37 | 38 | const ( 39 | OrderFailureNone OrderFailure = "NONE" 40 | OrderFailureUnknownInstrument OrderFailure = "UNKNOWN_INSTRUMENT" 41 | OrderFailureMarketClosed OrderFailure = "MARKET_CLOSED" 42 | OrderFailurePriceExceed OrderFailure = "PRICE_QTY_EXCEED_HARD_LIMITS" 43 | OrderFailureUnknownOrder OrderFailure = "UNKNOWN_ORDER" 44 | OrderFailureDuplicate OrderFailure = "DUPLICATE_ORDER" 45 | OrderFailureUnknownAccount OrderFailure = "UNKNOWN_ACCOUNT" 46 | OrderFailureInsufficientFunds OrderFailure = "INSUFFICIENT_BALANCE" 47 | OrderFailureAccountInaactive OrderFailure = "ACCOUNT_INACTIVE" 48 | OrderFailureAccountSettle OrderFailure = "ACCOUNT_CANNOT_SETTLE" 49 | ) 50 | 51 | type OrderSide string 52 | 53 | const ( 54 | OrderSideBuy OrderSide = "BUY" 55 | OrderSideSell OrderSide = "SELL" 56 | ) 57 | 58 | type TimeInForce string 59 | 60 | const ( 61 | TimeInForceGTC TimeInForce = "GTC" // Good Till Cancel 62 | TimeInForceIOC TimeInForce = "IOC" // Immediate or Cancel 63 | TimeInForceFOK TimeInForce = "FOK" // Fill or Kill 64 | ) 65 | 66 | type OrderRespType string 67 | 68 | const ( 69 | OrderRespTypeAsk = "ASK" 70 | OrderRespTypeResult = "RESULT" 71 | OrderRespTypeFull = "FULL" 72 | ) 73 | 74 | const MinStrategyType = 1000000 75 | 76 | type OrderReq struct { 77 | Symbol string `url:"symbol"` 78 | Side OrderSide `url:"side"` 79 | Type OrderType `url:"type"` 80 | TimeInForce TimeInForce `url:"timeInForce,omitempty"` 81 | Quantity string `url:"quantity,omitempty"` 82 | QuoteQuantity string `url:"quoteOrderQty,omitempty"` 83 | Price string `url:"price,omitempty"` 84 | NewClientOrderID string `url:"newClientOrderId,omitempty"` 85 | StrategyID int `url:"strategyId,omitempty"` 86 | StrategyType int `url:"strategyType,omitempty"` // Should be more than 1000000 87 | StopPrice string `url:"stopPrice,omitempty"` 88 | TrailingDelta int64 `url:"trailingDelta,omitempty"` // Used with STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, and TAKE_PROFIT_LIMIT orders. 89 | IcebergQty string `url:"icebergQty,omitempty"` 90 | OrderRespType OrderRespType `url:"newOrderRespType,omitempty"` 91 | SelfTradePreventionMode SelfTradePreventionMode `url:"selfTradePreventionMode,omitempty"` 92 | } 93 | 94 | type OrderRespAck struct { 95 | Symbol string `json:"symbol"` 96 | OrderID int64 `json:"orderId"` 97 | OrderListID int64 `json:"orderListId"` 98 | ClientOrderID string `json:"clientOrderId"` 99 | TransactTime int64 `json:"transactTime"` 100 | } 101 | 102 | type OrderRespResult struct { 103 | Symbol string `json:"symbol"` 104 | OrderID int64 `json:"orderId"` 105 | OrderListID int `json:"orderListId"` 106 | ClientOrderID string `json:"clientOrderId"` 107 | TransactTime int64 `json:"transactTime"` 108 | Price string `json:"price"` 109 | OrigQty string `json:"origQty"` 110 | ExecutedQty string `json:"executedQty"` 111 | CummulativeQuoteQty string `json:"cummulativeQuoteQty"` 112 | Status OrderStatus `json:"status"` 113 | TimeInForce string `json:"timeInForce"` 114 | Type OrderType `json:"type"` 115 | Side OrderSide `json:"side"` 116 | WorkingTime int64 `json:"workingTime"` 117 | SelfTradePreventionMode SelfTradePreventionMode `json:"selfTradePreventionMode,omitempty"` 118 | IcebergQty string `json:"IcebergQty"` 119 | StopPrice string `json:"stopPrice"` 120 | PreventedMatchID int64 `json:"preventedMatchId,omitempty"` 121 | PreventedQuantity string `json:"preventedQuantity,omitempty"` 122 | StrategyID int `json:"strategyId,omitempty"` 123 | StrategyType int `json:"strategyType,omitempty"` 124 | TrailingDelta int `json:"trailingDelta,omitempty"` 125 | TrailingTime int64 `json:"trailingTime,omitempty"` 126 | } 127 | 128 | type OrderRespFull struct { 129 | Symbol string `json:"symbol"` 130 | OrderID int64 `json:"orderId"` 131 | OrderListID int64 `json:"orderListId"` 132 | ClientOrderID string `json:"clientOrderId"` 133 | TransactTime int64 `json:"transactTime"` 134 | Price string `json:"price"` 135 | OrigQty string `json:"origQty"` 136 | ExecutedQty string `json:"executedQty"` 137 | CummulativeQuoteQty string `json:"cummulativeQuoteQty"` 138 | Status OrderStatus `json:"status"` 139 | TimeInForce string `json:"timeInForce"` 140 | Type OrderType `json:"type"` 141 | Side OrderSide `json:"side"` 142 | WorkingTime int64 `json:"workingTime"` 143 | SelfTradePreventionMode SelfTradePreventionMode `json:"selfTradePreventionMode,omitempty"` 144 | IcebergQty string `json:"IcebergQty"` 145 | StopPrice string `json:"stopPrice"` 146 | PreventedMatchID int64 `json:"preventedMatchId,omitempty"` 147 | PreventedQuantity string `json:"preventedQuantity,omitempty"` 148 | StrategyID int `json:"strategyId,omitempty"` 149 | StrategyType int `json:"strategyType,omitempty"` 150 | TrailingDelta int `json:"trailingDelta,omitempty"` 151 | TrailingTime int64 `json:"trailingTime,omitempty"` 152 | Fills []OrderRespFullFill `json:"fills"` 153 | } 154 | 155 | type OrderRespFullFill struct { 156 | Price string `json:"price"` 157 | Qty string `json:"qty"` 158 | Commission string `json:"commission"` 159 | CommissionAsset string `json:"commissionAsset"` 160 | TradeID int64 `json:"tradeId"` 161 | } 162 | 163 | type ServerTime struct { 164 | ServerTime int64 `json:"serverTime"` 165 | } 166 | 167 | type KlineInterval string 168 | 169 | const ( 170 | KlineInterval1sec KlineInterval = "1s" 171 | KlineInterval1min KlineInterval = "1m" 172 | KlineInterval3min KlineInterval = "3m" 173 | KlineInterval5min KlineInterval = "5m" 174 | KlineInterval15min KlineInterval = "15m" 175 | KlineInterval30min KlineInterval = "30m" 176 | KlineInterval1hour KlineInterval = "1h" 177 | KlineInterval2hour KlineInterval = "2h" 178 | KlineInterval4hour KlineInterval = "4h" 179 | KlineInterval6hour KlineInterval = "6h" 180 | KlineInterval8hour KlineInterval = "8h" 181 | KlineInterval12hour KlineInterval = "12h" 182 | KlineInterval1day KlineInterval = "1d" 183 | KlineInterval3day KlineInterval = "3d" 184 | KlineInterval1week KlineInterval = "1w" 185 | KlineInterval1month KlineInterval = "1M" 186 | ) 187 | 188 | const ( 189 | DefaultDepthLimit = 100 190 | MaxDepthLimit = 5000 191 | ) 192 | 193 | // DepthReq are used to specify symbol to retrieve order book for 194 | type DepthReq struct { 195 | Symbol string `url:"symbol"` // Symbol is the symbol to fetch data for 196 | Limit int `url:"limit,omitempty"` // Limit is the number of order book items to retrieve. Default 100; Max 5000 197 | } 198 | 199 | // DepthElem represents a specific order in the order book 200 | type DepthElem struct { 201 | Price decimal.Decimal `json:"price"` 202 | Quantity decimal.Decimal `json:"quantity"` 203 | } 204 | 205 | // UnmarshalJSON unmarshal the given depth raw data and converts to depth struct 206 | func (b *DepthElem) UnmarshalJSON(data []byte) error { 207 | if b == nil { 208 | return ErrEmptyJSONResponse 209 | } 210 | 211 | if len(data) <= 4 { 212 | return nil 213 | } 214 | qty, price := 3, 0 215 | next := false 216 | for qty < len(data)-1 { 217 | if data[qty] == '"' { 218 | if next { 219 | break 220 | } 221 | next = true 222 | price = qty 223 | qty += 3 224 | 225 | continue 226 | } 227 | qty++ 228 | } 229 | if price < 3 || qty < 4 || !next { 230 | return ErrInvalidJSON 231 | } 232 | var err error 233 | b.Price, err = decimal.NewFromString(b2s(data[2:price])) 234 | if err != nil { 235 | return err 236 | } 237 | b.Quantity, err = decimal.NewFromString(b2s(data[price+3 : qty])) 238 | 239 | return err 240 | } 241 | 242 | type Depth struct { 243 | LastUpdateID int64 `json:"lastUpdateId"` 244 | Bids []DepthElem `json:"bids"` 245 | Asks []DepthElem `json:"asks"` 246 | } 247 | 248 | const ( 249 | DefaultTradesLimit = 500 250 | MaxTradesLimit = 1000 251 | ) 252 | 253 | // TradeReq are used to specify symbol to get recent trades 254 | type TradeReq struct { 255 | Symbol string `url:"symbol"` // Symbol is the symbol to fetch data for 256 | Limit int `url:"limit,omitempty"` // Limit is the maximal number of elements to receive. Default 500; Max 1000 257 | } 258 | 259 | // HistoricalTradeReq are used to specify symbol to get older trades 260 | type HistoricalTradeReq struct { 261 | Symbol string `url:"symbol"` // Symbol is the symbol to fetch data for 262 | Limit int `url:"limit,omitempty"` // Limit is the maximal number of elements to receive. Default 500; Max 1000 263 | FromID int64 `url:"fromId,omitempty"` // FromID is trade ID to fetch from. Default gets most recent trades 264 | } 265 | 266 | type Trade struct { 267 | ID int64 `json:"id"` 268 | Price string `json:"price"` 269 | Qty string `json:"qty"` 270 | QuoteQty string `json:"quoteQty"` 271 | Time int64 `json:"time"` 272 | IsBuyerMaker bool `json:"isBuyerMaker"` 273 | IsBestMatch bool `json:"isBestMatch"` 274 | } 275 | 276 | const ( 277 | DefaultKlinesLimit = 500 278 | MaxKlinesLimit = 1000 279 | ) 280 | 281 | type KlinesReq struct { 282 | Symbol string `url:"symbol"` // Symbol is the symbol to fetch data for 283 | Interval KlineInterval `url:"interval"` // Interval is the interval for each kline/candlestick 284 | Limit int `url:"limit,omitempty"` // Limit is the maximal number of elements to receive. Default 500; Max 1000 285 | StartTime int64 `url:"startTime,omitempty"` 286 | EndTime int64 `url:"endTime,omitempty"` 287 | } 288 | 289 | type Kline struct { 290 | OpenPrice decimal.Decimal 291 | HighPrice decimal.Decimal 292 | LowPrice decimal.Decimal 293 | ClosePrice decimal.Decimal 294 | Volume decimal.Decimal 295 | QuoteAssetVolume decimal.Decimal 296 | TakerBuyBaseAssetVolume decimal.Decimal 297 | TakerBuyQuoteAssetVolume decimal.Decimal 298 | OpenTime int64 299 | CloseTime int64 300 | Trades int 301 | } 302 | 303 | var ( 304 | klinesQuote = []byte(`"`) 305 | klinesDelim = []byte(`,`) 306 | ) 307 | 308 | // UnmarshalJSON unmarshal the given depth raw data and converts to depth struct 309 | func (b *Kline) UnmarshalJSON(data []byte) error { 310 | if b == nil { 311 | return ErrEmptyJSONResponse 312 | } 313 | if len(data) == 0 { 314 | return nil 315 | } 316 | s := bytes.ReplaceAll(data, klinesQuote, nil) 317 | tokens := bytes.Split(s, klinesDelim) 318 | if len(tokens) < 11 { 319 | return ErrInvalidJSON 320 | } 321 | u, err := fasthttp.ParseUint(tokens[0][1:]) 322 | if err != nil { 323 | return ErrInvalidJSON 324 | } 325 | b.OpenTime = int64(u) 326 | b.OpenPrice, err = decimal.NewFromString(b2s(tokens[1])) 327 | if err != nil { 328 | return ErrInvalidJSON 329 | } 330 | b.HighPrice, err = decimal.NewFromString(b2s(tokens[2])) 331 | if err != nil { 332 | return ErrInvalidJSON 333 | } 334 | b.LowPrice, err = decimal.NewFromString(b2s(tokens[3])) 335 | if err != nil { 336 | return ErrInvalidJSON 337 | } 338 | b.ClosePrice, err = decimal.NewFromString(b2s(tokens[4])) 339 | if err != nil { 340 | return ErrInvalidJSON 341 | } 342 | b.Volume, err = decimal.NewFromString(b2s(tokens[5])) 343 | if err != nil { 344 | return ErrInvalidJSON 345 | } 346 | u, err = fasthttp.ParseUint(tokens[6]) 347 | if err != nil { 348 | return ErrInvalidJSON 349 | } 350 | b.CloseTime = int64(u) 351 | b.QuoteAssetVolume, err = decimal.NewFromString(b2s(tokens[7])) 352 | if err != nil { 353 | return ErrInvalidJSON 354 | } 355 | b.Trades, err = fasthttp.ParseUint(tokens[8]) 356 | if err != nil { 357 | return ErrInvalidJSON 358 | } 359 | b.TakerBuyBaseAssetVolume, err = decimal.NewFromString(b2s(tokens[9])) 360 | if err != nil { 361 | return ErrInvalidJSON 362 | } 363 | b.TakerBuyQuoteAssetVolume, err = decimal.NewFromString(b2s(tokens[10])) 364 | if err != nil { 365 | return ErrInvalidJSON 366 | } 367 | 368 | return nil 369 | } 370 | 371 | type AvgPriceReq struct { 372 | Symbol string `url:"symbol"` 373 | } 374 | 375 | type AvgPrice struct { 376 | Mins int `json:"mins"` 377 | Price string `json:"price"` 378 | } 379 | 380 | type BookTickerReq struct { 381 | Symbol string `url:"symbol"` 382 | } 383 | 384 | type BookTickersReq struct { 385 | Symbols []string `url:"symbols,omitempty"` 386 | } 387 | 388 | type BookTicker struct { 389 | Symbol string `json:"symbol"` 390 | BidPrice string `json:"bidPrice"` 391 | BidQty string `json:"bidQty"` 392 | AskPrice string `json:"askPrice"` 393 | AskQty string `json:"askQty"` 394 | } 395 | 396 | type TickerReq struct { 397 | Symbol string `url:"symbol"` 398 | WindowSize TickerWindowSize `url:"windowSize,omitempty"` 399 | RespType TickerRespType `url:"type,omitempty"` 400 | } 401 | 402 | type TickersReq struct { 403 | Symbols []string `url:"symbols,omitempty"` 404 | WindowSize TickerWindowSize `url:"windowSize,omitempty"` 405 | RespType TickerRespType `url:"type,omitempty"` 406 | } 407 | 408 | // TickerWindowSize used to compute statistics will be no more than 59999ms from the requested windowSize. 409 | // Defaults to 1d if no parameter provided 410 | // Supported windowSize values: 411 | // 1m,2m....59m for minutes 412 | // 1h, 2h....23h - for hours 413 | // 1d...7d - for days 414 | // Units cannot be combined (e.g. 1d2h is not allowed) 415 | type TickerWindowSize string 416 | 417 | func (s TickerWindowSize) IsValid() bool { 418 | if len(s) < 2 || len(s) > 3 { 419 | return false 420 | } 421 | 422 | switch s[(len(s) - 1)] { 423 | case 'm': 424 | if s[0] < '1' || s[0] > '9' { 425 | return false 426 | } 427 | if len(s) < 3 { 428 | break 429 | } 430 | if s[0] > '5' || s[0] < '1' { 431 | return false 432 | } 433 | case 'h': 434 | if s[0] < '1' || s[0] > '9' { 435 | return false 436 | } 437 | if len(s) < 3 { 438 | break 439 | } 440 | if s[0] == '2' && s[1] > '3' || (s[0] != '1' && s[0] != '2') { 441 | return false 442 | } 443 | case 'd': 444 | if s[0] < '1' || s[0] > '7' { 445 | return false 446 | } 447 | default: 448 | return false 449 | } 450 | 451 | return true 452 | } 453 | 454 | // Ticker24hReq represents the request for a specified ticker 455 | type Ticker24hReq struct { 456 | Symbol string `url:"symbol"` 457 | RespType TickerRespType `url:"type,omitempty"` 458 | } 459 | 460 | // Tickers24hReq represents the request for a specified tickers 461 | type Tickers24hReq struct { 462 | Symbols []string `url:"symbols,omitempty"` 463 | RespType TickerRespType `url:"type,omitempty"` 464 | } 465 | 466 | type TickerRespType string 467 | 468 | const ( 469 | TickerRespTypeFull TickerRespType = "FULL" 470 | TickerRespTypeMini TickerRespType = "MINI" 471 | ) 472 | 473 | // TickerStatFull is the stats for a specific symbol 474 | type TickerStatFull struct { 475 | Symbol string `json:"symbol"` 476 | PriceChange string `json:"priceChange"` 477 | PriceChangePercent string `json:"priceChangePercent"` 478 | WeightedAvgPrice string `json:"weightedAvgPrice"` 479 | PrevClosePrice string `json:"prevClosePrice"` 480 | LastPrice string `json:"lastPrice"` 481 | LastQty string `json:"lastQty"` 482 | BidPrice string `json:"bidPrice"` 483 | AskPrice string `json:"askPrice"` 484 | OpenPrice string `json:"openPrice"` 485 | HighPrice string `json:"highPrice"` // HighPrice is 24hr high price 486 | LowPrice string `json:"lowPrice"` // LowPrice is 24hr low price 487 | Volume string `json:"volume"` 488 | QuoteVolume string `json:"quoteVolume"` 489 | OpenTime int64 `json:"openTime"` 490 | CloseTime int64 `json:"closeTime"` 491 | FirstID int64 `json:"firstId"` 492 | LastID int64 `json:"lastId"` 493 | Count int `json:"count"` 494 | } 495 | 496 | type TickerStatMini struct { 497 | Symbol string `json:"symbol"` 498 | OpenPrice string `json:"openPrice"` 499 | HighPrice string `json:"highPrice"` // HighPrice is 24hr high price 500 | LowPrice string `json:"lowPrice"` // LowPrice is 24hr low price 501 | LastPrice string `json:"lastPrice"` 502 | Volume string `json:"volume"` 503 | QuoteVolume string `json:"quoteVolume"` 504 | OpenTime int64 `json:"openTime"` 505 | CloseTime int64 `json:"closeTime"` 506 | FirstID int64 `json:"firstId"` 507 | LastID int64 `json:"lastId"` 508 | Count int `json:"count"` 509 | } 510 | 511 | type TickerStat struct { 512 | Symbol string `json:"symbol"` 513 | PriceChange string `json:"priceChange"` 514 | PriceChangePercent string `json:"priceChangePercent"` 515 | WeightedAvgPrice string `json:"weightedAvgPrice"` 516 | OpenPrice string `json:"openPrice"` 517 | HighPrice string `json:"highPrice"` // HighPrice is 24hr high price 518 | LowPrice string `json:"lowPrice"` // LowPrice is 24hr low price 519 | LastPrice string `json:"lastPrice"` 520 | Volume string `json:"volume"` 521 | QuoteVolume string `json:"quoteVolume"` 522 | OpenTime int64 `json:"openTime"` 523 | CloseTime int64 `json:"closeTime"` 524 | FirstID int64 `json:"firstId"` 525 | LastID int64 `json:"lastId"` 526 | Count int `json:"count"` 527 | } 528 | 529 | type TickerPriceReq struct { 530 | Symbol string `url:"symbol"` 531 | } 532 | 533 | type TickerPricesReq struct { 534 | Symbols []string `url:"symbols,omitempty"` 535 | } 536 | 537 | type SymbolPrice struct { 538 | Symbol string 539 | Price string 540 | } 541 | 542 | // QueryOrderReq represents the request for querying an order 543 | // Remark: Either OrderID or OrigOrderiD must be set 544 | type QueryOrderReq struct { 545 | Symbol string `url:"symbol"` 546 | OrderID int64 `url:"orderId,omitempty"` 547 | OrigClientOrderID string `url:"origClientOrderId,omitempty"` 548 | } 549 | 550 | type QueryOrder struct { 551 | Symbol string `json:"symbol"` 552 | OrderID int64 `json:"orderId"` 553 | OrderListID int64 `json:"orderListId"` 554 | ClientOrderID string `json:"clientOrderId"` 555 | Price string `json:"price"` 556 | OrigQty string `json:"origQty"` 557 | ExecutedQty string `json:"executedQty"` 558 | CummulativeQuoteQty string `json:"cummulativeQuoteQty"` 559 | Status OrderStatus `json:"status"` 560 | TimeInForce TimeInForce `json:"timeInForce"` 561 | Type OrderType `json:"type"` 562 | Side OrderSide `json:"side"` 563 | Time int64 `json:"time"` 564 | UpdateTime int64 `json:"updateTime"` 565 | IsWorking bool `json:"isWorking"` 566 | WorkingTime int64 `json:"workingTime"` 567 | OrigQuoteOrderQty string `json:"origQuoteOrderQty"` 568 | SelfTradePreventionMode SelfTradePreventionMode `json:"selfTradePreventionMode,omitempty"` 569 | IcebergQty string `json:"IcebergQty"` 570 | StopPrice string `json:"stopPrice"` 571 | PreventedMatchID int64 `json:"preventedMatchId,omitempty"` 572 | PreventedQuantity string `json:"preventedQuantity,omitempty"` 573 | StrategyID int `json:"strategyId,omitempty"` 574 | StrategyType int `json:"strategyType,omitempty"` 575 | TrailingDelta int `json:"trailingDelta,omitempty"` 576 | TrailingTime int64 `json:"trailingTime,omitempty"` 577 | } 578 | 579 | // Remark: Either OrderID or OrigOrderID must be set 580 | type CancelOrderReq struct { 581 | Symbol string `url:"symbol"` 582 | OrderID int64 `url:"orderId,omitempty"` 583 | OrigClientOrderID string `url:"origClientOrderId,omitempty"` 584 | NewClientOrderID string `url:"newClientOrderId,omitempty"` 585 | CancelRestrictions CancelRestriction `url:"cancelRestrictions,omitempty"` 586 | } 587 | 588 | type CancelRestriction string 589 | 590 | const ( 591 | CancelRestrictionOnlyNew CancelRestriction = "ONLY_NEW" 592 | CancelRestrictionOnlyPartiallyFilled CancelRestriction = "ONLY_PARTIALLY_FILLED" 593 | ) 594 | 595 | type CancelOrder struct { 596 | Symbol string `json:"symbol"` 597 | OrigClientOrderID string `json:"origClientOrderId"` 598 | OrderID int64 `json:"orderId"` 599 | OrderListID int64 `json:"orderListId"` 600 | ClientOrderID string `json:"clientOrderId"` 601 | Price string `json:"price"` 602 | OrigQty string `json:"origQty"` 603 | ExecutedQty string `json:"executedQty"` 604 | CummulativeQuoteQty string `json:"cummulativeQuoteQty"` 605 | Status OrderStatus `json:"status"` 606 | TimeInForce TimeInForce `json:"timeInForce"` 607 | Type OrderType `json:"type"` 608 | Side OrderSide `json:"side"` 609 | SelfTradePreventionMode SelfTradePreventionMode `json:"selfTradePreventionMode,omitempty"` 610 | IcebergQty string `json:"IcebergQty"` 611 | StopPrice string `json:"stopPrice"` 612 | PreventedMatchID int64 `json:"preventedMatchId,omitempty"` 613 | PreventedQuantity string `json:"preventedQuantity,omitempty"` 614 | StrategyID int `json:"strategyId,omitempty"` 615 | StrategyType int `json:"strategyType,omitempty"` 616 | TrailingDelta int `json:"trailingDelta,omitempty"` 617 | TrailingTime int64 `json:"trailingTime,omitempty"` 618 | } 619 | 620 | type CancelOCOOrderType struct { 621 | ContingencyType ContingencyType `json:"contingencyType"` 622 | } 623 | 624 | type CancelOCOOrder struct{} 625 | 626 | type CancelReplaceResult string 627 | 628 | const ( 629 | CancelReplaceResultSuccess CancelReplaceResult = "SUCCESS" 630 | CancelReplaceResultFailure CancelReplaceResult = "FAILURE" 631 | CancelReplaceResultNotAttempted CancelReplaceResult = "NOT_ATTEMPTED" 632 | ) 633 | 634 | type CancelReplaceMode string 635 | 636 | const ( 637 | CancelReplaceModeStopOnFailure CancelReplaceMode = "STOP_ON_FAILURE" 638 | CancelReplaceModeAllowFailure CancelReplaceMode = "ALLOW_FAILURE" 639 | ) 640 | 641 | // Note: Either CancelOrderID or CancelOrigClientOrderID must be set 642 | type CancelReplaceOrderReq struct { 643 | OrderReq 644 | CancelRestrictions CancelRestriction `url:"cancelRestrictions,omitempty"` 645 | CancelReplaceMode CancelReplaceMode `url:"cancelReplaceMode"` 646 | CancelOrderID int64 `url:"cancelOrderId,omitempty"` 647 | CancelOrigClientOrderID string `url:"cancelOrigClientOrderId,omitempty"` 648 | CancelNewClientOrderID string `url:"cancelNewClientOrderId,omitempty"` 649 | } 650 | 651 | type CancelReplaceOrder struct { 652 | CancelResponse CancelOrder `json:"cancelResponse"` 653 | NewOrderResponse *OrderRespFull `json:"newOrderResponse,omitempty"` 654 | CancelResult CancelReplaceResult `json:"cancelResult"` 655 | NewOrderResult CancelReplaceResult `json:"newOrderResult"` 656 | } 657 | 658 | type OpenOrdersReq struct { 659 | Symbol string `url:"symbol,omitempty"` 660 | } 661 | 662 | type CancelOpenOrdersReq struct { 663 | Symbol string `url:"symbol"` 664 | } 665 | 666 | const ( 667 | DefaultOrderLimit = 500 668 | MaxOrderLimit = 1000 669 | ) 670 | 671 | // AllOrdersReq represents the request used for querying orders of the given symbol 672 | // Remark: If orderId is set, it will get orders >= that orderId. Otherwise most recent orders are returned 673 | type AllOrdersReq struct { 674 | Symbol string `url:"symbol"` // Symbol is the symbol to fetch orders for 675 | OrderID int64 `url:"orderId,omitempty"` // OrderID, if set, will filter all recent orders newer from the given ID 676 | Limit int `url:"limit,omitempty"` // Limit is the maximal number of elements to receive. Default 500; Max 1000 677 | StartTime int64 `url:"startTime,omitempty"` 678 | EndTime int64 `url:"endTime,omitempty"` 679 | } 680 | 681 | type OCOReq struct { 682 | Symbol string `url:"symbol"` 683 | Side OrderSide `url:"side"` 684 | Type OrderType `url:"type"` 685 | Quantity string `url:"quantity"` 686 | Price string `url:"price"` 687 | StopPrice string `url:"stopPrice"` 688 | ListClientOrderID string `url:"listClientOrderId,omitempty"` 689 | LimitClientOrderID string `url:"limitClientOrderId,omitempty"` 690 | LimitStrategyID int `url:"limitStrategyId,omitempty"` // Should be more than 1000000 691 | LimitStrategyType int `url:"limitStrategyType,omitempty"` 692 | LimitIcebergQty string `url:"limitIcebergQty,omitempty"` 693 | TrailingDelta int64 `url:"trailingDelta,omitempty"` 694 | StopClientOrderID string `url:"stopClientOrderId,omitempty"` 695 | StopStrategyID int `url:"stopStrategyId,omitempty"` // Should be more than 1000000 696 | StopStrategyType int `url:"stopStrategyType,omitempty"` 697 | StopLimitPrice string `url:"stopLimitPrice,omitempty"` 698 | StopIcebergQty string `url:"stopIcebergQty,omitempty"` 699 | StopLimitTimeInForce TimeInForce `url:"stopLimitTimeInForce,omitempty"` 700 | OrderRespType OrderRespType `url:"newOrderRespType,omitempty"` 701 | SelfTradePreventionMode SelfTradePreventionMode `url:"selfTradePreventionMode,omitempty"` 702 | } 703 | 704 | type OCOOrder struct { 705 | Symbol string `json:"symbol"` 706 | OrderListID int64 `json:"orderListId"` 707 | ContingencyType ContingencyType `json:"contingencyType"` 708 | ListStatusType OCOStatus `json:"listStatusType"` 709 | ListOrderStatus OCOOrderStatus `json:"listOrderStatus"` 710 | ListClientOrderID string `json:"listClientOrderId"` 711 | TransactionTime int64 `json:"transactionTime"` 712 | Orders []OCOOrderStatusResp `json:"orders"` 713 | OrderReports []OrderRespFull `json:"orderReports"` 714 | } 715 | 716 | type OCOOrderStatusResp struct { 717 | Symbol string `json:"symbol"` 718 | OrderID int `json:"orderId"` 719 | ClientOrderID string `json:"clientOrderId"` 720 | } 721 | 722 | // Remark: Either OrderListID or ListClientOrderID must be set 723 | type CancelOCOReq struct { 724 | Symbol string `url:"symbol"` 725 | OrderListID int64 `url:"orderListId,omitempty"` 726 | ListClientOrderID string `url:"listClientOrderId,omitempty"` 727 | NewClientOrderID string `url:"newClientOrderId,omitempty"` 728 | } 729 | 730 | // QueryOCOReq represents the request for querying an OCO order 731 | // Remark: Either OrderID or OrigOrderiD must be set 732 | type QueryOCOReq struct { 733 | OrderListID int64 `url:"orderListId,omitempty"` 734 | ListClientOrderID string `url:"listClientOrderId,omitempty"` 735 | } 736 | 737 | // AllOCOReq represents the request used for querying OCO orders 738 | type AllOCOReq struct { 739 | FromID int64 `url:"fromId,omitempty"` // If supplied, neither startTime or endTime can be provided 740 | Limit int `url:"limit,omitempty"` // Limit is the maximal number of elements to receive. Default 500; Max 1000 741 | StartTime int64 `url:"startTime,omitempty"` 742 | EndTime int64 `url:"endTime,omitempty"` 743 | } 744 | 745 | type Balance struct { 746 | Asset string `json:"asset"` 747 | Free string `json:"free"` 748 | Locked string `json:"locked"` 749 | } 750 | 751 | type PermissionType string 752 | 753 | const ( 754 | PermissionTypeSpot PermissionType = "SPOT" 755 | PermissionTypeMargin PermissionType = "MARGIN" 756 | PermissionTypeLeveraged PermissionType = "LEVERAGED" 757 | PermissionTypeGRP2 PermissionType = "TRD_GRP_002" 758 | PermissionTypeGRP3 PermissionType = "TRD_GRP_003" 759 | PermissionTypeGRP4 PermissionType = "TRD_GRP_004" 760 | PermissionTypeGRP5 PermissionType = "TRD_GRP_005" 761 | PermissionTypeGRP6 PermissionType = "TRD_GRP_006" 762 | PermissionTypeGRP7 PermissionType = "TRD_GRP_007" 763 | ) 764 | 765 | type AccountInfo struct { 766 | MakerCommission int `json:"makerCommission"` 767 | TakerCommission int `json:"takerCommission"` 768 | BuyerCommission int `json:"buyerCommission"` 769 | SellerCommission int `json:"sellerCommission"` 770 | CanTrade bool `json:"canTrade"` 771 | CanWithdraw bool `json:"canWithdraw"` 772 | CanDeposit bool `json:"canDeposit"` 773 | AccountType PermissionType `json:"accountType"` 774 | Balances []*Balance `json:"balances"` 775 | Permissions []PermissionType `json:"permissions"` 776 | } 777 | 778 | const MaxAccountTradesLimit = 500 779 | 780 | type AccountTradesReq struct { 781 | Symbol string `url:"symbol"` 782 | OrderID string `url:"orderId,omitempty"` // OrderID can only be used in combination with symbol 783 | Limit int `url:"limit,omitempty"` // Limit is the maximal number of elements to receive. Default 500; Max 1000 784 | FromID int64 `url:"fromId,omitempty"` // FromID is trade ID to fetch from. Default gets most recent trades 785 | StartTime int64 `url:"startTime,omitempty"` 786 | EndTime int64 `url:"endTime,omitempty"` 787 | } 788 | 789 | type AccountTrade struct { 790 | ID int64 `json:"id"` 791 | OrderID int64 `json:"orderId"` 792 | OrderListID int64 `json:"orderListId"` 793 | Symbol string `json:"symbol"` 794 | Price string `json:"price"` 795 | Qty string `json:"qty"` 796 | QuoteQty string `json:"quoteQty"` 797 | Commission string `json:"commission"` 798 | CommissionAsset string `json:"commissionAsset"` 799 | Time int64 `json:"time"` 800 | Buyer bool `json:"isBuyer"` 801 | Maker bool `json:"isMaker"` 802 | BestMatch bool `json:"isBestMatch"` 803 | } 804 | 805 | type DataStream struct { 806 | ListenKey string `json:"listenKey" url:"listenKey"` 807 | } 808 | 809 | type MyPreventedMatchesReq struct { 810 | Symbol string `url:"symbol"` 811 | PreventedMatchID int64 `url:"preventedMatchId,omitempty"` 812 | FromID int64 `url:"fromId,omitempty"` // FromID is trade ID to fetch from. Default gets most recent trades 813 | StartTime int64 `url:"startTime,omitempty"` 814 | EndTime int64 `url:"endTime,omitempty"` 815 | } 816 | 817 | type AggregatedTradeReq struct { 818 | Symbol string `url:"symbol"` // Symbol is the symbol to fetch data for 819 | FromID int64 `url:"fromId"` // FromID to get aggregate trades from INCLUSIVE. 820 | StartTime int64 `url:"startTime,omitempty"` // StartTime timestamp in ms to get aggregate trades from INCLUSIVE. 821 | EndTime int64 `url:"endTime,omitempty"` // EndTime timestamp in ms to get aggregate trades until INCLUSIVE. 822 | Limit int `url:"limit,omitempty"` // Limit is the maximal number of elements to receive. Default 500; Max 1000 823 | } 824 | 825 | type AggregatedTrade struct { 826 | TradeID int64 `json:"a"` // TradeID is the aggregate trade ID 827 | Price string `json:"p"` // Price is the trade price 828 | Quantity string `json:"q"` // Quantity is the trade quantity 829 | FirstTradeID int64 `json:"f"` // FirstTradeID is the first trade ID 830 | LastTradeID int64 `json:"l"` // LastTradeID is the last trade ID 831 | Timestamp int64 `json:"T"` // Timestamp is the trade timestamp 832 | Maker bool `json:"m"` // Maker indicates if the buyer is the maker 833 | BestMatch bool `json:"M"` // BestMatch indicates if the trade was at the best price match 834 | } 835 | 836 | type ExchangeInfoReq struct { 837 | Symbol string `url:"symbol,omitempty"` 838 | Symbols []string `url:"symbols,omitempty"` 839 | Permissions []PermissionType `url:"permissions,omitempty"` 840 | } 841 | 842 | type ExchangeInfo struct { 843 | Timezone string `json:"timezone"` 844 | ServerTime int64 `json:"serverTime"` 845 | RateLimits []*RateLimit `json:"rateLimits"` 846 | ExchangeFilters []*ExchangeFilter `json:"exchangeFilters"` 847 | Symbols []*SymbolInfo `json:"symbols"` 848 | } 849 | 850 | type RateLimitType string 851 | 852 | const ( 853 | RateLimitTypeRequestWeight RateLimitType = "REQUEST_WEIGHT" 854 | RateLimitTypeOrders RateLimitType = "ORDERS" 855 | RateLimitTypeRawRequests RateLimitType = "RAW_REQUESTS" 856 | ) 857 | 858 | type RateLimitInterval string 859 | 860 | const ( 861 | RateLimitIntervalSecond RateLimitInterval = "SECOND" 862 | RateLimitIntervalHour RateLimitInterval = "HOUR" 863 | RateLimitIntervalMinute RateLimitInterval = "MINUTE" 864 | RateLimitIntervalDay RateLimitInterval = "DAY" 865 | ) 866 | 867 | var RateLimitIntervalLetter = map[byte]RateLimitInterval{ 868 | 's': RateLimitIntervalSecond, 869 | 'S': RateLimitIntervalSecond, 870 | 'h': RateLimitIntervalHour, 871 | 'H': RateLimitIntervalHour, 872 | 'm': RateLimitIntervalMinute, 873 | 'M': RateLimitIntervalMinute, 874 | 'd': RateLimitIntervalDay, 875 | 'D': RateLimitIntervalDay, 876 | } 877 | 878 | type RateLimit struct { 879 | Type RateLimitType `json:"rateLimitType"` 880 | Interval RateLimitInterval `json:"interval"` 881 | IntervalNum int `json:"intervalNum"` 882 | Limit int `json:"limit"` 883 | Count int `json:"count,omitempty"` 884 | } 885 | 886 | type ExchangeFilterType string 887 | 888 | const ( 889 | ExchangeFilterTypeMaxNumOrders ExchangeFilterType = "EXCHANGE_MAX_NUM_ORDERS" 890 | ExchangeFilterTypeMaxAlgoOrders ExchangeFilterType = "EXCHANGE_MAX_ALGO_ORDERS" 891 | ) 892 | 893 | type ExchangeFilter struct { 894 | Type ExchangeFilterType `json:"filterType"` 895 | 896 | // EXCHANGE_MAX_NUM_ORDERS parameters 897 | MaxNumOrders int `json:"maxNumOrders"` 898 | 899 | // EXCHANGE_MAX_ALGO_ORDERS parameters 900 | MaxNumAlgoOrders int `json:"maxNumAlgoOrders"` 901 | } 902 | 903 | type SymbolInfo struct { 904 | Symbol string `json:"symbol"` 905 | Status SymbolStatus `json:"status"` 906 | BaseAsset string `json:"baseAsset"` 907 | BaseAssetPrecision int `json:"baseAssetPrecision"` 908 | QuoteAsset string `json:"quoteAsset"` 909 | QuotePrecision int `json:"quotePrecision"` 910 | QuoteAssetPrecision int `json:"quoteAssetPrecision"` 911 | BaseCommissionPrecision int `json:"baseCommissionPrecision"` 912 | QuoteCommissionPrecision int `json:"quoteCommissionPrecision"` 913 | OrderTypes []OrderType `json:"orderTypes"` 914 | IcebergAllowed bool `json:"icebergAllowed"` 915 | OCOAllowed bool `json:"ocoAllowed"` 916 | QuoteOrderQtyMarketAllowed bool `json:"quoteOrderQtyMarketAllowed"` 917 | AllowTrailingStop bool `json:"allowTrailingStop"` 918 | CancelReplaceAllowed bool `json:"cancelReplaceAllowed"` 919 | IsSpotTradingAllowed bool `json:"isSpotTradingAllowed"` 920 | IsMarginTradingAllowed bool `json:"isMarginTradingAllowed"` 921 | Filters []SymbolInfoFilter `json:"filters"` 922 | Permissions []PermissionType `json:"permissions"` 923 | DefaultSelfTradePreventionMode SelfTradePreventionMode `json:"defaultSelfTradePreventionMode"` 924 | AllowedSelfTradePreventionModes []SelfTradePreventionMode `json:"allowedSelfTradePreventionModes"` 925 | } 926 | 927 | type SymbolStatus string 928 | 929 | const ( 930 | SymbolStatusPreTrading SymbolStatus = "PRE_TRADING" 931 | SymbolStatusTrading SymbolStatus = "TRADING" 932 | SymbolStatusPostTrading SymbolStatus = "POST_TRADING" 933 | SymbolStatusEndOfDay SymbolStatus = "END_OF_DAY" 934 | SymbolStatusHalt SymbolStatus = "HALT" 935 | SymbolStatusAuctionMatch SymbolStatus = "AUCTION_MATCH" 936 | SymbolStatusBreak SymbolStatus = "BREAK" 937 | ) 938 | 939 | type FilterType string 940 | 941 | const ( 942 | FilterTypePrice FilterType = "PRICE_FILTER" 943 | FilterTypePercentPrice FilterType = "PERCENT_PRICE" 944 | FilterTypeLotSize FilterType = "LOT_SIZE" 945 | FilterTypeMinNotional FilterType = "MIN_NOTIONAL" 946 | FilterTypeIcebergParts FilterType = "ICEBERG_PARTS" 947 | FilterTypeMarketLotSize FilterType = "MARKET_LOT_SIZE" 948 | FilterTypeMaxNumOrders FilterType = "MAX_NUM_ORDERS" 949 | FilterTypeMaxNumAlgoOrders FilterType = "MAX_NUM_ALGO_ORDERS" 950 | FilterTypeMaxNumIcebergOrders FilterType = "MAX_NUM_ICEBERG_ORDERS" 951 | FilterTypeMaxPosition FilterType = "MAX_POSITION" 952 | ) 953 | 954 | type SymbolInfoFilter struct { 955 | Type FilterType `json:"filterType"` 956 | 957 | // PRICE_FILTER parameters 958 | MinPrice string `json:"minPrice"` 959 | MaxPrice string `json:"maxPrice"` 960 | TickSize string `json:"tickSize"` 961 | 962 | // PERCENT_PRICE parameters 963 | MultiplierUp string `json:"multiplierUp"` 964 | MultiplierDown string `json:"multiplierDown"` 965 | AvgPriceMins int `json:"avgPriceMins"` 966 | 967 | // LOT_SIZE or MARKET_LOT_SIZE parameters 968 | MinQty string `json:"minQty"` 969 | MaxQty string `json:"maxQty"` 970 | StepSize string `json:"stepSize"` 971 | 972 | // MIN_NOTIONAL parameter 973 | MinNotional string `json:"minNotional"` 974 | ApplyToMarket bool `json:"applyToMarket"` 975 | 976 | // ICEBERG_PARTS parameter 977 | IcebergLimit int `json:"limit"` 978 | 979 | // TRAILING_DELTA parameter 980 | MinTrailingAboveDelta int `json:"minTrailingAboveDelta"` 981 | MaxTrailingAboveDelta int `json:"maxTrailingAboveDelta"` 982 | MinTrailingBelowDelta int `json:"minTrailingBelowDelta"` 983 | MaxTrailingBelowDelta int `json:"maxTrailingBelowDelta"` 984 | 985 | // MAX_NUM_ORDERS parameter 986 | MaxNumOrders int `json:"maxNumOrders"` 987 | 988 | // MAX_NUM_ALGO_ORDERS parameter 989 | MaxNumAlgoOrders int `json:"maxNumAlgoOrders"` 990 | 991 | // MAX_NUM_ICEBERG_ORDERS parameter 992 | MaxNumIcebergOrders int `json:"maxNumIcebergOrders"` 993 | 994 | // MAX_POSITION parameter 995 | MaxPosition string `json:"maxPosition"` 996 | } 997 | 998 | type OCOStatus string 999 | 1000 | const ( 1001 | OCOStatusResponse OCOStatus = "RESPONSE" 1002 | OCOStatusExecStarted OCOStatus = "EXEC_STARTED" 1003 | OCOStatusAllDone OCOStatus = "ALL_DONE" 1004 | ) 1005 | 1006 | type OCOOrderStatus string 1007 | 1008 | const ( 1009 | OCOOrderStatusExecuting OCOOrderStatus = "EXECUTING" 1010 | OCOOrderStatusAllDone OCOOrderStatus = "ALL_DONE" 1011 | OCOOrderStatusReject OCOOrderStatus = "REJECT" 1012 | ) 1013 | 1014 | type ContingencyType string 1015 | 1016 | const ( 1017 | ContingencyTypeOCO ContingencyType = "OCO" 1018 | ) 1019 | 1020 | type SelfTradePreventionMode string 1021 | 1022 | const ( 1023 | SelfTradePreventionModeNone SelfTradePreventionMode = "NONE" 1024 | SelfTradePreventionModeExpireTaker SelfTradePreventionMode = "EXPIRE_TAKER" 1025 | SelfTradePreventionModeExpireMaker SelfTradePreventionMode = "EXPIRE_MAKER" 1026 | SelfTradePreventionModeExpireBoth SelfTradePreventionMode = "EXPIRE_BOTH" 1027 | ) 1028 | -------------------------------------------------------------------------------- /ws/account.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import "github.com/segmentio/encoding/json" 4 | 5 | // AccountInfo is a wrapper for account info websocket 6 | type AccountInfo struct { 7 | Conn 8 | } 9 | 10 | // Read reads a account info update message from account info websocket 11 | // Remark: The websocket is used to update two different structs, which both are flat, hence every call to this function 12 | // will return either one of the types initialized and the other one will be set to nil 13 | func (i *AccountInfo) Read() (AccountUpdateEventType, interface{}, error) { 14 | payload, err := i.Conn.ReadRaw() 15 | if err != nil { 16 | return AccountUpdateEventTypeUnknown, nil, err 17 | } 18 | et := UpdateEventType{} 19 | err = json.Unmarshal(payload, &et) 20 | if err != nil { 21 | return AccountUpdateEventTypeUnknown, nil, err 22 | } 23 | 24 | var resp interface{} 25 | switch et.EventType { 26 | case AccountUpdateEventTypeOutboundAccountPosition: 27 | resp = &AccountUpdateEvent{} 28 | case AccountUpdateEventTypeBalanceUpdate: 29 | resp = &BalanceUpdateEvent{} 30 | case AccountUpdateEventTypeOrderReport: 31 | resp = &OrderUpdateEvent{} 32 | case AccountUpdateEventTypeOCOReport: 33 | resp = &OCOOrderUpdateEvent{} 34 | default: 35 | buf := make([]byte, len(payload)) 36 | copy(buf, payload) 37 | return et.EventType, buf, nil 38 | } 39 | err = json.Unmarshal(payload, resp) 40 | 41 | return et.EventType, resp, err 42 | } 43 | 44 | func (i *AccountInfo) OrdersStream() <-chan *OrderUpdateEvent { 45 | updates := make(chan *OrderUpdateEvent) 46 | 47 | go i.NewStreamRaw(func(payload []byte, err error) error { 48 | if err != nil { 49 | close(updates) 50 | return err 51 | } 52 | 53 | var event UpdateEventType 54 | err = event.UnmarshalJSON(payload) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if event.EventType != AccountUpdateEventTypeOrderReport { 60 | return nil 61 | } 62 | 63 | u := &OrderUpdateEvent{} 64 | err = json.Unmarshal(payload, u) 65 | if err != nil { 66 | return err 67 | } 68 | updates <- u 69 | 70 | return nil 71 | }) 72 | 73 | return updates 74 | } 75 | 76 | func (i *AccountInfo) OCOOrdersStream() <-chan *OCOOrderUpdateEvent { 77 | updates := make(chan *OCOOrderUpdateEvent) 78 | 79 | go i.NewStreamRaw(func(payload []byte, err error) error { 80 | if err != nil { 81 | close(updates) 82 | return err 83 | } 84 | 85 | var event UpdateEventType 86 | err = event.UnmarshalJSON(payload) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | if event.EventType != AccountUpdateEventTypeOCOReport { 92 | return nil 93 | } 94 | 95 | u := &OCOOrderUpdateEvent{} 96 | err = json.Unmarshal(payload, u) 97 | if err != nil { 98 | return err 99 | } 100 | updates <- u 101 | 102 | return nil 103 | }) 104 | 105 | return updates 106 | } 107 | 108 | func (i *AccountInfo) BalancesStream() <-chan *BalanceUpdateEvent { 109 | updates := make(chan *BalanceUpdateEvent) 110 | 111 | go i.NewStreamRaw(func(payload []byte, err error) error { 112 | if err != nil { 113 | close(updates) 114 | return err 115 | } 116 | 117 | var event UpdateEventType 118 | err = event.UnmarshalJSON(payload) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | if event.EventType != AccountUpdateEventTypeBalanceUpdate { 124 | return nil 125 | } 126 | 127 | u := &BalanceUpdateEvent{} 128 | err = json.Unmarshal(payload, u) 129 | if err != nil { 130 | return err 131 | } 132 | updates <- u 133 | 134 | return nil 135 | }) 136 | 137 | return updates 138 | } 139 | 140 | func (i *AccountInfo) AccountStream() <-chan *AccountUpdateEvent { 141 | updates := make(chan *AccountUpdateEvent) 142 | 143 | go i.NewStreamRaw(func(payload []byte, err error) error { 144 | if err != nil { 145 | close(updates) 146 | return err 147 | } 148 | 149 | var event UpdateEventType 150 | err = event.UnmarshalJSON(payload) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | if event.EventType != AccountUpdateEventTypeOutboundAccountPosition { 156 | return nil 157 | } 158 | 159 | u := &AccountUpdateEvent{} 160 | err = json.Unmarshal(payload, u) 161 | if err != nil { 162 | return err 163 | } 164 | updates <- u 165 | 166 | return nil 167 | }) 168 | 169 | return updates 170 | } 171 | -------------------------------------------------------------------------------- /ws/account_test.go: -------------------------------------------------------------------------------- 1 | package ws_test 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | websocket "github.com/gobwas/ws" 11 | "github.com/gobwas/ws/wsutil" 12 | "github.com/segmentio/encoding/json" 13 | 14 | "github.com/xenking/binance-api" 15 | "github.com/xenking/binance-api/ws" 16 | ) 17 | 18 | type mockedClient struct { 19 | Callback func(method, endpoint string, data interface{}, sign, stream bool) ([]byte, error) 20 | } 21 | 22 | func (m *mockedClient) Do(method, endpoint string, data interface{}, sign, stream bool) ([]byte, error) { 23 | return m.Callback(method, endpoint, data, sign, stream) 24 | } 25 | 26 | func (m *mockedClient) UsedWeight() map[string]int64 { 27 | panic("not used") 28 | } 29 | 30 | func (m *mockedClient) OrderCount() map[string]int64 { 31 | panic("not used") 32 | } 33 | 34 | func (m *mockedClient) RetryAfter() int64 { 35 | panic("not used") 36 | } 37 | 38 | func (m *mockedClient) SetWindow(_ int) { 39 | panic("not used") 40 | } 41 | 42 | type accountTestSuite struct { 43 | baseTestSuite 44 | api *binance.Client 45 | mock *mockedClient 46 | listener net.Listener 47 | listenerDone chan struct{} 48 | expected chan interface{} 49 | } 50 | 51 | func (s *accountTestSuite) SetupSuite() { 52 | s.mock = &mockedClient{} 53 | s.api = binance.NewCustomClient(s.mock) 54 | s.ws = ws.NewCustomClient("ws://localhost:9844/", nil) 55 | 56 | var err error 57 | s.listener, err = net.Listen("tcp", ":9844") 58 | s.Require().NoError(err) 59 | 60 | s.listenerDone = make(chan struct{}, 1) 61 | go func() { 62 | http.Serve(s.listener, nil) //nolint:errcheck // don't care about error here 63 | s.listenerDone <- struct{}{} 64 | }() 65 | 66 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 67 | conn, _, _, err := websocket.UpgradeHTTP(r, w) 68 | if err != nil { 69 | http.Error(w, err.Error(), http.StatusBadRequest) 70 | return 71 | } 72 | 73 | go func() { 74 | defer conn.Close() 75 | 76 | writer := wsutil.NewWriter(conn, websocket.StateServerSide, websocket.OpText) 77 | enc := json.NewEncoder(writer) 78 | for ex := range s.expected { 79 | marshallErr := enc.Encode(ex) 80 | s.Require().NoError(marshallErr) 81 | flushErr := writer.Flush() 82 | s.Require().NoError(flushErr) 83 | } 84 | }() 85 | }) 86 | 87 | http.HandleFunc("/stream-key", handler) 88 | } 89 | 90 | func (s *accountTestSuite) TearDownSuite() { 91 | err := s.listener.Close() 92 | s.Require().NoError(err) 93 | 94 | select { 95 | case <-s.listenerDone: 96 | case <-time.After(time.Second * 5): 97 | s.Fail("timeout") 98 | } 99 | } 100 | 101 | func (s *accountTestSuite) SetupTest() { 102 | s.mock.Callback = func(method, endpoint string, data interface{}, sign, stream bool) ([]byte, error) { 103 | s.Require().Nil(data) 104 | return json.Marshal(&binance.DataStream{ 105 | ListenKey: "stream-key", 106 | }) 107 | } 108 | s.expected = make(chan interface{}) 109 | } 110 | 111 | func (s *accountTestSuite) TestAccountInfo_Read() { 112 | expected := []interface{}{ 113 | &ws.BalanceUpdateEvent{ 114 | EventType: ws.AccountUpdateEventTypeBalanceUpdate, 115 | Time: int64(rand.Uint32()), 116 | Asset: "BTC", 117 | BalanceDelta: "1", 118 | }, 119 | &ws.AccountUpdateEvent{ 120 | Balances: []ws.AccountBalance{ 121 | { 122 | Asset: "ETH", 123 | Free: "1", 124 | Locked: "0.5", 125 | }, 126 | }, 127 | EventType: ws.AccountUpdateEventTypeOutboundAccountPosition, 128 | Time: int64(rand.Uint32()), 129 | LastUpdate: int64(rand.Uint32()), 130 | }, 131 | &ws.OrderUpdateEvent{ 132 | EventType: ws.AccountUpdateEventTypeOrderReport, 133 | Symbol: "ETHBTC", 134 | Side: "BUY", 135 | OrderType: "LIMIT", 136 | TimeInForce: "GTC", 137 | OrigQty: "1", 138 | Price: "3400", 139 | Status: "FILLED", 140 | FilledQty: "1", 141 | TotalFilledQty: "1", 142 | FilledPrice: "3400", 143 | Commission: "0.00001", 144 | CommissionAsset: "BTC", 145 | Time: int64(rand.Uint32()), 146 | TradeTime: int64(rand.Uint32()), 147 | OrderCreatedTime: int64(rand.Uint32()), 148 | TradeID: rand.Int63(), 149 | OrderID: int64(rand.Uint32()), 150 | }, 151 | &ws.OCOOrderUpdateEvent{ 152 | EventType: ws.AccountUpdateEventTypeOCOReport, 153 | Orders: []ws.OCOOrderUpdateEventOrder{ 154 | { 155 | Symbol: "ETH", 156 | OrderID: int64(rand.Uint32()), 157 | }, 158 | { 159 | Symbol: "BTC", 160 | OrderID: int64(rand.Uint32()), 161 | }, 162 | }, 163 | Symbol: "ETHBTC", 164 | ContingencyType: "OCO", 165 | OCOStatus: "EXEC_STARTED", 166 | OrderStatus: "EXECUTING", 167 | OCORejectReason: "NONE", 168 | TransactTime: int64(rand.Uint32()), 169 | OrderListID: rand.Int63(), 170 | Time: int64(rand.Uint32()), 171 | }, 172 | } 173 | 174 | key, err := s.api.DataStream() 175 | s.Require().NoError(err) 176 | 177 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 178 | defer cancel() 179 | 180 | info, err := s.ws.AccountInfo(ctx, key) 181 | s.Require().NoError(err) 182 | 183 | for _, ex := range expected { 184 | s.expected <- ex 185 | _, actual, err := info.Read() 186 | s.Require().NoError(err) 187 | s.Require().EqualValues(ex, actual) 188 | } 189 | close(s.expected) 190 | 191 | s.mock.Callback = func(method, endpoint string, data interface{}, sign, stream bool) ([]byte, error) { 192 | s.Require().IsType(binance.DataStream{}, data) 193 | return nil, nil 194 | } 195 | 196 | err = s.api.DataStreamClose(key) 197 | s.Require().NoError(err) 198 | err = info.Close() 199 | s.Require().NoError(err) 200 | } 201 | 202 | func (s *accountTestSuite) TestAccountInfo_BalancesStream() { 203 | expected := []interface{}{ 204 | &ws.BalanceUpdateEvent{ 205 | EventType: ws.AccountUpdateEventTypeBalanceUpdate, 206 | Time: int64(rand.Uint32()), 207 | Asset: "BTC", 208 | BalanceDelta: "1", 209 | }, 210 | &ws.BalanceUpdateEvent{ 211 | EventType: ws.AccountUpdateEventTypeBalanceUpdate, 212 | Time: int64(rand.Uint32()), 213 | Asset: "ETH", 214 | BalanceDelta: "1", 215 | }, 216 | &ws.BalanceUpdateEvent{ 217 | EventType: ws.AccountUpdateEventTypeBalanceUpdate, 218 | Time: int64(rand.Uint32()), 219 | Asset: "BTC", 220 | BalanceDelta: "2", 221 | }, 222 | } 223 | 224 | key, err := s.api.DataStream() 225 | s.Require().NoError(err) 226 | 227 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 228 | defer cancel() 229 | 230 | info, err := s.ws.AccountInfo(ctx, key) 231 | s.Require().NoError(err) 232 | 233 | stream := info.BalancesStream() 234 | for _, ex := range expected { 235 | s.expected <- ex 236 | actual := <-stream 237 | s.Require().NoError(err) 238 | s.Require().EqualValues(ex, actual) 239 | } 240 | close(s.expected) 241 | 242 | s.mock.Callback = func(method, endpoint string, data interface{}, sign, stream bool) ([]byte, error) { 243 | s.Require().IsType(binance.DataStream{}, data) 244 | return nil, nil 245 | } 246 | 247 | err = s.api.DataStreamClose(key) 248 | s.Require().NoError(err) 249 | err = info.Close() 250 | s.Require().NoError(err) 251 | } 252 | 253 | func (s *accountTestSuite) TestAccountInfo_AccountStream() { 254 | expected := []interface{}{ 255 | &ws.AccountUpdateEvent{ 256 | Balances: []ws.AccountBalance{ 257 | { 258 | Asset: "ETH", 259 | Free: "1", 260 | Locked: "0.5", 261 | }, 262 | }, 263 | EventType: ws.AccountUpdateEventTypeOutboundAccountPosition, 264 | Time: int64(rand.Uint32()), 265 | LastUpdate: int64(rand.Uint32()), 266 | }, 267 | &ws.AccountUpdateEvent{ 268 | Balances: []ws.AccountBalance{ 269 | { 270 | Asset: "BTC", 271 | Free: "1", 272 | Locked: "0.5", 273 | }, 274 | }, 275 | EventType: ws.AccountUpdateEventTypeOutboundAccountPosition, 276 | Time: int64(rand.Uint32()), 277 | LastUpdate: int64(rand.Uint32()), 278 | }, 279 | } 280 | 281 | key, err := s.api.DataStream() 282 | s.Require().NoError(err) 283 | 284 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 285 | defer cancel() 286 | 287 | info, err := s.ws.AccountInfo(ctx, key) 288 | s.Require().NoError(err) 289 | 290 | stream := info.AccountStream() 291 | for _, ex := range expected { 292 | s.expected <- ex 293 | actual := <-stream 294 | s.Require().NoError(err) 295 | s.Require().EqualValues(ex, actual) 296 | } 297 | 298 | s.mock.Callback = func(method, endpoint string, data interface{}, sign, stream bool) ([]byte, error) { 299 | s.Require().IsType(binance.DataStream{}, data) 300 | return nil, nil 301 | } 302 | 303 | err = s.api.DataStreamClose(key) 304 | s.Require().NoError(err) 305 | err = info.Close() 306 | s.Require().NoError(err) 307 | } 308 | 309 | func (s *accountTestSuite) TestAccountInfo_OrdersStream() { 310 | expected := []interface{}{ 311 | &ws.OrderUpdateEvent{ 312 | EventType: ws.AccountUpdateEventTypeOrderReport, 313 | Symbol: "ETHBTC", 314 | Side: "BUY", 315 | OrderType: "LIMIT", 316 | TimeInForce: "GTC", 317 | OrigQty: "1", 318 | Price: "3400", 319 | Status: "FILLED", 320 | FilledQty: "1", 321 | TotalFilledQty: "1", 322 | FilledPrice: "3400", 323 | Commission: "0.00001", 324 | CommissionAsset: "BTC", 325 | Time: int64(rand.Uint32()), 326 | TradeTime: int64(rand.Uint32()), 327 | OrderCreatedTime: int64(rand.Uint32()), 328 | TradeID: rand.Int63(), 329 | OrderID: int64(rand.Uint32()), 330 | }, 331 | &ws.OrderUpdateEvent{ 332 | EventType: ws.AccountUpdateEventTypeOrderReport, 333 | Symbol: "ETHBTC", 334 | Side: "BUY", 335 | OrderType: "LIMIT", 336 | TimeInForce: "GTC", 337 | OrigQty: "1", 338 | Price: "3500", 339 | Status: "FILLED", 340 | FilledQty: "1", 341 | TotalFilledQty: "1", 342 | FilledPrice: "3500", 343 | Commission: "0.00001", 344 | CommissionAsset: "BTC", 345 | Time: int64(rand.Uint32()), 346 | TradeTime: int64(rand.Uint32()), 347 | OrderCreatedTime: int64(rand.Uint32()), 348 | TradeID: rand.Int63(), 349 | OrderID: int64(rand.Uint32()), 350 | }, 351 | &ws.OrderUpdateEvent{ 352 | EventType: ws.AccountUpdateEventTypeOrderReport, 353 | Symbol: "ETHBTC", 354 | Side: "BUY", 355 | OrderType: "LIMIT", 356 | TimeInForce: "GTC", 357 | OrigQty: "1", 358 | Price: "3600", 359 | Status: "FILLED", 360 | FilledQty: "1", 361 | TotalFilledQty: "1", 362 | FilledPrice: "3600", 363 | Commission: "0.00001", 364 | CommissionAsset: "BTC", 365 | Time: int64(rand.Uint32()), 366 | TradeTime: int64(rand.Uint32()), 367 | OrderCreatedTime: int64(rand.Uint32()), 368 | TradeID: rand.Int63(), 369 | OrderID: int64(rand.Uint32()), 370 | }, 371 | } 372 | 373 | key, err := s.api.DataStream() 374 | s.Require().NoError(err) 375 | 376 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 377 | defer cancel() 378 | 379 | info, err := s.ws.AccountInfo(ctx, key) 380 | s.Require().NoError(err) 381 | 382 | stream := info.OrdersStream() 383 | for _, ex := range expected { 384 | s.expected <- ex 385 | actual := <-stream 386 | s.Require().NoError(err) 387 | s.Require().EqualValues(ex, actual) 388 | } 389 | close(s.expected) 390 | 391 | s.mock.Callback = func(method, endpoint string, data interface{}, sign, stream bool) ([]byte, error) { 392 | s.Require().IsType(binance.DataStream{}, data) 393 | return nil, nil 394 | } 395 | 396 | err = s.api.DataStreamClose(key) 397 | s.Require().NoError(err) 398 | err = info.Close() 399 | s.Require().NoError(err) 400 | } 401 | 402 | func (s *accountTestSuite) TestAccountInfo_OCOOrdersStream() { 403 | expected := []interface{}{ 404 | &ws.OCOOrderUpdateEvent{ 405 | EventType: ws.AccountUpdateEventTypeOCOReport, 406 | Orders: []ws.OCOOrderUpdateEventOrder{ 407 | { 408 | Symbol: "ETH", 409 | OrderID: int64(rand.Uint32()), 410 | }, 411 | { 412 | Symbol: "BTC", 413 | OrderID: int64(rand.Uint32()), 414 | }, 415 | }, 416 | Symbol: "ETHBTC", 417 | ContingencyType: "OCO", 418 | OCOStatus: "EXEC_STARTED", 419 | OrderStatus: "EXECUTING", 420 | OCORejectReason: "NONE", 421 | TransactTime: int64(rand.Uint32()), 422 | OrderListID: rand.Int63(), 423 | Time: int64(rand.Uint32()), 424 | }, 425 | &ws.OCOOrderUpdateEvent{ 426 | EventType: ws.AccountUpdateEventTypeOCOReport, 427 | Orders: []ws.OCOOrderUpdateEventOrder{ 428 | { 429 | Symbol: "ETH", 430 | OrderID: int64(rand.Uint32()), 431 | }, 432 | { 433 | Symbol: "BTC", 434 | OrderID: int64(rand.Uint32()), 435 | }, 436 | }, 437 | Symbol: "ETHBTC", 438 | ContingencyType: "OCO", 439 | OCOStatus: "EXEC_STARTED", 440 | OrderStatus: "EXECUTING", 441 | OCORejectReason: "NONE", 442 | TransactTime: int64(rand.Uint32()), 443 | OrderListID: rand.Int63(), 444 | Time: int64(rand.Uint32()), 445 | }, 446 | } 447 | 448 | key, err := s.api.DataStream() 449 | s.Require().NoError(err) 450 | 451 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 452 | defer cancel() 453 | 454 | info, err := s.ws.AccountInfo(ctx, key) 455 | s.Require().NoError(err) 456 | 457 | stream := info.OCOOrdersStream() 458 | for _, ex := range expected { 459 | s.expected <- ex 460 | actual := <-stream 461 | s.Require().NoError(err) 462 | s.Require().EqualValues(ex, actual) 463 | } 464 | close(s.expected) 465 | 466 | s.mock.Callback = func(method, endpoint string, data interface{}, sign, stream bool) ([]byte, error) { 467 | s.Require().IsType(binance.DataStream{}, data) 468 | return nil, nil 469 | } 470 | 471 | err = s.api.DataStreamClose(key) 472 | s.Require().NoError(err) 473 | err = info.Close() 474 | s.Require().NoError(err) 475 | } 476 | -------------------------------------------------------------------------------- /ws/binance.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "strings" 7 | 8 | "github.com/gobwas/ws" 9 | 10 | "github.com/xenking/binance-api" 11 | ) 12 | 13 | const DefaultStreamPath = "wss://stream.binance.com:9443/ws/" 14 | 15 | type Client struct { 16 | conn net.Conn 17 | StreamPath string 18 | } 19 | 20 | func NewClient() *Client { 21 | return &Client{ 22 | StreamPath: DefaultStreamPath, 23 | } 24 | } 25 | 26 | func NewCustomClient(prefix string, conn net.Conn) *Client { 27 | return &Client{StreamPath: prefix, conn: conn} 28 | } 29 | 30 | // DiffDepth opens websocket with depth updates for the given symbol to locally manage an order book 31 | func (c *Client) DiffDepth(ctx context.Context, symbol string, frequency FrequencyType) (*Depth, error) { 32 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, strings.ToLower(symbol), EndpointDepthStream, string(frequency)) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return &Depth{NewConn(wsc)}, nil 38 | } 39 | 40 | // DepthLevel opens websocket with depth updates for the given symbol (eg @100ms frequency) 41 | func (c *Client) DepthLevel(ctx context.Context, symbol string, level DepthLevelType, frequency FrequencyType) (*DepthLevel, error) { 42 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, strings.ToLower(symbol), EndpointDepthStream, string(level), string(frequency)) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return &DepthLevel{NewConn(wsc)}, nil 48 | } 49 | 50 | // IndividualTicker opens websocket with individual ticker updates for the given symbol 51 | func (c *Client) IndividualTicker(ctx context.Context, symbol string) (*IndividualTicker, error) { 52 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, strings.ToLower(symbol), EndpointTickerStream) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return &IndividualTicker{NewConn(wsc)}, nil 58 | } 59 | 60 | // IndividualRollingWindowTicker opens websocket with individual ticker updates for the given symbol with a custom rolling window 61 | func (c *Client) IndividualRollingWindowTicker(ctx context.Context, symbol string, window WindowSizeType) (*IndividualTicker, error) { 62 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, strings.ToLower(symbol), EndpointWindowTickerStream, string(window)) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return &IndividualTicker{NewConn(wsc)}, nil 68 | } 69 | 70 | // AllMarketTickers opens websocket with ticker updates for all symbols 71 | func (c *Client) AllMarketTickers(ctx context.Context) (*AllMarketTicker, error) { 72 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, EndpointAllMarketTickersStream) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | return &AllMarketTicker{NewConn(wsc)}, nil 78 | } 79 | 80 | // AllMarketRollingWindowTickers opens websocket with ticker updates for all symbols with a custom rolling window 81 | func (c *Client) AllMarketRollingWindowTickers(ctx context.Context, window WindowSizeType) (*AllMarketTicker, error) { 82 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, EndpointAllMarketWindowTickersStream, string(window), "@arr") 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return &AllMarketTicker{NewConn(wsc)}, nil 88 | } 89 | 90 | // AllMarketMiniTickers opens websocket with 91 | func (c *Client) AllMarketMiniTickers(ctx context.Context) (*AllMarketMiniTicker, error) { 92 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, EndpointAllMarketMiniTickersStream) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return &AllMarketMiniTicker{NewConn(wsc)}, nil 98 | } 99 | 100 | // IndividualMiniTicker opens websocket with 101 | func (c *Client) IndividualMiniTicker(ctx context.Context, symbol string) (*IndividualMiniTicker, error) { 102 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, strings.ToLower(symbol), EndpointMiniTickerStream) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | return &IndividualMiniTicker{NewConn(wsc)}, nil 108 | } 109 | 110 | // IndividualBookTicker opens websocket with book ticker best bid or ask updates for the given symbol 111 | func (c *Client) IndividualBookTicker(ctx context.Context, symbol string) (*IndividualBookTicker, error) { 112 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, strings.ToLower(symbol), EndpointBookTickerStream) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | return &IndividualBookTicker{NewConn(wsc)}, nil 118 | } 119 | 120 | // Klines opens websocket with klines updates for the given symbol with the given interval 121 | func (c *Client) Klines(ctx context.Context, symbol string, interval binance.KlineInterval) (*Klines, error) { 122 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, strings.ToLower(symbol), EndpointKlineStream, string(interval)) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | return &Klines{NewConn(wsc)}, nil 128 | } 129 | 130 | // AggTrades opens websocket with aggregated trades updates for the given symbol 131 | func (c *Client) AggTrades(ctx context.Context, symbol string) (*AggTrades, error) { 132 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, strings.ToLower(symbol), EndpointAggregatedTradeStream) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | return &AggTrades{NewConn(wsc)}, nil 138 | } 139 | 140 | // Trades opens websocket with trades updates for the given symbol 141 | func (c *Client) Trades(ctx context.Context, symbol string) (*Trades, error) { 142 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, strings.ToLower(symbol), EndpointTradeStream) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | return &Trades{NewConn(wsc)}, nil 148 | } 149 | 150 | // AccountInfo opens websocket with account info updates 151 | func (c *Client) AccountInfo(ctx context.Context, listenKey string) (*AccountInfo, error) { 152 | wsc, err := newWSClient(ctx, c.conn, c.StreamPath, listenKey) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | return &AccountInfo{NewConn(wsc)}, nil 158 | } 159 | 160 | func newWSClient(ctx context.Context, conn net.Conn, paths ...string) (net.Conn, error) { 161 | path := strings.Join(paths, "") 162 | if conn != nil { 163 | _, err := ws.Upgrade(conn) 164 | return conn, err 165 | } 166 | newConn, _, _, err := ws.Dial(ctx, path) 167 | return newConn, err 168 | } 169 | -------------------------------------------------------------------------------- /ws/binance_test.go: -------------------------------------------------------------------------------- 1 | package ws_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | 8 | "github.com/xenking/binance-api/ws" 9 | ) 10 | 11 | func TestWSClient(t *testing.T) { 12 | suite.Run(t, new(tickerTestSuite)) 13 | suite.Run(t, new(accountTestSuite)) 14 | } 15 | 16 | type baseTestSuite struct { 17 | suite.Suite 18 | ws *ws.Client 19 | } 20 | 21 | func (s *baseTestSuite) SetupTest() { 22 | s.ws = ws.NewClient() 23 | } 24 | -------------------------------------------------------------------------------- /ws/conn.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "io" 5 | "net" 6 | 7 | "github.com/gobwas/ws" 8 | "github.com/gobwas/ws/wsutil" 9 | "github.com/segmentio/encoding/json" 10 | ) 11 | 12 | type Conn struct { 13 | conn net.Conn 14 | } 15 | 16 | func NewConn(conn net.Conn) Conn { 17 | return Conn{conn: conn} 18 | } 19 | 20 | func (c *Conn) Close() error { 21 | return c.conn.Close() 22 | } 23 | 24 | func (c *Conn) NetConn() net.Conn { 25 | return c.conn 26 | } 27 | 28 | func (c *Conn) ReadValue(value interface{}) error { 29 | h, r, err := wsutil.NextReader(c.conn, ws.StateClientSide) 30 | if err != nil { 31 | return err 32 | } 33 | if h.OpCode.IsControl() { 34 | return wsutil.ControlFrameHandler(c.conn, ws.StateClientSide)(h, r) 35 | } 36 | err = wsutil.WriteClientMessage(c.conn, ws.OpPong, nil) 37 | if err != nil { 38 | return err 39 | } 40 | return json.NewDecoder(r).Decode(value) 41 | } 42 | 43 | func (c *Conn) ReadRaw() ([]byte, error) { 44 | h, r, err := wsutil.NextReader(c.conn, ws.StateClientSide) 45 | if err != nil { 46 | return nil, err 47 | } 48 | if h.OpCode.IsControl() { 49 | return nil, wsutil.ControlFrameHandler(c.conn, ws.StateClientSide)(h, r) 50 | } 51 | err = wsutil.WriteClientMessage(c.conn, ws.OpPong, nil) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return io.ReadAll(r) 56 | } 57 | 58 | func (c *Conn) NewStream(callback func(dec *json.Decoder, err error) error) { 59 | var ( 60 | state = ws.StateClientSide 61 | reader = wsutil.NewReader(c.conn, state) 62 | controlHandler = wsutil.ControlFrameHandler(c.conn, state) 63 | decoder = json.NewDecoder(reader) 64 | ) 65 | defer c.conn.Close() 66 | 67 | for { 68 | hdr, err := reader.NextFrame() 69 | if err != nil { 70 | _ = callback(nil, err) 71 | return 72 | } 73 | if hdr.OpCode.IsControl() { 74 | err = controlHandler(hdr, reader) 75 | } 76 | err = callback(decoder, err) 77 | if err != nil { 78 | return 79 | } 80 | } 81 | } 82 | 83 | func (c *Conn) NewStreamRaw(callback func(buf []byte, err error) error) { 84 | var ( 85 | state = ws.StateClientSide 86 | reader = wsutil.NewReader(c.conn, state) 87 | controlHandler = wsutil.ControlFrameHandler(c.conn, state) 88 | ) 89 | defer c.conn.Close() 90 | 91 | for { 92 | hdr, err := reader.NextFrame() 93 | if err != nil { 94 | _ = callback(nil, err) 95 | return 96 | } 97 | if hdr.OpCode.IsControl() { 98 | err = controlHandler(hdr, reader) 99 | err = callback(nil, err) 100 | } 101 | if err != nil { 102 | return 103 | } 104 | err = callback(io.ReadAll(reader)) 105 | if err != nil { 106 | return 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /ws/endpoints.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | const ( 4 | EndpointDepthStream = "@depth" 5 | EndpointTickerStream = "@ticker" 6 | EndpointWindowTickerStream = "@ticker_" 7 | EndpointMiniTickerStream = "@miniTicker" 8 | EndpointBookTickerStream = "@bookTicker" 9 | EndpointKlineStream = "@kline_" 10 | EndpointAggregatedTradeStream = "@aggTrade" 11 | EndpointTradeStream = "@trade" 12 | EndpointAllMarketTickersStream = "!ticker@arr" 13 | EndpointAllMarketWindowTickersStream = "!ticker_" 14 | EndpointAllMarketMiniTickersStream = "!miniTicker@arr" 15 | ) 16 | -------------------------------------------------------------------------------- /ws/ticker.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import "github.com/segmentio/encoding/json" 4 | 5 | // Depth is a wrapper for depth websocket 6 | type Depth struct { 7 | Conn 8 | } 9 | 10 | // Read reads a depth update message from depth websocket 11 | func (d *Depth) Read() (*DepthUpdate, error) { 12 | r := &DepthUpdate{} 13 | err := d.Conn.ReadValue(r) 14 | 15 | return r, err 16 | } 17 | 18 | // Stream NewStream a depth update message from depth websocket to channel 19 | func (d *Depth) Stream() <-chan *DepthUpdate { 20 | updates := make(chan *DepthUpdate) 21 | go d.NewStream(func(dec *json.Decoder, err error) error { 22 | if err != nil { 23 | close(updates) 24 | return err 25 | } 26 | 27 | u := &DepthUpdate{} 28 | err = dec.Decode(u) 29 | if err != nil { 30 | close(updates) 31 | return err 32 | } 33 | updates <- u 34 | 35 | return nil 36 | }) 37 | 38 | return updates 39 | } 40 | 41 | // DepthLevel is a wrapper for depth level websocket 42 | type DepthLevel struct { 43 | Conn 44 | } 45 | 46 | // Read reads a depth update message from depth level websocket 47 | func (d *DepthLevel) Read() (*DepthLevelUpdate, error) { 48 | r := &DepthLevelUpdate{} 49 | err := d.Conn.ReadValue(r) 50 | 51 | return r, err 52 | } 53 | 54 | // Stream NewStream a depth update message from depth level websocket to channel 55 | func (d *DepthLevel) Stream() <-chan *DepthLevelUpdate { 56 | updates := make(chan *DepthLevelUpdate) 57 | go d.NewStream(func(dec *json.Decoder, err error) error { 58 | if err != nil { 59 | close(updates) 60 | return err 61 | } 62 | 63 | u := &DepthLevelUpdate{} 64 | err = dec.Decode(u) 65 | if err != nil { 66 | close(updates) 67 | return err 68 | } 69 | updates <- u 70 | 71 | return nil 72 | }) 73 | 74 | return updates 75 | } 76 | 77 | // AllMarketTicker is a wrapper for all markets tickers websocket 78 | type AllMarketTicker struct { 79 | Conn 80 | } 81 | 82 | // Read reads a market update message from all markets ticker websocket 83 | func (t *AllMarketTicker) Read() (*AllMarketTickerUpdate, error) { 84 | r := &AllMarketTickerUpdate{} 85 | err := t.Conn.ReadValue(r) 86 | 87 | return r, err 88 | } 89 | 90 | // Stream NewStream a market update message from all markets ticker websocket to channel 91 | func (t *AllMarketTicker) Stream() <-chan *AllMarketTickerUpdate { 92 | updates := make(chan *AllMarketTickerUpdate) 93 | go t.NewStream(func(dec *json.Decoder, err error) error { 94 | if err != nil { 95 | close(updates) 96 | return err 97 | } 98 | 99 | u := &AllMarketTickerUpdate{} 100 | err = dec.Decode(u) 101 | if err != nil { 102 | close(updates) 103 | return err 104 | } 105 | updates <- u 106 | 107 | return nil 108 | }) 109 | 110 | return updates 111 | } 112 | 113 | // IndividualTicker is a wrapper for an Individualidual ticker websocket 114 | type IndividualTicker struct { 115 | Conn 116 | } 117 | 118 | // Read reads a Individualidual symbol update message from Individualidual ticker websocket 119 | func (t *IndividualTicker) Read() (*IndividualTickerUpdate, error) { 120 | r := &IndividualTickerUpdate{} 121 | err := t.Conn.ReadValue(r) 122 | 123 | return r, err 124 | } 125 | 126 | // Stream NewStream a Individualidual update message from Individualidual ticker websocket to channel 127 | func (t *IndividualTicker) Stream() <-chan *IndividualTickerUpdate { 128 | updates := make(chan *IndividualTickerUpdate) 129 | go t.NewStream(func(dec *json.Decoder, err error) error { 130 | if err != nil { 131 | close(updates) 132 | return err 133 | } 134 | 135 | u := &IndividualTickerUpdate{} 136 | err = dec.Decode(u) 137 | if err != nil { 138 | close(updates) 139 | return err 140 | } 141 | updates <- u 142 | 143 | return nil 144 | }) 145 | 146 | return updates 147 | } 148 | 149 | // AllMarketMiniTicker is a wrapper for all markets mini-tickers websocket 150 | type AllMarketMiniTicker struct { 151 | Conn 152 | } 153 | 154 | // Read reads a market update message from all markets mini-ticker websocket 155 | func (t *AllMarketMiniTicker) Read() (*AllMarketMiniTickerUpdate, error) { 156 | r := &AllMarketMiniTickerUpdate{} 157 | err := t.Conn.ReadValue(r) 158 | 159 | return r, err 160 | } 161 | 162 | // Stream NewStream a market update message from all markets mini-ticker websocket to channel 163 | func (t *AllMarketMiniTicker) Stream() <-chan *AllMarketMiniTickerUpdate { 164 | updates := make(chan *AllMarketMiniTickerUpdate) 165 | go t.NewStream(func(dec *json.Decoder, err error) error { 166 | if err != nil { 167 | close(updates) 168 | return err 169 | } 170 | 171 | u := &AllMarketMiniTickerUpdate{} 172 | err = dec.Decode(u) 173 | if err != nil { 174 | close(updates) 175 | return err 176 | } 177 | updates <- u 178 | 179 | return nil 180 | }) 181 | 182 | return updates 183 | } 184 | 185 | // IndividualMiniTicker is a wrapper for an Individualidual mini-ticker websocket 186 | type IndividualMiniTicker struct { 187 | Conn 188 | } 189 | 190 | // Read reads a Individualidual symbol update message from Individualidual mini-ticker websocket 191 | func (t *IndividualMiniTicker) Read() (*IndividualMiniTickerUpdate, error) { 192 | r := &IndividualMiniTickerUpdate{} 193 | err := t.Conn.ReadValue(r) 194 | 195 | return r, err 196 | } 197 | 198 | // Stream NewStream a Individualidual update message from Individualidual mini-ticker websocket to channel 199 | func (t *IndividualMiniTicker) Stream() <-chan *IndividualMiniTickerUpdate { 200 | updates := make(chan *IndividualMiniTickerUpdate) 201 | go t.NewStream(func(dec *json.Decoder, err error) error { 202 | if err != nil { 203 | close(updates) 204 | return err 205 | } 206 | 207 | u := &IndividualMiniTickerUpdate{} 208 | err = dec.Decode(u) 209 | if err != nil { 210 | close(updates) 211 | return err 212 | } 213 | updates <- u 214 | 215 | return nil 216 | }) 217 | 218 | return updates 219 | } 220 | 221 | // AllBookTicker is a wrapper for all book tickers websocket 222 | type AllBookTicker struct { 223 | Conn 224 | } 225 | 226 | // Read reads a book update message from all book tickers websocket 227 | func (t *AllBookTicker) Read() (*AllBookTickerUpdate, error) { 228 | r := &AllBookTickerUpdate{} 229 | err := t.Conn.ReadValue(r) 230 | 231 | return r, err 232 | } 233 | 234 | // Stream NewStream a book update message from all book tickers websocket to channel 235 | func (t *AllBookTicker) Stream() <-chan *AllBookTickerUpdate { 236 | updates := make(chan *AllBookTickerUpdate) 237 | go t.NewStream(func(dec *json.Decoder, err error) error { 238 | if err != nil { 239 | close(updates) 240 | return err 241 | } 242 | 243 | u := &AllBookTickerUpdate{} 244 | err = dec.Decode(u) 245 | if err != nil { 246 | close(updates) 247 | return err 248 | } 249 | updates <- u 250 | 251 | return nil 252 | }) 253 | 254 | return updates 255 | } 256 | 257 | // IndividualBookTicker is a wrapper for an Individualidual book ticker websocket 258 | type IndividualBookTicker struct { 259 | Conn 260 | } 261 | 262 | // Read reads a Individualidual book symbol update message from Individualidual book ticker websocket 263 | func (t *IndividualBookTicker) Read() (*IndividualBookTickerUpdate, error) { 264 | r := &IndividualBookTickerUpdate{} 265 | err := t.Conn.ReadValue(r) 266 | 267 | return r, err 268 | } 269 | 270 | // Stream NewStream a Individualidual book symbol update message from Individualidual book ticker websocket to channel 271 | func (t *IndividualBookTicker) Stream() <-chan *IndividualBookTickerUpdate { 272 | updates := make(chan *IndividualBookTickerUpdate) 273 | go t.NewStream(func(dec *json.Decoder, err error) error { 274 | if err != nil { 275 | close(updates) 276 | return err 277 | } 278 | 279 | u := &IndividualBookTickerUpdate{} 280 | err = dec.Decode(u) 281 | if err != nil { 282 | close(updates) 283 | return err 284 | } 285 | updates <- u 286 | 287 | return nil 288 | }) 289 | 290 | return updates 291 | } 292 | 293 | // Klines is a wrapper for klines websocket 294 | type Klines struct { 295 | Conn 296 | } 297 | 298 | // Read reads a klines update message from klines websocket 299 | func (k *Klines) Read() (*KlinesUpdate, error) { 300 | r := &KlinesUpdate{} 301 | err := k.Conn.ReadValue(r) 302 | 303 | return r, err 304 | } 305 | 306 | // Stream NewStream a klines update message from klines websocket to channel 307 | func (k *Klines) Stream() <-chan *KlinesUpdate { 308 | updates := make(chan *KlinesUpdate) 309 | go k.NewStream(func(dec *json.Decoder, err error) error { 310 | if err != nil { 311 | close(updates) 312 | return err 313 | } 314 | 315 | u := &KlinesUpdate{} 316 | err = dec.Decode(u) 317 | if err != nil { 318 | close(updates) 319 | return err 320 | } 321 | updates <- u 322 | 323 | return nil 324 | }) 325 | 326 | return updates 327 | } 328 | 329 | // AggTrades is a wrapper for trades websocket 330 | type AggTrades struct { 331 | Conn 332 | } 333 | 334 | // Read reads a trades update message from aggregated trades websocket 335 | func (t *AggTrades) Read() (*AggTradeUpdate, error) { 336 | r := &AggTradeUpdate{} 337 | err := t.Conn.ReadValue(r) 338 | 339 | return r, err 340 | } 341 | 342 | // Stream NewStream a trades update message from aggregated trades websocket to channel 343 | func (t *AggTrades) Stream() <-chan *AggTradeUpdate { 344 | updates := make(chan *AggTradeUpdate) 345 | go t.NewStream(func(dec *json.Decoder, err error) error { 346 | if err != nil { 347 | close(updates) 348 | return err 349 | } 350 | 351 | u := &AggTradeUpdate{} 352 | err = dec.Decode(u) 353 | if err != nil { 354 | close(updates) 355 | return err 356 | } 357 | updates <- u 358 | 359 | return nil 360 | }) 361 | 362 | return updates 363 | } 364 | 365 | // Trades is a wrapper for trades websocket 366 | type Trades struct { 367 | Conn 368 | } 369 | 370 | // Read reads a trades update message from trades websocket 371 | func (t *Trades) Read() (*TradeUpdate, error) { 372 | r := &TradeUpdate{} 373 | err := t.Conn.ReadValue(r) 374 | 375 | return r, err 376 | } 377 | 378 | // Stream NewStream a trades update message from trades websocket to channel 379 | func (t *Trades) Stream() <-chan *TradeUpdate { 380 | updates := make(chan *TradeUpdate) 381 | go t.NewStream(func(dec *json.Decoder, err error) error { 382 | if err != nil { 383 | close(updates) 384 | return err 385 | } 386 | 387 | u := &TradeUpdate{} 388 | err = dec.Decode(u) 389 | if err != nil { 390 | close(updates) 391 | return err 392 | } 393 | updates <- u 394 | 395 | return nil 396 | }) 397 | 398 | return updates 399 | } 400 | -------------------------------------------------------------------------------- /ws/ticker_test.go: -------------------------------------------------------------------------------- 1 | package ws_test 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/xenking/binance-api" 8 | "github.com/xenking/binance-api/ws" 9 | ) 10 | 11 | type tickerTestSuite struct { 12 | baseTestSuite 13 | } 14 | 15 | func (s *tickerTestSuite) TestDepth_Read() { 16 | const symbol = "ETHBTC" 17 | 18 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 19 | defer cancel() 20 | 21 | depth, err := s.ws.DiffDepth(ctx, symbol, ws.Frequency1000ms) 22 | s.Require().NoError(err) 23 | defer depth.Close() 24 | 25 | u, err := depth.Read() 26 | 27 | s.Require().NoError(err) 28 | s.Require().Equal(symbol, u.Symbol) 29 | } 30 | 31 | func (s *tickerTestSuite) TestDepth_Stream() { 32 | const symbol = "ETHBTC" 33 | 34 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 35 | defer cancel() 36 | 37 | depth, err := s.ws.DiffDepth(ctx, symbol, ws.Frequency1000ms) 38 | s.Require().NoError(err) 39 | 40 | for u := range depth.Stream() { 41 | s.Require().Equal(symbol, u.Symbol) 42 | break 43 | } 44 | s.Require().NoError(depth.Close()) 45 | } 46 | 47 | func (s *tickerTestSuite) TestDepthLevel_Read() { 48 | const symbol = "ETHBTC" 49 | 50 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 51 | defer cancel() 52 | 53 | depthLevel, err := s.ws.DepthLevel(ctx, symbol, ws.DepthLevel10, ws.Frequency1000ms) 54 | s.Require().NoError(err) 55 | defer depthLevel.Close() 56 | 57 | u, err := depthLevel.Read() 58 | 59 | s.Require().NoError(err) 60 | s.Require().Equal(len(u.Asks), 10) 61 | s.Require().Equal(len(u.Bids), 10) 62 | } 63 | 64 | func (s *tickerTestSuite) TestDepthLevel_Stream() { 65 | const symbol = "ETHBTC" 66 | 67 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 68 | defer cancel() 69 | 70 | depthLevel, err := s.ws.DepthLevel(ctx, symbol, ws.DepthLevel5, ws.Frequency1000ms) 71 | s.Require().NoError(err) 72 | 73 | for u := range depthLevel.Stream() { 74 | s.Require().Equal(len(u.Asks), 5) 75 | s.Require().Equal(len(u.Bids), 5) 76 | 77 | break 78 | } 79 | 80 | s.Require().NoError(depthLevel.Close()) 81 | } 82 | 83 | func (s *tickerTestSuite) TestKlines_Read() { 84 | const symbol = "ETHBTC" 85 | 86 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 87 | defer cancel() 88 | 89 | klines, err := s.ws.Klines(ctx, symbol, binance.KlineInterval1min) 90 | s.Require().NoError(err) 91 | defer klines.Close() 92 | 93 | u, err := klines.Read() 94 | s.Require().NoError(err) 95 | s.Require().Equal(symbol, u.Symbol) 96 | } 97 | 98 | func (s *tickerTestSuite) TestKlines_Stream() { 99 | const symbol = "ETHBTC" 100 | 101 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 102 | defer cancel() 103 | 104 | klines, err := s.ws.Klines(ctx, symbol, binance.KlineInterval1min) 105 | s.Require().NoError(err) 106 | 107 | for u := range klines.Stream() { 108 | s.Require().Equal(symbol, u.Symbol) 109 | break 110 | } 111 | s.Require().NoError(klines.Close()) 112 | } 113 | 114 | func (s *tickerTestSuite) TestAllMarketTickers_Read() { 115 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 116 | defer cancel() 117 | 118 | tickers, err := s.ws.AllMarketTickers(ctx) 119 | s.Require().NoError(err) 120 | defer tickers.Close() 121 | 122 | u, err := tickers.Read() 123 | s.Require().NoError(err) 124 | s.Require().NotEmpty(u) 125 | } 126 | 127 | func (s *tickerTestSuite) TestAllMarketTickers_Stream() { 128 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 129 | defer cancel() 130 | 131 | tickers, err := s.ws.AllMarketTickers(ctx) 132 | s.Require().NoError(err) 133 | 134 | for u := range tickers.Stream() { 135 | s.Require().NotEmpty(u) 136 | break 137 | } 138 | s.Require().NoError(tickers.Close()) 139 | } 140 | 141 | func (s *tickerTestSuite) TestAllMarketMiniTickers_Read() { 142 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 143 | defer cancel() 144 | 145 | tickers, err := s.ws.AllMarketMiniTickers(ctx) 146 | s.Require().NoError(err) 147 | defer tickers.Close() 148 | 149 | u, err := tickers.Read() 150 | s.Require().NoError(err) 151 | s.Require().NotEmpty(u) 152 | } 153 | 154 | func (s *tickerTestSuite) TestAllMarketMiniTickers_Stream() { 155 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 156 | defer cancel() 157 | 158 | tickers, err := s.ws.AllMarketMiniTickers(ctx) 159 | s.Require().NoError(err) 160 | 161 | for u := range tickers.Stream() { 162 | s.Require().NotEmpty(u) 163 | break 164 | } 165 | s.Require().NoError(tickers.Close()) 166 | } 167 | 168 | func (s *tickerTestSuite) TestIndividualTicker_Read() { 169 | const symbol = "ETHBTC" 170 | 171 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 172 | defer cancel() 173 | 174 | ticker, err := s.ws.IndividualTicker(ctx, symbol) 175 | s.Require().NoError(err) 176 | defer ticker.Close() 177 | 178 | u, err := ticker.Read() 179 | s.Require().NoError(err) 180 | s.Require().Equal(symbol, u.Symbol) 181 | } 182 | 183 | func (s *tickerTestSuite) TestIndividualTickers_Stream() { 184 | const symbol = "ETHBTC" 185 | 186 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 187 | defer cancel() 188 | 189 | ticker, err := s.ws.IndividualTicker(ctx, symbol) 190 | s.Require().NoError(err) 191 | 192 | for u := range ticker.Stream() { 193 | s.Require().Equal(symbol, u.Symbol) 194 | break 195 | } 196 | s.Require().NoError(ticker.Close()) 197 | 198 | } 199 | 200 | func (s *tickerTestSuite) TestIndividualMiniTicker_Read() { 201 | const symbol = "ETHBTC" 202 | 203 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 204 | defer cancel() 205 | 206 | ticker, err := s.ws.IndividualMiniTicker(ctx, symbol) 207 | s.Require().NoError(err) 208 | defer ticker.Close() 209 | 210 | u, err := ticker.Read() 211 | s.Require().NoError(err) 212 | s.Require().Equal(symbol, u.Symbol) 213 | } 214 | 215 | func (s *tickerTestSuite) TestIndividualMiniTickers_Stream() { 216 | const symbol = "ETHBTC" 217 | 218 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 219 | defer cancel() 220 | 221 | miniTicker, err := s.ws.IndividualMiniTicker(ctx, symbol) 222 | s.Require().NoError(err) 223 | 224 | for u := range miniTicker.Stream() { 225 | s.Require().Equal(symbol, u.Symbol) 226 | break 227 | } 228 | s.Require().NoError(miniTicker.Close()) 229 | 230 | } 231 | 232 | func (s *tickerTestSuite) TestIndividualBookTicker_Read() { 233 | const symbol = "ETHBTC" 234 | 235 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 236 | defer cancel() 237 | 238 | bookTicker, err := s.ws.IndividualBookTicker(ctx, symbol) 239 | s.Require().NoError(err) 240 | defer bookTicker.Close() 241 | 242 | u, err := bookTicker.Read() 243 | s.Require().NoError(err) 244 | s.Require().Equal(symbol, u.Symbol) 245 | } 246 | 247 | func (s *tickerTestSuite) TestIndividualBookTickers_Stream() { 248 | const symbol = "ETHBTC" 249 | 250 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 251 | defer cancel() 252 | 253 | bookTicker, err := s.ws.IndividualBookTicker(ctx, symbol) 254 | s.Require().NoError(err) 255 | 256 | for u := range bookTicker.Stream() { 257 | s.Require().Equal(symbol, u.Symbol) 258 | break 259 | } 260 | s.Require().NoError(bookTicker.Close()) 261 | } 262 | 263 | func (s *tickerTestSuite) TestAggTrades_Read() { 264 | const symbol = "ETHBTC" 265 | 266 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 267 | defer cancel() 268 | 269 | trades, err := s.ws.AggTrades(ctx, symbol) 270 | s.Require().NoError(err) 271 | defer trades.Close() 272 | 273 | u, err := trades.Read() 274 | s.Require().NoError(err) 275 | s.Require().Equal(symbol, u.Symbol) 276 | } 277 | 278 | func (s *tickerTestSuite) TestAggTrades_Stream() { 279 | const symbol = "ETHBTC" 280 | 281 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 282 | defer cancel() 283 | 284 | trades, err := s.ws.AggTrades(ctx, symbol) 285 | s.Require().NoError(err) 286 | 287 | for u := range trades.Stream() { 288 | s.Require().Equal(symbol, u.Symbol) 289 | break 290 | } 291 | s.Require().NoError(trades.Close()) 292 | 293 | } 294 | 295 | func (s *tickerTestSuite) TestTrades_Read() { 296 | const symbol = "ETHBTC" 297 | 298 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 299 | defer cancel() 300 | 301 | trades, err := s.ws.Trades(ctx, symbol) 302 | s.Require().NoError(err) 303 | defer trades.Close() 304 | 305 | u, err := trades.Read() 306 | s.Require().NoError(err) 307 | s.Require().Equal(symbol, u.Symbol) 308 | } 309 | 310 | func (s *tickerTestSuite) TestTrades_Stream() { 311 | const symbol = "ETHBTC" 312 | 313 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 314 | defer cancel() 315 | 316 | trades, err := s.ws.Trades(ctx, symbol) 317 | s.Require().NoError(err) 318 | 319 | for u := range trades.Stream() { 320 | s.Require().Equal(symbol, u.Symbol) 321 | break 322 | } 323 | s.Require().NoError(trades.Close()) 324 | } 325 | -------------------------------------------------------------------------------- /ws/types.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "github.com/xenking/binance-api" 5 | ) 6 | 7 | // UpdateType represents type of account update event 8 | type UpdateType string 9 | 10 | const ( 11 | UpdateTypeDepth UpdateType = "depthUpdate" 12 | UpdateTypeIndividualTicker UpdateType = "24hrTicker" 13 | UpdateTypeKline UpdateType = "kline" 14 | UpdateTypeAggTrades UpdateType = "aggTrade" 15 | UpdateTypeTrades UpdateType = "trade" 16 | ) 17 | 18 | type AccountUpdateEventType string 19 | 20 | const ( 21 | // AccountUpdateEventTypeUnknown default for unknown type 22 | AccountUpdateEventTypeUnknown AccountUpdateEventType = "unknown" 23 | AccountUpdateEventTypeOutboundAccountPosition AccountUpdateEventType = "outboundAccountPosition" 24 | AccountUpdateEventTypeOrderReport AccountUpdateEventType = "executionReport" 25 | AccountUpdateEventTypeBalanceUpdate AccountUpdateEventType = "balanceUpdate" 26 | AccountUpdateEventTypeOCOReport AccountUpdateEventType = "listStatus" 27 | ) 28 | 29 | // FrequencyType is a interval for Depth update 30 | type FrequencyType string 31 | 32 | const ( 33 | // Frequency1000ms is default frequency 34 | Frequency1000ms FrequencyType = "@1000ms" 35 | 36 | // Frequency100ms for fastest updates 37 | Frequency100ms FrequencyType = "@100ms" 38 | ) 39 | 40 | // DepthLevelType is a level for DepthLevel update 41 | type DepthLevelType string 42 | 43 | const ( 44 | DepthLevel5 DepthLevelType = "5" 45 | DepthLevel10 DepthLevelType = "10" 46 | DepthLevel20 DepthLevelType = "20" 47 | ) 48 | 49 | type WindowSizeType string 50 | 51 | const ( 52 | WindowSize1h WindowSizeType = "1h" 53 | WindowSize4h WindowSizeType = "4h" 54 | WindowSize1d WindowSizeType = "1d" 55 | ) 56 | 57 | // IndividualTickerUpdate represents incoming ticker websocket feed 58 | type IndividualTickerUpdate struct { 59 | EventType UpdateType `json:"e"` // EventType represents the update type 60 | Time int64 `json:"E"` // Time represents the event time 61 | Symbol string `json:"s"` // Symbol represents the symbol related to the update 62 | Price string `json:"p"` // Price is the order price 63 | PricePercent string `json:"P"` // Price percent change 64 | WeightedPrice string `json:"w"` // Weighted average price 65 | FirstTrade string `json:"x"` // First trade(F)-1 price (first trade before the 24hr rolling window) 66 | LastPrice string `json:"c"` // Last price 67 | LastQty string `json:"Q"` // Last quantity 68 | BestBidPrice string `json:"b"` // Best bid price 69 | BestBidQty string `json:"B"` // Best bid quantity 70 | BestAskPrice string `json:"a"` // Best ask price 71 | BestAskQty string `json:"A"` // Best ask quantity 72 | OpenPrice string `json:"o"` // Open price 73 | HighPrice string `json:"h"` // High price 74 | LowPrice string `json:"l"` // Low price 75 | VolumeBase string `json:"v"` // Total traded base asset volume 76 | VolumeQuote string `json:"q"` // Total traded quote asset volume 77 | StatisticOT int64 `json:"O"` // Statistics open time 78 | StatisticsCT int64 `json:"C"` // Statistics close time 79 | FirstTradeID int64 `json:"F"` // First trade ID 80 | LastTradeID int64 `json:"L"` // Last trade ID 81 | TotalTrades int `json:"n"` // Total number of trades 82 | } 83 | 84 | // AllMarketTickerUpdate represents incoming ticker websocket feed for all tickers 85 | type AllMarketTickerUpdate []IndividualTickerUpdate 86 | 87 | // IndividualBookTickerUpdate represents incoming book ticker websocket feed 88 | type IndividualBookTickerUpdate struct { 89 | UpdateID int `json:"u"` // UpdateID to sync up with updateID in /ws/v3/depth 90 | Symbol string `json:"s"` // Symbol represents the symbol related to the update 91 | BidPrice string `json:"b"` // BidPrice 92 | BidQty string `json:"B"` // BidQty 93 | AskPrice string `json:"a"` // AskPrice 94 | AskQty string `json:"A"` // AskQty 95 | } 96 | 97 | // AllBookTickerUpdate represents incoming ticker websocket feed for all book tickers 98 | type AllBookTickerUpdate IndividualBookTickerUpdate 99 | 100 | // IndividualMiniTickerUpdate represents incoming mini-ticker websocket feed 101 | type IndividualMiniTickerUpdate struct { 102 | EventType UpdateType `json:"e"` // EventType represents the update type 103 | Time int64 `json:"E"` // Time represents the event time 104 | Symbol string `json:"s"` // Symbol represents the symbol related to the update 105 | LastPrice string `json:"c"` // Last price 106 | OpenPrice string `json:"o"` // Open price 107 | HighPrice string `json:"h"` // High price 108 | LowPrice string `json:"l"` // Low price 109 | VolumeBase string `json:"v"` // Total traded base asset volume 110 | VolumeQuote string `json:"q"` // Total traded quote asset volume 111 | } 112 | 113 | // AllMarketMiniTickerUpdate represents incoming mini-ticker websocket feed for all tickers 114 | type AllMarketMiniTickerUpdate []IndividualMiniTickerUpdate 115 | 116 | // DepthUpdate represents the incoming messages for depth websocket updates 117 | type DepthUpdate struct { 118 | EventType UpdateType `json:"e"` // EventType represents the update type 119 | Time int64 `json:"E"` // Time represents the event time 120 | Symbol string `json:"s"` // Symbol represents the symbol related to the update 121 | FirstUpdateID int64 `json:"U"` // FirstTradeID in event 122 | FinalUpdateID int64 `json:"u"` // FirstTradeID in event to sync in /ws/v3/depth 123 | Bids []binance.DepthElem `json:"b"` // Bids is a list of bids for symbol 124 | Asks []binance.DepthElem `json:"a"` // Asks is a list of asks for symbol 125 | } 126 | 127 | // DepthLevelUpdate represents the incoming messages for depth level websocket updates 128 | type DepthLevelUpdate struct { 129 | LastUpdateID int64 `json:"lastUpdateId"` // EventType represents the update type 130 | Bids []binance.DepthElem `json:"bids"` // Bids is a list of bids for symbol 131 | Asks []binance.DepthElem `json:"asks"` // Asks is a list of asks for symbol 132 | } 133 | 134 | // KlinesUpdate represents the incoming messages for klines websocket updates 135 | type KlinesUpdate struct { 136 | EventType UpdateType `json:"e"` // EventType represents the update type 137 | Time int64 `json:"E"` // Time represents the event time 138 | Symbol string `json:"s"` // Symbol represents the symbol related to the update 139 | Kline struct { 140 | StartTime int64 `json:"t"` // StartTime is the start time of this bar 141 | EndTime int64 `json:"T"` // EndTime is the end time of this bar 142 | Symbol string `json:"s"` // Symbol represents the symbol related to this kline 143 | Interval binance.KlineInterval `json:"i"` // Interval is the kline interval 144 | FirstTradeID int64 `json:"f"` // FirstTradeID is the first trade ID 145 | LastTradeID int64 `json:"L"` // LastTradeID is the first trade ID 146 | 147 | OpenPrice string `json:"o"` // OpenPrice represents the open price for this bar 148 | ClosePrice string `json:"c"` // ClosePrice represents the close price for this bar 149 | High string `json:"h"` // High represents the highest price for this bar 150 | Low string `json:"l"` // Low represents the lowest price for this bar 151 | Volume string `json:"v"` // Volume is the trades volume for this bar 152 | Trades int `json:"n"` // Trades is the number of conducted trades 153 | Final bool `json:"x"` // Final indicates whether this bar is final or yet may receive updates 154 | VolumeQuote string `json:"q"` // VolumeQuote indicates the quote volume for the symbol 155 | VolumeActiveBuy string `json:"V"` // VolumeActiveBuy represents the volume of active buy 156 | VolumeQuoteActiveBuy string `json:"Q"` // VolumeQuoteActiveBuy represents the quote volume of active buy 157 | } `json:"k"` // Kline is the kline update 158 | } 159 | 160 | // AggTradeUpdate represents the incoming messages for aggregated trades websocket updates 161 | type AggTradeUpdate struct { 162 | EventType UpdateType `json:"e"` // EventType represents the update type 163 | Time int64 `json:"E"` // Time represents the event time 164 | Symbol string `json:"s"` // Symbol represents the symbol related to the update 165 | TradeID int64 `json:"a"` // TradeID is the aggregated trade ID 166 | Price string `json:"p"` // Price is the trade price 167 | Quantity string `json:"q"` // Quantity is the trade quantity 168 | FirstBreakDownTradeID int64 `json:"f"` // FirstBreakDownTradeID is the first breakdown trade ID 169 | LastBreakDownTradeID int64 `json:"l"` // LastBreakDownTradeID is the last breakdown trade ID 170 | TradeTime int64 `json:"T"` // Time is the trade time 171 | Maker bool `json:"m"` // Maker indicates whether buyer is a maker 172 | } 173 | 174 | // TradeUpdate represents the incoming messages for trades websocket updates 175 | type TradeUpdate struct { 176 | EventType UpdateType `json:"e"` // EventType represents the update type 177 | Symbol string `json:"s"` // Symbol represents the symbol related to the update 178 | Price string `json:"p"` // Price is the trade price 179 | Quantity string `json:"q"` // Quantity is the trade quantity 180 | Time int64 `json:"E"` // Time represents the event time 181 | TradeTime int64 `json:"T"` // Time is the trade time 182 | TradeID int64 `json:"t"` // TradeID is the aggregated trade ID 183 | BuyerID int `json:"b"` // BuyerID is the buyer trade ID 184 | SellerID int `json:"a"` // SellerID is the seller trade ID 185 | Maker bool `json:"m"` // Maker indicates whether buyer is a maker 186 | } 187 | 188 | // UpdateEventType represents only incoming event type 189 | type UpdateEventType struct { 190 | EventType AccountUpdateEventType `json:"e"` // EventType represents the update type 191 | } 192 | 193 | // UnmarshalJSON need to getting partial json data 194 | func (e *UpdateEventType) UnmarshalJSON(b []byte) error { 195 | if len(b) < 21 { 196 | return binance.ErrInvalidJSON 197 | } 198 | // {"e":"outboundAccountPosition","E":1499405658658 199 | // {"e":"executionReport","E":1499405658658 200 | // {"e":"balanceUpdate","E":1499405658658 201 | // {"e":"listStatus","E":1499405658658 202 | switch { 203 | case b[16] == '"': 204 | e.EventType = AccountUpdateEventTypeOCOReport 205 | case b[19] == '"': 206 | e.EventType = AccountUpdateEventTypeBalanceUpdate 207 | case b[21] == '"': 208 | e.EventType = AccountUpdateEventTypeOrderReport 209 | case b[29] == '"': 210 | e.EventType = AccountUpdateEventTypeOutboundAccountPosition 211 | default: 212 | return binance.ErrIncorrectAccountEventType 213 | } 214 | 215 | return nil 216 | } 217 | 218 | // AccountUpdateEvent represents the incoming messages for account websocket updates 219 | type AccountUpdateEvent struct { 220 | EventType AccountUpdateEventType `json:"e"` // EventType represents the update type 221 | Balances []AccountBalance `json:"B"` // Balances represents the account balances 222 | Time int64 `json:"E"` // Time represents the event time 223 | LastUpdate int64 `json:"u"` // LastUpdate represents last account update 224 | } 225 | 226 | type AccountBalance struct { 227 | Asset string `json:"a"` 228 | Free string `json:"f"` 229 | Locked string `json:"l"` 230 | } 231 | 232 | // BalanceUpdateEvent represents the incoming message for account balances websocket updates 233 | type BalanceUpdateEvent struct { 234 | EventType AccountUpdateEventType `json:"e"` // EventType represents the update type 235 | Asset string `json:"a"` // Asset 236 | BalanceDelta string `json:"d"` // Balance Delta 237 | Time int64 `json:"E"` // Time represents the event time 238 | ClearTime int64 `json:"T"` // Clear Time 239 | } 240 | 241 | // OrderUpdateEvent represents the incoming messages for account orders websocket updates 242 | type OrderUpdateEvent struct { 243 | EventType AccountUpdateEventType `json:"e"` // EventType represents the update type 244 | Symbol string `json:"s"` // Symbol represents the symbol related to the update 245 | NewClientOrderID string `json:"c"` // NewClientOrderID is the new client order ID 246 | Side binance.OrderSide `json:"S"` // Side is the order side 247 | OrderType binance.OrderType `json:"o"` // OrderType represents the order type 248 | TimeInForce binance.TimeInForce `json:"f"` // TimeInForce represents the order TIF type 249 | OrigQty string `json:"q"` // OrigQty represents the order original quantity 250 | Price string `json:"p"` // Price is the order price 251 | StopPrice string `json:"P"` 252 | IcebergQty string `json:"F"` 253 | OrigClientOrderID string `json:"C"` 254 | ExecutionType binance.OrderStatus `json:"x"` // ExecutionType represents the execution type for the order 255 | Status binance.OrderStatus `json:"X"` // Status represents the order status for the order 256 | Error binance.OrderFailure `json:"r"` // Error represents an order rejection reason 257 | FilledQty string `json:"l"` // FilledQty represents the quantity of the last filled trade 258 | TotalFilledQty string `json:"z"` // TotalFilledQty is the accumulated quantity of filled trades on this order 259 | FilledPrice string `json:"L"` // FilledPrice is the price of last filled trade 260 | Commission string `json:"n"` // Commission is the commission for the trade 261 | CommissionAsset string `json:"N"` // CommissionAsset is the asset on which commission is taken 262 | QuoteTotalFilledQty string `json:"Z"` // Cumulative quote asset transacted quantity 263 | QuoteFilledQty string `json:"Y"` // Last quote asset transacted quantity (i.e. lastPrice * lastQty) 264 | QuoteQty string `json:"Q"` // Quote Order Qty 265 | Time int64 `json:"E"` // Time represents the event time 266 | TradeTime int64 `json:"T"` // TradeTime is the trade time 267 | OrderCreatedTime int64 `json:"O"` // OrderTime represents the order time 268 | OrderID int64 `json:"i"` // OrderID represents the order ID 269 | TradeID int64 `json:"t"` // TradeID represents the trade ID 270 | OrderListID int64 `json:"g"` 271 | StrategyID int `json:"j"` // Strategy ID; This is only visible if the strategyId parameter was provided upon order placement 272 | StrategyType int `json:"J"` // Strategy Type; This is only visible if the strategyType parameter was provided upon order placement 273 | Maker bool `json:"m"` // Maker represents whether buyer is maker or not 274 | } 275 | 276 | type OCOOrderUpdateEvent struct { 277 | EventType AccountUpdateEventType `json:"e"` 278 | Orders []OCOOrderUpdateEventOrder `json:"O"` 279 | Symbol string `json:"s"` 280 | ContingencyType binance.ContingencyType `json:"c"` 281 | OCOStatus binance.OCOStatus `json:"l"` 282 | OrderStatus binance.OrderStatus `json:"L"` 283 | OCORejectReason binance.OrderFailure `json:"r"` 284 | OCOClientOrderID string `json:"C"` 285 | TransactTime int64 `json:"T"` 286 | OrderListID int64 `json:"g"` 287 | Time int64 `json:"E"` 288 | } 289 | 290 | type OCOOrderUpdateEventOrder struct { 291 | Symbol string `json:"s"` 292 | ClientOrderID string `json:"c"` 293 | OrderID int64 `json:"i"` 294 | } 295 | --------------------------------------------------------------------------------