diff --git a/.appveyor.yml b/.appveyor.yml index 03edc788..e982c2ae 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,15 +13,35 @@ environment: GO111MODULE: on NODEJS_VER: 10.15.3 APPVEYOR_SAVE_CACHE_ON_ERROR: true + POSTGRES_PATH: C:\Program Files\PostgreSQL\9.6 + PGUSER: postgres + PGPASSWORD: Password12! + POSTGRES_ENV_POSTGRES_USER: postgres + POSTGRES_ENV_POSTGRES_PASSWORD: Password12! + POSTGRES_ENV_POSTGRES_DB: gct_dev_ci + PSQL_USER: postgres + PSQL_HOST: localhost + PSQL_PASS: Password12! + PSQL_DBNAME: gct_dev_ci + PSQL_SSLMODE: disable stack: go 1.13.x +services: + - postgresql96 + +init: + - SET PATH=%POSTGRES_PATH%\bin;%PATH% + install: - set Path=C:\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin;%Path% - ps: Install-Product node $env:NODEJS_VER - cd c:\gopath\src\github.com\thrasher-corp\gocryptotrader\web - npm install +build_script: + - createdb gct_dev_ci + before_test: - cd c:\gopath\src\github.com\thrasher-corp\gocryptotrader - go get diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..4fea60b9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +go.mod text eol=lf +go.sum text eol=lf \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index cd01660e..6c539d02 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -17,6 +17,7 @@ In order to maintain a consistent style across the codebase, the following codin - In line with gofmt, for loops and if statements don't require parenthesis. Block style example: + ```go func SendHTTPRequest(method, path string, headers map[string]string, body io.Reader) (string, error) { result := strings.ToUpper(method) @@ -38,51 +39,51 @@ func SendHTTPRequest(method, path string, headers map[string]string, body io.Rea ``` ## Effective Go Guidelines + [CodeLingo](https://codelingo.io) automatically checks every pull request against the following guidelines from [Effective Go](https://golang.org/doc/effective_go.html). - ### Comment First Word as Subject + Doc comments work best as complete sentences, which allow a wide variety of automated presentations. The first sentence should be a one-sentence summary that starts with the name being declared. - ### Good Package Name -It's helpful if everyone using the package can use the same name -to refer to its contents, which implies that the package name should -be good: short, concise, evocative. By convention, packages are -given lower case, single-word names; there should be no need for -underscores or mixedCaps. Err on the side of brevity, since everyone -using your package will be typing that name. And don't worry about -collisions a priori. The package name is only the default name for -imports; it need not be unique across all source code, and in the -rare case of a collision the importing package can choose a different -name to use locally. In any case, confusion is rare because the file + +It's helpful if everyone using the package can use the same name +to refer to its contents, which implies that the package name should +be good: short, concise, evocative. By convention, packages are +given lower case, single-word names; there should be no need for +underscores or mixedCaps. Err on the side of brevity, since everyone +using your package will be typing that name. And don't worry about +collisions a priori. The package name is only the default name for +imports; it need not be unique across all source code, and in the +rare case of a collision the importing package can choose a different +name to use locally. In any case, confusion is rare because the file name in the import determines just which package is being used. - ### Package Comment -Every package should have a package comment, a block comment preceding the package clause. -For multi-file packages, the package comment only needs to be present in one file, and any one will do. -The package comment should introduce the package and provide information relevant to the package as a + +Every package should have a package comment, a block comment preceding the package clause. +For multi-file packages, the package comment only needs to be present in one file, and any one will do. +The package comment should introduce the package and provide information relevant to the package as a whole. It will appear first on the godoc page and should set up the detailed documentation that follows. - ### Single Method Interface Name -By convention, one-method interfaces are named by the method name plus an -er suffix + +By convention, one-method interfaces are named by the method name plus an -er suffix or similar modification to construct an agent noun: Reader, Writer, Formatter, CloseNotifier etc. -There are a number of such names and it's productive to honor them and the function names they capture. -Read, Write, Close, Flush, String and so on have canonical signatures and meanings. To avoid confusion, -don't give your method one of those names unless it has the same signature and meaning. Conversely, -if your type implements a method with the same meaning as a method on a well-known type, give it the +There are a number of such names and it's productive to honor them and the function names they capture. +Read, Write, Close, Flush, String and so on have canonical signatures and meanings. To avoid confusion, +don't give your method one of those names unless it has the same signature and meaning. Conversely, +if your type implements a method with the same meaning as a method on a well-known type, give it the same name and signature; call your string-converter method String not ToString. - ### Avoid Annotations in Comments -Comments do not need extra formatting such as banners of stars. The generated output -may not even be presented in a fixed-width font, so don't depend on spacing for alignment—godoc, -like gofmt, takes care of that. The comments are uninterpreted plain text, so HTML and other -annotations such as _this_ will reproduce verbatim and should not be used. One adjustment godoc -does do is to display indented text in a fixed-width font, suitable for program snippets. -The package comment for the fmt package uses this to good effect. +Comments do not need extra formatting such as banners of stars. The generated output +may not even be presented in a fixed-width font, so don't depend on spacing for alignment—godoc, +like gofmt, takes care of that. The comments are uninterpreted plain text, so HTML and other +annotations such as _this_ will reproduce verbatim and should not be used. One adjustment godoc +does do is to display indented text in a fixed-width font, suitable for program snippets. +The package comment for the fmt package uses this to good effect. diff --git a/.github/CONTRIBUTING_TEMPLATE.md b/.github/CONTRIBUTING_TEMPLATE.md index f4aecf7c..cff2cb3f 100644 --- a/.github/CONTRIBUTING_TEMPLATE.md +++ b/.github/CONTRIBUTING_TEMPLATE.md @@ -17,6 +17,7 @@ In order to maintain a consistent style across the codebase, the following codin - In line with gofmt, for loops and if statements don't require parenthesis. Block style example: + ```go func SendHTTPRequest(method, path string, headers map[string]string, body io.Reader) (string, error) { result := strings.ToUpper(method) @@ -38,9 +39,12 @@ func SendHTTPRequest(method, path string, headers map[string]string, body io.Rea ``` ## Effective Go Guidelines + [CodeLingo](https://codelingo.io) automatically checks every pull request against the following guidelines from [Effective Go](https://golang.org/doc/effective_go.html). {{range .}} + ### {{.title}} + {{.body}} -{{end}} \ No newline at end of file +{{end}} diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index cbf11442..75073df6 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -30,4 +30,4 @@ Please provide detailed steps for reproducing the issue. ### Failure Logs -By default, GoCryptoTrader stores its `debug.log` file in `%APPDATA%\GoCryptoTrader` on Windows and `~/.gocryptotrader` on Linux/Unix/macOS. Raw text or a link to a pastebin type site is preferred. \ No newline at end of file +By default and if file logging is enabled, GoCryptoTrader stores its `log.txt` file in `%APPDATA%\GoCryptoTrader\logs` on Windows and `~/.gocryptotrader/logs` on Linux/Unix/macOS. Raw text or a link to a pastebin type site is preferred. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d3ddb8a0..edabdd52 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ -# Description +# PR Description -Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. +Please include a summary of the change, feature or issue which this pull request addresses. Please also include relevant motivation and context. List any dependencies that are required for this change. Fixes # (issue) @@ -13,22 +13,22 @@ Please delete options that are not relevant and add an `x` in `[]` as item is co - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update -# How Has This Been Tested? +## How has this been tested -Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration and +also consider improving test coverage whilst working on a certain feature or package. -## Please also consider improving test coverage whilst working on a certain package +- [ ] go test ./... -race +- [ ] golangci-lint run +- [ ] Test X -- [ ] Test A -- [ ] Test B - -# Checklist: +## Checklist - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] I have made corresponding changes to the documentation and regenerated documentation via the documentation tool +- [ ] I have made corresponding changes to the documentation and regenerated documentation via the documentation tool - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally and on Travis with my changes -- [ ] Any dependent changes have been merged and published in downstream modules \ No newline at end of file +- [ ] Any dependent changes have been merged and published in downstream modules diff --git a/.gitignore b/.gitignore index c3baa137..09c57015 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ lib .vscode testdata/dump +testdata/preengine_config.json testdata/writefiletest # InteliJ @@ -25,3 +26,5 @@ gocryptotrader # Output of the go coverage tool, specifically when used with LiteIDE *.out +sqlboiler.toml +sqlboiler.json diff --git a/.travis.yml b/.travis.yml index e04d4cf7..f41e6b4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,21 +16,54 @@ matrix: - language: go dist: xenial - name: 'GoCryptoTrader [back-end] [linux]' + name: 'GoCryptoTrader [back-end] [linux] [64-bit]' go: - 1.13.x env: - GO111MODULE=on + - PSQL_USER=postgres + - PSQL_HOST=localhost + - PSQL_DBNAME=gct_dev_ci install: true cache: directories: - $GOPATH/pkg/mod - + services: + - postgresql + before_script: + - psql -c 'create database gct_dev_ci;' -U postgres script: - make check after_success: - bash <(curl -s https://codecov.io/bash) + - language: go + dist: xenial + name: 'GoCryptoTrader [back-end] [linux] [32-bit]' + go: + - 1.13.x + env: + - GO111MODULE=on + - NO_RACE_TEST=1 + - PSQL_USER=postgres + - PSQL_HOST=localhost + - PSQL_DBNAME=gct_dev_ci + install: true + cache: + directories: + - $GOPATH/pkg/mod + services: + - postgresql + before_script: + - psql -c 'create database gct_dev_ci;' -U postgres + script: + - export GOARCH=386 + - export CGO_ENABLED=1 + - sudo apt-get install gcc-multilib + - make test + after_success: + - bash <(curl -s https://codecov.io/bash) + - language: go os: osx name: 'GoCryptoTrader [back-end] [darwin]' @@ -38,11 +71,22 @@ matrix: - 1.13.x env: - GO111MODULE=on + - PSQL_USER=postgres + - PSQL_HOST=localhost + - PSQL_DBNAME=gct_dev_ci + - PSQL_SSLMODE=disable + - PSQL_SKIPSQLCMD=true + - PSQL_TESTDBNAME=gct_dev_ci install: true cache: directories: - $GOPATH/pkg/mod - + before_install: + - rm -rf /usr/local/var/postgres + - initdb /usr/local/var/postgres + - pg_ctl start --pgdata /usr/local/var/postgres + - createuser -s postgres + - psql -c 'create database gct_dev_ci;' -U postgres script: - make check after_success: diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 3f7eeb20..3453fc18 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -8,8 +8,8 @@ ermalguni | https://github.com/ermalguni vadimzhukck | https://github.com/vadimzhukck 140am | https://github.com/140am marcofranssen | https://github.com/marcofranssen -cranktakular | https://github.com/cranktakular MadCozBadd | https://github.com/MadCozBadd +cranktakular | https://github.com/cranktakular leilaes | https://github.com/leilaes crackcomm | https://github.com/crackcomm andreygrehov | https://github.com/andreygrehov @@ -30,8 +30,9 @@ frankzougc | https://github.com/frankzougc starit | https://github.com/starit Jimexist | https://github.com/Jimexist lookfirst | https://github.com/lookfirst +idoall | https://github.com/idoall mattkanwisher | https://github.com/mattkanwisher mKurrels | https://github.com/mKurrels m1kola | https://github.com/m1kola cavapoo2 | https://github.com/cavapoo2 -zeldrinn | https://github.com/zeldrinn \ No newline at end of file +zeldrinn | https://github.com/zeldrinn diff --git a/Dockerfile b/Dockerfile index a82cff59..2524f16b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,18 @@ -FROM golang:1.12 as build +FROM golang:1.13 as build WORKDIR /go/src/github.com/thrasher-corp/gocryptotrader COPY . . RUN GO111MODULE=on go mod vendor RUN mv -vn config_example.json config.json \ - && GOARCH=386 GOOS=linux CGO_ENABLED=0 go build . \ - && mv gocryptotrader /go/bin/gocryptotrader + && GOARCH=386 GOOS=linux go build . \ + && GOARCH=386 GOOS=linux go build ./cmd/gctcli \ + && mv gocryptotrader /go/bin/gocryptotrader \ + && mv gctcli /go/bin/gctcli FROM alpine:latest -RUN apk update && apk add --no-cache ca-certificates +VOLUME /root/.gocryptotrader +RUN apk update && apk add --no-cache ca-certificates bash COPY --from=build /go/bin/gocryptotrader /app/ +COPY --from=build /go/bin/gctcli /app/ COPY --from=build /go/src/github.com/thrasher-corp/gocryptotrader/config.json /app/ -EXPOSE 9050 -CMD ["/app/gocryptotrader"] +EXPOSE 9050-9053 +ENTRYPOINT [ "/app/gocryptotrader" ] diff --git a/Makefile b/Makefile index 6a5c7eff..7094b991 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,8 @@ LINTBIN = $(GOPATH)/bin/golangci-lint GCTLISTENPORT=9050 GCTPROFILERLISTENPORT=8085 CRON = $(TRAVIS_EVENT_TYPE) +DRIVER ?= psql +RACE_FLAG := $(if $(NO_RACE_TEST),,-race) get: GO111MODULE=on go get $(GCTPKG) @@ -18,9 +20,9 @@ check: linter test test: ifeq ($(CRON), cron) - go test -race -tags=mock_test_off -coverprofile=coverage.txt -covermode=atomic ./... + go test $(RACE_FLAG) -tags=mock_test_off -coverprofile=coverage.txt -covermode=atomic ./... else - go test -race -coverprofile=coverage.txt -covermode=atomic ./... + go test $(RACE_FLAG) -coverprofile=coverage.txt -covermode=atomic ./... endif build: @@ -41,7 +43,14 @@ update_deps: .PHONY: profile_heap profile_heap: go tool pprof -http "localhost:$(GCTPROFILERLISTENPORT)" 'http://localhost:$(GCTLISTENPORT)/debug/pprof/heap' - + .PHONY: profile_cpu profile_cpu: - go tool pprof -http "localhost:$(GCTPROFILERLISTENPORT)" 'http://localhost:$(GCTLISTENPORT)/debug/pprof/profile' \ No newline at end of file + go tool pprof -http "localhost:$(GCTPROFILERLISTENPORT)" 'http://localhost:$(GCTLISTENPORT)/debug/pprof/profile' + +gen_db_models: +ifeq ($(DRIVER), psql) + sqlboiler -o database/models/postgres -p postgres --no-auto-timestamps --wipe $(DRIVER) +else + sqlboiler -o database/models/sqlite3 -p sqlite3 --no-auto-timestamps --wipe $(DRIVER) +endif diff --git a/README.md b/README.md index 76104bbc..5aee76a4 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | COINUT | Yes | Yes | NA | | Exmo | Yes | NA | NA | | CoinbasePro | Yes | Yes | No| +| Coinbene | Yes | No | No | | GateIO | Yes | Yes | NA | | Gemini | Yes | Yes | No | | HitBTC | Yes | Yes | No | @@ -53,19 +54,26 @@ We are aiming to support the top 20 highest volume exchanges based off the [Coin ## Current Features -+ Support for all Exchange fiat and digital currencies, with the ability to individually toggle them on/off. ++ Support for all exchange fiat and digital currencies, with the ability to individually toggle them on/off. + AES256 encrypted config file. + REST API support for all exchanges. + Websocket support for applicable exchanges. + Ability to turn off/on certain exchanges. -+ Ability to adjust manual polling timer for exchanges. + Communication packages (Slack, SMS via SMSGlobal, Telegram and SMTP) + HTTP rate limiter package. ++ Unified API for exchange usage. ++ Customisation of HTTP client features including setting a proxy, user agent and adjusting transport settings. ++ NTP client package. ++ Database support (Postgres and SQLite3). See [database](/database/README.md). ++ OTP generation tool. See [gen otp](/cmd/gen_otp). ++ Connection monitor package. ++ gRPC service and JSON RPC proxy. See [gRPC service](/gctrpc/README.md). ++ gRPC client. See [gctcli](/cmd/gctcli/README.md). + Forex currency converter packages (CurrencyConverterAPI, CurrencyLayer, Fixer.io, OpenExchangeRates) + Packages for handling currency pairs, tickers and orderbooks. + Portfolio management tool; fetches balances from supported exchanges and allows for custom address tracking. + Basic event trigger system. -+ WebGUI. ++ WebGUI (discontinued). ## Planned Features @@ -128,42 +136,41 @@ Binaries will be published once the codebase reaches a stable condition. ### A very special thank you to all who have contributed to this program: -|User|Github|Contribution Amount| -|--|--|--| -| thrasher- | https://github.com/thrasher- | 548 | -| shazbert | https://github.com/shazbert | 176 | -| gloriousCode | https://github.com/gloriousCode | 155 | -| xtda | https://github.com/xtda | 18 | -| ermalguni | https://github.com/ermalguni | 14 | -| vadimzhukck | https://github.com/vadimzhukck | 10 | -| 140am | https://github.com/140am | 8 | -| marcofranssen | https://github.com/marcofranssen | 8 | -| cranktakular | https://github.com/cranktakular | 5 | -| MadCozBadd | https://github.com/MadCozBadd | 3 | -| leilaes | https://github.com/leilaes | 3 | -| crackcomm | https://github.com/crackcomm | 3 | -| andreygrehov | https://github.com/andreygrehov | 2 | -| bretep | https://github.com/bretep | 2 | -| woshidama323 | https://github.com/woshidama323 | 2 | -| gam-phon | https://github.com/gam-phon | 2 | -| cornelk | https://github.com/cornelk | 2 | -| if1live | https://github.com/if1live | 2 | -| soxipy | https://github.com/soxipy | 2 | -| herenow | https://github.com/herenow | 2 | -| blombard | https://github.com/blombard | 1 | -| CodeLingoBot | https://github.com/CodeLingoBot | 1 | -| CodeLingoTeam | https://github.com/CodeLingoTeam | 1 | -| Daanikus | https://github.com/Daanikus | 1 | -| daniel-cohen | https://github.com/daniel-cohen | 1 | -| DirectX | https://github.com/DirectX | 1 | -| frankzougc | https://github.com/frankzougc | 1 | -| starit | https://github.com/starit | 1 | -| Jimexist | https://github.com/Jimexist | 1 | -| lookfirst | https://github.com/lookfirst | 1 | -| mattkanwisher | https://github.com/mattkanwisher | 1 | -| mKurrels | https://github.com/mKurrels | 1 | -| m1kola | https://github.com/m1kola | 1 | -| cavapoo2 | https://github.com/cavapoo2 | 1 | -| zeldrinn | https://github.com/zeldrinn | 1 | - - +|User|Contribution Amount| +|--|--| +| [thrasher-](https://github.com/thrasher-) | 551 | +| [shazbert](https://github.com/shazbert) | 176 | +| [gloriousCode](https://github.com/gloriousCode) | 155 | +| [xtda](https://github.com/xtda) | 18 | +| [ermalguni](https://github.com/ermalguni) | 14 | +| [vadimzhukck](https://github.com/vadimzhukck) | 10 | +| [140am](https://github.com/140am) | 8 | +| [marcofranssen](https://github.com/marcofranssen) | 8 | +| [MadCozBadd](https://github.com/MadCozBadd) | 6 | +| [cranktakular](https://github.com/cranktakular) | 5 | +| [leilaes](https://github.com/leilaes) | 3 | +| [crackcomm](https://github.com/crackcomm) | 3 | +| [andreygrehov](https://github.com/andreygrehov) | 2 | +| [bretep](https://github.com/bretep) | 2 | +| [woshidama323](https://github.com/woshidama323) | 2 | +| [gam-phon](https://github.com/gam-phon) | 2 | +| [cornelk](https://github.com/cornelk) | 2 | +| [if1live](https://github.com/if1live) | 2 | +| [soxipy](https://github.com/soxipy) | 2 | +| [herenow](https://github.com/herenow) | 2 | +| [blombard](https://github.com/blombard) | 1 | +| [CodeLingoBot](https://github.com/CodeLingoBot) | 1 | +| [CodeLingoTeam](https://github.com/CodeLingoTeam) | 1 | +| [Daanikus](https://github.com/Daanikus) | 1 | +| [daniel-cohen](https://github.com/daniel-cohen) | 1 | +| [DirectX](https://github.com/DirectX) | 1 | +| [frankzougc](https://github.com/frankzougc) | 1 | +| [starit](https://github.com/starit) | 1 | +| [Jimexist](https://github.com/Jimexist) | 1 | +| [lookfirst](https://github.com/lookfirst) | 1 | +| [idoall](https://github.com/idoall) | 1 | +| [mattkanwisher](https://github.com/mattkanwisher) | 1 | +| [mKurrels](https://github.com/mKurrels) | 1 | +| [m1kola](https://github.com/m1kola) | 1 | +| [cavapoo2](https://github.com/cavapoo2) | 1 | +| [zeldrinn](https://github.com/zeldrinn) | 1 | diff --git a/tools/config/config.go b/cmd/config/config.go similarity index 80% rename from tools/config/config.go rename to cmd/config/config.go index 74f23b15..3274b9ed 100644 --- a/tools/config/config.go +++ b/cmd/config/config.go @@ -2,9 +2,10 @@ package main import ( "flag" + "io/ioutil" "log" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/file" "github.com/thrasher-corp/gocryptotrader/config" ) @@ -42,19 +43,19 @@ func main() { key = string(result) } - file, err := common.ReadFile(inFile) + fileData, err := ioutil.ReadFile(inFile) if err != nil { log.Fatalf("Unable to read input file %s. Error: %s.", inFile, err) } - if config.ConfirmECS(file) && encrypt { + if config.ConfirmECS(fileData) && encrypt { log.Println("File is already encrypted. Decrypting..") encrypt = false } - if !config.ConfirmECS(file) && !encrypt { + if !config.ConfirmECS(fileData) && !encrypt { var result interface{} - errf := config.ConfirmConfigJSON(file, result) + errf := config.ConfirmConfigJSON(fileData, result) if errf != nil { log.Fatal("File isn't in JSON format") } @@ -64,18 +65,18 @@ func main() { var data []byte if encrypt { - data, err = config.EncryptConfigFile(file, []byte(key)) + data, err = config.EncryptConfigFile(fileData, []byte(key)) if err != nil { log.Fatalf("Unable to encrypt config data. Error: %s.", err) } } else { - data, err = config.DecryptConfigFile(file, []byte(key)) + data, err = config.DecryptConfigFile(fileData, []byte(key)) if err != nil { log.Fatalf("Unable to decrypt config data. Error: %s.", err) } } - err = common.WriteFile(outFile, data) + err = file.Write(outFile, data) if err != nil { log.Fatalf("Unable to write output file %s. Error: %s", outFile, err) } diff --git a/tools/config/config_test.go b/cmd/config/config_test.go similarity index 62% rename from tools/config/config_test.go rename to cmd/config/config_test.go index cd266ae5..7542cc09 100644 --- a/tools/config/config_test.go +++ b/cmd/config/config_test.go @@ -6,13 +6,13 @@ func TestEncryptOrDecrypt(t *testing.T) { reValue := EncryptOrDecrypt(true) if reValue != "encrypted" { t.Error( - "Test failed - Tools/Config/Config_test.go - EncryptOrDecrypt Error", + "Tools/Config/Config_test.go - EncryptOrDecrypt Error", ) } reValue = EncryptOrDecrypt(false) if reValue != "decrypted" { t.Error( - "Test failed - Tools/Config/Config_test.go - EncryptOrDecrypt Error", + "Tools/Config/Config_test.go - EncryptOrDecrypt Error", ) } } diff --git a/cmd/config_builder/builder.go b/cmd/config_builder/builder.go new file mode 100644 index 00000000..8c15b5ba --- /dev/null +++ b/cmd/config_builder/builder.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "log" + "sync" + + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/engine" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" +) + +func main() { + var err error + engine.Bot, err = engine.New() + if err != nil { + log.Fatalf("Failed to initialise engine. Err: %s", err) + } + + log.Printf("Loading exchanges..") + var wg sync.WaitGroup + for x := range exchange.Exchanges { + name := exchange.Exchanges[x] + err = engine.LoadExchange(name, true, &wg) + if err != nil { + log.Printf("Failed to load exchange %s. Err: %s", name, err) + continue + } + } + wg.Wait() + log.Println("Done.") + + var cfgs []config.ExchangeConfig + for x := range engine.Bot.Exchanges { + var cfg *config.ExchangeConfig + cfg, err = engine.Bot.Exchanges[x].GetDefaultConfig() + if err != nil { + log.Printf("Failed to get exchanges default config. Err: %s", err) + continue + } + log.Printf("Adding %s", engine.Bot.Exchanges[x].GetName()) + cfgs = append(cfgs, *cfg) + } + + data, err := json.MarshalIndent(cfgs, "", " ") + if err != nil { + log.Fatalf("Unable to marshal cfgs. Err: %s", err) + } + + log.Println(string(data)) +} diff --git a/cmd/dbmigrate/main.go b/cmd/dbmigrate/main.go new file mode 100644 index 00000000..496daadd --- /dev/null +++ b/cmd/dbmigrate/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "os" + "runtime" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/core" + "github.com/thrasher-corp/gocryptotrader/database" + dbPSQL "github.com/thrasher-corp/gocryptotrader/database/drivers/postgres" + dbsqlite3 "github.com/thrasher-corp/gocryptotrader/database/drivers/sqlite3" + "github.com/thrasher-corp/gocryptotrader/database/repository" + "github.com/thrasher-corp/goose" +) + +var ( + dbConn *database.Db + configFile string + defaultDataDir string + migrationDir string + command string + args string +) + +func openDbConnection(driver string) (err error) { + if driver == database.DBPostgreSQL { + dbConn, err = dbPSQL.Connect() + if err != nil { + return fmt.Errorf("database failed to connect: %v Some features that utilise a database will be unavailable", err) + } + return nil + } else if driver == database.DBSQLite || driver == database.DBSQLite3 { + dbConn, err = dbsqlite3.Connect() + if err != nil { + return fmt.Errorf("database failed to connect: %v Some features that utilise a database will be unavailable", err) + } + return nil + } + return errors.New("no connection established") +} + +func main() { + fmt.Println("GoCryptoTrader database migration tool") + fmt.Println(core.Copyright) + fmt.Println() + + defaultPath, err := config.GetFilePath("") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + flag.StringVar(&command, "command", "", "command to run status|up|up-by-one|up-to|down|create") + flag.StringVar(&args, "args", "", "arguments to pass to goose") + + flag.StringVar(&configFile, "config", defaultPath, "config file to load") + flag.StringVar(&defaultDataDir, "datadir", common.GetDefaultDataDir(runtime.GOOS), "default data directory for GoCryptoTrader files") + flag.StringVar(&migrationDir, "migrationdir", database.MigrationDir, "override migration folder") + + flag.Parse() + + conf := config.GetConfig() + + err = conf.LoadConfig(configFile, true) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if !conf.Database.Enabled { + fmt.Println("Database support is disabled") + os.Exit(1) + } + err = openDbConnection(conf.Database.Driver) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + drv := repository.GetSQLDialect() + + if drv == database.DBSQLite || drv == database.DBSQLite3 { + fmt.Printf("Database file: %s\n", conf.Database.Database) + } else { + fmt.Printf("Connected to: %s\n", conf.Database.Host) + } + + if command == "" { + _ = goose.Run("status", dbConn.SQL, drv, migrationDir, "") + fmt.Println() + flag.Usage() + return + } + + if err = goose.Run(command, dbConn.SQL, drv, migrationDir, args); err != nil { + fmt.Println(err) + } +} diff --git a/cmd/documentation/README.md b/cmd/documentation/README.md new file mode 100644 index 00000000..e6fb031f --- /dev/null +++ b/cmd/documentation/README.md @@ -0,0 +1,97 @@ +# GoCryptoTrader package Documentation + + + + +[![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/cmd/documentation) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This documentation package is part of the GoCryptoTrader codebase. + +## This is still in active development + +You can track ideas, planned features and what's in progresss on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for documentation + +#### This tool allows for the generation of new documentation through templating + +From the `gocryptotrader/cmd/documentation/` folder, using the go command: **go run documentation.go** this will auto-generate and regenerate documentation across the **GoCryptoTrader** code base. +>Using the -v command will, ie **go run documentation.go -v** put the tool into verbose mode allowing you to see what is happening with a little more depth. + +Be aware, this tool will: +- Works off a configuration JSON file located at ``gocryptotrader/cmd/documentation/`` for future use with multiple repositories. +- Automatically find the directory and file tree for the GoCryptoTrader source code and alert you of undocumented file systems which **need** to be updated. +- Automatically find the template folder tree. +- Fetch an updated contributor list and rank on pull request commit amount +- Sets up core folder docs for the root directory tree including **LICENSE** and **CONTRIBUTORS** + +### config.json example + +```json +{ +"githubRepo": "https://api.github.com/repos/thrasher-corp/gocryptotrader", This is your current repo +"exclusionList": { This allows for excluded directories and files +"Files": null, +"Directories": [ +"_templates", +".git", +"web" +] +}, +"rootReadmeActive": true, allows a root directory README.md +"licenseFileActive": true, allows for a license file to be generated +"contributorFileActive": true, fetches a new contributor list +"referencePathToRepo": "../../" +} +``` +### Template example +>place a new template **example_file.tmpl** located in the current gocryptotrader/cmd/documentation/ folder; when the documentation tool finishes it will give you the define template associated name e.g. ``Template not found for path ../../cmd/documentation create new template with \{\{define "cmd documentation" -\}\} TEMPLATE HERE \{\{end}}`` so you can replace the below example with ``\{\{define "cmd documentation" -}}`` + +``` +\{\{\define "example_definition_created_by_documentation_tool" -}} +\{\{\template "header" .}} +## Current Features for documentation + +#### A concise blurb about the package or tool system + ++ Coding examples +import "github.com/thrasher-corp/gocryptotrader/something" +testString := "aAaAa" +upper := strings.ToUpper(testString) +// upper == "AAAAA" + +{\{\template "contributions"}} +{\{\template "donations"}} +{\{\end}} +``` + +### ALL NEW UPDATES AND FILE SYSTEM ADDITIONS NEED A DOCUMENTATION UPDATE USING THIS TOOL OR PR MERGE REQUEST MAY BE POSTPONED. + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** diff --git a/cmd/documentation/cmd_templates/documentation.tmpl b/cmd/documentation/cmd_templates/documentation.tmpl new file mode 100644 index 00000000..1f6d4956 --- /dev/null +++ b/cmd/documentation/cmd_templates/documentation.tmpl @@ -0,0 +1,63 @@ +{{define "cmd documentation" -}} +{{template "header" .}} +## Current Features for {{.Name}} + +#### This tool allows for the generation of new documentation through templating + +From the `gocryptotrader/cmd/documentation/` folder, using the go command: **go run documentation.go** this will auto-generate and regenerate documentation across the **GoCryptoTrader** code base. +>Using the -v command will, ie **go run documentation.go -v** put the tool into verbose mode allowing you to see what is happening with a little more depth. + +Be aware, this tool will: +- Works off a configuration JSON file located at ``gocryptotrader/cmd/documentation/`` for future use with multiple repositories. +- Automatically find the directory and file tree for the GoCryptoTrader source code and alert you of undocumented file systems which **need** to be updated. +- Automatically find the template folder tree. +- Fetch an updated contributor list and rank on pull request commit amount +- Sets up core folder docs for the root directory tree including **LICENSE** and **CONTRIBUTORS** + +### config.json example + +```json +{ +"githubRepo": "https://api.github.com/repos/thrasher-corp/gocryptotrader", This is your current repo +"exclusionList": { This allows for excluded directories and files +"Files": null, +"Directories": [ +"_templates", +".git", +"web" +] +}, +"rootReadmeActive": true, allows a root directory README.md +"licenseFileActive": true, allows for a license file to be generated +"contributorFileActive": true, fetches a new contributor list +"referencePathToRepo": "../../" +} +``` +### Template example +>place a new template **example_file.tmpl** located in the current gocryptotrader/cmd/documentation/ folder; when the documentation tool finishes it will give you the define template associated name e.g. ``Template not found for path ../../cmd/documentation create new template with \{\{define "cmd documentation" -\}\} TEMPLATE HERE \{\{end}}`` so you can replace the below example with ``\{\{define "cmd documentation" -}}`` + +``` +\{\{\define "example_definition_created_by_documentation_tool" -}} +\{\{\template "header" .}} +## Current Features for {{.Name}} + +#### A concise blurb about the package or tool system + ++ Coding examples +import "github.com/thrasher-corp/gocryptotrader/something" +testString := "aAaAa" +upper := strings.ToUpper(testString) +// upper == "AAAAA" + +{\{\template "contributions"}} +{\{\template "donations"}} +{\{\end}} +``` + +### ALL NEW UPDATES AND FILE SYSTEM ADDITIONS NEED A DOCUMENTATION UPDATE USING THIS TOOL OR PR MERGE REQUEST MAY BE POSTPONED. + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations"}} +{{end}} diff --git a/tools/documentation/common_templates/common_readme.tmpl b/cmd/documentation/common_templates/common_readme.tmpl similarity index 91% rename from tools/documentation/common_templates/common_readme.tmpl rename to cmd/documentation/common_templates/common_readme.tmpl index e91f1daa..64200f7c 100644 --- a/tools/documentation/common_templates/common_readme.tmpl +++ b/cmd/documentation/common_templates/common_readme.tmpl @@ -11,7 +11,7 @@ import "github.com/thrasher-corp/gocryptotrader/common" testString := "aAaAa" -upper := common.StringToUpper(testString) +upper := strings.ToUpper(testString) // upper == "AAAAA" ``` diff --git a/tools/documentation/communications_templates/base.tmpl b/cmd/documentation/communications_templates/base.tmpl similarity index 100% rename from tools/documentation/communications_templates/base.tmpl rename to cmd/documentation/communications_templates/base.tmpl diff --git a/tools/documentation/communications_templates/comms.tmpl b/cmd/documentation/communications_templates/comms.tmpl similarity index 100% rename from tools/documentation/communications_templates/comms.tmpl rename to cmd/documentation/communications_templates/comms.tmpl diff --git a/tools/documentation/communications_templates/slack.tmpl b/cmd/documentation/communications_templates/slack.tmpl similarity index 100% rename from tools/documentation/communications_templates/slack.tmpl rename to cmd/documentation/communications_templates/slack.tmpl diff --git a/tools/documentation/communications_templates/smsglobal.tmpl b/cmd/documentation/communications_templates/smsglobal.tmpl similarity index 100% rename from tools/documentation/communications_templates/smsglobal.tmpl rename to cmd/documentation/communications_templates/smsglobal.tmpl diff --git a/tools/documentation/communications_templates/smtp.tmpl b/cmd/documentation/communications_templates/smtp.tmpl similarity index 100% rename from tools/documentation/communications_templates/smtp.tmpl rename to cmd/documentation/communications_templates/smtp.tmpl diff --git a/tools/documentation/communications_templates/telegram.tmpl b/cmd/documentation/communications_templates/telegram.tmpl similarity index 100% rename from tools/documentation/communications_templates/telegram.tmpl rename to cmd/documentation/communications_templates/telegram.tmpl diff --git a/tools/documentation/config_templates/config_readme.tmpl b/cmd/documentation/config_templates/config_readme.tmpl similarity index 100% rename from tools/documentation/config_templates/config_readme.tmpl rename to cmd/documentation/config_templates/config_readme.tmpl diff --git a/tools/documentation/currency_templates/currency_pair_readme.tmpl b/cmd/documentation/currency_templates/currency_pair_readme.tmpl similarity index 100% rename from tools/documentation/currency_templates/currency_pair_readme.tmpl rename to cmd/documentation/currency_templates/currency_pair_readme.tmpl diff --git a/tools/documentation/currency_templates/currency_readme.tmpl b/cmd/documentation/currency_templates/currency_readme.tmpl similarity index 100% rename from tools/documentation/currency_templates/currency_readme.tmpl rename to cmd/documentation/currency_templates/currency_readme.tmpl diff --git a/tools/documentation/currency_templates/currency_symbol_readme.tmpl b/cmd/documentation/currency_templates/currency_symbol_readme.tmpl similarity index 100% rename from tools/documentation/currency_templates/currency_symbol_readme.tmpl rename to cmd/documentation/currency_templates/currency_symbol_readme.tmpl diff --git a/tools/documentation/currency_templates/currency_translation_readme.tmpl b/cmd/documentation/currency_templates/currency_translation_readme.tmpl similarity index 100% rename from tools/documentation/currency_templates/currency_translation_readme.tmpl rename to cmd/documentation/currency_templates/currency_translation_readme.tmpl diff --git a/tools/documentation/currency_templates/fx.tmpl b/cmd/documentation/currency_templates/fx.tmpl similarity index 100% rename from tools/documentation/currency_templates/fx.tmpl rename to cmd/documentation/currency_templates/fx.tmpl diff --git a/tools/documentation/currency_templates/fx_base.tmpl b/cmd/documentation/currency_templates/fx_base.tmpl similarity index 100% rename from tools/documentation/currency_templates/fx_base.tmpl rename to cmd/documentation/currency_templates/fx_base.tmpl diff --git a/tools/documentation/currency_templates/fx_currencyconverterapi.tmpl b/cmd/documentation/currency_templates/fx_currencyconverterapi.tmpl similarity index 77% rename from tools/documentation/currency_templates/fx_currencyconverterapi.tmpl rename to cmd/documentation/currency_templates/fx_currencyconverterapi.tmpl index bad1a573..d6da5a20 100644 --- a/tools/documentation/currency_templates/fx_currencyconverterapi.tmpl +++ b/cmd/documentation/currency_templates/fx_currencyconverterapi.tmpl @@ -1,4 +1,4 @@ -{{define "currency forexprovider currencyconverter" -}} +{{define "currency forexprovider currencyconverterapi" -}} {{template "header" .}} ## Current Features for {{.Name}} @@ -11,15 +11,15 @@ + Individual package example below: ```go import ( -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencyconverter" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencyconverter" ) c := currencyconverter.CurrencyConverter{} // Define configuration newSettings := base.Settings{ - Name: "CurrencyConverter", + Name: "CurrencyConverter", Enabled: true, Verbose: false, RESTPollingDelay: time.Duration, diff --git a/tools/documentation/currency_templates/fx_currencylayer.tmpl b/cmd/documentation/currency_templates/fx_currencylayer.tmpl similarity index 83% rename from tools/documentation/currency_templates/fx_currencylayer.tmpl rename to cmd/documentation/currency_templates/fx_currencylayer.tmpl index e96a1513..b2654fd9 100644 --- a/tools/documentation/currency_templates/fx_currencylayer.tmpl +++ b/cmd/documentation/currency_templates/fx_currencylayer.tmpl @@ -11,15 +11,15 @@ + Individual package example below: ```go import ( -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencylayer" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencylayer" ) c := currencylayer.CurrencyLayer{} // Define configuration newSettings := base.Settings{ - Name: "CurrencyLayer", + Name: "CurrencyLayer", Enabled: true, Verbose: false, RESTPollingDelay: time.Duration, diff --git a/cmd/documentation/currency_templates/fx_exchangeratesapi.tmpl b/cmd/documentation/currency_templates/fx_exchangeratesapi.tmpl new file mode 100644 index 00000000..49830305 --- /dev/null +++ b/cmd/documentation/currency_templates/fx_exchangeratesapi.tmpl @@ -0,0 +1,40 @@ +{{define "currency forexprovider exchangeratesapi.io" -}} +{{template "header" .}} +## Current Features for {{.Name}} + ++ Fetches up to date curency data from [Exchange rates API]("http://exchangeratesapi.io") + +### How to enable + ++ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-currency-via-config-example) + ++ Individual package example below: +```go +import ( + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerates" +) + +c := exchangerates.ExchangeRates{} + +// Define configuration +newSettings := base.Settings{ + Name: "ExchangeRates", + Enabled: true, + Verbose: false, + RESTPollingDelay: time.Duration, + APIKey: "key", + APIKeyLvl: "keylvl", + PrimaryProvider: true, +} + +c.Setup(newSettings) + +mapstringfloat, err := c.GetRates("USD", "EUR,CHY") +// Handle error +``` + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations"}} +{{- end}} \ No newline at end of file diff --git a/tools/documentation/currency_templates/fx_fixer.tmpl b/cmd/documentation/currency_templates/fx_fixer.tmpl similarity index 78% rename from tools/documentation/currency_templates/fx_fixer.tmpl rename to cmd/documentation/currency_templates/fx_fixer.tmpl index 346e9b31..60a1e4dd 100644 --- a/tools/documentation/currency_templates/fx_fixer.tmpl +++ b/cmd/documentation/currency_templates/fx_fixer.tmpl @@ -1,4 +1,4 @@ -{{define "currency forexprovider fixer" -}} +{{define "currency forexprovider fixer.io" -}} {{template "header" .}} ## Current Features for {{.Name}} @@ -11,15 +11,15 @@ + Individual package example below: ```go import ( -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/fixer.io" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/fixer.io" ) c := fixer.Fixer{} // Define configuration newSettings := base.Settings{ - Name: "Fixer", + Name: "Fixer", Enabled: true, Verbose: false, RESTPollingDelay: time.Duration, diff --git a/tools/documentation/currency_templates/fx_openexchangerates.tmpl b/cmd/documentation/currency_templates/fx_openexchangerates.tmpl similarity index 82% rename from tools/documentation/currency_templates/fx_openexchangerates.tmpl rename to cmd/documentation/currency_templates/fx_openexchangerates.tmpl index 72dbd7c3..372e1936 100644 --- a/tools/documentation/currency_templates/fx_openexchangerates.tmpl +++ b/cmd/documentation/currency_templates/fx_openexchangerates.tmpl @@ -11,15 +11,15 @@ + Individual package example below: ```go import ( -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/openexchangerates" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/openexchangerates" ) c := openexchangerates.OXR{} // Define configuration newSettings := base.Settings{ - Name: "openexchangerates", + Name: "openexchangerates", Enabled: true, Verbose: false, RESTPollingDelay: time.Duration, diff --git a/cmd/documentation/documentation.go b/cmd/documentation/documentation.go new file mode 100644 index 00000000..1b7fe352 --- /dev/null +++ b/cmd/documentation/documentation.go @@ -0,0 +1,487 @@ +package main + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "html/template" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/core" +) + +const ( + // DefaultRepo is the main example repository + DefaultRepo = "https://api.github.com/repos/thrasher-corp/gocryptotrader" + + // GithubAPIEndpoint allows the program to query your repository + // contributor list + GithubAPIEndpoint = "/contributors" + + // LicenseFile defines a license file + LicenseFile = "LICENSE" + + // ContributorFile defines contributor file + ContributorFile = "CONTRIBUTORS" +) + +var ( + // DefaultExcludedDirectories defines the basic directory exclusion list for GCT + DefaultExcludedDirectories = []string{".github", + ".git", + "node_modules", + ".vscode", + ".idea", + "cmd_templates", + "common_templates", + "communications_templates", + "config_templates", + "currency_templates", + "events_templates", + "exchanges_templates", + "portfolio_templates", + "root_templates", + "sub_templates", + "testdata_templates", + "tools_templates", + "web_templates", + } + + // global flag for verbosity + verbose bool + // current tool directory to specify working templates + toolDir string + // exposes root directory if outside of document tool directory + repoDir string + // is a broken down version of the documentation tool dir for cross platform + // checking + ref = []string{"gocryptotrader", "cmd", "documentation"} +) + +// Contributor defines an account associated with this code base by doing +// contributions +type Contributor struct { + Login string `json:"login"` + URL string `json:"html_url"` + Contributions int `json:"contributions"` +} + +// Config defines the running config to deploy documentation across a github +// repository including exclusion lists for files and directories +type Config struct { + GithubRepo string `json:"githubRepo"` + Exclusions Exclusions `json:"exclusionList"` + RootReadme bool `json:"rootReadmeActive"` + LicenseFile bool `json:"licenseFileActive"` + ContributorFile bool `json:"contributorFileActive"` +} + +// Exclusions defines the exclusion list so documents are not generated +type Exclusions struct { + Files []string `json:"Files"` + Directories []string `json:"Directories"` +} + +// DocumentationDetails defines parameters to update documentation +type DocumentationDetails struct { + Directories []string + Tmpl *template.Template + Contributors []Contributor + Config *Config +} + +// Attributes defines specific documentation attributes when a template is +// executed +type Attributes struct { + Name string + Contributors []Contributor + NameURL string + Year int + CapitalName string +} + +func main() { + flag.BoolVar(&verbose, "v", false, "Verbose output") + flag.StringVar(&toolDir, "tooldir", "", "Pass in the documentation tool directory if outside tool folder") + flag.Parse() + + wd, err := os.Getwd() + if err != nil { + fmt.Println("Documentation tool error cannot get working dir:", err) + os.Exit(1) + } + + if strings.Contains(wd, filepath.Join(ref...)) { + rootdir := filepath.Dir(filepath.Dir(wd)) + repoDir = rootdir + toolDir = wd + } else { + if toolDir == "" { + fmt.Println("Please set documentation tool directory via the tooldir flag if working outside of tool directory") + os.Exit(1) + } + repoDir = wd + } + + fmt.Println(core.Banner) + fmt.Println("This will update and regenerate documentation for the different packages in your repo.") + fmt.Println() + + if verbose { + fmt.Println("Fetching configuration...") + } + + config, err := GetConfiguration() + if err != nil { + log.Fatalf("Documentation Generation Tool - GetConfiguration error %s", + err) + } + + if verbose { + fmt.Println("Fetching project directory tree...") + } + + dirList, err := GetProjectDirectoryTree(&config) + if err != nil { + log.Fatalf("Documentation Generation Tool - GetProjectDirectoryTree error %s", + err) + } + + var contributors []Contributor + if config.ContributorFile { + if verbose { + fmt.Println("Fetching repository contributor list...") + } + contributors, err = GetContributorList(config.GithubRepo) + if err != nil { + log.Fatalf("Documentation Generation Tool - GetContributorList error %s", + err) + } + + // Github API missing contributors + contributors = append(contributors, []Contributor{ + // idoall's contributors were forked and merged, so his contributions + // aren't automatically retrievable + { + Login: "idoall", + URL: "https://github.com/idoall", + Contributions: 1, + }, + { + Login: "mattkanwisher", + URL: "https://github.com/mattkanwisher", + Contributions: 1, + }, + { + Login: "mKurrels", + URL: "https://github.com/mKurrels", + Contributions: 1, + }, + { + Login: "m1kola", + URL: "https://github.com/m1kola", + Contributions: 1, + }, + { + Login: "cavapoo2", + URL: "https://github.com/cavapoo2", + Contributions: 1, + }, + { + Login: "zeldrinn", + URL: "https://github.com/zeldrinn", + Contributions: 1, + }, + }...) + + if verbose { + fmt.Println("Contributor List Fetched") + for i := range contributors { + fmt.Println(contributors[i].Login) + } + } + } else { + fmt.Println("Contributor list file disabled skipping fetching details") + } + + if verbose { + fmt.Println("Fetching template files...") + } + + tmpl, err := GetTemplateFiles() + if err != nil { + log.Fatalf("Documentation Generation Tool - GetTemplateFiles error %s", + err) + } + + if verbose { + fmt.Println("All core systems fetched, updating documentation...") + } + + err = UpdateDocumentation(DocumentationDetails{ + dirList, + tmpl, + contributors, + &config}) + if err != nil { + log.Fatalf("Documentation Generation Tool - UpdateDocumentation error %s", + err) + } + + fmt.Println("\nDocumentation Generation Tool - Finished") +} + +// GetConfiguration retrieves the documentation configuration +func GetConfiguration() (Config, error) { + var c Config + configFilePath := filepath.Join([]string{toolDir, "config.json"}...) + file, err := os.OpenFile(configFilePath, os.O_RDWR, os.ModePerm) + if err != nil { + fmt.Println("Creating configuration file, please check to add a different github repository path and change preferences") + + file, err = os.Create(configFilePath) + if err != nil { + return c, err + } + + // Set default params for configuration + c.GithubRepo = DefaultRepo + c.ContributorFile = true + c.LicenseFile = true + c.RootReadme = true + c.Exclusions.Directories = DefaultExcludedDirectories + + data, mErr := json.MarshalIndent(c, "", " ") + if mErr != nil { + return c, mErr + } + + _, err = file.WriteAt(data, 0) + if err != nil { + return c, err + } + } + + defer file.Close() + + config, err := ioutil.ReadAll(file) + if err != nil { + return c, err + } + + err = json.Unmarshal(config, &c) + if err != nil { + return c, err + } + + if c.GithubRepo == "" { + return c, errors.New("repository not set in config.json file, please change") + } + + return c, nil +} + +// IsExcluded returns if the file path is included in the exclusion list +func IsExcluded(path string, exclusion []string) bool { + for i := range exclusion { + if path == exclusion[i] { + return true + } + } + return false +} + +// GetProjectDirectoryTree uses filepath walk functions to get each individual +// directory name and path to match templates with +func GetProjectDirectoryTree(c *Config) ([]string, error) { + var directoryData []string + if c.RootReadme { // Projects root README.md + directoryData = append(directoryData, repoDir) + } + + if c.LicenseFile { // Standard license file + directoryData = append(directoryData, filepath.Join(repoDir, LicenseFile)) + } + + if c.ContributorFile { // Standard contributor file + directoryData = append(directoryData, filepath.Join(repoDir, ContributorFile)) + } + + walkfn := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + // Bypass what is contained in config.json directory exclusion + if IsExcluded(info.Name(), c.Exclusions.Directories) { + if verbose { + fmt.Println("Excluding Directory:", info.Name()) + } + return filepath.SkipDir + } + // Don't append parent directory + if strings.EqualFold(info.Name(), "..") { + return nil + } + directoryData = append(directoryData, path) + } + return nil + } + + return directoryData, filepath.Walk(repoDir, walkfn) +} + +// GetTemplateFiles parses and returns all template files in the documentation +// tree +func GetTemplateFiles() (*template.Template, error) { + tmpl := template.New("") + + walkfn := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + if path == "." || path == ".." { + return nil + } + + var parseError error + tmpl, parseError = tmpl.ParseGlob(filepath.Join(path, "*.tmpl")) + if parseError != nil { + if strings.Contains(parseError.Error(), "pattern matches no files") { + return nil + } + return parseError + } + return filepath.SkipDir + } + return nil + } + + return tmpl, filepath.Walk(toolDir, walkfn) +} + +// GetContributorList fetches a list of contributors from the github api +// endpoint +func GetContributorList(repo string) ([]Contributor, error) { + var resp []Contributor + return resp, common.SendHTTPGetRequest(repo+GithubAPIEndpoint, true, false, &resp) +} + +// GetDocumentationAttributes returns specific attributes for a file template +func GetDocumentationAttributes(packageName string, contributors []Contributor) Attributes { + return Attributes{ + Name: GetPackageName(packageName, false), + Contributors: contributors, + NameURL: GetGoDocURL(packageName), + Year: time.Now().Year(), + CapitalName: GetPackageName(packageName, true), + } +} + +// GetPackageName returns the package name after cleaning path as a string +func GetPackageName(name string, capital bool) string { + newStrings := strings.Split(name, " ") + var i int + if len(newStrings) > 1 { + i = 1 + } + if capital { + return strings.Title(newStrings[i]) + } + return newStrings[i] +} + +// GetGoDocURL returns a string for godoc package names +func GetGoDocURL(name string) string { + if strings.Contains(name, " ") { + return strings.Join(strings.Split(name, " "), "/") + } + if name == "testdata" || + name == "tools" || + name == ContributorFile || + name == LicenseFile { + return "" + } + return name +} + +// UpdateDocumentation generates or updates readme/documentation files across +// the codebase +func UpdateDocumentation(details DocumentationDetails) error { + for i := range details.Directories { + cutset := details.Directories[i][len(repoDir):] + if cutset != "" && cutset[0] == os.PathSeparator { + cutset = cutset[1:] + } + + data := strings.Split(cutset, string(os.PathSeparator)) + + var temp []string + for x := range data { + if data[x] == ".." { + continue + } + if data[x] == "" { + break + } + temp = append(temp, data[x]) + } + + var name string + if len(temp) == 0 { + name = "root" + } else { + name = strings.Join(temp, " ") + } + + if IsExcluded(name, details.Config.Exclusions.Files) { + if verbose { + fmt.Println("Excluding file:", name) + } + continue + } + + if details.Tmpl.Lookup(name) == nil { + fmt.Printf("Template not found for path %s create new template with {{define \"%s\" -}} TEMPLATE HERE {{end}}\n", + details.Directories[i], + name) + continue + } + + var mainPath string + if name == LicenseFile || name == ContributorFile { + mainPath = details.Directories[i] + } else { + mainPath = filepath.Join(details.Directories[i], "README.md") + } + + err := os.Remove(mainPath) + if err != nil && !(strings.Contains(err.Error(), "no such file or directory") || + strings.Contains(err.Error(), "The system cannot find the file specified.")) { + return err + } + + file, err := os.Create(mainPath) + if err != nil { + return err + } + + attr := GetDocumentationAttributes(name, details.Contributors) + + err = details.Tmpl.ExecuteTemplate(file, name, attr) + if err != nil { + file.Close() + return err + } + file.Close() + } + return nil +} diff --git a/tools/documentation/events_templates/events_readme.tmpl b/cmd/documentation/events_templates/events_readme.tmpl similarity index 100% rename from tools/documentation/events_templates/events_readme.tmpl rename to cmd/documentation/events_templates/events_readme.tmpl diff --git a/tools/documentation/exchanges_templates/alphapoint.tmpl b/cmd/documentation/exchanges_templates/alphapoint.tmpl similarity index 100% rename from tools/documentation/exchanges_templates/alphapoint.tmpl rename to cmd/documentation/exchanges_templates/alphapoint.tmpl diff --git a/tools/documentation/exchanges_templates/anx.tmpl b/cmd/documentation/exchanges_templates/anx.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/anx.tmpl rename to cmd/documentation/exchanges_templates/anx.tmpl index c1e2a351..ba79c69b 100644 --- a/tools/documentation/exchanges_templates/anx.tmpl +++ b/cmd/documentation/exchanges_templates/anx.tmpl @@ -33,22 +33,22 @@ main.go ```go var a exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "ANX" { - a = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "ANX" { + a = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := a.GetTickerPrice() +tick, err := a.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := a.GetOrderbookEx() +ob, err := a.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/binance.tmpl b/cmd/documentation/exchanges_templates/binance.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/binance.tmpl rename to cmd/documentation/exchanges_templates/binance.tmpl index 0b764357..aadea33f 100644 --- a/tools/documentation/exchanges_templates/binance.tmpl +++ b/cmd/documentation/exchanges_templates/binance.tmpl @@ -30,22 +30,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Binance" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Binance" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/bitfinex.tmpl b/cmd/documentation/exchanges_templates/bitfinex.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/bitfinex.tmpl rename to cmd/documentation/exchanges_templates/bitfinex.tmpl index 653349be..57a2c9f5 100644 --- a/tools/documentation/exchanges_templates/bitfinex.tmpl +++ b/cmd/documentation/exchanges_templates/bitfinex.tmpl @@ -30,22 +30,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bitfinex" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bitfinex" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/bitflyer.tmpl b/cmd/documentation/exchanges_templates/bitflyer.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/bitflyer.tmpl rename to cmd/documentation/exchanges_templates/bitflyer.tmpl index 9039e574..1e6259c8 100644 --- a/tools/documentation/exchanges_templates/bitflyer.tmpl +++ b/cmd/documentation/exchanges_templates/bitflyer.tmpl @@ -29,22 +29,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bitflyer" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bitflyer" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/bithumb.tmpl b/cmd/documentation/exchanges_templates/bithumb.tmpl similarity index 92% rename from tools/documentation/exchanges_templates/bithumb.tmpl rename to cmd/documentation/exchanges_templates/bithumb.tmpl index 1bee2047..849398cc 100644 --- a/tools/documentation/exchanges_templates/bithumb.tmpl +++ b/cmd/documentation/exchanges_templates/bithumb.tmpl @@ -29,22 +29,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bithumb" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bithumb" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/bitmex.tmpl b/cmd/documentation/exchanges_templates/bitmex.tmpl similarity index 92% rename from tools/documentation/exchanges_templates/bitmex.tmpl rename to cmd/documentation/exchanges_templates/bitmex.tmpl index 7c160828..69668b4c 100644 --- a/tools/documentation/exchanges_templates/bitmex.tmpl +++ b/cmd/documentation/exchanges_templates/bitmex.tmpl @@ -29,22 +29,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bitmex" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bitmex" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/bitstamp.tmpl b/cmd/documentation/exchanges_templates/bitstamp.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/bitstamp.tmpl rename to cmd/documentation/exchanges_templates/bitstamp.tmpl index c10ef122..ee1cadd0 100644 --- a/tools/documentation/exchanges_templates/bitstamp.tmpl +++ b/cmd/documentation/exchanges_templates/bitstamp.tmpl @@ -30,22 +30,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bitstamp" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bitstamp" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/bittrex.tmpl b/cmd/documentation/exchanges_templates/bittrex.tmpl similarity index 92% rename from tools/documentation/exchanges_templates/bittrex.tmpl rename to cmd/documentation/exchanges_templates/bittrex.tmpl index 2d3361c9..15409464 100644 --- a/tools/documentation/exchanges_templates/bittrex.tmpl +++ b/cmd/documentation/exchanges_templates/bittrex.tmpl @@ -29,22 +29,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bittrex" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bittrex" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/btcmarkets.tmpl b/cmd/documentation/exchanges_templates/btcmarkets.tmpl similarity index 91% rename from tools/documentation/exchanges_templates/btcmarkets.tmpl rename to cmd/documentation/exchanges_templates/btcmarkets.tmpl index af89bfb7..4d81557e 100644 --- a/tools/documentation/exchanges_templates/btcmarkets.tmpl +++ b/cmd/documentation/exchanges_templates/btcmarkets.tmpl @@ -5,6 +5,7 @@ ### Current Features + REST Support ++ Websocket Support ### How to enable @@ -29,22 +30,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "BTCMarkets" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "BTCMarkets" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } diff --git a/cmd/documentation/exchanges_templates/btse.tmpl b/cmd/documentation/exchanges_templates/btse.tmpl new file mode 100644 index 00000000..9df4638f --- /dev/null +++ b/cmd/documentation/exchanges_templates/btse.tmpl @@ -0,0 +1,99 @@ +{{define "exchanges btse" -}} +{{template "header" .}} +## BTCMarkets Exchange + +### Current Features + ++ REST Support ++ Websocket Support + +### How to enable + ++ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example) + ++ Individual package example below: + +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### How to do REST public/private calls + ++ If enabled via "configuration".json file the exchange will be added to the +IBotExchange array in the ```go var bot Bot``` and you will only be able to use +the wrapper interface functions for accessing exchange data. View routines.go +for an example of integration usage with GoCryptoTrader. Rudimentary example +below: + +main.go +```go +var b exchange.IBotExchange + +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "BTSE" { + b = bot.Exchanges[i] + } +} + +// Public calls - wrapper functions + +// Fetches current ticker information +tick, err := b.FetchTicker() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := b.FetchOrderbook() +if err != nil { + // Handle error +} + +// Private calls - wrapper functions - make sure your APIKEY and APISECRET are +// set and AuthenticatedAPISupport is set to true + +// Fetches current account information +accountInfo, err := b.GetAccountInfo() +if err != nil { + // Handle error +} +``` + ++ If enabled via individually importing package, rudimentary example below: + +```go +// Public calls + +// Fetches current ticker information +ticker, err := b.GetTicker() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := b.GetOrderBook() +if err != nil { + // Handle error +} + +// Private calls - make sure your APIKEY and APISECRET are set and +// AuthenticatedAPISupport is set to true + +// GetUserInfo returns account info +accountInfo, err := b.GetUserInfo(...) +if err != nil { + // Handle error +} + +// Submits an order and the exchange and returns its tradeID +tradeID, err := b.Trade(...) +if err != nil { + // Handle error +} +``` + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations"}} +{{end}} diff --git a/tools/documentation/exchanges_templates/coinbasepro.tmpl b/cmd/documentation/exchanges_templates/coinbasepro.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/coinbasepro.tmpl rename to cmd/documentation/exchanges_templates/coinbasepro.tmpl index 2abbde6c..05f0b049 100644 --- a/tools/documentation/exchanges_templates/coinbasepro.tmpl +++ b/cmd/documentation/exchanges_templates/coinbasepro.tmpl @@ -30,22 +30,22 @@ main.go ```go var c exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "CoinbasePro" { - c = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "CoinbasePro" { + c = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := c.GetTickerPrice() +tick, err := c.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := c.GetOrderbookEx() +ob, err := c.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/coinbene.tmpl b/cmd/documentation/exchanges_templates/coinbene.tmpl similarity index 90% rename from tools/documentation/exchanges_templates/coinbene.tmpl rename to cmd/documentation/exchanges_templates/coinbene.tmpl index f9de7d4c..7dc3b4d4 100644 --- a/tools/documentation/exchanges_templates/coinbene.tmpl +++ b/cmd/documentation/exchanges_templates/coinbene.tmpl @@ -30,22 +30,22 @@ main.go ```go var c exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Coinbene" { - c = bot.exchanges[i] +for i := range Bot.Exchanges { + if Bot.Exchanges[i].GetName() == "Coinbene" { + c = Bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := c.GetTickerPrice() +tick, err := c.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := c.GetOrderbookEx() +ob, err := c.FetchOrderbook() if err != nil { // Handle error } @@ -72,7 +72,7 @@ if err != nil { } // Fetches current orderbook information -ob, err := c.GetOrderBook() +ob, err := c.GetOrderbook() if err != nil { // Handle error } @@ -87,7 +87,7 @@ if err != nil { } // Submits an order and the exchange and returns its tradeID -tradeID, err := c.Trade(...) +resp, err := c.SubmitOrder(...) if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/coinut.tmpl b/cmd/documentation/exchanges_templates/coinut.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/coinut.tmpl rename to cmd/documentation/exchanges_templates/coinut.tmpl index 90453625..8b6f644c 100644 --- a/tools/documentation/exchanges_templates/coinut.tmpl +++ b/cmd/documentation/exchanges_templates/coinut.tmpl @@ -30,22 +30,22 @@ main.go ```go var c exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Coinut" { - c = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Coinut" { + c = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := c.GetTickerPrice() +tick, err := c.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := c.GetOrderbookEx() +ob, err := c.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/exchanges_orderbook_readme.tmpl b/cmd/documentation/exchanges_templates/exchanges_orderbook_readme.tmpl similarity index 95% rename from tools/documentation/exchanges_templates/exchanges_orderbook_readme.tmpl rename to cmd/documentation/exchanges_templates/exchanges_orderbook_readme.tmpl index 78de8a20..047d7c92 100644 --- a/tools/documentation/exchanges_templates/exchanges_orderbook_readme.tmpl +++ b/cmd/documentation/exchanges_templates/exchanges_orderbook_readme.tmpl @@ -16,7 +16,7 @@ exchange interface system set by exchange wrapper orderbook functions in Examples below: ```go -ob, err := yobitExchange.GetOrderbookEx() +ob, err := yobitExchange.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/exchanges_readme.tmpl b/cmd/documentation/exchanges_templates/exchanges_readme.tmpl similarity index 100% rename from tools/documentation/exchanges_templates/exchanges_readme.tmpl rename to cmd/documentation/exchanges_templates/exchanges_readme.tmpl diff --git a/tools/documentation/exchanges_templates/exchanges_stats_readme.tmpl b/cmd/documentation/exchanges_templates/exchanges_stats_readme.tmpl similarity index 100% rename from tools/documentation/exchanges_templates/exchanges_stats_readme.tmpl rename to cmd/documentation/exchanges_templates/exchanges_stats_readme.tmpl diff --git a/tools/documentation/exchanges_templates/exchanges_ticker_readme.tmpl b/cmd/documentation/exchanges_templates/exchanges_ticker_readme.tmpl similarity index 95% rename from tools/documentation/exchanges_templates/exchanges_ticker_readme.tmpl rename to cmd/documentation/exchanges_templates/exchanges_ticker_readme.tmpl index 0f47ef6d..afd210aa 100644 --- a/tools/documentation/exchanges_templates/exchanges_ticker_readme.tmpl +++ b/cmd/documentation/exchanges_templates/exchanges_ticker_readme.tmpl @@ -17,7 +17,7 @@ exchange interface system set by exchange wrapper orderbook functions in Examples below: ```go -tick, err := yobitExchange.GetTickerPrice() +tick, err := yobitExchange.FetchTicker() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/exmo.tmpl b/cmd/documentation/exchanges_templates/exmo.tmpl similarity index 92% rename from tools/documentation/exchanges_templates/exmo.tmpl rename to cmd/documentation/exchanges_templates/exmo.tmpl index da52c11e..4378a6dc 100644 --- a/tools/documentation/exchanges_templates/exmo.tmpl +++ b/cmd/documentation/exchanges_templates/exmo.tmpl @@ -29,22 +29,22 @@ main.go ```go var e exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Exmo" { - e = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Exmo" { + e = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := e.GetTickerPrice() +tick, err := e.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := e.GetOrderbookEx() +ob, err := e.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/gateio.tmpl b/cmd/documentation/exchanges_templates/gateio.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/gateio.tmpl rename to cmd/documentation/exchanges_templates/gateio.tmpl index f6b0f75c..2f311ac1 100644 --- a/tools/documentation/exchanges_templates/gateio.tmpl +++ b/cmd/documentation/exchanges_templates/gateio.tmpl @@ -29,22 +29,22 @@ main.go ```go var g exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "GateIO" { - g = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "GateIO" { + g = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := g.GetTickerPrice() +tick, err := g.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := g.GetOrderbookEx() +ob, err := g.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/gemini.tmpl b/cmd/documentation/exchanges_templates/gemini.tmpl similarity index 92% rename from tools/documentation/exchanges_templates/gemini.tmpl rename to cmd/documentation/exchanges_templates/gemini.tmpl index 296b83a3..9810785b 100644 --- a/tools/documentation/exchanges_templates/gemini.tmpl +++ b/cmd/documentation/exchanges_templates/gemini.tmpl @@ -29,22 +29,22 @@ main.go ```go var g exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Gemini" { - g = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Gemini" { + g = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := g.GetTickerPrice() +tick, err := g.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := g.GetOrderbookEx() +ob, err := g.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/hitbtc.tmpl b/cmd/documentation/exchanges_templates/hitbtc.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/hitbtc.tmpl rename to cmd/documentation/exchanges_templates/hitbtc.tmpl index 1860462b..624be46d 100644 --- a/tools/documentation/exchanges_templates/hitbtc.tmpl +++ b/cmd/documentation/exchanges_templates/hitbtc.tmpl @@ -30,22 +30,22 @@ main.go ```go var h exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "HitBTC" { - h = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "HitBTC" { + h = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := h.GetTickerPrice() +tick, err := h.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := h.GetOrderbookEx() +ob, err := h.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/huobi.tmpl b/cmd/documentation/exchanges_templates/huobi.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/huobi.tmpl rename to cmd/documentation/exchanges_templates/huobi.tmpl index 42db521f..f85ccd95 100644 --- a/tools/documentation/exchanges_templates/huobi.tmpl +++ b/cmd/documentation/exchanges_templates/huobi.tmpl @@ -29,22 +29,22 @@ main.go ```go var h exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Huobi" { - h = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Huobi" { + h = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := h.GetTickerPrice() +tick, err := h.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := h.GetOrderbookEx() +ob, err := h.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/itbit.tmpl b/cmd/documentation/exchanges_templates/itbit.tmpl similarity index 92% rename from tools/documentation/exchanges_templates/itbit.tmpl rename to cmd/documentation/exchanges_templates/itbit.tmpl index 1a8cca57..908c0e6d 100644 --- a/tools/documentation/exchanges_templates/itbit.tmpl +++ b/cmd/documentation/exchanges_templates/itbit.tmpl @@ -29,22 +29,22 @@ main.go ```go var i exchange.IBotExchange -for x := range bot.exchanges { - if bot.exchanges[x].GetName() == "Itbit" { - i = bot.exchanges[x] +for x := range bot.Exchanges { + if bot.Exchanges[x].GetName() == "Itbit" { + i = bot.Exchanges[x] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := i.GetTickerPrice() +tick, err := i.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := i.GetOrderbookEx() +ob, err := i.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/kraken.tmpl b/cmd/documentation/exchanges_templates/kraken.tmpl similarity index 92% rename from tools/documentation/exchanges_templates/kraken.tmpl rename to cmd/documentation/exchanges_templates/kraken.tmpl index 8b4d052c..3a6ec6a3 100644 --- a/tools/documentation/exchanges_templates/kraken.tmpl +++ b/cmd/documentation/exchanges_templates/kraken.tmpl @@ -29,22 +29,22 @@ main.go ```go var k exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Kraken" { - k = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Kraken" { + k = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := k.GetTickerPrice() +tick, err := k.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := k.GetOrderbookEx() +ob, err := k.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/lakebtc.tmpl b/cmd/documentation/exchanges_templates/lakebtc.tmpl similarity index 91% rename from tools/documentation/exchanges_templates/lakebtc.tmpl rename to cmd/documentation/exchanges_templates/lakebtc.tmpl index b422d6c8..28a0e9c3 100644 --- a/tools/documentation/exchanges_templates/lakebtc.tmpl +++ b/cmd/documentation/exchanges_templates/lakebtc.tmpl @@ -5,6 +5,7 @@ ### Current Features + REST Support ++ Websocket Support ### How to enable @@ -29,22 +30,22 @@ main.go ```go var l exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "LakeBTC" { - l = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "LakeBTC" { + l = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := l.GetTickerPrice() +tick, err := l.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := l.GetOrderbookEx() +ob, err := l.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/lbank.tmpl b/cmd/documentation/exchanges_templates/lbank.tmpl similarity index 92% rename from tools/documentation/exchanges_templates/lbank.tmpl rename to cmd/documentation/exchanges_templates/lbank.tmpl index 45fb9441..890fb91f 100644 --- a/tools/documentation/exchanges_templates/lbank.tmpl +++ b/cmd/documentation/exchanges_templates/lbank.tmpl @@ -29,22 +29,22 @@ main.go ```go var l exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Lbank" { - l = bot.exchanges[i] +for i := range Bot.Exchanges { + if Bot.Exchanges[i].GetName() == "Lbank" { + l = Bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := l.GetTickerPrice() +tick, err := l.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := l.GetOrderbookEx() +ob, err := l.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/localbitcoins.tmpl b/cmd/documentation/exchanges_templates/localbitcoins.tmpl similarity index 92% rename from tools/documentation/exchanges_templates/localbitcoins.tmpl rename to cmd/documentation/exchanges_templates/localbitcoins.tmpl index 13725b1c..8526c267 100644 --- a/tools/documentation/exchanges_templates/localbitcoins.tmpl +++ b/cmd/documentation/exchanges_templates/localbitcoins.tmpl @@ -29,22 +29,22 @@ main.go ```go var l exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "LocalBitcoins" { - l = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "LocalBitcoins" { + l = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := l.GetTickerPrice() +tick, err := l.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := l.GetOrderbookEx() +ob, err := l.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/nonce.tmpl b/cmd/documentation/exchanges_templates/nonce.tmpl similarity index 100% rename from tools/documentation/exchanges_templates/nonce.tmpl rename to cmd/documentation/exchanges_templates/nonce.tmpl diff --git a/tools/documentation/exchanges_templates/okcoin.tmpl b/cmd/documentation/exchanges_templates/okcoin.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/okcoin.tmpl rename to cmd/documentation/exchanges_templates/okcoin.tmpl index 3d4ba0e0..ec16b998 100644 --- a/tools/documentation/exchanges_templates/okcoin.tmpl +++ b/cmd/documentation/exchanges_templates/okcoin.tmpl @@ -30,22 +30,22 @@ main.go ```go var o exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "OKCoin" { - y = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "OKCoin" { + y = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := o.GetTickerPrice() +tick, err := o.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := o.GetOrderbookEx() +ob, err := o.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/okex.tmpl b/cmd/documentation/exchanges_templates/okex.tmpl similarity index 92% rename from tools/documentation/exchanges_templates/okex.tmpl rename to cmd/documentation/exchanges_templates/okex.tmpl index 368bb13f..9117ce49 100644 --- a/tools/documentation/exchanges_templates/okex.tmpl +++ b/cmd/documentation/exchanges_templates/okex.tmpl @@ -29,22 +29,22 @@ main.go ```go var o exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "OKex" { - y = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "OKex" { + y = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := o.GetTickerPrice() +tick, err := o.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := o.GetOrderbookEx() +ob, err := o.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/orders.tmpl b/cmd/documentation/exchanges_templates/orders.tmpl similarity index 100% rename from tools/documentation/exchanges_templates/orders.tmpl rename to cmd/documentation/exchanges_templates/orders.tmpl diff --git a/tools/documentation/exchanges_templates/poloniex.tmpl b/cmd/documentation/exchanges_templates/poloniex.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/poloniex.tmpl rename to cmd/documentation/exchanges_templates/poloniex.tmpl index cfb76abb..eae1386c 100644 --- a/tools/documentation/exchanges_templates/poloniex.tmpl +++ b/cmd/documentation/exchanges_templates/poloniex.tmpl @@ -30,22 +30,22 @@ main.go ```go var p exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Poloniex" { - y = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Poloniex" { + y = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := p.GetTickerPrice() +tick, err := p.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := p.GetOrderbookEx() +ob, err := p.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/request.tmpl b/cmd/documentation/exchanges_templates/request.tmpl similarity index 100% rename from tools/documentation/exchanges_templates/request.tmpl rename to cmd/documentation/exchanges_templates/request.tmpl diff --git a/tools/documentation/exchanges_templates/yobit.tmpl b/cmd/documentation/exchanges_templates/yobit.tmpl similarity index 92% rename from tools/documentation/exchanges_templates/yobit.tmpl rename to cmd/documentation/exchanges_templates/yobit.tmpl index 9fee688b..5b9acc9e 100644 --- a/tools/documentation/exchanges_templates/yobit.tmpl +++ b/cmd/documentation/exchanges_templates/yobit.tmpl @@ -29,22 +29,22 @@ main.go ```go var y exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Yobit" { - y = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Yobit" { + y = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := y.GetTickerPrice() +tick, err := y.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := y.GetOrderbookEx() +ob, err := y.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/exchanges_templates/zb.tmpl b/cmd/documentation/exchanges_templates/zb.tmpl similarity index 93% rename from tools/documentation/exchanges_templates/zb.tmpl rename to cmd/documentation/exchanges_templates/zb.tmpl index 8d37ae46..9dd6bb0e 100644 --- a/tools/documentation/exchanges_templates/zb.tmpl +++ b/cmd/documentation/exchanges_templates/zb.tmpl @@ -29,22 +29,22 @@ main.go ```go var z exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "ZB" { - z = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "ZB" { + z = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := z.GetTickerPrice() +tick, err := z.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := z.GetOrderbookEx() +ob, err := z.FetchOrderbook() if err != nil { // Handle error } diff --git a/tools/documentation/portfolio_templates/portfolio_readme.tmpl b/cmd/documentation/portfolio_templates/portfolio_readme.tmpl similarity index 100% rename from tools/documentation/portfolio_templates/portfolio_readme.tmpl rename to cmd/documentation/portfolio_templates/portfolio_readme.tmpl diff --git a/tools/documentation/root_templates/CONTRIBUTORS b/cmd/documentation/root_templates/CONTRIBUTORS.tmpl similarity index 69% rename from tools/documentation/root_templates/CONTRIBUTORS rename to cmd/documentation/root_templates/CONTRIBUTORS.tmpl index eda72237..0a25998d 100644 --- a/tools/documentation/root_templates/CONTRIBUTORS +++ b/cmd/documentation/root_templates/CONTRIBUTORS.tmpl @@ -1,6 +1,6 @@ {{define "CONTRIBUTORS"}} Thanks to the following contributors: -{{ range $contributor := .Contributors -}} +{{range $contributor := .Contributors -}} {{$contributor.Login}} | {{$contributor.URL}} -{{ end }} {{end}} +{{- end}} diff --git a/tools/documentation/root_templates/LICENSE b/cmd/documentation/root_templates/LICENSE.tmpl similarity index 100% rename from tools/documentation/root_templates/LICENSE rename to cmd/documentation/root_templates/LICENSE.tmpl diff --git a/tools/documentation/root_templates/root_readme.tmpl b/cmd/documentation/root_templates/root_readme.tmpl similarity index 90% rename from tools/documentation/root_templates/root_readme.tmpl rename to cmd/documentation/root_templates/root_readme.tmpl index 92c1c039..ec6f5159 100644 --- a/tools/documentation/root_templates/root_readme.tmpl +++ b/cmd/documentation/root_templates/root_readme.tmpl @@ -33,6 +33,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | COINUT | Yes | Yes | NA | | Exmo | Yes | NA | NA | | CoinbasePro | Yes | Yes | No| +| Coinbene | Yes | No | No | | GateIO | Yes | Yes | NA | | Gemini | Yes | Yes | No | | HitBTC | Yes | Yes | No | @@ -54,19 +55,26 @@ We are aiming to support the top 20 highest volume exchanges based off the [Coin ## Current Features -+ Support for all Exchange fiat and digital currencies, with the ability to individually toggle them on/off. ++ Support for all exchange fiat and digital currencies, with the ability to individually toggle them on/off. + AES256 encrypted config file. + REST API support for all exchanges. + Websocket support for applicable exchanges. + Ability to turn off/on certain exchanges. -+ Ability to adjust manual polling timer for exchanges. + Communication packages (Slack, SMS via SMSGlobal, Telegram and SMTP) + HTTP rate limiter package. ++ Unified API for exchange usage. ++ Customisation of HTTP client features including setting a proxy, user agent and adjusting transport settings. ++ NTP client package. ++ Database support (Postgres and SQLite3). See [database](/database/README.md). ++ OTP generation tool. See [gen otp](/cmd/gen_otp). ++ Connection monitor package. ++ gRPC service and JSON RPC proxy. See [gRPC service](/gctrpc/README.md). ++ gRPC client. See [gctcli](/cmd/gctcli/README.md). + Forex currency converter packages (CurrencyConverterAPI, CurrencyLayer, Fixer.io, OpenExchangeRates) + Packages for handling currency pairs, tickers and orderbooks. + Portfolio management tool; fetches balances from supported exchanges and allows for custom address tracking. + Basic event trigger system. -+ WebGUI. ++ WebGUI (discontinued). ## Planned Features @@ -125,4 +133,4 @@ If this framework helped you in any way, or you would like to support the develo Binaries will be published once the codebase reaches a stable condition. {{template "contributors" .}} -{{end}} +{{- end}} diff --git a/tools/documentation/sub_templates/contributions.tmpl b/cmd/documentation/sub_templates/contributions.tmpl similarity index 100% rename from tools/documentation/sub_templates/contributions.tmpl rename to cmd/documentation/sub_templates/contributions.tmpl diff --git a/cmd/documentation/sub_templates/contributors.tmpl b/cmd/documentation/sub_templates/contributors.tmpl new file mode 100644 index 00000000..35c8c455 --- /dev/null +++ b/cmd/documentation/sub_templates/contributors.tmpl @@ -0,0 +1,11 @@ +{{define "contributors"}} +## Contributor List + +### A very special thank you to all who have contributed to this program: + +|User|Contribution Amount| +|--|--| +{{range $contributor := .Contributors -}} +| [{{$contributor.Login}}]({{$contributor.URL}}) | {{$contributor.Contributions}} | +{{end}} +{{- end}} diff --git a/tools/documentation/sub_templates/donations.tmpl b/cmd/documentation/sub_templates/donations.tmpl similarity index 97% rename from tools/documentation/sub_templates/donations.tmpl rename to cmd/documentation/sub_templates/donations.tmpl index a864ed70..3fd5682f 100644 --- a/tools/documentation/sub_templates/donations.tmpl +++ b/cmd/documentation/sub_templates/donations.tmpl @@ -6,4 +6,4 @@ If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** -{{end}} +{{- end}} diff --git a/tools/documentation/sub_templates/header.tmpl b/cmd/documentation/sub_templates/header.tmpl similarity index 100% rename from tools/documentation/sub_templates/header.tmpl rename to cmd/documentation/sub_templates/header.tmpl diff --git a/tools/documentation/sub_templates/status.tmpl b/cmd/documentation/sub_templates/status.tmpl similarity index 100% rename from tools/documentation/sub_templates/status.tmpl rename to cmd/documentation/sub_templates/status.tmpl diff --git a/tools/documentation/testdata_templates/testdata_readme.tmpl b/cmd/documentation/testdata_templates/testdata_readme.tmpl similarity index 92% rename from tools/documentation/testdata_templates/testdata_readme.tmpl rename to cmd/documentation/testdata_templates/testdata_readme.tmpl index 15a6e5d9..68a08abf 100644 --- a/tools/documentation/testdata_templates/testdata_readme.tmpl +++ b/cmd/documentation/testdata_templates/testdata_readme.tmpl @@ -6,5 +6,5 @@ This folder contains a configuration test file for non-deployement test params. It also has the code coverage test files that allow us to monitor our entire codebase, click this link for more information [https://codecov.io/](https://codecov.io/). {{template "contributions"}} -{{template "donations"}} +{{template "donations" -}} {{end}} diff --git a/tools/documentation/tools_templates/config_tool.tmpl b/cmd/documentation/tools_templates/config_tool.tmpl similarity index 100% rename from tools/documentation/tools_templates/config_tool.tmpl rename to cmd/documentation/tools_templates/config_tool.tmpl diff --git a/tools/documentation/tools_templates/documentation_tool.tmpl b/cmd/documentation/tools_templates/documentation_tool.tmpl similarity index 100% rename from tools/documentation/tools_templates/documentation_tool.tmpl rename to cmd/documentation/tools_templates/documentation_tool.tmpl diff --git a/tools/documentation/tools_templates/exchange_tool.tmpl b/cmd/documentation/tools_templates/exchange_tool.tmpl similarity index 100% rename from tools/documentation/tools_templates/exchange_tool.tmpl rename to cmd/documentation/tools_templates/exchange_tool.tmpl diff --git a/tools/documentation/tools_templates/huobi_auth_tool.tmpl b/cmd/documentation/tools_templates/huobi_auth_tool.tmpl similarity index 100% rename from tools/documentation/tools_templates/huobi_auth_tool.tmpl rename to cmd/documentation/tools_templates/huobi_auth_tool.tmpl diff --git a/tools/documentation/tools_templates/portfolio_tool.tmpl b/cmd/documentation/tools_templates/portfolio_tool.tmpl similarity index 100% rename from tools/documentation/tools_templates/portfolio_tool.tmpl rename to cmd/documentation/tools_templates/portfolio_tool.tmpl diff --git a/tools/documentation/tools_templates/tools.tmpl b/cmd/documentation/tools_templates/tools.tmpl similarity index 100% rename from tools/documentation/tools_templates/tools.tmpl rename to cmd/documentation/tools_templates/tools.tmpl diff --git a/tools/documentation/tools_templates/websocket_client_tool.tmpl b/cmd/documentation/tools_templates/websocket_client_tool.tmpl similarity index 100% rename from tools/documentation/tools_templates/websocket_client_tool.tmpl rename to cmd/documentation/tools_templates/websocket_client_tool.tmpl diff --git a/tools/documentation/web_templates/web_readme.tmpl b/cmd/documentation/web_templates/web_readme.tmpl similarity index 100% rename from tools/documentation/web_templates/web_readme.tmpl rename to cmd/documentation/web_templates/web_readme.tmpl diff --git a/tools/exchange_template/exchange_template.go b/cmd/exchange_template/exchange_template.go similarity index 75% rename from tools/exchange_template/exchange_template.go rename to cmd/exchange_template/exchange_template.go index 2276365c..ef976e09 100644 --- a/tools/exchange_template/exchange_template.go +++ b/cmd/exchange_template/exchange_template.go @@ -7,10 +7,13 @@ import ( "log" "os" "os/exec" + "path/filepath" + "strings" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) const ( @@ -20,9 +23,8 @@ const ( packageMain = "%s.go" packageReadme = "README.md" - exchangePackageLocation = "..%s..%sexchanges%s" - exchangeLocation = "..%s..%sexchange.go" - exchangeConfigPath = "..%s..%stestdata%sconfigtest.json" + exchangePackageLocation = "../../exchanges" + exchangeConfigPath = "../../testdata/configtest.json" ) var ( @@ -32,7 +34,6 @@ var ( exchangeWrapper string exchangeMain string exchangeReadme string - exchangeJSON string ) type exchange struct { @@ -82,9 +83,9 @@ func main() { log.Fatal("GoCryptoTrader: Exchange templating tool stopped...") } - newExchangeName = common.StringToLower(newExchangeName) + newExchangeName = strings.ToLower(newExchangeName) v := newExchangeName[:1] - capName := common.StringToUpper(v) + newExchangeName[1:] + capName := strings.ToUpper(v) + newExchangeName[1:] exch := exchange{ Name: newExchangeName, @@ -95,18 +96,14 @@ func main() { FIX: fixSupport, } - osPathSlash := common.GetOSPathSlash() - exchangeJSON := fmt.Sprintf(exchangeConfigPath, osPathSlash, osPathSlash, osPathSlash) - configTestFile := config.GetConfig() - err = configTestFile.LoadConfig(exchangeJSON) + err = configTestFile.LoadConfig(exchangeConfigPath, true) if err != nil { log.Fatal("GoCryptoTrader: Exchange templating configuration retrieval error ", err) } // NOTE need to nullify encrypt configuration var configTestExchanges []string - for x := range configTestFile.Exchanges { configTestExchanges = append(configTestExchanges, configTestFile.Exchanges[x].Name) } @@ -118,31 +115,34 @@ func main() { newExchConfig := config.ExchangeConfig{} newExchConfig.Name = capName newExchConfig.Enabled = true - newExchConfig.RESTPollingDelay = 10 - newExchConfig.APIKey = "Key" - newExchConfig.APISecret = "Secret" - newExchConfig.AssetTypes = orderbook.Spot + newExchConfig.API.Credentials.Key = "Key" + newExchConfig.API.Credentials.Secret = "Secret" + newExchConfig.CurrencyPairs = ¤cy.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + } configTestFile.Exchanges = append(configTestFile.Exchanges, newExchConfig) - // TODO sorting function so exchanges are in alphabetical order - low priority - err = configTestFile.SaveConfig(exchangeJSON) + err = configTestFile.SaveConfig(exchangeConfigPath, false) if err != nil { log.Fatal("GoCryptoTrader: Exchange templating configuration error - cannot save") } - exchangeDirectory = fmt.Sprintf( - exchangePackageLocation+newExchangeName+"%s", - osPathSlash, - osPathSlash, - osPathSlash, - osPathSlash) - - exchangeTest = fmt.Sprintf(exchangeDirectory+packageTests, newExchangeName) - exchangeTypes = fmt.Sprintf(exchangeDirectory+packageTypes, newExchangeName) - exchangeWrapper = fmt.Sprintf(exchangeDirectory+packageWrapper, newExchangeName) - exchangeMain = fmt.Sprintf(exchangeDirectory+packageMain, newExchangeName) - exchangeReadme = exchangeDirectory + packageReadme + exchangeDirectory = filepath.Join(exchangePackageLocation, newExchangeName) + exchangeTest = filepath.Join(exchangeDirectory, fmt.Sprintf(packageTests, newExchangeName)) + exchangeTypes = filepath.Join(exchangeDirectory, fmt.Sprintf(packageTypes, newExchangeName)) + exchangeWrapper = filepath.Join(exchangeDirectory, fmt.Sprintf(packageWrapper, newExchangeName)) + exchangeMain = filepath.Join(exchangeDirectory, fmt.Sprintf(packageMain, newExchangeName)) + exchangeReadme = filepath.Join(exchangeDirectory, packageReadme) err = os.Mkdir(exchangeDirectory, 0700) if err != nil { @@ -220,12 +220,13 @@ func main() { } fmt.Println("GoCryptoTrader: Exchange templating tool service complete") - fmt.Println("When wrapper is finished add exchange to exchange.go") - fmt.Println("Test exchange.go") - fmt.Println("Update the config_test.go file") - fmt.Println("Test config.go") + fmt.Println("When the exchange code implementation has been completed (REST/Websocket/wrappers and tests), please add the exchange to engine/exchange.go") + fmt.Println("Add the exchange config settings to config_example.json (it will automatically be added to testdata/configtest.json)") + fmt.Println("Increment the available exchanges counter in config/config_test.go") + fmt.Println("Add the exchange name to exchanges/support.go") + fmt.Println("Ensure go test ./... -race passes") fmt.Println("Open a pull request") - fmt.Println("If help is needed please post a message on Slack.") + fmt.Println("If help is needed, please post a message in Slack.") } func newFile(path string) { diff --git a/cmd/exchange_template/main_file.tmpl b/cmd/exchange_template/main_file.tmpl new file mode 100644 index 00000000..ef86003a --- /dev/null +++ b/cmd/exchange_template/main_file.tmpl @@ -0,0 +1,24 @@ +{{define "main"}} +package {{.Name}} + +import ( + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" +) + +// {{.CapitalName}} is the overarching type across this package +type {{.CapitalName}} struct { + exchange.Base +} + +const ( + {{.Name}}APIURL = "" + {{.Name}}APIVersion = "" + + // Public endpoints + + // Authenticated endpoints +) + +// Start implementing public and private exchange API funcs below + +{{end}} diff --git a/tools/exchange_template/readme_file.tmpl b/cmd/exchange_template/readme_file.tmpl similarity index 100% rename from tools/exchange_template/readme_file.tmpl rename to cmd/exchange_template/readme_file.tmpl diff --git a/cmd/exchange_template/test_file.tmpl b/cmd/exchange_template/test_file.tmpl new file mode 100644 index 00000000..989238e8 --- /dev/null +++ b/cmd/exchange_template/test_file.tmpl @@ -0,0 +1,52 @@ +{{define "test"}} +package {{.Name}} + +import ( + "log" + "os" + "testing" + + "github.com/thrasher-corp/gocryptotrader/config" +) + +// Please supply your own keys here to do authenticated endpoint testing +const ( + apiKey = "" + apiSecret = "" + canManipulateRealOrders = false +) + +var {{.Variable}} {{.CapitalName}} + +func TestMain(m *testing.M) { + {{.Variable}}.SetDefaults() + cfg := config.GetConfig() + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal(err) + } + + exchCfg, err := cfg.GetExchangeConfig("{{.CapitalName}}") + if err != nil { + log.Fatal(err) + } + + exchCfg.API.AuthenticatedSupport = true + {{ if .WS }} exchCfg.API.AuthenticatedWebsocketSupport = true {{ end }} + exchCfg.API.Credentials.Key = apiKey + exchCfg.API.Credentials.Secret = apiSecret + + err = {{.Variable}}.Setup(exchCfg) + if err != nil { + log.Fatal(err) + } + + os.Exit(m.Run()) +} + +func areTestAPIKeysSet() bool { + return {{.Variable}}.ValidateAPICredentials() +} + +// Implement tests for API endpoints below +{{end}} diff --git a/tools/exchange_template/type_file.tmpl b/cmd/exchange_template/type_file.tmpl similarity index 100% rename from tools/exchange_template/type_file.tmpl rename to cmd/exchange_template/type_file.tmpl diff --git a/cmd/exchange_template/wrapper_file.tmpl b/cmd/exchange_template/wrapper_file.tmpl new file mode 100644 index 00000000..0ed4873f --- /dev/null +++ b/cmd/exchange_template/wrapper_file.tmpl @@ -0,0 +1,397 @@ +{{define "wrapper"}} +package {{.Name}} + +import ( + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// GetDefaultConfig returns a default exchange config +func ({{.Variable}} *{{.CapitalName}}) GetDefaultConfig() (*config.ExchangeConfig, error) { + {{.Variable}}.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = {{.Variable}}.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = {{.Variable}}.BaseCurrencies + + err := {{.Variable}}.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if {{.Variable}}.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = {{.Variable}}.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for {{.CapitalName}} +func ({{.Variable}} *{{.CapitalName}}) SetDefaults() { + {{.Variable}}.Name = "{{.CapitalName}}" + {{.Variable}}.Enabled = true + {{.Variable}}.Verbose = true + {{.Variable}}.API.CredentialsValidator.RequiresKey = true + {{.Variable}}.API.CredentialsValidator.RequiresSecret = true + {{.Variable}}.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "-", + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "-", + }, + } + // Fill out the capabilities/features that the exchange supports + {{.Variable}}.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + {{ if .REST }} REST: true, {{ end }} + {{ if .WS }} Websocket: true, {{ end }} + RESTCapabilities: protocol.Features{ + TickerFetching: true, + OrderbookFetching: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + OrderbookFetching: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.AutoWithdrawFiat, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + {{.Variable}}.Requester = request.New({{.Variable}}.Name, + request.NewRateLimit(time.Second, 0), + request.NewRateLimit(time.Second, 0), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + {{.Variable}}.API.Endpoints.URLDefault = {{.Name}}APIURL + {{.Variable}}.API.Endpoints.URL = {{.Variable}}.API.Endpoints.URLDefault + {{.Variable}}.Websocket = wshandler.New() + {{.Variable}}.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + {{.Variable}}.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + {{.Variable}}.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup takes in the supplied exchange configuration details and sets params +func ({{.Variable}} *{{.CapitalName}}) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + {{.Variable}}.SetEnabled(false) + return nil + } + + err := {{.Variable}}.SetupDefaults(exch) + if err != nil { + return err + } + + // If websocket is supported, please fill out the following + /* + err = {{.Variable}}.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: {{.Name}}WSURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: {{.Variable}}.WsConnect, + Subscriber: {{.Variable}}.Subscribe, + UnSubscriber: {{.Variable}}.Unsubscribe, + Features: &{{.Variable}}.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + {{.Variable}}.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: {{.Variable}}.Name, + URL: {{.Variable}}.Websocket.GetWebsocketURL(), + ProxyURL: {{.Variable}}.Websocket.GetProxyAddress(), + Verbose: {{.Variable}}.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + // NOTE: PLEASE ENSURE YOU SET THE ORDERBOOK BUFFER SETTINGS CORRECTLY + {{.Variable}}.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + true, + true, + false, + false, + exch.Name) + */ + return nil +} + +// Start starts the {{.CapitalName}} go routine +func ({{.Variable}} *{{.CapitalName}}) Start(wg *sync.WaitGroup) { + wg.Add(1) + go func() { + {{.Variable}}.Run() + wg.Done() + }() +} + +// Run implements the {{.CapitalName}} wrapper +func ({{.Variable}} *{{.CapitalName}}) Run() { + if {{.Variable}}.Verbose { + {{ if .WS }} log.Debugf(log.ExchangeSys, + "%s Websocket: %s.", + {{.Variable}}.Name, + common.IsEnabled({{.Variable}}.Websocket.IsEnabled())) {{ end }} + {{.Variable}}.PrintEnabledPairs() + } + + if !{{.Variable}}.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := {{.Variable}}.UpdateTradablePairs(false) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + {{.Variable}}.Name, + err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func ({{.Variable}} *{{.CapitalName}}) FetchTradablePairs(asset asset.Item) ([]string, error) { + // Implement fetching the exchange available pairs if supported + return nil, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func ({{.Variable}} *{{.CapitalName}}) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := {{.Variable}}.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + return {{.Variable}}.UpdatePairs(currency.NewPairsFromStrings(pairs), + asset.Spot, false, forceUpdate) +} + + +// UpdateTicker updates and returns the ticker for a currency pair +func ({{.Variable}} *{{.CapitalName}}) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + // NOTE: EXAMPLE FOR GETTING TICKER PRICE + /* + var tickerPrice ticker.Price + tick, err := {{.Variable}}.GetTicker(p.String()) + if err != nil { + return tickerPrice, err + } + tickerPrice = ticker.Price{ + High: tick.High, + Low: tick.Low, + Bid: tick.Bid, + Ask: tick.Ask, + Open: tick.Open, + Close: tick.Close, + Pair: p, + } + err = ticker.ProcessTicker({{.Variable}}.Name, &tickerPrice, assetType) + if err != nil { + return tickerPrice, err + } + */ + return ticker.GetTicker({{.Variable}}.Name, p, assetType) +} + +// FetchTicker returns the ticker for a currency pair +func ({{.Variable}} *{{.CapitalName}}) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker({{.Variable}}.Name, p, assetType) + if err != nil { + return {{.Variable}}.UpdateTicker(p, assetType) + } + return tickerNew, nil +} + +// FetchOrderbook returns orderbook base on the currency pair +func ({{.Variable}} *{{.CapitalName}}) FetchOrderbook(currency currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get({{.Variable}}.Name, currency, assetType) + if err != nil { + return {{.Variable}}.UpdateOrderbook(currency, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func ({{.Variable}} *{{.CapitalName}}) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + var orderBook orderbook.Base + // NOTE: UPDATE ORDERBOOK EXAMPLE + /* + orderbookNew, err := {{.Variable}}.GetOrderBook(exchange.FormatExchangeCurrency({{.Variable}}.Name, p).String(), 1000) + if err != nil { + return orderBook, err + } + + for x := range orderbookNew.Bids { + orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[x].Quantity, + Price: orderbookNew.Bids[x].Price, + }) + } + + for x := range orderbookNew.Asks { + orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + Amount: orderBook.Asks[x].Quantity, + Price: orderBook.Asks[x].Price, + }) + } + */ + + + orderBook.Pair = p + orderBook.ExchangeName = {{.Variable}}.Name + orderBook.AssetType = assetType + + err := orderBook.Process() + if err != nil { + return orderBook, err + } + + return orderbook.Get({{.Variable}}.Name, p, assetType) +} + +// GetAccountInfo retrieves balances for all enabled currencies for the +// {{.CapitalName}} exchange +func ({{.Variable}} *{{.CapitalName}}) GetAccountInfo() (exchange.AccountInfo, error) { + return exchange.AccountInfo{}, common.ErrNotYetImplemented +} + +// GetFundingHistory returns funding history, deposits and +// withdrawals +func ({{.Variable}} *{{.CapitalName}}) GetFundingHistory() ([]exchange.FundHistory, error) { + return nil, common.ErrNotYetImplemented +} + +// GetExchangeHistory returns historic trade data since exchange opening. +func ({{.Variable}} *{{.CapitalName}}) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented +} + +// SubmitOrder submits a new order +func ({{.Variable}} *{{.CapitalName}}) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } + return submitOrderResponse, common.ErrNotYetImplemented +} + +// ModifyOrder will allow of changing orderbook placement and limit to +// market conversion +func ({{.Variable}} *{{.CapitalName}}) ModifyOrder(action *order.Modify) (string, error) { + return "", common.ErrNotYetImplemented +} + +// CancelOrder cancels an order by its corresponding ID number +func ({{.Variable}} *{{.CapitalName}}) CancelOrder(order *order.Cancel) error { + return common.ErrNotYetImplemented +} + +// CancelAllOrders cancels all orders associated with a currency pair +func ({{.Variable}} *{{.CapitalName}}) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) { + return order.CancelAllResponse{}, common.ErrNotYetImplemented +} + +// GetOrderInfo returns information on a current open order +func ({{.Variable}} *{{.CapitalName}}) GetOrderInfo(orderID string) (order.Detail, error) { + return order.Detail{}, common.ErrNotYetImplemented +} + +// GetDepositAddress returns a deposit address for a specified currency +func ({{.Variable}} *{{.CapitalName}}) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) { + return "", common.ErrNotYetImplemented +} + +// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is +// submitted +func ({{.Variable}} *{{.CapitalName}}) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { + return "", common.ErrNotYetImplemented +} + +// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is +// submitted +func ({{.Variable}} *{{.CapitalName}}) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { + return "", common.ErrNotYetImplemented +} + +// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is +// submitted +func ({{.Variable}} *{{.CapitalName}}) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { + return "", common.ErrNotYetImplemented +} + +// GetWebsocket returns a pointer to the exchange websocket +func ({{.Variable}} *{{.CapitalName}}) GetWebsocket() (*wshandler.Websocket, error) { + return nil, common.ErrNotYetImplemented +} + +// GetActiveOrders retrieves any orders that are active/open +func ({{.Variable}} *{{.CapitalName}}) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { + return nil, common.ErrNotYetImplemented +} + +// GetOrderHistory retrieves account order information +// Can Limit response to specific order status +func ({{.Variable}} *{{.CapitalName}}) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { + return nil, common.ErrNotYetImplemented +} + +// GetFeeByType returns an estimate of fee based on the type of transaction +func ({{.Variable}} *{{.CapitalName}}) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { + return 0, common.ErrNotYetImplemented +} + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func ({{.Variable}} *{{.CapitalName}}) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + {{.Variable}}.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func ({{.Variable}} *{{.CapitalName}}) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + {{.Variable}}.Websocket.RemoveSubscribedChannels(channels) + return nil +} + +// GetSubscriptions returns a copied list of subscriptions +func ({{.Variable}} *{{.CapitalName}}) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { + return nil, common.ErrNotYetImplemented +} + +// AuthenticateWebsocket sends an authentication message to the websocket +func ({{.Variable}} *{{.CapitalName}}) AuthenticateWebsocket() error { + return common.ErrNotYetImplemented +} + +{{end}} diff --git a/cmd/exchange_wrapper_coverage/main.go b/cmd/exchange_wrapper_coverage/main.go new file mode 100644 index 00000000..40dba073 --- /dev/null +++ b/cmd/exchange_wrapper_coverage/main.go @@ -0,0 +1,191 @@ +package main + +import ( + "log" + "math/rand" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/engine" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) + +const ( + totalWrappers = 20 +) + +func main() { + var err error + engine.Bot, err = engine.New() + if err != nil { + log.Fatalf("Failed to initialise engine. Err: %s", err) + } + + engine.Bot.Settings = engine.Settings{ + DisableExchangeAutoPairUpdates: true, + } + + log.Printf("Loading exchanges..") + var wg sync.WaitGroup + for x := range exchange.Exchanges { + name := exchange.Exchanges[x] + err := engine.LoadExchange(name, true, &wg) + if err != nil { + log.Printf("Failed to load exchange %s. Err: %s", name, err) + continue + } + } + wg.Wait() + log.Println("Done.") + + log.Printf("Testing exchange wrappers..") + results := make(map[string][]string) + wg = sync.WaitGroup{} + for x := range engine.Bot.Exchanges { + wg.Add(1) + go func(num int) { + name := engine.Bot.Exchanges[num].GetName() + results[name] = testWrappers(engine.Bot.Exchanges[num]) + wg.Done() + }(x) + } + wg.Wait() + log.Println("Done.") + + log.Println() + for name, funcs := range results { + pct := float64(totalWrappers-len(funcs)) / float64(totalWrappers) * 100 + log.Printf("Exchange %s wrapper coverage [%d/%d - %.2f%%] | Total missing: %d", name, totalWrappers-len(funcs), totalWrappers, pct, len(funcs)) + log.Printf("\t Wrappers not implemented:") + + for x := range funcs { + log.Printf("\t - %s", funcs[x]) + } + log.Println() + } +} + +func testWrappers(e exchange.IBotExchange) []string { + p := currency.NewPair(currency.BTC, currency.USD) + assetType := asset.Spot + if !e.SupportsAsset(assetType) { + assets := e.GetAssetTypes() + rand.Seed(time.Now().Unix()) + assetType = assets[rand.Intn(len(assets))] + } + + var funcs []string + + _, err := e.FetchTicker(p, assetType) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "FetchTicker") + } + + _, err = e.UpdateTicker(p, assetType) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "UpdateTicker") + } + + _, err = e.FetchOrderbook(p, assetType) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "FetchOrderbook") + } + + _, err = e.UpdateOrderbook(p, assetType) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "UpdateOrderbook") + } + + _, err = e.FetchTradablePairs(asset.Spot) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "FetchTradablePairs") + } + + err = e.UpdateTradablePairs(false) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "UpdateTradablePairs") + } + + _, err = e.GetAccountInfo() + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "GetAccountInfo") + } + + _, err = e.GetExchangeHistory(p, assetType) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "GetExchangeHistory") + } + + _, err = e.GetFundingHistory() + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "GetFundingHistory") + } + + s := &order.Submit{ + Pair: p, + OrderSide: order.Buy, + OrderType: order.Limit, + Amount: 1000000, + Price: 10000000000, + ClientID: "meow", + } + _, err = e.SubmitOrder(s) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "SubmitOrder") + } + + _, err = e.ModifyOrder(&order.Modify{}) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "ModifyOrder") + } + + err = e.CancelOrder(&order.Cancel{}) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "CancelOrder") + } + + _, err = e.CancelAllOrders(&order.Cancel{}) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "CancelAllOrders") + } + + _, err = e.GetOrderInfo("1") + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "GetOrderInfo") + } + + _, err = e.GetOrderHistory(&order.GetOrdersRequest{}) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "GetOrderHistory") + } + + _, err = e.GetActiveOrders(&order.GetOrdersRequest{}) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "GetActiveOrders") + } + + _, err = e.GetDepositAddress(currency.BTC, "") + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "GetDepositAddress") + } + + _, err = e.WithdrawCryptocurrencyFunds(&exchange.CryptoWithdrawRequest{}) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "WithdrawCryptocurrencyFunds") + } + + _, err = e.WithdrawFiatFunds(&exchange.FiatWithdrawRequest{}) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "WithdrawFiatFunds") + } + _, err = e.WithdrawFiatFundsToInternationalBank(&exchange.FiatWithdrawRequest{}) + if err == common.ErrNotYetImplemented { + funcs = append(funcs, "WithdrawFiatFundsToInternationalBank") + } + + return funcs +} diff --git a/cmd/exchange_wrapper_issues/.gitignore b/cmd/exchange_wrapper_issues/.gitignore new file mode 100644 index 00000000..ff5a41ed --- /dev/null +++ b/cmd/exchange_wrapper_issues/.gitignore @@ -0,0 +1,3 @@ +/data.json +/output.json +/report.html \ No newline at end of file diff --git a/cmd/exchange_wrapper_issues/main.go b/cmd/exchange_wrapper_issues/main.go new file mode 100644 index 00000000..e4d2b6e4 --- /dev/null +++ b/cmd/exchange_wrapper_issues/main.go @@ -0,0 +1,839 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "text/template" + + "github.com/thrasher-corp/gocryptotrader/common/file" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/engine" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" +) + +func main() { + log.Println("Loading flags...") + parseCLFlags() + var err error + log.Println("Loading engine...") + engine.Bot, err = engine.New() + if err != nil { + log.Fatalf("Failed to initialise engine. Err: %s", err) + } + + engine.Bot.Settings = engine.Settings{ + DisableExchangeAutoPairUpdates: true, + Verbose: verboseOverride, + } + + log.Println("Loading config...") + wrapperConfig, err := loadConfig() + if err != nil { + log.Printf("Error loading config: '%v', generating empty config", err) + wrapperConfig = Config{ + Exchanges: make(map[string]*config.APICredentialsConfig), + } + } + + log.Println("Loading exchanges..") + + var wg sync.WaitGroup + for x := range exchange.Exchanges { + name := exchange.Exchanges[x] + if _, ok := wrapperConfig.Exchanges[name]; !ok { + wrapperConfig.Exchanges[strings.ToLower(name)] = &config.APICredentialsConfig{} + } + if shouldLoadExchange(name) { + err = engine.LoadExchange(name, true, &wg) + if err != nil { + log.Printf("Failed to load exchange %s. Err: %s", name, err) + continue + } + } + } + wg.Wait() + log.Println("Done.") + + if withdrawAddressOverride != "" { + wrapperConfig.WalletAddress = withdrawAddressOverride + } + if orderTypeOverride != "LIMIT" { + wrapperConfig.OrderSubmission.OrderType = orderTypeOverride + } + if orderSideOverride != "BUY" { + wrapperConfig.OrderSubmission.OrderSide = orderSideOverride + } + if orderPriceOverride > 0 { + wrapperConfig.OrderSubmission.Price = orderPriceOverride + } + if orderAmountOverride > 0 { + wrapperConfig.OrderSubmission.Amount = orderAmountOverride + } + + log.Println("Testing exchange wrappers..") + var exchangeResponses []ExchangeResponses + + for x := range engine.Bot.Exchanges { + base := engine.Bot.Exchanges[x].GetBase() + if !base.Config.Enabled { + log.Printf("Exchange %v not enabled, skipping", base.GetName()) + continue + } + base.Config.Verbose = verboseOverride + base.Verbose = verboseOverride + base.HTTPDebugging = false + base.Config.HTTPDebugging = false + wg.Add(1) + + go func(num int) { + name := engine.Bot.Exchanges[num].GetName() + authenticated := setExchangeAPIKeys(name, wrapperConfig.Exchanges, base) + wrapperResult := ExchangeResponses{ + ID: fmt.Sprintf("Exchange%v", num), + ExchangeName: name, + APIKeysSet: authenticated, + AssetPairResponses: testWrappers(engine.Bot.Exchanges[num], base, &wrapperConfig), + } + for i := range wrapperResult.AssetPairResponses { + wrapperResult.ErrorCount += wrapperResult.AssetPairResponses[i].ErrorCount + } + exchangeResponses = append(exchangeResponses, wrapperResult) + wg.Done() + }(x) + } + wg.Wait() + + log.Println("Done.") + log.Println() + + sort.Slice(exchangeResponses, func(i, j int) bool { + return exchangeResponses[i].ExchangeName < exchangeResponses[j].ExchangeName + }) + + if strings.EqualFold(outputOverride, "Console") { + outputToConsole(exchangeResponses) + } + if strings.EqualFold(outputOverride, "JSON") { + outputToJSON(exchangeResponses) + } + if strings.EqualFold(outputOverride, "HTML") { + outputToHTML(exchangeResponses) + } + + saveConfig(&wrapperConfig) +} + +func parseCLFlags() { + flag.StringVar(&exchangesToUseOverride, "exchanges", "", "a + delimited list of exchange names to run tests against eg -exchanges=bitfinex+anx") + flag.StringVar(&exchangesToExcludeOverride, "excluded-exchanges", "", "a + delimited list of exchange names to ignore when they're being temperamental eg -exchangesToExlude=lbank") + flag.StringVar(&assetTypeOverride, "asset", "", "the asset type to run tests against (where applicable)") + flag.StringVar(¤cyPairOverride, "currency", "", "the currency to run tests against (where applicable)") + flag.StringVar(&outputOverride, "output", "HTML", "JSON, HTML or Console") + flag.BoolVar(&authenticatedOnly, "auth-only", false, "skip any wrapper function that doesn't require auth") + flag.BoolVar(&verboseOverride, "verbose", false, "verbose CL output - if console output is selected then wrapper response is included") + flag.StringVar(&orderSideOverride, "orderside", "BUY", "the order type for all order based wrapper tests") + flag.StringVar(&orderTypeOverride, "ordertype", "LIMIT", "the order type for all order based wrapper tests") + flag.Float64Var(&orderAmountOverride, "orderamount", 0, "the order amount for all order based wrapper tests") + flag.Float64Var(&orderPriceOverride, "orderprice", 0, "the order price for all order based wrapper tests") + flag.StringVar(&withdrawAddressOverride, "withdraw-wallet", "", "withdraw wallet address") + flag.StringVar(&outputFileName, "filename", "report", "name of the output file eg 'report'.html or 'report'.json") + flag.Parse() + + if exchangesToUseOverride != "" { + exchangesToUseList = strings.Split(exchangesToUseOverride, "+") + } + if exchangesToExcludeOverride != "" { + exchangesToExcludeList = strings.Split(exchangesToExcludeOverride, "+") + } +} + +func shouldLoadExchange(name string) bool { + shouldLoadExchange := true + if len(exchangesToUseList) > 0 { + var found bool + for i := range exchangesToUseList { + if strings.EqualFold(name, exchangesToUseList[i]) { + found = true + } + } + if !found { + shouldLoadExchange = false + } + } + + if len(exchangesToExcludeList) > 0 { + for i := range exchangesToExcludeList { + if strings.EqualFold(name, exchangesToExcludeList[i]) { + if shouldLoadExchange { + shouldLoadExchange = false + } + } + } + } + return shouldLoadExchange +} + +func setExchangeAPIKeys(name string, keys map[string]*config.APICredentialsConfig, base *exchange.Base) bool { + lowerExchangeName := strings.ToLower(name) + + if base.API.CredentialsValidator.RequiresKey && keys[lowerExchangeName].Key == "" { + keys[lowerExchangeName].Key = config.DefaultAPIKey + } + if base.API.CredentialsValidator.RequiresSecret && keys[lowerExchangeName].Secret == "" { + keys[lowerExchangeName].Secret = config.DefaultAPISecret + } + if base.API.CredentialsValidator.RequiresPEM && keys[lowerExchangeName].PEMKey == "" { + keys[lowerExchangeName].PEMKey = "PEM" + } + if base.API.CredentialsValidator.RequiresClientID && keys[lowerExchangeName].ClientID == "" { + keys[lowerExchangeName].ClientID = config.DefaultAPIClientID + } + if keys[lowerExchangeName].OTPSecret == "" { + keys[lowerExchangeName].OTPSecret = "-" // Ensure OTP is available for use + } + + base.API.Credentials.Key = keys[lowerExchangeName].Key + base.Config.API.Credentials.Key = keys[lowerExchangeName].Key + + base.API.Credentials.Secret = keys[lowerExchangeName].Secret + base.Config.API.Credentials.Secret = keys[lowerExchangeName].Secret + + base.API.Credentials.ClientID = keys[lowerExchangeName].ClientID + base.Config.API.Credentials.ClientID = keys[lowerExchangeName].ClientID + + if keys[lowerExchangeName].OTPSecret != "-" { + base.Config.API.Credentials.OTPSecret = keys[lowerExchangeName].OTPSecret + } + + base.API.AuthenticatedSupport = true + base.API.AuthenticatedWebsocketSupport = true + base.Config.API.AuthenticatedSupport = true + base.Config.API.AuthenticatedWebsocketSupport = true + + return base.ValidateAPICredentials() +} + +func parseOrderSide(orderSide string) order.Side { + switch orderSide { + case order.AnySide.String(): + return order.AnySide + case order.Buy.String(): + return order.Buy + case order.Sell.String(): + return order.Sell + case order.Bid.String(): + return order.Bid + case order.Ask.String(): + return order.Ask + default: + log.Printf("Orderside '%v' not recognised, defaulting to BUY", orderSide) + return order.Buy + } +} + +func parseOrderType(orderType string) order.Type { + switch orderType { + case order.AnyType.String(): + return order.AnyType + case order.Limit.String(): + return order.Limit + case order.Market.String(): + return order.Market + case order.ImmediateOrCancel.String(): + return order.ImmediateOrCancel + case order.Stop.String(): + return order.Stop + case order.TrailingStop.String(): + return order.TrailingStop + case order.Unknown.String(): + return order.Unknown + default: + log.Printf("OrderType '%v' not recognised, defaulting to LIMIT", + orderTypeOverride) + return order.Limit + } +} + +func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) []ExchangeAssetPairResponses { + var response []ExchangeAssetPairResponses + testOrderSide := parseOrderSide(config.OrderSubmission.OrderSide) + testOrderType := parseOrderType(config.OrderSubmission.OrderType) + assetTypes := base.GetAssetTypes() + if assetTypeOverride != "" { + if asset.IsValid(asset.Item(assetTypeOverride)) { + assetTypes = asset.Items{asset.Item(assetTypeOverride)} + } else { + log.Printf("%v Asset Type '%v' not recognised, defaulting to exchange defaults", base.GetName(), assetTypeOverride) + } + } + for i := range assetTypes { + var msg string + var p currency.Pair + log.Printf("%v %v", base.GetName(), assetTypes[i]) + if _, ok := base.Config.CurrencyPairs.Pairs[assetTypes[i]]; !ok { + continue + } + + switch { + case currencyPairOverride != "": + p = currency.NewPairFromString(currencyPairOverride) + case len(base.Config.CurrencyPairs.Pairs[assetTypes[i]].Enabled) == 0: + if len(base.Config.CurrencyPairs.Pairs[assetTypes[i]].Available) == 0 { + log.Printf("%v has no enabled or available currencies. Skipping", base.GetName()) + continue + } + p = base.Config.CurrencyPairs.Pairs[assetTypes[i]].Available.GetRandomPair() + default: + p = base.Config.CurrencyPairs.Pairs[assetTypes[i]].Enabled.GetRandomPair() + } + + responseContainer := ExchangeAssetPairResponses{ + AssetType: assetTypes[i], + CurrencyPair: p, + } + + log.Printf("Setup config for %v %v %v", base.GetName(), assetTypes[i], p) + err := e.Setup(base.Config) + if err != nil { + log.Printf("%v Encountered error reloading config: '%v'", base.GetName(), err) + } + log.Printf("Executing wrappers for %v %v %v", base.GetName(), assetTypes[i], p) + + if !authenticatedOnly { + var r1 ticker.Price + r1, err = e.FetchTicker(p, assetTypes[i]) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{p, assetTypes[i]}), + Function: "FetchTicker", + Error: msg, + Response: jsonifyInterface([]interface{}{r1}), + }) + + var r2 ticker.Price + r2, err = e.UpdateTicker(p, assetTypes[i]) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{p, assetTypes[i]}), + Function: "UpdateTicker", + Error: msg, + Response: jsonifyInterface([]interface{}{r2}), + }) + + var r3 orderbook.Base + r3, err = e.FetchOrderbook(p, assetTypes[i]) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{p, assetTypes[i]}), + Function: "FetchOrderbook", + Error: msg, + Response: jsonifyInterface([]interface{}{r3}), + }) + + var r4 orderbook.Base + r4, err = e.UpdateOrderbook(p, assetTypes[i]) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{p, assetTypes[i]}), + Function: "UpdateOrderbook", + Error: msg, + Response: jsonifyInterface([]interface{}{r4}), + }) + + var r5 []string + r5, err = e.FetchTradablePairs(assetTypes[i]) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{assetTypes[i]}), + Function: "FetchTradablePairs", + Error: msg, + Response: jsonifyInterface([]interface{}{r5}), + }) + // r6 + err = e.UpdateTradablePairs(false) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{false}), + Function: "UpdateTradablePairs", + Error: msg, + Response: jsonifyInterface([]interface{}{nil}), + }) + } + + var r7 exchange.AccountInfo + r7, err = e.GetAccountInfo() + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + Function: "GetAccountInfo", + Error: msg, + Response: jsonifyInterface([]interface{}{r7}), + }) + + var r8 []exchange.TradeHistory + r8, err = e.GetExchangeHistory(p, assetTypes[i]) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{p, assetTypes[i]}), + Function: "GetExchangeHistory", + Error: msg, + Response: jsonifyInterface([]interface{}{r8}), + }) + + var r9 []exchange.FundHistory + r9, err = e.GetFundingHistory() + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + Function: "GetFundingHistory", + Error: msg, + Response: jsonifyInterface([]interface{}{r9}), + }) + + feeType := exchange.FeeBuilder{ + FeeType: exchange.CryptocurrencyTradeFee, + Pair: p, + PurchasePrice: config.OrderSubmission.Price, + Amount: config.OrderSubmission.Amount, + } + var r10 float64 + r10, err = e.GetFeeByType(&feeType) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{feeType}), + Function: "GetFeeByType-Trade", + Error: msg, + Response: jsonifyInterface([]interface{}{r10}), + }) + + s := &order.Submit{ + Pair: p, + OrderSide: testOrderSide, + OrderType: testOrderType, + Amount: config.OrderSubmission.Amount, + Price: config.OrderSubmission.Price, + ClientID: config.OrderSubmission.OrderID, + } + var r11 order.SubmitResponse + r11, err = e.SubmitOrder(s) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{*s}), + Function: "SubmitOrder", + Error: msg, + Response: jsonifyInterface([]interface{}{r11}), + }) + + modifyRequest := order.Modify{ + OrderID: config.OrderSubmission.OrderID, + Type: testOrderType, + Side: testOrderSide, + CurrencyPair: p, + Price: config.OrderSubmission.Price, + Amount: config.OrderSubmission.Amount, + } + var r12 string + r12, err = e.ModifyOrder(&modifyRequest) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{modifyRequest}), + Function: "ModifyOrder", + Error: msg, + Response: r12, + }) + // r13 + cancelRequest := order.Cancel{ + Side: testOrderSide, + CurrencyPair: p, + OrderID: config.OrderSubmission.OrderID, + } + err = e.CancelOrder(&cancelRequest) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{cancelRequest}), + Function: "CancelOrder", + Error: msg, + Response: jsonifyInterface([]interface{}{nil}), + }) + + var r14 order.CancelAllResponse + r14, err = e.CancelAllOrders(&cancelRequest) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{cancelRequest}), + Function: "CancelAllOrders", + Error: msg, + Response: jsonifyInterface([]interface{}{r14}), + }) + + var r15 order.Detail + r15, err = e.GetOrderInfo(config.OrderSubmission.OrderID) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{config.OrderSubmission.OrderID}), + Function: "GetOrderInfo", + Error: msg, + Response: jsonifyInterface([]interface{}{r15}), + }) + + historyRequest := order.GetOrdersRequest{ + OrderType: testOrderType, + OrderSide: testOrderSide, + Currencies: []currency.Pair{p}, + } + var r16 []order.Detail + r16, err = e.GetOrderHistory(&historyRequest) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{historyRequest}), + Function: "GetOrderHistory", + Error: msg, + Response: jsonifyInterface([]interface{}{r16}), + }) + + orderRequest := order.GetOrdersRequest{ + OrderType: testOrderType, + OrderSide: testOrderSide, + Currencies: []currency.Pair{p}, + } + var r17 []order.Detail + r17, err = e.GetActiveOrders(&orderRequest) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{orderRequest}), + Function: "GetActiveOrders", + Error: msg, + Response: jsonifyInterface([]interface{}{r17}), + }) + + var r18 string + r18, err = e.GetDepositAddress(p.Base, "") + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{p.Base, ""}), + Function: "GetDepositAddress", + Error: msg, + Response: r18, + }) + + feeType = exchange.FeeBuilder{ + FeeType: exchange.CryptocurrencyWithdrawalFee, + Pair: p, + PurchasePrice: config.OrderSubmission.Price, + Amount: config.OrderSubmission.Amount, + } + var r19 float64 + r19, err = e.GetFeeByType(&feeType) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{feeType}), + Function: "GetFeeByType-Crypto-Withdraw", + Error: msg, + Response: jsonifyInterface([]interface{}{r19}), + }) + + genericWithdrawRequest := exchange.GenericWithdrawRequestInfo{ + Amount: config.OrderSubmission.Amount, + Currency: p.Quote, + } + withdrawRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: genericWithdrawRequest, + Address: withdrawAddressOverride, + } + var r20 string + r20, err = e.WithdrawCryptocurrencyFunds(&withdrawRequest) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{withdrawRequest}), + Function: "WithdrawCryptocurrencyFunds", + Error: msg, + Response: r20, + }) + + feeType = exchange.FeeBuilder{ + FeeType: exchange.InternationalBankWithdrawalFee, + Pair: p, + PurchasePrice: config.OrderSubmission.Price, + Amount: config.OrderSubmission.Amount, + FiatCurrency: currency.AUD, + BankTransactionType: exchange.WireTransfer, + } + var r21 float64 + r21, err = e.GetFeeByType(&feeType) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{feeType}), + Function: "GetFeeByType-FIAT-Withdraw", + Error: msg, + Response: jsonifyInterface([]interface{}{r21}), + }) + + fiatWithdrawRequest := exchange.FiatWithdrawRequest{ + GenericWithdrawRequestInfo: genericWithdrawRequest, + BankAccountName: config.BankDetails.BankAccountName, + BankAccountNumber: config.BankDetails.BankAccountNumber, + SwiftCode: config.BankDetails.SwiftCode, + IBAN: config.BankDetails.Iban, + BankCity: config.BankDetails.BankCity, + BankName: config.BankDetails.BankName, + BankAddress: config.BankDetails.BankAddress, + BankCountry: config.BankDetails.BankCountry, + BankPostalCode: config.BankDetails.BankPostalCode, + BankCode: config.BankDetails.BankCode, + IsExpressWire: config.BankDetails.IsExpressWire, + RequiresIntermediaryBank: config.BankDetails.RequiresIntermediaryBank, + IntermediaryBankName: config.BankDetails.IntermediaryBankName, + IntermediaryBankAccountNumber: config.BankDetails.IntermediaryBankAccountNumber, + IntermediarySwiftCode: config.BankDetails.IntermediarySwiftCode, + IntermediaryIBAN: config.BankDetails.IntermediaryIban, + IntermediaryBankCity: config.BankDetails.IntermediaryBankCity, + IntermediaryBankAddress: config.BankDetails.IntermediaryBankAddress, + IntermediaryBankCountry: config.BankDetails.IntermediaryBankCountry, + IntermediaryBankPostalCode: config.BankDetails.IntermediaryBankPostalCode, + IntermediaryBankCode: config.BankDetails.IntermediaryBankCode, + } + var r22 string + r22, err = e.WithdrawFiatFunds(&fiatWithdrawRequest) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{fiatWithdrawRequest}), + Function: "WithdrawFiatFunds", + Error: msg, + Response: r22, + }) + + var r23 string + r23, err = e.WithdrawFiatFundsToInternationalBank(&fiatWithdrawRequest) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{fiatWithdrawRequest}), + Function: "WithdrawFiatFundsToInternationalBank", + Error: msg, + Response: r23, + }) + response = append(response, responseContainer) + } + return response +} + +func jsonifyInterface(params []interface{}) json.RawMessage { + response, _ := json.MarshalIndent(params, "", " ") + return response +} + +func loadConfig() (Config, error) { + var config Config + file, err := os.OpenFile("wrapperconfig.json", os.O_RDONLY, os.ModePerm) + if err != nil { + return config, err + } + defer file.Close() + keys, err := ioutil.ReadAll(file) + if err != nil { + return config, err + } + + err = json.Unmarshal(keys, &config) + return config, err +} + +func saveConfig(config *Config) { + log.Println("JSONifying config...") + jsonOutput, err := json.MarshalIndent(config, "", " ") + if err != nil { + log.Fatalf("Encountered error encoding JSON: %v", err) + } + + dir, err := os.Getwd() + if err != nil { + log.Printf("Encountered error retrieving output directory: %v", err) + return + } + + log.Printf("Outputting to: %v", filepath.Join(dir, "wrapperconfig.json")) + err = file.Write(filepath.Join(dir, "wrapperconfig.json"), jsonOutput) + if err != nil { + log.Printf("Encountered error writing to disk: %v", err) + return + } +} + +func outputToJSON(exchangeResponses []ExchangeResponses) { + log.Println("JSONifying results...") + jsonOutput, err := json.MarshalIndent(exchangeResponses, "", " ") + if err != nil { + log.Fatalf("Encountered error encoding JSON: %v", err) + } + + dir, err := os.Getwd() + if err != nil { + log.Printf("Encountered error retrieving output directory: %v", err) + return + } + + log.Printf("Outputting to: %v", filepath.Join(dir, fmt.Sprintf("%v.json", outputFileName))) + err = file.Write(filepath.Join(dir, fmt.Sprintf("%v.json", outputFileName)), jsonOutput) + if err != nil { + log.Printf("Encountered error writing to disk: %v", err) + return + } +} + +func outputToHTML(exchangeResponses []ExchangeResponses) { + log.Println("Generating HTML report...") + dir, err := os.Getwd() + if err != nil { + log.Print(err) + return + } + + tmpl, err := template.New("report.tmpl").ParseFiles(filepath.Join(dir, "report.tmpl")) + if err != nil { + log.Print(err) + return + } + + log.Printf("Outputting to: %v", filepath.Join(dir, fmt.Sprintf("%v.html", outputFileName))) + file, err := os.Create(filepath.Join(dir, fmt.Sprintf("%v.html", outputFileName))) + if err != nil { + log.Print(err) + return + } + + defer file.Close() + err = tmpl.Execute(file, exchangeResponses) + if err != nil { + log.Print(err) + return + } +} + +func outputToConsole(exchangeResponses []ExchangeResponses) { + var totalErrors int64 + for i := range exchangeResponses { + log.Printf("------------%v Results-------------\n", exchangeResponses[i].ExchangeName) + for j := range exchangeResponses[i].AssetPairResponses { + for k := range exchangeResponses[i].AssetPairResponses[j].EndpointResponses { + log.Printf("%v Result: %v", exchangeResponses[i].ExchangeName, k) + log.Printf("Function:\t%v", exchangeResponses[i].AssetPairResponses[j].EndpointResponses[k].Function) + log.Printf("AssetType:\t%v", exchangeResponses[i].AssetPairResponses[j].AssetType) + log.Printf("Currency:\t%v\n", exchangeResponses[i].AssetPairResponses[j].CurrencyPair) + log.Printf("Wrapper Params:\t%s\n", exchangeResponses[i].AssetPairResponses[j].EndpointResponses[k].SentParams) + if exchangeResponses[i].AssetPairResponses[j].EndpointResponses[k].Error != "" { + totalErrors++ + log.Printf("Error:\t%v", exchangeResponses[i].AssetPairResponses[j].EndpointResponses[k].Error) + } else { + log.Print("Error:\tnone") + } + if verboseOverride { + log.Printf("Wrapper Response:\t%s", exchangeResponses[i].AssetPairResponses[j].EndpointResponses[k].Response) + } + log.Println() + } + } + log.Println() + } +} diff --git a/cmd/exchange_wrapper_issues/report.tmpl b/cmd/exchange_wrapper_issues/report.tmpl new file mode 100644 index 00000000..2fcede34 --- /dev/null +++ b/cmd/exchange_wrapper_issues/report.tmpl @@ -0,0 +1,155 @@ +{{ $output := . }} + + + + +Wrapper results + + + + + + + + + + +
+ {{ range $i, $exchangeResult := $output }} +
+
+
+

{{ $exchangeResult.ExchangeName }}

+
+
+
+ {{ if $exchangeResult.APIKeysSet }} + API Keys Set {{ else }} API Keys Not Set {{ end }} +
+
+
+ +
+
+
+ +
+ + {{ range $j, $assetPairResponses := $exchangeResult.AssetPairResponses }} + {{ $length := len $exchangeResult.AssetPairResponses }} + {{ if eq $length 1 }} + {{ else }} +
+

{{ $assetPairResponses.AssetType }} {{ $assetPairResponses.CurrencyPair }} Results

+ +
+ {{ end }} + + + + + + + + + + + + {{ range $k, $endpointResponse := $assetPairResponses.EndpointResponses }} + + + + + + + + + + {{ end }} +
+ Asset Type + + Currency Pair + + Function + + Wrapper Params Sent + + Error + + Wrapper Response +
+ {{ $assetPairResponses.AssetType }} + + {{ $assetPairResponses.CurrencyPair }} + + {{ $endpointResponse.Function }} + + +
+
+ {{ $endpointResponse.SentParams | printf "%s" }} +
+
+
+ {{ $endpointResponse.Error }} + + +
+
+ {{ $endpointResponse.Response | printf "%s" }} +
+
+
+ {{ end }} +
+ {{ end }} +
+ + + \ No newline at end of file diff --git a/cmd/exchange_wrapper_issues/types.go b/cmd/exchange_wrapper_issues/types.go new file mode 100644 index 00000000..08b7e467 --- /dev/null +++ b/cmd/exchange_wrapper_issues/types.go @@ -0,0 +1,107 @@ +package main + +import ( + "encoding/json" + + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +// variables for command line overrides +var ( + orderTypeOverride string + outputOverride string + orderSideOverride string + currencyPairOverride string + assetTypeOverride string + orderPriceOverride float64 + orderAmountOverride float64 + withdrawAddressOverride string + authenticatedOnly bool + verboseOverride bool + exchangesToUseOverride string + exchangesToExcludeOverride string + outputFileName string + exchangesToUseList []string + exchangesToExcludeList []string +) + +// Config the data structure for wrapperconfig.json to store all customisation +type Config struct { + OrderSubmission OrderSubmission `json:"orderSubmission"` + WalletAddress string `json:"withdrawWalletAddress"` + BankDetails Bank `json:"bankAccount"` + Exchanges map[string]*config.APICredentialsConfig `json:"exchanges"` +} + +// Key is the format for wrapperconfig.json to store API credentials +type Key struct { + APIKey string `json:"apiKey"` + APISecret string `json:"apiSecret,omitempty"` + ClientID string `json:"clientId,omitempty"` + OTPSecret string `json:"otpSecret,omitempty"` +} + +// ExchangeResponses contains all responses +// associated with an exchange +type ExchangeResponses struct { + ID string + ExchangeName string `json:"exchangeName"` + AssetPairResponses []ExchangeAssetPairResponses `json:"responses"` + ErrorCount int64 `json:"errorCount"` + APIKeysSet bool `json:"apiKeysSet"` +} + +// ExchangeAssetPairResponses contains all responses +// associated with an asset type and currency pair +type ExchangeAssetPairResponses struct { + ErrorCount int64 `json:"errorCount"` + AssetType asset.Item `json:"asset"` + CurrencyPair currency.Pair `json:"currency"` + EndpointResponses []EndpointResponse `json:"responses"` +} + +// EndpointResponse is the data for an individual wrapper response +type EndpointResponse struct { + Function string `json:"function"` + Error string `json:"error"` + Response interface{} `json:"response"` + SentParams json.RawMessage `json:"sentParams"` +} + +// Bank contains all required data for a wrapper withdrawal request +type Bank struct { + BankAccountName string `json:"bankAccountName"` + BankAccountNumber string `json:"bankAccountNumber"` + BankAddress string `json:"bankAddress"` + BankCity string `json:"bankCity"` + BankCountry string `json:"bankCountry"` + BankName string `json:"bankName"` + BankPostalCode string `json:"bankPostalCode"` + Iban string `json:"iban"` + IntermediaryBankAccountName string `json:"intermediaryBankAccountName"` + IntermediaryBankAccountNumber float64 `json:"intermediaryBankAccountNumber"` + IntermediaryBankAddress string `json:"intermediaryBankAddress"` + IntermediaryBankCity string `json:"intermediaryBankCity"` + IntermediaryBankCountry string `json:"intermediaryBankCountry"` + IntermediaryBankName string `json:"intermediaryBankName"` + IntermediaryBankPostalCode string `json:"intermediaryBankPostalCode"` + IntermediaryIban string `json:"intermediaryIban"` + IntermediaryIsExpressWire bool `json:"intermediaryIsExpressWire"` + IntermediarySwiftCode string `json:"intermediarySwiftCode"` + IsExpressWire bool `json:"isExpressWire"` + RequiresIntermediaryBank bool `json:"requiresIntermediaryBank"` + SwiftCode string `json:"swiftCode"` + BankCode float64 `json:"bankCode"` + IntermediaryBankCode float64 `json:"intermediaryBankCode"` +} + +// OrderSubmission contains all data required for a wrapper order submission +type OrderSubmission struct { + OrderSide string `json:"orderSide"` + OrderType string `json:"orderType"` + Amount float64 `json:"amount"` + Price float64 `json:"price"` + OrderID string `json:"orderID"` +} diff --git a/cmd/exchange_wrapper_issues/wrapperconfig.json b/cmd/exchange_wrapper_issues/wrapperconfig.json new file mode 100644 index 00000000..3d54a318 --- /dev/null +++ b/cmd/exchange_wrapper_issues/wrapperconfig.json @@ -0,0 +1,182 @@ +{ + "orderSubmission": { + "orderSide": "BUY", + "orderType": "LIMIT", + "amount": 1333333337, + "price": 1333333337, + "orderID": "" + }, + "withdrawWalletAddress": "", + "bankAccount": { + "bankAccountName": "bankAccountName", + "bankAccountNumber": 1337, + "bankAddress": "bankAddress", + "bankCity": "bankCity", + "bankCountry": "bankCountry", + "bankName": "bankName", + "bankPostalCode": "bankPostalCode", + "iban": "iban", + "intermediaryBankAccountName": "intermediaryBankAccountName", + "intermediaryBankAccountNumber": 1337, + "intermediaryBankAddress": "intermediaryBankAddress", + "intermediaryBankCity": "intermediaryBankCity", + "intermediaryBankCountry": "intermediaryBankCountry", + "intermediaryBankName": "intermediaryBankName", + "intermediaryBankPostalCode": "intermediaryBankPostalCode", + "intermediaryIban": "intermediaryIban", + "intermediaryIsExpressWire": false, + "intermediarySwiftCode": "intermediarySwiftCode", + "isExpressWire": false, + "requiresIntermediaryBank": false, + "swiftCode": "swiftCode", + "bankCode": 1337, + "intermediaryBankCode": 1337 + }, + "exchanges": { + "alphapoint": {}, + "anx": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "binance": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "bitfinex": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "bitflyer": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "bithumb": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "bitmex": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "bitstamp": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID", + "otpSecret": "-" + }, + "bittrex": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "btc markets": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "btse": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "coinbasepro": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID", + "otpSecret": "-" + }, + "coinbene": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "coinut": { + "key": "Key", + "clientID": "ClientID", + "otpSecret": "-" + }, + "exmo": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "gateio": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "gemini": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "hitbtc": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "huobi": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "itbit": { + "secret": "Secret", + "clientID": "ClientID", + "otpSecret": "-" + }, + "kraken": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "lakebtc": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "lbank": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "localbitcoins": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "okcoin international": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID", + "otpSecret": "-" + }, + "okex": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID", + "otpSecret": "-" + }, + "poloniex": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "yobit": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, + "zb": { + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + } + } +} \ No newline at end of file diff --git a/cmd/gctcli/README.md b/cmd/gctcli/README.md new file mode 100644 index 00000000..6a22e7ae --- /dev/null +++ b/cmd/gctcli/README.md @@ -0,0 +1,38 @@ +# GoCryptoTrader gRPC client + + + +[![Build Status](https://travis-ci.com/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.com/thrasher-corp/gocryptotrader) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + +A cryptocurrency trading bot supporting multiple exchanges written in Golang. + +**Please note that this bot is under development and is not ready for production!** + +## Community + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Background + +GoCryptoTrader utilises gRPC for client/server interaction. Authentication is done +by a self signed TLS cert, which only supports connections from localhost and also +through basic authorisation specified by the users config file. + +## Usage + +GoCryptoTrader must be running with gRPC enabled in order to use the client features. + +```bash +go build or go run . +``` + +For a full list of commands, you can run `gctcli --help`. Alternatively, you can also +visit our [GoCryptoTrader API reference.](https://api.gocryptotrader.app/) + +## Autocomplete + +Bash/ZSH autocomplete entries can be found [here](/contrib). diff --git a/cmd/gctcli/commands.go b/cmd/gctcli/commands.go new file mode 100644 index 00000000..1b596368 --- /dev/null +++ b/cmd/gctcli/commands.go @@ -0,0 +1,2917 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/gctrpc" + "github.com/urfave/cli" +) + +var getInfoCommand = cli.Command{ + Name: "getinfo", + Usage: "gets GoCryptoTrader info", + Action: getInfo, +} + +func getInfo(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetInfo(context.Background(), + &gctrpc.GetInfoRequest{}, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getSubsystemsCommand = cli.Command{ + Name: "getsubsystems", + Usage: "gets GoCryptoTrader subsystems and their status", + Action: getSubsystems, +} + +func getSubsystems(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetSubsystems(context.Background(), + &gctrpc.GetSubsystemsRequest{}, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var enableSubsystemCommand = cli.Command{ + Name: "enablesubsystem", + Usage: "enables an engine subsystem", + ArgsUsage: "", + Action: enableSubsystem, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "subsystem", + Usage: "the subsystem to enable", + }, + }, +} + +func enableSubsystem(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "enablesubsystem") + return nil + } + + var subsystemName string + if c.IsSet("subsystem") { + subsystemName = c.String("subsystem") + } else { + subsystemName = c.Args().First() + } + + if subsystemName == "" { + return errors.New("invalid subsystem supplied") + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.EnableSubsystem(context.Background(), + &gctrpc.GenericSubsystemRequest{ + Subsystem: subsystemName, + }, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var disableSubsystemCommand = cli.Command{ + Name: "disablesubsystem", + Usage: "disables an engine subsystem", + ArgsUsage: "", + Action: disableSubsystem, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "subsystem", + Usage: "the subsystem to disable", + }, + }, +} + +func disableSubsystem(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "disablesubsystem") + return nil + } + + var subsystemName string + if c.IsSet("subsystem") { + subsystemName = c.String("subsystem") + } else { + subsystemName = c.Args().First() + } + + if subsystemName == "" { + return errors.New("invalid subsystem supplied") + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.DisableSubsystem(context.Background(), + &gctrpc.GenericSubsystemRequest{ + Subsystem: subsystemName, + }, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getRPCEndpointsCommand = cli.Command{ + Name: "getrpcendpoints", + Usage: "gets GoCryptoTrader endpoints info", + Action: getRPCEndpoints, +} + +func getRPCEndpoints(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetRPCEndpoints(context.Background(), + &gctrpc.GetRPCEndpointsRequest{}, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getCommunicationRelayersCommand = cli.Command{ + Name: "getcommsrelayers", + Usage: "gets GoCryptoTrader communication relayers", + Action: getCommunicationRelayers, +} + +func getCommunicationRelayers(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetCommunicationRelayers(context.Background(), + &gctrpc.GetCommunicationRelayersRequest{}, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getExchangesCommand = cli.Command{ + Name: "getexchanges", + Usage: "gets a list of enabled or available exchanges", + ArgsUsage: "", + Action: getExchanges, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "enabled", + Usage: "whether to list enabled exchanges or not", + }, + }, +} + +func getExchanges(c *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + var enabledOnly bool + if c.IsSet("enabled") { + enabledOnly = c.Bool("enabled") + } + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetExchanges(context.Background(), + &gctrpc.GetExchangesRequest{ + Enabled: enabledOnly, + }, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var enableExchangeCommand = cli.Command{ + Name: "enableexchange", + Usage: "enables an exchange", + ArgsUsage: "", + Action: enableExchange, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to enable", + }, + }, +} + +func enableExchange(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "enableexchange") + return nil + } + + var exchangeName string + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.EnableExchange(context.Background(), + &gctrpc.GenericExchangeNameRequest{ + Exchange: exchangeName, + }, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var disableExchangeCommand = cli.Command{ + Name: "disableexchange", + Usage: "disables an exchange", + ArgsUsage: "", + Action: disableExchange, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to disable", + }, + }, +} + +func disableExchange(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "disableexchange") + return nil + } + + var exchangeName string + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.DisableExchange(context.Background(), + &gctrpc.GenericExchangeNameRequest{ + Exchange: exchangeName, + }, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getExchangeOTPCommand = cli.Command{ + Name: "getexchangeotp", + Usage: "gets a specific exchange OTP code", + ArgsUsage: "", + Action: getExchangeOTPCode, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the OTP code for", + }, + }, +} + +func getExchangeOTPCode(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getexchangeotp") + return nil + } + + var exchangeName string + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetExchangeOTPCode(context.Background(), + &gctrpc.GenericExchangeNameRequest{ + Exchange: exchangeName, + }, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getExchangeOTPsCommand = cli.Command{ + Name: "getexchangeotps", + Usage: "gets all exchange OTP codes", + Action: getExchangeOTPCodes, +} + +func getExchangeOTPCodes(c *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetExchangeOTPCodes(context.Background(), + &gctrpc.GetExchangeOTPsRequest{}) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getExchangeInfoCommand = cli.Command{ + Name: "getexchangeinfo", + Usage: "gets a specific exchanges info", + ArgsUsage: "", + Action: getExchangeInfo, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the info for", + }, + }, +} + +func getExchangeInfo(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getexchangeinfo") + return nil + } + + var exchangeName string + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetExchangeInfo(context.Background(), + &gctrpc.GenericExchangeNameRequest{ + Exchange: exchangeName, + }, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getTickerCommand = cli.Command{ + Name: "getticker", + Usage: "gets the ticker for a specific currency pair and exchange", + ArgsUsage: " ", + Action: getTicker, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the ticker for", + }, + cli.StringFlag{ + Name: "pair", + Usage: "the currency pair to get the ticker for", + }, + cli.StringFlag{ + Name: "asset", + Usage: "the asset type of the currency pair to get the ticker for", + }, + }, +} + +func getTicker(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getticker") + return nil + } + + var exchangeName string + var currencyPair string + var assetType string + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + if c.IsSet("pair") { + currencyPair = c.String("pair") + } else { + currencyPair = c.Args().Get(1) + } + + if !validPair(currencyPair) { + return errInvalidPair + } + + if c.IsSet("asset") { + assetType = c.String("asset") + } else { + assetType = c.Args().Get(2) + } + + assetType = strings.ToLower(assetType) + if !validAsset(assetType) { + return errInvalidAsset + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + p := currency.NewPairDelimiter(currencyPair, pairDelimiter) + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetTicker(context.Background(), + &gctrpc.GetTickerRequest{ + Exchange: exchangeName, + Pair: &gctrpc.CurrencyPair{ + Delimiter: p.Delimiter, + Base: p.Base.String(), + Quote: p.Quote.String(), + }, + AssetType: assetType, + }, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getTickersCommand = cli.Command{ + Name: "gettickers", + Usage: "gets all tickers for all enabled exchanges and currency pairs", + Action: getTickers, +} + +func getTickers(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetTickers(context.Background(), &gctrpc.GetTickersRequest{}) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getOrderbookCommand = cli.Command{ + Name: "getorderbook", + Usage: "gets the orderbook for a specific currency pair and exchange", + ArgsUsage: " ", + Action: getOrderbook, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the orderbook for", + }, + cli.StringFlag{ + Name: "pair", + Usage: "the currency pair to get the orderbook for", + }, + cli.StringFlag{ + Name: "asset", + Usage: "the asset type of the currency pair to get the orderbook for", + }, + }, +} + +func getOrderbook(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getorderbook") + return nil + } + + var exchangeName string + var currencyPair string + var assetType string + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + if c.IsSet("pair") { + currencyPair = c.String("pair") + } else { + currencyPair = c.Args().Get(1) + } + + if !validPair(currencyPair) { + return errInvalidPair + } + + if c.IsSet("asset") { + assetType = c.String("asset") + } else { + assetType = c.Args().Get(2) + } + + assetType = strings.ToLower(assetType) + if !validAsset(assetType) { + return errInvalidAsset + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + p := currency.NewPairDelimiter(currencyPair, pairDelimiter) + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetOrderbook(context.Background(), + &gctrpc.GetOrderbookRequest{ + Exchange: exchangeName, + Pair: &gctrpc.CurrencyPair{ + Delimiter: p.Delimiter, + Base: p.Base.String(), + Quote: p.Quote.String(), + }, + AssetType: assetType, + }, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getOrderbooksCommand = cli.Command{ + Name: "getorderbooks", + Usage: "gets all orderbooks for all enabled exchanges and currency pairs", + Action: getOrderbooks, +} + +func getOrderbooks(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetOrderbooks(context.Background(), &gctrpc.GetOrderbooksRequest{}) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getAccountInfoCommand = cli.Command{ + Name: "getaccountinfo", + Usage: "gets the exchange account balance info", + ArgsUsage: "", + Action: getAccountInfo, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the account info for", + }, + }, +} + +func getAccountInfo(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getaccountinfo") + return nil + } + + var exchange string + if c.IsSet("exchange") { + exchange = c.String("exchange") + } else { + exchange = c.Args().First() + } + + if !validExchange(exchange) { + return errInvalidExchange + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetAccountInfo(context.Background(), + &gctrpc.GetAccountInfoRequest{ + Exchange: exchange, + }, + ) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getConfigCommand = cli.Command{ + Name: "getconfig", + Usage: "gets the config", + Action: getConfig, +} + +func getConfig(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetConfig(context.Background(), &gctrpc.GetConfigRequest{}) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getPortfolioCommand = cli.Command{ + Name: "getportfolio", + Usage: "gets the portfolio", + Action: getPortfolio, +} + +func getPortfolio(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetPortfolio(context.Background(), &gctrpc.GetPortfolioRequest{}) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getPortfolioSummaryCommand = cli.Command{ + Name: "getportfoliosummary", + Usage: "gets the portfolio summary", + Action: getPortfolioSummary, +} + +func getPortfolioSummary(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetPortfolioSummary(context.Background(), &gctrpc.GetPortfolioSummaryRequest{}) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var addPortfolioAddressCommand = cli.Command{ + Name: "addportfolioaddress", + Usage: "adds an address to the portfolio", + ArgsUsage: "
", + Action: addPortfolioAddress, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "address", + Usage: "the address to add to the portfolio", + }, + cli.StringFlag{ + Name: "coin_type", + Usage: "the coin type e.g ('BTC')", + }, + cli.StringFlag{ + Name: "description", + Usage: "description of the address", + }, + cli.Float64Flag{ + Name: "balance", + Usage: "balance of the address", + }, + }, +} + +func addPortfolioAddress(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "addportfolioaddress") + return nil + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + var address string + var coinType string + var description string + var balance float64 + + if c.IsSet("address") { + address = c.String("address") + } else { + address = c.Args().First() + } + + if c.IsSet("coin_type") { + coinType = c.String("coin_type") + } else { + coinType = c.Args().Get(1) + } + + if c.IsSet("description") { + description = c.String("asset") + } else { + description = c.Args().Get(2) + } + + if c.IsSet("balance") { + balance = c.Float64("balance") + } else { + balance, _ = strconv.ParseFloat(c.Args().Get(3), 64) + } + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.AddPortfolioAddress(context.Background(), + &gctrpc.AddPortfolioAddressRequest{ + Address: address, + CoinType: coinType, + Description: description, + Balance: balance, + }, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var removePortfolioAddressCommand = cli.Command{ + Name: "removeportfolioaddress", + Usage: "removes an address from the portfolio", + ArgsUsage: "
", + Action: removePortfolioAddress, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "address", + Usage: "the address to add to the portfolio", + }, + cli.StringFlag{ + Name: "coin_type", + Usage: "the coin type e.g ('BTC')", + }, + cli.StringFlag{ + Name: "description", + Usage: "description of the address", + }, + }, +} + +func removePortfolioAddress(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "removeportfolioaddress") + return nil + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + var address string + var coinType string + var description string + + if c.IsSet("address") { + address = c.String("address") + } else { + address = c.Args().First() + } + + if c.IsSet("coin_type") { + coinType = c.String("coin_type") + } else { + coinType = c.Args().Get(1) + } + + if c.IsSet("description") { + description = c.String("asset") + } else { + description = c.Args().Get(2) + } + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.RemovePortfolioAddress(context.Background(), + &gctrpc.RemovePortfolioAddressRequest{ + Address: address, + CoinType: coinType, + Description: description, + }, + ) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getForexProvidersCommand = cli.Command{ + Name: "getforexproviders", + Usage: "gets the available forex providers", + Action: getForexProviders, +} + +func getForexProviders(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetForexProviders(context.Background(), &gctrpc.GetForexProvidersRequest{}) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getForexRatesCommand = cli.Command{ + Name: "getforexrates", + Usage: "gets forex rates", + Action: getForexRates, +} + +func getForexRates(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetForexRates(context.Background(), &gctrpc.GetForexRatesRequest{}) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getOrdersCommand = cli.Command{ + Name: "getorders", + Usage: "gets the open orders", + ArgsUsage: " ", + Action: getOrders, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get orders for", + }, + cli.StringFlag{ + Name: "asset_type", + Usage: "the asset type to get orders for", + }, + cli.StringFlag{ + Name: "pair", + Usage: "the currency pair to get orders for", + }, + }, +} + +func getOrders(c *cli.Context) error { + var exchangeName string + var assetType string + var currencyPair string + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + if c.IsSet("asset_type") { + assetType = c.String("asset_type") + } else { + assetType = c.Args().Get(1) + } + + assetType = strings.ToLower(assetType) + if !validAsset(assetType) { + return errInvalidAsset + } + + if c.IsSet("pair") { + currencyPair = c.String("pair") + } else { + currencyPair = c.Args().Get(2) + } + + if !validPair(currencyPair) { + return errInvalidPair + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + p := currency.NewPairDelimiter(currencyPair, pairDelimiter) + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetOrders(context.Background(), &gctrpc.GetOrdersRequest{ + Exchange: exchangeName, + AssetType: assetType, + Pair: &gctrpc.CurrencyPair{ + Delimiter: p.Delimiter, + Base: p.Base.String(), + Quote: p.Quote.String(), + }, + }) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getOrderCommand = cli.Command{ + Name: "getorder", + Usage: "gets the specified order info", + ArgsUsage: " ", + Action: getOrder, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the order for", + }, + cli.StringFlag{ + Name: "order_id", + Usage: "the order id to retrieve", + }, + }, +} + +func getOrder(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getorder") + return nil + } + + var exchangeName string + var orderID string + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + if c.IsSet("order_id") { + orderID = c.String("order_id") + } else { + orderID = c.Args().Get(1) + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetOrder(context.Background(), &gctrpc.GetOrderRequest{ + Exchange: exchangeName, + OrderId: orderID, + }) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var submitOrderCommand = cli.Command{ + Name: "submitorder", + Usage: "submit order submits an exchange order", + ArgsUsage: " ", + Action: submitOrder, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to submit the order for", + }, + cli.StringFlag{ + Name: "pair", + Usage: "the currency pair", + }, + cli.StringFlag{ + Name: "side", + Usage: "the order side to use (BUY OR SELL)", + }, + cli.StringFlag{ + Name: "order_type", + Usage: "the order type (MARKET OR LIMIT)", + }, + cli.Float64Flag{ + Name: "amount", + Usage: "the amount for the order", + }, + cli.Float64Flag{ + Name: "price", + Usage: "the price for the order", + }, + cli.StringFlag{ + Name: "client_id", + Usage: "the optional client order ID", + }, + }, +} + +func submitOrder(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "submitorder") + return nil + } + + var exchangeName string + var currencyPair string + var orderSide string + var orderType string + var amount float64 + var price float64 + var clientID string + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + if c.IsSet("pair") { + currencyPair = c.String("pair") + } else { + currencyPair = c.Args().Get(1) + } + + if !validPair(currencyPair) { + return errInvalidPair + } + + if c.IsSet("side") { + orderSide = c.String("side") + } else { + orderSide = c.Args().Get(2) + } + + if c.IsSet("order_type") { + orderType = c.String("order_type") + } else { + orderType = c.Args().Get(3) + } + + if c.IsSet("amount") { + amount = c.Float64("amount") + } else { + amount, _ = strconv.ParseFloat(c.Args().Get(4), 64) + } + + if c.IsSet("price") { + price = c.Float64("price") + } else { + price, _ = strconv.ParseFloat(c.Args().Get(5), 64) + } + + if c.IsSet("client_id") { + clientID = c.String("client_id") + } else { + clientID = c.Args().Get(6) + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + p := currency.NewPairDelimiter(currencyPair, pairDelimiter) + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.SubmitOrder(context.Background(), &gctrpc.SubmitOrderRequest{ + Exchange: exchangeName, + Pair: &gctrpc.CurrencyPair{ + Delimiter: p.Delimiter, + Base: p.Base.String(), + Quote: p.Quote.String(), + }, + Side: orderSide, + OrderType: orderType, + Amount: amount, + Price: price, + ClientId: clientID, + }) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var simulateOrderCommand = cli.Command{ + Name: "simulateorder", + Usage: "simulate order simulates an exchange order", + ArgsUsage: " ", + Action: simulateOrder, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to simulate the order for", + }, + cli.StringFlag{ + Name: "pair", + Usage: "the currency pair", + }, + cli.StringFlag{ + Name: "side", + Usage: "the order side to use (BUY OR SELL)", + }, + cli.Float64Flag{ + Name: "amount", + Usage: "the amount for the order", + }, + }, +} + +func simulateOrder(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "simulateorder") + return nil + } + + var exchangeName string + var currencyPair string + var orderSide string + var amount float64 + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + if c.IsSet("pair") { + currencyPair = c.String("pair") + } else { + currencyPair = c.Args().Get(1) + } + + if !validPair(currencyPair) { + return errInvalidPair + } + + if c.IsSet("side") { + orderSide = c.String("side") + } else { + orderSide = c.Args().Get(2) + } + + if c.IsSet("amount") { + amount = c.Float64("amount") + } else { + amount, _ = strconv.ParseFloat(c.Args().Get(3), 64) + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + p := currency.NewPairDelimiter(currencyPair, pairDelimiter) + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.SimulateOrder(context.Background(), &gctrpc.SimulateOrderRequest{ + Exchange: exchangeName, + Pair: &gctrpc.CurrencyPair{ + Delimiter: p.Delimiter, + Base: p.Base.String(), + Quote: p.Quote.String(), + }, + Side: orderSide, + Amount: amount, + }) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var whaleBombCommand = cli.Command{ + Name: "whalebomb", + Usage: "whale bomb finds the amount required to reach a price target", + ArgsUsage: " ", + Action: whaleBomb, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to whale bomb", + }, + cli.StringFlag{ + Name: "pair", + Usage: "the currency pair", + }, + cli.StringFlag{ + Name: "side", + Usage: "the order side to use (BUY OR SELL)", + }, + cli.Float64Flag{ + Name: "price", + Usage: "the price target", + }, + }, +} + +func whaleBomb(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "whalebomb") + return nil + } + + var exchangeName string + var currencyPair string + var orderSide string + var price float64 + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + if c.IsSet("pair") { + currencyPair = c.String("pair") + } else { + currencyPair = c.Args().Get(1) + } + + if !validPair(currencyPair) { + return errInvalidPair + } + + if c.IsSet("side") { + orderSide = c.String("side") + } else { + orderSide = c.Args().Get(2) + } + + if c.IsSet("price") { + price = c.Float64("price") + } else { + price, _ = strconv.ParseFloat(c.Args().Get(3), 64) + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + p := currency.NewPairDelimiter(currencyPair, pairDelimiter) + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.WhaleBomb(context.Background(), &gctrpc.WhaleBombRequest{ + Exchange: exchangeName, + Pair: &gctrpc.CurrencyPair{ + Delimiter: p.Delimiter, + Base: p.Base.String(), + Quote: p.Quote.String(), + }, + Side: orderSide, + PriceTarget: price, + }) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var cancelOrderCommand = cli.Command{ + Name: "cancelorder", + Usage: "cancel order cancels an exchange order", + ArgsUsage: " ", + Action: cancelOrder, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to cancel the order for", + }, + cli.StringFlag{ + Name: "account_id", + Usage: "the account id", + }, + cli.StringFlag{ + Name: "order_id", + Usage: "the order id", + }, + cli.StringFlag{ + Name: "pair", + Usage: "the currency pair to cancel the order for", + }, + cli.StringFlag{ + Name: "asset_type", + Usage: "the asset type", + }, + cli.Float64Flag{ + Name: "wallet_address", + Usage: "the wallet address", + }, + cli.StringFlag{ + Name: "side", + Usage: "the order side", + }, + }, +} + +func cancelOrder(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "cancelorder") + return nil + } + + var exchangeName string + var accountID string + var orderID string + var currencyPair string + var assetType string + var walletAddress string + var orderSide string + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + if c.IsSet("order_id") { + orderID = c.String("order_id") + } else { + orderID = c.Args().Get(2) + } + + if c.IsSet("account_id") { + accountID = c.String("account_id") + } + + if c.IsSet("pair") { + currencyPair = c.String("pair") + } + + if c.IsSet("asset_type") { + assetType = c.String("asset_type") + } + + assetType = strings.ToLower(assetType) + if !validAsset(assetType) { + return errInvalidAsset + } + + if c.IsSet("wallet_address") { + walletAddress = c.String("wallet_address") + } + + if c.IsSet("order_side") { + orderSide = c.String("order_side") + } + + var p currency.Pair + if len(currencyPair) > 0 { + if !validPair(currencyPair) { + return errInvalidPair + } + p = currency.NewPairDelimiter(currencyPair, pairDelimiter) + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.CancelOrder(context.Background(), &gctrpc.CancelOrderRequest{ + Exchange: exchangeName, + AccountId: accountID, + OrderId: orderID, + Pair: &gctrpc.CurrencyPair{ + Delimiter: p.Delimiter, + Base: p.Base.String(), + Quote: p.Quote.String(), + }, + AssetType: assetType, + WalletAddress: walletAddress, + Side: orderSide, + }) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var cancelAllOrdersCommand = cli.Command{ + Name: "cancelallorders", + Usage: "cancels all orders (all or by exchange name)", + ArgsUsage: "", + Action: cancelAllOrders, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to cancel all orders on", + }, + }, +} + +func cancelAllOrders(c *cli.Context) error { + var exchangeName string + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + // exchange name is an optional param + if exchangeName != "" { + if !validExchange(exchangeName) { + return errInvalidExchange + } + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.CancelAllOrders(context.Background(), &gctrpc.CancelAllOrdersRequest{ + Exchange: exchangeName, + }) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getEventsCommand = cli.Command{ + Name: "getevents", + Usage: "gets all events", + Action: getEvents, +} + +func getEvents(_ *cli.Context) error { + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetEvents(context.Background(), &gctrpc.GetEventsRequest{}) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var addEventCommand = cli.Command{ + Name: "addevent", + Usage: "adds an event", + ArgsUsage: " ", + Action: addEvent, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to add an event for", + }, + cli.StringFlag{ + Name: "item", + Usage: "the item to trigger the event", + }, + cli.StringFlag{ + Name: "condition", + Usage: "the condition for the event", + }, + cli.Float64Flag{ + Name: "price", + Usage: "the price to trigger the event", + }, + cli.BoolFlag{ + Name: "check_bids", + Usage: "whether to check the bids (if false, asks will be used)", + }, + cli.BoolFlag{ + Name: "check_bids_and_asks", + Usage: "the wallet address", + }, + cli.Float64Flag{ + Name: "orderbook_amount", + Usage: "the orderbook amount to trigger the event", + }, + cli.StringFlag{ + Name: "pair", + Usage: "the currency pair", + }, + cli.StringFlag{ + Name: "asset_type", + Usage: "the asset type", + }, + cli.StringFlag{ + Name: "action", + Usage: "the action for the event to perform upon trigger", + }, + }, +} + +func addEvent(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "addevent") + return nil + } + + var exchangeName string + var item string + var condition string + var price float64 + var checkBids bool + var checkBidsAndAsks bool + var orderbookAmount float64 + var currencyPair string + var assetType string + var action string + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + return fmt.Errorf("exchange name is required") + } + + if c.IsSet("item") { + item = c.String("item") + } else { + return fmt.Errorf("item is required") + } + + if c.IsSet("condition") { + condition = c.String("condition") + } else { + return fmt.Errorf("condition is required") + } + + if c.IsSet("price") { + price = c.Float64("price") + } + + if c.IsSet("check_bids") { + checkBids = c.Bool("check_bids") + } + + if c.IsSet("check_bids_and_asks") { + checkBids = c.Bool("check_bids_and_asks") + } + + if c.IsSet("orderbook_amount") { + orderbookAmount = c.Float64("orderbook_amount") + } + + if c.IsSet("pair") { + currencyPair = c.String("pair") + } else { + return fmt.Errorf("currency pair is required") + } + + if c.IsSet("asset_type") { + assetType = c.String("asset_type") + } + + assetType = strings.ToLower(assetType) + if !validAsset(assetType) { + return errInvalidAsset + } + + if c.IsSet("action") { + action = c.String("action") + } else { + return fmt.Errorf("action is required") + } + + if !validPair(currencyPair) { + return errInvalidPair + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + p := currency.NewPairDelimiter(currencyPair, pairDelimiter) + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.AddEvent(context.Background(), &gctrpc.AddEventRequest{ + Exchange: exchangeName, + Item: item, + ConditionParams: &gctrpc.ConditionParams{ + Condition: condition, + Price: price, + CheckBids: checkBids, + CheckBidsAndAsks: checkBidsAndAsks, + OrderbookAmount: orderbookAmount, + }, + Pair: &gctrpc.CurrencyPair{ + Delimiter: p.Delimiter, + Base: p.Base.String(), + Quote: p.Quote.String(), + }, + AssetType: assetType, + Action: action, + }) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var removeEventCommand = cli.Command{ + Name: "removeevent", + Usage: "removes an event", + ArgsUsage: "", + Action: removeEvent, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "event_id", + Usage: "the event id to remove", + }, + }, +} + +func removeEvent(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "removeevent") + return nil + } + + var eventID int64 + if c.IsSet("event_id") { + eventID = c.Int64("event_id") + } else { + evtID, err := strconv.Atoi(c.Args().Get(0)) + if err != nil { + return fmt.Errorf("unable to strconv input to int. Err: %s", err) + } + eventID = int64(evtID) + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.RemoveEvent(context.Background(), + &gctrpc.RemoveEventRequest{Id: eventID}) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getCryptocurrencyDepositAddressesCommand = cli.Command{ + Name: "getcryptocurrencydepositaddresses", + Usage: "gets the cryptocurrency deposit addresses for an exchange", + ArgsUsage: "", + Action: getCryptocurrencyDepositAddresses, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the cryptocurrency deposit addresses for", + }, + }, +} + +func getCryptocurrencyDepositAddresses(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getcryptocurrencydepositaddresses") + return nil + } + + var exchangeName string + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetCryptocurrencyDepositAddresses(context.Background(), + &gctrpc.GetCryptocurrencyDepositAddressesRequest{Exchange: exchangeName}) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var getCryptocurrencyDepositAddressCommand = cli.Command{ + Name: "getcryptocurrencydepositaddress", + Usage: "gets the cryptocurrency deposit address for an exchange and cryptocurrency", + ArgsUsage: " ", + Action: getCryptocurrencyDepositAddress, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the cryptocurrency deposit address for", + }, + cli.StringFlag{ + Name: "cryptocurrency", + Usage: "the cryptocurrency to get the deposit address for", + }, + }, +} + +func getCryptocurrencyDepositAddress(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getcryptocurrencydepositaddresses") + return nil + } + + var exchangeName string + var cryptocurrency string + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + if c.IsSet("cryptocurrency") { + cryptocurrency = c.String("cryptocurrency") + } else { + cryptocurrency = c.Args().Get(1) + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetCryptocurrencyDepositAddress(context.Background(), + &gctrpc.GetCryptocurrencyDepositAddressRequest{ + Exchange: exchangeName, + Cryptocurrency: cryptocurrency, + }, + ) + if err != nil { + return err + } + + jsonOutput(result) + return nil +} + +var withdrawCryptocurrencyFundsCommand = cli.Command{ + Name: "withdrawcryptocurrencyfunds", + Usage: "withdraws cryptocurrency funds from the desired exchange", + ArgsUsage: " ", + Action: withdrawCryptocurrencyFunds, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to withdraw from", + }, + cli.StringFlag{ + Name: "cryptocurrency", + Usage: "the cryptocurrency to withdraw funds from", + }, + }, +} + +func withdrawCryptocurrencyFunds(_ *cli.Context) error { + return common.ErrNotYetImplemented +} + +var withdrawFiatFundsCommand = cli.Command{ + Name: "withdrawfiatfunds", + Usage: "withdraws fiat funds from the desired exchange", + ArgsUsage: " ", + Action: withdrawFiatFunds, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to withdraw from", + }, + cli.StringFlag{ + Name: "fiat_currency", + Usage: "the fiat currency to withdraw funds from", + }, + }, +} + +func withdrawFiatFunds(_ *cli.Context) error { + return common.ErrNotYetImplemented +} + +var getLoggerDetailsCommand = cli.Command{ + Name: "getloggerdetails", + Usage: "gets an individual loggers details", + ArgsUsage: "", + Action: getLoggerDetails, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "logger", + Usage: "logger to get level details of", + }, + }, +} + +func getLoggerDetails(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getloggerdetails") + return nil + } + + var logger string + if c.IsSet("logger") { + logger = c.String("logger") + } else { + logger = c.Args().First() + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + + result, err := client.GetLoggerDetails(context.Background(), + &gctrpc.GetLoggerDetailsRequest{ + Logger: logger, + }, + ) + if err != nil { + return err + } + jsonOutput(result) + return nil +} + +var setLoggerDetailsCommand = cli.Command{ + Name: "setloggerdetails", + Usage: "sets an individual loggers details", + ArgsUsage: " ", + Action: setLoggerDetails, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "logger", + Usage: "logger to get level details of", + }, + cli.StringFlag{ + Name: "flags", + Usage: "pipe separated value of loggers e.g INFO|WARN", + }, + }, +} + +func setLoggerDetails(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "setloggerdetails") + return nil + } + + var logger string + var level string + + if c.IsSet("logger") { + logger = c.String("logger") + } else { + logger = c.Args().First() + } + + if c.IsSet("level") { + level = c.String("level") + } else { + level = c.Args().Get(1) + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + + result, err := client.SetLoggerDetails(context.Background(), + &gctrpc.SetLoggerDetailsRequest{ + Logger: logger, + Level: level, + }, + ) + if err != nil { + return err + } + jsonOutput(result) + return nil +} + +var getExchangePairsCommand = cli.Command{ + Name: "getexchangepairs", + Usage: "gets an exchanges supported currency pairs (available and enabled) plus asset types", + ArgsUsage: " ", + Action: getExchangePairs, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to list of the currency pairs of", + }, + cli.StringFlag{ + Name: "asset", + Usage: "the asset type to filter by", + }, + }, +} + +func getExchangePairs(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getexchangepairs") + return nil + } + + var exchange string + var asset string + + if c.IsSet("exchange") { + exchange = c.String("exchange") + } else { + exchange = c.Args().First() + } + + if !validExchange(exchange) { + return errInvalidExchange + } + + if c.IsSet("asset") { + asset = c.String("asset") + } else { + asset = c.Args().Get(1) + } + + asset = strings.ToLower(asset) + if !validAsset(asset) { + return errInvalidAsset + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetExchangePairs(context.Background(), + &gctrpc.GetExchangePairsRequest{ + Exchange: exchange, + Asset: asset, + }, + ) + if err != nil { + return err + } + jsonOutput(result) + return nil +} + +var enableExchangePairCommand = cli.Command{ + Name: "enableexchangepair", + Usage: "enables an exchange currency pair", + ArgsUsage: " ", + Action: enableExchangePair, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to enable the currency pair for", + }, + cli.StringFlag{ + Name: "pair", + Usage: "the currency pair to enable", + }, + cli.StringFlag{ + Name: "asset", + Usage: "the asset type to enable the currency pair for", + }, + }, +} + +func enableExchangePair(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "enableexchangepair") + return nil + } + + var exchange string + var pair string + var asset string + + if c.IsSet("exchange") { + exchange = c.String("exchange") + } else { + exchange = c.Args().First() + } + + if !validExchange(exchange) { + return errInvalidExchange + } + + if c.IsSet("pair") { + pair = c.String("pair") + } else { + pair = c.Args().Get(1) + } + + if !validPair(pair) { + return errInvalidPair + } + + if c.IsSet("asset") { + asset = c.String("asset") + } else { + asset = c.Args().Get(2) + } + + asset = strings.ToLower(asset) + if !validAsset(asset) { + return errInvalidAsset + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + p := currency.NewPairDelimiter(pair, pairDelimiter) + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.EnableExchangePair(context.Background(), + &gctrpc.ExchangePairRequest{ + Exchange: exchange, + Pair: &gctrpc.CurrencyPair{ + Delimiter: p.Delimiter, + Base: p.Base.String(), + Quote: p.Quote.String(), + }, + AssetType: asset, + }, + ) + if err != nil { + return err + } + jsonOutput(result) + return nil +} + +var disableExchangePairCommand = cli.Command{ + Name: "disableexchangepair", + Usage: "disables a previously enabled exchange currency pair", + ArgsUsage: " ", + Action: disableExchangePair, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to disable the currency pair for", + }, + cli.StringFlag{ + Name: "pair", + Usage: "the currency pair to disable", + }, + cli.StringFlag{ + Name: "asset", + Usage: "the asset type to disable the currency pair for", + }, + }, +} + +func disableExchangePair(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "disableexchangepair") + return nil + } + + var exchange string + var pair string + var asset string + + if c.IsSet("exchange") { + exchange = c.String("exchange") + } else { + exchange = c.Args().First() + } + + if !validExchange(exchange) { + return errInvalidExchange + } + + if c.IsSet("pair") { + pair = c.String("pair") + } else { + pair = c.Args().Get(1) + } + + if !validPair(pair) { + return errInvalidPair + } + + if c.IsSet("asset") { + asset = c.String("asset") + } else { + asset = c.Args().Get(2) + } + + asset = strings.ToLower(asset) + if !validAsset(asset) { + return errInvalidAsset + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + p := currency.NewPairDelimiter(pair, pairDelimiter) + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.DisableExchangePair(context.Background(), + &gctrpc.ExchangePairRequest{ + Exchange: exchange, + Pair: &gctrpc.CurrencyPair{ + Delimiter: p.Delimiter, + Base: p.Base.String(), + Quote: p.Quote.String(), + }, + AssetType: asset, + }, + ) + if err != nil { + return err + } + jsonOutput(result) + return nil +} + +var getOrderbookStreamCommand = cli.Command{ + Name: "getorderbookstream", + Usage: "gets the orderbook stream for a specific currency pair and exchange", + ArgsUsage: " ", + Action: getOrderbookStream, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the orderbook from", + }, + cli.StringFlag{ + Name: "pair", + Usage: "currency pair", + }, + cli.StringFlag{ + Name: "asset", + Usage: "the asset type of the currency pair", + }, + }, +} + +func getOrderbookStream(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getorderbookstream") + return nil + } + + var exchangeName string + var pair string + var assetType string + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + if c.IsSet("pair") { + pair = c.String("pair") + } else { + pair = c.Args().Get(1) + } + + if !validPair(pair) { + return errInvalidPair + } + + if c.IsSet("asset") { + assetType = c.String("asset") + } else { + assetType = c.Args().Get(2) + } + + assetType = strings.ToLower(assetType) + + if !validAsset(assetType) { + return errInvalidAsset + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + p := currency.NewPairDelimiter(pair, pairDelimiter) + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetOrderbookStream(context.Background(), + &gctrpc.GetOrderbookStreamRequest{ + Exchange: exchangeName, + Pair: &gctrpc.CurrencyPair{ + Base: p.Base.String(), + Quote: p.Quote.String(), + Delimiter: p.Delimiter, + }, + AssetType: assetType, + }, + ) + + if err != nil { + return err + } + + for { + resp, err := result.Recv() + if err != nil { + return err + } + + err = clearScreen() + if err != nil { + return err + } + + fmt.Printf("Orderbook stream for %s %s:\n\n", exchangeName, + resp.Pair.String()) + fmt.Println("\t\tBids\t\t\t\tAsks") + fmt.Println() + + bidLen := len(resp.Bids) - 1 + askLen := len(resp.Asks) - 1 + + var maxLen int + if bidLen >= askLen { + maxLen = bidLen + } else { + maxLen = askLen + } + + for i := 0; i < maxLen; i++ { + var bidAmount, bidPrice float64 + if i <= bidLen { + bidAmount = resp.Bids[i].Amount + bidPrice = resp.Bids[i].Price + } + + var askAmount, askPrice float64 + if i <= askLen { + askAmount = resp.Asks[i].Amount + askPrice = resp.Asks[i].Price + } + + fmt.Printf("%f %s @ %f %s\t\t%f %s @ %f %s\n", + bidAmount, + resp.Pair.Base, + bidPrice, + resp.Pair.Quote, + askAmount, + resp.Pair.Base, + askPrice, + resp.Pair.Quote) + + if i >= 49 { + // limits orderbook display output + break + } + } + } +} + +var getExchangeOrderbookStreamCommand = cli.Command{ + Name: "getexchangeorderbookstream", + Usage: "gets a stream for all orderbooks associated with an exchange", + ArgsUsage: "", + Action: getExchangeOrderbookStream, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the orderbook from", + }, + }, +} + +func getExchangeOrderbookStream(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getexchangeorderbookstream") + return nil + } + + var exchangeName string + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetExchangeOrderbookStream(context.Background(), + &gctrpc.GetExchangeOrderbookStreamRequest{ + Exchange: exchangeName, + }) + + if err != nil { + return err + } + + for { + resp, err := result.Recv() + if err != nil { + return err + } + + err = clearScreen() + if err != nil { + return err + } + + fmt.Printf("Orderbook streamed for %s %s", + exchangeName, + resp.Pair.String()) + } +} + +var getTickerStreamCommand = cli.Command{ + Name: "gettickerstream", + Usage: "gets the ticker stream for a specific currency pair and exchange", + ArgsUsage: " ", + Action: getTickerStream, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the ticker from", + }, + cli.StringFlag{ + Name: "pair", + Usage: "currency pair", + }, + cli.StringFlag{ + Name: "asset", + Usage: "the asset type of the currency pair", + }, + }, +} + +func getTickerStream(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "gettickerstream") + return nil + } + + var exchangeName string + var pair string + var assetType string + + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + if c.IsSet("pair") { + pair = c.String("pair") + } else { + pair = c.Args().Get(1) + } + + if !validPair(pair) { + return errInvalidPair + } + + if c.IsSet("asset") { + assetType = c.String("asset") + } else { + assetType = c.Args().Get(2) + } + + assetType = strings.ToLower(assetType) + + if !validAsset(assetType) { + return errInvalidAsset + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + p := currency.NewPairDelimiter(pair, pairDelimiter) + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetTickerStream(context.Background(), + &gctrpc.GetTickerStreamRequest{ + Exchange: exchangeName, + Pair: &gctrpc.CurrencyPair{ + Base: p.Base.String(), + Quote: p.Quote.String(), + Delimiter: p.Delimiter, + }, + AssetType: assetType, + }, + ) + + if err != nil { + return err + } + + for { + resp, err := result.Recv() + if err != nil { + return err + } + + err = clearScreen() + if err != nil { + return err + } + + fmt.Printf("Ticker stream for %s %s:\n", exchangeName, + resp.Pair.String()) + fmt.Println() + + fmt.Printf("LAST: %f\n HIGH: %f\n LOW: %f\n BID: %f\n ASK: %f\n VOLUME: %f\n PRICEATH: %f\n LASTUPDATED: %d\n", + resp.Last, + resp.High, + resp.Low, + resp.Bid, + resp.Ask, + resp.Volume, + resp.PriceAth, + resp.LastUpdated) + } +} + +var getExchangeTickerStreamCommand = cli.Command{ + Name: "getexchangetickerstream", + Usage: "gets a stream for all tickers associated with an exchange", + ArgsUsage: "", + Action: getExchangeTickerStream, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "exchange", + Usage: "the exchange to get the ticker from", + }, + }, +} + +func getExchangeTickerStream(c *cli.Context) error { + if c.NArg() == 0 && c.NumFlags() == 0 { + cli.ShowCommandHelp(c, "getexchangetickerstream") + return nil + } + + var exchangeName string + if c.IsSet("exchange") { + exchangeName = c.String("exchange") + } else { + exchangeName = c.Args().First() + } + + if !validExchange(exchangeName) { + return errInvalidExchange + } + + conn, err := setupClient() + if err != nil { + return err + } + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + result, err := client.GetExchangeTickerStream(context.Background(), + &gctrpc.GetExchangeTickerStreamRequest{ + Exchange: exchangeName, + }) + + if err != nil { + return err + } + + for { + resp, err := result.Recv() + if err != nil { + return err + } + + fmt.Printf("Ticker stream for %s %s:\n", + exchangeName, + resp.Pair.String()) + + fmt.Printf("LAST: %f HIGH: %f LOW: %f BID: %f ASK: %f VOLUME: %f PRICEATH: %f LASTUPDATED: %d\n", + resp.Last, + resp.High, + resp.Low, + resp.Bid, + resp.Ask, + resp.Volume, + resp.PriceAth, + resp.LastUpdated) + } +} + +func clearScreen() error { + switch runtime.GOOS { + case "windows": + cmd := exec.Command("cmd", "/c", "cls") + cmd.Stdout = os.Stdout + return cmd.Run() + default: + cmd := exec.Command("clear") + cmd.Stdout = os.Stdout + return cmd.Run() + } +} + +const timeFormat = "2006-01-02 15:04:05" + +var startTime, endTime, order string +var limit int + +var getAuditEventCommand = cli.Command{ + Name: "getauditevent", + Usage: "gets audit events matching query parameters", + ArgsUsage: " ", + Action: getAuditEvent, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "start, s", + Usage: "start date to search", + Value: time.Now().Add(-time.Hour).Format(timeFormat), + Destination: &startTime, + }, + cli.StringFlag{ + Name: "end, e", + Usage: "end time to search", + Value: time.Now().Format(timeFormat), + Destination: &endTime, + }, + cli.StringFlag{ + Name: "order, o", + Usage: "order results by ascending/descending", + Value: "asc", + Destination: &order, + }, + cli.IntFlag{ + Name: "limit, l", + Usage: "how many results to retrieve", + Value: 100, + Destination: &limit, + }, + }, +} + +func getAuditEvent(c *cli.Context) error { + if !c.IsSet("start") { + if c.Args().Get(0) != "" { + startTime = c.Args().Get(0) + } + } + + if !c.IsSet("end") { + if c.Args().Get(1) != "" { + endTime = c.Args().Get(1) + } + } + + if !c.IsSet("order") { + if c.Args().Get(2) != "" { + order = c.Args().Get(2) + } + } + + if !c.IsSet("limit") { + if c.Args().Get(3) != "" { + limitStr, err := strconv.ParseInt(c.Args().Get(3), 10, 32) + if err == nil { + limit = int(limitStr) + } + } + } + + s, err := time.Parse(timeFormat, startTime) + if err != nil { + return fmt.Errorf("invalid time format for start: %v", err) + } + + e, err := time.Parse(timeFormat, endTime) + if err != nil { + return fmt.Errorf("invalid time format for end: %v", err) + } + + if e.Before(s) { + return errors.New("start cannot be after before") + } + + conn, err := setupClient() + if err != nil { + return err + } + + defer conn.Close() + + client := gctrpc.NewGoCryptoTraderClient(conn) + + _, offset := time.Now().Zone() + loc := time.FixedZone("", -offset) + + result, err := client.GetAuditEvent(context.Background(), + &gctrpc.GetAuditEventRequest{ + StartDate: s.In(loc).Format(timeFormat), + EndDate: e.In(loc).Format(timeFormat), + Limit: int32(limit), + OrderBy: order, + Offset: int32(offset), + }) + + if err != nil { + return err + } + + jsonOutput(result) + return nil +} diff --git a/cmd/gctcli/main.go b/cmd/gctcli/main.go new file mode 100644 index 00000000..d16aa5e1 --- /dev/null +++ b/cmd/gctcli/main.go @@ -0,0 +1,142 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "runtime" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/core" + "github.com/thrasher-corp/gocryptotrader/gctrpc/auth" + "github.com/urfave/cli" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +var ( + host string + username string + password string + pairDelimiter string +) + +func jsonOutput(in interface{}) { + j, err := json.MarshalIndent(in, "", " ") + if err != nil { + return + } + fmt.Print(string(j)) +} + +func setupClient() (*grpc.ClientConn, error) { + targetPath := filepath.Join(common.GetDefaultDataDir(runtime.GOOS), "tls", "cert.pem") + creds, err := credentials.NewClientTLSFromFile(targetPath, "") + if err != nil { + return nil, err + } + + opts := []grpc.DialOption{grpc.WithTransportCredentials(creds), + grpc.WithPerRPCCredentials(auth.BasicAuth{ + Username: username, + Password: password, + }), + } + conn, err := grpc.Dial(host, opts...) + if err != nil { + return nil, err + } + + return conn, err +} + +func main() { + app := cli.NewApp() + app.Name = "gctcli" + app.Version = core.Version(true) + app.EnableBashCompletion = true + app.Usage = "command line interface for managing the gocryptotrader daemon" + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "rpchost", + Value: "localhost:9052", + Usage: "the gRPC host to connect to", + Destination: &host, + }, + cli.StringFlag{ + Name: "rpcuser", + Value: "admin", + Usage: "the gRPC username", + Destination: &username, + }, + cli.StringFlag{ + Name: "rpcpassword", + Value: "Password", + Usage: "the gRPC password", + Destination: &password, + }, + cli.StringFlag{ + Name: "delimiter", + Value: "-", + Usage: "the default currency pair delimiter used to standardise currency pair input", + Destination: &pairDelimiter, + }, + } + app.Commands = []cli.Command{ + getInfoCommand, + getSubsystemsCommand, + enableSubsystemCommand, + disableSubsystemCommand, + getRPCEndpointsCommand, + getCommunicationRelayersCommand, + getExchangesCommand, + enableExchangeCommand, + disableExchangeCommand, + getExchangeOTPCommand, + getExchangeOTPsCommand, + getExchangeInfoCommand, + getTickerCommand, + getTickersCommand, + getOrderbookCommand, + getOrderbooksCommand, + getAccountInfoCommand, + getConfigCommand, + getPortfolioCommand, + getPortfolioSummaryCommand, + addPortfolioAddressCommand, + removePortfolioAddressCommand, + getForexProvidersCommand, + getForexRatesCommand, + getOrdersCommand, + getOrderCommand, + submitOrderCommand, + simulateOrderCommand, + whaleBombCommand, + cancelOrderCommand, + cancelAllOrdersCommand, + getEventsCommand, + addEventCommand, + removeEventCommand, + getCryptocurrencyDepositAddressesCommand, + getCryptocurrencyDepositAddressCommand, + withdrawCryptocurrencyFundsCommand, + withdrawFiatFundsCommand, + getLoggerDetailsCommand, + setLoggerDetailsCommand, + getExchangePairsCommand, + enableExchangePairCommand, + disableExchangePairCommand, + getOrderbookStreamCommand, + getExchangeOrderbookStreamCommand, + getTickerStreamCommand, + getExchangeTickerStreamCommand, + getAuditEventCommand, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} diff --git a/cmd/gctcli/validation.go b/cmd/gctcli/validation.go new file mode 100644 index 00000000..fd308b06 --- /dev/null +++ b/cmd/gctcli/validation.go @@ -0,0 +1,27 @@ +package main + +import ( + "errors" + "strings" + + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +var ( + errInvalidPair = errors.New("invalid currency pair supplied") + errInvalidExchange = errors.New("invalid exchange supplied") + errInvalidAsset = errors.New("invalid asset supplied") +) + +func validPair(pair string) bool { + return strings.Contains(pair, pairDelimiter) +} + +func validExchange(exch string) bool { + return exchange.IsSupported(exch) +} + +func validAsset(i string) bool { + return asset.IsValid(asset.Item(i)) +} diff --git a/cmd/gen_cert/main.go b/cmd/gen_cert/main.go new file mode 100644 index 00000000..d8e96a6e --- /dev/null +++ b/cmd/gen_cert/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "log" + "math/big" + "net" + "os" + "time" + + "github.com/thrasher-corp/gocryptotrader/common/file" +) + +func main() { + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + log.Fatalf("failed to generate private key: %s", err) + } + + notBefore := time.Now() + notAfter := notBefore.Add(time.Hour * 24 * 365) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + log.Fatalf("failed to generate serial number: %s", err) + } + + host, err := os.Hostname() + if err != nil { + log.Fatalf("failed to get hostname: %s", err) + } + + dnsNames := []string{host} + if host != "localhost" { + dnsNames = append(dnsNames, "localhost") + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"gocryptotrader"}, + CommonName: host, + }, + NotBefore: notBefore, + NotAfter: notAfter, + IsCA: true, + BasicConstraintsValid: true, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + + IPAddresses: []net.IP{ + net.ParseIP("127.0.0.1"), + net.ParseIP("::1"), + }, + DNSNames: dnsNames, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) + if err != nil { + log.Fatalf("Failed to create certificate: %s", err) + } + + certData := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if certData == nil { + log.Fatalf("cert data is nil") + } + + b, err := x509.MarshalECPrivateKey(privKey) + if err != nil { + log.Printf("failed to marshal ECDSA private key: %s", err) + } + + keyData := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) + if keyData == nil { + log.Fatalf("key pem data is nil") + } + + err = file.Write("key.pem", keyData) + if err != nil { + log.Fatalf("failed to write key.pem file %s", err) + } + log.Printf("wrote key.pem file") + + err = file.Write("cert.pem", certData) + if err != nil { + log.Fatalf("failed to write cert.pem file %s", err) + } + log.Printf("wrote cert.pem file") + + log.Printf("testing tls.LoadX509Keypair..") + _, err = tls.LoadX509KeyPair("cert.pem", "key.pem") + if err != nil { + log.Fatal(err) + } + + log.Printf("ok!") +} diff --git a/cmd/gen_otp/otp_gen.go b/cmd/gen_otp/otp_gen.go new file mode 100644 index 00000000..f48c4fe2 --- /dev/null +++ b/cmd/gen_otp/otp_gen.go @@ -0,0 +1,88 @@ +package main + +import ( + "flag" + "fmt" + "log" + "time" + + "github.com/pquerna/otp/totp" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/core" +) + +const defaultSleepTime = time.Second * 30 + +func containsOTP(cfg *config.Config) bool { + for x := range cfg.Exchanges { + if cfg.Exchanges[x].API.Credentials.OTPSecret != "" { + return true + } + } + return false +} + +func main() { + var cfgFile, code string + var single bool + var err error + + defaultCfg, err := config.GetFilePath("") + if err != nil { + log.Fatal(err) + } + + flag.StringVar(&cfgFile, "config", defaultCfg, "The config input file to process.") + flag.BoolVar(&single, "single", false, "prompt for single use OTP code gen") + flag.Parse() + + log.Println("GoCryptoTrader: OTP code generator tool.") + log.Println(core.Copyright) + + // Handle single use OTP code gen + if single { + var input string + for { + log.Println("Please enter in your OTP secret:") + fmt.Scanln(&input) + if input != "" { + break + } + } + + for { + code, err = totp.GenerateCode(input, time.Now()) + if err != nil { + log.Fatalf("Unable to generate OTP code. Err: %s", err) + } + log.Printf("OTP code: %s\n", code) + time.Sleep(defaultSleepTime) + } + } + + // Otherwise default to loading the config file and generating OTP codes from it + var cfg config.Config + err = cfg.LoadConfig(cfgFile, true) + if err != nil { + log.Fatal(err) + } + log.Println("Loaded config file.") + + if !containsOTP(&cfg) { + log.Fatal("No exchanges with OTP code stored. Exiting.") + } + + for { + for x := range cfg.Exchanges { + if cfg.Exchanges[x].API.Credentials.OTPSecret != "" { + code, err = totp.GenerateCode(cfg.Exchanges[x].API.Credentials.OTPSecret, time.Now()) + if err != nil { + log.Printf("Exchange %s: Failed to generate OTP code. Err: %s\n", cfg.Exchanges[x].Name, err) + continue + } + log.Printf("%s: %s\n", cfg.Exchanges[x].Name, code) + } + } + time.Sleep(defaultSleepTime) + } +} diff --git a/cmd/gen_sqlboiler_config/main.go b/cmd/gen_sqlboiler_config/main.go new file mode 100644 index 00000000..9d739a2c --- /dev/null +++ b/cmd/gen_sqlboiler_config/main.go @@ -0,0 +1,107 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/core" + "github.com/thrasher-corp/gocryptotrader/database" + "github.com/thrasher-corp/gocryptotrader/database/repository" +) + +var ( + configFile string + defaultDataDir string + outputFolder string +) + +var sqlboilerConfig map[string]driverConfig + +type driverConfig struct { + DBName string `json:"dbname,omitempty"` + Host string `json:"host,omitempty"` + Port uint16 `json:"port,omitempty"` + User string `json:"user,omitempty"` + Pass string `json:"pass,omitempty"` + Schema string `json:"schema,omitempty"` + SSLMode string `json:"sslmode,omitempty"` + Blacklist []string `json:"blacklist,omitempty"` +} + +func main() { + fmt.Println("GoCryptoTrader SQLBoiler config generation tool") + fmt.Println(core.Copyright) + fmt.Println() + + defaultPath, err := config.GetFilePath("") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + flag.StringVar(&configFile, "config", defaultPath, "config file to load") + flag.StringVar(&defaultDataDir, "datadir", common.GetDefaultDataDir(runtime.GOOS), "default data directory for GoCryptoTrader files") + flag.StringVar(&outputFolder, "outdir", "", "overwrite default output folder") + flag.Parse() + + conf := config.GetConfig() + + err = conf.LoadConfig(configFile, true) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + convertGCTtoSQLBoilerConfig(&conf.Database) + + jsonOutput, err := json.MarshalIndent(sqlboilerConfig, "", " ") + if err != nil { + fmt.Printf("Marshal failed: %v", err) + os.Exit(1) + } + + path := filepath.Join(outputFolder, "sqlboiler.json") + err = ioutil.WriteFile(path, jsonOutput, 0770) + if err != nil { + fmt.Printf("Write failed: %v", err) + os.Exit(1) + } + fmt.Println("sqlboiler.json file created") +} + +func convertGCTtoSQLBoilerConfig(c *database.Config) { + tempConfig := driverConfig{ + Blacklist: []string{"goose_db_version"}, + } + + sqlboilerConfig = make(map[string]driverConfig) + + dbType := repository.GetSQLDialect() + + if dbType == database.DBPostgreSQL { + dbType = "psql" + } + if dbType == database.DBSQLite || dbType == database.DBSQLite3 { + tempConfig.DBName = convertDBName(c.Database) + } else { + tempConfig.User = c.Username + tempConfig.Pass = c.Password + tempConfig.Port = c.Port + tempConfig.Host = c.Host + tempConfig.DBName = c.Database + tempConfig.SSLMode = c.SSLMode + } + + sqlboilerConfig[dbType] = tempConfig +} + +func convertDBName(in string) string { + return filepath.Join(common.GetDefaultDataDir(runtime.GOOS), "/database", in) +} diff --git a/tools/huobi_auth/main.go b/cmd/huobi_auth/main.go similarity index 81% rename from tools/huobi_auth/main.go rename to cmd/huobi_auth/main.go index 319c99d2..2b2a9572 100644 --- a/tools/huobi_auth/main.go +++ b/cmd/huobi_auth/main.go @@ -6,12 +6,14 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/x509" + "encoding/json" "encoding/pem" "errors" "fmt" + "io/ioutil" "log" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/file" ) func encodePEM(privKey *ecdsa.PrivateKey, pubKey bool) ([]byte, error) { @@ -52,13 +54,13 @@ func decodePEM(pemPrivKey []byte) (*ecdsa.PrivateKey, error) { return x509.ParseECPrivateKey(x509Enc) } -func writeFile(file string, data []byte) error { - return common.WriteFile(file, data) +func writeFile(fileName string, data []byte) error { + return file.Write(fileName, data) } func main() { genKeys := false - privKeyData, err := common.ReadFile("privatekey.pem") + privKeyData, err := ioutil.ReadFile("privatekey.pem") if err != nil { genKeys = true } @@ -98,7 +100,7 @@ func main() { } } else { var pubKeyData []byte - pubKeyData, err = common.ReadFile("publickey.pem") + pubKeyData, err = ioutil.ReadFile("publickey.pem") if err != nil { log.Fatal(err) } @@ -128,19 +130,21 @@ func main() { fmt.Println(string(pubKey)) type JSONGeneration struct { - APIAuthPEMKey string + PEMKey string + PEMKeySupport bool } r := JSONGeneration{ - APIAuthPEMKey: string(privKey), + PEMKey: string(privKey), + PEMKeySupport: true, } - resultk, err := common.JSONEncode(r) + resultk, err := json.MarshalIndent(r, "", " ") if err != nil { log.Fatal(err) } log.Println("Please visit https://github.com/huobiapi/API_Docs_en/wiki/Signing_API_Requests and follow from step 2 onwards.") - log.Printf("After completing the above instructions, please copy and paste the below key (including the following ',') into your Huobi exchange config file:\n\n") + log.Printf("After completing the above instructions, please copy and paste the below key in the API section (including the following ',') into your Huobi exchange config file:\n\n") fmt.Println(string(resultk[1:len(resultk)-1]) + ",") } diff --git a/tools/portfolio/portfolio.go b/cmd/portfolio/portfolio.go similarity index 91% rename from tools/portfolio/portfolio.go rename to cmd/portfolio/portfolio.go index c40d70ca..d9594674 100644 --- a/tools/portfolio/portfolio.go +++ b/cmd/portfolio/portfolio.go @@ -3,11 +3,13 @@ package main import ( "flag" "fmt" + "log" + "os" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex" - log "github.com/thrasher-corp/gocryptotrader/logger" "github.com/thrasher-corp/gocryptotrader/portfolio" ) @@ -25,7 +27,7 @@ func printSummary(msg string, amount float64) { currency.USD, displayCurrency) if err != nil { - log.Error(err) + log.Println(err) } else { symb, err := currency.GetSymbolByCurrencyName(displayCurrency) if err != nil { @@ -64,7 +66,8 @@ func main() { defaultCfg, err := config.GetFilePath("") if err != nil { - log.Fatal(err) + log.Println(err) + os.Exit(1) } flag.StringVar(&inFile, "infile", defaultCfg, "The config input file to process.") @@ -74,15 +77,16 @@ func main() { log.Println("GoCryptoTrader: portfolio tool.") var cfg config.Config - err = cfg.LoadConfig(inFile) + err = cfg.LoadConfig(inFile, true) if err != nil { - log.Fatal(err) + log.Println(err) + os.Exit(1) } log.Println("Loaded config file.") - displayCurrency = cfg.FiatDisplayCurrency + displayCurrency = cfg.Currency.FiatDisplayCurrency port := portfolio.Base{} - port.SeedPortfolio(cfg.Portfolio) + port.Seed(cfg.Portfolio) result := port.GetPortfolioSummary() log.Println("Fetched portfolio data.") @@ -92,7 +96,7 @@ func main() { Subtotal float64 } - cfg.RetrieveConfigCurrencyPairs(true) + cfg.RetrieveConfigCurrencyPairs(true, asset.Spot) portfolioMap := make(map[currency.Code]PortfolioTemp) total := float64(0) @@ -105,7 +109,8 @@ func main() { } err = currency.SeedForeignExchangeData(fiatCurrencies) if err != nil { - log.Fatal(err) + log.Println(err) + os.Exit(1) } log.Println("Fetched currency data.") @@ -132,6 +137,8 @@ func main() { } } else { bf := bitfinex.Bitfinex{} + bf.SetDefaults() + bf.Verbose = false ticker, errf := bf.GetTicker(y.Coin.String() + currency.USD.String()) if errf != nil { log.Println(errf) diff --git a/tools/portfolio/portfolio_test.go b/cmd/portfolio/portfolio_test.go similarity index 100% rename from tools/portfolio/portfolio_test.go rename to cmd/portfolio/portfolio_test.go diff --git a/tools/websocket_client/main.go b/cmd/websocket_client/main.go similarity index 86% rename from tools/websocket_client/main.go rename to cmd/websocket_client/main.go index bf306df4..858dbc86 100644 --- a/tools/websocket_client/main.go +++ b/cmd/websocket_client/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "errors" "fmt" "log" @@ -8,7 +9,9 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) // Vars for the websocket client @@ -40,9 +43,9 @@ type WebsocketEventResponse struct { // WebsocketOrderbookTickerRequest is a struct used for ticker and orderbook // requests type WebsocketOrderbookTickerRequest struct { - Exchange string `json:"exchangeName"` - Currency string `json:"currency"` - AssetType string `json:"assetType"` + Exchange string `json:"exchangeName"` + Currency string `json:"currency"` + AssetType asset.Item `json:"assetType"` } // SendWebsocketEvent sends a websocket event message @@ -74,12 +77,12 @@ func SendWebsocketEvent(event string, reqData interface{}, result *WebsocketEven func main() { cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigFile) + err := cfg.LoadConfig(config.File, true) if err != nil { log.Fatalf("Failed to load config file: %s", err) } - listenAddr := cfg.Webserver.ListenAddress + listenAddr := cfg.RemoteControl.WebsocketRPC.ListenAddress wsHost := fmt.Sprintf("ws://%s:%d/ws", common.ExtractHost(listenAddr), common.ExtractPort(listenAddr)) log.Printf("Connecting to websocket host: %s", wsHost) @@ -95,8 +98,8 @@ func main() { log.Println("Authenticating..") var wsResp WebsocketEventResponse reqData := WebsocketAuth{ - Username: cfg.Webserver.AdminUsername, - Password: common.HexEncodeToString(common.GetSHA256([]byte(cfg.Webserver.AdminPassword))), + Username: cfg.RemoteControl.Username, + Password: crypto.HexEncodeToString(crypto.GetSHA256([]byte(cfg.RemoteControl.Password))), } err = SendWebsocketEvent("auth", reqData, &wsResp) if err != nil { @@ -111,13 +114,13 @@ func main() { } log.Printf("Fetched config.") - dataJSON, err := common.JSONEncode(&wsResp.Data) + dataJSON, err := json.Marshal(&wsResp.Data) if err != nil { log.Fatal(err) } var resultCfg config.Config - err = common.JSONDecode(dataJSON, &resultCfg) + err = json.Unmarshal(dataJSON, &resultCfg) if err != nil { log.Fatal(err) } @@ -155,7 +158,7 @@ func main() { dataReq := WebsocketOrderbookTickerRequest{ Exchange: "Bitfinex", Currency: "BTCUSD", - AssetType: "SPOT", + AssetType: asset.Spot, } err = SendWebsocketEvent("GetTicker", dataReq, &wsResp) diff --git a/codelingo.yaml b/codelingo.yaml index 4e839223..03c1f23e 100644 --- a/codelingo.yaml +++ b/codelingo.yaml @@ -8,7 +8,7 @@ tenets: - import: codelingo/effective-go/unnecessary-else - import: codelingo/code-review-comments/declare-empty-slice - import: codelingo/effective-go/defer-close-file - - import: codelingo/effective-go/comment-first-word-when-empty + # - import: codelingo/effective-go/comment-first-word-when-empty # this has been disabled temporarily - name: missing-stop-ticker actions: codelingo/review: diff --git a/common/README.md b/common/README.md index a8e2085a..d7c634d0 100644 --- a/common/README.md +++ b/common/README.md @@ -29,7 +29,7 @@ import "github.com/thrasher-corp/gocryptotrader/common" testString := "aAaAa" -upper := common.StringToUpper(testString) +upper := strings.ToUpper(testString) // upper == "AAAAA" ``` @@ -55,4 +55,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/common/common.go b/common/common.go index 4d56a13d..ecccac19 100644 --- a/common/common.go +++ b/common/common.go @@ -1,30 +1,18 @@ package common import ( - "crypto/hmac" - "crypto/md5" // nolint:gosec - "crypto/rand" - "crypto/sha1" // nolint:gosec - "crypto/sha256" - "crypto/sha512" - "encoding/base64" "encoding/csv" - "encoding/hex" "encoding/json" "errors" "fmt" - "hash" "io" "io/ioutil" - "math" "net/http" "net/url" "os" "os/user" "path/filepath" - "reflect" "regexp" - "runtime" "strconv" "strings" "time" @@ -34,7 +22,8 @@ import ( // Vars for common.go operations var ( - HTTPClient *http.Client + HTTPClient *http.Client + HTTPUserAgent string // ErrNotYetImplemented defines a common error across the code base that // alerts of a function that has not been completed or tied into main code @@ -47,11 +36,6 @@ var ( // Const declarations for common.go operations const ( - HashSHA1 = iota - HashSHA256 - HashSHA512 - HashSHA512_384 - HashMD5 SatoshisPerBTC = 100000000 SatoshisPerLTC = 100000000 WeiPerEther = 1000000000000000000 @@ -71,95 +55,6 @@ func NewHTTPClientWithTimeout(t time.Duration) *http.Client { return h } -// GetRandomSalt returns a random salt -func GetRandomSalt(input []byte, saltLen int) ([]byte, error) { - if saltLen <= 0 { - return nil, errors.New("salt length is too small") - } - salt := make([]byte, saltLen) - if _, err := io.ReadFull(rand.Reader, salt); err != nil { - return nil, err - } - - var result []byte - if input != nil { - result = input - } - result = append(result, salt...) - return result, nil -} - -// GetMD5 returns a MD5 hash of a byte array -func GetMD5(input []byte) []byte { - m := md5.New() // nolint:gosec - m.Write(input) - return m.Sum(nil) -} - -// GetSHA512 returns a SHA512 hash of a byte array -func GetSHA512(input []byte) []byte { - sha := sha512.New() - sha.Write(input) - return sha.Sum(nil) -} - -// GetSHA256 returns a SHA256 hash of a byte array -func GetSHA256(input []byte) []byte { - sha := sha256.New() - sha.Write(input) - return sha.Sum(nil) -} - -// GetHMAC returns a keyed-hash message authentication code using the desired -// hashtype -func GetHMAC(hashType int, input, key []byte) []byte { - var hasher func() hash.Hash - - switch hashType { - case HashSHA1: - hasher = sha1.New - case HashSHA256: - hasher = sha256.New - case HashSHA512: - hasher = sha512.New - case HashSHA512_384: - hasher = sha512.New384 - case HashMD5: - hasher = md5.New - } - - h := hmac.New(hasher, key) - h.Write(input) - return h.Sum(nil) -} - -// Sha1ToHex takes a string, sha1 hashes it and return a hex string of the -// result -func Sha1ToHex(data string) string { - h := sha1.New() // nolint:gosec - h.Write([]byte(data)) - return hex.EncodeToString(h.Sum(nil)) -} - -// HexEncodeToString takes in a hexadecimal byte array and returns a string -func HexEncodeToString(input []byte) string { - return hex.EncodeToString(input) -} - -// Base64Decode takes in a Base64 string and returns a byte array and an error -func Base64Decode(input string) ([]byte, error) { - result, err := base64.StdEncoding.DecodeString(input) - if err != nil { - return nil, err - } - return result, nil -} - -// Base64Encode takes in a byte array then returns an encoded base64 string -func Base64Encode(input []byte) string { - return base64.StdEncoding.EncodeToString(input) -} - // StringSliceDifference concatenates slices together based on its index and // returns an individual string array func StringSliceDifference(slice1, slice2 []string) []string { @@ -184,12 +79,6 @@ func StringSliceDifference(slice1, slice2 []string) []string { return diff } -// StringContains checks a substring if it contains your input then returns a -// bool -func StringContains(input, substring string) bool { - return strings.Contains(input, substring) -} - // StringDataContains checks the substring array with an input and returns a bool func StringDataContains(haystack []string, needle string) bool { data := strings.Join(haystack, ",") @@ -221,66 +110,13 @@ func StringDataCompareInsensitive(haystack []string, needle string) bool { // a bool irrespective of lower or upper case strings func StringDataContainsInsensitive(haystack []string, needle string) bool { for _, data := range haystack { - if strings.Contains(StringToUpper(data), StringToUpper(needle)) { + if strings.Contains(strings.ToUpper(data), strings.ToUpper(needle)) { return true } } return false } -// JoinStrings joins an array together with the required separator and returns -// it as a string -func JoinStrings(input []string, separator string) string { - return strings.Join(input, separator) -} - -// SplitStrings splits blocks of strings from string into a string array using -// a separator ie "," or "_" -func SplitStrings(input, separator string) []string { - return strings.Split(input, separator) -} - -// TrimString trims unwanted prefixes or postfixes -func TrimString(input, cutset string) string { - return strings.Trim(input, cutset) -} - -// ReplaceString replaces a string with another -func ReplaceString(input, old, newStr string, n int) string { - return strings.Replace(input, old, newStr, n) -} - -// StringToUpper changes strings to uppercase -func StringToUpper(input string) string { - return strings.ToUpper(input) -} - -// StringToLower changes strings to lowercase -func StringToLower(input string) string { - return strings.ToLower(input) -} - -// RoundFloat rounds your floating point number to the desired decimal place -func RoundFloat(x float64, prec int) float64 { - var rounder float64 - pow := math.Pow(10, float64(prec)) - intermed := x * pow - _, frac := math.Modf(intermed) - intermed += .5 - x = .5 - if frac < 0.0 { - x = -.5 - intermed-- - } - if frac >= x { - rounder = math.Ceil(intermed) - } else { - rounder = math.Floor(intermed) - } - - return rounder / pow -} - // IsEnabled takes in a boolean param and returns a string if it is enabled // or disabled func IsEnabled(isEnabled bool) string { @@ -294,7 +130,7 @@ func IsEnabled(isEnabled bool) string { // regexp package // Validation issues occurring because "3" is contained in // litecoin and Bitcoin addresses - non-fatal func IsValidCryptoAddress(address, crypto string) (bool, error) { - switch StringToLower(crypto) { + switch strings.ToLower(crypto) { case "btc": return regexp.MatchString("^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$", address) case "ltc": @@ -308,39 +144,12 @@ func IsValidCryptoAddress(address, crypto string) (bool, error) { // YesOrNo returns a boolean variable to check if input is "y" or "yes" func YesOrNo(input string) bool { - if StringToLower(input) == "y" || StringToLower(input) == "yes" { + if strings.EqualFold(input, "y") || strings.EqualFold(input, "yes") { return true } return false } -// CalculateAmountWithFee returns a calculated fee included amount on fee -func CalculateAmountWithFee(amount, fee float64) float64 { - return amount + CalculateFee(amount, fee) -} - -// CalculateFee returns a simple fee on amount -func CalculateFee(amount, fee float64) float64 { - return amount * (fee / 100) -} - -// CalculatePercentageGainOrLoss returns the percentage rise over a certain -// period -func CalculatePercentageGainOrLoss(priceNow, priceThen float64) float64 { - return (priceNow - priceThen) / priceThen * 100 -} - -// CalculatePercentageDifference returns the percentage of difference between -// multiple time periods -func CalculatePercentageDifference(amount, secondAmount float64) float64 { - return (amount - secondAmount) / ((amount + secondAmount) / 2) * 100 -} - -// CalculateNetProfit returns net profit -func CalculateNetProfit(amount, priceThen, priceNow, costs float64) float64 { - return (priceNow * amount) - (priceThen * amount) - costs -} - // SendHTTPRequest sends a request using the http package and returns a response // as a string and an error func SendHTTPRequest(method, urlPath string, headers map[string]string, body io.Reader) (string, error) { @@ -361,6 +170,10 @@ func SendHTTPRequest(method, urlPath string, headers map[string]string, body io. req.Header.Add(k, v) } + if HTTPUserAgent != "" && req.Header.Get("User-Agent") == "" { + req.Header.Add("User-Agent", HTTPUserAgent) + } + resp, err := HTTPClient.Do(req) if err != nil { return "", err @@ -381,7 +194,7 @@ func SendHTTPRequest(method, urlPath string, headers map[string]string, body io. // on failure. func SendHTTPGetRequest(urlPath string, jsonDecode, isVerbose bool, result interface{}) error { if isVerbose { - log.Debugf("Raw URL: %s", urlPath) + log.Debugf(log.Global, "Raw URL: %s\n", urlPath) } initialiseHTTPClient() @@ -401,13 +214,13 @@ func SendHTTPGetRequest(urlPath string, jsonDecode, isVerbose bool, result inter } if isVerbose { - log.Debugf("Raw Resp: %s", string(contents)) + log.Debugf(log.Global, "Raw Resp: %s\n", string(contents)) } defer res.Body.Close() if jsonDecode { - err := JSONDecode(contents, result) + err := json.Unmarshal(contents, result) if err != nil { return err } @@ -416,19 +229,6 @@ func SendHTTPGetRequest(urlPath string, jsonDecode, isVerbose bool, result inter return nil } -// JSONEncode encodes structure data into JSON -func JSONEncode(v interface{}) ([]byte, error) { - return json.Marshal(v) -} - -// JSONDecode decodes JSON data into a structure -func JSONDecode(data []byte, to interface{}) error { - if !StringContains(reflect.ValueOf(to).Type().String(), "*") { - return errors.New("json decode error - memory address not supplied") - } - return json.Unmarshal(data, to) -} - // EncodeURLValues concatenates url values onto a url string and returns a // string func EncodeURLValues(urlPath string, values url.Values) string { @@ -441,7 +241,7 @@ func EncodeURLValues(urlPath string, values url.Values) string { // ExtractHost returns the hostname out of a string func ExtractHost(address string) string { - host := SplitStrings(address, ":")[0] + host := strings.Split(address, ":")[0] if host == "" { return "localhost" } @@ -450,16 +250,16 @@ func ExtractHost(address string) string { // ExtractPort returns the port name out of a string func ExtractPort(host string) int { - portStr := SplitStrings(host, ":")[1] + portStr := strings.Split(host, ":")[1] port, _ := strconv.Atoi(portStr) return port } // OutputCSV dumps data into a file as comma-separated values func OutputCSV(filePath string, data [][]string) error { - _, err := ReadFile(filePath) + _, err := ioutil.ReadFile(filePath) if err != nil { - errTwo := WriteFile(filePath, nil) + errTwo := ioutil.WriteFile(filePath, nil, 0770) if errTwo != nil { return errTwo } @@ -469,47 +269,10 @@ func OutputCSV(filePath string, data [][]string) error { if err != nil { return err } + defer file.Close() writer := csv.NewWriter(file) - - err = writer.WriteAll(data) - if err != nil { - return err - } - - writer.Flush() - file.Close() - return nil -} - -// UnixTimestampToTime returns time.time -func UnixTimestampToTime(timeint64 int64) time.Time { - return time.Unix(timeint64, 0) -} - -// UnixTimestampStrToTime returns a time.time and an error -func UnixTimestampStrToTime(timeStr string) (time.Time, error) { - i, err := strconv.ParseInt(timeStr, 10, 64) - if err != nil { - return time.Time{}, err - } - - return time.Unix(i, 0), nil -} - -// ReadFile reads a file and returns read data as byte array. -func ReadFile(file string) ([]byte, error) { - return ioutil.ReadFile(file) -} - -// WriteFile writes selected data to a file and returns an error -func WriteFile(file string, data []byte) error { - return ioutil.WriteFile(file, data, 0644) -} - -// RemoveFile removes a file -func RemoveFile(file string) error { - return os.Remove(file) + return writer.WriteAll(data) } // GetURIPath returns the path of a URL given a URI @@ -519,7 +282,7 @@ func GetURIPath(uri string) string { return "" } if urip.RawQuery != "" { - return fmt.Sprintf("%s?%s", urip.Path, urip.RawQuery) + return urip.Path + "?" + urip.RawQuery } return urip.Path } @@ -533,73 +296,6 @@ func GetExecutablePath() (string, error) { return filepath.Dir(ex), nil } -// GetOSPathSlash returns the slash used by the operating systems -// file system -func GetOSPathSlash() string { - if runtime.GOOS == "windows" { - return "\\" - } - return "/" -} - -// UnixMillis converts a UnixNano timestamp to milliseconds -func UnixMillis(t time.Time) int64 { - return t.UnixNano() / int64(time.Millisecond) -} - -// RecvWindow converts a supplied time.Duration to milliseconds -func RecvWindow(d time.Duration) int64 { - return int64(d) / int64(time.Millisecond) -} - -// FloatFromString format -func FloatFromString(raw interface{}) (float64, error) { - str, ok := raw.(string) - if !ok { - return 0, fmt.Errorf("unable to parse, value not string: %T", raw) - } - flt, err := strconv.ParseFloat(str, 64) - if err != nil { - return 0, fmt.Errorf("could not convert value: %s Error: %s", str, err) - } - return flt, nil -} - -// IntFromString format -func IntFromString(raw interface{}) (int, error) { - str, ok := raw.(string) - if !ok { - return 0, fmt.Errorf("unable to parse, value not string: %T", raw) - } - n, err := strconv.Atoi(str) - if err != nil { - return 0, fmt.Errorf("unable to parse as int: %T", raw) - } - return n, nil -} - -// Int64FromString format -func Int64FromString(raw interface{}) (int64, error) { - str, ok := raw.(string) - if !ok { - return 0, fmt.Errorf("unable to parse, value not string: %T", raw) - } - n, err := strconv.ParseInt(str, 10, 64) - if err != nil { - return 0, fmt.Errorf("unable to parse as int64: %T", raw) - } - return n, nil -} - -// TimeFromUnixTimestampFloat format -func TimeFromUnixTimestampFloat(raw interface{}) (time.Time, error) { - ts, ok := raw.(float64) - if !ok { - return time.Time{}, fmt.Errorf("unable to parse, value not float64: %T", raw) - } - return time.Unix(0, int64(ts)*int64(time.Millisecond)), nil -} - // GetDefaultDataDir returns the default data directory // Windows - C:\Users\%USER%\AppData\Roaming\GoCryptoTrader // Linux/Unix or OSX - $HOME/.gocryptotrader @@ -615,7 +311,7 @@ func GetDefaultDataDir(env string) string { dir, err := os.UserHomeDir() if err != nil { - log.Warn("Environment variable unset, defaulting to current directory") + log.Warnln(log.Global, "Environment variable unset, defaulting to current directory") dir = "." } return filepath.Join(dir, ".gocryptotrader") @@ -628,12 +324,12 @@ func CreateDir(dir string) error { return nil } - log.Warnf("Directory %s does not exist.. creating.", dir) + log.Warnf(log.Global, "Directory %s does not exist.. creating.\n", dir) return os.MkdirAll(dir, 0770) } -// ChangePerm lists all the directories and files in an array -func ChangePerm(directory string) error { +// ChangePermission lists all the directories and files in an array +func ChangePermission(directory string) error { return filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { if err != nil { return err diff --git a/common/common_test.go b/common/common_test.go index ae049892..7f892fbf 100644 --- a/common/common_test.go +++ b/common/common_test.go @@ -1,7 +1,6 @@ package common import ( - "bytes" "net/url" "os" "os/user" @@ -10,7 +9,6 @@ import ( "runtime" "strings" "testing" - "time" ) func TestIsEnabled(t *testing.T) { @@ -18,253 +16,58 @@ func TestIsEnabled(t *testing.T) { expected := "Enabled" actual := IsEnabled(true) if actual != expected { - t.Errorf("Test failed. Expected %s. Actual %s", expected, actual) + t.Errorf("Expected %s. Actual %s", expected, actual) } expected = "Disabled" actual = IsEnabled(false) if actual != expected { - t.Errorf("Test failed. Expected %s. Actual %s", expected, actual) + t.Errorf("Expected %s. Actual %s", expected, actual) } } func TestIsValidCryptoAddress(t *testing.T) { t.Parallel() - b, err := IsValidCryptoAddress("1Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "bTC") if err != nil && !b { - t.Errorf("Test Failed - Common IsValidCryptoAddress error: %s", err) + t.Errorf("Common IsValidCryptoAddress error: %s", err) } b, err = IsValidCryptoAddress("0Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "btc") if err == nil && b { - t.Error("Test Failed - Common IsValidCryptoAddress error") + t.Error("Common IsValidCryptoAddress error") } b, err = IsValidCryptoAddress("1Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "lTc") if err == nil && b { - t.Error("Test Failed - Common IsValidCryptoAddress error") + t.Error("Common IsValidCryptoAddress error") } b, err = IsValidCryptoAddress("3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj", "ltc") if err != nil && !b { - t.Errorf("Test Failed - Common IsValidCryptoAddress error: %s", err) + t.Errorf("Common IsValidCryptoAddress error: %s", err) } b, err = IsValidCryptoAddress("NCDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj", "lTc") if err == nil && b { - t.Error("Test Failed - Common IsValidCryptoAddress error") + t.Error("Common IsValidCryptoAddress error") } b, err = IsValidCryptoAddress( "0xb794f5ea0ba39494ce839613fffba74279579268", "eth", ) if err != nil && b { - t.Errorf("Test Failed - Common IsValidCryptoAddress error: %s", err) + t.Errorf("Common IsValidCryptoAddress error: %s", err) } b, err = IsValidCryptoAddress( "xxb794f5ea0ba39494ce839613fffba74279579268", "eTh", ) if err == nil && b { - t.Error("Test Failed - Common IsValidCryptoAddress error") + t.Error("Common IsValidCryptoAddress error") } b, err = IsValidCryptoAddress( "xxb794f5ea0ba39494ce839613fffba74279579268", "ding", ) if err == nil && b { - t.Error("Test Failed - Common IsValidCryptoAddress error") - } -} - -func TestGetRandomSalt(t *testing.T) { - t.Parallel() - - _, err := GetRandomSalt(nil, -1) - if err == nil { - t.Fatal("Test failed. Expected err on negative salt length") - } - - salt, err := GetRandomSalt(nil, 10) - if err != nil { - t.Fatal(err) - } - - if len(salt) != 10 { - t.Fatal("Test failed. Expected salt of len=10") - } - - salt, err = GetRandomSalt([]byte("RAWR"), 12) - if err != nil { - t.Fatal(err) - } - - if len(salt) != 16 { - t.Fatal("Test failed. Expected salt of len=16") - } -} - -func TestGetMD5(t *testing.T) { - t.Parallel() - var originalString = []byte("I am testing the MD5 function in common!") - var expectedOutput = []byte("18fddf4a41ba90a7352765e62e7a8744") - actualOutput := GetMD5(originalString) - actualStr := HexEncodeToString(actualOutput) - if !bytes.Equal(expectedOutput, []byte(actualStr)) { - t.Errorf("Test failed. Expected '%s'. Actual '%s'", - expectedOutput, []byte(actualStr)) - } -} - -func TestGetSHA512(t *testing.T) { - t.Parallel() - var originalString = []byte("I am testing the GetSHA512 function in common!") - var expectedOutput = []byte( - `a2273f492ea73fddc4f25c267b34b3b74998bd8a6301149e1e1c835678e3c0b90859fce22e4e7af33bde1711cbb924809aedf5d759d648d61774b7185c5dc02b`, - ) - actualOutput := GetSHA512(originalString) - actualStr := HexEncodeToString(actualOutput) - if !bytes.Equal(expectedOutput, []byte(actualStr)) { - t.Errorf("Test failed. Expected '%x'. Actual '%x'", - expectedOutput, []byte(actualStr)) - } -} - -func TestGetSHA256(t *testing.T) { - t.Parallel() - var originalString = []byte("I am testing the GetSHA256 function in common!") - var expectedOutput = []byte( - "0962813d7a9f739cdcb7f0c0be0c2a13bd630167e6e54468266e4af6b1ad9303", - ) - actualOutput := GetSHA256(originalString) - actualStr := HexEncodeToString(actualOutput) - if !bytes.Equal(expectedOutput, []byte(actualStr)) { - t.Errorf("Test failed. Expected '%x'. Actual '%x'", expectedOutput, - []byte(actualStr)) - } -} - -func TestGetHMAC(t *testing.T) { - t.Parallel() - expectedSha1 := []byte{ - 74, 253, 245, 154, 87, 168, 110, 182, 172, 101, 177, 49, 142, 2, 253, 165, - 100, 66, 86, 246, - } - expectedsha256 := []byte{ - 54, 68, 6, 12, 32, 158, 80, 22, 142, 8, 131, 111, 248, 145, 17, 202, 224, - 59, 135, 206, 11, 170, 154, 197, 183, 28, 150, 79, 168, 105, 62, 102, - } - expectedsha512 := []byte{ - 249, 212, 31, 38, 23, 3, 93, 220, 81, 209, 214, 112, 92, 75, 126, 40, 109, - 95, 247, 182, 210, 54, 217, 224, 199, 252, 129, 226, 97, 201, 245, 220, 37, - 201, 240, 15, 137, 236, 75, 6, 97, 12, 190, 31, 53, 153, 223, 17, 214, 11, - 153, 203, 49, 29, 158, 217, 204, 93, 179, 109, 140, 216, 202, 71, - } - expectedsha512384 := []byte{ - 121, 203, 109, 105, 178, 68, 179, 57, 21, 217, 76, 82, 94, 100, 210, 1, 55, - 201, 8, 232, 194, 168, 165, 58, 192, 26, 193, 167, 254, 183, 172, 4, 189, - 158, 158, 150, 173, 33, 119, 125, 94, 13, 125, 89, 241, 184, 166, 128, - } - expectedmd5 := []byte{ - 113, 64, 132, 129, 213, 68, 231, 99, 252, 15, 175, 109, 198, 132, 139, 39, - } - - sha1 := GetHMAC(HashSHA1, []byte("Hello,World"), []byte("1234")) - if string(sha1) != string(expectedSha1) { - t.Errorf("Test failed. Common GetHMAC error: Expected '%x'. Actual '%x'", - expectedSha1, sha1, - ) - } - sha256 := GetHMAC(HashSHA256, []byte("Hello,World"), []byte("1234")) - if string(sha256) != string(expectedsha256) { - t.Errorf("Test failed. Common GetHMAC error: Expected '%x'. Actual '%x'", - expectedsha256, sha256, - ) - } - sha512 := GetHMAC(HashSHA512, []byte("Hello,World"), []byte("1234")) - if string(sha512) != string(expectedsha512) { - t.Errorf("Test failed. Common GetHMAC error: Expected '%x'. Actual '%x'", - expectedsha512, sha512, - ) - } - sha512384 := GetHMAC(HashSHA512_384, []byte("Hello,World"), []byte("1234")) - if string(sha512384) != string(expectedsha512384) { - t.Errorf("Test failed. Common GetHMAC error: Expected '%x'. Actual '%x'", - expectedsha512384, sha512384, - ) - } - md5 := GetHMAC(HashMD5, []byte("Hello World"), []byte("1234")) - if string(md5) != string(expectedmd5) { - t.Errorf("Test failed. Common GetHMAC error: Expected '%x'. Actual '%x'", - expectedmd5, md5, - ) - } -} - -func TestSha1Tohex(t *testing.T) { - t.Parallel() - expectedResult := "fcfbfcd7d31d994ef660f6972399ab5d7a890149" - actualResult := Sha1ToHex("Testing Sha1ToHex") - if actualResult != expectedResult { - t.Errorf("Test failed. Expected '%s'. Actual '%s'", - expectedResult, actualResult) - } -} - -func TestStringToLower(t *testing.T) { - t.Parallel() - upperCaseString := "HEY MAN" - expectedResult := "hey man" - actualResult := StringToLower(upperCaseString) - if actualResult != expectedResult { - t.Errorf("Test failed. Expected '%s'. Actual '%s'", - expectedResult, actualResult) - } -} - -func TestStringToUpper(t *testing.T) { - t.Parallel() - upperCaseString := "hey man" - expectedResult := "HEY MAN" - actualResult := StringToUpper(upperCaseString) - if actualResult != expectedResult { - t.Errorf("Test failed. Expected '%s'. Actual '%s'", - expectedResult, actualResult) - } -} - -func TestHexEncodeToString(t *testing.T) { - t.Parallel() - originalInput := []byte("string") - expectedOutput := "737472696e67" - actualResult := HexEncodeToString(originalInput) - if actualResult != expectedOutput { - t.Errorf("Test failed. Expected '%s'. Actual '%s'", - expectedOutput, actualResult) - } -} - -func TestBase64Decode(t *testing.T) { - t.Parallel() - originalInput := "aGVsbG8=" - expectedOutput := []byte("hello") - actualResult, err := Base64Decode(originalInput) - if !bytes.Equal(actualResult, expectedOutput) { - t.Errorf("Test failed. Expected '%s'. Actual '%s'. Error: %s", - expectedOutput, actualResult, err) - } - - _, err = Base64Decode("-") - if err == nil { - t.Error("Test failed. Bad base64 string failed returned nil error") - } -} - -func TestBase64Encode(t *testing.T) { - t.Parallel() - originalInput := []byte("hello") - expectedOutput := "aGVsbG8=" - actualResult := Base64Encode(originalInput) - if actualResult != expectedOutput { - t.Errorf("Test failed. Expected '%s'. Actual '%s'", - expectedOutput, actualResult) + t.Error("Common IsValidCryptoAddress error") } } @@ -275,19 +78,7 @@ func TestStringSliceDifference(t *testing.T) { expectedOutput := []string{"hello moto"} actualResult := StringSliceDifference(originalInputOne, originalInputTwo) if reflect.DeepEqual(expectedOutput, actualResult) { - t.Errorf("Test failed. Expected '%s'. Actual '%s'", - expectedOutput, actualResult) - } -} - -func TestStringContains(t *testing.T) { - t.Parallel() - originalInput := "hello" - originalInputSubstring := "he" - expectedOutput := true - actualResult := StringContains(originalInput, originalInputSubstring) - if actualResult != expectedOutput { - t.Errorf("Test failed. Expected '%v'. Actual '%v'", + t.Errorf("Expected '%s'. Actual '%s'", expectedOutput, actualResult) } } @@ -301,12 +92,12 @@ func TestStringDataContains(t *testing.T) { expectedOutputTwo := false actualResult := StringDataContains(originalHaystack, originalNeedle) if actualResult != expectedOutput { - t.Errorf("Test failed. Expected '%v'. Actual '%v'", + t.Errorf("Expected '%v'. Actual '%v'", expectedOutput, actualResult) } actualResult = StringDataContains(originalHaystack, anotherNeedle) if actualResult != expectedOutputTwo { - t.Errorf("Test failed. Expected '%v'. Actual '%v'", + t.Errorf("Expected '%v'. Actual '%v'", expectedOutput, actualResult) } } @@ -320,12 +111,12 @@ func TestStringDataCompare(t *testing.T) { expectedOutputTwo := false actualResult := StringDataCompare(originalHaystack, originalNeedle) if actualResult != expectedOutput { - t.Errorf("Test failed. Expected '%v'. Actual '%v'", + t.Errorf("Expected '%v'. Actual '%v'", expectedOutput, actualResult) } actualResult = StringDataCompare(originalHaystack, anotherNeedle) if actualResult != expectedOutputTwo { - t.Errorf("Test failed. Expected '%v'. Actual '%v'", + t.Errorf("Expected '%v'. Actual '%v'", expectedOutput, actualResult) } } @@ -339,13 +130,13 @@ func TestStringDataCompareUpper(t *testing.T) { expectedOutputTwo := false actualResult := StringDataCompareInsensitive(originalHaystack, originalNeedle) if actualResult != expectedOutput { - t.Errorf("Test failed. Expected '%v'. Actual '%v'", + t.Errorf("Expected '%v'. Actual '%v'", expectedOutput, actualResult) } actualResult = StringDataCompareInsensitive(originalHaystack, anotherNeedle) if actualResult != expectedOutputTwo { - t.Errorf("Test failed. Expected '%v'. Actual '%v'", + t.Errorf("Expected '%v'. Actual '%v'", expectedOutput, actualResult) } } @@ -359,162 +150,26 @@ func TestStringDataContainsUpper(t *testing.T) { expectedOutputTwo := false actualResult := StringDataContainsInsensitive(originalHaystack, originalNeedle) if actualResult != expectedOutput { - t.Errorf("Test failed. Expected '%v'. Actual '%v'", + t.Errorf("Expected '%v'. Actual '%v'", expectedOutput, actualResult) } actualResult = StringDataContainsInsensitive(originalHaystack, anotherNeedle) if actualResult != expectedOutputTwo { - t.Errorf("Test failed. Expected '%v'. Actual '%v'", + t.Errorf("Expected '%v'. Actual '%v'", expectedOutput, actualResult) } } -func TestJoinStrings(t *testing.T) { - t.Parallel() - originalInputOne := []string{"hello", "moto"} - separator := "," - expectedOutput := "hello,moto" - actualResult := JoinStrings(originalInputOne, separator) - if expectedOutput != actualResult { - t.Errorf("Test failed. Expected '%s'. Actual '%s'", - expectedOutput, actualResult) - } -} - -func TestSplitStrings(t *testing.T) { - t.Parallel() - originalInputOne := "hello,moto" - separator := "," - expectedOutput := []string{"hello", "moto"} - actualResult := SplitStrings(originalInputOne, separator) - if !reflect.DeepEqual(expectedOutput, actualResult) { - t.Errorf("Test failed. Expected '%s'. Actual '%s'", - expectedOutput, actualResult) - } -} - -func TestTrimString(t *testing.T) { - t.Parallel() - originalInput := "abcd" - cutset := "ad" - expectedOutput := "bc" - actualResult := TrimString(originalInput, cutset) - if expectedOutput != actualResult { - t.Errorf("Test failed. Expected '%s'. Actual '%s'", - expectedOutput, actualResult) - } -} - -// TestReplaceString replaces a string with another -func TestReplaceString(t *testing.T) { - t.Parallel() - currency := "BTC-USD" - expectedOutput := "BTCUSD" - - actualResult := ReplaceString(currency, "-", "", -1) - if expectedOutput != actualResult { - t.Errorf( - "Test failed. Expected '%s'. Actual '%s'", expectedOutput, actualResult, - ) - } - - currency = "BTC-USD--" - actualResult = ReplaceString(currency, "-", "", 3) - if expectedOutput != actualResult { - t.Errorf( - "Test failed. Expected '%s'. Actual '%s'", expectedOutput, actualResult, - ) - } -} - -func TestRoundFloat(t *testing.T) { - t.Parallel() - // mapping of input vs expected result - testTable := map[float64]float64{ - 2.3232323: 2.32, - -2.3232323: -2.32, - } - for testInput, expectedOutput := range testTable { - actualOutput := RoundFloat(testInput, 2) - if actualOutput != expectedOutput { - t.Errorf("Test failed. RoundFloat Expected '%f'. Actual '%f'.", - expectedOutput, actualOutput) - } - } -} - func TestYesOrNo(t *testing.T) { t.Parallel() if !YesOrNo("y") { - t.Error("Test failed - Common YesOrNo Error.") + t.Error("Common YesOrNo Error.") } if !YesOrNo("yes") { - t.Error("Test failed - Common YesOrNo Error.") + t.Error("Common YesOrNo Error.") } if YesOrNo("ding") { - t.Error("Test failed - Common YesOrNo Error.") - } -} - -func TestCalculateFee(t *testing.T) { - t.Parallel() - originalInput := float64(1) - fee := float64(1) - expectedOutput := float64(0.01) - actualResult := CalculateFee(originalInput, fee) - if expectedOutput != actualResult { - t.Errorf( - "Test failed. Expected '%f'. Actual '%f'.", expectedOutput, actualResult) - } -} - -func TestCalculateAmountWithFee(t *testing.T) { - t.Parallel() - originalInput := float64(1) - fee := float64(1) - expectedOutput := float64(1.01) - actualResult := CalculateAmountWithFee(originalInput, fee) - if expectedOutput != actualResult { - t.Errorf( - "Test failed. Expected '%f'. Actual '%f'.", expectedOutput, actualResult) - } -} - -func TestCalculatePercentageGainOrLoss(t *testing.T) { - t.Parallel() - originalInput := float64(9300) - secondInput := float64(9000) - expectedOutput := 3.3333333333333335 - actualResult := CalculatePercentageGainOrLoss(originalInput, secondInput) - if expectedOutput != actualResult { - t.Errorf( - "Test failed. Expected '%f'. Actual '%f'.", expectedOutput, actualResult) - } -} - -func TestCalculatePercentageDifference(t *testing.T) { - t.Parallel() - originalInput := float64(10) - secondAmount := float64(5) - expectedOutput := 66.66666666666666 - actualResult := CalculatePercentageDifference(originalInput, secondAmount) - if expectedOutput != actualResult { - t.Errorf( - "Test failed. Expected '%f'. Actual '%f'.", expectedOutput, actualResult) - } -} - -func TestCalculateNetProfit(t *testing.T) { - t.Parallel() - amount := float64(5) - priceThen := float64(1) - priceNow := float64(10) - costs := float64(1) - expectedOutput := float64(44) - actualResult := CalculateNetProfit(amount, priceThen, priceNow, costs) - if expectedOutput != actualResult { - t.Errorf( - "Test failed. Expected '%f'. Actual '%f'.", expectedOutput, actualResult) + t.Error("Common YesOrNo Error.") } } @@ -532,46 +187,47 @@ func TestSendHTTPRequest(t *testing.T) { strings.NewReader(""), ) if err == nil { - t.Error("Test failed. ") + t.Error("Expected error 'invalid HTTP method specified'") } _, err = SendHTTPRequest( methodPost, "https://www.google.com", headers, strings.NewReader(""), ) if err != nil { - t.Errorf("Test failed. %s ", err) + t.Error(err) } _, err = SendHTTPRequest( methodGet, "https://www.google.com", headers, strings.NewReader(""), ) if err != nil { - t.Errorf("Test failed. %s ", err) + t.Error(err) } _, err = SendHTTPRequest( methodDelete, "https://www.google.com", headers, strings.NewReader(""), ) if err != nil { - t.Errorf("Test failed. %s ", err) + t.Error(err) } _, err = SendHTTPRequest( methodGet, ":missingprotocolscheme", headers, strings.NewReader(""), ) if err == nil { - t.Error("Test failed. Common HTTPRequest accepted missing protocol") + t.Error("Common HTTPRequest accepted missing protocol") } _, err = SendHTTPRequest( methodGet, "test://unsupportedprotocolscheme", headers, strings.NewReader(""), ) if err == nil { - t.Error("Test failed. Common HTTPRequest accepted invalid protocol") + t.Error("Common HTTPRequest accepted invalid protocol") } } func TestSendHTTPGetRequest(t *testing.T) { + t.Parallel() type test struct { Address string `json:"address"` ETH struct { @@ -587,82 +243,28 @@ func TestSendHTTPGetRequest(t *testing.T) { err := SendHTTPGetRequest(ethURL, true, true, &result) if err != nil { - t.Errorf("Test failed - common SendHTTPGetRequest error: %s", err) + t.Errorf("common SendHTTPGetRequest error: %s", err) } err = SendHTTPGetRequest("DINGDONG", true, false, &result) if err == nil { - t.Error("Test failed - common SendHTTPGetRequest error") + t.Error("common SendHTTPGetRequest error") } err = SendHTTPGetRequest(ethURL, false, false, &result) if err != nil { - t.Errorf("Test failed - common SendHTTPGetRequest error: %s", err) + t.Errorf("common SendHTTPGetRequest error: %s", err) } err = SendHTTPGetRequest("https://httpstat.us/202", false, false, &result) if err == nil { - t.Error("Test failed = common SendHTTPGetRequest error: Ignored unexpected status code") + t.Error("= common SendHTTPGetRequest error: Ignored unexpected status code") } err = SendHTTPGetRequest(ethURL, true, false, &badresult) if err == nil { - t.Error("Test failed - common SendHTTPGetRequest error: Unmarshalled into bad type") - } -} - -func TestJSONEncode(t *testing.T) { - type test struct { - Status int `json:"status"` - Data []struct { - Address string `json:"address"` - Balance float64 `json:"balance"` - Nonce interface{} `json:"nonce"` - Code string `json:"code"` - Name interface{} `json:"name"` - Storage interface{} `json:"storage"` - FirstSeen interface{} `json:"firstSeen"` - } `json:"data"` - } - expectOutputString := `{"status":0,"data":null}` - v := test{} - - bitey, err := JSONEncode(v) - if err != nil { - t.Errorf("Test failed - common JSONEncode error: %s", err) - } - if string(bitey) != expectOutputString { - t.Error("Test failed - common JSONEncode error") - } - _, err = JSONEncode("WigWham") - if err != nil { - t.Errorf("Test failed - common JSONEncode error: %s", err) - } -} - -func TestJSONDecode(t *testing.T) { - t.Parallel() - var data []byte - result := "Not a memory address" - err := JSONDecode(data, result) - if err == nil { - t.Error("Test failed. Common JSONDecode, unmarshalled when address not supplied") - } - - type test struct { - Status int `json:"status"` - Data []struct { - Address string `json:"address"` - Balance float64 `json:"balance"` - } `json:"data"` - } - - var v test - data = []byte(`{"status":1,"data":null}`) - err = JSONDecode(data, &v) - if err != nil || v.Status != 1 { - t.Errorf("Test failed. Common JSONDecode. Data: %v \nError: %s", - v, err) + t.Error("common SendHTTPGetRequest error: Unmarshalled into bad type") } } func TestEncodeURLValues(t *testing.T) { + t.Parallel() urlstring := "https://www.test.com" expectedOutput := `https://www.test.com?env=TEST%2FDATABASE&format=json` values := url.Values{} @@ -671,7 +273,7 @@ func TestEncodeURLValues(t *testing.T) { output := EncodeURLValues(urlstring, values) if output != expectedOutput { - t.Error("Test Failed - common EncodeURLValues error") + t.Error("common EncodeURLValues error") } } @@ -683,12 +285,12 @@ func TestExtractHost(t *testing.T) { actualResult := ExtractHost(address) if expectedOutput != actualResult { t.Errorf( - "Test failed. Expected '%s'. Actual '%s'.", expectedOutput, actualResult) + "Expected '%s'. Actual '%s'.", expectedOutput, actualResult) } actualResultTwo := ExtractHost(addresstwo) if expectedOutput != actualResultTwo { t.Errorf( - "Test failed. Expected '%s'. Actual '%s'.", expectedOutput, actualResult) + "Expected '%s'. Actual '%s'.", expectedOutput, actualResult) } address = "192.168.1.100:1337" @@ -696,7 +298,7 @@ func TestExtractHost(t *testing.T) { actualResult = ExtractHost(address) if expectedOutput != actualResult { t.Errorf( - "Test failed. Expected '%s'. Actual '%s'.", expectedOutput, actualResult) + "Expected '%s'. Actual '%s'.", expectedOutput, actualResult) } } @@ -707,7 +309,7 @@ func TestExtractPort(t *testing.T) { actualResult := ExtractPort(address) if expectedOutput != actualResult { t.Errorf( - "Test failed. Expected '%d'. Actual '%d'.", expectedOutput, actualResult) + "Expected '%d'. Actual '%d'.", expectedOutput, actualResult) } } @@ -720,89 +322,11 @@ func TestOutputCSV(t *testing.T) { err := OutputCSV(path, data) if err != nil { - t.Errorf("Test failed - common OutputCSV error: %s", err) + t.Errorf("common OutputCSV error: %s", err) } err = OutputCSV("/:::notapath:::", data) if err == nil { - t.Error("Test failed - common OutputCSV, tried writing to invalid path") - } -} - -func TestUnixTimestampToTime(t *testing.T) { - t.Parallel() - testTime := int64(1489439831) - tm := time.Unix(testTime, 0) - expectedOutput := "2017-03-13 21:17:11 +0000 UTC" - actualResult := UnixTimestampToTime(testTime) - if tm.String() != actualResult.String() { - t.Errorf( - "Test failed. Expected '%s'. Actual '%s'.", expectedOutput, actualResult) - } -} - -func TestUnixTimestampStrToTime(t *testing.T) { - t.Parallel() - testTime := "1489439831" - incorrectTime := "DINGDONG" - expectedOutput := "2017-03-13 21:17:11 +0000 UTC" - actualResult, err := UnixTimestampStrToTime(testTime) - if err != nil { - t.Error(err) - } - if actualResult.UTC().String() != expectedOutput { - t.Errorf( - "Test failed. Expected '%s'. Actual '%s'.", expectedOutput, actualResult) - } - actualResult, err = UnixTimestampStrToTime(incorrectTime) - if err == nil { - t.Error("Test failed. Common UnixTimestampStrToTime error") - } -} - -func TestReadFile(t *testing.T) { - pathCorrect := "../testdata/dump" - pathIncorrect := "testdata/dump" - - _, err := ReadFile(pathCorrect) - if err != nil { - t.Errorf("Test failed - Common ReadFile error: %s", err) - } - _, err = ReadFile(pathIncorrect) - if err == nil { - t.Errorf("Test failed - Common ReadFile error") - } -} - -func TestWriteFile(t *testing.T) { - path := "../testdata/writefiletest" - err := WriteFile(path, nil) - if err != nil { - t.Errorf("Test failed. Common WriteFile error: %s", err) - } - _, err = ReadFile(path) - if err != nil { - t.Errorf("Test failed. Common WriteFile error: %s", err) - } - - err = WriteFile("", nil) - if err == nil { - t.Error("Test failed. Common WriteFile allowed bad path") - } -} - -func TestRemoveFile(t *testing.T) { - TestWriteFile(t) - path := "../testdata/writefiletest" - err := RemoveFile(path) - if err != nil { - t.Errorf("Test failed. Common RemoveFile error: %s", err) - } - - TestOutputCSV(t) - path = "../testdata/dump" - err = RemoveFile(path) - if err != nil { - t.Errorf("Test failed. Common RemoveFile error: %s", err) + t.Error("common OutputCSV, tried writing to invalid path") } } @@ -817,7 +341,7 @@ func TestGetURIPath(t *testing.T) { for testInput, expectedOutput := range testTable { actualOutput := GetURIPath(testInput) if actualOutput != expectedOutput { - t.Errorf("Test failed. Expected '%s'. Actual '%s'.", + t.Errorf("Expected '%s'. Actual '%s'.", expectedOutput, actualOutput) } } @@ -827,128 +351,7 @@ func TestGetExecutablePath(t *testing.T) { t.Parallel() _, err := GetExecutablePath() if err != nil { - t.Errorf("Test failed. Common GetExecutablePath. Error: %s", err) - } -} - -func TestGetOSPathSlash(t *testing.T) { - output := GetOSPathSlash() - if output != "/" && output != "\\" { - t.Errorf("Test failed. Common GetOSPathSlash. Returned '%s'", output) - } -} - -func TestUnixMillis(t *testing.T) { - t.Parallel() - testTime := time.Date(2014, time.October, 28, 0, 32, 0, 0, time.UTC) - expectedOutput := int64(1414456320000) - - actualOutput := UnixMillis(testTime) - if actualOutput != expectedOutput { - t.Errorf("Test failed. Common UnixMillis. Expected '%d'. Actual '%d'.", - expectedOutput, actualOutput) - } -} - -func TestRecvWindow(t *testing.T) { - t.Parallel() - testTime := time.Duration(24760000) - expectedOutput := int64(24) - - actualOutput := RecvWindow(testTime) - if actualOutput != expectedOutput { - t.Errorf("Test failed. Common RecvWindow. Expected '%d'. Actual '%d'", - expectedOutput, actualOutput) - } -} - -func TestFloatFromString(t *testing.T) { - t.Parallel() - testString := "1.41421356237" - expectedOutput := float64(1.41421356237) - - actualOutput, err := FloatFromString(testString) - if actualOutput != expectedOutput || err != nil { - t.Errorf("Test failed. Common FloatFromString. Expected '%v'. Actual '%v'. Error: %s", - expectedOutput, actualOutput, err) - } - - var testByte []byte - _, err = FloatFromString(testByte) - if err == nil { - t.Error("Test failed. Common FloatFromString. Converted non-string.") - } - - testString = " something unconvertible " - _, err = FloatFromString(testString) - if err == nil { - t.Error("Test failed. Common FloatFromString. Converted invalid syntax.") - } -} - -func TestIntFromString(t *testing.T) { - t.Parallel() - testString := "1337" - expectedOutput := 1337 - - actualOutput, err := IntFromString(testString) - if actualOutput != expectedOutput || err != nil { - t.Errorf("Test failed. Common IntFromString. Expected '%v'. Actual '%v'. Error: %s", - expectedOutput, actualOutput, err) - } - - var testByte []byte - _, err = IntFromString(testByte) - if err == nil { - t.Error("Test failed. Common IntFromString. Converted non-string.") - } - - testString = "1.41421356237" - _, err = IntFromString(testString) - if err == nil { - t.Error("Test failed. Common IntFromString. Converted invalid syntax.") - } -} - -func TestInt64FromString(t *testing.T) { - t.Parallel() - testString := "4398046511104" - expectedOutput := int64(1 << 42) - - actualOutput, err := Int64FromString(testString) - if actualOutput != expectedOutput || err != nil { - t.Errorf("Test failed. Common Int64FromString. Expected '%v'. Actual '%v'. Error: %s", - expectedOutput, actualOutput, err) - } - - var testByte []byte - _, err = Int64FromString(testByte) - if err == nil { - t.Error("Test failed. Common Int64FromString. Converted non-string.") - } - - testString = "1.41421356237" - _, err = Int64FromString(testString) - if err == nil { - t.Error("Test failed. Common Int64FromString. Converted invalid syntax.") - } -} - -func TestTimeFromUnixTimestampFloat(t *testing.T) { - t.Parallel() - testTimestamp := float64(1414456320000) - expectedOutput := time.Date(2014, time.October, 28, 0, 32, 0, 0, time.UTC) - - actualOutput, err := TimeFromUnixTimestampFloat(testTimestamp) - if actualOutput.UTC().String() != expectedOutput.UTC().String() || err != nil { - t.Errorf("Test failed. Common TimeFromUnixTimestampFloat. Expected '%v'. Actual '%v'. Error: %s", - expectedOutput, actualOutput, err) - } - - testString := "Time" - _, err = TimeFromUnixTimestampFloat(testString) - if err == nil { - t.Error("Test failed. Common TimeFromUnixTimestampFloat. Converted invalid syntax.") + t.Errorf("Common GetExecutablePath. Error: %s", err) } } @@ -1008,7 +411,7 @@ func TestCreateDir(t *testing.T) { if !ok { t.Fatal("LookupEnv failed. APPDATA is not set") } - dir = dir + GetOSPathSlash() + "GoCryptoTrader\\TestFileASDFG" + dir = filepath.Join(dir, "GoCryptoTrader", "TestFileASDFG") err = CreateDir(dir) if err != nil { t.Fatalf("CreateDir failed. Err: %v", err) @@ -1045,53 +448,54 @@ func TestCreateDir(t *testing.T) { } } -func TestChangePerm(t *testing.T) { +func TestChangePermission(t *testing.T) { + testDir := filepath.Join(os.TempDir(), "TestFileASDFGHJ") switch runtime.GOOS { case "windows": - err := ChangePerm("*") + err := ChangePermission("*") if err == nil { t.Fatal("expected an error on non-existent path") } - err = os.Mkdir(GetDefaultDataDir(runtime.GOOS)+GetOSPathSlash()+"TestFileASDFGHJ", 0777) + err = os.Mkdir(testDir, 0777) if err != nil { t.Fatalf("Mkdir failed. Err: %v", err) } - err = ChangePerm(GetDefaultDataDir(runtime.GOOS)) + err = ChangePermission(testDir) if err != nil { t.Fatalf("ChangePerm was unsuccessful. Err: %v", err) } - _, err = os.Stat(GetDefaultDataDir(runtime.GOOS) + GetOSPathSlash() + "TestFileASDFGHJ") + _, err = os.Stat(testDir) if err != nil { t.Fatalf("os.Stat failed. Err: %v", err) } - err = RemoveFile(GetDefaultDataDir(runtime.GOOS) + GetOSPathSlash() + "TestFileASDFGHJ") + err = os.Remove(testDir) if err != nil { - t.Fatalf("RemoveFile failed. Err: %v", err) + t.Fatalf("os.Remove failed. Err: %v", err) } default: - err := ChangePerm("") + err := ChangePermission("") if err == nil { t.Fatal("expected an error on non-existent path") } - err = os.Mkdir(GetDefaultDataDir(runtime.GOOS)+GetOSPathSlash()+"TestFileASDFGHJ", 0777) + err = os.Mkdir(testDir, 0777) if err != nil { t.Fatalf("Mkdir failed. Err: %v", err) } - err = ChangePerm(GetDefaultDataDir(runtime.GOOS)) + err = ChangePermission(testDir) if err != nil { t.Fatalf("ChangePerm was unsuccessful. Err: %v", err) } var a os.FileInfo - a, err = os.Stat(GetDefaultDataDir(runtime.GOOS) + GetOSPathSlash() + "TestFileASDFGHJ") + a, err = os.Stat(testDir) if err != nil { t.Fatalf("os.Stat failed. Err: %v", err) } if a.Mode().Perm() != 0770 { t.Fatalf("expected file permissions differ. expecting 0770 got %#o", a.Mode().Perm()) } - err = RemoveFile(GetDefaultDataDir(runtime.GOOS) + GetOSPathSlash() + "TestFileASDFGHJ") + err = os.Remove(testDir) if err != nil { - t.Fatalf("RemoveFile failed. Err: %v", err) + t.Fatalf("os.Remove failed. Err: %v", err) } } } diff --git a/common/convert/convert.go b/common/convert/convert.go new file mode 100644 index 00000000..1993de89 --- /dev/null +++ b/common/convert/convert.go @@ -0,0 +1,109 @@ +package convert + +import ( + "errors" + "fmt" + "math" + "strconv" + "strings" + "time" +) + +// FloatFromString format +func FloatFromString(raw interface{}) (float64, error) { + str, ok := raw.(string) + if !ok { + return 0, fmt.Errorf("unable to parse, value not string: %T", raw) + } + flt, err := strconv.ParseFloat(str, 64) + if err != nil { + return 0, fmt.Errorf("could not convert value: %s Error: %s", str, err) + } + return flt, nil +} + +// IntFromString format +func IntFromString(raw interface{}) (int, error) { + str, ok := raw.(string) + if !ok { + return 0, fmt.Errorf("unable to parse, value not string: %T", raw) + } + n, err := strconv.Atoi(str) + if err != nil { + return 0, fmt.Errorf("unable to parse as int: %T", raw) + } + return n, nil +} + +// Int64FromString format +func Int64FromString(raw interface{}) (int64, error) { + str, ok := raw.(string) + if !ok { + return 0, fmt.Errorf("unable to parse, value not string: %T", raw) + } + n, err := strconv.ParseInt(str, 10, 64) + if err != nil { + return 0, fmt.Errorf("unable to parse as int64: %T", raw) + } + return n, nil +} + +// TimeFromUnixTimestampFloat format +func TimeFromUnixTimestampFloat(raw interface{}) (time.Time, error) { + ts, ok := raw.(float64) + if !ok { + return time.Time{}, fmt.Errorf("unable to parse, value not float64: %T", raw) + } + return time.Unix(0, int64(ts)*int64(time.Millisecond)), nil +} + +// UnixTimestampToTime returns time.time +func UnixTimestampToTime(timeint64 int64) time.Time { + return time.Unix(timeint64, 0) +} + +// UnixTimestampStrToTime returns a time.time and an error +func UnixTimestampStrToTime(timeStr string) (time.Time, error) { + i, err := strconv.ParseInt(timeStr, 10, 64) + if err != nil { + return time.Time{}, err + } + return time.Unix(i, 0), nil +} + +// UnixMillis converts a UnixNano timestamp to milliseconds +func UnixMillis(t time.Time) int64 { + return t.UnixNano() / int64(time.Millisecond) +} + +// RecvWindow converts a supplied time.Duration to milliseconds +func RecvWindow(d time.Duration) int64 { + return int64(d) / int64(time.Millisecond) +} + +// SplitFloatDecimals takes in a float64 and splits +// the decimals into their own integers +// Warning. Passing in numbers with many decimals +// can lead to a loss of accuracy +func SplitFloatDecimals(input float64) (baseNum, decimalNum int64, err error) { + if input > float64(math.MaxInt64) { + return 0, 0, errors.New("number too large to split into integers") + } + if input == float64(int64(input)) { + return int64(input), 0, nil + } + decStr := strconv.FormatFloat(input, 'f', -1, 64) + splitNum := strings.Split(decStr, ".") + baseNum, err = strconv.ParseInt(splitNum[0], 10, 64) + if err != nil { + return 0, 0, err + } + decimalNum, err = strconv.ParseInt(splitNum[1], 10, 64) + if err != nil { + return 0, 0, err + } + if baseNum < 0 { + decimalNum *= -1 + } + return baseNum, decimalNum, nil +} diff --git a/common/convert/convert_test.go b/common/convert/convert_test.go new file mode 100644 index 00000000..60d84153 --- /dev/null +++ b/common/convert/convert_test.go @@ -0,0 +1,207 @@ +package convert + +import ( + "math" + "testing" + "time" +) + +func TestFloatFromString(t *testing.T) { + t.Parallel() + testString := "1.41421356237" + expectedOutput := float64(1.41421356237) + + actualOutput, err := FloatFromString(testString) + if actualOutput != expectedOutput || err != nil { + t.Errorf("Common FloatFromString. Expected '%v'. Actual '%v'. Error: %s", + expectedOutput, actualOutput, err) + } + + var testByte []byte + _, err = FloatFromString(testByte) + if err == nil { + t.Error("Common FloatFromString. Converted non-string.") + } + + testString = " something unconvertible " + _, err = FloatFromString(testString) + if err == nil { + t.Error("Common FloatFromString. Converted invalid syntax.") + } +} + +func TestIntFromString(t *testing.T) { + t.Parallel() + testString := "1337" + expectedOutput := 1337 + + actualOutput, err := IntFromString(testString) + if actualOutput != expectedOutput || err != nil { + t.Errorf("Common IntFromString. Expected '%v'. Actual '%v'. Error: %s", + expectedOutput, actualOutput, err) + } + + var testByte []byte + _, err = IntFromString(testByte) + if err == nil { + t.Error("Common IntFromString. Converted non-string.") + } + + testString = "1.41421356237" + _, err = IntFromString(testString) + if err == nil { + t.Error("Common IntFromString. Converted invalid syntax.") + } +} + +func TestInt64FromString(t *testing.T) { + t.Parallel() + testString := "4398046511104" + expectedOutput := int64(1 << 42) + + actualOutput, err := Int64FromString(testString) + if actualOutput != expectedOutput || err != nil { + t.Errorf("Common Int64FromString. Expected '%v'. Actual '%v'. Error: %s", + expectedOutput, actualOutput, err) + } + + var testByte []byte + _, err = Int64FromString(testByte) + if err == nil { + t.Error("Common Int64FromString. Converted non-string.") + } + + testString = "1.41421356237" + _, err = Int64FromString(testString) + if err == nil { + t.Error("Common Int64FromString. Converted invalid syntax.") + } +} + +func TestTimeFromUnixTimestampFloat(t *testing.T) { + t.Parallel() + testTimestamp := float64(1414456320000) + expectedOutput := time.Date(2014, time.October, 28, 0, 32, 0, 0, time.UTC) + + actualOutput, err := TimeFromUnixTimestampFloat(testTimestamp) + if actualOutput.UTC().String() != expectedOutput.UTC().String() || err != nil { + t.Errorf("Common TimeFromUnixTimestampFloat. Expected '%v'. Actual '%v'. Error: %s", + expectedOutput, actualOutput, err) + } + + testString := "Time" + _, err = TimeFromUnixTimestampFloat(testString) + if err == nil { + t.Error("Common TimeFromUnixTimestampFloat. Converted invalid syntax.") + } +} + +func TestUnixTimestampToTime(t *testing.T) { + t.Parallel() + testTime := int64(1489439831) + tm := time.Unix(testTime, 0) + expectedOutput := "2017-03-13 21:17:11 +0000 UTC" + actualResult := UnixTimestampToTime(testTime) + if tm.String() != actualResult.String() { + t.Errorf( + "Expected '%s'. Actual '%s'.", expectedOutput, actualResult) + } +} + +func TestUnixTimestampStrToTime(t *testing.T) { + t.Parallel() + testTime := "1489439831" + incorrectTime := "DINGDONG" + expectedOutput := "2017-03-13 21:17:11 +0000 UTC" + actualResult, err := UnixTimestampStrToTime(testTime) + if err != nil { + t.Error(err) + } + if actualResult.UTC().String() != expectedOutput { + t.Errorf( + "Expected '%s'. Actual '%s'.", expectedOutput, actualResult) + } + actualResult, err = UnixTimestampStrToTime(incorrectTime) + if err == nil { + t.Error("Common UnixTimestampStrToTime error") + } +} + +func TestUnixMillis(t *testing.T) { + t.Parallel() + testTime := time.Date(2014, time.October, 28, 0, 32, 0, 0, time.UTC) + expectedOutput := int64(1414456320000) + + actualOutput := UnixMillis(testTime) + if actualOutput != expectedOutput { + t.Errorf("Common UnixMillis. Expected '%d'. Actual '%d'.", + expectedOutput, actualOutput) + } +} + +func TestRecvWindow(t *testing.T) { + t.Parallel() + testTime := time.Duration(24760000) + expectedOutput := int64(24) + + actualOutput := RecvWindow(testTime) + if actualOutput != expectedOutput { + t.Errorf("Common RecvWindow. Expected '%d'. Actual '%d'", + expectedOutput, actualOutput) + } +} + +// TestSplitFloatDecimals ensures SplitFloatDecimals +// accurately splits decimals into integers +func TestSplitFloatDecimals(t *testing.T) { + x, y, err := SplitFloatDecimals(1.2) + if err != nil { + t.Error(err) + } + if x != 1 && y != 2 { + t.Error("Conversion error") + } + x, y, err = SplitFloatDecimals(123456.654321) + if err != nil { + t.Error(err) + } + if x != 123456 && y != 654321 { + t.Error("Conversion error") + } + x, y, err = SplitFloatDecimals(123.111000) + if err != nil { + t.Error(err) + } + if x != 123 && y != 111 { + t.Error("Conversion error") + } + x, y, err = SplitFloatDecimals(0123.111001) + if err != nil { + t.Error(err) + } + if x != 123 && y != 111001 { + t.Error("Conversion error") + } + x, y, err = SplitFloatDecimals(1) + if err != nil { + t.Error(err) + } + if x != 1 && y != 0 { + t.Error("Conversion error") + } + _, _, err = SplitFloatDecimals(float64(math.MaxInt64) + 1) + if err == nil { + t.Error("Expected conversion error") + } + _, _, err = SplitFloatDecimals(1797693134862315700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111) + if err == nil { + t.Error("Expected conversion error") + } + x, y, err = SplitFloatDecimals(-1.2) + if err != nil { + t.Error(err) + } + if x != -1 && y != -2 { + t.Error("Conversion error") + } +} diff --git a/common/crypto/crypto.go b/common/crypto/crypto.go new file mode 100644 index 00000000..75ae6d9e --- /dev/null +++ b/common/crypto/crypto.go @@ -0,0 +1,113 @@ +package crypto + +import ( + "crypto/hmac" + "crypto/md5" // nolint:gosec + "crypto/rand" + "crypto/sha1" // nolint:gosec + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "errors" + "hash" + "io" +) + +// Const declarations for common.go operations +const ( + HashSHA1 = iota + HashSHA256 + HashSHA512 + HashSHA512_384 + HashMD5 +) + +// HexEncodeToString takes in a hexadecimal byte array and returns a string +func HexEncodeToString(input []byte) string { + return hex.EncodeToString(input) +} + +// Base64Decode takes in a Base64 string and returns a byte array and an error +func Base64Decode(input string) ([]byte, error) { + result, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return nil, err + } + return result, nil +} + +// Base64Encode takes in a byte array then returns an encoded base64 string +func Base64Encode(input []byte) string { + return base64.StdEncoding.EncodeToString(input) +} + +// GetRandomSalt returns a random salt +func GetRandomSalt(input []byte, saltLen int) ([]byte, error) { + if saltLen <= 0 { + return nil, errors.New("salt length is too small") + } + salt := make([]byte, saltLen) + if _, err := io.ReadFull(rand.Reader, salt); err != nil { + return nil, err + } + + var result []byte + if input != nil { + result = input + } + result = append(result, salt...) + return result, nil +} + +// GetMD5 returns a MD5 hash of a byte array +func GetMD5(input []byte) []byte { + m := md5.New() // nolint:gosec + m.Write(input) + return m.Sum(nil) +} + +// GetSHA512 returns a SHA512 hash of a byte array +func GetSHA512(input []byte) []byte { + sha := sha512.New() + sha.Write(input) + return sha.Sum(nil) +} + +// GetSHA256 returns a SHA256 hash of a byte array +func GetSHA256(input []byte) []byte { + sha := sha256.New() + sha.Write(input) + return sha.Sum(nil) +} + +// GetHMAC returns a keyed-hash message authentication code using the desired +// hashtype +func GetHMAC(hashType int, input, key []byte) []byte { + var hasher func() hash.Hash + + switch hashType { + case HashSHA1: + hasher = sha1.New + case HashSHA256: + hasher = sha256.New + case HashSHA512: + hasher = sha512.New + case HashSHA512_384: + hasher = sha512.New384 + case HashMD5: + hasher = md5.New + } + + h := hmac.New(hasher, key) + h.Write(input) + return h.Sum(nil) +} + +// Sha1ToHex takes a string, sha1 hashes it and return a hex string of the +// result +func Sha1ToHex(data string) string { + h := sha1.New() // nolint:gosec + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/common/crypto/crypto_test.go b/common/crypto/crypto_test.go new file mode 100644 index 00000000..4873988b --- /dev/null +++ b/common/crypto/crypto_test.go @@ -0,0 +1,178 @@ +package crypto + +import ( + "bytes" + "testing" +) + +func TestHexEncodeToString(t *testing.T) { + t.Parallel() + originalInput := []byte("string") + expectedOutput := "737472696e67" + actualResult := HexEncodeToString(originalInput) + if actualResult != expectedOutput { + t.Errorf("Expected '%s'. Actual '%s'", + expectedOutput, actualResult) + } +} + +func TestBase64Decode(t *testing.T) { + t.Parallel() + originalInput := "aGVsbG8=" + expectedOutput := []byte("hello") + actualResult, err := Base64Decode(originalInput) + if !bytes.Equal(actualResult, expectedOutput) { + t.Errorf("Expected '%s'. Actual '%s'. Error: %s", + expectedOutput, actualResult, err) + } + + _, err = Base64Decode("-") + if err == nil { + t.Error("Bad base64 string failed returned nil error") + } +} + +func TestBase64Encode(t *testing.T) { + t.Parallel() + originalInput := []byte("hello") + expectedOutput := "aGVsbG8=" + actualResult := Base64Encode(originalInput) + if actualResult != expectedOutput { + t.Errorf("Expected '%s'. Actual '%s'", + expectedOutput, actualResult) + } +} + +func TestGetRandomSalt(t *testing.T) { + t.Parallel() + + _, err := GetRandomSalt(nil, -1) + if err == nil { + t.Fatal("Expected err on negative salt length") + } + + salt, err := GetRandomSalt(nil, 10) + if err != nil { + t.Fatal(err) + } + + if len(salt) != 10 { + t.Fatal("Expected salt of len=10") + } + + salt, err = GetRandomSalt([]byte("RAWR"), 12) + if err != nil { + t.Fatal(err) + } + + if len(salt) != 16 { + t.Fatal("Expected salt of len=16") + } +} + +func TestGetMD5(t *testing.T) { + t.Parallel() + var originalString = []byte("I am testing the MD5 function in common!") + var expectedOutput = []byte("18fddf4a41ba90a7352765e62e7a8744") + actualOutput := GetMD5(originalString) + actualStr := HexEncodeToString(actualOutput) + if !bytes.Equal(expectedOutput, []byte(actualStr)) { + t.Errorf("Expected '%s'. Actual '%s'", + expectedOutput, []byte(actualStr)) + } +} + +func TestGetSHA512(t *testing.T) { + t.Parallel() + var originalString = []byte("I am testing the GetSHA512 function in common!") + var expectedOutput = []byte( + `a2273f492ea73fddc4f25c267b34b3b74998bd8a6301149e1e1c835678e3c0b90859fce22e4e7af33bde1711cbb924809aedf5d759d648d61774b7185c5dc02b`, + ) + actualOutput := GetSHA512(originalString) + actualStr := HexEncodeToString(actualOutput) + if !bytes.Equal(expectedOutput, []byte(actualStr)) { + t.Errorf("Expected '%x'. Actual '%x'", + expectedOutput, []byte(actualStr)) + } +} + +func TestGetSHA256(t *testing.T) { + t.Parallel() + var originalString = []byte("I am testing the GetSHA256 function in common!") + var expectedOutput = []byte( + "0962813d7a9f739cdcb7f0c0be0c2a13bd630167e6e54468266e4af6b1ad9303", + ) + actualOutput := GetSHA256(originalString) + actualStr := HexEncodeToString(actualOutput) + if !bytes.Equal(expectedOutput, []byte(actualStr)) { + t.Errorf("Expected '%x'. Actual '%x'", expectedOutput, + []byte(actualStr)) + } +} + +func TestGetHMAC(t *testing.T) { + t.Parallel() + expectedSha1 := []byte{ + 74, 253, 245, 154, 87, 168, 110, 182, 172, 101, 177, 49, 142, 2, 253, 165, + 100, 66, 86, 246, + } + expectedsha256 := []byte{ + 54, 68, 6, 12, 32, 158, 80, 22, 142, 8, 131, 111, 248, 145, 17, 202, 224, + 59, 135, 206, 11, 170, 154, 197, 183, 28, 150, 79, 168, 105, 62, 102, + } + expectedsha512 := []byte{ + 249, 212, 31, 38, 23, 3, 93, 220, 81, 209, 214, 112, 92, 75, 126, 40, 109, + 95, 247, 182, 210, 54, 217, 224, 199, 252, 129, 226, 97, 201, 245, 220, 37, + 201, 240, 15, 137, 236, 75, 6, 97, 12, 190, 31, 53, 153, 223, 17, 214, 11, + 153, 203, 49, 29, 158, 217, 204, 93, 179, 109, 140, 216, 202, 71, + } + expectedsha512384 := []byte{ + 121, 203, 109, 105, 178, 68, 179, 57, 21, 217, 76, 82, 94, 100, 210, 1, 55, + 201, 8, 232, 194, 168, 165, 58, 192, 26, 193, 167, 254, 183, 172, 4, 189, + 158, 158, 150, 173, 33, 119, 125, 94, 13, 125, 89, 241, 184, 166, 128, + } + expectedmd5 := []byte{ + 113, 64, 132, 129, 213, 68, 231, 99, 252, 15, 175, 109, 198, 132, 139, 39, + } + + sha1 := GetHMAC(HashSHA1, []byte("Hello,World"), []byte("1234")) + if string(sha1) != string(expectedSha1) { + t.Errorf("Common GetHMAC error: Expected '%x'. Actual '%x'", + expectedSha1, sha1, + ) + } + sha256 := GetHMAC(HashSHA256, []byte("Hello,World"), []byte("1234")) + if string(sha256) != string(expectedsha256) { + t.Errorf("Common GetHMAC error: Expected '%x'. Actual '%x'", + expectedsha256, sha256, + ) + } + sha512 := GetHMAC(HashSHA512, []byte("Hello,World"), []byte("1234")) + if string(sha512) != string(expectedsha512) { + t.Errorf("Common GetHMAC error: Expected '%x'. Actual '%x'", + expectedsha512, sha512, + ) + } + sha512384 := GetHMAC(HashSHA512_384, []byte("Hello,World"), []byte("1234")) + if string(sha512384) != string(expectedsha512384) { + t.Errorf("Common GetHMAC error: Expected '%x'. Actual '%x'", + expectedsha512384, sha512384, + ) + } + md5 := GetHMAC(HashMD5, []byte("Hello World"), []byte("1234")) + if string(md5) != string(expectedmd5) { + t.Errorf("Common GetHMAC error: Expected '%x'. Actual '%x'", + expectedmd5, md5, + ) + } +} + +func TestSha1Tohex(t *testing.T) { + t.Parallel() + expectedResult := "fcfbfcd7d31d994ef660f6972399ab5d7a890149" + actualResult := Sha1ToHex("Testing Sha1ToHex") + if actualResult != expectedResult { + t.Errorf("Expected '%s'. Actual '%s'", + expectedResult, actualResult) + } +} diff --git a/common/file/file.go b/common/file/file.go new file mode 100644 index 00000000..ec3ed644 --- /dev/null +++ b/common/file/file.go @@ -0,0 +1,47 @@ +package file + +import ( + "fmt" + "io" + "io/ioutil" + "os" +) + +// Write writes selected data to a file or returns an error if it fails. This +// func also ensures that all files are set to this permission (only rw access +// for the running user and the group the user is a member of) +func Write(file string, data []byte) error { + return ioutil.WriteFile(file, data, 0770) +} + +// Move moves a file from a source path to a destination path +// This must be used across the codebase for compatibility with Docker volumes +// and Golang (fixes Invalid cross-device link when using os.Rename) +func Move(sourcePath, destPath string) error { + inputFile, err := os.Open(sourcePath) + if err != nil { + return err + } + + outputFile, err := os.Create(destPath) + if err != nil { + inputFile.Close() + return err + } + + _, err = io.Copy(outputFile, inputFile) + inputFile.Close() + outputFile.Close() + if err != nil { + if errRem := os.Remove(destPath); errRem != nil { + return fmt.Errorf( + "unable to os.Remove error: %s after io.Copy error: %s", + errRem, + err, + ) + } + return err + } + + return os.Remove(sourcePath) +} diff --git a/common/file/file_test.go b/common/file/file_test.go new file mode 100644 index 00000000..f0db7d1f --- /dev/null +++ b/common/file/file_test.go @@ -0,0 +1,106 @@ +package file + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestWrite(t *testing.T) { + tester := func(in string) error { + err := Write(in, []byte("GoCryptoTrader")) + if err != nil { + return err + } + return os.Remove(in) + } + + type testTable struct { + InFile string + ErrExpected bool + Cleanup bool + } + + var tests []testTable + testFile := filepath.Join(os.TempDir(), "gcttest.txt") + switch runtime.GOOS { + case "windows": + tests = []testTable{ + {InFile: "*", ErrExpected: true}, + {InFile: testFile, ErrExpected: false}, + } + default: + tests = []testTable{ + {InFile: "", ErrExpected: true}, + {InFile: testFile, ErrExpected: false}, + } + } + + for x := range tests { + err := tester(tests[x].InFile) + if err != nil && !tests[x].ErrExpected { + t.Errorf("Test %d failed, unexpected err %s\n", x, err) + } + } +} + +func TestMove(t *testing.T) { + tester := func(in, out string, write bool) error { + if write { + if err := ioutil.WriteFile(in, []byte("GoCryptoTrader"), 0770); err != nil { + return err + } + } + + if err := Move(in, out); err != nil { + return err + } + + contents, err := ioutil.ReadFile(out) + if err != nil { + return err + } + + if !strings.Contains(string(contents), "GoCryptoTrader") { + return fmt.Errorf("unable to find previously written data") + } + + return os.Remove(out) + } + + type testTable struct { + InFile string + OutFile string + Write bool + ErrExpected bool + } + + var tests []testTable + switch runtime.GOOS { + case "windows": + tests = []testTable{ + {InFile: "*", OutFile: "gct.txt", Write: true, ErrExpected: true}, + {InFile: "*", OutFile: "gct.txt", Write: false, ErrExpected: true}, + {InFile: "in.txt", OutFile: "*", Write: true, ErrExpected: true}, + {InFile: "in.txt", OutFile: "gct.txt", Write: true, ErrExpected: false}, + } + default: + tests = []testTable{ + {InFile: "", OutFile: "gct.txt", Write: true, ErrExpected: true}, + {InFile: "", OutFile: "gct.txt", Write: false, ErrExpected: true}, + {InFile: "in.txt", OutFile: "", Write: true, ErrExpected: true}, + {InFile: "in.txt", OutFile: "gct.txt", Write: true, ErrExpected: false}, + } + } + + for x := range tests { + err := tester(tests[x].InFile, tests[x].OutFile, tests[x].Write) + if err != nil && !tests[x].ErrExpected { + t.Errorf("Test %d failed, unexpected err %s\n", x, err) + } + } +} diff --git a/common/math/math.go b/common/math/math.go new file mode 100644 index 00000000..fccd6e72 --- /dev/null +++ b/common/math/math.go @@ -0,0 +1,51 @@ +package math + +import "math" + +// CalculateAmountWithFee returns a calculated fee included amount on fee +func CalculateAmountWithFee(amount, fee float64) float64 { + return amount + CalculateFee(amount, fee) +} + +// CalculateFee returns a simple fee on amount +func CalculateFee(amount, fee float64) float64 { + return amount * (fee / 100) +} + +// CalculatePercentageGainOrLoss returns the percentage rise over a certain +// period +func CalculatePercentageGainOrLoss(priceNow, priceThen float64) float64 { + return (priceNow - priceThen) / priceThen * 100 +} + +// CalculatePercentageDifference returns the percentage of difference between +// multiple time periods +func CalculatePercentageDifference(amount, secondAmount float64) float64 { + return (amount - secondAmount) / ((amount + secondAmount) / 2) * 100 +} + +// CalculateNetProfit returns net profit +func CalculateNetProfit(amount, priceThen, priceNow, costs float64) float64 { + return (priceNow * amount) - (priceThen * amount) - costs +} + +// RoundFloat rounds your floating point number to the desired decimal place +func RoundFloat(x float64, prec int) float64 { + var rounder float64 + pow := math.Pow(10, float64(prec)) + intermed := x * pow + _, frac := math.Modf(intermed) + intermed += .5 + x = .5 + if frac < 0.0 { + x = -.5 + intermed-- + } + if frac >= x { + rounder = math.Ceil(intermed) + } else { + rounder = math.Floor(intermed) + } + + return rounder / pow +} diff --git a/common/math/math_test.go b/common/math/math_test.go new file mode 100644 index 00000000..37de7cdf --- /dev/null +++ b/common/math/math_test.go @@ -0,0 +1,81 @@ +package math + +import "testing" + +func TestCalculateFee(t *testing.T) { + t.Parallel() + originalInput := float64(1) + fee := float64(1) + expectedOutput := float64(0.01) + actualResult := CalculateFee(originalInput, fee) + if expectedOutput != actualResult { + t.Errorf( + "Expected '%f'. Actual '%f'.", expectedOutput, actualResult) + } +} + +func TestCalculateAmountWithFee(t *testing.T) { + t.Parallel() + originalInput := float64(1) + fee := float64(1) + expectedOutput := float64(1.01) + actualResult := CalculateAmountWithFee(originalInput, fee) + if expectedOutput != actualResult { + t.Errorf( + "Expected '%f'. Actual '%f'.", expectedOutput, actualResult) + } +} + +func TestCalculatePercentageGainOrLoss(t *testing.T) { + t.Parallel() + originalInput := float64(9300) + secondInput := float64(9000) + expectedOutput := 3.3333333333333335 + actualResult := CalculatePercentageGainOrLoss(originalInput, secondInput) + if expectedOutput != actualResult { + t.Errorf( + "Expected '%f'. Actual '%f'.", expectedOutput, actualResult) + } +} + +func TestCalculatePercentageDifference(t *testing.T) { + t.Parallel() + originalInput := float64(10) + secondAmount := float64(5) + expectedOutput := 66.66666666666666 + actualResult := CalculatePercentageDifference(originalInput, secondAmount) + if expectedOutput != actualResult { + t.Errorf( + "Expected '%f'. Actual '%f'.", expectedOutput, actualResult) + } +} + +func TestCalculateNetProfit(t *testing.T) { + t.Parallel() + amount := float64(5) + priceThen := float64(1) + priceNow := float64(10) + costs := float64(1) + expectedOutput := float64(44) + actualResult := CalculateNetProfit(amount, priceThen, priceNow, costs) + if expectedOutput != actualResult { + t.Errorf( + "Expected '%f'. Actual '%f'.", expectedOutput, actualResult) + } +} + +func TestRoundFloat(t *testing.T) { + t.Parallel() + // mapping of input vs expected result + testTable := map[float64]float64{ + 2.3232323: 2.32, + -2.3232323: -2.32, + } + for testInput, expectedOutput := range testTable { + actualOutput := RoundFloat(testInput, 2) + if actualOutput != expectedOutput { + t.Errorf("RoundFloat Expected '%f'. Actual '%f'.", + expectedOutput, actualOutput) + } + } +} diff --git a/common/timedmutex/timed_mutex.go b/common/timedmutex/timed_mutex.go new file mode 100644 index 00000000..6f70d762 --- /dev/null +++ b/common/timedmutex/timed_mutex.go @@ -0,0 +1,77 @@ +package timedmutex + +import ( + "sync" + "time" +) + +// NewTimedMutex creates a new timed mutex with a +// specified duration +func NewTimedMutex(length time.Duration) *TimedMutex { + return &TimedMutex{ + duration: length, + } +} + +// LockForDuration will start a timer, lock the mutex, +// then allow the caller to continue +// After the duration, the mutex will be unlocked +func (t *TimedMutex) LockForDuration() { + var wg sync.WaitGroup + wg.Add(1) + go t.lockAndSetTimer(&wg) + wg.Wait() +} + +func (t *TimedMutex) lockAndSetTimer(wg *sync.WaitGroup) { + t.mtx.Lock() + t.setTimer() + wg.Done() +} + +// UnlockIfLocked will unlock the mutex if its currently locked +// Will return true if successfully unlocked +func (t *TimedMutex) UnlockIfLocked() bool { + if t.isTimerNil() { + return false + } + + if !t.stopTimer() { + return false + } + t.mtx.Unlock() + return true +} + +// stopTimer will return true if timer has been stopped by this command +// If the timer has expired, clear the channel +func (t *TimedMutex) stopTimer() bool { + t.timerLock.Lock() + defer t.timerLock.Unlock() + if !t.timer.Stop() { + select { + case <-t.timer.C: + default: + } + return false + } + return true +} + +// isTimerNil safely read locks to detect nil +func (t *TimedMutex) isTimerNil() bool { + t.timerLock.RLock() + defer t.timerLock.RUnlock() + return t.timer == nil +} + +// setTimer safely locks and sets a timer +// which will automatically execute a mutex unlock +// once timer expires +func (t *TimedMutex) setTimer() { + t.timerLock.Lock() + t.timer = time.AfterFunc(t.duration, func() { + t.mtx.Unlock() + }) + t.timerLock.Unlock() +} diff --git a/common/timedmutex/timed_mutex_test.go b/common/timedmutex/timed_mutex_test.go new file mode 100644 index 00000000..bf9c3941 --- /dev/null +++ b/common/timedmutex/timed_mutex_test.go @@ -0,0 +1,86 @@ +package timedmutex + +import ( + "testing" + "time" +) + +func BenchmarkTimedMutexTime(b *testing.B) { + tm := NewTimedMutex(20 * time.Millisecond) + for i := 0; i < b.N; i++ { + tm.LockForDuration() + } +} + +func TestConsistencyOfPanicFreeUnlock(t *testing.T) { + t.Parallel() + duration := 20 * time.Millisecond + tm := NewTimedMutex(duration) + for i := 1; i <= 50; i++ { + testUnlockTime := time.Duration(i) * time.Millisecond + tm.LockForDuration() + time.Sleep(testUnlockTime) + tm.UnlockIfLocked() + } +} + +func TestUnlockAfterTimeout(t *testing.T) { + t.Parallel() + tm := NewTimedMutex(time.Second) + tm.LockForDuration() + time.Sleep(2 * time.Second) + wasUnlocked := tm.UnlockIfLocked() + if wasUnlocked { + t.Error("Mutex should have been unlocked by timeout, not command") + } +} + +func TestUnlockBeforeTimeout(t *testing.T) { + t.Parallel() + tm := NewTimedMutex(2 * time.Second) + tm.LockForDuration() + time.Sleep(time.Second) + wasUnlocked := tm.UnlockIfLocked() + if !wasUnlocked { + t.Error("Mutex should have been unlocked by command, not timeout") + } +} + +// TestUnlockAtSameTimeAsTimeout this test ensures +// that even if the timeout and the command occur at +// the same time, no panics occur. The result of the +// 'who' unlocking this doesn't matter, so long as +// the unlock occurs without this test panicking +func TestUnlockAtSameTimeAsTimeout(t *testing.T) { + t.Parallel() + duration := time.Second + tm := NewTimedMutex(duration) + tm.LockForDuration() + time.Sleep(duration) + tm.UnlockIfLocked() +} + +func TestMultipleUnlocks(t *testing.T) { + t.Parallel() + tm := NewTimedMutex(10 * time.Second) + tm.LockForDuration() + wasUnlocked := tm.UnlockIfLocked() + if !wasUnlocked { + t.Error("Mutex should have been unlocked by command, not timeout") + } + wasUnlocked = tm.UnlockIfLocked() + if wasUnlocked { + t.Error("Mutex should have been already unlocked by command") + } + wasUnlocked = tm.UnlockIfLocked() + if wasUnlocked { + t.Error("Mutex should have been already unlocked by command") + } +} + +func TestJustWaitItOut(t *testing.T) { + t.Parallel() + tm := NewTimedMutex(1 * time.Second) + tm.LockForDuration() + time.Sleep(2 * time.Second) +} diff --git a/common/timedmutex/timed_mutex_types.go b/common/timedmutex/timed_mutex_types.go new file mode 100644 index 00000000..841ebcdc --- /dev/null +++ b/common/timedmutex/timed_mutex_types.go @@ -0,0 +1,15 @@ +package timedmutex + +import ( + "sync" + "time" +) + +// TimedMutex is a blocking mutex which will unlock +// after a specified time +type TimedMutex struct { + mtx sync.Mutex + timerLock sync.RWMutex + timer *time.Timer + duration time.Duration +} diff --git a/communications/base/README.md b/communications/base/README.md index 1802f1df..dd3655be 100644 --- a/communications/base/README.md +++ b/communications/base/README.md @@ -44,4 +44,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/communications/base/base.go b/communications/base/base.go index 4384fcab..4090eaa1 100644 --- a/communications/base/base.go +++ b/communications/base/base.go @@ -1,55 +1,15 @@ package base import ( - "fmt" - "sync" "time" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" ) // global vars contain staged update data that will be sent to the communication // mediums var ( - TickerStaged map[string]map[string]map[string]ticker.Price - OrderbookStaged map[string]map[string]map[string]Orderbook - PortfolioStaged Portfolio - SettingsStaged Settings - ServiceStarted time.Time - m sync.Mutex + ServiceStarted time.Time ) -// Orderbook holds the minimal orderbook details to be sent to a communication -// medium -type Orderbook struct { - CurrencyPair string - AssetType string - TotalAsks float64 - TotalBids float64 - LastUpdated string -} - -// Ticker holds the minimal orderbook details to be sent to a communication -// medium -type Ticker struct { - CurrencyPair string - LastUpdated string -} - -// Portfolio holds the minimal portfolio details to be sent to a communication -// medium -type Portfolio struct { - ProfitLoss string -} - -// Settings holds the minimal setting details to be sent to a communication -// medium -type Settings struct { - EnabledExchanges string - EnabledCommunications string -} - // Base enforces standard variables across communication packages type Base struct { Name string @@ -60,9 +20,14 @@ type Base struct { // Event is a generalise event type type Event struct { - Type string - GainLoss string - TradeDetails string + Type string + Message string +} + +// CommsStatus stores the status of a comms relayer +type CommsStatus struct { + Enabled bool `json:"enabled"` + Connected bool `json:"connected"` } // IsEnabled returns if the comms package has been enabled in the configuration @@ -81,83 +46,6 @@ func (b *Base) GetName() string { return b.Name } -// GetTicker returns staged ticker data -func (b *Base) GetTicker(exchangeName string) string { - m.Lock() - defer m.Unlock() - - tickerPrice, ok := TickerStaged[exchangeName] - if !ok { - return "" - } - - var tickerPrices []ticker.Price - for x := range tickerPrice { - for y := range tickerPrice[x] { - tickerPrices = append(tickerPrices, tickerPrice[x][y]) - } - } - - var packagedTickers []string - for i := range tickerPrices { - packagedTickers = append(packagedTickers, fmt.Sprintf( - "Currency Pair: %s Ask: %f, Bid: %f High: %f Last: %f Low: %f ATH: %f Volume: %f", - tickerPrices[i].Pair, - tickerPrices[i].Ask, - tickerPrices[i].Bid, - tickerPrices[i].High, - tickerPrices[i].Last, - tickerPrices[i].Low, - tickerPrices[i].PriceATH, - tickerPrices[i].Volume)) - } - return common.JoinStrings(packagedTickers, "\n") -} - -// GetOrderbook returns staged orderbook data -func (b *Base) GetOrderbook(exchangeName string) string { - m.Lock() - defer m.Unlock() - - orderbook, ok := OrderbookStaged[exchangeName] - if !ok { - return "" - } - - var orderbooks []Orderbook - for _, x := range orderbook { - for _, y := range x { - orderbooks = append(orderbooks, y) - } - } - - var packagedOrderbooks []string - for i := range orderbooks { - packagedOrderbooks = append(packagedOrderbooks, fmt.Sprintf( - "Currency Pair: %s AssetType: %s, LastUpdated: %s TotalAsks: %f TotalBids: %f", - orderbooks[i].CurrencyPair, - orderbooks[i].AssetType, - orderbooks[i].LastUpdated, - orderbooks[i].TotalAsks, - orderbooks[i].TotalBids)) - } - return common.JoinStrings(packagedOrderbooks, "\n") -} - -// GetPortfolio returns staged portfolio info -func (b *Base) GetPortfolio() string { - m.Lock() - defer m.Unlock() - return fmt.Sprintf("%v", PortfolioStaged) -} - -// GetSettings returns stage setting info -func (b *Base) GetSettings() string { - m.Lock() - defer m.Unlock() - return fmt.Sprintf("%v", SettingsStaged) -} - // GetStatus returns status data func (b *Base) GetStatus() string { return ` diff --git a/communications/base/base_interface.go b/communications/base/base_interface.go index b4848087..e068a735 100644 --- a/communications/base/base_interface.go +++ b/communications/base/base_interface.go @@ -1,11 +1,10 @@ package base import ( + "errors" "time" "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -25,16 +24,15 @@ type ICommunicate interface { // Setup sets up communication variables and intiates a connection to the // communication mediums func (c IComm) Setup() { - TickerStaged = make(map[string]map[string]map[string]ticker.Price) - OrderbookStaged = make(map[string]map[string]map[string]Orderbook) ServiceStarted = time.Now() - for i := range c { if c[i].IsEnabled() && !c[i].IsConnected() { err := c[i].Connect() if err != nil { - log.Errorf("Communications: %s failed to connect. Err: %s", c[i].GetName(), err) + log.Errorf(log.CommunicationMgr, "Communications: %s failed to connect. Err: %s", c[i].GetName(), err) + continue } + log.Debugf(log.CommunicationMgr, "Communications: %v is enabled and online.", c[i].GetName()) } } } @@ -45,64 +43,38 @@ func (c IComm) PushEvent(event Event) { if c[i].IsEnabled() && c[i].IsConnected() { err := c[i].PushEvent(event) if err != nil { - log.Errorf("Communications error - PushEvent() in package %s with %v", - c[i].GetName(), event) + log.Errorf(log.CommunicationMgr, "Communications error - PushEvent() in package %s with %v. Err %s", + c[i].GetName(), event, err) } } } } +// GetStatus returns the status of the comms relayers +func (c IComm) GetStatus() map[string]CommsStatus { + result := make(map[string]CommsStatus) + for x := range c { + result[c[x].GetName()] = CommsStatus{ + Enabled: c[x].IsEnabled(), + Connected: c[x].IsConnected(), + } + } + return result +} + // GetEnabledCommunicationMediums prints out enabled and connected communication // packages // (#debug output only) -func (c IComm) GetEnabledCommunicationMediums() { +func (c IComm) GetEnabledCommunicationMediums() error { var count int for i := range c { if c[i].IsEnabled() && c[i].IsConnected() { - log.Debugf("Communications: Medium %s is enabled.", c[i].GetName()) + log.Debugf(log.CommunicationMgr, "Communications: Medium %s is enabled.", c[i].GetName()) count++ } } if count == 0 { - log.Warnf("Communications: No communication mediums are enabled.") + return errors.New("no communication mediums are enabled") } -} - -// StageTickerData stages updated ticker data for the communications package -func (c IComm) StageTickerData(exchangeName, assetType string, tickerPrice *ticker.Price) { - m.Lock() - defer m.Unlock() - - if _, ok := TickerStaged[exchangeName]; !ok { - TickerStaged[exchangeName] = make(map[string]map[string]ticker.Price) - } - - if _, ok := TickerStaged[exchangeName][assetType]; !ok { - TickerStaged[exchangeName][assetType] = make(map[string]ticker.Price) - } - - TickerStaged[exchangeName][assetType][tickerPrice.Pair.String()] = *tickerPrice -} - -// StageOrderbookData stages updated orderbook data for the communications -// package -func (c IComm) StageOrderbookData(exchangeName, assetType string, ob *orderbook.Base) { - m.Lock() - defer m.Unlock() - - if _, ok := OrderbookStaged[exchangeName]; !ok { - OrderbookStaged[exchangeName] = make(map[string]map[string]Orderbook) - } - - if _, ok := OrderbookStaged[exchangeName][assetType]; !ok { - OrderbookStaged[exchangeName][assetType] = make(map[string]Orderbook) - } - - _, totalAsks := ob.TotalAsksAmount() - _, totalBids := ob.TotalBidsAmount() - - OrderbookStaged[exchangeName][assetType][ob.Pair.String()] = Orderbook{ - CurrencyPair: ob.Pair.String(), - TotalAsks: totalAsks, - TotalBids: totalBids} + return nil } diff --git a/communications/base/base_test.go b/communications/base/base_test.go index 1121ed44..37f9613f 100644 --- a/communications/base/base_test.go +++ b/communications/base/base_test.go @@ -2,14 +2,10 @@ package base import ( "testing" - - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" ) var ( b Base - i IComm ) func TestStart(t *testing.T) { @@ -23,54 +19,19 @@ func TestStart(t *testing.T) { func TestIsEnabled(t *testing.T) { if !b.IsEnabled() { - t.Error("test failed - base IsEnabled() error") + t.Error("base IsEnabled() error") } } func TestIsConnected(t *testing.T) { if !b.IsConnected() { - t.Error("test failed - base IsConnected() error") + t.Error("base IsConnected() error") } } func TestGetName(t *testing.T) { if b.GetName() != "test" { - t.Error("test failed - base GetName() error") - } -} - -func TestGetTicker(t *testing.T) { - v := b.GetTicker("ANX") - if v != "" { - t.Error("test failed - base GetTicker() error") - } -} - -func TestGetOrderbook(t *testing.T) { - v := b.GetOrderbook("ANX") - if v != "" { - t.Error("test failed - base GetOrderbook() error") - } -} - -func TestGetPortfolio(t *testing.T) { - v := b.GetPortfolio() - if v != "{}" { - t.Error("test failed - base GetPortfolio() error") - } -} - -func TestGetSettings(t *testing.T) { - v := b.GetSettings() - if v != "{ }" { - t.Error("test failed - base GetSettings() error") - } -} - -func TestGetStatus(t *testing.T) { - v := b.GetStatus() - if v == "" { - t.Error("test failed - base GetStatus() error") + t.Error("base GetName() error") } } @@ -166,58 +127,3 @@ func TestPushEvent(t *testing.T) { } } } - -func TestStageTickerData(t *testing.T) { - _, ok := TickerStaged["bitstamp"]["someAsset"]["BTCUSD"] - if ok { - t.Fatalf("key should not exists") - } - - price := ticker.Price{} - var i IComm - i.Setup() - - i.StageTickerData("bitstamp", "someAsset", &price) - - _, ok = TickerStaged["bitstamp"]["someAsset"][price.Pair.String()] - if !ok { - t.Fatalf("key should exists") - } -} - -func TestOrderbookData(t *testing.T) { - _, ok := OrderbookStaged["bitstamp"]["someAsset"]["someOrderbook"] - if ok { - t.Fatal("key should not exists") - } - - ob := orderbook.Base{ - Asks: []orderbook.Item{ - {Amount: 1, Price: 2, ID: 3}, - {Amount: 4, Price: 5, ID: 6}, - }, - } - var i IComm - i.Setup() - - i.StageOrderbookData("bitstamp", "someAsset", &ob) - - orderbook, ok := OrderbookStaged["bitstamp"]["someAsset"][ob.Pair.String()] - if !ok { - t.Fatal("key should exists") - } - - if ob.Pair.String() != orderbook.CurrencyPair { - t.Fatal("currency missmatched") - } - - _, totalAsks := ob.TotalAsksAmount() - if totalAsks != orderbook.TotalAsks { - t.Fatal("total asks missmatched") - } - - _, totalBids := ob.TotalBidsAmount() - if totalBids != orderbook.TotalBids { - t.Fatal("total bids missmatched") - } -} diff --git a/communications/communications.go b/communications/communications.go index f950da6f..96439b3c 100644 --- a/communications/communications.go +++ b/communications/communications.go @@ -1,6 +1,8 @@ package communications import ( + "errors" + "github.com/thrasher-corp/gocryptotrader/communications/base" "github.com/thrasher-corp/gocryptotrader/communications/slack" "github.com/thrasher-corp/gocryptotrader/communications/smsglobal" @@ -15,9 +17,12 @@ type Communications struct { } // NewComm sets up and returns a pointer to a Communications object -func NewComm(cfg *config.CommunicationsConfig) *Communications { - var comm Communications +func NewComm(cfg *config.CommunicationsConfig) (*Communications, error) { + if !cfg.IsAnyEnabled() { + return nil, errors.New("no communication relayers enabled") + } + var comm Communications if cfg.TelegramConfig.Enabled { Telegram := new(telegram.Telegram) Telegram.Setup(cfg) @@ -43,5 +48,5 @@ func NewComm(cfg *config.CommunicationsConfig) *Communications { } comm.Setup() - return &comm + return &comm, nil } diff --git a/communications/communications_test.go b/communications/communications_test.go index 8fa1f68d..82717bc6 100644 --- a/communications/communications_test.go +++ b/communications/communications_test.go @@ -8,21 +8,22 @@ import ( func TestNewComm(t *testing.T) { var cfg config.CommunicationsConfig - communications := NewComm(&cfg) - - if len(communications.IComm) != 0 { - t.Errorf("Test failed, communications NewComm, expected len 0, got len %d", - len(communications.IComm)) + _, err := NewComm(&cfg) + if err == nil { + t.Error("NewComm should have failed on no enabled communication mediums") } cfg.TelegramConfig.Enabled = true cfg.SMSGlobalConfig.Enabled = true cfg.SMTPConfig.Enabled = true cfg.SlackConfig.Enabled = true - communications = NewComm(&cfg) + communications, err := NewComm(&cfg) + if err != nil { + t.Error("Unexpected result") + } if len(communications.IComm) != 4 { - t.Errorf("Test failed, communications NewComm, expected len 4, got len %d", + t.Errorf("communications NewComm, expected len 4, got len %d", len(communications.IComm)) } } diff --git a/communications/slack/README.md b/communications/slack/README.md index 39b1675d..b68f14d1 100644 --- a/communications/slack/README.md +++ b/communications/slack/README.md @@ -92,4 +92,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/communications/slack/slack.go b/communications/slack/slack.go index fabc361d..eb7b0b81 100644 --- a/communications/slack/slack.go +++ b/communications/slack/slack.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "strings" "sync" "time" @@ -23,21 +24,13 @@ import ( const ( SlackURL = "https://slack.com/api/rtm.start" - cmdStatus = "!status" - cmdHelp = "!help" - cmdSettings = "!settings" - cmdTicker = "!ticker" - cmdPortfolio = "!portfolio" - cmdOrderbook = "!orderbook" + cmdStatus = "!status" + cmdHelp = "!help" getHelp = `GoCryptoTrader SlackBot, thank you for using this service! Current commands are: !status - Displays current working status of bot - !help - Displays help text - !settings - Displays current settings - !ticker - Displays recent ANX ticker - !portfolio - Displays portfolio data - !orderbook - Displays current ANX orderbook` + !help - Displays help text` ) // Slack starts a websocket connection and uses https://api.slack.com/rtm real @@ -57,6 +50,11 @@ type Slack struct { sync.Mutex } +// IsConnected returns whether or not the connection is connected +func (s *Slack) IsConnected() bool { + return s.Connected +} + // Setup takes in a slack configuration, sets bots target channel and // sets verification token to access workspace func (s *Slack) Setup(cfg *config.CommunicationsConfig) { @@ -69,12 +67,22 @@ func (s *Slack) Setup(cfg *config.CommunicationsConfig) { // Connect connects to the service func (s *Slack) Connect() error { - return s.NewConnection() + err := s.NewConnection() + if err != nil { + return err + } + + s.Connected = true + return nil } // PushEvent pushes an event to either a slack channel or specific client -func (s *Slack) PushEvent(base.Event) error { - return common.ErrNotYetImplemented +func (s *Slack) PushEvent(event base.Event) error { + if s.Connected { + return s.WebsocketSend("message", + fmt.Sprintf("event: %s %s", event.Type, event.Message)) + } + return errors.New("slack not connected") } // BuildURL returns an appended token string with the SlackURL @@ -154,19 +162,22 @@ func (s *Slack) NewConnection() error { } if s.Verbose { - log.Debugf("%s [%s] connected to %s [%s] \nWebsocket URL: %s.\n", + log.Debugf(log.CommunicationMgr, "Slack: %s [%s] connected to %s [%s] \nWebsocket URL: %s.\n", s.Details.Self.Name, s.Details.Self.ID, s.Details.Team.Domain, s.Details.Team.ID, s.Details.URL) - log.Debugf("Slack channels: %s", s.GetChannelsString()) + log.Debugf(log.CommunicationMgr, "Slack: Public channels: %s\n", s.GetChannelsString()) } s.TargetChannelID, err = s.GetIDByName(s.TargetChannel) if err != nil { return err } + + log.Debugf(log.CommunicationMgr, "Slack: Target channel ID: %v [#%v]\n", s.TargetChannelID, + s.TargetChannel) return s.WebsocketConnect() } return errors.New("slack.go NewConnection() Already Connected") @@ -197,14 +208,13 @@ func (s *Slack) WebsocketReader() { for { _, resp, err := s.WebsocketConn.ReadMessage() if err != nil { - log.Error(err) + log.Errorln(log.CommunicationMgr, err) } var data WebsocketResponse - - err = common.JSONDecode(resp, &data) + err = json.Unmarshal(resp, &data) if err != nil { - log.Error(err) + log.Errorln(log.CommunicationMgr, err) continue } @@ -233,22 +243,22 @@ func (s *Slack) WebsocketReader() { } case "pong": if s.Verbose { - log.Debugf("Pong received from server") + log.Debugln(log.CommunicationMgr, "Slack: Pong received from server") } default: - log.Debugf(string(resp)) + log.Debugln(log.CommunicationMgr, string(resp)) } } } func (s *Slack) handlePresenceChange(resp []byte) error { var pres PresenceChange - err := common.JSONDecode(resp, &pres) + err := json.Unmarshal(resp, &pres) if err != nil { return err } if s.Verbose { - log.Debugf("Presence change. User %s [%s] changed status to %s\n", + log.Debugf(log.CommunicationMgr, "Slack: Presence change. User %s [%s] changed status to %s\n", s.GetUsernameByID(pres.User), pres.User, pres.Presence) } @@ -260,12 +270,12 @@ func (s *Slack) handleMessageResponse(resp []byte, data WebsocketResponse) error return errors.New("reply to is != 0") } var msg Message - err := common.JSONDecode(resp, &msg) + err := json.Unmarshal(resp, &msg) if err != nil { return err } if s.Verbose { - log.Debugf("Msg received by %s [%s] with text: %s\n", + log.Debugf(log.CommunicationMgr, "Slack: Message received by %s [%s] with text: %s\n", s.GetUsernameByID(msg.User), msg.User, msg.Text) } @@ -277,7 +287,7 @@ func (s *Slack) handleMessageResponse(resp []byte, data WebsocketResponse) error func (s *Slack) handleErrorResponse(data WebsocketResponse) error { if data.Error.Msg == "Socket URL has expired" { if s.Verbose { - log.Debugf("Slack websocket URL has expired.. Reconnecting") + log.Debugln(log.CommunicationMgr, "Slack websocket URL has expired.. Reconnecting") } if s.WebsocketConn == nil { @@ -285,7 +295,7 @@ func (s *Slack) handleErrorResponse(data WebsocketResponse) error { } if err := s.WebsocketConn.Close(); err != nil { - log.Error(err) + log.Errorln(log.CommunicationMgr, err) } s.ReconnectURL = "" @@ -297,7 +307,7 @@ func (s *Slack) handleErrorResponse(data WebsocketResponse) error { func (s *Slack) handleHelloResponse() { if s.Verbose { - log.Debugln("Websocket connected successfully.") + log.Debugln(log.CommunicationMgr, "Slack: Websocket connected successfully.") } s.Connected = true go s.WebsocketKeepAlive() @@ -308,13 +318,13 @@ func (s *Slack) handleReconnectResponse(resp []byte) error { URL string `json:"url"` } var recURL reconnectResponse - err := common.JSONDecode(resp, &recURL) + err := json.Unmarshal(resp, &recURL) if err != nil { return err } s.ReconnectURL = recURL.URL if s.Verbose { - log.Debugf("Reconnect URL set to %s\n", s.ReconnectURL) + log.Debugf(log.CommunicationMgr, "Slack: Reconnect URL set to %s\n", s.ReconnectURL) } return nil } @@ -327,7 +337,7 @@ func (s *Slack) WebsocketKeepAlive() { for { <-ticker.C if err := s.WebsocketSend("ping", ""); err != nil { - log.Debugf("slack WebsocketKeepAlive() error %s", err) + log.Errorf(log.CommunicationMgr, "Slack: WebsocketKeepAlive() error %s\n", err) } } } @@ -346,6 +356,11 @@ func (s *Slack) WebsocketSend(eventType, text string) error { if err != nil { return err } + + if s.Verbose { + log.Debugf(log.CommunicationMgr, "Slack: Sending websocket message: %s\n", string(data)) + } + if s.WebsocketConn == nil { return errors.New("websocket not connected") } @@ -355,29 +370,17 @@ func (s *Slack) WebsocketSend(eventType, text string) error { // HandleMessage handles incoming messages and/or commands from slack func (s *Slack) HandleMessage(msg *Message) error { if msg == nil { - return errors.New("msg is nil") + return errors.New("slack msg is nil") } - msg.Text = common.StringToLower(msg.Text) + msg.Text = strings.ToLower(msg.Text) switch { - case common.StringContains(msg.Text, cmdStatus): + case strings.Contains(msg.Text, cmdStatus): return s.WebsocketSend("message", s.GetStatus()) - case common.StringContains(msg.Text, cmdHelp): + case strings.Contains(msg.Text, cmdHelp): return s.WebsocketSend("message", getHelp) - case common.StringContains(msg.Text, cmdTicker): - return s.WebsocketSend("message", s.GetTicker("ANX")) - - case common.StringContains(msg.Text, cmdOrderbook): - return s.WebsocketSend("message", s.GetOrderbook("ANX")) - - case common.StringContains(msg.Text, cmdSettings): - return s.WebsocketSend("message", s.GetSettings()) - - case common.StringContains(msg.Text, cmdPortfolio): - return s.WebsocketSend("message", s.GetPortfolio()) - default: return s.WebsocketSend("message", "GoCryptoTrader SlackBot - Command Unknown!") } diff --git a/communications/slack/slack_test.go b/communications/slack/slack_test.go index 9f3fad35..5b1563c0 100644 --- a/communications/slack/slack_test.go +++ b/communications/slack/slack_test.go @@ -1,9 +1,9 @@ package slack import ( + "encoding/json" "testing" - "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/communications/base" "github.com/thrasher-corp/gocryptotrader/config" ) @@ -15,33 +15,17 @@ const ( var s Slack type group struct { - ID string `json:"id"` - Name string `json:"name"` - IsGroup bool `json:"is_group"` - Created int64 `json:"created"` - Creator string `json:"creator"` - IsArchived bool `json:"is_archived"` - NameNormalised string `json:"name_normalised"` - IsMPIM bool `json:"is_mpim"` - HasPins bool `json:"has_pins"` - IsOpen bool `json:"is_open"` - LastRead string `json:"last_read"` - Members []string `json:"members"` - Topic struct { - Value string `json:"value"` - Creator string `json:"creator"` - LastSet int64 `json:"last_set"` - } `json:"topic"` - Purpose struct { - Value string `json:"value"` - Creator string `json:"creator"` - LastSet int64 `json:"last_set"` - } `json:"purpose"` + ID string `json:"id"` + Name string `json:"name"` + Members []string `json:"members"` } func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig(config.ConfigTestFile) + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + t.Fatal(err) + } commsCfg := cfg.GetCommunicationsConfig() s.Setup(&commsCfg) @@ -51,7 +35,7 @@ func TestSetup(t *testing.T) { func TestConnect(t *testing.T) { err := s.Connect() if err == nil { - t.Error("test failed - slack Connect() error") + t.Error("slack Connect() error cannot be nil") } } @@ -59,7 +43,7 @@ func TestPushEvent(t *testing.T) { t.Parallel() err := s.PushEvent(base.Event{}) if err == nil { - t.Error("test failed - slack PushEvent() error") + t.Error("slack PushEvent() error cannot be nil") } } @@ -67,22 +51,13 @@ func TestBuildURL(t *testing.T) { t.Parallel() v := s.BuildURL("lol123") if v != "https://slack.com/api/rtm.start?token=lol123" { - t.Error("test failed - slack BuildURL() error") + t.Error("slack BuildURL() error") } } func TestGetChannelsString(t *testing.T) { s.Details.Channels = append(s.Details.Channels, struct { - Created int `json:"created"` - Creator string `json:"creator"` - HasPins bool `json:"has_pins"` ID string `json:"id"` - IsArchived bool `json:"is_archived"` - IsChannel bool `json:"is_channel"` - IsGeneral bool `json:"is_general"` - IsMember bool `json:"is_member"` - IsOrgShared bool `json:"is_org_shared"` - IsShared bool `json:"is_shared"` Name string `json:"name"` NameNormalized string `json:"name_normalized"` PreviousNames []string `json:"previous_names"` @@ -98,39 +73,20 @@ func TestGetChannelsString(t *testing.T) { } } if !testpassed { - t.Error("test failed - slack GetChannelsString() error") + t.Error("slack GetChannelsString() error") } } func TestGetUsernameByID(t *testing.T) { username := s.GetUsernameByID("1337") if username != "" { - t.Error("test failed - slack GetUsernameByID() error") + t.Error("slack GetUsernameByID() error") } s.Details.Users = append(s.Details.Users, struct { - Deleted bool `json:"deleted"` - ID string `json:"id"` - IsBot bool `json:"is_bot"` - Name string `json:"name"` - Presence string `json:"presence"` - Profile struct { - AvatarHash string `json:"avatar_hash"` - Email string `json:"email"` - Fields interface{} `json:"fields"` - FirstName string `json:"first_name"` - Image192 string `json:"image_192"` - Image24 string `json:"image_24"` - Image32 string `json:"image_32"` - Image48 string `json:"image_48"` - Image512 string `json:"image_512"` - Image72 string `json:"image_72"` - LastName string `json:"last_name"` - RealName string `json:"real_name"` - RealNameNormalized string `json:"real_name_normalized"` - } `json:"profile"` - TeamID string `json:"team_id"` - Updated int `json:"updated"` + ID string `json:"id"` + Name string `json:"name"` + TeamID string `json:"team_id"` }{ ID: "1337", Name: "cranktakular", @@ -138,14 +94,14 @@ func TestGetUsernameByID(t *testing.T) { username = s.GetUsernameByID("1337") if username != "cranktakular" { - t.Error("test failed - slack GetUsernameByID() error") + t.Error("slack GetUsernameByID() error") } } func TestGetIDByName(t *testing.T) { id, err := s.GetIDByName("batman") if err == nil || id != "" { - t.Error("test failed - slack GetIDByName() error") + t.Error("slack GetIDByName() error") } s.Details.Groups = append(s.Details.Groups, group{ @@ -154,7 +110,7 @@ func TestGetIDByName(t *testing.T) { }) id, err = s.GetIDByName("this is a group") if err != nil || id != "210314" { - t.Errorf("test failed - slack GetIDByName() Expected '210314' Actual '%s' Error: %s", + t.Errorf("slack GetIDByName() Expected '210314' Actual '%s' Error: %s", id, err) } } @@ -162,7 +118,7 @@ func TestGetIDByName(t *testing.T) { func TestGetGroupIDByName(t *testing.T) { id, err := s.GetGroupIDByName("batman") if err == nil || id != "" { - t.Error("test failed - slack GetGroupIDByName() error") + t.Error("slack GetGroupIDByName() error") } s.Details.Groups = append(s.Details.Groups, group{ @@ -171,7 +127,7 @@ func TestGetGroupIDByName(t *testing.T) { }) id, err = s.GetGroupIDByName("another group") if err != nil || id != "11223344" { - t.Errorf("test failed - slack GetGroupIDByName() Expected '11223344' Actual '%s' Error: %s", + t.Errorf("slack GetGroupIDByName() Expected '11223344' Actual '%s' Error: %s", id, err) } } @@ -179,20 +135,11 @@ func TestGetGroupIDByName(t *testing.T) { func TestGetChannelIDByName(t *testing.T) { id, err := s.GetChannelIDByName("1337") if err == nil || id != "" { - t.Error("test failed - slack GetChannelIDByName() error") + t.Error("slack GetChannelIDByName() error") } s.Details.Channels = append(s.Details.Channels, struct { - Created int `json:"created"` - Creator string `json:"creator"` - HasPins bool `json:"has_pins"` ID string `json:"id"` - IsArchived bool `json:"is_archived"` - IsChannel bool `json:"is_channel"` - IsGeneral bool `json:"is_general"` - IsMember bool `json:"is_member"` - IsOrgShared bool `json:"is_org_shared"` - IsShared bool `json:"is_shared"` Name string `json:"name"` NameNormalized string `json:"name_normalized"` PreviousNames []string `json:"previous_names"` @@ -203,7 +150,7 @@ func TestGetChannelIDByName(t *testing.T) { id, err = s.GetChannelIDByName("Slack Test") if err != nil || id != "2048" { - t.Errorf("test failed - slack GetChannelIDByName() Expected '2048' Actual '%s' Error: %s", + t.Errorf("slack GetChannelIDByName() Expected '2048' Actual '%s' Error: %s", id, err) } } @@ -211,7 +158,7 @@ func TestGetChannelIDByName(t *testing.T) { func TestGetUsersInGroup(t *testing.T) { username := s.GetUsersInGroup("supergroup") if len(username) != 0 { - t.Error("test failed - slack GetUsersInGroup() error") + t.Error("slack GetUsersInGroup() error") } s.Details.Groups = append(s.Details.Groups, group{ @@ -222,7 +169,7 @@ func TestGetUsersInGroup(t *testing.T) { username = s.GetUsersInGroup("three guys") if len(username) != 3 { - t.Errorf("test failed - slack GetUsersInGroup() Expected '3' Actual '%s'", + t.Errorf("slack GetUsersInGroup() Expected '3' Actual '%s'", username) } } @@ -230,14 +177,14 @@ func TestGetUsersInGroup(t *testing.T) { func TestNewConnection(t *testing.T) { err := s.NewConnection() if err == nil { - t.Error("test failed - slack NewConnection() error") + t.Error("slack NewConnection() error") } } func TestWebsocketConnect(t *testing.T) { err := s.WebsocketConnect() if err == nil { - t.Error("test failed - slack WebsocketConnect() error") + t.Error("slack WebsocketConnect() error") } } @@ -248,13 +195,13 @@ func TestHandlePresenceChange(t *testing.T) { err := s.handlePresenceChange([]byte(`{"malformedjson}`)) if err == nil { - t.Error("test failed - slack handlePresenceChange(), unmarshalled malformed json") + t.Error("slack handlePresenceChange(), unmarshalled malformed json") } - data, _ := common.JSONEncode(pres) + data, _ := json.Marshal(pres) err = s.handlePresenceChange(data) if err != nil { - t.Errorf("test failed - slack handlePresenceChange() Error: %s", err) + t.Errorf("slack handlePresenceChange() Error: %s", err) } } @@ -264,7 +211,7 @@ func TestHandleMessageResponse(t *testing.T) { err := s.handleMessageResponse(nil, data) if err.Error() != "reply to is != 0" { - t.Errorf("test failed - slack handleMessageResponse(), Incorrect Error: %s", + t.Errorf("slack handleMessageResponse(), Incorrect Error: %s", err) } @@ -272,25 +219,25 @@ func TestHandleMessageResponse(t *testing.T) { err = s.handleMessageResponse([]byte(`{"malformedjson}`), data) if err == nil { - t.Error("test failed - slack handleMessageResponse(), unmarshalled malformed json") + t.Error("slack handleMessageResponse(), unmarshalled malformed json") } var msg Message msg.User = "1337" msg.Text = "Hello World!" - resp, _ := common.JSONEncode(msg) + resp, _ := json.Marshal(msg) err = s.handleMessageResponse(resp, data) if err != nil { - t.Error("test failed - slack HandleMessage(), Sent message through nil websocket") + t.Error("slack HandleMessage(), Sent message through nil websocket") } msg.Text = "!notacommand" - resp, _ = common.JSONEncode(msg) + resp, _ = json.Marshal(msg) err = s.handleMessageResponse(resp, data) if err == nil { - t.Errorf("test failed - slack handleMessageResponse() Error: %s", err) + t.Errorf("slack handleMessageResponse() Expected error") } } @@ -298,13 +245,13 @@ func TestHandleErrorResponse(t *testing.T) { var data WebsocketResponse err := s.handleErrorResponse(data) if err == nil { - t.Error("test failed - slack handleErrorResponse() Ignored strange input") + t.Error("slack handleErrorResponse() Ignored strange input") } data.Error.Msg = "Socket URL has expired" err = s.handleErrorResponse(data) if err == nil { - t.Error("test failed - slack handleErrorResponse() Didn't error on nil websocket") + t.Error("slack handleErrorResponse() Didn't error on nil websocket") } } @@ -315,7 +262,7 @@ func TestHandleHelloResponse(t *testing.T) { func TestHandleReconnectResponse(t *testing.T) { err := s.handleReconnectResponse([]byte(`{"malformedjson}`)) if err == nil { - t.Error("test failed - slack handleReconnectResponse(), unmarshalled malformed json") + t.Error("slack handleReconnectResponse(), unmarshalled malformed json") } var testURL struct { @@ -323,11 +270,11 @@ func TestHandleReconnectResponse(t *testing.T) { } testURL.URL = "https://www.thrasher.io" - data, _ := common.JSONEncode(testURL) + data, _ := json.Marshal(testURL) err = s.handleReconnectResponse(data) if err != nil || s.ReconnectURL != "https://www.thrasher.io" { - t.Errorf("test failed - slack handleReconnectResponse() Expected 'https://www.thrasher.io' Actual '%s' Error: %s", + t.Errorf("slack handleReconnectResponse() Expected 'https://www.thrasher.io' Actual '%s' Error: %s", s.ReconnectURL, err) } } @@ -335,45 +282,24 @@ func TestHandleReconnectResponse(t *testing.T) { func TestWebsocketSend(t *testing.T) { err := s.WebsocketSend("test", "Hello World!") if err == nil { - t.Error("test failed - slack WebsocketSend(), Sent message through nil websocket") + t.Error("slack WebsocketSend(), Sent message through nil websocket") } } func TestHandleMessage(t *testing.T) { msg := &Message{} - err := s.HandleMessage(msg) if err == nil { - t.Error("test failed - slack HandleMessage(), Sent message through nil websocket") + t.Error("slack HandleMessage(), Sent message through nil websocket") } msg.Text = cmdStatus err = s.HandleMessage(msg) if err == nil { - t.Error("test failed - slack HandleMessage(), Sent message through nil websocket") + t.Error("slack HandleMessage(), Sent message through nil websocket") } msg.Text = cmdHelp err = s.HandleMessage(msg) if err == nil { - t.Error("test failed - slack HandleMessage(), Sent message through nil websocket") - } - msg.Text = cmdTicker - err = s.HandleMessage(msg) - if err == nil { - t.Error("test failed - slack HandleMessage(), Sent message through nil websocket") - } - msg.Text = cmdOrderbook - err = s.HandleMessage(msg) - if err == nil { - t.Error("test failed - slack HandleMessage(), Sent message through nil websocket") - } - msg.Text = cmdSettings - err = s.HandleMessage(msg) - if err == nil { - t.Error("test failed - slack HandleMessage(), Sent message through nil websocket") - } - msg.Text = cmdPortfolio - err = s.HandleMessage(msg) - if err == nil { - t.Error("test failed - slack HandleMessage(), Sent message through nil websocket") + t.Error("slack HandleMessage(), Sent message through nil websocket") } } diff --git a/communications/slack/slack_types.go b/communications/slack/slack_types.go index 07358656..e738aab2 100644 --- a/communications/slack/slack_types.go +++ b/communications/slack/slack_types.go @@ -36,406 +36,32 @@ type PresenceChange struct { // Response is a generalised response type type Response struct { - Bots []struct { - AppID string `json:"app_id"` - Deleted bool `json:"deleted"` - Icons struct { - Image36 string `json:"image_36"` - Image48 string `json:"image_48"` - Image72 string `json:"image_72"` - } `json:"icons"` - ID string `json:"id"` - Name string `json:"name"` - Updated int `json:"updated"` - } `json:"bots"` - CacheTs int `json:"cache_ts"` - CacheTsVersion string `json:"cache_ts_version"` - CacheVersion string `json:"cache_version"` - CanManageSharedChannels bool `json:"can_manage_shared_channels"` - Channels []struct { - Created int `json:"created"` - Creator string `json:"creator"` - HasPins bool `json:"has_pins"` + Channels []struct { ID string `json:"id"` - IsArchived bool `json:"is_archived"` - IsChannel bool `json:"is_channel"` - IsGeneral bool `json:"is_general"` - IsMember bool `json:"is_member"` - IsOrgShared bool `json:"is_org_shared"` - IsShared bool `json:"is_shared"` Name string `json:"name"` NameNormalized string `json:"name_normalized"` PreviousNames []string `json:"previous_names"` } `json:"channels"` - Dnd struct { - DndEnabled bool `json:"dnd_enabled"` - NextDndEndTs int `json:"next_dnd_end_ts"` - NextDndStartTs int `json:"next_dnd_start_ts"` - SnoozeEnabled bool `json:"snooze_enabled"` - } `json:"dnd"` Groups []struct { - ID string `json:"id"` - Name string `json:"name"` - IsGroup bool `json:"is_group"` - Created int64 `json:"created"` - Creator string `json:"creator"` - IsArchived bool `json:"is_archived"` - NameNormalised string `json:"name_normalised"` - IsMPIM bool `json:"is_mpim"` - HasPins bool `json:"has_pins"` - IsOpen bool `json:"is_open"` - LastRead string `json:"last_read"` - Members []string `json:"members"` - Topic struct { - Value string `json:"value"` - Creator string `json:"creator"` - LastSet int64 `json:"last_set"` - } `json:"topic"` - Purpose struct { - Value string `json:"value"` - Creator string `json:"creator"` - LastSet int64 `json:"last_set"` - } `json:"purpose"` + ID string `json:"id"` + Name string `json:"name"` + Members []string `json:"members"` } `json:"groups"` - Ims []struct { - Created int `json:"created"` - HasPins bool `json:"has_pins"` - ID string `json:"id"` - IsIm bool `json:"is_im"` - IsOpen bool `json:"is_open"` - IsOrgShared bool `json:"is_org_shared"` - LastRead string `json:"last_read"` - User string `json:"user"` - } `json:"ims"` - LatestEventTs string `json:"latest_event_ts"` - Ok bool `json:"ok"` - Error string `json:"error"` - ReadOnlyChannels []interface{} `json:"read_only_channels"` - Self struct { - Created int `json:"created"` - ID string `json:"id"` - ManualPresence string `json:"manual_presence"` - Name string `json:"name"` - Prefs struct { - A11yAnimations bool `json:"a11y_animations"` - A11yFontSize string `json:"a11y_font_size"` - AllChannelsLoud bool `json:"all_channels_loud"` - AllNotificationsPrefs string `json:"all_notifications_prefs"` - AllUnreadsSortOrder string `json:"all_unreads_sort_order"` - AllowCallsToSetCurrentStatus bool `json:"allow_calls_to_set_current_status"` - AnalyticsUpsellCoachmarkSeen bool `json:"analytics_upsell_coachmark_seen"` - ArrowHistory bool `json:"arrow_history"` - AtChannelSuppressedChannels string `json:"at_channel_suppressed_channels"` - BoxEnabled bool `json:"box_enabled"` - ChannelSort string `json:"channel_sort"` - ClientLogsPri string `json:"client_logs_pri"` - ColorNamesInList bool `json:"color_names_in_list"` - ConfirmClearAllUnreads bool `json:"confirm_clear_all_unreads"` - ConfirmShCallStart bool `json:"confirm_sh_call_start"` - ConfirmUserMarkedAway bool `json:"confirm_user_marked_away"` - ConvertEmoticons bool `json:"convert_emoticons"` - DisplayDisplayNames bool `json:"display_display_names"` - DisplayRealNamesOverride int `json:"display_real_names_override"` - DndEnabled bool `json:"dnd_enabled"` - DndEndHour string `json:"dnd_end_hour"` - DndStartHour string `json:"dnd_start_hour"` - DropboxEnabled bool `json:"dropbox_enabled"` - EmailAlerts string `json:"email_alerts"` - EmailAlertsSleepUntil int `json:"email_alerts_sleep_until"` - EmailMisc bool `json:"email_misc"` - EmailWeekly bool `json:"email_weekly"` - EmojiAutocompleteBig bool `json:"emoji_autocomplete_big"` - EmojiMode string `json:"emoji_mode"` - EmojiUse string `json:"emoji_use"` - EnableReactEmojiPicker bool `json:"enable_react_emoji_picker"` - EnableUnreadView bool `json:"enable_unread_view"` - EnhancedDebugging bool `json:"enhanced_debugging"` - EnterIsSpecialInTbt bool `json:"enter_is_special_in_tbt"` - EnterpriseMdmCustomMsg string `json:"enterprise_mdm_custom_msg"` - EnterpriseMigrationSeen bool `json:"enterprise_migration_seen"` - ExpandInlineImgs bool `json:"expand_inline_imgs"` - ExpandInternalInlineImgs bool `json:"expand_internal_inline_imgs"` - ExpandNonMediaAttachments bool `json:"expand_non_media_attachments"` - ExpandSnippets bool `json:"expand_snippets"` - FKeySearch bool `json:"f_key_search"` - FlannelServerPool string `json:"flannel_server_pool"` - FrecencyEntJumper string `json:"frecency_ent_jumper"` - FrecencyJumper string `json:"frecency_jumper"` - FullTextExtracts bool `json:"full_text_extracts"` - FullerTimestamps bool `json:"fuller_timestamps"` - GdriveAuthed bool `json:"gdrive_authed"` - GdriveEnabled bool `json:"gdrive_enabled"` - GraphicEmoticons bool `json:"graphic_emoticons"` - GrowlsEnabled bool `json:"growls_enabled"` - GrowthMsgLimitApproachingCtaCount int `json:"growth_msg_limit_approaching_cta_count"` - GrowthMsgLimitApproachingCtaTs int `json:"growth_msg_limit_approaching_cta_ts"` - GrowthMsgLimitLongReachedCtaCount int `json:"growth_msg_limit_long_reached_cta_count"` - GrowthMsgLimitLongReachedCtaLastTs int `json:"growth_msg_limit_long_reached_cta_last_ts"` - GrowthMsgLimitReachedCtaCount int `json:"growth_msg_limit_reached_cta_count"` - GrowthMsgLimitReachedCtaLastTs int `json:"growth_msg_limit_reached_cta_last_ts"` - HasCreatedChannel bool `json:"has_created_channel"` - HasInvited bool `json:"has_invited"` - HasSearched bool `json:"has_searched"` - HasUploaded bool `json:"has_uploaded"` - HideHexSwatch bool `json:"hide_hex_swatch"` - HideUserGroupInfoPane bool `json:"hide_user_group_info_pane"` - HighlightWords string `json:"highlight_words"` - IntroToAppsMessageSeen bool `json:"intro_to_apps_message_seen"` - Jumbomoji bool `json:"jumbomoji"` - KKeyOmnibox bool `json:"k_key_omnibox"` - KKeyOmniboxAutoHideCount int `json:"k_key_omnibox_auto_hide_count"` - LastSeenAtChannelWarning int `json:"last_seen_at_channel_warning"` - LastSnippetType string `json:"last_snippet_type"` - LastTosAcknowledged interface{} `json:"last_tos_acknowledged"` - LoadLato2 bool `json:"load_lato_2"` - Locale string `json:"locale"` - LoudChannels string `json:"loud_channels"` - LoudChannelsSet string `json:"loud_channels_set"` - LsDisabled bool `json:"ls_disabled"` - MacSsbBounce string `json:"mac_ssb_bounce"` - MacSsbBullet bool `json:"mac_ssb_bullet"` - MarkMsgsReadImmediately bool `json:"mark_msgs_read_immediately"` - MeasureCSSUsage bool `json:"measure_css_usage"` - MentionsExcludeAtChannels bool `json:"mentions_exclude_at_channels"` - MentionsExcludeAtUserGroups bool `json:"mentions_exclude_at_user_groups"` - MessagesTheme string `json:"messages_theme"` - MsgPreview bool `json:"msg_preview"` - MsgPreviewPersistent bool `json:"msg_preview_persistent"` - MuteSounds bool `json:"mute_sounds"` - MutedChannels string `json:"muted_channels"` - NeverChannels string `json:"never_channels"` - NewMsgSnd string `json:"new_msg_snd"` - NewxpSeenLastMessage int `json:"newxp_seen_last_message"` - NoCreatedOverlays bool `json:"no_created_overlays"` - NoInvitesWidgetInSidebar bool `json:"no_invites_widget_in_sidebar"` - NoJoinedOverlays bool `json:"no_joined_overlays"` - NoMacelectronBanner bool `json:"no_macelectron_banner"` - NoMacssb1Banner bool `json:"no_macssb1_banner"` - NoMacssb2Banner bool `json:"no_macssb2_banner"` - NoOmniboxInChannels bool `json:"no_omnibox_in_channels"` - NoTextInNotifications bool `json:"no_text_in_notifications"` - NoWinssb1Banner bool `json:"no_winssb1_banner"` - ObeyInlineImgLimit bool `json:"obey_inline_img_limit"` - OnboardingCancelled bool `json:"onboarding_cancelled"` - OnboardingSlackbotConversationStep int `json:"onboarding_slackbot_conversation_step"` - OverloadedMessageEnabled bool `json:"overloaded_message_enabled"` - PagekeysHandled bool `json:"pagekeys_handled"` - PostsFormattingGuide bool `json:"posts_formatting_guide"` - PreferredSkinTone string `json:"preferred_skin_tone"` - PrevNextBtn bool `json:"prev_next_btn"` - PrivacyPolicySeen bool `json:"privacy_policy_seen"` - PromptedForEmailDisabling bool `json:"prompted_for_email_disabling"` - PushAtChannelSuppressedChannels string `json:"push_at_channel_suppressed_channels"` - PushDmAlert bool `json:"push_dm_alert"` - PushEverything bool `json:"push_everything"` - PushIdleWait int `json:"push_idle_wait"` - PushLoudChannels string `json:"push_loud_channels"` - PushLoudChannelsSet string `json:"push_loud_channels_set"` - PushMentionAlert bool `json:"push_mention_alert"` - PushMentionChannels string `json:"push_mention_channels"` - PushShowPreview bool `json:"push_show_preview"` - PushSound string `json:"push_sound"` - QuestsEnabled bool `json:"quests_enabled"` - RequireAt bool `json:"require_at"` - SearchExcludeBots bool `json:"search_exclude_bots"` - SearchExcludeChannels string `json:"search_exclude_channels"` - SearchOnlyCurrentTeam bool `json:"search_only_current_team"` - SearchOnlyMyChannels bool `json:"search_only_my_channels"` - SearchSort string `json:"search_sort"` - SeenAppSpaceCoachmark bool `json:"seen_app_space_coachmark"` - SeenAppSpaceTutorial bool `json:"seen_app_space_tutorial"` - SeenCallsSsMainCoachmark bool `json:"seen_calls_ss_main_coachmark"` - SeenCallsSsWindowCoachmark bool `json:"seen_calls_ss_window_coachmark"` - SeenCallsVideoBetaCoachmark bool `json:"seen_calls_video_beta_coachmark"` - SeenCallsVideoGaCoachmark bool `json:"seen_calls_video_ga_coachmark"` - SeenCustomStatusBadge bool `json:"seen_custom_status_badge"` - SeenCustomStatusCallout bool `json:"seen_custom_status_callout"` - SeenDomainInviteReminder bool `json:"seen_domain_invite_reminder"` - SeenGdriveCoachmark bool `json:"seen_gdrive_coachmark"` - SeenGuestAdminSlackbotAnnouncement bool `json:"seen_guest_admin_slackbot_announcement"` - SeenHighlightsArrowsCoachmark bool `json:"seen_highlights_arrows_coachmark"` - SeenHighlightsCoachmark bool `json:"seen_highlights_coachmark"` - SeenHighlightsWarmWelcome bool `json:"seen_highlights_warm_welcome"` - SeenIntlChannelNamesCoachmark bool `json:"seen_intl_channel_names_coachmark"` - SeenMemberInviteReminder bool `json:"seen_member_invite_reminder"` - SeenOnboardingChannels bool `json:"seen_onboarding_channels"` - SeenOnboardingDirectMessages bool `json:"seen_onboarding_direct_messages"` - SeenOnboardingInvites bool `json:"seen_onboarding_invites"` - SeenOnboardingPrivateGroups bool `json:"seen_onboarding_private_groups"` - SeenOnboardingRecentMentions bool `json:"seen_onboarding_recent_mentions"` - SeenOnboardingSearch bool `json:"seen_onboarding_search"` - SeenOnboardingSlackbotConversation bool `json:"seen_onboarding_slackbot_conversation"` - SeenOnboardingStarredItems bool `json:"seen_onboarding_starred_items"` - SeenOnboardingStart bool `json:"seen_onboarding_start"` - SeenRepliesCoachmark bool `json:"seen_replies_coachmark"` - SeenSingleEmojiMsg bool `json:"seen_single_emoji_msg"` - SeenSsbPrompt bool `json:"seen_ssb_prompt"` - SeenThreadsNotificationBanner bool `json:"seen_threads_notification_banner"` - SeenUnreadViewCoachmark bool `json:"seen_unread_view_coachmark"` - SeenWelcome2 bool `json:"seen_welcome_2"` - SeparatePrivateChannels bool `json:"separate_private_channels"` - SeparateSharedChannels bool `json:"separate_shared_channels"` - ShowAllSkinTones bool `json:"show_all_skin_tones"` - ShowJumperScores bool `json:"show_jumper_scores"` - ShowMemoryInstrument bool `json:"show_memory_instrument"` - ShowTyping bool `json:"show_typing"` - SidebarBehavior string `json:"sidebar_behavior"` - SidebarTheme string `json:"sidebar_theme"` - SidebarThemeCustomValues string `json:"sidebar_theme_custom_values"` - SnippetEditorWrapLongLines bool `json:"snippet_editor_wrap_long_lines"` - SpacesNewXpBannerDismissed bool `json:"spaces_new_xp_banner_dismissed"` - SsEmojis bool `json:"ss_emojis"` - SsbSpaceWindow string `json:"ssb_space_window"` - StartScrollAtOldest bool `json:"start_scroll_at_oldest"` - TabUIReturnSelects bool `json:"tab_ui_return_selects"` - ThreadsEverything bool `json:"threads_everything"` - Time24 bool `json:"time24"` - TwoFactorAuthEnabled bool `json:"two_factor_auth_enabled"` - TwoFactorBackupType interface{} `json:"two_factor_backup_type"` - TwoFactorType interface{} `json:"two_factor_type"` - Tz interface{} `json:"tz"` - UseReactSidebar bool `json:"use_react_sidebar"` - UserColors string `json:"user_colors"` - WebappSpellcheck bool `json:"webapp_spellcheck"` - WelcomeMessageHidden bool `json:"welcome_message_hidden"` - WhatsNewRead int `json:"whats_new_read"` - WinssbRunFromTray bool `json:"winssb_run_from_tray"` - WinssbWindowFlashBehavior string `json:"winssb_window_flash_behavior"` - } `json:"prefs"` + Ok bool `json:"ok"` + Error string `json:"error"` + Self struct { + ID string `json:"id"` + Name string `json:"name"` } `json:"self"` - Subteams struct { - All []interface{} `json:"all"` - Self []interface{} `json:"self"` - } `json:"subteams"` Team struct { - ApproachingMsgLimit bool `json:"approaching_msg_limit"` - AvatarBaseURL string `json:"avatar_base_url"` - Domain string `json:"domain"` - EmailDomain string `json:"email_domain"` - Icon struct { - Image102 string `json:"image_102"` - Image132 string `json:"image_132"` - Image230 string `json:"image_230"` - Image34 string `json:"image_34"` - Image44 string `json:"image_44"` - Image68 string `json:"image_68"` - Image88 string `json:"image_88"` - ImageOriginal string `json:"image_original"` - } `json:"icon"` - ID string `json:"id"` - MessagesCount int `json:"messages_count"` - MsgEditWindowMins int `json:"msg_edit_window_mins"` - Name string `json:"name"` - OverIntegrationsLimit bool `json:"over_integrations_limit"` - OverStorageLimit bool `json:"over_storage_limit"` - Plan string `json:"plan"` - Prefs struct { - AllowCalls bool `json:"allow_calls"` - AllowMessageDeletion bool `json:"allow_message_deletion"` - AllowRetentionOverride bool `json:"allow_retention_override"` - AllowSharedChannelPermsOverride bool `json:"allow_shared_channel_perms_override"` - AuthMode string `json:"auth_mode"` - CallingAppName string `json:"calling_app_name"` - ChannelHandyRxns interface{} `json:"channel_handy_rxns"` - ComplianceExportStart int `json:"compliance_export_start"` - CustomStatusDefaultEmoji string `json:"custom_status_default_emoji"` - CustomStatusPresets [][]string `json:"custom_status_presets"` - DefaultChannels []string `json:"default_channels"` - DefaultRxns []string `json:"default_rxns"` - DisableFileDeleting bool `json:"disable_file_deleting"` - DisableFileEditing bool `json:"disable_file_editing"` - DisableFileUploads string `json:"disable_file_uploads"` - DisallowPublicFileUrls bool `json:"disallow_public_file_urls"` - Discoverable string `json:"discoverable"` - DisplayEmailAddresses bool `json:"display_email_addresses"` - DisplayRealNames bool `json:"display_real_names"` - DmRetentionDuration int `json:"dm_retention_duration"` - DmRetentionType int `json:"dm_retention_type"` - DndEnabled bool `json:"dnd_enabled"` - DndEndHour string `json:"dnd_end_hour"` - DndStartHour string `json:"dnd_start_hour"` - EnterpriseDefaultChannels []interface{} `json:"enterprise_default_channels"` - EnterpriseMandatoryChannels []interface{} `json:"enterprise_mandatory_channels"` - EnterpriseMdmDateEnabled int `json:"enterprise_mdm_date_enabled"` - EnterpriseMdmLevel int `json:"enterprise_mdm_level"` - EnterpriseTeamCreationRequest struct { - IsEnabled bool `json:"is_enabled"` - } `json:"enterprise_team_creation_request"` - FileRetentionDuration int `json:"file_retention_duration"` - FileRetentionType int `json:"file_retention_type"` - GdriveEnabledTeam bool `json:"gdrive_enabled_team"` - GroupRetentionDuration int `json:"group_retention_duration"` - GroupRetentionType int `json:"group_retention_type"` - HideReferers bool `json:"hide_referers"` - InvitesLimit bool `json:"invites_limit"` - InvitesOnlyAdmins bool `json:"invites_only_admins"` - LimitReachedTs int `json:"limit_reached_ts"` - Locale string `json:"locale"` - LoudChannelMentionsLimit int `json:"loud_channel_mentions_limit"` - MsgEditWindowMins int `json:"msg_edit_window_mins"` - RequireAtForMention bool `json:"require_at_for_mention"` - RetentionDuration int `json:"retention_duration"` - RetentionType int `json:"retention_type"` - ShowJoinLeave bool `json:"show_join_leave"` - TeamHandyRxns struct { - List []struct { - Name string `json:"name"` - Title string `json:"title"` - } `json:"list"` - Restrict bool `json:"restrict"` - } `json:"team_handy_rxns"` - UsesCustomizedCustomStatusPresets bool `json:"uses_customized_custom_status_presets"` - WarnBeforeAtChannel string `json:"warn_before_at_channel"` - WhoCanArchiveChannels string `json:"who_can_archive_channels"` - WhoCanAtChannel string `json:"who_can_at_channel"` - WhoCanAtEveryone string `json:"who_can_at_everyone"` - WhoCanChangeTeamProfile string `json:"who_can_change_team_profile"` - WhoCanCreateChannels string `json:"who_can_create_channels"` - WhoCanCreateDeleteUserGroups string `json:"who_can_create_delete_user_groups"` - WhoCanCreateGroups string `json:"who_can_create_groups"` - WhoCanCreateSharedChannels string `json:"who_can_create_shared_channels"` - WhoCanEditUserGroups string `json:"who_can_edit_user_groups"` - WhoCanKickChannels string `json:"who_can_kick_channels"` - WhoCanKickGroups string `json:"who_can_kick_groups"` - WhoCanManageGuests struct { - Type []string `json:"type"` - } `json:"who_can_manage_guests"` - WhoCanManageIntegrations struct { - Type []string `json:"type"` - } `json:"who_can_manage_integrations"` - WhoCanManageSharedChannels struct { - Type []string `json:"type"` - } `json:"who_can_manage_shared_channels"` - WhoCanPostGeneral string `json:"who_can_post_general"` - WhoCanPostInSharedChannels struct { - Type []string `json:"type"` - } `json:"who_can_post_in_shared_channels"` - WhoHasTeamVisibility string `json:"who_has_team_visibility"` - } `json:"prefs"` + Domain string `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` } `json:"team"` URL string `json:"url"` Users []struct { - Deleted bool `json:"deleted"` - ID string `json:"id"` - IsBot bool `json:"is_bot"` - Name string `json:"name"` - Presence string `json:"presence"` - Profile struct { - AvatarHash string `json:"avatar_hash"` - Email string `json:"email"` - Fields interface{} `json:"fields"` - FirstName string `json:"first_name"` - Image192 string `json:"image_192"` - Image24 string `json:"image_24"` - Image32 string `json:"image_32"` - Image48 string `json:"image_48"` - Image512 string `json:"image_512"` - Image72 string `json:"image_72"` - LastName string `json:"last_name"` - RealName string `json:"real_name"` - RealNameNormalized string `json:"real_name_normalized"` - } `json:"profile"` - TeamID string `json:"team_id"` - Updated int `json:"updated"` + ID string `json:"id"` + Name string `json:"name"` + TeamID string `json:"team_id"` } `json:"users"` } diff --git a/communications/smsglobal/README.md b/communications/smsglobal/README.md index 04cf4cb4..938ef324 100644 --- a/communications/smsglobal/README.md +++ b/communications/smsglobal/README.md @@ -77,4 +77,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/communications/smsglobal/smsglobal.go b/communications/smsglobal/smsglobal.go index e070609a..269afb37 100644 --- a/communications/smsglobal/smsglobal.go +++ b/communications/smsglobal/smsglobal.go @@ -11,6 +11,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/communications/base" "github.com/thrasher-corp/gocryptotrader/config" + log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( @@ -39,6 +40,7 @@ func (s *SMSGlobal) Setup(cfg *config.CommunicationsConfig) { s.Verbose = cfg.SMSGlobalConfig.Verbose s.Username = cfg.SMSGlobalConfig.Username s.Password = cfg.SMSGlobalConfig.Password + s.SendFrom = cfg.SMSGlobalConfig.From var contacts []Contact for x := range cfg.SMSGlobalConfig.Contacts { @@ -49,10 +51,19 @@ func (s *SMSGlobal) Setup(cfg *config.CommunicationsConfig) { Enabled: cfg.SMSGlobalConfig.Contacts[x].Enabled, }, ) + log.Debugf(log.CommunicationMgr, "SMSGlobal: SMS Contact: %s. Number: %s. Enabled: %v\n", + cfg.SMSGlobalConfig.Contacts[x].Name, + cfg.SMSGlobalConfig.Contacts[x].Number, + cfg.SMSGlobalConfig.Contacts[x].Enabled) } s.Contacts = contacts } +// IsConnected returns whether or not the connection is connected +func (s *SMSGlobal) IsConnected() bool { + return s.Connected +} + // Connect connects to the service func (s *SMSGlobal) Connect() error { s.Connected = true @@ -60,8 +71,8 @@ func (s *SMSGlobal) Connect() error { } // PushEvent pushes an event to a contact list via SMS -func (s *SMSGlobal) PushEvent(base.Event) error { - return common.ErrNotYetImplemented +func (s *SMSGlobal) PushEvent(event base.Event) error { + return s.SendMessageToAll(event.Message) } // GetEnabledContacts returns how many SMS contacts are enabled in the @@ -89,7 +100,7 @@ func (s *SMSGlobal) GetContactByNumber(number string) (Contact, error) { // GetContactByName returns a contact with supplied name func (s *SMSGlobal) GetContactByName(name string) (Contact, error) { for x := range s.Contacts { - if common.StringToLower(s.Contacts[x].Name) == common.StringToLower(name) { + if strings.EqualFold(s.Contacts[x].Name, name) { return s.Contacts[x], nil } } @@ -113,7 +124,7 @@ func (s *SMSGlobal) AddContact(contact Contact) error { // ContactExists checks to see if a contact exists func (s *SMSGlobal) ContactExists(contact Contact) bool { for x := range s.Contacts { - if s.Contacts[x].Number == contact.Number && common.StringToLower(s.Contacts[x].Name) == common.StringToLower(contact.Name) { + if s.Contacts[x].Number == contact.Number && strings.EqualFold(s.Contacts[x].Name, contact.Name) { return true } } @@ -139,6 +150,10 @@ func (s *SMSGlobal) RemoveContact(contact Contact) error { func (s *SMSGlobal) SendMessageToAll(message string) error { for x := range s.Contacts { if s.Contacts[x].Enabled { + if s.Verbose { + log.Debugf(log.CommunicationMgr, "SMSGlobal: Sending SMS to %s. Number: %s. Message: %s [From: %s]\n", + s.Contacts[x].Name, s.Contacts[x].Number, message, s.SendFrom) + } err := s.SendMessage(s.Contacts[x].Number, message) if err != nil { return err @@ -173,7 +188,7 @@ func (s *SMSGlobal) SendMessage(to, message string) error { return err } - if !common.StringContains(resp, "OK: 0; Sent queued message") { + if !strings.Contains(resp, "OK: 0; Sent queued message") { return errSMSNotSent } return nil diff --git a/communications/smsglobal/smsglobal_test.go b/communications/smsglobal/smsglobal_test.go index ae728730..0db1298f 100644 --- a/communications/smsglobal/smsglobal_test.go +++ b/communications/smsglobal/smsglobal_test.go @@ -11,7 +11,10 @@ var s SMSGlobal func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + t.Fatal(err) + } commsCfg := cfg.GetCommunicationsConfig() s.Setup(&commsCfg) } @@ -19,82 +22,82 @@ func TestSetup(t *testing.T) { func TestConnect(t *testing.T) { err := s.Connect() if err != nil { - t.Error("test failed - SMSGlobal Connect() error") + t.Error("SMSGlobal Connect() error", err) } } func TestPushEvent(t *testing.T) { err := s.PushEvent(base.Event{}) - if err == nil { - t.Error("test failed - SMSGlobal PushEvent() error") + if err != nil { + t.Error("SMSGlobal PushEvent() error", err) } } func TestGetEnabledContacts(t *testing.T) { v := s.GetEnabledContacts() if v != 1 { - t.Error("test failed - SMSGlobal GetEnabledContacts() error") + t.Error("SMSGlobal GetEnabledContacts() error") } } func TestGetContactByNumber(t *testing.T) { _, err := s.GetContactByNumber("1231424") if err != nil { - t.Error("test failed - SMSGlobal GetContactByNumber() error", err) + t.Error("SMSGlobal GetContactByNumber() error", err) } _, err = s.GetContactByNumber("basketball") if err == nil { - t.Error("test failed - SMSGlobal GetContactByNumber() error") + t.Error("SMSGlobal GetContactByNumber() error") } } func TestGetContactByName(t *testing.T) { _, err := s.GetContactByName("StyleGherkin") if err != nil { - t.Error("test failed - SMSGlobal GetContactByName() error", err) + t.Error("SMSGlobal GetContactByName() error", err) } _, err = s.GetContactByName("blah") if err == nil { - t.Error("test failed - SMSGlobal GetContactByName() error") + t.Error("SMSGlobal GetContactByName() error") } } func TestAddContact(t *testing.T) { err := s.AddContact(Contact{Name: "bra", Number: "2876", Enabled: true}) if err != nil { - t.Error("test failed - SMSGlobal AddContact() error", err) + t.Error("SMSGlobal AddContact() error", err) } err = s.AddContact(Contact{Name: "StyleGherkin", Number: "1231424", Enabled: true}) if err == nil { - t.Error("test failed - SMSGlobal AddContact() error") + t.Error("SMSGlobal AddContact() error") } err = s.AddContact(Contact{Name: "", Number: "", Enabled: true}) if err == nil { - t.Error("test failed - SMSGlobal AddContact() error") + t.Error("SMSGlobal AddContact() error") } } func TestRemoveContact(t *testing.T) { err := s.RemoveContact(Contact{Name: "StyleGherkin", Number: "1231424", Enabled: true}) if err != nil { - t.Error("test failed - SMSGlobal RemoveContact() error", err) + t.Error("SMSGlobal RemoveContact() error", err) } err = s.RemoveContact(Contact{Name: "frieda", Number: "243453", Enabled: true}) if err == nil { - t.Error("test failed - SMSGlobal RemoveContact() error", err) + t.Error("SMSGlobal RemoveContact() Expected error") } } func TestSendMessageToAll(t *testing.T) { err := s.SendMessageToAll("Hello,World!") if err != nil { - t.Error("test failed - SMSGlobal SendMessageToAll() error", err) + t.Error("SMSGlobal SendMessageToAll() error", err) } } func TestSendMessage(t *testing.T) { err := s.SendMessage("1337", "Hello!") if err != nil { - t.Error("test failed - SMSGlobal SendMessage() error", err) + t.Error("SMSGlobal SendMessage() error", err) } } diff --git a/communications/smtpservice/smtpservice.go b/communications/smtpservice/smtpservice.go index dd962393..11b2d3b4 100644 --- a/communications/smtpservice/smtpservice.go +++ b/communications/smtpservice/smtpservice.go @@ -4,10 +4,11 @@ import ( "errors" "fmt" "net/smtp" + "strings" - "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/communications/base" "github.com/thrasher-corp/gocryptotrader/config" + log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( @@ -22,6 +23,7 @@ type SMTPservice struct { Port string AccountName string AccountPassword string + From string RecipientList string } @@ -35,7 +37,14 @@ func (s *SMTPservice) Setup(cfg *config.CommunicationsConfig) { s.Port = cfg.SMTPConfig.Port s.AccountName = cfg.SMTPConfig.AccountName s.AccountPassword = cfg.SMTPConfig.AccountPassword + s.From = cfg.SMTPConfig.From s.RecipientList = cfg.SMTPConfig.RecipientList + log.Debugf(log.CommunicationMgr, "SMTP: Setup - From: %v. To: %s. Server: %s.\n", s.From, s.RecipientList, s.Host) +} + +// IsConnected returns whether or not the connection is connected +func (s *SMTPservice) IsConnected() bool { + return s.Connected } // Connect connects to service @@ -45,36 +54,34 @@ func (s *SMTPservice) Connect() error { } // PushEvent sends an event to supplied recipient list via SMTP -func (s *SMTPservice) PushEvent(base.Event) error { - return common.ErrNotYetImplemented +func (s *SMTPservice) PushEvent(e base.Event) error { + return s.Send(e.Type, e.Message) } // Send sends an email template to the recipient list via your SMTP host when // an internal event is triggered by GoCryptoTrader -func (s *SMTPservice) Send(subject, alert string) error { - if subject == "" || alert == "" { +func (s *SMTPservice) Send(subject, msg string) error { + if subject == "" || msg == "" { return errors.New("STMPservice Send() please add subject and alert") } - list := common.SplitStrings(s.RecipientList, ",") + log.Debugf(log.CommunicationMgr, "SMTP: Sending email to %v. Subject: %s Message: %s [From: %s]\n", s.RecipientList, + subject, msg, s.From) + messageToSend := fmt.Sprintf( + msgSMTP, + s.RecipientList, + subject, + mime, + msg) - for i := range list { - messageToSend := fmt.Sprintf( - msgSMTP, - list[i], - subject, - mime, - alert) - - err := smtp.SendMail( - s.Host+":"+s.Port, - smtp.PlainAuth("", s.AccountName, s.AccountPassword, s.Host), - s.AccountName, - []string{list[i]}, - []byte(messageToSend)) - if err != nil { - return err - } + err := smtp.SendMail( + s.Host+":"+s.Port, + smtp.PlainAuth("", s.AccountName, s.AccountPassword, s.Host), + s.From, + strings.Split(s.RecipientList, ","), + []byte(messageToSend)) + if err != nil { + return err } return nil } diff --git a/communications/smtpservice/smtpservice_test.go b/communications/smtpservice/smtpservice_test.go index 1a5be56f..f1996348 100644 --- a/communications/smtpservice/smtpservice_test.go +++ b/communications/smtpservice/smtpservice_test.go @@ -11,7 +11,10 @@ var s SMTPservice func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + t.Fatal(err) + } commsCfg := cfg.GetCommunicationsConfig() s.Setup(&commsCfg) } @@ -19,24 +22,24 @@ func TestSetup(t *testing.T) { func TestConnect(t *testing.T) { err := s.Connect() if err != nil { - t.Error("test failed - smtpservice Connect() error", err) + t.Error("smtpservice Connect() error", err) } } func TestPushEvent(t *testing.T) { err := s.PushEvent(base.Event{}) if err == nil { - t.Error("test failed - smtpservice PushEvent() error", err) + t.Error("smtpservice PushEvent() error cannot be nil") } } func TestSend(t *testing.T) { err := s.Send("", "") if err == nil { - t.Error("test failed - smtpservice Send() error", err) + t.Error("smtpservice Send() error cannot be nil") } err = s.Send("subject", "alertmessage") if err == nil { - t.Error("test failed - smtpservice Send() error", err) + t.Error("smtpservice Send() error cannot be nil") } } diff --git a/communications/telegram/README.md b/communications/telegram/README.md index 3163302b..eff3ee88 100644 --- a/communications/telegram/README.md +++ b/communications/telegram/README.md @@ -92,4 +92,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/communications/telegram/telegram.go b/communications/telegram/telegram.go index 66ce70ab..30a41b33 100644 --- a/communications/telegram/telegram.go +++ b/communications/telegram/telegram.go @@ -5,9 +5,11 @@ package telegram import ( "bytes" + "encoding/json" "errors" "fmt" "net/http" + "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" @@ -23,23 +25,17 @@ const ( methodGetUpdates = "getUpdates" methodSendMessage = "sendMessage" - cmdStart = "/start" - cmdStatus = "/status" - cmdHelp = "/help" - cmdSettings = "/settings" - cmdTicker = "/ticker" - cmdPortfolio = "/portfolio" - cmdOrders = "/orderbooks" + cmdStart = "/start" + cmdStatus = "/status" + cmdHelp = "/help" + cmdSettings = "/settings" cmdHelpReply = `GoCryptoTrader TelegramBot, thank you for using this service! Current commands are: /start - Will authenticate your ID /status - Displays the status of the bot /help - Displays current command list - /settings - Displays current bot settings - /ticker - Displays current ANX ticker data - /portfolio - Displays your current portfolio - /orderbooks - Displays current orderbooks for ANX` + /settings - Displays current bot settings` talkRoot = "GoCryptoTrader bot" ) @@ -59,6 +55,9 @@ type Telegram struct { AuthorisedClients []int64 } +// IsConnected returns whether or not the connection is connected +func (t *Telegram) IsConnected() bool { return t.Connected } + // Setup takes in a Telegram configuration and sets verification token func (t *Telegram) Setup(cfg *config.CommunicationsConfig) { t.Name = cfg.TelegramConfig.Name @@ -73,7 +72,7 @@ func (t *Telegram) Connect() error { return err } - log.Debugln("Telegram: Connected successfully!") + log.Debugln(log.CommunicationMgr, "Telegram: Connected successfully!") t.Connected = true go t.PollerStart() return nil @@ -81,8 +80,8 @@ func (t *Telegram) Connect() error { // PushEvent sends an event to a supplied recipient list via telegram func (t *Telegram) PushEvent(event base.Event) error { - msg := fmt.Sprintf("Type: %s Details: %s GainOrLoss: %s", - event.Type, event.TradeDetails, event.GainLoss) + msg := fmt.Sprintf("Type: %s Message: %s", + event.Type, event.Message) for i := range t.AuthorisedClients { err := t.SendMessage(msg, t.AuthorisedClients[i]) if err != nil { @@ -95,7 +94,7 @@ func (t *Telegram) PushEvent(event base.Event) error { // PollerStart starts the long polling sequence func (t *Telegram) PollerStart() { errWait := func(err error) { - log.Error(err) + log.Errorln(log.CommunicationMgr, err) time.Sleep(ErrWaiter) } @@ -120,7 +119,7 @@ func (t *Telegram) PollerStart() { if string(resp.Result[i].Message.Text[0]) == "/" { err = t.HandleMessages(resp.Result[i].Message.Text, resp.Result[i].Message.From.ID) if err != nil { - log.Errorf("Telegram: Unable to HandleMessages. Error: %s\n", err) + log.Errorf(log.CommunicationMgr, "Telegram: Unable to HandleMessages. Error: %s\n", err) continue } } @@ -152,7 +151,7 @@ func (t *Telegram) InitialConnect() error { for userName, ID := range warmWelcomeList { err = t.SendMessage(fmt.Sprintf("GoCryptoTrader bot has connected: Hello, %s!", userName), ID) if err != nil { - log.Errorf("Telegram: Unable to send welcome message. Error: %s\n", err) + log.Errorf(log.CommunicationMgr, "Telegram: Unable to send welcome message. Error: %s\n", err) continue } } @@ -168,31 +167,19 @@ func (t *Telegram) InitialConnect() error { // HandleMessages handles incoming message from the long polling routine func (t *Telegram) HandleMessages(text string, chatID int64) error { if t.Verbose { - log.Debugf("Telegram: Received message: %s\n", text) + log.Debugf(log.CommunicationMgr, "Telegram: Received message: %s\n", text) } switch { - case common.StringContains(text, cmdHelp): + case strings.Contains(text, cmdHelp): return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, cmdHelpReply), chatID) - case common.StringContains(text, cmdStart): + case strings.Contains(text, cmdStart): return t.SendMessage(fmt.Sprintf("%s: START COMMANDS HERE", talkRoot), chatID) - case common.StringContains(text, cmdOrders): - return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, t.GetOrderbook("ANX")), chatID) - - case common.StringContains(text, cmdStatus): + case strings.Contains(text, cmdStatus): return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, t.GetStatus()), chatID) - case common.StringContains(text, cmdTicker): - return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, t.GetTicker("ANX")), chatID) - - case common.StringContains(text, cmdSettings): - return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, t.GetSettings()), chatID) - - case common.StringContains(text, cmdPortfolio): - return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, t.GetPortfolio()), chatID) - default: return t.SendMessage(fmt.Sprintf("Command %s not recognized", text), chatID) } @@ -233,7 +220,7 @@ func (t *Telegram) SendMessage(text string, chatID int64) error { text, } - json, err := common.JSONEncode(&messageToSend) + json, err := json.Marshal(&messageToSend) if err != nil { return err } @@ -249,20 +236,23 @@ func (t *Telegram) SendMessage(text string, chatID int64) error { } if t.Verbose { - log.Debugf("Telegram: Sent '%s'\n", text) + log.Debugf(log.CommunicationMgr, "Telegram: Sent '%s'\n", text) } return nil } // SendHTTPRequest sends an authenticated HTTP request -func (t *Telegram) SendHTTPRequest(path string, json []byte, result interface{}) error { +func (t *Telegram) SendHTTPRequest(path string, data []byte, result interface{}) error { headers := make(map[string]string) headers["content-type"] = "application/json" - resp, err := common.SendHTTPRequest(http.MethodPost, path, headers, bytes.NewBuffer(json)) + resp, err := common.SendHTTPRequest(http.MethodPost, + path, + headers, + bytes.NewBuffer(data)) if err != nil { return err } - return common.JSONDecode([]byte(resp), result) + return json.Unmarshal([]byte(resp), result) } diff --git a/communications/telegram/telegram_test.go b/communications/telegram/telegram_test.go index b4194254..d50ba75e 100644 --- a/communications/telegram/telegram_test.go +++ b/communications/telegram/telegram_test.go @@ -15,32 +15,37 @@ var T Telegram func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + t.Fatal(err) + } commsCfg := cfg.GetCommunicationsConfig() T.Setup(&commsCfg) - if T.Name != "Telegram" || T.Enabled || - T.Token != "testest" || T.Verbose { - t.Error("test failed - telegram Setup() error, unexpected setup values", - T.Name, T.Enabled, T.Token, T.Verbose) + if T.Name != "Telegram" || T.Enabled || T.Token != "testest" || T.Verbose { + t.Error("telegram Setup() error, unexpected setup values", + T.Name, + T.Enabled, + T.Token, + T.Verbose) } } func TestConnect(t *testing.T) { err := T.Connect() if err == nil { - t.Error("test failed - telegram Connect() error") + t.Error("telegram Connect() error") } } func TestPushEvent(t *testing.T) { err := T.PushEvent(base.Event{}) if err != nil { - t.Error("test failed - telegram PushEvent() error", err) + t.Error("telegram PushEvent() error", err) } T.AuthorisedClients = append(T.AuthorisedClients, 1337) err = T.PushEvent(base.Event{}) if err.Error() != testErrNotFound { - t.Errorf("test failed - telegram PushEvent() error, expected 'Not found' got '%s'", + t.Errorf("telegram PushEvent() error, expected 'Not found' got '%s'", err) } } @@ -50,42 +55,27 @@ func TestHandleMessages(t *testing.T) { chatID := int64(1337) err := T.HandleMessages(cmdHelp, chatID) if err.Error() != testErrNotFound { - t.Errorf("test failed - telegram HandleMessages() error, expected 'Not found' got '%s'", + t.Errorf("telegram HandleMessages() error, expected 'Not found' got '%s'", err) } err = T.HandleMessages(cmdStart, chatID) if err.Error() != testErrNotFound { - t.Errorf("test failed - telegram HandleMessages() error, expected 'Not found' got '%s'", - err) - } - err = T.HandleMessages(cmdOrders, chatID) - if err.Error() != testErrNotFound { - t.Errorf("test failed - telegram HandleMessages() error, expected 'Not found' got '%s'", + t.Errorf("telegram HandleMessages() error, expected 'Not found' got '%s'", err) } err = T.HandleMessages(cmdStatus, chatID) if err.Error() != testErrNotFound { - t.Errorf("test failed - telegram HandleMessages() error, expected 'Not found' got '%s'", - err) - } - err = T.HandleMessages(cmdTicker, chatID) - if err.Error() != testErrNotFound { - t.Errorf("test failed - telegram HandleMessages() error, expected 'Not found' got '%s'", + t.Errorf("telegram HandleMessages() error, expected 'Not found' got '%s'", err) } err = T.HandleMessages(cmdSettings, chatID) if err.Error() != testErrNotFound { - t.Errorf("test failed - telegram HandleMessages() error, expected 'Not found' got '%s'", - err) - } - err = T.HandleMessages(cmdPortfolio, chatID) - if err.Error() != testErrNotFound { - t.Errorf("test failed - telegram HandleMessages() error, expected 'Not found' got '%s'", + t.Errorf("telegram HandleMessages() error, expected 'Not found' got '%s'", err) } err = T.HandleMessages("Not a command", chatID) if err.Error() != testErrNotFound { - t.Errorf("test failed - telegram HandleMessages() error, expected 'Not found' got '%s'", + t.Errorf("telegram HandleMessages() error, expected 'Not found' got '%s'", err) } } @@ -94,7 +84,7 @@ func TestGetUpdates(t *testing.T) { t.Parallel() _, err := T.GetUpdates() if err != nil { - t.Error("test failed - telegram GetUpdates() error", err) + t.Error("telegram GetUpdates() error", err) } } @@ -102,7 +92,7 @@ func TestTestConnection(t *testing.T) { t.Parallel() err := T.TestConnection() if err.Error() != testErrNotFound { - t.Errorf("test failed - telegram TestConnection() error, expected 'Not found' got '%s'", + t.Errorf("telegram TestConnection() error, expected 'Not found' got '%s'", err) } } @@ -111,7 +101,7 @@ func TestSendMessage(t *testing.T) { t.Parallel() err := T.SendMessage("Test message", int64(1337)) if err.Error() != testErrNotFound { - t.Errorf("test failed - telegram SendMessage() error, expected 'Not found' got '%s'", + t.Errorf("telegram SendMessage() error, expected 'Not found' got '%s'", err) } } @@ -120,6 +110,6 @@ func TestSendHTTPRequest(t *testing.T) { t.Parallel() err := T.SendHTTPRequest("0.0.0.0", nil, nil) if err == nil { - t.Error("test failed - telegram SendHTTPRequest() error") + t.Error("telegram SendHTTPRequest() error") } } diff --git a/config/README.md b/config/README.md index 70772f5a..73ee7e7d 100644 --- a/config/README.md +++ b/config/README.md @@ -249,4 +249,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/config/config.go b/config/config.go index 525ab026..94892c1f 100644 --- a/config/config.go +++ b/config/config.go @@ -7,284 +7,26 @@ import ( "flag" "fmt" "io" + "io/ioutil" "os" "path/filepath" "runtime" "strconv" "strings" - "sync" "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/thrasher-corp/gocryptotrader/common/file" "github.com/thrasher-corp/gocryptotrader/connchecker" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/database" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" log "github.com/thrasher-corp/gocryptotrader/logger" - "github.com/thrasher-corp/gocryptotrader/portfolio" ) -// Constants declared here are filename strings and test strings -const ( - FXProviderFixer = "fixer" - EncryptedConfigFile = "config.dat" - ConfigFile = "config.json" - ConfigTestFile = "../testdata/configtest.json" - configFileEncryptionPrompt = 0 - configFileEncryptionEnabled = 1 - configFileEncryptionDisabled = -1 - configPairsLastUpdatedWarningThreshold = 30 // 30 days - configDefaultHTTPTimeout = time.Second * 15 - configDefaultWebsocketResponseCheckTimeout = time.Millisecond * 30 - configDefaultWebsocketResponseMaxLimit = time.Second * 7 - configDefaultWebsocketOrderbookBufferLimit = 5 - configMaxAuthFailres = 3 - defaultNTPAllowedDifference = 50000000 - defaultNTPAllowedNegativeDifference = 50000000 -) - -// Constants here hold some messages -const ( - ErrExchangeNameEmpty = "exchange #%d name is empty" - ErrExchangeAvailablePairsEmpty = "exchange %s available pairs is empty" - ErrExchangeEnabledPairsEmpty = "exchange %s enabled pairs is empty" - ErrExchangeBaseCurrenciesEmpty = "exchange %s base currencies is empty" - ErrExchangeNotFound = "exchange %s not found" - ErrNoEnabledExchanges = "no exchanges enabled" - ErrCryptocurrenciesEmpty = "cryptocurrencies variable is empty" - ErrFailureOpeningConfig = "fatal error opening %s file. Error: %s" - ErrCheckingConfigValues = "fatal error checking config values. Error: %s" - ErrSavingConfigBytesMismatch = "config file %q bytes comparison doesn't match, read %s expected %s" - WarningWebserverCredentialValuesEmpty = "webserver support disabled due to empty Username/Password values" - WarningWebserverListenAddressInvalid = "webserver support disabled due to invalid listen address" - WarningExchangeAuthAPIDefaultOrEmptyValues = "exchange %s authenticated API support disabled due to default/empty APIKey/Secret/ClientID values" - WarningPairsLastUpdatedThresholdExceeded = "exchange %s last manual update of available currency pairs has exceeded %d days. Manual update required!" -) - -// Constants here define unset default values displayed in the config.json -// file -const ( - APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API" - WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - DefaultUnsetAPIKey = "Key" - DefaultUnsetAPISecret = "Secret" - DefaultUnsetAccountPlan = "accountPlan" - DefaultForexProviderExchangeRatesAPI = "ExchangeRates" -) - -// Variables here are used for configuration -var ( - Cfg Config - IsInitialSetup bool - testBypass bool - m sync.Mutex -) - -// WebserverConfig struct holds the prestart variables for the webserver. -type WebserverConfig struct { - Enabled bool `json:"enabled"` - AdminUsername string `json:"adminUsername"` - AdminPassword string `json:"adminPassword"` - ListenAddress string `json:"listenAddress"` - WebsocketConnectionLimit int `json:"websocketConnectionLimit"` - WebsocketMaxAuthFailures int `json:"websocketMaxAuthFailures"` - WebsocketAllowInsecureOrigin bool `json:"websocketAllowInsecureOrigin"` -} - -// Post holds the bot configuration data -type Post struct { - Data Config `json:"data"` -} - -// CurrencyPairFormatConfig stores the users preferred currency pair display -type CurrencyPairFormatConfig struct { - Uppercase bool `json:"uppercase"` - Delimiter string `json:"delimiter,omitempty"` - Separator string `json:"separator,omitempty"` - Index string `json:"index,omitempty"` -} - -// Config is the overarching object that holds all the information for -// prestart management of Portfolio, Communications, Webserver and Enabled -// Exchanges -type Config struct { - Name string `json:"name"` - EncryptConfig int `json:"encryptConfig"` - GlobalHTTPTimeout time.Duration `json:"globalHTTPTimeout"` - Logging log.Logging `json:"logging"` - Profiler ProfilerConfig `json:"profiler"` - NTPClient NTPClientConfig `json:"ntpclient"` - Currency CurrencyConfig `json:"currencyConfig"` - Communications CommunicationsConfig `json:"communications"` - Portfolio portfolio.Base `json:"portfolioAddresses"` - Webserver WebserverConfig `json:"webserver"` - Exchanges []ExchangeConfig `json:"exchanges"` - BankAccounts []BankAccount `json:"bankAccounts"` - ConnectionMonitor ConnectionMonitorConfig `json:"connectionMonitor"` - - // Deprecated config settings, will be removed at a future date - CurrencyPairFormat *CurrencyPairFormatConfig `json:"currencyPairFormat,omitempty"` - FiatDisplayCurrency currency.Code `json:"fiatDispayCurrency,omitempty"` - Cryptocurrencies currency.Currencies `json:"cryptocurrencies,omitempty"` - SMS *SMSGlobalConfig `json:"smsGlobal,omitempty"` -} - -// ConnectionMonitorConfig defines the connection monitor variables to ensure -// that there is internet connectivity -type ConnectionMonitorConfig struct { - DNSList []string `json:"preferredDNSList"` - PublicDomainList []string `json:"preferredDomainList"` - CheckInterval time.Duration `json:"checkInterval"` -} - -// ProfilerConfig defines the profiler configuration to enable pprof -type ProfilerConfig struct { - Enabled bool `json:"enabled"` -} - -// NTPClientConfig defines a network time protocol configuration to allow for -// positive and negative differences -type NTPClientConfig struct { - Level int `json:"enabled"` - Pool []string `json:"pool"` - AllowedDifference *time.Duration `json:"allowedDifference"` - AllowedNegativeDifference *time.Duration `json:"allowedNegativeDifference"` -} - -// ExchangeConfig holds all the information needed for each enabled Exchange. -type ExchangeConfig struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - Websocket bool `json:"websocket"` - UseSandbox bool `json:"useSandbox"` - RESTPollingDelay time.Duration `json:"restPollingDelay"` - HTTPTimeout time.Duration `json:"httpTimeout"` - WebsocketResponseCheckTimeout time.Duration `json:"websocketResponseCheckTimeout"` - WebsocketResponseMaxLimit time.Duration `json:"websocketResponseMaxLimit"` - WebsocketOrderbookBufferLimit int `json:"websocketOrderbookBufferLimit"` - HTTPUserAgent string `json:"httpUserAgent"` - HTTPDebugging bool `json:"httpDebugging"` - AuthenticatedAPISupport bool `json:"authenticatedApiSupport"` - AuthenticatedWebsocketAPISupport bool `json:"authenticatedWebsocketApiSupport"` - APIKey string `json:"apiKey"` - APISecret string `json:"apiSecret"` - APIAuthPEMKeySupport bool `json:"apiAuthPemKeySupport,omitempty"` - APIAuthPEMKey string `json:"apiAuthPemKey,omitempty"` - APIURL string `json:"apiUrl"` - APIURLSecondary string `json:"apiUrlSecondary"` - ProxyAddress string `json:"proxyAddress"` - WebsocketURL string `json:"websocketUrl"` - ClientID string `json:"clientId,omitempty"` - AvailablePairs currency.Pairs `json:"availablePairs"` - EnabledPairs currency.Pairs `json:"enabledPairs"` - BaseCurrencies currency.Currencies `json:"baseCurrencies"` - AssetTypes string `json:"assetTypes"` - SupportsAutoPairUpdates bool `json:"supportsAutoPairUpdates"` - PairsLastUpdated int64 `json:"pairsLastUpdated,omitempty"` - ConfigCurrencyPairFormat *CurrencyPairFormatConfig `json:"configCurrencyPairFormat"` - RequestCurrencyPairFormat *CurrencyPairFormatConfig `json:"requestCurrencyPairFormat"` - BankAccounts []BankAccount `json:"bankAccounts"` -} - -// BankAccount holds differing bank account details by supported funding -// currency -type BankAccount struct { - Enabled bool `json:"enabled,omitempty"` - BankName string `json:"bankName"` - BankAddress string `json:"bankAddress"` - AccountName string `json:"accountName"` - AccountNumber string `json:"accountNumber"` - SWIFTCode string `json:"swiftCode"` - IBAN string `json:"iban"` - BSBNumber string `json:"bsbNumber,omitempty"` - SupportedCurrencies string `json:"supportedCurrencies"` - SupportedExchanges string `json:"supportedExchanges,omitempty"` -} - -// BankTransaction defines a related banking transaction -type BankTransaction struct { - ReferenceNumber string `json:"referenceNumber"` - TransactionNumber string `json:"transactionNumber"` - PaymentInstructions string `json:"paymentInstructions"` -} - -// CurrencyConfig holds all the information needed for currency related manipulation -type CurrencyConfig struct { - ForexProviders []base.Settings `json:"forexProviders"` - CryptocurrencyProvider CryptocurrencyProvider `json:"cryptocurrencyProvider"` - Cryptocurrencies currency.Currencies `json:"cryptocurrencies"` - CurrencyPairFormat *CurrencyPairFormatConfig `json:"currencyPairFormat"` - FiatDisplayCurrency currency.Code `json:"fiatDisplayCurrency"` - CurrencyFileUpdateDuration time.Duration `json:"currencyFileUpdateDuration"` - ForeignExchangeUpdateDuration time.Duration `json:"foreignExchangeUpdateDuration"` -} - -// CryptocurrencyProvider defines coinmarketcap tools -type CryptocurrencyProvider struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - APIkey string `json:"apiKey"` - AccountPlan string `json:"accountPlan"` -} - -// CommunicationsConfig holds all the information needed for each -// enabled communication package -type CommunicationsConfig struct { - SlackConfig SlackConfig `json:"slack"` - SMSGlobalConfig SMSGlobalConfig `json:"smsGlobal"` - SMTPConfig SMTPConfig `json:"smtp"` - TelegramConfig TelegramConfig `json:"telegram"` -} - -// SlackConfig holds all variables to start and run the Slack package -type SlackConfig struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - TargetChannel string `json:"targetChannel"` - VerificationToken string `json:"verificationToken"` -} - -// SMSContact stores the SMS contact info -type SMSContact struct { - Name string `json:"name"` - Number string `json:"number"` - Enabled bool `json:"enabled"` -} - -// SMSGlobalConfig structure holds all the variables you need for instant -// messaging and broadcast used by SMSGlobal -type SMSGlobalConfig struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - Username string `json:"username"` - Password string `json:"password"` - Contacts []SMSContact `json:"contacts"` -} - -// SMTPConfig holds all variables to start and run the SMTP package -type SMTPConfig struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - Host string `json:"host"` - Port string `json:"port"` - AccountName string `json:"accountName"` - AccountPassword string `json:"accountPassword"` - RecipientList string `json:"recipientList"` -} - -// TelegramConfig holds all variables to start and run the Telegram package -type TelegramConfig struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - VerificationToken string `json:"verificationToken"` -} - // GetCurrencyConfig returns currency configurations func (c *Config) GetCurrencyConfig() CurrencyConfig { return c.Currency @@ -299,7 +41,7 @@ func (c *Config) GetExchangeBankAccounts(exchangeName, depositingCurrency string for x := range c.Exchanges { if strings.EqualFold(c.Exchanges[x].Name, exchangeName) { for y := range c.Exchanges[x].BankAccounts { - if common.StringContains(c.Exchanges[x].BankAccounts[y].SupportedCurrencies, + if strings.Contains(c.Exchanges[x].BankAccounts[y].SupportedCurrencies, depositingCurrency) { return c.Exchanges[x].BankAccounts[y], nil } @@ -334,9 +76,9 @@ func (c *Config) GetClientBankAccounts(exchangeName, targetCurrency string) (Ban defer m.Unlock() for x := range c.BankAccounts { - if (common.StringContains(c.BankAccounts[x].SupportedExchanges, exchangeName) || + if (strings.Contains(c.BankAccounts[x].SupportedExchanges, exchangeName) || c.BankAccounts[x].SupportedExchanges == "ALL") && - common.StringContains(c.BankAccounts[x].SupportedCurrencies, targetCurrency) { + strings.Contains(c.BankAccounts[x].SupportedCurrencies, targetCurrency) { return c.BankAccounts[x], nil } } @@ -361,16 +103,19 @@ func (c *Config) UpdateClientBankAccounts(bankCfg *BankAccount) error { } // CheckClientBankAccounts checks client bank details -func (c *Config) CheckClientBankAccounts() error { +func (c *Config) CheckClientBankAccounts() { m.Lock() defer m.Unlock() if len(c.BankAccounts) == 0 { c.BankAccounts = append(c.BankAccounts, BankAccount{ - BankName: "test", - BankAddress: "test", - AccountName: "TestAccount", + BankName: "Test Bank", + BankAddress: "42 Bank Street", + BankPostalCode: "13337", + BankPostalCity: "Satoshiville", + BankCountry: "Japan", + AccountName: "Satoshi Nakamoto", AccountNumber: "0234", SWIFTCode: "91272837", IBAN: "98218738671897", @@ -378,32 +123,46 @@ func (c *Config) CheckClientBankAccounts() error { SupportedExchanges: "ANX,Kraken", }, ) - return nil + return } for i := range c.BankAccounts { if c.BankAccounts[i].Enabled { - if c.BankAccounts[i].BankName == "" || c.BankAccounts[i].BankAddress == "" { - return fmt.Errorf("banking details for %s is enabled but variables not set correctly", - c.BankAccounts[i].BankName) - } - - if c.BankAccounts[i].AccountName == "" || c.BankAccounts[i].AccountNumber == "" { - return fmt.Errorf("banking account details for %s variables not set correctly", - c.BankAccounts[i].BankName) - } - if c.BankAccounts[i].IBAN == "" && c.BankAccounts[i].SWIFTCode == "" && c.BankAccounts[i].BSBNumber == "" { - return fmt.Errorf("critical banking numbers not set for %s in %s account", - c.BankAccounts[i].BankName, - c.BankAccounts[i].AccountName) - } - - if c.BankAccounts[i].SupportedExchanges == "" { - c.BankAccounts[i].SupportedExchanges = "ALL" + err := c.BankAccounts[i].Validate() + if err != nil { + c.BankAccounts[i].Enabled = false + log.Warn(log.ConfigMgr, err.Error()) } } } - return nil +} + +// PurgeExchangeAPICredentials purges the stored API credentials +func (c *Config) PurgeExchangeAPICredentials() { + m.Lock() + defer m.Unlock() + for x := range c.Exchanges { + if !c.Exchanges[x].API.AuthenticatedSupport && !c.Exchanges[x].API.AuthenticatedWebsocketSupport { + continue + } + c.Exchanges[x].API.AuthenticatedSupport = false + c.Exchanges[x].API.AuthenticatedWebsocketSupport = false + + if c.Exchanges[x].API.CredentialsValidator.RequiresKey { + c.Exchanges[x].API.Credentials.Key = DefaultAPIKey + } + + if c.Exchanges[x].API.CredentialsValidator.RequiresSecret { + c.Exchanges[x].API.Credentials.Secret = DefaultAPISecret + } + + if c.Exchanges[x].API.CredentialsValidator.RequiresClientID { + c.Exchanges[x].API.Credentials.ClientID = DefaultAPIClientID + } + + c.Exchanges[x].API.Credentials.PEMKey = "" + c.Exchanges[x].API.Credentials.OTPSecret = "" + } } // GetCommunicationsConfig returns the communications configuration @@ -468,6 +227,7 @@ func (c *Config) CheckCommunicationsConfig() { } else { c.Communications.SMSGlobalConfig = SMSGlobalConfig{ Name: "SMSGlobal", + From: c.Name, Username: "main", Password: "test", @@ -496,6 +256,15 @@ func (c *Config) CheckCommunicationsConfig() { } } } else { + if c.Communications.SMSGlobalConfig.From == "" { + c.Communications.SMSGlobalConfig.From = c.Name + } + + if len(c.Communications.SMSGlobalConfig.From) > 11 { + log.Warnf(log.ConfigMgr, "SMSGlobal config supplied from name exceeds 11 characters, trimming.\n") + c.Communications.SMSGlobalConfig.From = c.Communications.SMSGlobalConfig.From[:11] + } + if c.SMS != nil { // flush old SMS config c.SMS = nil @@ -524,14 +293,14 @@ func (c *Config) CheckCommunicationsConfig() { c.Communications.SMSGlobalConfig.Name != "SMSGlobal" || c.Communications.SMTPConfig.Name != "SMTP" || c.Communications.TelegramConfig.Name != "Telegram" { - log.Warn("Communications config name/s not set correctly") + log.Warnln(log.ConfigMgr, "Communications config name/s not set correctly") } if c.Communications.SlackConfig.Enabled { if c.Communications.SlackConfig.TargetChannel == "" || c.Communications.SlackConfig.VerificationToken == "" || c.Communications.SlackConfig.VerificationToken == "testtest" { c.Communications.SlackConfig.Enabled = false - log.Warn("Slack enabled in config but variable data not set, disabling.") + log.Warnln(log.ConfigMgr, "Slack enabled in config but variable data not set, disabling.") } } if c.Communications.SMSGlobalConfig.Enabled { @@ -539,7 +308,7 @@ func (c *Config) CheckCommunicationsConfig() { c.Communications.SMSGlobalConfig.Password == "" || len(c.Communications.SMSGlobalConfig.Contacts) == 0 { c.Communications.SMSGlobalConfig.Enabled = false - log.Warn("SMSGlobal enabled in config but variable data not set, disabling.") + log.Warnln(log.ConfigMgr, "SMSGlobal enabled in config but variable data not set, disabling.") } } if c.Communications.SMTPConfig.Enabled { @@ -548,43 +317,77 @@ func (c *Config) CheckCommunicationsConfig() { c.Communications.SMTPConfig.AccountName == "" || c.Communications.SMTPConfig.AccountPassword == "" { c.Communications.SMTPConfig.Enabled = false - log.Warn("SMTP enabled in config but variable data not set, disabling.") + log.Warnln(log.ConfigMgr, "SMTP enabled in config but variable data not set, disabling.") } } if c.Communications.TelegramConfig.Enabled { if c.Communications.TelegramConfig.VerificationToken == "" { c.Communications.TelegramConfig.Enabled = false - log.Warn("Telegram enabled in config but variable data not set, disabling.") + log.Warnln(log.ConfigMgr, "Telegram enabled in config but variable data not set, disabling.") } } } -// CheckPairConsistency checks to see if the enabled pair exists in the -// available pairs list -func (c *Config) CheckPairConsistency(exchName string) error { - enabledPairs, err := c.GetEnabledPairs(exchName) +// GetExchangeAssetTypes returns the exchanges supported asset types +func (c *Config) GetExchangeAssetTypes(exchName string) (asset.Items, error) { + exchCfg, err := c.GetExchangeConfig(exchName) if err != nil { - return err + return nil, err } - availPairs, err := c.GetAvailablePairs(exchName) - if err != nil { - return err + if exchCfg.CurrencyPairs == nil { + return nil, fmt.Errorf("exchange %s currency pairs is nil", exchName) } - var pairs, pairsRemoved currency.Pairs - update := false - for x := range enabledPairs { - if !availPairs.Contains(enabledPairs[x], true) { - update = true - pairsRemoved = append(pairsRemoved, enabledPairs[x]) - continue + return exchCfg.CurrencyPairs.AssetTypes, nil +} + +// SupportsExchangeAssetType returns whether or not the exchange supports the supplied asset type +func (c *Config) SupportsExchangeAssetType(exchName string, assetType asset.Item) (bool, error) { + exchCfg, err := c.GetExchangeConfig(exchName) + if err != nil { + return false, err + } + + if exchCfg.CurrencyPairs == nil { + return false, fmt.Errorf("exchange %s currency pairs is nil", exchName) + } + + if !asset.IsValid(assetType) { + return false, fmt.Errorf("exchange %s invalid asset types", exchName) + } + + return exchCfg.CurrencyPairs.AssetTypes.Contains(assetType), nil +} + +// CheckExchangeAssetsConsistency checks the exchanges supported assets compared to the stored +// entries and removes any non supported +func (c *Config) CheckExchangeAssetsConsistency(exchName string) { + exchCfg, err := c.GetExchangeConfig(exchName) + if err != nil { + return + } + + exchangeAssetTypes, err := c.GetExchangeAssetTypes(exchName) + if err != nil { + return + } + + storedAssetTypes := exchCfg.CurrencyPairs.GetAssetTypes() + for x := range storedAssetTypes { + if !exchangeAssetTypes.Contains(storedAssetTypes[x]) { + log.Warnf(log.ConfigMgr, + "%s has non-needed stored asset type %v. Removing..\n", + exchName, storedAssetTypes[x]) + exchCfg.CurrencyPairs.Delete(storedAssetTypes[x]) } - pairs = append(pairs, enabledPairs[x]) } +} - if !update { - return nil +// SetPairs sets the exchanges currency pairs +func (c *Config) SetPairs(exchName string, assetType asset.Item, enabled bool, pairs currency.Pairs) error { + if len(pairs) == 0 { + return fmt.Errorf("pairs is nil") } exchCfg, err := c.GetExchangeConfig(exchName) @@ -592,59 +395,241 @@ func (c *Config) CheckPairConsistency(exchName string) error { return err } - if len(pairs) == 0 { - exchCfg.EnabledPairs = append(exchCfg.EnabledPairs, - availPairs.GetRandomPair()) - log.Debugf("Exchange %s: No enabled pairs found in available pairs, randomly added %v\n", - exchName, - exchCfg.EnabledPairs) - } else { - exchCfg.EnabledPairs = pairs - } - - err = c.UpdateExchangeConfig(&exchCfg) + supports, err := c.SupportsExchangeAssetType(exchName, assetType) if err != nil { return err } - log.Debugf("Exchange %s: Removing enabled pair(s) %v from enabled pairs as it isn't an available pair", - exchName, - pairsRemoved.Strings()) + if !supports { + return fmt.Errorf("exchange %s does not support asset type %v", exchName, assetType) + } + + exchCfg.CurrencyPairs.StorePairs(assetType, pairs, enabled) + return nil +} + +// GetCurrencyPairConfig returns currency pair config for the desired exchange and asset type +func (c *Config) GetCurrencyPairConfig(exchName string, assetType asset.Item) (*currency.PairStore, error) { + exchCfg, err := c.GetExchangeConfig(exchName) + if err != nil { + return nil, err + } + + supports, err := c.SupportsExchangeAssetType(exchName, assetType) + if err != nil { + return nil, err + } + + if !supports { + return nil, fmt.Errorf("exchange %s does not support asset type %v", exchName, assetType) + } + + return exchCfg.CurrencyPairs.Get(assetType), nil +} + +// CheckPairConfigFormats checks to see if the pair config format is valid +func (c *Config) CheckPairConfigFormats(exchName string) error { + assetTypes, err := c.GetExchangeAssetTypes(exchName) + if err != nil { + return err + } + + for x := range assetTypes { + assetType := assetTypes[x] + pairFmt, err := c.GetPairFormat(exchName, assetType) + if err != nil { + return err + } + + // No err checking is required as the above checks the same + // conditions + pairs, _ := c.GetCurrencyPairConfig(exchName, assetType) + + if len(pairs.Available) == 0 || len(pairs.Enabled) == 0 { + continue + } + + checker := func(enabled bool) error { + pairsType := "enabled" + loadedPairs := pairs.Enabled + if !enabled { + pairsType = "available" + loadedPairs = pairs.Available + } + + for y := range loadedPairs { + if pairFmt.Delimiter != "" && pairFmt.Index != "" { + return fmt.Errorf( + "exchange %s %s %s cannot have an index and delimiter set at the same time", + exchName, pairsType, assetType) + } + if pairFmt.Delimiter != "" { + if !strings.Contains(loadedPairs[y].String(), pairFmt.Delimiter) { + return fmt.Errorf( + "exchange %s %s %s pairs does not contain delimiter", + exchName, pairsType, assetType) + } + } + if pairFmt.Index != "" { + if !strings.Contains(loadedPairs[y].String(), pairFmt.Index) { + return fmt.Errorf("exchange %s %s %s pairs does not contain an index", + exchName, pairsType, assetType) + } + } + } + return nil + } + + err = checker(true) + if err != nil { + return err + } + + err = checker(false) + if err != nil { + return err + } + } + + return nil +} + +// CheckPairConsistency checks to see if the enabled pair exists in the +// available pairs list +func (c *Config) CheckPairConsistency(exchName string) error { + assetTypes, err := c.GetExchangeAssetTypes(exchName) + if err != nil { + return err + } + + for x := range assetTypes { + enabledPairs, err := c.GetEnabledPairs(exchName, assetTypes[x]) + if err != nil { + return err + } + + availPairs, _ := c.GetAvailablePairs(exchName, assetTypes[x]) + if len(availPairs) == 0 { + continue + } + + var pairs, pairsRemoved currency.Pairs + update := false + + if len(enabledPairs) > 0 { + for x := range enabledPairs { + if !availPairs.Contains(enabledPairs[x], true) { + update = true + pairsRemoved = append(pairsRemoved, enabledPairs[x]) + continue + } + pairs = append(pairs, enabledPairs[x]) + } + } else { + update = true + } + + if !update { + continue + } + + if len(pairs) == 0 || len(enabledPairs) == 0 { + newPair := availPairs.GetRandomPair() + c.SetPairs(exchName, assetTypes[x], true, currency.Pairs{newPair}) + log.Warnf(log.ExchangeSys, "Exchange %s: [%v] No enabled pairs found in available pairs, randomly added %v pair.\n", + exchName, assetTypes[x], newPair) + continue + } else { + c.SetPairs(exchName, assetTypes[x], true, pairs) + } + log.Warnf(log.ExchangeSys, "Exchange %s: [%v] Removing enabled pair(s) %v from enabled pairs as it isn't an available pair.\n", + exchName, assetTypes[x], pairsRemoved.Strings()) + } return nil } // SupportsPair returns true or not whether the exchange supports the supplied // pair -func (c *Config) SupportsPair(exchName string, p currency.Pair) (bool, error) { - pairs, err := c.GetAvailablePairs(exchName) +func (c *Config) SupportsPair(exchName string, p currency.Pair, assetType asset.Item) (bool, error) { + pairs, err := c.GetAvailablePairs(exchName, assetType) if err != nil { return false, err } return pairs.Contains(p, false), nil } +// GetPairFormat returns the exchanges pair config storage format +func (c *Config) GetPairFormat(exchName string, assetType asset.Item) (currency.PairFormat, error) { + exchCfg, err := c.GetExchangeConfig(exchName) + if err != nil { + return currency.PairFormat{}, err + } + + supports, err := c.SupportsExchangeAssetType(exchName, assetType) + if err != nil { + return currency.PairFormat{}, err + } + + if !supports { + return currency.PairFormat{}, + fmt.Errorf("exchange %s does not support asset type %s", exchName, + assetType) + } + + if exchCfg.CurrencyPairs.UseGlobalFormat { + return *exchCfg.CurrencyPairs.ConfigFormat, nil + } + + p := exchCfg.CurrencyPairs.Get(assetType) + if p == nil { + return currency.PairFormat{}, + fmt.Errorf("exchange %s pair store for asset type %s is nil", exchName, + assetType) + } + + return *p.ConfigFormat, nil +} + // GetAvailablePairs returns a list of currency pairs for a specifc exchange -func (c *Config) GetAvailablePairs(exchName string) (currency.Pairs, error) { +func (c *Config) GetAvailablePairs(exchName string, assetType asset.Item) (currency.Pairs, error) { exchCfg, err := c.GetExchangeConfig(exchName) if err != nil { return nil, err } - return exchCfg.AvailablePairs.Format(exchCfg.ConfigCurrencyPairFormat.Delimiter, - exchCfg.ConfigCurrencyPairFormat.Index, - exchCfg.ConfigCurrencyPairFormat.Uppercase), nil + pairFormat, err := c.GetPairFormat(exchName, assetType) + if err != nil { + return nil, err + } + + pairs := exchCfg.CurrencyPairs.GetPairs(assetType, false) + if pairs == nil { + return nil, nil + } + + return pairs.Format(pairFormat.Delimiter, pairFormat.Index, + pairFormat.Uppercase), nil } // GetEnabledPairs returns a list of currency pairs for a specifc exchange -func (c *Config) GetEnabledPairs(exchName string) (currency.Pairs, error) { +func (c *Config) GetEnabledPairs(exchName string, assetType asset.Item) ([]currency.Pair, error) { exchCfg, err := c.GetExchangeConfig(exchName) if err != nil { return nil, err } - return exchCfg.EnabledPairs.Format(exchCfg.ConfigCurrencyPairFormat.Delimiter, - exchCfg.ConfigCurrencyPairFormat.Index, - exchCfg.ConfigCurrencyPairFormat.Uppercase), nil + pairFormat, err := c.GetPairFormat(exchName, assetType) + if err != nil { + return nil, err + } + + pairs := exchCfg.CurrencyPairs.GetPairs(assetType, true) + if pairs == nil { + return nil, nil + } + + return pairs.Format(pairFormat.Delimiter, pairFormat.Index, + pairFormat.Uppercase), nil } // GetEnabledExchanges returns a list of enabled exchanges @@ -680,26 +665,6 @@ func (c *Config) CountEnabledExchanges() int { return counter } -// GetConfigCurrencyPairFormat returns the config currency pair format -// for a specific exchange -func (c *Config) GetConfigCurrencyPairFormat(exchName string) (*CurrencyPairFormatConfig, error) { - exchCfg, err := c.GetExchangeConfig(exchName) - if err != nil { - return nil, err - } - return exchCfg.ConfigCurrencyPairFormat, nil -} - -// GetRequestCurrencyPairFormat returns the request currency pair format -// for a specific exchange -func (c *Config) GetRequestCurrencyPairFormat(exchName string) (*CurrencyPairFormatConfig, error) { - exchCfg, err := c.GetExchangeConfig(exchName) - if err != nil { - return nil, err - } - return exchCfg.RequestCurrencyPairFormat, nil -} - // GetCurrencyPairDisplayConfig retrieves the currency pair display preference func (c *Config) GetCurrencyPairDisplayConfig() *CurrencyPairFormatConfig { return c.Currency.CurrencyPairFormat @@ -713,15 +678,15 @@ func (c *Config) GetAllExchangeConfigs() []ExchangeConfig { } // GetExchangeConfig returns exchange configurations by its indivdual name -func (c *Config) GetExchangeConfig(name string) (ExchangeConfig, error) { +func (c *Config) GetExchangeConfig(name string) (*ExchangeConfig, error) { m.Lock() defer m.Unlock() for i := range c.Exchanges { if strings.EqualFold(c.Exchanges[i].Name, name) { - return c.Exchanges[i], nil + return &c.Exchanges[i], nil } } - return ExchangeConfig{}, fmt.Errorf(ErrExchangeNotFound, name) + return nil, fmt.Errorf(ErrExchangeNotFound, name) } // GetForexProviderConfig returns a forex provider configuration by its name @@ -736,6 +701,13 @@ func (c *Config) GetForexProviderConfig(name string) (base.Settings, error) { return base.Settings{}, errors.New("provider not found") } +// GetForexProvidersConfig returns a list of available forex providers +func (c *Config) GetForexProvidersConfig() []base.Settings { + m.Lock() + defer m.Unlock() + return c.Currency.ForexProviders +} + // GetPrimaryForexProvider returns the primary forex provider func (c *Config) GetPrimaryForexProvider() string { m.Lock() @@ -764,119 +736,239 @@ func (c *Config) UpdateExchangeConfig(e *ExchangeConfig) error { // CheckExchangeConfigValues returns configuation values for all enabled // exchanges func (c *Config) CheckExchangeConfigValues() error { + if len(c.Exchanges) == 0 { + return errors.New("no exchange configs found") + } + exchanges := 0 for i := range c.Exchanges { if strings.EqualFold(c.Exchanges[i].Name, "GDAX") { c.Exchanges[i].Name = "CoinbasePro" } - if c.Exchanges[i].WebsocketURL != WebsocketURLNonDefaultMessage { - if c.Exchanges[i].WebsocketURL == "" { - c.Exchanges[i].WebsocketURL = WebsocketURLNonDefaultMessage + // Check to see if the old API storage format is used + if c.Exchanges[i].APIKey != nil { + // It is, migrate settings to new format + c.Exchanges[i].API.AuthenticatedSupport = *c.Exchanges[i].AuthenticatedAPISupport + if c.Exchanges[i].AuthenticatedWebsocketAPISupport != nil { + c.Exchanges[i].API.AuthenticatedWebsocketSupport = *c.Exchanges[i].AuthenticatedWebsocketAPISupport + } + c.Exchanges[i].API.Credentials.Key = *c.Exchanges[i].APIKey + c.Exchanges[i].API.Credentials.Secret = *c.Exchanges[i].APISecret + + if c.Exchanges[i].APIAuthPEMKey != nil { + c.Exchanges[i].API.Credentials.PEMKey = *c.Exchanges[i].APIAuthPEMKey + } + + if c.Exchanges[i].APIAuthPEMKeySupport != nil { + c.Exchanges[i].API.PEMKeySupport = *c.Exchanges[i].APIAuthPEMKeySupport + } + + if c.Exchanges[i].ClientID != nil { + c.Exchanges[i].API.Credentials.ClientID = *c.Exchanges[i].ClientID + } + + if c.Exchanges[i].WebsocketURL != nil { + c.Exchanges[i].API.Endpoints.WebsocketURL = *c.Exchanges[i].WebsocketURL + } + + c.Exchanges[i].API.Endpoints.URL = *c.Exchanges[i].APIURL + c.Exchanges[i].API.Endpoints.URLSecondary = *c.Exchanges[i].APIURLSecondary + + // Flush settings + c.Exchanges[i].AuthenticatedAPISupport = nil + c.Exchanges[i].AuthenticatedWebsocketAPISupport = nil + c.Exchanges[i].APIKey = nil + c.Exchanges[i].APISecret = nil + c.Exchanges[i].ClientID = nil + c.Exchanges[i].APIAuthPEMKeySupport = nil + c.Exchanges[i].APIAuthPEMKey = nil + c.Exchanges[i].APIURL = nil + c.Exchanges[i].APIURLSecondary = nil + c.Exchanges[i].WebsocketURL = nil + } + + if c.Exchanges[i].Features == nil { + c.Exchanges[i].Features = &FeaturesConfig{} + } + + if c.Exchanges[i].SupportsAutoPairUpdates != nil { + c.Exchanges[i].Features.Supports.RESTCapabilities.AutoPairUpdates = *c.Exchanges[i].SupportsAutoPairUpdates + c.Exchanges[i].Features.Enabled.AutoPairUpdates = *c.Exchanges[i].SupportsAutoPairUpdates + c.Exchanges[i].SupportsAutoPairUpdates = nil + } + + if c.Exchanges[i].Websocket != nil { + c.Exchanges[i].Features.Enabled.Websocket = *c.Exchanges[i].Websocket + c.Exchanges[i].Websocket = nil + } + + if c.Exchanges[i].API.Endpoints.URL != APIURLNonDefaultMessage { + if c.Exchanges[i].API.Endpoints.URL == "" { + // Set default if nothing set + c.Exchanges[i].API.Endpoints.URL = APIURLNonDefaultMessage } } - if c.Exchanges[i].APIURL != APIURLNonDefaultMessage { - if c.Exchanges[i].APIURL == "" { + if c.Exchanges[i].API.Endpoints.URLSecondary != APIURLNonDefaultMessage { + if c.Exchanges[i].API.Endpoints.URLSecondary == "" { // Set default if nothing set - c.Exchanges[i].APIURL = APIURLNonDefaultMessage + c.Exchanges[i].API.Endpoints.URLSecondary = APIURLNonDefaultMessage } } - if c.Exchanges[i].APIURLSecondary != APIURLNonDefaultMessage { - if c.Exchanges[i].APIURLSecondary == "" { - // Set default if nothing set - c.Exchanges[i].APIURLSecondary = APIURLNonDefaultMessage + if c.Exchanges[i].API.Endpoints.WebsocketURL != WebsocketURLNonDefaultMessage { + if c.Exchanges[i].API.Endpoints.WebsocketURL == "" { + c.Exchanges[i].API.Endpoints.WebsocketURL = WebsocketURLNonDefaultMessage } } + // Check if see if the new currency pairs format is empty and flesh it out if so + if c.Exchanges[i].CurrencyPairs == nil { + c.Exchanges[i].CurrencyPairs = new(currency.PairsManager) + c.Exchanges[i].CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) + + if c.Exchanges[i].PairsLastUpdated != nil { + c.Exchanges[i].CurrencyPairs.LastUpdated = *c.Exchanges[i].PairsLastUpdated + } + + c.Exchanges[i].CurrencyPairs.ConfigFormat = c.Exchanges[i].ConfigCurrencyPairFormat + c.Exchanges[i].CurrencyPairs.RequestFormat = c.Exchanges[i].RequestCurrencyPairFormat + + if c.Exchanges[i].AssetTypes == nil { + c.Exchanges[i].CurrencyPairs.AssetTypes = asset.Items{ + asset.Spot, + } + } else { + c.Exchanges[i].CurrencyPairs.AssetTypes = asset.New( + strings.ToLower(*c.Exchanges[i].AssetTypes), + ) + } + + var availPairs, enabledPairs currency.Pairs + if c.Exchanges[i].AvailablePairs != nil { + availPairs = *c.Exchanges[i].AvailablePairs + } + + if c.Exchanges[i].EnabledPairs != nil { + enabledPairs = *c.Exchanges[i].EnabledPairs + } + + c.Exchanges[i].CurrencyPairs.UseGlobalFormat = true + c.Exchanges[i].CurrencyPairs.Store(asset.Spot, + currency.PairStore{ + Available: availPairs, + Enabled: enabledPairs, + }, + ) + + // flush old values + c.Exchanges[i].PairsLastUpdated = nil + c.Exchanges[i].ConfigCurrencyPairFormat = nil + c.Exchanges[i].RequestCurrencyPairFormat = nil + c.Exchanges[i].AssetTypes = nil + c.Exchanges[i].AvailablePairs = nil + c.Exchanges[i].EnabledPairs = nil + } + if c.Exchanges[i].Enabled { if c.Exchanges[i].Name == "" { - return fmt.Errorf(ErrExchangeNameEmpty, i) - } - if len(c.Exchanges[i].AvailablePairs) == 0 { - return fmt.Errorf(ErrExchangeAvailablePairsEmpty, c.Exchanges[i].Name) - } - if len(c.Exchanges[i].EnabledPairs) == 0 { - return fmt.Errorf(ErrExchangeEnabledPairsEmpty, c.Exchanges[i].Name) - } - if len(c.Exchanges[i].BaseCurrencies) == 0 { - return fmt.Errorf(ErrExchangeBaseCurrenciesEmpty, c.Exchanges[i].Name) + log.Errorf(log.ConfigMgr, ErrExchangeNameEmpty, i) + c.Exchanges[i].Enabled = false + continue } + if (c.Exchanges[i].API.AuthenticatedSupport || c.Exchanges[i].API.AuthenticatedWebsocketSupport) && c.Exchanges[i].API.CredentialsValidator != nil { + var failed bool + if c.Exchanges[i].API.CredentialsValidator.RequiresKey && (c.Exchanges[i].API.Credentials.Key == "" || c.Exchanges[i].API.Credentials.Key == DefaultAPIKey) { + failed = true + } - var areAuthenticatedCredentialsValid bool - if c.Exchanges[i].AuthenticatedWebsocketAPISupport || c.Exchanges[i].AuthenticatedAPISupport { - areAuthenticatedCredentialsValid = c.areAuthenticatedCredentialsValid(i) - } - if c.Exchanges[i].AuthenticatedWebsocketAPISupport { - c.Exchanges[i].AuthenticatedWebsocketAPISupport = areAuthenticatedCredentialsValid - } - if c.Exchanges[i].AuthenticatedAPISupport { - c.Exchanges[i].AuthenticatedAPISupport = areAuthenticatedCredentialsValid - } + if c.Exchanges[i].API.CredentialsValidator.RequiresSecret && (c.Exchanges[i].API.Credentials.Secret == "" || c.Exchanges[i].API.Credentials.Secret == DefaultAPISecret) { + failed = true + } - if !c.Exchanges[i].SupportsAutoPairUpdates { - lastUpdated := common.UnixTimestampToTime(c.Exchanges[i].PairsLastUpdated) - lastUpdated = lastUpdated.AddDate(0, 0, configPairsLastUpdatedWarningThreshold) + if c.Exchanges[i].API.CredentialsValidator.RequiresClientID && (c.Exchanges[i].API.Credentials.ClientID == DefaultAPIClientID || c.Exchanges[i].API.Credentials.ClientID == "") { + failed = true + } + + if failed { + c.Exchanges[i].API.AuthenticatedSupport = false + c.Exchanges[i].API.AuthenticatedWebsocketSupport = false + log.Warnf(log.ExchangeSys, WarningExchangeAuthAPIDefaultOrEmptyValues, c.Exchanges[i].Name) + } + } + if !c.Exchanges[i].Features.Supports.RESTCapabilities.AutoPairUpdates && !c.Exchanges[i].Features.Supports.WebsocketCapabilities.AutoPairUpdates { + lastUpdated := convert.UnixTimestampToTime(c.Exchanges[i].CurrencyPairs.LastUpdated) + lastUpdated = lastUpdated.AddDate(0, 0, pairsLastUpdatedWarningThreshold) if lastUpdated.Unix() <= time.Now().Unix() { - log.Warnf(WarningPairsLastUpdatedThresholdExceeded, c.Exchanges[i].Name, configPairsLastUpdatedWarningThreshold) + log.Warnf(log.ExchangeSys, WarningPairsLastUpdatedThresholdExceeded, c.Exchanges[i].Name, pairsLastUpdatedWarningThreshold) + } + } + if c.Exchanges[i].HTTPTimeout <= 0 { + log.Warnf(log.ExchangeSys, "Exchange %s HTTP Timeout value not set, defaulting to %v.\n", c.Exchanges[i].Name, defaultHTTPTimeout) + c.Exchanges[i].HTTPTimeout = defaultHTTPTimeout + } + + if c.Exchanges[i].HTTPRateLimiter != nil { + if c.Exchanges[i].HTTPRateLimiter.Authenticated.Duration < 0 { + log.Warnf(log.ExchangeSys, "Exchange %s HTTP Rate Limiter authenticated duration set to negative value, defaulting to 0\n", c.Exchanges[i].Name) + c.Exchanges[i].HTTPRateLimiter.Authenticated.Duration = 0 + } + + if c.Exchanges[i].HTTPRateLimiter.Authenticated.Rate < 0 { + log.Warnf(log.ExchangeSys, "Exchange %s HTTP Rate Limiter authenticated rate set to negative value, defaulting to 0\n", c.Exchanges[i].Name) + c.Exchanges[i].HTTPRateLimiter.Authenticated.Rate = 0 + } + + if c.Exchanges[i].HTTPRateLimiter.Unauthenticated.Duration < 0 { + log.Warnf(log.ExchangeSys, "Exchange %s HTTP Rate Limiter unauthenticated duration set to negative value, defaulting to 0\n", c.Exchanges[i].Name) + c.Exchanges[i].HTTPRateLimiter.Unauthenticated.Duration = 0 + } + + if c.Exchanges[i].HTTPRateLimiter.Unauthenticated.Rate < 0 { + log.Warnf(log.ExchangeSys, "Exchange %s HTTP Rate Limiter unauthenticated rate set to negative value, defaulting to 0\n", c.Exchanges[i].Name) + c.Exchanges[i].HTTPRateLimiter.Unauthenticated.Rate = 0 } } - if c.Exchanges[i].HTTPTimeout <= 0 { - log.Warnf("Exchange %s HTTP Timeout value not set, defaulting to %v.", c.Exchanges[i].Name, configDefaultHTTPTimeout) - c.Exchanges[i].HTTPTimeout = configDefaultHTTPTimeout - } - if c.Exchanges[i].WebsocketResponseCheckTimeout <= 0 { - log.Warnf("Exchange %s Websocket response check timeout value not set, defaulting to %v.", c.Exchanges[i].Name, configDefaultWebsocketResponseCheckTimeout) - c.Exchanges[i].WebsocketResponseCheckTimeout = configDefaultWebsocketResponseCheckTimeout + log.Warnf(log.ExchangeSys, "Exchange %s Websocket response check timeout value not set, defaulting to %v.", + c.Exchanges[i].Name, defaultWebsocketResponseCheckTimeout) + c.Exchanges[i].WebsocketResponseCheckTimeout = defaultWebsocketResponseCheckTimeout } if c.Exchanges[i].WebsocketResponseMaxLimit <= 0 { - log.Warnf("Exchange %s Websocket response max limit value not set, defaulting to %v.", c.Exchanges[i].Name, configDefaultWebsocketResponseMaxLimit) - c.Exchanges[i].WebsocketResponseMaxLimit = configDefaultWebsocketResponseMaxLimit + log.Warnf(log.ExchangeSys, "Exchange %s Websocket response max limit value not set, defaulting to %v.", + c.Exchanges[i].Name, defaultWebsocketResponseMaxLimit) + c.Exchanges[i].WebsocketResponseMaxLimit = defaultWebsocketResponseMaxLimit + } + if c.Exchanges[i].WebsocketTrafficTimeout <= 0 { + log.Warnf(log.ExchangeSys, "Exchange %s Websocket response traffic timeout value not set, defaulting to %v.", + c.Exchanges[i].Name, defaultWebsocketTrafficTimeout) + c.Exchanges[i].WebsocketTrafficTimeout = defaultWebsocketTrafficTimeout } if c.Exchanges[i].WebsocketOrderbookBufferLimit <= 0 { - log.Warnf("Exchange %s Websocket orderbook buffer limit value not set, defaulting to %v.", c.Exchanges[i].Name, configDefaultWebsocketOrderbookBufferLimit) - c.Exchanges[i].WebsocketOrderbookBufferLimit = configDefaultWebsocketOrderbookBufferLimit + log.Warnf(log.ExchangeSys, "Exchange %s Websocket orderbook buffer limit value not set, defaulting to %v.", + c.Exchanges[i].Name, defaultWebsocketOrderbookBufferLimit) + c.Exchanges[i].WebsocketOrderbookBufferLimit = defaultWebsocketOrderbookBufferLimit } err := c.CheckPairConsistency(c.Exchanges[i].Name) if err != nil { - log.Errorf("Exchange %s: CheckPairConsistency error: %s", c.Exchanges[i].Name, err) + log.Errorf(log.ExchangeSys, "Exchange %s: CheckPairConsistency error: %s\n", c.Exchanges[i].Name, err) + c.Exchanges[i].Enabled = false + continue } - if len(c.Exchanges[i].BankAccounts) == 0 { - c.Exchanges[i].BankAccounts = append(c.Exchanges[i].BankAccounts, BankAccount{}) - } else { - for y := range c.Exchanges[i].BankAccounts { - bankAccount := &c.Exchanges[i].BankAccounts[y] - if bankAccount.Enabled { - if bankAccount.BankName == "" || bankAccount.BankAddress == "" { - log.Warnf("banking details for %s is enabled but variables not set", - c.Exchanges[i].Name) - bankAccount.Enabled = false - } + c.CheckExchangeAssetsConsistency(c.Exchanges[i].Name) - if bankAccount.AccountName == "" || bankAccount.AccountNumber == "" { - log.Warnf("banking account details for %s variables not set", - c.Exchanges[i].Name) - bankAccount.Enabled = false - } - - if bankAccount.SupportedCurrencies == "" { - log.Warnf("banking account details for %s acceptable funding currencies not set", - c.Exchanges[i].Name) - bankAccount.Enabled = false - } - - if bankAccount.BSBNumber == "" && bankAccount.IBAN == "" && - bankAccount.SWIFTCode == "" { - log.Warnf("banking account details for %s critical banking numbers not set", - c.Exchanges[i].Name) - bankAccount.Enabled = false - } - } + for x := range c.Exchanges[i].BankAccounts { + if !c.Exchanges[i].BankAccounts[x].Enabled { + continue + } + err := c.Exchanges[i].BankAccounts[x].Validate() + if err != nil { + c.Exchanges[i].BankAccounts[x].Enabled = false + log.Warn(log.ConfigMgr, err.Error()) } } exchanges++ @@ -888,81 +980,15 @@ func (c *Config) CheckExchangeConfigValues() error { return nil } -func (c *Config) areAuthenticatedCredentialsValid(i int) bool { - if c.Exchanges == nil { - log.Error("Config: Failed to check exchange authenticated credentials due to c.Exchanges not setup") - return false - } - if i < 0 || c.Exchanges == nil || len(c.Exchanges) < i { - log.Error("Config: Failed to check exchange authenticated credentials due to invalid index") - return false - } - - resp := true - if c.Exchanges[i].APIKey == "" || c.Exchanges[i].APIKey == DefaultUnsetAPIKey { - resp = false - } - - if (c.Exchanges[i].APISecret == "" || c.Exchanges[i].APISecret == DefaultUnsetAPISecret) && - c.Exchanges[i].Name != "COINUT" { - resp = false - } - - if (c.Exchanges[i].ClientID == "ClientID" || c.Exchanges[i].ClientID == "") && - (c.Exchanges[i].Name == "ITBIT" || c.Exchanges[i].Name == "Bitstamp" || c.Exchanges[i].Name == "COINUT" || c.Exchanges[i].Name == "CoinbasePro") { - resp = false - } - // non-fatal error - if !resp { - log.Warnf(WarningExchangeAuthAPIDefaultOrEmptyValues, c.Exchanges[i].Name) - } - return resp -} - -// CheckWebserverConfigValues checks information before webserver starts and -// returns an error if values are incorrect. -func (c *Config) CheckWebserverConfigValues() error { - if c.Webserver.AdminUsername == "" || c.Webserver.AdminPassword == "" { - return errors.New(WarningWebserverCredentialValuesEmpty) - } - - if !common.StringContains(c.Webserver.ListenAddress, ":") { - return errors.New(WarningWebserverListenAddressInvalid) - } - - portStr := common.SplitStrings(c.Webserver.ListenAddress, ":")[1] - port, err := strconv.Atoi(portStr) - if err != nil { - return errors.New(WarningWebserverListenAddressInvalid) - } - - if port < 1 || port > 65355 { - return errors.New(WarningWebserverListenAddressInvalid) - } - - if c.Webserver.WebsocketConnectionLimit <= 0 { - c.Webserver.WebsocketConnectionLimit = 1 - } - - if c.Webserver.WebsocketMaxAuthFailures <= 0 { - c.Webserver.WebsocketMaxAuthFailures = 3 - } - - return nil -} - // CheckCurrencyConfigValues checks to see if the currency config values are correct or not func (c *Config) CheckCurrencyConfigValues() error { - fxProviders := forexprovider.GetAvailableForexProviders() - if len(fxProviders) == 0 { - return errors.New("no forex providers available") - } + fxProviders := forexprovider.GetSupportedForexProviders() if len(fxProviders) != len(c.Currency.ForexProviders) { for x := range fxProviders { _, err := c.GetForexProviderConfig(fxProviders[x]) if err != nil { - log.Warnf("%s forex provider not found, adding to config..", fxProviders[x]) + log.Warnf(log.Global, "%s forex provider not found, adding to config..\n", fxProviders[x]) c.Currency.ForexProviders = append(c.Currency.ForexProviders, base.Settings{ Name: fxProviders[x], RESTPollingDelay: 600, @@ -976,29 +1002,27 @@ func (c *Config) CheckCurrencyConfigValues() error { count := 0 for i := range c.Currency.ForexProviders { if c.Currency.ForexProviders[i].Enabled { - if c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey && c.Currency.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI { - log.Warnf("%s enabled forex provider API key not set. Please set this in your config.json file", c.Currency.ForexProviders[i].Name) + if c.Currency.ForexProviders[i].Name == "CurrencyConverter" && + c.Currency.ForexProviders[i].PrimaryProvider && + (c.Currency.ForexProviders[i].APIKey == "" || + c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey) { + log.Warnln(log.Global, "CurrencyConverter forex provider no longer supports unset API key requests. Switching to ExchangeRates FX provider..") + c.Currency.ForexProviders[i].Enabled = false + c.Currency.ForexProviders[i].PrimaryProvider = false + c.Currency.ForexProviders[i].APIKey = DefaultUnsetAPIKey + c.Currency.ForexProviders[i].APIKeyLvl = -1 + continue + } + if c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey && + c.Currency.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI { + log.Warnf(log.Global, "%s enabled forex provider API key not set. Please set this in your config.json file\n", c.Currency.ForexProviders[i].Name) c.Currency.ForexProviders[i].Enabled = false c.Currency.ForexProviders[i].PrimaryProvider = false continue } - if c.Currency.ForexProviders[i].Name == "CurrencyConverter" { - if c.Currency.ForexProviders[i].Enabled && - c.Currency.ForexProviders[i].PrimaryProvider && - (c.Currency.ForexProviders[i].APIKey == "" || - c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey) { - log.Warnf("CurrencyConverter forex provider no longer supports unset API key requests. Switching to ExchangeRates FX provider..") - c.Currency.ForexProviders[i].Enabled = false - c.Currency.ForexProviders[i].PrimaryProvider = false - c.Currency.ForexProviders[i].APIKey = DefaultUnsetAPIKey - c.Currency.ForexProviders[i].APIKeyLvl = -1 - continue - } - } - if c.Currency.ForexProviders[i].APIKeyLvl == -1 && c.Currency.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI { - log.Warnf("%s APIKey Level not set, functions limited. Please set this in your config.json file", + log.Warnf(log.Global, "%s APIKey Level not set, functions limited. Please set this in your config.json file\n", c.Currency.ForexProviders[i].Name) } count++ @@ -1010,7 +1034,7 @@ func (c *Config) CheckCurrencyConfigValues() error { if c.Currency.ForexProviders[x].Name == DefaultForexProviderExchangeRatesAPI { c.Currency.ForexProviders[x].Enabled = true c.Currency.ForexProviders[x].PrimaryProvider = true - log.Warn("Using ExchangeRatesAPI for default forex provider.") + log.Warnln(log.ConfigMgr, "Using ExchangeRatesAPI for default forex provider.") } } } @@ -1026,11 +1050,11 @@ func (c *Config) CheckCurrencyConfigValues() error { if c.Currency.CryptocurrencyProvider.Enabled { if c.Currency.CryptocurrencyProvider.APIkey == "" || c.Currency.CryptocurrencyProvider.APIkey == DefaultUnsetAPIKey { - log.Warnf("CryptocurrencyProvider enabled but api key is unset please set this in your config.json file") + log.Warnln(log.ConfigMgr, "CryptocurrencyProvider enabled but api key is unset please set this in your config.json file") } if c.Currency.CryptocurrencyProvider.AccountPlan == "" || c.Currency.CryptocurrencyProvider.AccountPlan == DefaultUnsetAccountPlan { - log.Warnf("CryptocurrencyProvider enabled but account plan is unset please set this in your config.json file") + log.Warnln(log.ConfigMgr, "CryptocurrencyProvider enabled but account plan is unset please set this in your config.json file") } } else { if c.Currency.CryptocurrencyProvider.APIkey == "" { @@ -1042,8 +1066,8 @@ func (c *Config) CheckCurrencyConfigValues() error { } if c.Currency.Cryptocurrencies.Join() == "" { - if c.Cryptocurrencies.Join() != "" { - c.Currency.Cryptocurrencies = c.Cryptocurrencies + if c.Cryptocurrencies != nil { + c.Currency.Cryptocurrencies = *c.Cryptocurrencies c.Cryptocurrencies = nil } else { c.Currency.Cryptocurrencies = currency.GetDefaultCryptocurrencies() @@ -1063,19 +1087,25 @@ func (c *Config) CheckCurrencyConfigValues() error { } if c.Currency.FiatDisplayCurrency.IsEmpty() { - if c.FiatDisplayCurrency.IsEmpty() { - c.Currency.FiatDisplayCurrency = c.FiatDisplayCurrency - c.FiatDisplayCurrency = currency.NewCode("") + if c.FiatDisplayCurrency != nil { + c.Currency.FiatDisplayCurrency = *c.FiatDisplayCurrency + c.FiatDisplayCurrency = nil } else { c.Currency.FiatDisplayCurrency = currency.USD } } + + // Flush old setting which still exists + if c.FiatDisplayCurrency != nil { + c.FiatDisplayCurrency = nil + } + return nil } // RetrieveConfigCurrencyPairs splits, assigns and verifies enabled currency // pairs either cryptoCurrencies or fiatCurrencies -func (c *Config) RetrieveConfigCurrencyPairs(enabledOnly bool) error { +func (c *Config) RetrieveConfigCurrencyPairs(enabledOnly bool, assetType asset.Item) error { cryptoCurrencies := c.Currency.Cryptocurrencies fiatCurrencies := currency.GetFiatCurrencies() @@ -1084,6 +1114,11 @@ func (c *Config) RetrieveConfigCurrencyPairs(enabledOnly bool) error { continue } + supports, _ := c.SupportsExchangeAssetType(c.Exchanges[x].Name, assetType) + if !supports { + continue + } + baseCurrencies := c.Exchanges[x].BaseCurrencies for y := range baseCurrencies { if !fiatCurrencies.Contains(baseCurrencies[y]) { @@ -1093,12 +1128,17 @@ func (c *Config) RetrieveConfigCurrencyPairs(enabledOnly bool) error { } for x := range c.Exchanges { + supports, _ := c.SupportsExchangeAssetType(c.Exchanges[x].Name, assetType) + if !supports { + continue + } + var pairs []currency.Pair var err error if !c.Exchanges[x].Enabled && enabledOnly { - pairs, err = c.GetEnabledPairs(c.Exchanges[x].Name) + pairs, err = c.GetEnabledPairs(c.Exchanges[x].Name, assetType) } else { - pairs, err = c.GetAvailablePairs(c.Exchanges[x].Name) + pairs, err = c.GetAvailablePairs(c.Exchanges[x].Name, assetType) } if err != nil { @@ -1129,35 +1169,66 @@ func (c *Config) CheckLoggerConfig() error { m.Lock() defer m.Unlock() - // check if enabled is nil or level is a blank string - if c.Logging.Enabled == nil || c.Logging.Level == "" { - // Creates a new pointer to bool and sets it as true - t := func(t bool) *bool { return &t }(true) - - log.Warn("Missing or invalid config settings using safe defaults") - - // Set logger to safe defaults - - c.Logging = log.Logging{ - Enabled: t, - Level: "DEBUG|INFO|WARN|ERROR|FATAL", - ColourOutput: false, - File: "debug.txt", - Rotate: false, - } - log.Logger = &c.Logging - } else { - log.Logger = &c.Logging + if c.Logging.Enabled == nil || c.Logging.Output == "" { + c.Logging = log.GenDefaultSettings() } - if len(c.Logging.File) > 0 { - logPath := filepath.Join(common.GetDefaultDataDir(runtime.GOOS), "logs") - err := common.CreateDir(logPath) + f := func(f bool) *bool { return &f }(false) + + if c.Logging.LoggerFileConfig != nil { + if c.Logging.LoggerFileConfig.FileName == "" { + c.Logging.LoggerFileConfig.FileName = "log.txt" + } + if c.Logging.LoggerFileConfig.Rotate == nil { + c.Logging.LoggerFileConfig.Rotate = f + } + if c.Logging.LoggerFileConfig.MaxSize < 0 { + c.Logging.LoggerFileConfig.MaxSize = 100 + } + log.FileLoggingConfiguredCorrectly = true + } + + log.GlobalLogConfig = &c.Logging + + logPath := filepath.Join(common.GetDefaultDataDir(runtime.GOOS), "logs") + err := common.CreateDir(logPath) + if err != nil { + return err + } + log.LogPath = logPath + + return nil +} + +func (c *Config) checkDatabaseConfig() error { + m.Lock() + defer m.Unlock() + + if (c.Database == database.Config{}) { + c.Database.Driver = database.DBSQLite3 + c.Database.Database = database.DefaultSQLiteDatabase + } + + if !c.Database.Enabled { + return nil + } + + if !common.StringDataCompare(database.SupportedDrivers, c.Database.Driver) { + c.Database.Enabled = false + return fmt.Errorf("unsupported database driver %v, database disabled", c.Database.Driver) + } + + if c.Database.Driver == database.DBSQLite || c.Database.Driver == database.DBSQLite3 { + databaseDir := filepath.Join(common.GetDefaultDataDir(runtime.GOOS), "/database") + err := common.CreateDir(databaseDir) if err != nil { return err } - log.LogPath = logPath + database.DB.DataPath = databaseDir } + + database.DB.Config = &c.Database + return nil } @@ -1177,7 +1248,7 @@ func (c *Config) CheckNTPConfig() { } if len(c.NTPClient.Pool) < 1 { - log.Warn("NTPClient enabled with no servers configured enabling default pool") + log.Warnln(log.ConfigMgr, "NTPClient enabled with no servers configured, enabling default pool.") c.NTPClient.Pool = []string{"pool.ntp.org:123"} } } @@ -1188,8 +1259,8 @@ func (c *Config) DisableNTPCheck(input io.Reader) (string, error) { defer m.Unlock() reader := bufio.NewReader(input) - log.Warn("Your system time is out of sync, this may cause issues with trading") - log.Warn("How would you like to show future notifications? (a)lert / (w)arn / (d)isable \n") + log.Warnln(log.ConfigMgr, "Your system time is out of sync, this may cause issues with trading") + log.Warnln(log.ConfigMgr, "How would you like to show future notifications? (a)lert / (w)arn / (d)isable") var resp string answered := false @@ -1214,7 +1285,8 @@ func (c *Config) DisableNTPCheck(input io.Reader) (string, error) { resp = "Future notifications for out of time sync has been disabled" answered = true default: - log.Warn("Invalid option selected, please try again (a)lert / (w)arn / (d)isable") + log.Warnln(log.ConfigMgr, + "Invalid option selected, please try again (a)lert / (w)arn / (d)isable") } } return resp, nil @@ -1240,13 +1312,13 @@ func (c *Config) CheckConnectionMonitorConfig() { // GetFilePath returns the desired config file or the default config file name // based on if the application is being run under test or normal mode. -func GetFilePath(file string) (string, error) { - if file != "" { - return file, nil +func GetFilePath(configfile string) (string, error) { + if configfile != "" { + return configfile, nil } if flag.Lookup("test.v") != nil && !testBypass { - return ConfigTestFile, nil + return TestFile, nil } exePath, err := common.GetExecutablePath() @@ -1254,34 +1326,53 @@ func GetFilePath(file string) (string, error) { return "", err } - oldDir := exePath + common.GetOSPathSlash() - oldDirs := []string{oldDir + ConfigFile, oldDir + EncryptedConfigFile} + oldDirs := []string{ + filepath.Join(exePath, File), + filepath.Join(exePath, EncryptedFile), + } - newDir := common.GetDefaultDataDir(runtime.GOOS) + common.GetOSPathSlash() + newDir := common.GetDefaultDataDir(runtime.GOOS) err = common.CreateDir(newDir) if err != nil { return "", err } - newDirs := []string{newDir + ConfigFile, newDir + EncryptedConfigFile} + newDirs := []string{ + filepath.Join(newDir, File), + filepath.Join(newDir, EncryptedFile), + } - // First upgrade the old dir config file if it exists to the corresponding new one + // First upgrade the old dir config file if it exists to the corresponding + // new one for x := range oldDirs { _, err := os.Stat(oldDirs[x]) if os.IsNotExist(err) { continue } + _, err = os.Stat(newDirs[x]) + if !os.IsNotExist(err) { + log.Warnf(log.ConfigMgr, + "config.json file found in root dir and gct dir; cannot overwrite, defaulting to gct dir config.json at %s", + newDirs[x]) + return newDirs[x], nil + } if filepath.Ext(oldDirs[x]) == ".json" { - err = os.Rename(oldDirs[x], newDirs[0]) + err = file.Move(oldDirs[x], newDirs[0]) if err != nil { return "", err } - log.Debugf("Renamed old config file %s to %s", oldDirs[x], newDirs[0]) + log.Debugf(log.ConfigMgr, + "Renamed old config file %s to %s\n", + oldDirs[x], + newDirs[0]) } else { - err = os.Rename(oldDirs[x], newDirs[1]) + err = file.Move(oldDirs[x], newDirs[1]) if err != nil { return "", err } - log.Debugf("Renamed old config file %s to %s", oldDirs[x], newDirs[1]) + log.Debugf(log.ConfigMgr, + "Renamed old config file %s to %s\n", + oldDirs[x], + newDirs[1]) } } @@ -1292,7 +1383,7 @@ func GetFilePath(file string) (string, error) { continue } - data, err := common.ReadFile(newDirs[x]) + data, err := ioutil.ReadFile(newDirs[x]) if err != nil { return "", err } @@ -1302,7 +1393,7 @@ func GetFilePath(file string) (string, error) { return newDirs[x], nil } - err = os.Rename(newDirs[x], newDirs[1]) + err = file.Move(newDirs[x], newDirs[1]) if err != nil { return "", err } @@ -1313,7 +1404,7 @@ func GetFilePath(file string) (string, error) { return newDirs[x], nil } - err = os.Rename(newDirs[x], newDirs[0]) + err = file.Move(newDirs[x], newDirs[0]) if err != nil { return "", err } @@ -1321,67 +1412,68 @@ func GetFilePath(file string) (string, error) { return newDirs[0], nil } - return "", errors.New("config default file path error") + return "", fmt.Errorf("config.json file not found in %s, please follow README.md in root dir for config generation", + newDir) } // ReadConfig verifies and checks for encryption and verifies the unencrypted // file contains JSON. -func (c *Config) ReadConfig(configPath string) error { +func (c *Config) ReadConfig(configPath string, dryrun bool) error { defaultPath, err := GetFilePath(configPath) if err != nil { return err } - file, err := common.ReadFile(defaultPath) + fileData, err := ioutil.ReadFile(defaultPath) if err != nil { return err } - if !ConfirmECS(file) { - err = ConfirmConfigJSON(file, &c) + if !ConfirmECS(fileData) { + err = ConfirmConfigJSON(fileData, &c) if err != nil { return err } - if c.EncryptConfig == configFileEncryptionDisabled { + if c.EncryptConfig == fileEncryptionDisabled { return nil } - if c.EncryptConfig == configFileEncryptionPrompt { + if c.EncryptConfig == fileEncryptionPrompt { m.Lock() IsInitialSetup = true m.Unlock() - if c.PromptForConfigEncryption() { - c.EncryptConfig = configFileEncryptionEnabled - return c.SaveConfig(defaultPath) + if c.PromptForConfigEncryption(configPath, dryrun) { + c.EncryptConfig = fileEncryptionEnabled + return c.SaveConfig(defaultPath, dryrun) } } } else { errCounter := 0 for { - if errCounter >= configMaxAuthFailres { + if errCounter >= maxAuthFailures { return errors.New("failed to decrypt config after 3 attempts") } key, err := PromptForConfigKey(IsInitialSetup) if err != nil { - log.Errorf("PromptForConfigKey err: %s", err) + log.Errorf(log.ConfigMgr, "PromptForConfigKey err: %s", err) errCounter++ continue } var f []byte - f = append(f, file...) + f = append(f, fileData...) data, err := DecryptConfigFile(f, key) if err != nil { - log.Errorf("DecryptConfigFile err: %s", err) + log.Errorf(log.ConfigMgr, "DecryptConfigFile err: %s", err) errCounter++ continue } err = ConfirmConfigJSON(data, &c) if err != nil { - if errCounter < configMaxAuthFailres { - log.Errorf("Invalid password.") + if errCounter < maxAuthFailures { + log.Error(log.ConfigMgr, "Invalid password.") } errCounter++ continue @@ -1393,7 +1485,11 @@ func (c *Config) ReadConfig(configPath string) error { } // SaveConfig saves your configuration to your desired path -func (c *Config) SaveConfig(configPath string) error { +func (c *Config) SaveConfig(configPath string, dryrun bool) error { + if dryrun { + return nil + } + defaultPath, err := GetFilePath(configPath) if err != nil { return err @@ -1404,7 +1500,7 @@ func (c *Config) SaveConfig(configPath string) error { return err } - if c.EncryptConfig == configFileEncryptionEnabled { + if c.EncryptConfig == fileEncryptionEnabled { var key []byte if IsInitialSetup { @@ -1420,26 +1516,73 @@ func (c *Config) SaveConfig(configPath string) error { return err } } - return common.WriteFile(defaultPath, payload) + return file.Write(defaultPath, payload) +} + +// CheckRemoteControlConfig checks to see if the old c.Webserver field is used +// and migrates the existing settings to the new RemoteControl struct +func (c *Config) CheckRemoteControlConfig() { + m.Lock() + defer m.Unlock() + + if c.Webserver != nil { + port := common.ExtractPort(c.Webserver.ListenAddress) + host := common.ExtractHost(c.Webserver.ListenAddress) + + c.RemoteControl = RemoteControlConfig{ + Username: c.Webserver.AdminUsername, + Password: c.Webserver.AdminPassword, + + DeprecatedRPC: DepcrecatedRPCConfig{ + Enabled: c.Webserver.Enabled, + ListenAddress: host + ":" + strconv.Itoa(port), + }, + } + + port++ + c.RemoteControl.WebsocketRPC = WebsocketRPCConfig{ + Enabled: c.Webserver.Enabled, + ListenAddress: host + ":" + strconv.Itoa(port), + ConnectionLimit: c.Webserver.WebsocketConnectionLimit, + MaxAuthFailures: c.Webserver.WebsocketMaxAuthFailures, + AllowInsecureOrigin: c.Webserver.WebsocketAllowInsecureOrigin, + } + + port++ + gRPCProxyPort := port + 1 + c.RemoteControl.GRPC = GRPCConfig{ + Enabled: c.Webserver.Enabled, + ListenAddress: host + ":" + strconv.Itoa(port), + GRPCProxyEnabled: c.Webserver.Enabled, + GRPCProxyListenAddress: host + ":" + strconv.Itoa(gRPCProxyPort), + } + + // Then flush the old webserver settings + c.Webserver = nil + } } // CheckConfig checks all config settings func (c *Config) CheckConfig() error { - err := c.CheckExchangeConfigValues() + err := c.CheckLoggerConfig() + if err != nil { + log.Errorf(log.ConfigMgr, "Failed to configure logger, some logging features unavailable: %s\n", err) + } + + err = c.checkDatabaseConfig() + if err != nil { + log.Errorf(log.DatabaseMgr, "Failed to configure database: %v", err) + } + + err = c.CheckExchangeConfigValues() if err != nil { return fmt.Errorf(ErrCheckingConfigValues, err) } c.CheckConnectionMonitorConfig() c.CheckCommunicationsConfig() - - if c.Webserver.Enabled { - err = c.CheckWebserverConfigValues() - if err != nil { - log.Warnf(ErrCheckingConfigValues, err) - c.Webserver.Enabled = false - } - } + c.CheckClientBankAccounts() + c.CheckRemoteControlConfig() err = c.CheckCurrencyConfigValues() if err != nil { @@ -1447,16 +1590,20 @@ func (c *Config) CheckConfig() error { } if c.GlobalHTTPTimeout <= 0 { - log.Warnf("Global HTTP Timeout value not set, defaulting to %v.", configDefaultHTTPTimeout) - c.GlobalHTTPTimeout = configDefaultHTTPTimeout + log.Warnf(log.ConfigMgr, "Global HTTP Timeout value not set, defaulting to %v.\n", defaultHTTPTimeout) + c.GlobalHTTPTimeout = defaultHTTPTimeout } - return c.CheckClientBankAccounts() + if c.NTPClient.Level != 0 { + c.CheckNTPConfig() + } + + return nil } // LoadConfig loads your configuration file into your configuration object -func (c *Config) LoadConfig(configPath string) error { - err := c.ReadConfig(configPath) +func (c *Config) LoadConfig(configPath string, dryrun bool) error { + err := c.ReadConfig(configPath, dryrun) if err != nil { return fmt.Errorf(ErrFailureOpeningConfig, configPath, err) } @@ -1465,7 +1612,7 @@ func (c *Config) LoadConfig(configPath string) error { } // UpdateConfig updates the config with a supplied config file -func (c *Config) UpdateConfig(configPath string, newCfg *Config) error { +func (c *Config) UpdateConfig(configPath string, newCfg *Config, dryrun bool) error { err := newCfg.CheckConfig() if err != nil { return err @@ -1480,12 +1627,12 @@ func (c *Config) UpdateConfig(configPath string, newCfg *Config) error { c.Webserver = newCfg.Webserver c.Exchanges = newCfg.Exchanges - err = c.SaveConfig(configPath) + err = c.SaveConfig(configPath, dryrun) if err != nil { return err } - return c.LoadConfig(configPath) + return c.LoadConfig(configPath, dryrun) } // GetConfig returns a pointer to a configuration object diff --git a/config/config_encryption.go b/config/config_encryption.go index bd8a7311..51f9464d 100644 --- a/config/config_encryption.go +++ b/config/config_encryption.go @@ -5,11 +5,13 @@ import ( "crypto/aes" "crypto/cipher" "crypto/rand" + "encoding/json" "errors" "fmt" "io" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" log "github.com/thrasher-corp/gocryptotrader/logger" "golang.org/x/crypto/scrypt" ) @@ -32,8 +34,8 @@ var ( ) // PromptForConfigEncryption asks for encryption key -func (c *Config) PromptForConfigEncryption() bool { - log.Println("Would you like to encrypt your config file (y/n)?") +func (c *Config) PromptForConfigEncryption(configPath string, dryrun bool) bool { + fmt.Println("Would you like to encrypt your config file (y/n)?") input := "" _, err := fmt.Scanln(&input) @@ -42,8 +44,11 @@ func (c *Config) PromptForConfigEncryption() bool { } if !common.YesOrNo(input) { - c.EncryptConfig = configFileEncryptionDisabled - c.SaveConfig("") + c.EncryptConfig = fileEncryptionDisabled + err := c.SaveConfig(configPath, dryrun) + if err != nil { + log.Errorf(log.ConfigMgr, "cannot save config %s", err) + } return false } return true @@ -54,7 +59,7 @@ func PromptForConfigKey(initialSetup bool) ([]byte, error) { var cryptoKey []byte for { - log.Println("Please enter in your password: ") + fmt.Println("Please enter in your password: ") pwPrompt := func(i *[]byte) error { _, err := fmt.Scanln(i) return err @@ -72,7 +77,7 @@ func PromptForConfigKey(initialSetup bool) ([]byte, error) { } var p2 []byte - log.Println("Please re-enter your password: ") + fmt.Println("Please re-enter your password: ") err = pwPrompt(&p2) if err != nil { return nil, err @@ -82,7 +87,7 @@ func PromptForConfigKey(initialSetup bool) ([]byte, error) { cryptoKey = p1 break } - log.Printf("Passwords did not match, please try again.") + fmt.Printf("Passwords did not match, please try again.") } return cryptoKey, nil } @@ -164,7 +169,7 @@ func DecryptConfigFile(configData, key []byte) ([]byte, error) { // ConfirmConfigJSON confirms JSON in file func ConfirmConfigJSON(file []byte, result interface{}) error { - return common.JSONDecode(file, &result) + return json.Unmarshal(file, &result) } // ConfirmSalt checks whether the encrypted data contains a salt @@ -191,7 +196,7 @@ func getScryptDK(key, salt []byte) ([]byte, error) { func makeNewSessionDK(key []byte) ([]byte, error) { var err error - storedSalt, err = common.GetRandomSalt([]byte(SaltPrefix), SaltRandomLength) + storedSalt, err = crypto.GetRandomSalt([]byte(SaltPrefix), SaltRandomLength) if err != nil { return nil, err } diff --git a/config/config_encryption_test.go b/config/config_encryption_test.go index bc355dfa..3eabe392 100644 --- a/config/config_encryption_test.go +++ b/config/config_encryption_test.go @@ -1,16 +1,15 @@ package config import ( + "io/ioutil" "testing" - - "github.com/thrasher-corp/gocryptotrader/common" ) func TestPromptForConfigEncryption(t *testing.T) { t.Parallel() - if Cfg.PromptForConfigEncryption() { - t.Error("Test failed. PromptForConfigEncryption return incorrect bool") + if Cfg.PromptForConfigEncryption("", true) { + t.Error("PromptForConfigEncryption return incorrect bool") } } @@ -19,25 +18,25 @@ func TestPromptForConfigKey(t *testing.T) { byteyBite, err := PromptForConfigKey(true) if err == nil && len(byteyBite) > 1 { - t.Errorf("Test failed. PromptForConfigKey: %s", err) + t.Errorf("PromptForConfigKey: %s", err) } _, err = PromptForConfigKey(false) if err == nil { - t.Fatal(err) + t.Error("Expected error") } } func TestEncryptConfigFile(t *testing.T) { _, err := EncryptConfigFile([]byte("test"), nil) if err == nil { - t.Fatal("Test failed. Expected different result") + t.Fatal("Expected error") } sessionDK = []byte("a") _, err = EncryptConfigFile([]byte("test"), nil) if err == nil { - t.Fatal("Test failed. Expected different result") + t.Fatal("Expected error") } sessionDK, err = makeNewSessionDK([]byte("asdf")) @@ -61,17 +60,17 @@ func TestDecryptConfigFile(t *testing.T) { _, err = DecryptConfigFile(result, nil) if err == nil { - t.Fatal("Test failed. Expected different result") + t.Fatal("Expected error") } _, err = DecryptConfigFile([]byte("test"), nil) if err == nil { - t.Fatal("Test failed. Expected different result") + t.Fatal("Expected error") } _, err = DecryptConfigFile([]byte("test"), []byte("AAAAAAAAAAAAAAAA")) if err == nil { - t.Fatalf("Test failed. Expected %s", errAESBlockSize) + t.Fatalf("Expected %s", errAESBlockSize) } result, err = EncryptConfigFile([]byte("test"), []byte("key")) @@ -87,14 +86,14 @@ func TestDecryptConfigFile(t *testing.T) { func TestConfirmConfigJSON(t *testing.T) { var result interface{} - testConfirmJSON, err := common.ReadFile(ConfigTestFile) + testConfirmJSON, err := ioutil.ReadFile(TestFile) if err != nil { - t.Errorf("Test failed. testConfirmJSON: %s", err) + t.Errorf("testConfirmJSON: %s", err) } err = ConfirmConfigJSON(testConfirmJSON, &result) if err != nil || result == nil { - t.Errorf("Test failed. testConfirmJSON: %s", err) + t.Errorf("testConfirmJSON: %s", err) } } @@ -103,7 +102,7 @@ func TestConfirmECS(t *testing.T) { ECStest := []byte(EncryptConfirmString) if !ConfirmECS(ECStest) { - t.Errorf("Test failed. TestConfirmECS: Error finding ECS.") + t.Errorf("TestConfirmECS: Error finding ECS.") } } @@ -114,7 +113,7 @@ func TestRemoveECS(t *testing.T) { isremoved := RemoveECS(ECStest) if string(isremoved) != "" { - t.Errorf("Test failed. TestConfirmECS: Error ECS not deleted.") + t.Errorf("TestConfirmECS: Error ECS not deleted.") } } @@ -123,6 +122,6 @@ func TestMakeNewSessionDK(t *testing.T) { _, err := makeNewSessionDK(nil) if err == nil { - t.Fatal("Test failed. makeNewSessionDK passed with nil key") + t.Fatal("makeNewSessionDK passed with nil key") } } diff --git a/config/config_test.go b/config/config_test.go index c5f59b18..45a0409a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -5,7 +5,10 @@ import ( "testing" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/connchecker" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/database" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" log "github.com/thrasher-corp/gocryptotrader/logger" "github.com/thrasher-corp/gocryptotrader/ntpclient" ) @@ -14,44 +17,46 @@ const ( // Default number of enabled exchanges. Modify this whenever an exchange is // added or removed defaultEnabledExchanges = 28 + testFakeExchangeName = "Stampbit" + testPair = "BTC-USD" ) func TestGetCurrencyConfig(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. GetCurrencyConfig LoadConfig error", err) + t.Error("GetCurrencyConfig LoadConfig error", err) } _ = cfg.GetCurrencyConfig() } func TestGetExchangeBankAccounts(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. GetExchangeBankAccounts LoadConfig error", err) + t.Error("GetExchangeBankAccounts LoadConfig error", err) } _, err = cfg.GetExchangeBankAccounts("Bitfinex", "USD") if err != nil { - t.Error("Test failed. GetExchangeBankAccounts error", err) + t.Error("GetExchangeBankAccounts error", err) } _, err = cfg.GetExchangeBankAccounts("Not an exchange", "Not a currency") if err == nil { - t.Error("Test failed. GetExchangeBankAccounts, no error returned for invalid exchange") + t.Error("GetExchangeBankAccounts, no error returned for invalid exchange") } } func TestUpdateExchangeBankAccounts(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. UpdateExchangeBankAccounts LoadConfig error", err) + t.Error("UpdateExchangeBankAccounts LoadConfig error", err) } b := []BankAccount{{Enabled: false}} err = cfg.UpdateExchangeBankAccounts("Bitfinex", b) if err != nil { - t.Error("Test failed. UpdateExchangeBankAccounts error", err) + t.Error("UpdateExchangeBankAccounts error", err) } var count int for _, exch := range cfg.Exchanges { @@ -62,50 +67,50 @@ func TestUpdateExchangeBankAccounts(t *testing.T) { } } if count != 1 { - t.Error("Test failed. UpdateExchangeBankAccounts error") + t.Error("UpdateExchangeBankAccounts error") } err = cfg.UpdateExchangeBankAccounts("Not an exchange", b) if err == nil { - t.Error("Test failed. UpdateExchangeBankAccounts, no error returned for invalid exchange") + t.Error("UpdateExchangeBankAccounts, no error returned for invalid exchange") } } func TestGetClientBankAccounts(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. GetClientBankAccounts LoadConfig error", err) + t.Error("GetClientBankAccounts LoadConfig error", err) } _, err = cfg.GetClientBankAccounts("Kraken", "USD") if err != nil { - t.Error("Test failed. GetClientBankAccounts error", err) + t.Error("GetClientBankAccounts error", err) } _, err = cfg.GetClientBankAccounts("Bla", "USD") if err == nil { - t.Error("Test failed. GetClientBankAccounts error") + t.Error("GetClientBankAccounts error") } _, err = cfg.GetClientBankAccounts("Kraken", "JPY") if err == nil { - t.Error("Test failed. GetClientBankAccounts error", err) + t.Error("GetClientBankAccounts Expected error") } } func TestUpdateClientBankAccounts(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. UpdateClientBankAccounts LoadConfig error", err) + t.Error("UpdateClientBankAccounts LoadConfig error", err) } b := BankAccount{Enabled: false, BankName: "test", AccountNumber: "0234"} err = cfg.UpdateClientBankAccounts(&b) if err != nil { - t.Error("Test failed. UpdateClientBankAccounts error", err) + t.Error("UpdateClientBankAccounts error", err) } err = cfg.UpdateClientBankAccounts(&BankAccount{}) if err == nil { - t.Error("Test failed. UpdateClientBankAccounts error") + t.Error("UpdateClientBankAccounts error") } var count int @@ -117,98 +122,185 @@ func TestUpdateClientBankAccounts(t *testing.T) { } } if count != 1 { - t.Error("Test failed. UpdateClientBankAccounts error") + t.Error("UpdateClientBankAccounts error") } } func TestCheckClientBankAccounts(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. CheckClientBankAccounts LoadConfig error", err) + t.Error("CheckClientBankAccounts LoadConfig error", err) } cfg.BankAccounts = nil - err = cfg.CheckClientBankAccounts() - if err != nil || len(cfg.BankAccounts) == 0 { - t.Error("Test failed. CheckClientBankAccounts error:", err) + cfg.CheckClientBankAccounts() + if len(cfg.BankAccounts) == 0 { + t.Error("CheckClientBankAccounts error:", err) } cfg.BankAccounts = nil - cfg.BankAccounts = append(cfg.BankAccounts, BankAccount{ - Enabled: true, - BankName: "test", - }) - err = cfg.CheckClientBankAccounts() - if err.Error() != "banking details for test is enabled but variables not set correctly" { - t.Error("Test failed. CheckClientBankAccounts unexpected error:", err) + cfg.BankAccounts = []BankAccount{ + { + Enabled: true, + }, } - cfg.BankAccounts[0].BankAddress = "test" - err = cfg.CheckClientBankAccounts() - if err.Error() != "banking account details for test variables not set correctly" { - t.Error("Test failed. CheckClientBankAccounts unexpected error:", err) + cfg.CheckClientBankAccounts() + if cfg.BankAccounts[0].Enabled { + t.Error("unexpected result") } - cfg.BankAccounts[0].AccountName = "Thrasher" - cfg.BankAccounts[0].AccountNumber = "1337" - err = cfg.CheckClientBankAccounts() - if err.Error() != "critical banking numbers not set for test in Thrasher account" { - t.Error("Test failed. CheckClientBankAccounts unexpected error:", err) + b := BankAccount{ + Enabled: true, + BankName: "Commonwealth Bank of Awesome", + BankAddress: "123 Fake Street", + BankPostalCode: "1337", + BankPostalCity: "Satoshiville", + BankCountry: "Genesis", + AccountName: "Satoshi Nakamoto", + AccountNumber: "1231006505", + SupportedCurrencies: "USD", + } + cfg.BankAccounts = []BankAccount{b} + cfg.CheckClientBankAccounts() + if cfg.BankAccounts[0].Enabled || + cfg.BankAccounts[0].SupportedExchanges != "ALL" { + t.Error("unexpected result") } - cfg.BankAccounts[0].IBAN = "12345678" - err = cfg.CheckClientBankAccounts() + // AU based bank, with no BSB number (required for domestic and international + // transfers) + b.SupportedCurrencies = "AUD" + b.SWIFTCode = "BACXSI22" + cfg.BankAccounts = []BankAccount{b} + cfg.CheckClientBankAccounts() + if cfg.BankAccounts[0].Enabled { + t.Error("unexpected result") + } + + // Valid AU bank + b.BSBNumber = "061337" + cfg.BankAccounts = []BankAccount{b} + cfg.CheckClientBankAccounts() + if !cfg.BankAccounts[0].Enabled { + t.Error("unexpected result") + } + + // Valid SWIFT/IBAN compliant bank + b.Enabled = true + b.IBAN = "SI56290000170073837" + b.SWIFTCode = "BACXSI22" + cfg.BankAccounts = []BankAccount{b} + cfg.CheckClientBankAccounts() + if !cfg.BankAccounts[0].Enabled { + t.Error("unexpected result") + } +} + +func TestPurgeExchangeCredentials(t *testing.T) { + t.Parallel() + var c Config + c.Exchanges = []ExchangeConfig{ + { + Name: "test", + API: APIConfig{ + AuthenticatedSupport: true, + AuthenticatedWebsocketSupport: true, + CredentialsValidator: &APICredentialsValidatorConfig{ + RequiresKey: true, + RequiresSecret: true, + RequiresClientID: true, + }, + Credentials: APICredentialsConfig{ + Key: "asdf123", + Secret: "secretp4ssw0rd", + ClientID: "1337", + OTPSecret: "otp", + PEMKey: "aaa", + }, + }, + }, + { + Name: "test123", + API: APIConfig{ + CredentialsValidator: &APICredentialsValidatorConfig{ + RequiresKey: true, + }, + Credentials: APICredentialsConfig{ + Key: "asdf", + Secret: DefaultAPISecret, + }, + }, + }, + } + + c.PurgeExchangeAPICredentials() + + exchCfg, err := c.GetExchangeConfig("test") if err != nil { - t.Error("Test failed. CheckClientBankAccounts error:", err) + t.Error(err) } - if cfg.BankAccounts[0].SupportedExchanges == "" { - t.Error("Test failed. CheckClientBankAccounts SupportedExchanges unexpectedly nil, data:", - cfg.BankAccounts[0]) + + if exchCfg.API.Credentials.Key != DefaultAPIKey && + exchCfg.API.Credentials.ClientID != DefaultAPIClientID && + exchCfg.API.Credentials.Secret != DefaultAPISecret && + exchCfg.API.Credentials.OTPSecret != "" && + exchCfg.API.Credentials.PEMKey != "" { + t.Error("unexpected values") + } + + exchCfg, err = c.GetExchangeConfig("test123") + if err != nil { + t.Error(err) + } + + if exchCfg.API.Credentials.Key != "asdf" { + t.Error("unexpected values") } } func TestGetCommunicationsConfig(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. GetCommunicationsConfig LoadConfig error", err) + t.Error("GetCommunicationsConfig LoadConfig error", err) } _ = cfg.GetCommunicationsConfig() } func TestUpdateCommunicationsConfig(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. UpdateCommunicationsConfig LoadConfig error", err) + t.Error("UpdateCommunicationsConfig LoadConfig error", err) } cfg.UpdateCommunicationsConfig(&CommunicationsConfig{SlackConfig: SlackConfig{Name: "TEST"}}) if cfg.Communications.SlackConfig.Name != "TEST" { - t.Error("Test failed. UpdateCommunicationsConfig LoadConfig error") + t.Error("UpdateCommunicationsConfig LoadConfig error") } } func TestGetCryptocurrencyProviderConfig(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. GetCryptocurrencyProviderConfig LoadConfig error", err) + t.Error("GetCryptocurrencyProviderConfig LoadConfig error", err) } _ = cfg.GetCryptocurrencyProviderConfig() } func TestUpdateCryptocurrencyProviderConfig(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. UpdateCryptocurrencyProviderConfig LoadConfig error", err) + t.Error("UpdateCryptocurrencyProviderConfig LoadConfig error", err) } orig := cfg.GetCryptocurrencyProviderConfig() cfg.UpdateCryptocurrencyProviderConfig(CryptocurrencyProvider{Name: "SERIOUS TESTING PROCEDURE!"}) if cfg.Currency.CryptocurrencyProvider.Name != "SERIOUS TESTING PROCEDURE!" { - t.Error("Test failed. UpdateCurrencyProviderConfig LoadConfig error") + t.Error("UpdateCurrencyProviderConfig LoadConfig error") } cfg.UpdateCryptocurrencyProviderConfig(orig) @@ -216,9 +308,9 @@ func TestUpdateCryptocurrencyProviderConfig(t *testing.T) { func TestCheckCommunicationsConfig(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. CheckCommunicationsConfig LoadConfig error", err) + t.Error("CheckCommunicationsConfig LoadConfig error", err) } cfg.Communications = CommunicationsConfig{} @@ -227,7 +319,7 @@ func TestCheckCommunicationsConfig(t *testing.T) { cfg.Communications.SMSGlobalConfig.Name != "SMSGlobal" || cfg.Communications.SMTPConfig.Name != "SMTP" || cfg.Communications.TelegramConfig.Name != "Telegram" { - t.Error("Test failed. CheckCommunicationsConfig unexpected data:", + t.Error("CheckCommunicationsConfig unexpected data:", cfg.Communications) } @@ -235,7 +327,7 @@ func TestCheckCommunicationsConfig(t *testing.T) { cfg.Communications.SMSGlobalConfig.Name = "" cfg.CheckCommunicationsConfig() if cfg.Communications.SMSGlobalConfig.Password != "test" { - t.Error("Test failed. CheckCommunicationsConfig error:", err) + t.Error("CheckCommunicationsConfig error:", err) } cfg.SMS.Contacts = append(cfg.SMS.Contacts, SMSContact{ @@ -246,13 +338,25 @@ func TestCheckCommunicationsConfig(t *testing.T) { cfg.Communications.SMSGlobalConfig.Name = "" cfg.CheckCommunicationsConfig() if cfg.Communications.SMSGlobalConfig.Contacts[0].Name != "Bobby" { - t.Error("Test failed. CheckCommunicationsConfig error:", err) + t.Error("CheckCommunicationsConfig error:", err) + } + + cfg.Communications.SMSGlobalConfig.From = "" + cfg.CheckCommunicationsConfig() + if cfg.Communications.SMSGlobalConfig.From != cfg.Name { + t.Error("CheckCommunicationsConfig From value should have been set to the config name") + } + + cfg.Communications.SMSGlobalConfig.From = "aaaaaaaaaaaaaaaaaaa" + cfg.CheckCommunicationsConfig() + if cfg.Communications.SMSGlobalConfig.From != "aaaaaaaaaaa" { + t.Error("CheckCommunicationsConfig From value should have been trimmed to 11 characters") } cfg.SMS = &SMSGlobalConfig{} cfg.CheckCommunicationsConfig() if cfg.SMS != nil { - t.Error("Test failed. CheckCommunicationsConfig unexpected data:", + t.Error("CheckCommunicationsConfig unexpected data:", cfg.SMS) } @@ -263,7 +367,7 @@ func TestCheckCommunicationsConfig(t *testing.T) { cfg.Communications.SlackConfig.Enabled = true cfg.CheckCommunicationsConfig() if cfg.Communications.SlackConfig.Enabled { - t.Error("Test failed. CheckCommunicationsConfig Slack is enabled when it shouldn't be.") + t.Error("CheckCommunicationsConfig Slack is enabled when it shouldn't be.") } cfg.Communications.SlackConfig.Enabled = false @@ -271,7 +375,7 @@ func TestCheckCommunicationsConfig(t *testing.T) { cfg.Communications.SMSGlobalConfig.Password = "" cfg.CheckCommunicationsConfig() if cfg.Communications.SlackConfig.Enabled { - t.Error("Test failed. CheckCommunicationsConfig SMSGlobal is enabled when it shouldn't be.") + t.Error("CheckCommunicationsConfig SMSGlobal is enabled when it shouldn't be.") } cfg.Communications.SMSGlobalConfig.Enabled = false @@ -279,7 +383,7 @@ func TestCheckCommunicationsConfig(t *testing.T) { cfg.Communications.SMTPConfig.AccountPassword = "" cfg.CheckCommunicationsConfig() if cfg.Communications.SlackConfig.Enabled { - t.Error("Test failed. CheckCommunicationsConfig SMTPConfig is enabled when it shouldn't be.") + t.Error("CheckCommunicationsConfig SMTPConfig is enabled when it shouldn't be.") } cfg.Communications.SMTPConfig.Enabled = false @@ -287,327 +391,751 @@ func TestCheckCommunicationsConfig(t *testing.T) { cfg.Communications.TelegramConfig.VerificationToken = "" cfg.CheckCommunicationsConfig() if cfg.Communications.TelegramConfig.Enabled { - t.Error("Test failed. CheckCommunicationsConfig TelegramConfig is enabled when it shouldn't be.") + t.Error("CheckCommunicationsConfig TelegramConfig is enabled when it shouldn't be.") + } +} + +func TestGetExchangeAssetTypes(t *testing.T) { + t.Parallel() + var c Config + _, err := c.GetExchangeAssetTypes("void") + if err == nil { + t.Error("err should have been thrown on a non-existent exchange") + } + + c.Exchanges = append(c.Exchanges, + ExchangeConfig{ + Name: testFakeExchangeName, + CurrencyPairs: ¤cy.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + asset.Futures, + }, + }, + }, + ) + + var assets asset.Items + assets, err = c.GetExchangeAssetTypes(testFakeExchangeName) + if err != nil { + t.Error(err) + } + + if assets.JoinToString(",") != "spot,futures" { + t.Error("unexpected results") + } + + c.Exchanges[0].CurrencyPairs = nil + _, err = c.GetExchangeAssetTypes(testFakeExchangeName) + if err == nil { + t.Error("Expected error from nil currency pair") + } +} + +func TestSupportsExchangeAssetType(t *testing.T) { + t.Parallel() + var c Config + _, err := c.SupportsExchangeAssetType("void", asset.Spot) + if err == nil { + t.Error("Expected error for non-existent exchange") + } + + c.Exchanges = append(c.Exchanges, + ExchangeConfig{ + Name: testFakeExchangeName, + CurrencyPairs: ¤cy.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + asset.Futures, + }, + }, + }, + ) + + supports, err := c.SupportsExchangeAssetType(testFakeExchangeName, asset.Spot) + if err != nil { + t.Error(err) + } + + if !supports { + t.Error("exchange should support spot asset item") + } + + _, err = c.SupportsExchangeAssetType(testFakeExchangeName, "asdf") + if err == nil { + t.Error("Expected error from invalid asset item") + } + + c.Exchanges[0].CurrencyPairs = nil + _, err = c.SupportsExchangeAssetType(testFakeExchangeName, asset.Spot) + if err == nil { + t.Error("Expected error from nil pair manager") + } +} + +func TestCheckExchangeAssetsConsistency(t *testing.T) { + t.Parallel() + var c Config + // Test for non-existent exchange + c.CheckExchangeAssetsConsistency("void") + + c.Exchanges = append(c.Exchanges, + ExchangeConfig{ + Name: testFakeExchangeName, + }, + ) + + // Tests for nil currency pairs store but valid exchange name + c.CheckExchangeAssetsConsistency(testFakeExchangeName) + + // Simulate testing a diff between stored asset types (config loading) + // and pair store + c.Exchanges[0].CurrencyPairs = ¤cy.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + asset.Futures, + asset.Index, + }, + } + c.Exchanges[0].CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) + c.Exchanges[0].CurrencyPairs.Pairs[asset.PerpetualContract] = ¤cy.PairStore{} + c.CheckExchangeAssetsConsistency(testFakeExchangeName) + + supports, err := c.SupportsExchangeAssetType(testFakeExchangeName, asset.PerpetualContract) + if err != nil { + t.Error(err) + } + + if supports { + t.Error("perpetual contract should have been removed from the pair manager") + } +} + +func TestSetPairs(t *testing.T) { + t.Parallel() + + var c Config + pairs := currency.Pairs{ + currency.NewPair(currency.BTC, currency.USD), + currency.NewPair(currency.BTC, currency.EUR), + } + + err := c.SetPairs("asdf", asset.Spot, true, nil) + if err == nil { + t.Error("Expected error from nil pairs") + } + + err = c.SetPairs("asdf", asset.Spot, true, pairs) + if err == nil { + t.Error("Expected error from non-existent exchange") + } + + c.Exchanges = append(c.Exchanges, + ExchangeConfig{ + Name: testFakeExchangeName, + }, + ) + + err = c.SetPairs(testFakeExchangeName, asset.Index, true, pairs) + if err == nil { + t.Error("Expected error from non initialised pair manager") + } + + c.Exchanges[0].CurrencyPairs = ¤cy.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + asset.Futures, + }, + } + + err = c.SetPairs(testFakeExchangeName, asset.Index, true, pairs) + if err == nil { + t.Error("Expected error from non supported asset type") + } + + err = c.SetPairs(testFakeExchangeName, asset.Spot, true, pairs) + if err != nil { + t.Error(err) + } +} + +func TestGetCurrencyPairConfig(t *testing.T) { + t.Parallel() + + var c Config + _, err := c.GetCurrencyPairConfig("asdfg", asset.Spot) + if err == nil { + t.Error("Expected error with non-existent exchange") + } + + c.Exchanges = append(c.Exchanges, + ExchangeConfig{ + Name: testFakeExchangeName, + }, + ) + + _, err = c.GetCurrencyPairConfig(testFakeExchangeName, asset.Index) + if err == nil { + t.Error("Expected error with nil currency pair store") + } + + pm := ¤cy.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + asset.Futures, + }, + Pairs: map[asset.Item]*currency.PairStore{ + asset.Spot: { + RequestFormat: ¤cy.PairFormat{ + Uppercase: false, + Delimiter: "_", + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "~", + }, + }, + }, + } + + c.Exchanges[0].CurrencyPairs = pm + _, err = c.GetCurrencyPairConfig(testFakeExchangeName, asset.Index) + if err == nil { + t.Error("Expected error with unsupported asset") + } + + var p *currency.PairStore + p, err = c.GetCurrencyPairConfig(testFakeExchangeName, asset.Spot) + if err != nil { + t.Error(err) + } + + if p.RequestFormat.Delimiter != "_" || + p.RequestFormat.Uppercase || + !p.ConfigFormat.Uppercase || + p.ConfigFormat.Delimiter != "~" { + t.Error("unexpected values") + } +} + +func TestCheckPairConfigFormats(t *testing.T) { + var c Config + if err := c.CheckPairConfigFormats("non-existent"); err == nil { + t.Error("non-existent exchange should throw an error") + } + + // Test nil pair store + c.Exchanges = append(c.Exchanges, + ExchangeConfig{ + Name: testFakeExchangeName, + CurrencyPairs: ¤cy.PairsManager{ + AssetTypes: asset.Items{ + asset.Item("wrong"), + }, + }, + }, + ) + + if err := c.CheckPairConfigFormats(testFakeExchangeName); err == nil { + t.Error("nil pair store should return an error") + } + + c.Exchanges[0].CurrencyPairs.AssetTypes = asset.Items{asset.Spot} + c.Exchanges[0].CurrencyPairs.Pairs = map[asset.Item]*currency.PairStore{ + asset.Spot: { + RequestFormat: ¤cy.PairFormat{}, + ConfigFormat: ¤cy.PairFormat{}, + }, + asset.Futures: { + RequestFormat: ¤cy.PairFormat{}, + ConfigFormat: ¤cy.PairFormat{}, + }, + } + if err := c.CheckPairConfigFormats(testFakeExchangeName); err != nil { + t.Error("nil pairs should be okay to continue") + } + + // Test having a pair index and delimiter set at the same time throws an error + c.Exchanges[0].CurrencyPairs.AssetTypes = asset.Items{asset.Spot} + c.Exchanges[0].CurrencyPairs.Pairs = map[asset.Item]*currency.PairStore{ + asset.Spot: { + RequestFormat: ¤cy.PairFormat{ + Uppercase: false, + Delimiter: "_", + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "~", + Index: "USD", + }, + Available: currency.Pairs{ + currency.NewPairDelimiter(testPair, "-"), + }, + Enabled: currency.Pairs{ + currency.NewPairDelimiter("BTC~USD", "~"), + }, + }, + } + + if err := c.CheckPairConfigFormats(testFakeExchangeName); err == nil { + t.Error("invalid pair delimiter and index should throw an error") + } + + // Test wrong pair delimiter throws an error + c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].ConfigFormat.Index = "" + if err := c.CheckPairConfigFormats(testFakeExchangeName); err == nil { + t.Error("invalid pair delimiter should throw an error") + } + + // Test wrong pair index in the enabled pairs throw an error + c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ + ConfigFormat: ¤cy.PairFormat{ + Index: currency.AUD.String(), + }, + } + c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Available = currency.Pairs{ + currency.NewPair(currency.BTC, currency.AUD), + } + c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = currency.Pairs{ + currency.NewPair(currency.BTC, currency.KRW), + } + + if err := c.CheckPairConfigFormats(testFakeExchangeName); err == nil { + t.Error("invalid pair index should throw an error") } } func TestCheckPairConsistency(t *testing.T) { - cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) - if err != nil { - t.Error("Test failed. CheckPairConsistency LoadConfig error", err) + t.Parallel() + + var c Config + if err := c.CheckPairConsistency("asdf"); err == nil { + t.Error("non-existent exchange should return an error") } - err = cfg.CheckPairConsistency("asdf") - if err == nil { - t.Error("Test failed. CheckPairConsistency. Non-existent exchange returned nil error") - } - - cfg.Exchanges = append(cfg.Exchanges, ExchangeConfig{ - Name: "TestExchange", - Enabled: true, - AvailablePairs: currency.NewPairsFromStrings([]string{"DOGE_USD,DOGE_AUD"}), - EnabledPairs: currency.NewPairsFromStrings([]string{"DOGE_USD,DOGE_AUD,DOGE_BTC"}), - ConfigCurrencyPairFormat: &CurrencyPairFormatConfig{ - Uppercase: true, - Delimiter: "_", + c.Exchanges = append(c.Exchanges, + ExchangeConfig{ + Name: testFakeExchangeName, + CurrencyPairs: ¤cy.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + }, }, - }) - tec, err := cfg.GetExchangeConfig("TestExchange") - if err != nil { - t.Error("Test failed. CheckPairConsistency GetExchangeConfig error", err) + ) + + // Test nil pair store + if err := c.CheckPairConsistency(testFakeExchangeName); err == nil { + t.Error("nil pair store should return an error") } - err = cfg.CheckPairConsistency("TestExchange") - if err != nil { - t.Error("Test failed. CheckPairConsistency error:", err) - } - // Calling again immediately to hit the if !update {return nil} - err = cfg.CheckPairConsistency("TestExchange") - if err != nil { - t.Error("Test failed. CheckPairConsistency error:", err) + c.Exchanges[0].CurrencyPairs.Pairs = map[asset.Item]*currency.PairStore{ + asset.Spot: { + RequestFormat: ¤cy.PairFormat{ + Uppercase: false, + Delimiter: "_", + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "_", + }, + Enabled: currency.Pairs{ + currency.NewPairDelimiter("BTC_USD", "_"), + }, + }, } - tec.EnabledPairs = currency.NewPairsFromStrings([]string{"DOGE_LTC,BTC_LTC"}) - err = cfg.UpdateExchangeConfig(&tec) - if err != nil { - t.Error("Test failed. CheckPairConsistency Update config failed, error:", err) + // Test for nil avail pairs + if err := c.CheckPairConsistency(testFakeExchangeName); err != nil { + t.Error("nil available pairs should continue") } - err = cfg.CheckPairConsistency("TestExchange") - if err != nil { - t.Error("Test failed. CheckPairConsistency error:", err) + // Test that enabled pair is not found in the available pairs + c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Available = currency.Pairs{ + currency.NewPairDelimiter("LTC_USD", "_"), + } + if err := c.CheckPairConsistency(testFakeExchangeName); err != nil { + t.Error("unexpected result") + } + + // Test that an empty enabled pair is populated with an available pair + c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = nil + if err := c.CheckPairConsistency(testFakeExchangeName); err != nil { + t.Error("unexpected result") + } + + // Test that an invalid enabled pair is removed from the list + c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = currency.Pairs{ + currency.NewPairDelimiter("LTC_USD", "_"), + currency.NewPairDelimiter("BTC_USD", "_"), + } + if err := c.CheckPairConsistency(testFakeExchangeName); err != nil { + t.Error("unexpected result") + } + + // Test when no update is required as the available pairs and enabled pairs + // are consistent + if err := c.CheckPairConsistency(testFakeExchangeName); err != nil { + t.Error("unexpected result") } } func TestSupportsPair(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { t.Errorf( - "Test failed. TestSupportsPair. LoadConfig Error: %s", err.Error(), + "TestSupportsPair. LoadConfig Error: %s", err.Error(), ) } + assetType := asset.Spot _, err = cfg.SupportsPair("asdf", - currency.NewPair(currency.BTC, currency.USD)) + currency.NewPair(currency.BTC, currency.USD), assetType) if err == nil { t.Error( - "Test failed. TestSupportsPair. Non-existent exchange returned nil error", + "TestSupportsPair. Expected error from Non-existent exchange", ) } _, err = cfg.SupportsPair("Bitfinex", - currency.NewPair(currency.BTC, currency.USD)) + currency.NewPair(currency.BTC, currency.USD), assetType) if err != nil { t.Errorf( - "Test failed. TestSupportsPair. Incorrect values. Err: %s", err, + "TestSupportsPair. Incorrect values. Err: %s", err, ) } } -func TestGetAvailablePairs(t *testing.T) { - cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) - if err != nil { - t.Errorf( - "Test failed. TestGetAvailablePairs. LoadConfig Error: %s", err.Error()) - } +func TestGetPairFormat(t *testing.T) { + t.Parallel() - _, err = cfg.GetAvailablePairs("asdf") + var c Config + _, err := c.GetPairFormat("meow", asset.Spot) if err == nil { - t.Error( - "Test failed. TestGetAvailablePairs. Non-existent exchange returned nil error") + t.Error("Expected error from non-existent exchange") } - _, err = cfg.GetAvailablePairs("Bitfinex") + c.Exchanges = append(c.Exchanges, + ExchangeConfig{ + Name: testFakeExchangeName, + }, + ) + _, err = c.GetPairFormat(testFakeExchangeName, asset.Spot) + if err == nil { + t.Error("Expected error from nil pair manager") + } + + c.Exchanges[0].CurrencyPairs = ¤cy.PairsManager{ + AssetTypes: asset.Items{asset.Spot}, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: false, + Delimiter: "_", + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "_", + }, + } + _, err = c.GetPairFormat(testFakeExchangeName, asset.Item("invalid")) + if err == nil { + t.Error("Expected error from non-existent asset item") + } + + _, err = c.GetPairFormat(testFakeExchangeName, asset.Futures) + if err == nil { + t.Error("Expected error from valid but non supported asset type") + } + + var p currency.PairFormat + p, err = c.GetPairFormat(testFakeExchangeName, asset.Spot) if err != nil { - t.Errorf( - "Test failed. TestGetAvailablePairs. Incorrect values. Err: %s", err) + t.Error(err) + } + + if !p.Uppercase && p.Delimiter != "_" { + t.Error("unexpected results") + } + + // Test nil pair store + c.Exchanges[0].CurrencyPairs.UseGlobalFormat = false + _, err = c.GetPairFormat(testFakeExchangeName, asset.Spot) + if err == nil { + t.Error("Expected error") + } + + c.Exchanges[0].CurrencyPairs.Pairs = map[asset.Item]*currency.PairStore{ + asset.Spot: { + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "~", + }, + }, + } + p, err = c.GetPairFormat(testFakeExchangeName, asset.Spot) + if err != nil { + t.Error(err) + } + + if p.Delimiter != "~" && !p.Uppercase { + t.Error("unexpected results") + } +} + +func TestGetAvailablePairs(t *testing.T) { + t.Parallel() + + var c Config + _, err := c.GetAvailablePairs("asdf", asset.Spot) + if err == nil { + t.Error("Expected error from non-existent exchange") + } + + c.Exchanges = append(c.Exchanges, + ExchangeConfig{ + Name: testFakeExchangeName, + CurrencyPairs: ¤cy.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + }, + }, + ) + + _, err = c.GetAvailablePairs(testFakeExchangeName, asset.Spot) + if err == nil { + t.Error("Expected error from nil pair manager") + } + + c.Exchanges[0].CurrencyPairs.Pairs = map[asset.Item]*currency.PairStore{ + asset.Spot: { + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + }, + } + _, err = c.GetAvailablePairs(testFakeExchangeName, asset.Spot) + if err != nil { + t.Error("Expected error from nil pairs") + } + + c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Available = currency.Pairs{ + currency.NewPair(currency.BTC, currency.USD), + } + _, err = c.GetAvailablePairs(testFakeExchangeName, asset.Spot) + if err != nil { + t.Error(err) } } func TestGetEnabledPairs(t *testing.T) { - cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) - if err != nil { - t.Errorf( - "Test failed. TestGetEnabledPairs. LoadConfig Error: %s", err.Error()) - } + t.Parallel() - _, err = cfg.GetEnabledPairs("asdf") + var c Config + _, err := c.GetEnabledPairs("asdf", asset.Spot) if err == nil { - t.Error( - "Test failed. TestGetEnabledPairs. Non-existent exchange returned nil error") + t.Error("Expected error from non-existent exchange") } - _, err = cfg.GetEnabledPairs("Bitfinex") + c.Exchanges = append(c.Exchanges, + ExchangeConfig{ + Name: testFakeExchangeName, + CurrencyPairs: ¤cy.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + }, + }, + ) + + _, err = c.GetEnabledPairs(testFakeExchangeName, asset.Spot) + if err == nil { + t.Error("Expected error from nil pair manager") + } + + c.Exchanges[0].CurrencyPairs.Pairs = map[asset.Item]*currency.PairStore{ + asset.Spot: { + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + }, + } + _, err = c.GetEnabledPairs(testFakeExchangeName, asset.Spot) if err != nil { - t.Errorf( - "Test failed. TestGetEnabledPairs. Incorrect values. Err: %s", err) + t.Error("nil pairs should return a nil error") + } + + c.Exchanges[0].CurrencyPairs.Pairs[asset.Spot].Enabled = currency.Pairs{ + currency.NewPair(currency.BTC, currency.USD), + } + _, err = c.GetEnabledPairs(testFakeExchangeName, asset.Spot) + if err != nil { + t.Error(err) } } func TestGetEnabledExchanges(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { t.Errorf( - "Test failed. TestGetEnabledExchanges. LoadConfig Error: %s", err.Error(), + "TestGetEnabledExchanges. LoadConfig Error: %s", err.Error(), ) } exchanges := cfg.GetEnabledExchanges() if len(exchanges) != defaultEnabledExchanges { t.Error( - "Test failed. TestGetEnabledExchanges. Enabled exchanges value mismatch", + "TestGetEnabledExchanges. Enabled exchanges value mismatch", ) } if !common.StringDataCompare(exchanges, "Bitfinex") { t.Error( - "Test failed. TestGetEnabledExchanges. Expected exchange Bitfinex not found", + "TestGetEnabledExchanges. Expected exchange Bitfinex not found", ) } } func TestGetDisabledExchanges(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { t.Errorf( - "Test failed. TestGetDisabledExchanges. LoadConfig Error: %s", err.Error(), + "TestGetDisabledExchanges. LoadConfig Error: %s", err.Error(), ) } exchanges := cfg.GetDisabledExchanges() if len(exchanges) != 0 { t.Error( - "Test failed. TestGetDisabledExchanges. Enabled exchanges value mismatch", + "TestGetDisabledExchanges. Enabled exchanges value mismatch", ) } exchCfg, err := cfg.GetExchangeConfig("Bitfinex") if err != nil { t.Errorf( - "Test failed. TestGetDisabledExchanges. GetExchangeConfig Error: %s", err.Error(), + "TestGetDisabledExchanges. GetExchangeConfig Error: %s", err.Error(), ) } exchCfg.Enabled = false - err = cfg.UpdateExchangeConfig(&exchCfg) + err = cfg.UpdateExchangeConfig(exchCfg) if err != nil { t.Errorf( - "Test failed. TestGetDisabledExchanges. UpdateExchangeConfig Error: %s", err.Error(), + "TestGetDisabledExchanges. UpdateExchangeConfig Error: %s", err.Error(), ) } if len(cfg.GetDisabledExchanges()) != 1 { t.Error( - "Test failed. TestGetDisabledExchanges. Enabled exchanges value mismatch", + "TestGetDisabledExchanges. Enabled exchanges value mismatch", ) } } func TestCountEnabledExchanges(t *testing.T) { GetConfigEnabledExchanges := GetConfig() - err := GetConfigEnabledExchanges.LoadConfig(ConfigTestFile) + err := GetConfigEnabledExchanges.LoadConfig(TestFile, true) if err != nil { t.Error( - "Test failed. GetConfigEnabledExchanges load config error: " + err.Error(), + "GetConfigEnabledExchanges load config error: " + err.Error(), ) } enabledExch := GetConfigEnabledExchanges.CountEnabledExchanges() if enabledExch != defaultEnabledExchanges { - t.Errorf("Test failed. Expected %v, Received %v", defaultEnabledExchanges, enabledExch) - } -} - -func TestGetConfigCurrencyPairFormat(t *testing.T) { - cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) - if err != nil { - t.Errorf( - "Test failed. TestGetConfigCurrencyPairFormat. LoadConfig Error: %s", err.Error(), - ) - } - _, err = cfg.GetConfigCurrencyPairFormat("asdasdasd") - if err == nil { - t.Errorf( - "Test failed. TestGetRequestCurrencyPairFormat. Non-existent exchange returned nil error", - ) - } - - exchFmt, err := cfg.GetConfigCurrencyPairFormat("Yobit") - if err != nil { - t.Errorf("Test failed. TestGetConfigCurrencyPairFormat err: %s", err) - } - if !exchFmt.Uppercase || exchFmt.Delimiter != "_" { - t.Errorf( - "Test failed. TestGetConfigCurrencyPairFormat. Invalid values", - ) - } -} - -func TestGetRequestCurrencyPairFormat(t *testing.T) { - cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) - if err != nil { - t.Errorf( - "Test failed. TestGetRequestCurrencyPairFormat. LoadConfig Error: %s", err.Error(), - ) - } - - _, err = cfg.GetRequestCurrencyPairFormat("asdasdasd") - if err == nil { - t.Errorf( - "Test failed. TestGetRequestCurrencyPairFormat. Non-existent exchange returned nil error", - ) - } - - exchFmt, err := cfg.GetRequestCurrencyPairFormat("Yobit") - if err != nil { - t.Errorf("Test failed. TestGetRequestCurrencyPairFormat. Err: %s", err) - } - if exchFmt.Uppercase || exchFmt.Delimiter != "_" || exchFmt.Separator != "-" { - t.Errorf( - "Test failed. TestGetRequestCurrencyPairFormat. Invalid values", - ) + t.Errorf("Expected %v, Received %v", defaultEnabledExchanges, enabledExch) } } func TestGetCurrencyPairDisplayConfig(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { t.Errorf( - "Test failed. GetCurrencyPairDisplayConfig. LoadConfig Error: %s", err.Error(), + "GetCurrencyPairDisplayConfig. LoadConfig Error: %s", err.Error(), ) } settings := cfg.GetCurrencyPairDisplayConfig() if settings.Delimiter != "-" || !settings.Uppercase { t.Errorf( - "Test failed. GetCurrencyPairDisplayConfi. Invalid values", + "GetCurrencyPairDisplayConfi. Invalid values", ) } } func TestGetAllExchangeConfigs(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. GetAllExchangeConfigs. LoadConfig error", err) + t.Error("GetAllExchangeConfigs. LoadConfig error", err) } if len(cfg.GetAllExchangeConfigs()) < 26 { - t.Error("Test failed. GetAllExchangeConfigs error") + t.Error("GetAllExchangeConfigs error") } } func TestGetExchangeConfig(t *testing.T) { GetExchangeConfig := GetConfig() - err := GetExchangeConfig.LoadConfig(ConfigTestFile) + err := GetExchangeConfig.LoadConfig(TestFile, true) if err != nil { t.Errorf( - "Test failed. GetExchangeConfig.LoadConfig Error: %s", err.Error(), + "GetExchangeConfig.LoadConfig Error: %s", err.Error(), ) } _, err = GetExchangeConfig.GetExchangeConfig("ANX") if err != nil { - t.Errorf("Test failed. GetExchangeConfig.GetExchangeConfig Error: %s", + t.Errorf("GetExchangeConfig.GetExchangeConfig Error: %s", err.Error()) } _, err = GetExchangeConfig.GetExchangeConfig("Testy") if err == nil { - t.Error("Test failed. GetExchangeConfig.GetExchangeConfig Error") + t.Error("GetExchangeConfig.GetExchangeConfig Expected error") } } func TestGetForexProviderConfig(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. GetForexProviderConfig. LoadConfig error", err) + t.Error("GetForexProviderConfig. LoadConfig error", err) } _, err = cfg.GetForexProviderConfig("Fixer") if err != nil { - t.Error("Test failed. GetForexProviderConfig error", err) + t.Error("GetForexProviderConfig error", err) } _, err = cfg.GetForexProviderConfig("this is not a forex provider") if err == nil { - t.Error("Test failed. GetForexProviderConfig no error for invalid provider") + t.Error("GetForexProviderConfig no error for invalid provider") + } +} + +func TestGetForexProvidersConfig(t *testing.T) { + cfg := GetConfig() + err := cfg.LoadConfig(TestFile, true) + if err != nil { + t.Error(err) + } + + if r := cfg.GetForexProvidersConfig(); len(r) != 5 { + t.Error("unexpected length of forex providers") } } func TestGetPrimaryForexProvider(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. GetPrimaryForexProvider. LoadConfig error", err) + t.Error("GetPrimaryForexProvider. LoadConfig error", err) } primary := cfg.GetPrimaryForexProvider() if primary == "" { - t.Error("Test failed. GetPrimaryForexProvider error") + t.Error("GetPrimaryForexProvider error") } for i := range cfg.Currency.ForexProviders { @@ -615,264 +1143,381 @@ func TestGetPrimaryForexProvider(t *testing.T) { } primary = cfg.GetPrimaryForexProvider() if primary != "" { - t.Error("Test failed. GetPrimaryForexProvider error, expected nil got:", primary) + t.Error("GetPrimaryForexProvider error, expected nil got:", primary) } } func TestUpdateExchangeConfig(t *testing.T) { - UpdateExchangeConfig := GetConfig() - err := UpdateExchangeConfig.LoadConfig(ConfigTestFile) + c := GetConfig() + err := c.LoadConfig(TestFile, true) if err != nil { - t.Errorf( - "Test failed. UpdateExchangeConfig.LoadConfig Error: %s", err.Error(), - ) + t.Error(err) } - e, err2 := UpdateExchangeConfig.GetExchangeConfig("ANX") - if err2 != nil { - t.Errorf( - "Test failed. UpdateExchangeConfig.GetExchangeConfig: %s", err.Error(), - ) - } - e.APIKey = "test1234" - err3 := UpdateExchangeConfig.UpdateExchangeConfig(&e) - if err3 != nil { - t.Errorf( - "Test failed. UpdateExchangeConfig.UpdateExchangeConfig: %s", err.Error(), - ) - } - e.Name = "testyTest" - err = UpdateExchangeConfig.UpdateExchangeConfig(&e) + + e := &ExchangeConfig{} + err = c.UpdateExchangeConfig(e) if err == nil { - t.Error("Test failed. UpdateExchangeConfig.UpdateExchangeConfig Error") + t.Error("Expected error from non-existent exchange") + } + + e, err = c.GetExchangeConfig("ANX") + if err != nil { + t.Error(err) + } + + e.API.Credentials.Key = "test1234" + err = c.UpdateExchangeConfig(e) + if err != nil { + t.Error(err) } } // TestCheckExchangeConfigValues logic test func TestCheckExchangeConfigValues(t *testing.T) { - checkExchangeConfigValues := Config{} - err := checkExchangeConfigValues.LoadConfig(ConfigTestFile) + var cfg Config + if err := cfg.CheckExchangeConfigValues(); err == nil { + t.Error("nil exchanges should throw an err") + } + + err := cfg.LoadConfig(TestFile, true) if err != nil { - t.Errorf( - "Test failed. checkExchangeConfigValues.LoadConfig: %s", err.Error(), - ) + t.Fatal(err) } - err = checkExchangeConfigValues.CheckExchangeConfigValues() + + // Test our default test config and report any errors + err = cfg.CheckExchangeConfigValues() if err != nil { - t.Errorf( - "Test failed. checkExchangeConfigValues.CheckExchangeConfigValues: %s", - err.Error(), - ) + t.Fatal(err) } - checkExchangeConfigValues.Exchanges[0].WebsocketResponseMaxLimit = 0 - checkExchangeConfigValues.Exchanges[0].WebsocketResponseCheckTimeout = 0 - checkExchangeConfigValues.Exchanges[0].WebsocketOrderbookBufferLimit = 0 - checkExchangeConfigValues.Exchanges[0].HTTPTimeout = 0 - err = checkExchangeConfigValues.CheckExchangeConfigValues() + cfg.Exchanges[0].Name = "GDAX" + err = cfg.CheckExchangeConfigValues() if err != nil { - t.Errorf("Test failed. checkExchangeConfigValues.CheckExchangeConfigValues: %s", - err.Error(), - ) + t.Error(err) + } + if cfg.Exchanges[0].Name != "CoinbasePro" { + t.Error("exchange name should have been updated from GDAX to CoinbasePRo") } - if checkExchangeConfigValues.Exchanges[0].HTTPTimeout == 0 { - t.Fatalf("Test failed. Expected exchange %s to have updated HTTPTimeout value", checkExchangeConfigValues.Exchanges[0].Name) - } + // Test API settings migration + sptr := func(s string) *string { return &s } + bptr := func(b bool) *bool { return &b } + int64ptr := func(i int64) *int64 { return &i } - if checkExchangeConfigValues.Exchanges[0].WebsocketResponseMaxLimit == 0 { - t.Fatalf("Test failed. Expected exchange %s to have updated WebsocketResponseMaxLimit value", checkExchangeConfigValues.Exchanges[0].Name) - } - - if checkExchangeConfigValues.Exchanges[0].WebsocketOrderbookBufferLimit == 0 { - t.Fatalf("Test failed. Expected exchange %s to have updated WebsocketOrderbookBufferLimit value", checkExchangeConfigValues.Exchanges[0].Name) - } - - checkExchangeConfigValues.Exchanges[0].APIKey = "Key" - checkExchangeConfigValues.Exchanges[0].APISecret = "Secret" - checkExchangeConfigValues.Exchanges[0].AuthenticatedAPISupport = true - checkExchangeConfigValues.Exchanges[0].AuthenticatedWebsocketAPISupport = true - err = checkExchangeConfigValues.CheckExchangeConfigValues() + cfg.Exchanges[0].APIKey = sptr("awesomeKey") + cfg.Exchanges[0].APISecret = sptr("meowSecret") + cfg.Exchanges[0].ClientID = sptr("clientIDerino") + cfg.Exchanges[0].APIAuthPEMKey = sptr("-----BEGIN EC PRIVATE KEY-----\nASDF\n-----END EC PRIVATE KEY-----\n") + cfg.Exchanges[0].APIAuthPEMKeySupport = bptr(true) + cfg.Exchanges[0].AuthenticatedAPISupport = bptr(true) + cfg.Exchanges[0].AuthenticatedWebsocketAPISupport = bptr(true) + cfg.Exchanges[0].WebsocketURL = sptr("wss://1337") + cfg.Exchanges[0].APIURL = sptr(APIURLNonDefaultMessage) + cfg.Exchanges[0].APIURLSecondary = sptr(APIURLNonDefaultMessage) + err = cfg.CheckExchangeConfigValues() if err != nil { - t.Errorf( - "Test failed. checkExchangeConfigValues.CheckExchangeConfigValues Error", - ) - } - if checkExchangeConfigValues.Exchanges[0].AuthenticatedWebsocketAPISupport { - t.Error("Expected AuthenticatedWebsocketAPISupport to be false from invalid API keys") + t.Error(err) } - checkExchangeConfigValues.Exchanges[0].AuthenticatedAPISupport = true - checkExchangeConfigValues.Exchanges[0].APIKey = "TESTYTEST" - checkExchangeConfigValues.Exchanges[0].APISecret = "TESTYTEST" - checkExchangeConfigValues.Exchanges[0].Name = "ITBIT" - err = checkExchangeConfigValues.CheckExchangeConfigValues() - if err != nil { - t.Errorf( - "Test failed. checkExchangeConfigValues.CheckExchangeConfigValues Error", - ) - } - if checkExchangeConfigValues.Exchanges[0].AuthenticatedAPISupport { - t.Error("Expected AuthenticatedAPISupport to be true from valid API keys") + // Ensure that all of our previous settings are migrated + if cfg.Exchanges[0].API.Credentials.Key != "awesomeKey" || + cfg.Exchanges[0].API.Credentials.Secret != "meowSecret" || + cfg.Exchanges[0].API.Credentials.ClientID != "clientIDerino" || + !strings.Contains(cfg.Exchanges[0].API.Credentials.PEMKey, "ASDF") || + !cfg.Exchanges[0].API.PEMKeySupport || + !cfg.Exchanges[0].API.AuthenticatedSupport || + !cfg.Exchanges[0].API.AuthenticatedWebsocketSupport || + cfg.Exchanges[0].API.Endpoints.WebsocketURL != "wss://1337" || + cfg.Exchanges[0].API.Endpoints.URL != APIURLNonDefaultMessage || + cfg.Exchanges[0].API.Endpoints.URLSecondary != APIURLNonDefaultMessage { + t.Error("unexpected values") } - checkExchangeConfigValues.Exchanges[0].AuthenticatedAPISupport = true - checkExchangeConfigValues.Exchanges[0].AuthenticatedWebsocketAPISupport = true - checkExchangeConfigValues.Exchanges[0].APIKey = "" - checkExchangeConfigValues.Exchanges[0].APISecret = "" - checkExchangeConfigValues.Exchanges[0].Name = "ITBIT" - err = checkExchangeConfigValues.CheckExchangeConfigValues() - if err != nil { - t.Errorf( - "Test failed. checkExchangeConfigValues.CheckExchangeConfigValues Error", - ) + if cfg.Exchanges[0].APIKey != nil || + cfg.Exchanges[0].APISecret != nil || + cfg.Exchanges[0].ClientID != nil || + cfg.Exchanges[0].APIAuthPEMKey != nil || + cfg.Exchanges[0].APIAuthPEMKeySupport != nil || + cfg.Exchanges[0].AuthenticatedAPISupport != nil || + cfg.Exchanges[0].AuthenticatedWebsocketAPISupport != nil || + cfg.Exchanges[0].WebsocketURL != nil || + cfg.Exchanges[0].APIURL != nil || + cfg.Exchanges[0].APIURLSecondary != nil { + t.Error("unexpected values") } - if checkExchangeConfigValues.Exchanges[0].AuthenticatedAPISupport || checkExchangeConfigValues.Exchanges[0].AuthenticatedWebsocketAPISupport { + + // Test feature and endpoint migrations migrations + cfg.Exchanges[0].Features = nil + cfg.Exchanges[0].SupportsAutoPairUpdates = bptr(true) + cfg.Exchanges[0].Websocket = bptr(true) + cfg.Exchanges[0].API.Endpoints.URL = "" + cfg.Exchanges[0].API.Endpoints.URLSecondary = "" + cfg.Exchanges[0].API.Endpoints.WebsocketURL = "" + + err = cfg.CheckExchangeConfigValues() + if err != nil { + t.Error(err) + } + + if !cfg.Exchanges[0].Features.Enabled.AutoPairUpdates || + !cfg.Exchanges[0].Features.Enabled.Websocket || + !cfg.Exchanges[0].Features.Supports.RESTCapabilities.AutoPairUpdates { + t.Error("unexpected values") + } + + if cfg.Exchanges[0].API.Endpoints.URL != APIURLNonDefaultMessage || + cfg.Exchanges[0].API.Endpoints.URLSecondary != APIURLNonDefaultMessage || + cfg.Exchanges[0].API.Endpoints.WebsocketURL != WebsocketURLNonDefaultMessage { + t.Error("unexpected values") + } + + // Test currency pair migration + setupPairs := func(emptyAssets bool) { + cfg.Exchanges[0].CurrencyPairs = nil + p := currency.Pairs{ + currency.NewPairDelimiter(testPair, "-"), + } + cfg.Exchanges[0].PairsLastUpdated = int64ptr(1234567) + + if !emptyAssets { + cfg.Exchanges[0].AssetTypes = sptr("spot") + } + + cfg.Exchanges[0].AvailablePairs = &p + cfg.Exchanges[0].EnabledPairs = &p + cfg.Exchanges[0].ConfigCurrencyPairFormat = ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "-", + } + cfg.Exchanges[0].RequestCurrencyPairFormat = ¤cy.PairFormat{ + Uppercase: false, + Delimiter: "~", + } + } + + setupPairs(false) + err = cfg.CheckExchangeConfigValues() + if err != nil { + t.Error(err) + } + + setupPairs(true) + err = cfg.CheckExchangeConfigValues() + if err != nil { + t.Error(err) + } + + if cfg.Exchanges[0].CurrencyPairs.LastUpdated != 1234567 { + t.Error("last updated has wrong value") + } + + pFmt := cfg.Exchanges[0].CurrencyPairs.ConfigFormat + if pFmt.Delimiter != "-" || + !pFmt.Uppercase { + t.Error("unexpected config format values") + } + + pFmt = cfg.Exchanges[0].CurrencyPairs.RequestFormat + if pFmt.Delimiter != "~" || + pFmt.Uppercase { + t.Error("unexpected request format values") + } + + if cfg.Exchanges[0].CurrencyPairs.AssetTypes.JoinToString(",") != "spot" || + !cfg.Exchanges[0].CurrencyPairs.UseGlobalFormat { + t.Error("unexpected results") + } + + pairs := cfg.Exchanges[0].CurrencyPairs.GetPairs(asset.Spot, true) + if len(pairs) == 0 || pairs.Join() != testPair { + t.Error("pairs not set properly") + } + + pairs = cfg.Exchanges[0].CurrencyPairs.GetPairs(asset.Spot, false) + if len(pairs) == 0 || pairs.Join() != testPair { + t.Error("pairs not set properly") + } + + // Ensure that all old settings are flushed + if cfg.Exchanges[0].PairsLastUpdated != nil || + cfg.Exchanges[0].ConfigCurrencyPairFormat != nil || + cfg.Exchanges[0].RequestCurrencyPairFormat != nil || + cfg.Exchanges[0].AssetTypes != nil || + cfg.Exchanges[0].AvailablePairs != nil || + cfg.Exchanges[0].EnabledPairs != nil { + t.Error("unexpected results") + } + + // Test AutoPairUpdates + cfg.Exchanges[0].Features.Supports.RESTCapabilities.AutoPairUpdates = false + cfg.Exchanges[0].Features.Supports.WebsocketCapabilities.AutoPairUpdates = false + cfg.Exchanges[0].CurrencyPairs.LastUpdated = 0 + cfg.CheckExchangeConfigValues() + + // Test HTTP rate limiter negative values + cfg.Exchanges[0].HTTPRateLimiter = &HTTPRateLimitConfig{ + Unauthenticated: HTTPRateConfig{ + Duration: -1, + Rate: -1, + }, + Authenticated: HTTPRateConfig{ + Duration: -1, + Rate: -1, + }, + } + err = cfg.CheckExchangeConfigValues() + if err != nil { + t.Error(err) + } + + if cfg.Exchanges[0].HTTPRateLimiter.Authenticated.Duration != 0 || + cfg.Exchanges[0].HTTPRateLimiter.Authenticated.Rate != 0 || + cfg.Exchanges[0].HTTPRateLimiter.Unauthenticated.Duration != 0 || + cfg.Exchanges[0].HTTPRateLimiter.Unauthenticated.Rate != 0 { + t.Error("unexpected results") + } + + // Test exchange pair consistency error + cfg.Exchanges[0].CurrencyPairs.UseGlobalFormat = false + backup := cfg.Exchanges[0].CurrencyPairs.Pairs[asset.Spot] + cfg.Exchanges[0].CurrencyPairs.Pairs[asset.Spot] = nil + err = cfg.CheckExchangeConfigValues() + if err != nil { + t.Error(err) + } + if cfg.Exchanges[0].Enabled { + t.Error("exchange should have been disabled") + } + + // Restore to previous state + cfg.Exchanges[0].Enabled = true + cfg.Exchanges[0].CurrencyPairs.UseGlobalFormat = true + cfg.Exchanges[0].CurrencyPairs.Pairs[asset.Spot] = backup + + // Test websocket and HTTP timeout values + cfg.Exchanges[0].WebsocketResponseMaxLimit = 0 + cfg.Exchanges[0].WebsocketResponseCheckTimeout = 0 + cfg.Exchanges[0].WebsocketOrderbookBufferLimit = 0 + cfg.Exchanges[0].WebsocketTrafficTimeout = 0 + cfg.Exchanges[0].HTTPTimeout = 0 + err = cfg.CheckExchangeConfigValues() + if err != nil { + t.Error(err) + } + + if cfg.Exchanges[0].WebsocketResponseMaxLimit == 0 { + t.Errorf("expected exchange %s to have updated WebsocketResponseMaxLimit value", + cfg.Exchanges[0].Name) + } + if cfg.Exchanges[0].WebsocketOrderbookBufferLimit == 0 { + t.Errorf("expected exchange %s to have updated WebsocketOrderbookBufferLimit value", + cfg.Exchanges[0].Name) + } + if cfg.Exchanges[0].WebsocketTrafficTimeout == 0 { + t.Errorf("expected exchange %s to have updated WebsocketTrafficTimeout value", + cfg.Exchanges[0].Name) + } + if cfg.Exchanges[0].HTTPTimeout == 0 { + t.Errorf("expected exchange %s to have updated HTTPTimeout value", + cfg.Exchanges[0].Name) + } + + v := &APICredentialsValidatorConfig{ + RequiresKey: true, + RequiresSecret: true, + } + cfg.Exchanges[0].API.CredentialsValidator = v + cfg.Exchanges[0].API.Credentials.Key = "Key" + cfg.Exchanges[0].API.Credentials.Secret = "Secret" + cfg.Exchanges[0].API.AuthenticatedSupport = true + cfg.Exchanges[0].API.AuthenticatedWebsocketSupport = true + cfg.CheckExchangeConfigValues() + if cfg.Exchanges[0].API.AuthenticatedSupport || + cfg.Exchanges[0].API.AuthenticatedWebsocketSupport { + t.Error("Expected authenticated endpoints to be false from invalid API keys") + } + + v.RequiresKey = false + v.RequiresClientID = true + cfg.Exchanges[0].API.AuthenticatedSupport = true + cfg.Exchanges[0].API.AuthenticatedWebsocketSupport = true + cfg.Exchanges[0].API.Credentials.ClientID = DefaultAPIClientID + cfg.Exchanges[0].API.Credentials.Secret = "TESTYTEST" + cfg.CheckExchangeConfigValues() + if cfg.Exchanges[0].API.AuthenticatedSupport || + cfg.Exchanges[0].API.AuthenticatedWebsocketSupport { + t.Error("Expected AuthenticatedAPISupport to be false from invalid API keys") + } + + v.RequiresKey = true + cfg.Exchanges[0].API.AuthenticatedSupport = true + cfg.Exchanges[0].API.AuthenticatedWebsocketSupport = true + cfg.Exchanges[0].API.Credentials.Key = "meow" + cfg.Exchanges[0].API.Credentials.Secret = "test123" + cfg.Exchanges[0].API.Credentials.ClientID = "clientIDerino" + cfg.CheckExchangeConfigValues() + if !cfg.Exchanges[0].API.AuthenticatedSupport || + !cfg.Exchanges[0].API.AuthenticatedWebsocketSupport { t.Error("Expected AuthenticatedAPISupport and AuthenticatedWebsocketAPISupport to be false from invalid API keys") } - checkExchangeConfigValues.Exchanges[0].BaseCurrencies = currency.NewCurrenciesFromStringArray([]string{""}) - err = checkExchangeConfigValues.CheckExchangeConfigValues() - if err == nil { + // Test exchage bank accounts + b := BankAccount{ + Enabled: true, + BankName: "Commonwealth Bank of Awesome", + BankAddress: "123 Fake Street", + BankPostalCode: "1337", + BankPostalCity: "Satoshiville", + BankCountry: "Genesis", + AccountName: "Satoshi Nakamoto", + AccountNumber: "1231006505", + SupportedCurrencies: "USD", + } + cfg.Exchanges[0].BankAccounts = []BankAccount{b} + cfg.CheckExchangeConfigValues() + if cfg.Exchanges[0].BankAccounts[0].Enabled { + t.Error("unexpected result") + } + + // Test empty exchange name for an enabled exchange + cfg.Exchanges[0].Enabled = true + cfg.Exchanges[0].Name = "" + cfg.CheckExchangeConfigValues() + if cfg.Exchanges[0].Enabled { t.Errorf( - "Test failed. checkExchangeConfigValues.CheckExchangeConfigValues Error", + "Exchange with no name should be empty", ) } - checkExchangeConfigValues.Exchanges[0].EnabledPairs = currency.NewPairsFromStrings([]string{""}) - err = checkExchangeConfigValues.CheckExchangeConfigValues() + // Test no enabled exchanges + cfg.Exchanges = cfg.Exchanges[:1] + cfg.Exchanges[0].Enabled = false + err = cfg.CheckExchangeConfigValues() if err == nil { - t.Errorf( - "Test failed. checkExchangeConfigValues.CheckExchangeConfigValues Error", - ) - } - - checkExchangeConfigValues.Exchanges[0].AvailablePairs = currency.NewPairsFromStrings([]string{""}) - err = checkExchangeConfigValues.CheckExchangeConfigValues() - if err == nil { - t.Errorf( - "Test failed. checkExchangeConfigValues.CheckExchangeConfigValues Error", - ) - } - - checkExchangeConfigValues.Exchanges[0].Name = "" - err = checkExchangeConfigValues.CheckExchangeConfigValues() - if err == nil { - t.Errorf( - "Test failed. checkExchangeConfigValues.CheckExchangeConfigValues Error", - ) - } - - checkExchangeConfigValues.Cryptocurrencies = currency.NewCurrenciesFromStringArray([]string{""}) - err = checkExchangeConfigValues.CheckExchangeConfigValues() - if err == nil { - t.Errorf( - "Test failed. checkExchangeConfigValues.CheckExchangeConfigValues Error", - ) - } - - checkExchangeConfigValues.Exchanges = checkExchangeConfigValues.Exchanges[:0] - checkExchangeConfigValues.Cryptocurrencies = currency.NewCurrenciesFromStringArray([]string{"TESTYTEST"}) - err = checkExchangeConfigValues.CheckExchangeConfigValues() - if err == nil { - t.Errorf( - "Test failed. checkExchangeConfigValues.CheckExchangeConfigValues Error", - ) - } -} - -func TestCheckWebserverConfigValues(t *testing.T) { - checkWebserverConfigValues := GetConfig() - err := checkWebserverConfigValues.LoadConfig(ConfigTestFile) - if err != nil { - t.Errorf( - "Test failed. checkWebserverConfigValues.LoadConfig: %s", err.Error(), - ) - } - - err = checkWebserverConfigValues.CheckWebserverConfigValues() - if err != nil { - t.Errorf( - "Test failed. checkWebserverConfigValues.CheckWebserverConfigValues: %s", - err.Error(), - ) - } - - checkWebserverConfigValues.Webserver.WebsocketConnectionLimit = -1 - err = checkWebserverConfigValues.CheckWebserverConfigValues() - if err != nil { - t.Errorf( - "Test failed. checkWebserverConfigValues.CheckWebserverConfigValues: %s", - err.Error(), - ) - } - - if checkWebserverConfigValues.Webserver.WebsocketConnectionLimit != 1 { - t.Error( - "Test failed. checkWebserverConfigValues.CheckWebserverConfigValues error", - ) - } - - checkWebserverConfigValues.Webserver.WebsocketMaxAuthFailures = -1 - checkWebserverConfigValues.CheckWebserverConfigValues() - if checkWebserverConfigValues.Webserver.WebsocketMaxAuthFailures != 3 { - t.Error( - "Test failed. checkWebserverConfigValues.CheckWebserverConfigValues error", - ) - } - - checkWebserverConfigValues.Webserver.ListenAddress = ":0" - err = checkWebserverConfigValues.CheckWebserverConfigValues() - if err == nil { - t.Error( - "Test failed. checkWebserverConfigValues.CheckWebserverConfigValues error", - ) - } - - checkWebserverConfigValues.Webserver.ListenAddress = ":LOLOLOL" - err = checkWebserverConfigValues.CheckWebserverConfigValues() - if err == nil { - t.Error( - "Test failed. checkWebserverConfigValues.CheckWebserverConfigValues error", - ) - } - - checkWebserverConfigValues.Webserver.ListenAddress = "LOLOLOL" - err = checkWebserverConfigValues.CheckWebserverConfigValues() - if err == nil { - t.Error( - "Test failed. checkWebserverConfigValues.CheckWebserverConfigValues error", - ) - } - - checkWebserverConfigValues.Webserver.AdminUsername = "" - err = checkWebserverConfigValues.CheckWebserverConfigValues() - if err == nil { - t.Error( - "Test failed. checkWebserverConfigValues.CheckWebserverConfigValues error", - ) + t.Error("Expected error from no enabled exchanges") } } func TestRetrieveConfigCurrencyPairs(t *testing.T) { cfg := GetConfig() - err := cfg.LoadConfig(ConfigTestFile) + err := cfg.LoadConfig(TestFile, true) if err != nil { t.Errorf( - "Test failed. TestRetrieveConfigCurrencyPairs.LoadConfig: %s", err.Error(), + "TestRetrieveConfigCurrencyPairs.LoadConfig: %s", err.Error(), ) } - err = cfg.RetrieveConfigCurrencyPairs(true) + err = cfg.RetrieveConfigCurrencyPairs(true, asset.Spot) if err != nil { t.Errorf( - "Test failed. TestRetrieveConfigCurrencyPairs.RetrieveConfigCurrencyPairs: %s", + "TestRetrieveConfigCurrencyPairs.RetrieveConfigCurrencyPairs: %s", err.Error(), ) } - err = cfg.RetrieveConfigCurrencyPairs(false) + err = cfg.RetrieveConfigCurrencyPairs(false, asset.Spot) if err != nil { t.Errorf( - "Test failed. TestRetrieveConfigCurrencyPairs.RetrieveConfigCurrencyPairs: %s", + "TestRetrieveConfigCurrencyPairs.RetrieveConfigCurrencyPairs: %s", err.Error(), ) } @@ -880,44 +1525,62 @@ func TestRetrieveConfigCurrencyPairs(t *testing.T) { func TestReadConfig(t *testing.T) { readConfig := GetConfig() - err := readConfig.ReadConfig(ConfigTestFile) + err := readConfig.ReadConfig(TestFile, true) if err != nil { - t.Errorf("Test failed. TestReadConfig %s", err.Error()) + t.Errorf("TestReadConfig %s", err.Error()) } - err = readConfig.ReadConfig("bla") + err = readConfig.ReadConfig("bla", true) if err == nil { - t.Error("Test failed. TestReadConfig error cannot be nil") + t.Error("TestReadConfig error cannot be nil") } - err = readConfig.ReadConfig("") + err = readConfig.ReadConfig("", true) if err != nil { - t.Error("Test failed. TestReadConfig error") + t.Error("TestReadConfig error") } } func TestLoadConfig(t *testing.T) { loadConfig := GetConfig() - err := loadConfig.LoadConfig(ConfigTestFile) + err := loadConfig.LoadConfig(TestFile, true) if err != nil { - t.Error("Test failed. TestLoadConfig " + err.Error()) + t.Error("TestLoadConfig " + err.Error()) } - err = loadConfig.LoadConfig("testy") + err = loadConfig.LoadConfig("testy", true) if err == nil { - t.Error("Test failed. TestLoadConfig ") + t.Error("TestLoadConfig Expected error") } } func TestSaveConfig(t *testing.T) { saveConfig := GetConfig() - err := saveConfig.LoadConfig(ConfigTestFile) + err := saveConfig.LoadConfig(TestFile, true) if err != nil { - t.Errorf("Test failed. TestSaveConfig.LoadConfig: %s", err.Error()) + t.Errorf("TestSaveConfig.LoadConfig: %s", err.Error()) } - err2 := saveConfig.SaveConfig(ConfigTestFile) + err2 := saveConfig.SaveConfig(TestFile, true) if err2 != nil { - t.Errorf("Test failed. TestSaveConfig.SaveConfig, %s", err2.Error()) + t.Errorf("TestSaveConfig.SaveConfig, %s", err2.Error()) + } +} + +func TestCheckConnectionMonitorConfig(t *testing.T) { + t.Parallel() + + var c Config + c.ConnectionMonitor.CheckInterval = 0 + c.ConnectionMonitor.DNSList = nil + c.ConnectionMonitor.PublicDomainList = nil + c.CheckConnectionMonitorConfig() + + if c.ConnectionMonitor.CheckInterval != connchecker.DefaultCheckInterval || + len(common.StringSliceDifference( + c.ConnectionMonitor.DNSList, connchecker.DefaultDNSList)) != 0 || + len(common.StringSliceDifference( + c.ConnectionMonitor.PublicDomainList, connchecker.DefaultDomainList)) != 0 { + t.Error("unexpected values") } } @@ -925,22 +1588,60 @@ func TestGetFilePath(t *testing.T) { expected := "blah.json" result, _ := GetFilePath("blah.json") if result != "blah.json" { - t.Errorf("Test failed. TestGetFilePath: expected %s got %s", expected, result) + t.Errorf("TestGetFilePath: expected %s got %s", expected, result) } - expected = ConfigTestFile + expected = TestFile result, _ = GetFilePath("") if result != expected { - t.Errorf("Test failed. TestGetFilePath: expected %s got %s", expected, result) + t.Errorf("TestGetFilePath: expected %s got %s", expected, result) } testBypass = true } +func TestCheckRemoteControlConfig(t *testing.T) { + t.Parallel() + + var c Config + c.Webserver = &WebserverConfig{ + Enabled: true, + AdminUsername: "satoshi", + AdminPassword: "ultrasecurepassword", + ListenAddress: ":9050", + WebsocketConnectionLimit: 5, + WebsocketMaxAuthFailures: 10, + WebsocketAllowInsecureOrigin: true, + } + + c.CheckRemoteControlConfig() + + if c.RemoteControl.Username != "satoshi" || + c.RemoteControl.Password != "ultrasecurepassword" || + !c.RemoteControl.GRPC.Enabled || + c.RemoteControl.GRPC.ListenAddress != "localhost:9052" || + !c.RemoteControl.GRPC.GRPCProxyEnabled || + c.RemoteControl.GRPC.GRPCProxyListenAddress != "localhost:9053" || + !c.RemoteControl.DeprecatedRPC.Enabled || + c.RemoteControl.DeprecatedRPC.ListenAddress != "localhost:9050" || + !c.RemoteControl.WebsocketRPC.Enabled || + c.RemoteControl.WebsocketRPC.ListenAddress != "localhost:9051" || + !c.RemoteControl.WebsocketRPC.AllowInsecureOrigin || + c.RemoteControl.WebsocketRPC.ConnectionLimit != 5 || + c.RemoteControl.WebsocketRPC.MaxAuthFailures != 10 { + t.Error("unexpected results") + } + + // Now test to ensure the previous settings are flushed + if c.Webserver != nil { + t.Error("old webserver settings should be nil") + } +} + func TestCheckConfig(t *testing.T) { var c Config - err := c.LoadConfig(ConfigTestFile) + err := c.LoadConfig(TestFile, true) if err != nil { - t.Errorf("Test failed. %s", err) + t.Errorf("%s", err) } err = c.CheckConfig() @@ -951,58 +1652,75 @@ func TestCheckConfig(t *testing.T) { func TestUpdateConfig(t *testing.T) { var c Config - err := c.LoadConfig(ConfigTestFile) + err := c.LoadConfig(TestFile, true) if err != nil { - t.Errorf("Test failed. %s", err) + t.Errorf("%s", err) } newCfg := c - err = c.UpdateConfig(ConfigTestFile, &newCfg) + err = c.UpdateConfig(TestFile, &newCfg, true) if err != nil { - t.Fatalf("Test failed. %s", err) + t.Fatalf("%s", err) } - err = c.UpdateConfig("//non-existantpath\\", &newCfg) + err = c.UpdateConfig("//non-existantpath\\", &newCfg, true) if err == nil { - t.Fatalf("Test failed. Error should of been thrown for invalid path") + t.Fatalf("Error should have been thrown for invalid path") } newCfg.Currency.Cryptocurrencies = currency.NewCurrenciesFromStringArray([]string{""}) - err = c.UpdateConfig(ConfigTestFile, &newCfg) + err = c.UpdateConfig(TestFile, &newCfg, true) if err != nil { - t.Errorf("Test failed. %s", err) + t.Errorf("%s", err) } if c.Currency.Cryptocurrencies.Join() == "" { - t.Fatalf("Test failed. Cryptocurrencies should have been repopulated") + t.Fatalf("Cryptocurrencies should have been repopulated") } } func BenchmarkUpdateConfig(b *testing.B) { var c Config - - err := c.LoadConfig(ConfigTestFile) + err := c.LoadConfig(TestFile, true) if err != nil { b.Errorf("Unable to benchmark UpdateConfig(): %s", err) } newCfg := c for i := 0; i < b.N; i++ { - _ = c.UpdateConfig(ConfigTestFile, &newCfg) + _ = c.UpdateConfig(TestFile, &newCfg, true) } } func TestCheckLoggerConfig(t *testing.T) { - c := GetConfig() - err := c.LoadConfig(ConfigTestFile) + t.Parallel() + + var c Config + c.Logging = log.Config{} + err := c.CheckLoggerConfig() if err != nil { - t.Fatal(err) + t.Errorf("Failed to create default logger. Error: %s", err) } - c.Logging = log.Logging{} + + if !*c.Logging.Enabled { + t.Error("unexpected result") + } + + c.Logging.LoggerFileConfig.FileName = "" + c.Logging.LoggerFileConfig.Rotate = nil + c.Logging.LoggerFileConfig.MaxSize = -1 + err = c.CheckLoggerConfig() if err != nil { - t.Errorf("Failed to create default logger reason: %v", err) + t.Error(err) } - c.LoadConfig(ConfigTestFile) + + if c.Logging.LoggerFileConfig.FileName != "log.txt" || + c.Logging.LoggerFileConfig.Rotate == nil || + c.Logging.LoggerFileConfig.MaxSize != 100 { + t.Error("unexpected result") + } + + c.LoadConfig(TestFile, true) err = c.CheckLoggerConfig() if err != nil { t.Errorf("Failed to create logger with user settings: reason: %v", err) @@ -1010,15 +1728,17 @@ func TestCheckLoggerConfig(t *testing.T) { } func TestDisableNTPCheck(t *testing.T) { + t.Parallel() + c := GetConfig() - err := c.LoadConfig(ConfigTestFile) + err := c.LoadConfig(TestFile, true) if err != nil { t.Fatal(err) } warn, err := c.DisableNTPCheck(strings.NewReader("w\n")) if err != nil { - t.Fatalf("test failed to create ntpclient failed reason: %v", err) + t.Fatalf("to create ntpclient failed reason: %v", err) } if warn != "Time sync has been set to warn only" { @@ -1040,6 +1760,33 @@ func TestDisableNTPCheck(t *testing.T) { } } +func TestCheckDatabaseConfig(t *testing.T) { + t.Parallel() + + var c Config + if err := c.checkDatabaseConfig(); err != nil { + t.Error(err) + } + + if c.Database.Driver != database.DBSQLite3 || + c.Database.Database != database.DefaultSQLiteDatabase || + c.Database.Enabled { + t.Error("unexpected results") + } + + c.Database.Enabled = true + c.Database.Driver = "mssqlisthebest" + if err := c.checkDatabaseConfig(); err == nil { + t.Error("unexpected result") + } + + c.Database.Driver = database.DBSQLite3 + c.Database.Enabled = true + if err := c.checkDatabaseConfig(); err != nil { + t.Error(err) + } +} + func TestCheckNTPConfig(t *testing.T) { c := GetConfig() @@ -1051,11 +1798,11 @@ func TestCheckNTPConfig(t *testing.T) { c.CheckNTPConfig() _, err := ntpclient.NTPClient(c.NTPClient.Pool) if err != nil { - t.Fatalf("test failed to create ntpclient failed reason: %v", err) + t.Fatalf("to create ntpclient failed reason: %v", err) } if c.NTPClient.Pool[0] != "pool.ntp.org:123" { - t.Error("ntpclient with no valid pool should default to pool.ntp.org ") + t.Error("ntpclient with no valid pool should default to pool.ntp.org") } if c.NTPClient.AllowedDifference == nil { @@ -1067,71 +1814,72 @@ func TestCheckNTPConfig(t *testing.T) { } } -// TestAreAuthenticatedCredentialsValid logic test -func TestAreAuthenticatedCredentialsValid(t *testing.T) { - var c Config - resp := c.areAuthenticatedCredentialsValid(0) - if resp { - t.Error("Expecting false with no exchanges loaded") +func TestCheckCurrencyConfigValues(t *testing.T) { + c := GetConfig() + c.Currency.ForexProviders = nil + c.Currency.CryptocurrencyProvider = CryptocurrencyProvider{} + err := c.CheckCurrencyConfigValues() + if err != nil { + t.Error(err) } - resp = c.areAuthenticatedCredentialsValid(-1) - if resp { - t.Error("Expecting false with an invalid index") + if c.Currency.ForexProviders == nil { + t.Error("Failed to populate c.Currency.ForexProviders") + } + if c.Currency.CryptocurrencyProvider.APIkey != DefaultUnsetAPIKey { + t.Error("Failed to set the api key to the default key") + } + if c.Currency.CryptocurrencyProvider.Name != "CoinMarketCap" { + t.Error("Failed to set the c.Currency.CryptocurrencyProvider.Name") } - c.Exchanges = []ExchangeConfig{ - { - APIKey: "", - APISecret: "", - ClientID: "", - Name: "", - }, + c.Currency.ForexProviders[0].Enabled = true + c.Currency.ForexProviders[0].Name = "CurrencyConverter" + c.Currency.ForexProviders[0].PrimaryProvider = true + c.Currency.Cryptocurrencies = nil + c.Cryptocurrencies = nil + c.Currency.CurrencyPairFormat = nil + c.CurrencyPairFormat = &CurrencyPairFormatConfig{ + Uppercase: true, } - resp = c.areAuthenticatedCredentialsValid(0) - if resp { - t.Error("Expecting false with no credentials set") + c.Currency.FiatDisplayCurrency = currency.Code{} + c.FiatDisplayCurrency = ¤cy.BTC + c.Currency.CryptocurrencyProvider.Enabled = true + err = c.CheckCurrencyConfigValues() + if err != nil { + t.Error(err) + } + if c.Currency.ForexProviders[0].Enabled { + t.Error("Failed to disable invalid forex provider") + } + if !c.Currency.CurrencyPairFormat.Uppercase { + t.Error("Failed to apply c.CurrencyPairFormat format to c.Currency.CurrencyPairFormat") } - c.Exchanges[0].APIKey = DefaultUnsetAPIKey - c.Exchanges[0].APISecret = DefaultUnsetAPISecret - resp = c.areAuthenticatedCredentialsValid(0) - if resp { - t.Error("Expecting false with default credentials set") + c.Currency.CryptocurrencyProvider.Enabled = false + c.Currency.CryptocurrencyProvider.APIkey = "" + c.Currency.CryptocurrencyProvider.AccountPlan = "" + c.FiatDisplayCurrency = ¤cy.BTC + c.Currency.ForexProviders[0].Enabled = true + c.Currency.ForexProviders[0].Name = "Name" + c.Currency.ForexProviders[0].PrimaryProvider = true + c.Currency.Cryptocurrencies = currency.Currencies{} + c.Cryptocurrencies = ¤cy.Currencies{} + err = c.CheckCurrencyConfigValues() + if err != nil { + t.Error(err) } - - c.Exchanges[0].Name = "COINUT" - resp = c.areAuthenticatedCredentialsValid(0) - if resp { - t.Error("Expecting false with COINUT and no APIKEY set") + if c.FiatDisplayCurrency != nil { + t.Error("Failed to clear c.FiatDisplayCurrency") } - c.Exchanges[0].APIKey = "Im a key!" - c.Exchanges[0].ClientID = "Im a Client!" - resp = c.areAuthenticatedCredentialsValid(0) - if !resp { - t.Error("Expecting true with COINUT api credentials set") - } - - c.Exchanges[0].Name = "Bitstamp" - resp = c.areAuthenticatedCredentialsValid(0) - if resp { - t.Error("Expecting false with Bitstamp and no APISecret set") - } - c.Exchanges[0].APISecret = "Im a Secret!" - resp = c.areAuthenticatedCredentialsValid(0) - if !resp { - t.Error("Expecting true with Bitstamp api credentials set") - } - - c.Exchanges[0].Name = "ANX" - c.Exchanges[0].APIKey = DefaultUnsetAPIKey - c.Exchanges[0].APISecret = "Im a Secret!" - resp = c.areAuthenticatedCredentialsValid(0) - if resp { - t.Error("Expecting false with ANX and no APIKey set") - } - - resp = c.areAuthenticatedCredentialsValid(1337) - if resp { - t.Error("Expecting false with an invalid index") + if c.Currency.CryptocurrencyProvider.APIkey != DefaultUnsetAPIKey || + c.Currency.CryptocurrencyProvider.AccountPlan != DefaultUnsetAccountPlan { + t.Error("Failed to set CryptocurrencyProvider.APIkey and AccountPlan") + } +} + +func TestPreengineConfigUpgrade(t *testing.T) { + var c Config + if err := c.LoadConfig("../testdata/preengine_config.json", false); err != nil { + t.Fatal(err) } } diff --git a/config/config_types.go b/config/config_types.go new file mode 100644 index 00000000..6e629610 --- /dev/null +++ b/config/config_types.go @@ -0,0 +1,446 @@ +package config + +import ( + "fmt" + "strings" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/database" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/gocryptotrader/portfolio" +) + +// Constants declared here are filename strings and test strings +const ( + FXProviderFixer = "fixer" + EncryptedFile = "config.dat" + File = "config.json" + TestFile = "../testdata/configtest.json" + fileEncryptionPrompt = 0 + fileEncryptionEnabled = 1 + fileEncryptionDisabled = -1 + pairsLastUpdatedWarningThreshold = 30 // 30 days + defaultHTTPTimeout = time.Second * 15 + defaultWebsocketResponseCheckTimeout = time.Millisecond * 30 + defaultWebsocketResponseMaxLimit = time.Second * 7 + defaultWebsocketOrderbookBufferLimit = 5 + defaultWebsocketTrafficTimeout = time.Second * 30 + maxAuthFailures = 3 + defaultNTPAllowedDifference = 50000000 + defaultNTPAllowedNegativeDifference = 50000000 + DefaultAPIKey = "Key" + DefaultAPISecret = "Secret" + DefaultAPIClientID = "ClientID" +) + +// Constants here hold some messages +const ( + ErrExchangeNameEmpty = "exchange #%d name is empty" + ErrExchangeAvailablePairsEmpty = "exchange %s available pairs is empty" + ErrExchangeEnabledPairsEmpty = "exchange %s enabled pairs is empty" + ErrExchangeBaseCurrenciesEmpty = "exchange %s base currencies is empty" + ErrExchangeNotFound = "exchange %s not found" + ErrNoEnabledExchanges = "no exchanges enabled" + ErrCryptocurrenciesEmpty = "cryptocurrencies variable is empty" + ErrFailureOpeningConfig = "fatal error opening %s file. Error: %s" + ErrCheckingConfigValues = "fatal error checking config values. Error: %s" + ErrSavingConfigBytesMismatch = "config file %q bytes comparison doesn't match, read %s expected %s" + WarningWebserverCredentialValuesEmpty = "webserver support disabled due to empty Username/Password values" + WarningWebserverListenAddressInvalid = "webserver support disabled due to invalid listen address" + WarningExchangeAuthAPIDefaultOrEmptyValues = "exchange %s authenticated API support disabled due to default/empty APIKey/Secret/ClientID values" + WarningPairsLastUpdatedThresholdExceeded = "exchange %s last manual update of available currency pairs has exceeded %d days. Manual update required!" +) + +// Constants here define unset default values displayed in the config.json +// file +const ( + APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API" + WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + DefaultUnsetAPIKey = "Key" + DefaultUnsetAPISecret = "Secret" + DefaultUnsetAccountPlan = "accountPlan" + DefaultForexProviderExchangeRatesAPI = "ExchangeRates" +) + +// Variables here are used for configuration +var ( + Cfg Config + IsInitialSetup bool + testBypass bool + m sync.Mutex +) + +// Config is the overarching object that holds all the information for +// prestart management of Portfolio, Communications, Webserver and Enabled +// Exchanges +type Config struct { + Name string `json:"name"` + EncryptConfig int `json:"encryptConfig"` + GlobalHTTPTimeout time.Duration `json:"globalHTTPTimeout"` + Database database.Config `json:"database"` + Logging log.Config `json:"logging"` + ConnectionMonitor ConnectionMonitorConfig `json:"connectionMonitor"` + Profiler ProfilerConfig `json:"profiler"` + NTPClient NTPClientConfig `json:"ntpclient"` + Currency CurrencyConfig `json:"currencyConfig"` + Communications CommunicationsConfig `json:"communications"` + RemoteControl RemoteControlConfig `json:"remoteControl"` + Portfolio portfolio.Base `json:"portfolioAddresses"` + Exchanges []ExchangeConfig `json:"exchanges"` + BankAccounts []BankAccount `json:"bankAccounts"` + + // Deprecated config settings, will be removed at a future date + Webserver *WebserverConfig `json:"webserver,omitempty"` + CurrencyPairFormat *CurrencyPairFormatConfig `json:"currencyPairFormat,omitempty"` + FiatDisplayCurrency *currency.Code `json:"fiatDispayCurrency,omitempty"` + Cryptocurrencies *currency.Currencies `json:"cryptocurrencies,omitempty"` + SMS *SMSGlobalConfig `json:"smsGlobal,omitempty"` +} + +// ConnectionMonitorConfig defines the connection monitor variables to ensure +// that there is internet connectivity +type ConnectionMonitorConfig struct { + DNSList []string `json:"preferredDNSList"` + PublicDomainList []string `json:"preferredDomainList"` + CheckInterval time.Duration `json:"checkInterval"` +} + +// ExchangeConfig holds all the information needed for each enabled Exchange. +type ExchangeConfig struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + UseSandbox bool `json:"useSandbox,omitempty"` + HTTPTimeout time.Duration `json:"httpTimeout"` + HTTPUserAgent string `json:"httpUserAgent,omitempty"` + HTTPDebugging bool `json:"httpDebugging,omitempty"` + HTTPRateLimiter *HTTPRateLimitConfig `json:"httpRateLimiter,omitempty"` + WebsocketResponseCheckTimeout time.Duration `json:"websocketResponseCheckTimeout"` + WebsocketResponseMaxLimit time.Duration `json:"websocketResponseMaxLimit"` + WebsocketTrafficTimeout time.Duration `json:"websocketTrafficTimeout"` + WebsocketOrderbookBufferLimit int `json:"websocketOrderbookBufferLimit"` + ProxyAddress string `json:"proxyAddress,omitempty"` + BaseCurrencies currency.Currencies `json:"baseCurrencies"` + CurrencyPairs *currency.PairsManager `json:"currencyPairs"` + API APIConfig `json:"api"` + Features *FeaturesConfig `json:"features"` + BankAccounts []BankAccount `json:"bankAccounts,omitempty"` + + // Deprecated settings which will be removed in a future update + AvailablePairs *currency.Pairs `json:"availablePairs,omitempty"` + EnabledPairs *currency.Pairs `json:"enabledPairs,omitempty"` + AssetTypes *string `json:"assetTypes,omitempty"` + PairsLastUpdated *int64 `json:"pairsLastUpdated,omitempty"` + ConfigCurrencyPairFormat *currency.PairFormat `json:"configCurrencyPairFormat,omitempty"` + RequestCurrencyPairFormat *currency.PairFormat `json:"requestCurrencyPairFormat,omitempty"` + AuthenticatedAPISupport *bool `json:"authenticatedApiSupport,omitempty"` + AuthenticatedWebsocketAPISupport *bool `json:"authenticatedWebsocketApiSupport,omitempty"` + APIKey *string `json:"apiKey,omitempty"` + APISecret *string `json:"apiSecret,omitempty"` + APIAuthPEMKeySupport *bool `json:"apiAuthPemKeySupport,omitempty"` + APIAuthPEMKey *string `json:"apiAuthPemKey,omitempty"` + APIURL *string `json:"apiUrl,omitempty"` + APIURLSecondary *string `json:"apiUrlSecondary,omitempty"` + ClientID *string `json:"clientId,omitempty"` + SupportsAutoPairUpdates *bool `json:"supportsAutoPairUpdates,omitempty"` + Websocket *bool `json:"websocket,omitempty"` + WebsocketURL *string `json:"websocketUrl,omitempty"` +} + +// ProfilerConfig defines the profiler configuration to enable pprof +type ProfilerConfig struct { + Enabled bool `json:"enabled"` +} + +// NTPClientConfig defines a network time protocol configuration to allow for +// positive and negative differences +type NTPClientConfig struct { + Level int `json:"enabled"` + Pool []string `json:"pool"` + AllowedDifference *time.Duration `json:"allowedDifference"` + AllowedNegativeDifference *time.Duration `json:"allowedNegativeDifference"` +} + +// GRPCConfig stores the gRPC settings +type GRPCConfig struct { + Enabled bool `json:"enabled"` + ListenAddress string `json:"listenAddress"` + GRPCProxyEnabled bool `json:"grpcProxyEnabled"` + GRPCProxyListenAddress string `json:"grpcProxyListenAddress"` +} + +// DepcrecatedRPCConfig stores the deprecatedRPCConfig settings +type DepcrecatedRPCConfig struct { + Enabled bool `json:"enabled"` + ListenAddress string `json:"listenAddress"` +} + +// WebsocketRPCConfig stores the websocket config info +type WebsocketRPCConfig struct { + Enabled bool `json:"enabled"` + ListenAddress string `json:"listenAddress"` + ConnectionLimit int `json:"connectionLimit"` + MaxAuthFailures int `json:"maxAuthFailures"` + AllowInsecureOrigin bool `json:"allowInsecureOrigin"` +} + +// RemoteControlConfig stores the RPC services config +type RemoteControlConfig struct { + Username string `json:"username"` + Password string `json:"password"` + + GRPC GRPCConfig `json:"gRPC"` + DeprecatedRPC DepcrecatedRPCConfig `json:"deprecatedRPC"` + WebsocketRPC WebsocketRPCConfig `json:"websocketRPC"` +} + +// WebserverConfig stores the old webserver config +type WebserverConfig struct { + Enabled bool `json:"enabled"` + AdminUsername string `json:"adminUsername"` + AdminPassword string `json:"adminPassword"` + ListenAddress string `json:"listenAddress"` + WebsocketConnectionLimit int `json:"websocketConnectionLimit"` + WebsocketMaxAuthFailures int `json:"websocketMaxAuthFailures"` + WebsocketAllowInsecureOrigin bool `json:"websocketAllowInsecureOrigin"` +} + +// Post holds the bot configuration data +type Post struct { + Data Config `json:"data"` +} + +// CurrencyPairFormatConfig stores the users preferred currency pair display +type CurrencyPairFormatConfig struct { + Uppercase bool `json:"uppercase"` + Delimiter string `json:"delimiter,omitempty"` + Separator string `json:"separator,omitempty"` + Index string `json:"index,omitempty"` +} + +// BankAccount holds differing bank account details by supported funding +// currency +type BankAccount struct { + Enabled bool `json:"enabled"` + BankName string `json:"bankName"` + BankAddress string `json:"bankAddress"` + BankPostalCode string `json:"bankPostalCode"` + BankPostalCity string `json:"bankPostalCity"` + BankCountry string `json:"bankCountry"` + AccountName string `json:"accountName"` + AccountNumber string `json:"accountNumber"` + SWIFTCode string `json:"swiftCode"` + IBAN string `json:"iban"` + BSBNumber string `json:"bsbNumber,omitempty"` + SupportedCurrencies string `json:"supportedCurrencies"` + SupportedExchanges string `json:"supportedExchanges,omitempty"` +} + +// Validate validates bank account settings +func (b *BankAccount) Validate() error { + if b.BankName == "" || + b.BankAddress == "" || + b.BankPostalCode == "" || + b.BankPostalCity == "" || + b.BankCountry == "" || + b.AccountName == "" || + b.SupportedCurrencies == "" { + return fmt.Errorf( + "banking details for %s is enabled but variables not set correctly", + b.BankName) + } + + if b.SupportedExchanges == "" { + b.SupportedExchanges = "ALL" + } + + if strings.Contains(strings.ToUpper( + b.SupportedCurrencies), + currency.AUD.String()) { + if b.BSBNumber == "" || + b.SWIFTCode == "" { + return fmt.Errorf( + "banking details for %s is enabled but BSB/SWIFT values not set", + b.BankName) + } + } else { + // Either IBAN or SWIFT code is OK + if b.IBAN == "" && b.SWIFTCode == "" { + return fmt.Errorf( + "banking details for %s is enabled but SWIFT/IBAN values not set", + b.BankName) + } + } + return nil +} + +// BankTransaction defines a related banking transaction +type BankTransaction struct { + ReferenceNumber string `json:"referenceNumber"` + TransactionNumber string `json:"transactionNumber"` + PaymentInstructions string `json:"paymentInstructions"` +} + +// CurrencyConfig holds all the information needed for currency related manipulation +type CurrencyConfig struct { + ForexProviders []base.Settings `json:"forexProviders"` + CryptocurrencyProvider CryptocurrencyProvider `json:"cryptocurrencyProvider"` + Cryptocurrencies currency.Currencies `json:"cryptocurrencies"` + CurrencyPairFormat *CurrencyPairFormatConfig `json:"currencyPairFormat"` + FiatDisplayCurrency currency.Code `json:"fiatDisplayCurrency"` + CurrencyFileUpdateDuration time.Duration `json:"currencyFileUpdateDuration"` + ForeignExchangeUpdateDuration time.Duration `json:"foreignExchangeUpdateDuration"` +} + +// CryptocurrencyProvider defines coinmarketcap tools +type CryptocurrencyProvider struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + APIkey string `json:"apiKey"` + AccountPlan string `json:"accountPlan"` +} + +// CommunicationsConfig holds all the information needed for each +// enabled communication package +type CommunicationsConfig struct { + SlackConfig SlackConfig `json:"slack"` + SMSGlobalConfig SMSGlobalConfig `json:"smsGlobal"` + SMTPConfig SMTPConfig `json:"smtp"` + TelegramConfig TelegramConfig `json:"telegram"` +} + +// IsAnyEnabled returns whether or any any comms relayers +// are enabled +func (c *CommunicationsConfig) IsAnyEnabled() bool { + if c.SMSGlobalConfig.Enabled || + c.SMTPConfig.Enabled || + c.SlackConfig.Enabled || + c.TelegramConfig.Enabled { + return true + } + return false +} + +// SlackConfig holds all variables to start and run the Slack package +type SlackConfig struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + TargetChannel string `json:"targetChannel"` + VerificationToken string `json:"verificationToken"` +} + +// SMSContact stores the SMS contact info +type SMSContact struct { + Name string `json:"name"` + Number string `json:"number"` + Enabled bool `json:"enabled"` +} + +// SMSGlobalConfig structure holds all the variables you need for instant +// messaging and broadcast used by SMSGlobal +type SMSGlobalConfig struct { + Name string `json:"name"` + From string `json:"from"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + Username string `json:"username"` + Password string `json:"password"` + Contacts []SMSContact `json:"contacts"` +} + +// SMTPConfig holds all variables to start and run the SMTP package +type SMTPConfig struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + Host string `json:"host"` + Port string `json:"port"` + AccountName string `json:"accountName"` + AccountPassword string `json:"accountPassword"` + From string `json:"from"` + RecipientList string `json:"recipientList"` +} + +// TelegramConfig holds all variables to start and run the Telegram package +type TelegramConfig struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + VerificationToken string `json:"verificationToken"` +} + +// FeaturesSupportedConfig stores the exchanges supported features +type FeaturesSupportedConfig struct { + REST bool `json:"restAPI"` + RESTCapabilities protocol.Features `json:"restCapabilities,omitempty"` + Websocket bool `json:"websocketAPI"` + WebsocketCapabilities protocol.Features `json:"websocketCapabilities,omitempty"` +} + +// FeaturesEnabledConfig stores the exchanges enabled features +type FeaturesEnabledConfig struct { + AutoPairUpdates bool `json:"autoPairUpdates"` + Websocket bool `json:"websocketAPI"` +} + +// FeaturesConfig stores the exchanges supported and enabled features +type FeaturesConfig struct { + Supports FeaturesSupportedConfig `json:"supports"` + Enabled FeaturesEnabledConfig `json:"enabled"` +} + +// APIEndpointsConfig stores the API endpoint addresses +type APIEndpointsConfig struct { + URL string `json:"url"` + URLSecondary string `json:"urlSecondary"` + WebsocketURL string `json:"websocketURL"` +} + +// APICredentialsConfig stores the API credentials +type APICredentialsConfig struct { + Key string `json:"key,omitempty"` + Secret string `json:"secret,omitempty"` + ClientID string `json:"clientID,omitempty"` + PEMKey string `json:"pemKey,omitempty"` + OTPSecret string `json:"otpSecret,omitempty"` +} + +// APICredentialsValidatorConfig stores the API credentials validator settings +type APICredentialsValidatorConfig struct { + // For Huobi (optional) + RequiresPEM bool `json:"requiresPEM,omitempty"` + + RequiresKey bool `json:"requiresKey,omitempty"` + RequiresSecret bool `json:"requiresSecret,omitempty"` + RequiresClientID bool `json:"requiresClientID,omitempty"` + RequiresBase64DecodeSecret bool `json:"requiresBase64DecodeSecret,omitempty"` +} + +// APIConfig stores the exchange API config +type APIConfig struct { + AuthenticatedSupport bool `json:"authenticatedSupport"` + AuthenticatedWebsocketSupport bool `json:"authenticatedWebsocketApiSupport"` + PEMKeySupport bool `json:"pemKeySupport,omitempty"` + + Endpoints APIEndpointsConfig `json:"endpoints"` + Credentials APICredentialsConfig `json:"credentials"` + CredentialsValidator *APICredentialsValidatorConfig `json:"credentialsValidator,omitempty"` +} + +// HTTPRateConfig stores the exchanges HTTP rate limiter config +type HTTPRateConfig struct { + Duration time.Duration `json:"duration"` + Rate int `json:"rate"` +} + +// HTTPRateLimitConfig stores the rate limit config +type HTTPRateLimitConfig struct { + Unauthenticated HTTPRateConfig `json:"unauthenticated"` + Authenticated HTTPRateConfig `json:"authenticated"` +} diff --git a/config_example.json b/config_example.json index 17d4a32c..ee3b31ca 100644 --- a/config_example.json +++ b/config_example.json @@ -2,12 +2,52 @@ "name": "Skynet", "encryptConfig": 0, "globalHTTPTimeout": 15000000000, + "database": { + "enabled": false, + "verbose": false, + "driver": "sqlite", + "connectionDetails": { + "host": "", + "port": 0, + "username": "", + "password": "", + "database": "", + "sslmode": "" + } + }, "logging": { "enabled": true, - "file": "debug.txt", - "colour": false, - "level": "DEBUG|WARN|INFO|ERROR|FATAL", - "rotate": false + "level": "INFO|WARN|DEBUG|ERROR", + "output": "console", + "fileSettings": { + "filename": "log.txt", + "rotate": true, + "maxsize": 250 + }, + "advancedSettings": { + "spacer": " | ", + "timeStampFormat": "02/01/2006 15:04:05", + "headers": { + "info": "[INFO] ", + "warn": "[WARN] ", + "debug": "[DEBUG]", + "error": "[ERROR]" + } + } + }, + "connectionMonitor": { + "preferredDNSList": [ + "8.8.8.8", + "8.8.4.4", + "1.1.1.1", + "1.0.0.1" + ], + "preferredDomainList": [ + "www.google.com", + "www.cloudflare.com", + "www.facebook.com" + ], + "checkInterval": 1000000000 }, "profiler": { "enabled": false @@ -94,6 +134,7 @@ }, "smsGlobal": { "name": "SMSGlobal", + "from": "Skynet", "enabled": false, "verbose": false, "username": "Username", @@ -114,6 +155,7 @@ "port": "537", "accountName": "some", "accountPassword": "password", + "from": "", "recipientList": "lol123@gmail.com" }, "telegram": { @@ -123,8 +165,29 @@ "verificationToken": "testest" } }, + "remoteControl": { + "username": "admin", + "password": "Password", + "gRPC": { + "enabled": true, + "listenAddress": "localhost:9052", + "grpcProxyEnabled": false, + "grpcProxyListenAddress": "localhost:9053" + }, + "deprecatedRPC": { + "enabled": true, + "listenAddress": "localhost:9050" + }, + "websocketRPC": { + "enabled": true, + "listenAddress": "localhost:9051", + "connectionLimit": 1, + "maxAuthFailures": 3, + "allowInsecureOrigin": true + } + }, "portfolioAddresses": { - "Addresses": [ + "addresses": [ { "Address": "1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy", "CoinType": "BTC", @@ -151,53 +214,76 @@ } ] }, - "webserver": { - "enabled": true, - "adminUsername": "admin", - "adminPassword": "Password", - "listenAddress": ":9050", - "websocketConnectionLimit": 1, - "websocketMaxAuthFailures": 3, - "websocketAllowInsecureOrigin": true - }, "exchanges": [ { "name": "ANX", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "ATENC_GBP,ATENC_NZD,BTC_AUD,BTC_SGD,LTC_BTC,START_GBP,STR_BTC,XRP_BTC,ATENC_SGD,BTC_GBP,DOGE_BTC,OAX_ETH,START_AUD,START_JPY,ATENC_USD,BTC_EUR,GNT_ETH,START_EUR,ATENC_EUR,BTC_CAD,START_BTC,START_CAD,ATENC_HKD,ATENC_JPY,ETH_BTC,ETH_HKD,START_HKD,START_USD,ATENC_AUD,ETH_USD,START_SGD,ATENC_CAD,BTC_HKD,BTC_JPY,BTC_NZD,BTC_USD,START_NZD", - "enabledPairs": "BTC_USD,BTC_HKD,BTC_EUR,BTC_CAD,BTC_AUD,BTC_SGD,BTC_JPY,BTC_GBP,BTC_NZD,LTC_BTC,STR_BTC,XRP_BTC", "baseCurrencies": "USD,HKD,EUR,CAD,AUD,SGD,JPY,GBP,NZD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_USD,BTC_HKD,BTC_EUR,BTC_CAD,BTC_AUD,BTC_SGD,BTC_JPY,BTC_GBP,BTC_NZD,LTC_BTC,STR_BTC,XRP_BTC", + "available": "ATENC_GBP,ATENC_NZD,BTC_AUD,BTC_SGD,LTC_BTC,START_GBP,STR_BTC,XRP_BTC,ATENC_SGD,BTC_GBP,DOGE_BTC,OAX_ETH,START_AUD,START_JPY,ATENC_USD,BTC_EUR,GNT_ETH,START_EUR,ATENC_EUR,BTC_CAD,START_BTC,START_CAD,ATENC_HKD,ATENC_JPY,ETH_BTC,ETH_HKD,START_HKD,START_USD,ATENC_AUD,ETH_USD,START_SGD,ATENC_CAD,BTC_HKD,BTC_JPY,BTC_NZD,BTC_USD,START_NZD" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresBase64DecodeSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -210,39 +296,71 @@ "name": "Binance", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "ETH-BTC,LTC-BTC,BNB-BTC,NEO-BTC,QTUM-ETH,EOS-ETH,SNT-ETH,BNT-ETH,GAS-BTC,BNB-ETH,BTC-USDT,ETH-USDT,OAX-ETH,DNT-ETH,MCO-ETH,MCO-BTC,WTC-BTC,WTC-ETH,LRC-BTC,LRC-ETH,QTUM-BTC,YOYO-BTC,OMG-BTC,OMG-ETH,ZRX-BTC,ZRX-ETH,STRAT-BTC,STRAT-ETH,SNGLS-BTC,SNGLS-ETH,BQX-BTC,BQX-ETH,KNC-BTC,KNC-ETH,FUN-BTC,FUN-ETH,SNM-BTC,SNM-ETH,NEO-ETH,IOTA-BTC,IOTA-ETH,LINK-BTC,LINK-ETH,XVG-BTC,XVG-ETH,MDA-BTC,MDA-ETH,MTL-BTC,MTL-ETH,EOS-BTC,SNT-BTC,ETC-ETH,ETC-BTC,MTH-BTC,MTH-ETH,ENG-BTC,ENG-ETH,DNT-BTC,ZEC-BTC,ZEC-ETH,BNT-BTC,AST-BTC,AST-ETH,DASH-BTC,DASH-ETH,OAX-BTC,BTG-BTC,BTG-ETH,EVX-BTC,EVX-ETH,REQ-BTC,REQ-ETH,VIB-BTC,VIB-ETH,TRX-BTC,TRX-ETH,POWR-BTC,POWR-ETH,ARK-BTC,ARK-ETH,YOYO-ETH,XRP-BTC,XRP-ETH,ENJ-BTC,ENJ-ETH,STORJ-BTC,STORJ-ETH,BNB-USDT,YOYO-BNB,POWR-BNB,KMD-BTC,KMD-ETH,NULS-BNB,RCN-BTC,RCN-ETH,RCN-BNB,NULS-BTC,NULS-ETH,RDN-BTC,RDN-ETH,RDN-BNB,XMR-BTC,XMR-ETH,DLT-BNB,WTC-BNB,DLT-BTC,DLT-ETH,AMB-BTC,AMB-ETH,AMB-BNB,BAT-BTC,BAT-ETH,BAT-BNB,BCPT-BTC,BCPT-ETH,BCPT-BNB,ARN-BTC,ARN-ETH,GVT-BTC,GVT-ETH,CDT-BTC,CDT-ETH,GXS-BTC,GXS-ETH,NEO-USDT,NEO-BNB,POE-BTC,POE-ETH,QSP-BTC,QSP-ETH,QSP-BNB,BTS-BTC,BTS-ETH,BTS-BNB,XZC-BTC,XZC-ETH,XZC-BNB,LSK-BTC,LSK-ETH,LSK-BNB,TNT-BTC,TNT-ETH,FUEL-BTC,FUEL-ETH,MANA-BTC,MANA-ETH,BCD-BTC,BCD-ETH,DGD-BTC,DGD-ETH,IOTA-BNB,ADX-BTC,ADX-ETH,ADX-BNB,ADA-BTC,ADA-ETH,PPT-BTC,PPT-ETH,CMT-BTC,CMT-ETH,CMT-BNB,XLM-BTC,XLM-ETH,XLM-BNB,CND-BTC,CND-ETH,CND-BNB,LEND-BTC,LEND-ETH,WABI-BTC,WABI-ETH,WABI-BNB,LTC-ETH,LTC-USDT,LTC-BNB,TNB-BTC,TNB-ETH,WAVES-BTC,WAVES-ETH,WAVES-BNB,GTO-BTC,GTO-ETH,GTO-BNB,ICX-BTC,ICX-ETH,ICX-BNB,OST-BTC,OST-ETH,OST-BNB,ELF-BTC,ELF-ETH,AION-BTC,AION-ETH,AION-BNB,NEBL-BTC,NEBL-ETH,NEBL-BNB,BRD-BTC,BRD-ETH,BRD-BNB,MCO-BNB,EDO-BTC,EDO-ETH,NAV-BTC,NAV-ETH,NAV-BNB,LUN-BTC,LUN-ETH,APPC-BTC,APPC-ETH,APPC-BNB,VIBE-BTC,VIBE-ETH,RLC-BTC,RLC-ETH,RLC-BNB,INS-BTC,INS-ETH,PIVX-BTC,PIVX-ETH,PIVX-BNB,IOST-BTC,IOST-ETH,STEEM-BTC,STEEM-ETH,STEEM-BNB,NANO-BTC,NANO-ETH,NANO-BNB,VIA-BTC,VIA-ETH,VIA-BNB,BLZ-BTC,BLZ-ETH,BLZ-BNB,AE-BTC,AE-ETH,AE-BNB,NCASH-BTC,NCASH-ETH,NCASH-BNB,POA-BTC,POA-ETH,POA-BNB,ZIL-BTC,ZIL-ETH,ZIL-BNB,ONT-BTC,ONT-ETH,ONT-BNB,STORM-BTC,STORM-ETH,STORM-BNB,QTUM-BNB,QTUM-USDT,XEM-BTC,XEM-ETH,XEM-BNB,WAN-BTC,WAN-ETH,WAN-BNB,WPR-BTC,WPR-ETH,QLC-BTC,QLC-ETH,SYS-BTC,SYS-ETH,SYS-BNB,QLC-BNB,GRS-BTC,GRS-ETH,ADA-USDT,ADA-BNB,GNT-BTC,GNT-ETH,GNT-BNB,LOOM-BTC,LOOM-ETH,LOOM-BNB,XRP-USDT,REP-BTC,REP-ETH,REP-BNB,BTC-TUSD,ETH-TUSD,ZEN-BTC,ZEN-ETH,ZEN-BNB,SKY-BTC,SKY-ETH,SKY-BNB,EOS-USDT,EOS-BNB,CVC-BTC,CVC-ETH,CVC-BNB,THETA-BTC,THETA-ETH,THETA-BNB,XRP-BNB,TUSD-USDT,IOTA-USDT,XLM-USDT,IOTX-BTC,IOTX-ETH,QKC-BTC,QKC-ETH,AGI-BTC,AGI-ETH,AGI-BNB,NXS-BTC,NXS-ETH,NXS-BNB,ENJ-BNB,DATA-BTC,DATA-ETH,ONT-USDT,TRX-BNB,TRX-USDT,ETC-USDT,ETC-BNB,ICX-USDT,SC-BTC,SC-ETH,SC-BNB,NPXS-BTC,NPXS-ETH,KEY-BTC,KEY-ETH,NAS-BTC,NAS-ETH,NAS-BNB,MFT-BTC,MFT-ETH,MFT-BNB,DENT-BTC,DENT-ETH,ARDR-BTC,ARDR-ETH,ARDR-BNB,NULS-USDT,HOT-BTC,HOT-ETH,VET-BTC,VET-ETH,VET-USDT,VET-BNB,DOCK-BTC,DOCK-ETH,POLY-BTC,POLY-BNB,HC-BTC,HC-ETH,GO-BTC,GO-BNB,PAX-USDT,RVN-BTC,RVN-BNB,DCR-BTC,DCR-BNB,MITH-BTC,MITH-BNB,BCHABC-BTC,BCHABC-USDT,BNB-PAX,BTC-PAX,ETH-PAX,XRP-PAX,EOS-PAX,XLM-PAX,REN-BTC,REN-BNB,BNB-TUSD,XRP-TUSD,EOS-TUSD,XLM-TUSD,BNB-USDC,BTC-USDC,ETH-USDC,XRP-USDC,EOS-USDC,XLM-USDC,USDC-USDT,ADA-TUSD,TRX-TUSD,NEO-TUSD,TRX-XRP,XZC-XRP,PAX-TUSD,USDC-TUSD,USDC-PAX,LINK-USDT,LINK-TUSD,LINK-PAX,LINK-USDC,WAVES-USDT,WAVES-TUSD,WAVES-PAX,WAVES-USDC,BCHABC-TUSD,BCHABC-PAX,BCHABC-USDC,LTC-TUSD,LTC-PAX,LTC-USDC,TRX-PAX,TRX-USDC,BTT-BTC,BTT-BNB,BTT-USDT,BNB-USDS,BTC-USDS,USDS-USDT,USDS-PAX,USDS-TUSD,USDS-USDC,BTT-PAX,BTT-TUSD,BTT-USDC,ONG-BNB,ONG-BTC,ONG-USDT,HOT-BNB,HOT-USDT,ZIL-USDT,ZRX-BNB,ZRX-USDT,FET-BNB,FET-BTC,FET-USDT,BAT-USDT,XMR-BNB,XMR-USDT,ZEC-BNB,ZEC-USDT,ZEC-PAX,ZEC-TUSD,ZEC-USDC,IOST-BNB,IOST-USDT,CELR-BNB,CELR-BTC,CELR-USDT,ADA-PAX,ADA-USDC,NEO-PAX,NEO-USDC,DASH-BNB,DASH-USDT,NANO-USDT,OMG-BNB,OMG-USDT,THETA-USDT,ENJ-USDT,MITH-USDT,MATIC-BNB,MATIC-BTC,MATIC-USDT,ATOM-BNB,ATOM-BTC,ATOM-USDT,ATOM-USDC,ATOM-PAX,ATOM-TUSD,ETC-USDC,ETC-PAX,ETC-TUSD,BAT-USDC,BAT-PAX,BAT-TUSD,PHB-BNB,PHB-BTC,PHB-USDC,PHB-TUSD,PHB-PAX,TFUEL-BNB,TFUEL-BTC,TFUEL-USDT,TFUEL-USDC,TFUEL-TUSD,TFUEL-PAX,ONE-BNB,ONE-BTC,ONE-USDT,ONE-TUSD,ONE-PAX,ONE-USDC,FTM-BNB,FTM-BTC,FTM-USDT,FTM-TUSD,FTM-PAX,FTM-USDC,BTCB-BTC,BCPT-TUSD,BCPT-PAX,BCPT-USDC,ALGO-BNB,ALGO-BTC,ALGO-USDT,ALGO-TUSD,ALGO-PAX,ALGO-USDC,USDSB-USDT,USDSB-USDS,GTO-USDT,GTO-PAX,GTO-TUSD,GTO-USDC,ERD-BNB,ERD-BTC,ERD-USDT,ERD-PAX,ERD-USDC,DOGE-BNB,DOGE-BTC,DOGE-USDT,DOGE-PAX,DOGE-USDC,DUSK-BNB,DUSK-BTC,DUSK-USDT,DUSK-USDC,DUSK-PAX,BGBP-USDC,ANKR-BNB,ANKR-BTC,ANKR-USDT,ANKR-TUSD,ANKR-PAX,ANKR-USDC,ONT-PAX,ONT-USDC,WIN-BNB,WIN-BTC,WIN-USDT,WIN-USDC,COS-BNB,COS-BTC,COS-USDT,TUSDB-TUSD,NPXS-USDT,NPXS-USDC,COCOS-BNB,COCOS-BTC,COCOS-USDT,MTL-USDT,TOMO-BNB,TOMO-BTC,TOMO-USDT,TOMO-USDC", - "enabledPairs": "BTC-USDT,ETH-USDT,LTC-USDT,ADA-USDT,XRP-USDT", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,ADA-USDT,XRP-USDT", + "available": "ETH-BTC,LTC-BTC,BNB-BTC,NEO-BTC,QTUM-ETH,EOS-ETH,SNT-ETH,BNT-ETH,GAS-BTC,BNB-ETH,BTC-USDT,ETH-USDT,OAX-ETH,DNT-ETH,MCO-ETH,MCO-BTC,WTC-BTC,WTC-ETH,LRC-BTC,LRC-ETH,QTUM-BTC,YOYO-BTC,OMG-BTC,OMG-ETH,ZRX-BTC,ZRX-ETH,STRAT-BTC,STRAT-ETH,SNGLS-BTC,BQX-BTC,BQX-ETH,KNC-BTC,KNC-ETH,FUN-BTC,FUN-ETH,SNM-BTC,SNM-ETH,NEO-ETH,IOTA-BTC,IOTA-ETH,LINK-BTC,LINK-ETH,XVG-BTC,XVG-ETH,MDA-BTC,MDA-ETH,MTL-BTC,MTL-ETH,EOS-BTC,SNT-BTC,ETC-ETH,ETC-BTC,MTH-BTC,MTH-ETH,ENG-BTC,ENG-ETH,DNT-BTC,ZEC-BTC,ZEC-ETH,BNT-BTC,AST-BTC,AST-ETH,DASH-BTC,DASH-ETH,OAX-BTC,BTG-BTC,BTG-ETH,EVX-BTC,EVX-ETH,REQ-BTC,REQ-ETH,VIB-BTC,VIB-ETH,TRX-BTC,TRX-ETH,POWR-BTC,POWR-ETH,ARK-BTC,ARK-ETH,YOYO-ETH,XRP-BTC,XRP-ETH,ENJ-BTC,ENJ-ETH,STORJ-BTC,STORJ-ETH,BNB-USDT,YOYO-BNB,POWR-BNB,KMD-BTC,KMD-ETH,NULS-BNB,RCN-BTC,RCN-ETH,RCN-BNB,NULS-BTC,NULS-ETH,RDN-BTC,RDN-ETH,RDN-BNB,XMR-BTC,XMR-ETH,DLT-BNB,WTC-BNB,DLT-BTC,DLT-ETH,AMB-BTC,AMB-ETH,AMB-BNB,BAT-BTC,BAT-ETH,BAT-BNB,BCPT-BTC,BCPT-ETH,BCPT-BNB,ARN-BTC,ARN-ETH,GVT-BTC,GVT-ETH,CDT-BTC,CDT-ETH,GXS-BTC,GXS-ETH,NEO-USDT,NEO-BNB,POE-BTC,POE-ETH,QSP-BTC,QSP-ETH,QSP-BNB,BTS-BTC,BTS-ETH,XZC-BTC,XZC-ETH,XZC-BNB,LSK-BTC,LSK-ETH,LSK-BNB,TNT-BTC,TNT-ETH,FUEL-BTC,MANA-BTC,MANA-ETH,BCD-BTC,BCD-ETH,DGD-BTC,DGD-ETH,IOTA-BNB,ADX-BTC,ADX-ETH,ADX-BNB,ADA-BTC,ADA-ETH,PPT-BTC,PPT-ETH,CMT-BTC,CMT-ETH,CMT-BNB,XLM-BTC,XLM-ETH,XLM-BNB,CND-BTC,CND-ETH,CND-BNB,LEND-BTC,LEND-ETH,WABI-BTC,WABI-ETH,WABI-BNB,LTC-ETH,LTC-USDT,LTC-BNB,TNB-BTC,TNB-ETH,WAVES-BTC,WAVES-ETH,WAVES-BNB,GTO-BTC,GTO-ETH,GTO-BNB,ICX-BTC,ICX-ETH,ICX-BNB,OST-BTC,OST-ETH,OST-BNB,ELF-BTC,ELF-ETH,AION-BTC,AION-ETH,AION-BNB,NEBL-BTC,NEBL-ETH,NEBL-BNB,BRD-BTC,BRD-ETH,BRD-BNB,MCO-BNB,EDO-BTC,EDO-ETH,NAV-BTC,LUN-BTC,APPC-BTC,APPC-ETH,APPC-BNB,VIBE-BTC,VIBE-ETH,RLC-BTC,RLC-ETH,RLC-BNB,INS-BTC,INS-ETH,PIVX-BTC,PIVX-ETH,PIVX-BNB,IOST-BTC,IOST-ETH,STEEM-BTC,STEEM-ETH,STEEM-BNB,NANO-BTC,NANO-ETH,NANO-BNB,VIA-BTC,VIA-ETH,VIA-BNB,BLZ-BTC,BLZ-ETH,BLZ-BNB,AE-BTC,AE-ETH,AE-BNB,NCASH-BTC,NCASH-ETH,POA-BTC,POA-ETH,POA-BNB,ZIL-BTC,ZIL-ETH,ZIL-BNB,ONT-BTC,ONT-ETH,ONT-BNB,STORM-BTC,STORM-ETH,STORM-BNB,QTUM-BNB,QTUM-USDT,XEM-BTC,XEM-ETH,XEM-BNB,WAN-BTC,WAN-ETH,WAN-BNB,WPR-BTC,WPR-ETH,QLC-BTC,QLC-ETH,SYS-BTC,SYS-ETH,SYS-BNB,QLC-BNB,GRS-BTC,GRS-ETH,ADA-USDT,ADA-BNB,GNT-BTC,GNT-ETH,GNT-BNB,LOOM-BTC,LOOM-ETH,LOOM-BNB,XRP-USDT,REP-BTC,REP-ETH,BTC-TUSD,ETH-TUSD,ZEN-BTC,ZEN-ETH,ZEN-BNB,SKY-BTC,SKY-ETH,SKY-BNB,EOS-USDT,EOS-BNB,CVC-BTC,CVC-ETH,THETA-BTC,THETA-ETH,THETA-BNB,XRP-BNB,TUSD-USDT,IOTA-USDT,XLM-USDT,IOTX-BTC,IOTX-ETH,QKC-BTC,QKC-ETH,AGI-BTC,AGI-ETH,AGI-BNB,NXS-BTC,NXS-ETH,NXS-BNB,ENJ-BNB,DATA-BTC,DATA-ETH,ONT-USDT,TRX-BNB,TRX-USDT,ETC-USDT,ETC-BNB,ICX-USDT,SC-BTC,SC-ETH,SC-BNB,NPXS-ETH,KEY-BTC,KEY-ETH,NAS-BTC,NAS-ETH,NAS-BNB,MFT-BTC,MFT-ETH,MFT-BNB,DENT-ETH,ARDR-BTC,ARDR-ETH,ARDR-BNB,NULS-USDT,HOT-BTC,HOT-ETH,VET-BTC,VET-ETH,VET-USDT,VET-BNB,DOCK-BTC,DOCK-ETH,POLY-BTC,POLY-BNB,HC-BTC,HC-ETH,GO-BTC,GO-BNB,PAX-USDT,RVN-BTC,RVN-BNB,DCR-BTC,DCR-BNB,MITH-BTC,MITH-BNB,BNB-PAX,BTC-PAX,ETH-PAX,XRP-PAX,EOS-PAX,XLM-PAX,REN-BTC,REN-BNB,BNB-TUSD,XRP-TUSD,EOS-TUSD,XLM-TUSD,BNB-USDC,BTC-USDC,ETH-USDC,XRP-USDC,EOS-USDC,XLM-USDC,USDC-USDT,ADA-TUSD,TRX-TUSD,NEO-TUSD,TRX-XRP,XZC-XRP,PAX-TUSD,USDC-TUSD,USDC-PAX,LINK-USDT,LINK-TUSD,LINK-PAX,LINK-USDC,WAVES-USDT,WAVES-TUSD,WAVES-USDC,LTC-TUSD,LTC-PAX,LTC-USDC,TRX-PAX,TRX-USDC,BTT-BNB,BTT-USDT,BNB-USDS,BTC-USDS,USDS-USDT,USDS-PAX,USDS-TUSD,USDS-USDC,BTT-PAX,BTT-TUSD,BTT-USDC,ONG-BNB,ONG-BTC,ONG-USDT,HOT-BNB,HOT-USDT,ZIL-USDT,ZRX-BNB,ZRX-USDT,FET-BNB,FET-BTC,FET-USDT,BAT-USDT,XMR-BNB,XMR-USDT,ZEC-BNB,ZEC-USDT,ZEC-PAX,ZEC-TUSD,ZEC-USDC,IOST-BNB,IOST-USDT,CELR-BNB,CELR-BTC,CELR-USDT,ADA-PAX,ADA-USDC,NEO-PAX,NEO-USDC,DASH-BNB,DASH-USDT,NANO-USDT,OMG-BNB,OMG-USDT,THETA-USDT,ENJ-USDT,MITH-USDT,MATIC-BNB,MATIC-BTC,MATIC-USDT,ATOM-BNB,ATOM-BTC,ATOM-USDT,ATOM-USDC,ATOM-TUSD,ETC-TUSD,BAT-USDC,BAT-PAX,BAT-TUSD,PHB-BNB,PHB-BTC,PHB-TUSD,TFUEL-BNB,TFUEL-BTC,TFUEL-USDT,ONE-BNB,ONE-BTC,ONE-USDT,ONE-USDC,FTM-BNB,FTM-BTC,FTM-USDT,FTM-USDC,ALGO-BNB,ALGO-BTC,ALGO-USDT,ALGO-TUSD,ALGO-PAX,ALGO-USDC,GTO-USDT,ERD-BNB,ERD-BTC,ERD-USDT,DOGE-BNB,DOGE-BTC,DOGE-USDT,DUSK-BNB,DUSK-BTC,DUSK-USDT,DUSK-USDC,DUSK-PAX,BGBP-USDC,ANKR-BNB,ANKR-BTC,ANKR-USDT,ONT-PAX,ONT-USDC,WIN-BNB,WIN-USDT,WIN-USDC,COS-BNB,COS-BTC,COS-USDT,NPXS-USDT,COCOS-BNB,COCOS-BTC,COCOS-USDT,MTL-USDT,TOMO-BNB,TOMO-BTC,TOMO-USDT,TOMO-USDC,PERL-BNB,PERL-BTC,PERL-USDC,PERL-USDT,DENT-USDT,MFT-USDT,KEY-USDT,STORM-USDT,DOCK-USDT,WAN-USDT,FUN-USDT,CVC-USDT,BTT-TRX,WIN-TRX,CHZ-BNB,CHZ-BTC,CHZ-USDT,BAND-BNB,BAND-BTC,BAND-USDT,BNB-BUSD,BTC-BUSD,BUSD-USDT,BEAM-BNB,BEAM-BTC,BEAM-USDT,XTZ-BNB,XTZ-BTC,XTZ-USDT,REN-USDT,RVN-USDT,HC-USDT,HBAR-BNB,HBAR-BTC,HBAR-USDT,NKN-BNB,NKN-BTC,NKN-USDT,XRP-BUSD,ETH-BUSD,LTC-BUSD,LINK-BUSD,ETC-BUSD,STX-BNB,STX-BTC,STX-USDT,KAVA-BNB,KAVA-BTC,KAVA-USDT,BUSD-NGN,BNB-NGN,BTC-NGN,ARPA-BNB,ARPA-BTC,ARPA-USDT,TRX-BUSD,EOS-BUSD,IOTX-USDT,RLC-USDT,MCO-USDT,XLM-BUSD,ADA-BUSD,CTXC-BNB,CTXC-BTC,CTXC-USDT,BCH-BNB,BCH-BTC,BCH-USDT,BCH-USDC,BCH-TUSD,BCH-PAX,BCH-BUSD,BTC-RUB,ETH-RUB,XRP-RUB,BNB-RUB" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -255,38 +373,70 @@ "name": "Bitfinex", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC,BTCEUR,BTCJPY,XRPUSD,XRPBTC,IOTUSD,IOTBTC,IOTETH,EOSUSD,EOSBTC,EOSETH,SANUSD,SANBTC,SANETH,OMGUSD,OMGBTC,OMGETH,NEOUSD,NEOBTC,NEOETH,ETPUSD,ETPBTC,ETPETH,QTMUSD,QTMBTC,QTMETH,AVTUSD,AVTBTC,AVTETH,EDOUSD,EDOBTC,EDOETH,BTGUSD,BTGBTC,DATUSD,DATBTC,DATETH,QSHUSD,QSHBTC,QSHETH,YYWUSD,YYWBTC,YYWETH,GNTUSD,GNTBTC,GNTETH,SNTUSD,SNTBTC,SNTETH,IOTEUR,BATUSD,BATBTC,BATETH,MNAUSD,MNABTC,MNAETH,FUNUSD,FUNBTC,FUNETH,ZRXUSD,ZRXBTC,ZRXETH,TNBUSD,TNBBTC,TNBETH,SPKUSD,SPKBTC,SPKETH,TRXUSD,TRXBTC,TRXETH,RCNUSD,RCNBTC,RCNETH,RLCUSD,RLCBTC,RLCETH,AIDUSD,AIDBTC,AIDETH,SNGUSD,SNGBTC,SNGETH,REPUSD,REPBTC,REPETH,ELFUSD,ELFBTC,ELFETH,NECUSD,NECBTC,NECETH,BTCGBP,ETHEUR,ETHJPY,ETHGBP,NEOEUR,NEOJPY,NEOGBP,EOSEUR,EOSJPY,EOSGBP,IOTJPY,IOTGBP,IOSUSD,IOSBTC,IOSETH,AIOUSD,AIOBTC,AIOETH,REQUSD,REQBTC,REQETH,RDNUSD,RDNBTC,RDNETH,LRCUSD,LRCBTC,LRCETH,WAXUSD,WAXBTC,WAXETH,DAIUSD,DAIBTC,DAIETH,AGIUSD,AGIBTC,AGIETH,BFTUSD,BFTBTC,BFTETH,MTNUSD,MTNBTC,MTNETH,ODEUSD,ODEBTC,ODEETH,ANTUSD,ANTBTC,ANTETH,DTHUSD,DTHBTC,DTHETH,MITUSD,MITBTC,MITETH,STJUSD,STJBTC,STJETH,XLMUSD,XLMEUR,XLMJPY,XLMGBP,XLMBTC,XLMETH,XVGUSD,XVGEUR,XVGJPY,XVGGBP,XVGBTC,XVGETH,BCIUSD,BCIBTC,MKRUSD,MKRBTC,MKRETH,KNCUSD,KNCBTC,KNCETH,POAUSD,POABTC,POAETH,EVTUSD,LYMUSD,LYMBTC,LYMETH,UTKUSD,UTKBTC,UTKETH,VEEUSD,VEEBTC,VEEETH,DADUSD,DADBTC,DADETH,ORSUSD,ORSBTC,ORSETH,AUCUSD,AUCBTC,AUCETH,POYUSD,POYBTC,POYETH,FSNUSD,FSNBTC,FSNETH,CBTUSD,CBTBTC,CBTETH,ZCNUSD,ZCNBTC,ZCNETH,SENUSD,SENBTC,SENETH,NCAUSD,NCABTC,NCAETH,CNDUSD,CNDBTC,CNDETH,CTXUSD,CTXBTC,CTXETH,PAIUSD,PAIBTC,SEEUSD,SEEBTC,SEEETH,ESSUSD,ESSBTC,ESSETH,ATMUSD,ATMBTC,ATMETH,HOTUSD,HOTBTC,HOTETH,DTAUSD,DTABTC,DTAETH,IQXUSD,IQXBTC,IQXEOS,WPRUSD,WPRBTC,WPRETH,ZILUSD,ZILBTC,ZILETH,BNTUSD,BNTBTC,BNTETH,ABSUSD,ABSETH,XRAUSD,XRAETH,MANUSD,MANETH,BBNUSD,BBNETH,NIOUSD,NIOETH,DGXUSD,DGXETH,VETUSD,VETBTC,VETETH,UTNUSD,UTNETH,TKNUSD,TKNETH,GOTUSD,GOTEUR,GOTETH,XTZUSD,XTZBTC,CNNUSD,CNNETH,BOXUSD,BOXETH,TRXEUR,TRXGBP,TRXJPY,MGOUSD,MGOETH,RTEUSD,RTEETH,YGGUSD,YGGETH,MLNUSD,MLNETH,WTCUSD,WTCETH,CSXUSD,CSXETH,OMNUSD,OMNBTC,INTUSD,INTETH,DRNUSD,DRNETH,PNKUSD,PNKETH,DGBUSD,DGBBTC,BSVUSD,BSVBTC,BABUSD,BABBTC,WLOUSD,WLOXLM,VLDUSD,VLDETH,ENJUSD,ENJETH,ONLUSD,ONLETH,RBTUSD,RBTBTC,USTUSD,EUTEUR,EUTUSD,GSDUSD,UDCUSD,TSDUSD,PAXUSD,RIFUSD,RIFBTC,PASUSD,PASETH,VSYUSD,VSYBTC,ZRXDAI,MKRDAI,OMGDAI,BTTUSD,BTTBTC,BTCUST,ETHUST,CLOUSD,CLOBTC,IMPUSD,IMPETH,LTCUST,EOSUST,BABUST,SCRUSD,SCRETH,GNOUSD,GNOETH,GENUSD,GENETH,ATOUSD,ATOBTC,ATOETH,WBTUSD,XCHUSD,EUSUSD,WBTETH,XCHETH,EUSETH,LEOUSD,LEOBTC,LEOUST,LEOEOS,LEOETH,ASTUSD,ASTETH,FOAUSD,FOAETH,UFRUSD,UFRETH,ZBTUSD,ZBTUST,OKBUSD,USKUSD,GTXUSD,KANUSD,OKBUST,OKBETH,OKBBTC,USKUST,USKETH,USKBTC,USKEOS,GTXUST,KANUST,AMPUSD,ALGUSD,ALGBTC,ALGUST,BTCXCH,SWMUSD,SWMETH,TRIUSD,TRIETH,LOOUSD,LOOETH,AMPUST,DUSK:USD,DUSK:BTC,UOSUSD,UOSBTC,RRBUSD,RRBUST,DTXUSD,DTXUST,AMPBTC,FTTUSD,FTTUST,BTCF0:USTF0,ETHF0:USTF0", - "enabledPairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC", + "available": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC,BTCEUR,BTCJPY,XRPUSD,XRPBTC,IOTUSD,IOTBTC,IOTETH,EOSUSD,EOSBTC,EOSETH,SANUSD,SANBTC,SANETH,OMGUSD,OMGBTC,OMGETH,NEOUSD,NEOBTC,NEOETH,ETPUSD,ETPBTC,ETPETH,QTMUSD,QTMBTC,QTMETH,AVTUSD,AVTBTC,AVTETH,EDOUSD,EDOBTC,EDOETH,BTGUSD,BTGBTC,DATUSD,DATBTC,DATETH,QSHUSD,QSHBTC,QSHETH,YYWUSD,YYWBTC,YYWETH,GNTUSD,GNTBTC,GNTETH,SNTUSD,SNTBTC,SNTETH,IOTEUR,BATUSD,BATBTC,BATETH,MNAUSD,MNABTC,MNAETH,FUNUSD,FUNBTC,FUNETH,ZRXUSD,ZRXBTC,ZRXETH,TNBUSD,TNBBTC,TNBETH,SPKUSD,SPKBTC,SPKETH,TRXUSD,TRXBTC,TRXETH,RCNUSD,RCNBTC,RCNETH,RLCUSD,RLCBTC,RLCETH,AIDUSD,AIDBTC,AIDETH,SNGUSD,SNGBTC,SNGETH,REPUSD,REPBTC,REPETH,ELFUSD,ELFBTC,ELFETH,NECUSD,NECBTC,NECETH,BTCGBP,ETHEUR,ETHJPY,ETHGBP,NEOEUR,NEOJPY,NEOGBP,EOSEUR,EOSJPY,EOSGBP,IOTJPY,IOTGBP,IOSUSD,IOSBTC,IOSETH,AIOUSD,AIOBTC,AIOETH,REQUSD,REQBTC,REQETH,RDNUSD,RDNBTC,RDNETH,LRCUSD,LRCBTC,LRCETH,WAXUSD,WAXBTC,WAXETH,DAIUSD,DAIBTC,DAIETH,AGIUSD,AGIBTC,AGIETH,BFTUSD,BFTBTC,BFTETH,MTNUSD,MTNBTC,MTNETH,ODEUSD,ODEBTC,ODEETH,ANTUSD,ANTBTC,ANTETH,DTHUSD,DTHBTC,DTHETH,MITUSD,MITBTC,MITETH,STJUSD,STJBTC,STJETH,XLMUSD,XLMEUR,XLMJPY,XLMGBP,XLMBTC,XLMETH,XVGUSD,XVGEUR,XVGJPY,XVGGBP,XVGBTC,XVGETH,BCIUSD,BCIBTC,MKRUSD,MKRBTC,MKRETH,KNCUSD,KNCBTC,KNCETH,POAUSD,POABTC,POAETH,EVTUSD,LYMUSD,LYMBTC,LYMETH,UTKUSD,UTKBTC,UTKETH,VEEUSD,VEEBTC,VEEETH,DADUSD,DADBTC,DADETH,ORSUSD,ORSBTC,ORSETH,AUCUSD,AUCBTC,AUCETH,POYUSD,POYBTC,POYETH,FSNUSD,FSNBTC,FSNETH,CBTUSD,CBTBTC,CBTETH,ZCNUSD,ZCNBTC,ZCNETH,SENUSD,SENBTC,SENETH,NCAUSD,NCABTC,NCAETH,CNDUSD,CNDBTC,CNDETH,CTXUSD,CTXBTC,CTXETH,PAIUSD,PAIBTC,SEEUSD,SEEBTC,SEEETH,ESSUSD,ESSBTC,ESSETH,ATMUSD,ATMBTC,ATMETH,HOTUSD,HOTBTC,HOTETH,DTAUSD,DTABTC,DTAETH,IQXUSD,IQXBTC,IQXEOS,WPRUSD,WPRBTC,WPRETH,ZILUSD,ZILBTC,ZILETH,BNTUSD,BNTBTC,BNTETH,ABSUSD,ABSETH,XRAUSD,XRAETH,MANUSD,MANETH,BBNUSD,BBNETH,NIOUSD,NIOETH,DGXUSD,DGXETH,VETUSD,VETBTC,VETETH,UTNUSD,UTNETH,TKNUSD,TKNETH,GOTUSD,GOTEUR,GOTETH,XTZUSD,XTZBTC,CNNUSD,CNNETH,BOXUSD,BOXETH,TRXEUR,TRXGBP,TRXJPY,MGOUSD,MGOETH,RTEUSD,RTEETH,YGGUSD,YGGETH,MLNUSD,MLNETH,WTCUSD,WTCETH,CSXUSD,CSXETH,OMNUSD,OMNBTC,INTUSD,INTETH,DRNUSD,DRNETH,PNKUSD,PNKETH,DGBUSD,DGBBTC,BSVUSD,BSVBTC,BABUSD,BABBTC,WLOUSD,WLOXLM,VLDUSD,VLDETH,ENJUSD,ENJETH,ONLUSD,ONLETH,RBTUSD,RBTBTC,USTUSD,EUTEUR,EUTUSD,GSDUSD,UDCUSD,TSDUSD,PAXUSD,RIFUSD,RIFBTC,PASUSD,PASETH,VSYUSD,VSYBTC,ZRXDAI,MKRDAI,OMGDAI,BTTUSD,BTTBTC,BTCUST,ETHUST,CLOUSD,CLOBTC,IMPUSD,IMPETH,LTCUST,EOSUST,BABUST,SCRUSD,SCRETH,GNOUSD,GNOETH,GENUSD,GENETH,ATOUSD,ATOBTC,ATOETH,WBTUSD,XCHUSD,EUSUSD,WBTETH,XCHETH,EUSETH,LEOUSD,LEOBTC,LEOUST,LEOEOS,LEOETH,ASTUSD,ASTETH,FOAUSD,FOAETH,UFRUSD,UFRETH,ZBTUSD,ZBTUST,OKBUSD,USKUSD,GTXUSD,KANUSD,OKBUST,OKBETH,OKBBTC,USKUST,USKETH,USKBTC,USKEOS,GTXUST,KANUST,AMPUSD,ALGUSD,ALGBTC,ALGUST,BTCXCH,SWMUSD,SWMETH,TRIUSD,TRIETH,LOOUSD,LOOETH,AMPUST,DUSK:USD,DUSK:BTC,UOSUSD,UOSBTC,RRBUSD,RRBUST,DTXUSD,DTXUST,AMPBTC,FTTUSD,FTTUST,PAXUST,UDCUST,TSDUST,BTC:CNHT,UST:CNHT,CNH:CNHT,CHZUSD,CHZUST,BTCF0:USTF0,ETHF0:USTF0" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -299,41 +449,73 @@ "name": "Bitflyer", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC_JPY,FXBTC_JPY,ETH_BTC,BCH_BTC", - "enabledPairs": "BTC_JPY,ETH_BTC,BCH_BTC", "baseCurrencies": "JPY", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": false, - "pairsLastUpdated": 1566798411, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "lastUpdated": 1566798411, + "assetTypes": [ + "spot", + "futures" + ], + "pairs": { + "spot": { + "enabled": "BTC_JPY,ETH_BTC,BCH_BTC", + "available": "BTC_JPY,FXBTC_JPY,ETH_BTC,BCH_BTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": false, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -346,40 +528,72 @@ "name": "Bithumb", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "clientId": "ClientID", - "availablePairs": "VETKRW,REPKRW,ARNKRW,OCNKRW,ETHOSKRW,STEEMKRW,LRCKRW,ETCKRW,CMTKRW,HDACKRW,WTCKRW,PLYKRW,QTUMKRW,MCOKRW,NPXSKRW,ABTKRW,BSVKRW,SNTKRW,STRATKRW,BATKRW,ETHKRW,CTXCKRW,AUTOKRW,HYCKRW,POLYKRW,QKCKRW,TMTGKRW,BCHKRW,MXCKRW,XEMKRW,GTOKRW,BTTKRW,APISKRW,DACKRW,ELFKRW,XLMKRW,DACCKRW,GNTKRW,EOSKRW,TRXKRW,BZNTKRW,ETZKRW,XRPKRW,WAVESKRW,WETKRW,HCKRW,XMRKRW,PPTKRW,LOOMKRW,KNCKRW,MIXKRW,RDNKRW,ADAKRW,ENJKRW,ZRXKRW,DASHKRW,PIVXKRW,THETAKRW,VALORKRW,BHPKRW,OMGKRW,RNTKRW,GXCKRW,AMOKRW,CROKRW,LAMBKRW,LINKKRW,ROMKRW,ZILKRW,ORBSKRW,POWRKRW,INSKRW,CONKRW,XVGKRW,BCDKRW,ICXKRW,BTCKRW,BTGKRW,LBAKRW,MTLKRW,MITHKRW,PAYKRW,WAXKRW,ANKRKRW,IOSTKRW,AEKRW,LTCKRW,ITCKRW,SALTKRW,ZECKRW,TRUEKRW,PSTKRW", - "enabledPairs": "BTCKRW,ETHKRW,DASHKRW,LTCKRW,ETCKRW,XRPKRW,BCHKRW,XMRKRW,ZECKRW,QTUMKRW,BTGKRW,EOSKRW", "baseCurrencies": "KRW", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "index": "KRW" + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "index": "KRW" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCKRW,ETHKRW,DASHKRW,LTCKRW,ETCKRW,XRPKRW,BCHKRW,XMRKRW,ZECKRW,QTUMKRW,BTGKRW,EOSKRW", + "available": "POWRKRW,CMTKRW,ABTKRW,WICCKRW,ADAKRW,ITCKRW,ETHKRW,XMRKRW,BATKRW,MIXKRW,BTGKRW,DVPKRW,XRPKRW,LOOMKRW,BCDKRW,ETZKRW,KNCKRW,CHRKRW,OGOKRW,DASHKRW,CROKRW,TRUEKRW,LAMBKRW,ANKRKRW,STRATKRW,HDACKRW,VALORKRW,PCMKRW,ZECKRW,PAYKRW,INSKRW,FNBKRW,XLMKRW,BSVKRW,BZNTKRW,REPKRW,TRXKRW,AEKRW,ZILKRW,THETAKRW,QKCKRW,GNTKRW,WTCKRW,BTCKRW,ORBSKRW,WOMKRW,FABKRW,WETKRW,ELFKRW,AMOKRW,OMGKRW,POLYKRW,IOSTKRW,HCKRW,PIVXKRW,BCHKRW,AUTOKRW,BHPKRW,ICXKRW,AOAKRW,ETHOSKRW,FCTKRW,NPXSKRW,WAXPKRW,ENJKRW,WAVESKRW,FXKRW,OCNKRW,ARNKRW,MTLKRW,ZRXKRW,QTUMKRW,LRCKRW,APISKRW,MXCKRW,PLYKRW,STEEMKRW,SNTKRW,RNTKRW,EOSKRW,DADKRW,XVGKRW,SALTKRW,TMTGKRW,XSRKRW,CTXCKRW,LBAKRW,PPTKRW,LINKKRW,MCOKRW,FZZKRW,GXCKRW,VETKRW,DACKRW,CONKRW,MITHKRW,BTTKRW,XEMKRW,ETCKRW,HYCKRW,DACCKRW,TRVKRW,LTCKRW,RDNKRW,ROMKRW,PSTKRW,GTOKRW" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -392,38 +606,106 @@ "name": "Bitmex", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "XRPU19,BCHU19,ADAU19,EOSU19,TRXU19,XBTUSD,XBT7D_U105,XBT7D_D95,XBTU19,XBTZ19,ETHUSD,ETHU19,LTCU19", - "enabledPairs": "XBTUSD", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "assetTypes": [ + "perpetualcontract", + "futures", + "downsideprofitcontract", + "upsideprofitcontract" + ], + "pairs": { + "downsideprofitcontract": { + "available": "XBT7D_D95", + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + } + }, + "futures": { + "available": "XRPZ19,BCHZ19,ADAZ19,EOSZ19,TRXZ19,XBTZ19,ETHZ19,LTCZ19", + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + } + }, + "perpetualcontract": { + "available": "XBTUSD,ETHUSD", + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + } + }, + "spot": { + "enabled": "XBTUSD", + "available": "XRPU19,BCHU19,ADAU19,EOSU19,TRXU19,XBTUSD,XBT7D_U105,XBT7D_D95,XBTU19,XBTZ19,ETHUSD,ETHU19,LTCU19" + }, + "upsideprofitcontract": { + "available": "XBT7D_U105", + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + } + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -436,39 +718,71 @@ "name": "Bitstamp", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "clientId": "ClientID", - "availablePairs": "LTCUSD,ETHUSD,XRPEUR,BCHUSD,BCHEUR,BTCEUR,XRPBTC,EURUSD,BCHBTC,LTCEUR,BTCUSD,LTCBTC,XRPUSD,ETHBTC,ETHEUR", - "enabledPairs": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", "baseCurrencies": "USD,EUR", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", + "available": "LTCUSD,ETHUSD,XRPEUR,BCHUSD,BCHEUR,BTCEUR,XRPBTC,EURUSD,BCHBTC,LTCEUR,BTCUSD,LTCBTC,XRPUSD,ETHBTC,ETHEUR" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -481,40 +795,72 @@ "name": "Bittrex", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC-LTC,BTC-DOGE,BTC-VTC,BTC-PPC,BTC-FTC,BTC-RDD,BTC-NXT,BTC-DASH,BTC-POT,BTC-BLK,BTC-EMC2,BTC-XMY,BTC-GLC,BTC-GRS,BTC-NLG,BTC-MONA,BTC-VRC,BTC-CURE,BTC-XMR,BTC-XDN,BTC-NAV,BTC-XST,BTC-VIA,BTC-PINK,BTC-IOC,BTC-SYS,BTC-DGB,BTC-BURST,BTC-EXCL,BTC-BLOCK,BTC-BTS,BTC-XRP,BTC-GAME,BTC-NXS,BTC-GEO,BTC-FLDC,BTC-FLO,BTC-MUE,BTC-XEM,BTC-SPHR,BTC-OK,BTC-AEON,BTC-ETH,BTC-EXP,BTC-AMP,BTC-XLM,USDT-BTC,BTC-RVR,BTC-FCT,BTC-MAID,BTC-SLS,BTC-RADS,BTC-DCR,BTC-XVG,BTC-PIVX,BTC-MEME,BTC-STEEM,BTC-LSK,BTC-DGD,BTC-WAVES,BTC-LBC,BTC-SBD,BTC-ETC,ETH-ETC,BTC-STRAT,BTC-REP,BTC-SHIFT,BTC-ARDR,BTC-XZC,BTC-NEO,BTC-ZEC,BTC-IOP,BTC-UBQ,BTC-KMD,BTC-SIB,BTC-ION,BTC-CRW,BTC-SWT,BTC-MLN,BTC-ARK,BTC-INCNT,BTC-GBYTE,BTC-GNT,BTC-EDG,BTC-MORE,ETH-GNT,ETH-REP,USDT-ETH,BTC-WINGS,BTC-RLC,BTC-GNO,BTC-GUP,ETH-GNO,BTC-HMQ,BTC-ANT,ETH-ANT,BTC-SC,ETH-BAT,BTC-BAT,BTC-ZEN,BTC-1ST,BTC-QRL,BTC-PTOY,BTC-BNT,ETH-BNT,BTC-NMR,ETH-LTC,ETH-XRP,BTC-SNT,ETH-SNT,BTC-DCT,BTC-XEL,BTC-MCO,ETH-MCO,BTC-ADT,BTC-FUN,BTC-PAY,ETH-PAY,BTC-MTL,BTC-STORJ,BTC-ADX,ETH-ADX,ETH-DASH,ETH-SC,ETH-ZEC,USDT-ZEC,USDT-LTC,USDT-ETC,USDT-XRP,BTC-OMG,ETH-OMG,BTC-CVC,ETH-CVC,BTC-PART,BTC-QTUM,ETH-QTUM,ETH-XMR,ETH-XEM,ETH-XLM,ETH-NEO,USDT-XMR,USDT-DASH,ETH-BCH,USDT-BCH,BTC-BCH,BTC-DNT,USDT-NEO,ETH-WAVES,ETH-STRAT,ETH-DGB,USDT-OMG,BTC-ADA,BTC-MANA,ETH-MANA,BTC-RCN,BTC-VIB,ETH-VIB,BTC-MER,BTC-POWR,ETH-POWR,ETH-ADA,BTC-ENG,ETH-ENG,USDT-ADA,USDT-XVG,USDT-NXT,BTC-UKG,ETH-UKG,BTC-IGNIS,BTC-SRN,ETH-SRN,BTC-WAX,ETH-WAX,BTC-ZRX,ETH-ZRX,BTC-VEE,BTC-BCPT,BTC-TRX,ETH-TRX,BTC-TUSD,BTC-LRC,ETH-TUSD,BTC-UP,BTC-DMT,ETH-DMT,USDT-TUSD,BTC-POLY,ETH-POLY,BTC-PRO,USDT-SC,USDT-TRX,BTC-BLT,BTC-STORM,ETH-STORM,BTC-AID,BTC-NGC,BTC-GTO,USDT-DCR,BTC-OCN,ETH-OCN,USD-BTC,USD-USDT,USD-TUSD,BTC-TUBE,BTC-CMCT,USD-ETH,BTC-NLC2,BTC-BKX,BTC-MFT,BTC-LOOM,BTC-RFR,USDT-DGB,BTC-RVN,USD-XRP,USD-ETC,BTC-BFT,BTC-GO,BTC-HYDRO,BTC-UPP,USD-ADA,USD-ZEC,USDT-DOGE,BTC-ENJ,BTC-MET,USD-LTC,USD-TRX,BTC-DTA,BTC-EDR,BTC-BOXX,BTC-IHT,USD-BCH,BTC-XHV,USDT-ZRX,BTC-NPXS,BTC-PMA,USDT-BAT,USDT-RVN,BTC-PAL,USD-SC,BTC-PAX,USDT-PAX,BTC-ZIL,BTC-MOC,BTC-OST,BTC-SPC,BTC-MEDX,BTC-BSV,BTC-IOST,BTC-XNK,USDT-BSV,ETH-BSV,BTC-NCASH,BTC-SOLVE,BTC-USDS,USDT-PMA,ETH-NPXS,USDT-NPXS,USD-ZRX,BTC-JNT,BTC-LBA,BTC-MOBI,USD-BAT,USD-BSV,BTC-DENT,USD-USDS,BTC-DRGN,USD-PAX,BTC-VITE,BTC-IOTX,USD-DGB,BTC-BTM,BTC-ELF,USD-EDR,BTC-QNT,BTC-BTU,USD-ZEN,BTC-SPND,BTC-BTT,BTC-NKN,USD-KMD,USDT-BTT,BTC-GRIN,BTC-CTXC,BTC-HXRO,BTC-META,USDT-GRIN,BTC-FSN,BTC-HST,BTC-ANKR,USDT-XLM,BTC-TRAC,BTC-CRO,BTC-ONT,ETH-SOLVE,BTC-ONG,BTC-AERGO,BTC-TTC,USD-SPND,BTC-SLT,BTC-PTON,BTC-PI,ETH-ANKR,BTC-PLA,BTC-ART,BTC-ORBS,USDT-ENJ,BTC-VBK,BTC-BORA,BTC-CND,USDT-ONT,BTC-TRIO,BTC-FX,ETH-FX,BTC-ATOM,USDT-ATOM,ETH-ATOM,BTC-XYO,BTC-OCEAN,USDT-OCEAN,BTC-WIB,BTC-BWX,BTC-SNX,BTC-SUSD,BTC-VDX,USDT-VDX,ETH-VDX,BTC-COSM,BTC-OGO,USDT-OGO,BTC-ITM,BTC-LAMB,BTC-STPT,BTC-FET,BTC-MKR,ETH-MKR,BTC-DAI,ETH-DAI,USDT-DAI,BTC-CPT,BTC-ABT,BTC-PROM,BTC-FTM,BTC-ABYSS,BTC-EOS,ETH-EOS,USDT-EOS,BTC-FXC,BTC-DUSK,BTC-URAC,BTC-BLOC,BTC-BRZ,BTC-TEMCO,BTC-SPIN,BTC-HINT,BTC-LUNA,BTC-CHR,BTC-TUDA,BTC-UTK,BTC-PXL,BTC-AKRO,BTC-TSHP", - "enabledPairs": "USDT-BTC", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "USDT-BTC", + "available": "BTC-LTC,BTC-DOGE,BTC-VTC,BTC-PPC,BTC-FTC,BTC-RDD,BTC-NXT,BTC-DASH,BTC-POT,BTC-BLK,BTC-EMC2,BTC-XMY,BTC-GRS,BTC-NLG,BTC-MONA,BTC-VRC,BTC-CURE,BTC-XMR,BTC-XDN,BTC-NAV,BTC-XST,BTC-VIA,BTC-PINK,BTC-IOC,BTC-SYS,BTC-DGB,BTC-BURST,BTC-EXCL,BTC-BLOCK,BTC-BTS,BTC-XRP,BTC-GAME,BTC-NXS,BTC-GEO,BTC-FLO,BTC-MUE,BTC-XEM,BTC-SPHR,BTC-OK,BTC-AEON,BTC-ETH,BTC-EXP,BTC-XLM,USDT-BTC,BTC-FCT,BTC-MAID,BTC-SLS,BTC-RADS,BTC-DCR,BTC-XVG,BTC-PIVX,BTC-MEME,BTC-STEEM,BTC-LSK,BTC-WAVES,BTC-LBC,BTC-SBD,BTC-ETC,ETH-ETC,BTC-STRAT,BTC-REP,BTC-ARDR,BTC-XZC,BTC-NEO,BTC-ZEC,BTC-UBQ,BTC-KMD,BTC-SIB,BTC-ION,BTC-CRW,BTC-ARK,BTC-INCNT,BTC-GBYTE,BTC-GNT,BTC-EDG,BTC-MORE,ETH-GNT,ETH-REP,USDT-ETH,BTC-RLC,BTC-GNO,ETH-GNO,BTC-HMQ,BTC-ANT,ETH-ANT,BTC-SC,ETH-BAT,BTC-BAT,BTC-ZEN,BTC-1ST,BTC-QRL,BTC-PTOY,BTC-BNT,ETH-BNT,BTC-NMR,ETH-LTC,ETH-XRP,BTC-SNT,ETH-SNT,BTC-DCT,BTC-XEL,BTC-MCO,ETH-MCO,BTC-ADT,BTC-PAY,ETH-PAY,BTC-MTL,BTC-STORJ,BTC-ADX,ETH-ADX,ETH-DASH,ETH-SC,ETH-ZEC,USDT-ZEC,USDT-LTC,USDT-ETC,USDT-XRP,BTC-OMG,ETH-OMG,BTC-CVC,ETH-CVC,BTC-PART,BTC-QTUM,ETH-QTUM,ETH-XMR,ETH-XEM,ETH-XLM,ETH-NEO,USDT-XMR,USDT-DASH,ETH-BCH,USDT-BCH,BTC-BCH,BTC-DNT,USDT-NEO,ETH-WAVES,ETH-STRAT,ETH-DGB,USDT-OMG,BTC-ADA,BTC-MANA,ETH-MANA,BTC-RCN,BTC-VIB,ETH-VIB,BTC-MER,ETH-ADA,BTC-ENG,ETH-ENG,USDT-ADA,USDT-XVG,BTC-UKG,ETH-UKG,BTC-IGNIS,BTC-SRN,ETH-SRN,BTC-WAXP,ETH-WAXP,BTC-ZRX,ETH-ZRX,BTC-VEE,BTC-TRX,ETH-TRX,BTC-TUSD,BTC-LRC,ETH-TUSD,BTC-DMT,ETH-DMT,USDT-TUSD,USDT-SC,USDT-TRX,BTC-STORM,ETH-STORM,BTC-AID,BTC-GTO,USDT-DCR,USD-BTC,USD-USDT,USD-TUSD,BTC-TUBE,BTC-CMCT,USD-ETH,BTC-NLC2,BTC-BKX,BTC-MFT,BTC-LOOM,BTC-RFR,USDT-DGB,BTC-RVN,USD-XRP,USD-ETC,BTC-BFT,BTC-GO,BTC-HYDRO,BTC-UPP,USD-ADA,USD-ZEC,USDT-DOGE,BTC-ENJ,BTC-MET,USD-LTC,USD-TRX,BTC-DTA,BTC-EDR,BTC-IHT,USD-BCH,BTC-XHV,USDT-ZRX,BTC-NPXS,BTC-PMA,USDT-BAT,USDT-RVN,BTC-PAL,USD-SC,BTC-PAX,BTC-ZIL,BTC-MOC,BTC-OST,BTC-SPC,BTC-MED,BTC-BSV,BTC-IOST,USDT-BSV,ETH-BSV,BTC-SOLVE,BTC-USDS,USDT-PMA,ETH-NPXS,USDT-NPXS,USD-ZRX,BTC-JNT,BTC-LBA,USD-BAT,USD-BSV,BTC-DENT,USD-USDS,BTC-DRGN,USD-PAX,BTC-VITE,BTC-IOTX,USD-DGB,BTC-BTM,BTC-ELF,BTC-QNT,BTC-BTU,USD-ZEN,BTC-SPND,BTC-BTT,BTC-NKN,USD-KMD,USDT-BTT,BTC-GRIN,BTC-CTXC,BTC-HXRO,BTC-META,USDT-GRIN,BTC-FSN,BTC-ANKR,USDT-XLM,BTC-TRAC,BTC-CRO,BTC-ONT,ETH-SOLVE,BTC-ONG,BTC-TTC,BTC-PTON,BTC-PI,ETH-ANKR,BTC-PLA,BTC-ART,BTC-ORBS,USDT-ENJ,BTC-VBK,BTC-BORA,BTC-CND,USDT-ONT,BTC-FX,ETH-FX,BTC-ATOM,USDT-ATOM,ETH-ATOM,BTC-OCEAN,USDT-OCEAN,BTC-BWX,BTC-VDX,USDT-VDX,ETH-VDX,BTC-COSM,BTC-LAMB,BTC-STPT,BTC-DAI,ETH-DAI,USDT-DAI,BTC-CPT,BTC-FNB,BTC-PROM,BTC-ABYSS,BTC-EOS,ETH-EOS,USDT-EOS,BTC-FXC,BTC-DUSK,BTC-URAC,BTC-BLOC,BTC-TEMCO,BTC-SPIN,BTC-LUNA,BTC-CHR,BTC-TUDA,BTC-UTK,BTC-PXL,BTC-AKRO,BTC-TSHP,BTC-HEDG,BTC-MRPH,BTC-HBAR,ETH-HBAR,USD-HBAR,USDT-HBAR,BTC-PLG,BTC-VET,USDT-VET,BTC-SIX,BTC-WGP,BTC-APM,BTC-FLETA,USD-DCR,BTC-BLTV" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -527,40 +873,71 @@ "name": "BTSE", "enabled": true, "verbose": false, - "websocket": true, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC-CNY,BTC-EUR,BTC-GBP,BTC-HKD,BTC-JPY,BTC-SGD,BTC-USD,ETH-CNY,ETH-EUR,ETH-GBP,ETH-HKD,ETH-JPY,ETH-SGD,ETH-USD,LTC-CNY,LTC-EUR,LTC-GBP,LTC-HKD,LTC-JPY,LTC-SGD,LTC-USD,USDT-CNY,USDT-EUR,USDT-GBP,USDT-HKD,USDT-JPY,USDT-SGD,USDT-USD", - "enabledPairs": "BTC-USD", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USD", + "available": "BTC-CNY,BTC-EUR,BTC-GBP,BTC-HKD,BTC-JPY,BTC-SGD,BTC-USD,ETH-CNY,ETH-EUR,ETH-GBP,ETH-HKD,ETH-JPY,ETH-SGD,ETH-USD,LTC-CNY,LTC-EUR,LTC-GBP,LTC-HKD,LTC-JPY,LTC-SGD,LTC-USD,USDT-CNY,USDT-EUR,USDT-GBP,USDT-HKD,USDT-JPY,USDT-SGD,USDT-USD,XMR-CNY,XMR-EUR,XMR-GBP,XMR-HKD,XMR-JPY,XMR-SGD,XMR-USD" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -573,39 +950,71 @@ "name": "BTC Markets", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC-AUD,LTC-AUD,LTC-BTC,ETH-BTC,ETH-AUD,ETC-AUD,ETC-BTC,XRP-AUD,XRP-BTC,POWR-AUD,POWR-BTC,OMG-AUD,OMG-BTC,BCHABC-AUD,BCHABC-BTC,BCHSV-AUD,BCHSV-BTC,GNT-AUD,GNT-BTC,BAT-AUD,BAT-BTC,XLM-AUD,XLM-BTC", - "enabledPairs": "BTC-AUD", "baseCurrencies": "AUD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-AUD", + "available": "BTC-AUD,LTC-AUD,LTC-BTC,ETH-BTC,ETH-AUD,ETC-AUD,ETC-BTC,XRP-AUD,XRP-BTC,POWR-AUD,POWR-BTC,OMG-AUD,OMG-BTC,BCHABC-AUD,BCHABC-BTC,BCHSV-AUD,BCHSV-BTC,GNT-AUD,GNT-BTC,BAT-AUD,BAT-BTC,XLM-AUD,XLM-BTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresBase64DecodeSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -618,39 +1027,70 @@ "name": "COINUT", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "clientId": "ClientID", - "availablePairs": "BTCUSDT,ETCBTC,ETHUSDT,BTCCAD,ETHLTC,ETHUSD,LTCUSD,BTCSGD,ETCSGD,ETHSGD,ZECBTC,ZECUSD,ZECUSDT,ETHBTC,LTCBTC,USDTSGD,USDTUSD,XMRUSDT,BTCUSD,LTCCAD,LTCSGD,LTCUSDT,XMRBTC,XMRLTC,ZECLTC,ETCUSDT,ZECSGD,ETCLTC,ETHCAD,ZECCAD", - "enabledPairs": "LTCBTC,ETCBTC,ETHBTC", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "LTC-USDT", + "available": "ETH-BTC,LTC-SGD,BTC-CAD,DAI-SGD,ETH-CAD,ETH-SGD,ETH-USD,ZEC-USDT,BTC-USD,ETC-SGD,ETH-LTC,ZEC-SGD,BTC-SGD,ETC-USDT,XMR-BTC,XMR-USDT,ZEC-BTC,ZEC-CAD,ZEC-LTC,LTC-CAD,LTC-USDT,LTC-USD,XMR-LTC,ETC-LTC,LTC-BTC,ETH-USDT,ZEC-USD,USDT-SGD,USDT-USD,BTC-USDT,ETC-BTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -663,41 +1103,73 @@ "name": "EXMO", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "WAVES_BTC,BTC_RUB,DCR_UAH,XMR_UAH,USDC_BTC,XEM_USD,XLM_RUB,ATMCASH_BTC,QTUM_USD,ADA_USD,TRX_BTC,XRP_BTC,MKR_DAI,STQ_USD,ETH_USD,KICK_USDT,ZRX_USD,USDC_ETH,GUSD_BTC,ZRX_ETH,DASH_BTC,ETC_BTC,LTC_RUB,BTC_USD,STQ_EUR,BCH_RUB,XRP_USDT,WAVES_ETH,XTZ_ETH,QTUM_BTC,XEM_BTC,LSK_BTC,TRX_RUB,ETH_PLN,PTI_USDT,MNC_ETH,DAI_BTC,NEO_USD,KICK_BTC,ETH_BTC,ZEC_BTC,ETZ_USDT,DAI_ETH,DAI_USD,GNT_ETH,HBZ_USD,DXT_BTC,XRP_TRY,DAI_RUB,MNX_BTC,BCH_ETH,WAVES_USD,TRX_USD,INK_ETH,XLM_BTC,XMR_USD,KICK_ETH,DASH_RUB,LTC_BTC,USDT_RUB,USDT_EUR,DOGE_USD,DASH_UAH,XTZ_USD,ETZ_ETH,HB_BTC,GUSD_RUB,BTC_TRY,ADA_BTC,ADA_ETH,BTG_BTC,BCH_USDT,USDT_UAH,PTI_RUB,XTZ_RUB,DASH_USD,LTC_USD,ETH_USDT,MNC_BTC,XEM_EUR,GUSD_USD,XMR_BTC,XRP_EUR,SMART_USD,HBZ_BTC,BCH_USD,ETH_RUB,XRP_ETH,ZEC_RUB,XRP_RUB,DCR_BTC,DCR_RUB,PTI_EOS,EOS_USD,DXT_USD,ETH_LTC,BTC_USDT,USDT_USD,DASH_USDT,BTG_ETH,BCH_UAH,ROOBEE_ETH,TRX_UAH,MNC_USD,QTUM_ETH,BTCZ_BTC,XRP_UAH,USDC_USDT,NEO_BTC,OMG_ETH,STQ_BTC,ETC_USD,XMR_EUR,EOS_EUR,BTC_PLN,NEO_RUB,ZRX_BTC,INK_BTC,MNX_ETH,ETH_UAH,LSK_RUB,BCH_BTC,ETH_EUR,XLM_USD,ETC_RUB,DOGE_BTC,EXM_BTC,ROOBEE_BTC,LSK_USD,HBZ_ETH,LTC_EUR,USD_RUB,KICK_RUB,USDC_USD,PTI_BTC,OMG_USD,XRP_USD,XEM_UAH,GNT_BTC,LTC_UAH,SMART_BTC,SMART_EUR,SMART_RUB,BTG_USD,GAS_USD,BTC_UAH,XTZ_BTC,ZEC_USD,MKR_BTC,INK_USD,EOS_BTC,STQ_RUB,ZEC_EUR,XMR_ETH,BTC_EUR,XMR_RUB,XLM_TRY,GAS_BTC,MNX_USD,WAVES_RUB,ETZ_BTC,ETH_TRY,OMG_BTC,BCH_EUR", - "enabledPairs": "BTC_USD,LTC_USD", "baseCurrencies": "USD,EUR,RUB,PLN,UAH", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "_", + "separator": "," + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_USD,LTC_USD", + "available": "DCR_RUB,DAI_USD,ZRX_ETH,VLX_BTC,XRP_BTC,DASH_UAH,ETH_RUB,ZEC_RUB,USD_RUB,XTZ_BTC,PTI_EOS,ETH_LTC,SMART_USD,ADA_BTC,GNT_BTC,XRP_RUB,USDC_USDT,ETZ_BTC,HB_BTC,DAI_BTC,SMART_EUR,BCH_ETH,LTC_BTC,USDT_RUB,BTC_PLN,XMR_UAH,TRX_UAH,XRP_TRY,TRX_RUB,LTC_UAH,USDC_USD,ZRX_BTC,DASH_USD,DCR_BTC,PTI_USDT,MKR_BTC,DASH_RUB,DASH_USDT,XTZ_RUB,QTUM_ETH,OMG_USD,MNX_ETH,XRP_UAH,NEO_BTC,GNT_ETH,INK_ETH,BTG_USD,ZAG_BTC,XTZ_USD,LSK_RUB,BTT_BTC,BCH_EUR,ETH_USDT,WAVES_BTC,TRX_USD,MNX_USD,BCH_RUB,GAS_BTC,ETH_UAH,KICK_ETH,USDC_BTC,ETZ_USDT,LSK_USD,BCH_UAH,XMR_RUB,ETH_USD,LTC_EUR,USDT_USD,XLM_USD,BTG_BTC,BCH_BTC,SMART_BTC,XEM_USD,XMR_BTC,QTUM_BTC,OMG_ETH,ZEC_EUR,XTZ_ETH,MNC_ETH,DAI_RUB,XEM_EUR,DXT_USD,ETH_EUR,ZEC_USD,DOGE_BTC,DCR_UAH,MKR_DAI,QTUM_USD,XRP_USDT,WAVES_ETH,BTG_ETH,KICK_RUB,ETC_RUB,KICK_USDT,ZEC_BTC,XEM_UAH,DOGE_USD,GUSD_BTC,GUSD_RUB,GAS_USD,LSK_BTC,MNX_BTC,DXT_BTC,LTC_USD,BTC_USD,EXM_BTC,MNC_BTC,BTC_USDT,PTI_RUB,SMART_RUB,XMR_EUR,GUSD_USD,OMG_BTC,ETH_PLN,EOS_EUR,XRP_ETH,BTC_TRY,ROOBEE_BTC,ETH_TRY,XMR_ETH,ADA_USD,XLM_RUB,ETC_BTC,ETC_USD,XRP_USD,ATMCASH_BTC,XEM_BTC,NEO_RUB,XMR_USD,USDT_EUR,INK_USD,USDC_ETH,ETZ_ETH,TRX_BTC,DASH_BTC,ZRX_USD,EOS_USD,XRP_EUR,BTC_EUR,BTT_UAH,INK_BTC,BCH_USD,ETH_BTC,XLM_TRY,NEO_USD,BTCZ_BTC,LTC_RUB,KICK_BTC,BCH_USDT,ROOBEE_ETH,MNC_USD,DAI_ETH,EOS_BTC,WAVES_USD,BTC_RUB,BTC_UAH,ADA_ETH,WAVES_RUB,USDT_UAH,BTT_RUB,PTI_BTC,XLM_BTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_", - "separator": "," + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -710,40 +1182,73 @@ "name": "CoinbasePro", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "clientId": "ClientID", - "availablePairs": "EOSUSD,ETHBTC,ETHUSDC,ETHEUR,ZECUSDC,REPUSD,LIN-ETH,EOSBTC,LTCGBP,CVCUSDC,XLMEUR,ETCGBP,XTZBTC,XRPUSD,XRPBTC,ALG-USD,BTCUSDC,GNTUSDC,ZRXBTC,DNTUSDC,BTCUSD,LTCBTC,LTCUSD,ETHGBP,ZRXUSD,BATETH,ZRXEUR,REPBTC,ETCEUR,XRPEUR,EOSEUR,BCHEUR,MAN-USDC,XLMUSD,BATUSDC,LOO-USDC,BTCEUR,BCHGBP,LTCEUR,BCHBTC,LIN-USD,DAIUSDC,XTZUSD,ETCBTC,BCHUSD,BTCGBP,ETHUSD,XLMBTC,ETCUSD,ZECBTC,ETHDAI", - "enabledPairs": "BTCUSD,BTCGBP,BTCEUR", "baseCurrencies": "USD,GBP,EUR", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USD", + "available": "BTC-GBP,BTC-USD,REP-BTC,LTC-EUR,BCH-BTC,XRP-USD,BTC-USDC,DNT-USDC,ETH-DAI,GNT-USDC,EOS-BTC,XTZ-BTC,EOS-USD,ZEC-USDC,BTC-EUR,BAT-USDC,DASH-USD,ETC-USD,XLM-BTC,XRP-EUR,ETH-BTC,BCH-GBP,XRP-BTC,LTC-BTC,MANA-USDC,LOOM-USDC,BAT-ETH,ZRX-BTC,REP-USD,LTC-USD,EOS-EUR,BCH-USD,XLM-EUR,XTZ-USD,ETC-BTC,ZEC-BTC,ETC-EUR,ZRX-EUR,ETH-EUR,LTC-GBP,DAI-USDC,ZRX-USD,ETH-USDC,BCH-EUR,LINK-ETH,ETC-GBP,DASH-BTC,XLM-USD,CVC-USDC,ETH-USD,ETH-GBP,ALGO-USD,LINK-USD" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true, + "requiresBase64DecodeSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -753,90 +1258,153 @@ ] }, { - "name": "Coinbene", - "enabled": true, - "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "clientId": "ClientID", - "availablePairs": "ABBC/BTC,ABT/ETH,ABT/USDT,ABYSS/ETH,ACDC/BTC,ACDC/USDT,ADI/ETH,ADK/BTC,ADN/BTC,AE/BTC,AE/USDT,AID/BTC,AIDOC/BTC,AION/BTC,AIPE/USDT,AIT/USDT,ALGO/USDT,ALI/ETH,ALX/ETH,APL/ETH,ATX/BTC,B2G/BTC,B91/USDT,BAAS/BTC,BAT/BTC,BCHABC/USDT,BCHSV/USDT,BEAUTY/ETH,BETHER/ETH,BEZ/BTC,BGC/USDT,BKG/BTC,BNT/BTC,BOA/USDT,BSTN/ETH,BTC/USDT,BTFM/USDT,BTNT/BTC,BTSC/BTC,BTT/USDT,BU/ETH,BVT/ETH,C3W/ETH,CAN/ETH,CCC/ETH,CCE/USDT,CC/USDT,CEDEX/ETH,CENT/BTC,CFT/USDT,CLO/BTC,CMT/ETH,CMT/USDT,CNN/BTC,CNN/ETH,CNN/USDT,CONI/USDT,COSM/BTC,COSM/ETH,COZP/BTC,CPC/BTC,CPMS/USDT,CREDO/ETH,CRN/BTC,CS/ETH,CS/USDT,CTXC/ETH,CUST/USDT,CVC/BTC,CXC/USDT,CXP/BTC,DCA/ETH,DCT/BTC,DENT/BTC,DGD/BTC,DOCK/ETH,DSCB/USDT,DTA/ETH,DUC/BTC,DVC/ETH,EBC/BTC,EBC/ETH,EBC/USDT,ECA/BTC,EDC/BTC,EDR/ETH,ELF/BTC,EMT/USDT,EOS/BTC,EOS/USDT,EQUAD/BTC,ETC/BTC,ETC/USDT,ETH/BTC,ETH/USDT,ETK/BTC,ETN/BTC,FAB/ETH,FACC/ETH,FCC/BTC,FDS/USDT,FND/ETH,FNKOS/ETH,FTN/BTC,FTN/USDT,FTT/BTC,FXT/ETH,GETX/ETH,GLDR/ETH,GMTK/ETH,GOM/USDT,GRAM/USDT,GRIN/BTC,GRN/BTC,GSTT/USDT,GUSD/USDT,GVT/BTC,HAPPY/BTC,HDAC/BTC,HMB/USDT,HNB/USDT,HPT/ETH,HUP/USDT,INCX/ETH,IOST/BTC,IOTE/USDT,ISR/BTC,ISR/ETH,IVY/ETH,JOB/BTC,KBC/BTC,KBC/USDT,KMD/BTC,KNT/ETH,KST/BTC,KUE/BTC,KUE/ETH,KUKY/BTC,LAMB/USDT,LATX/BTC,LBK/BTC,LINK/BTC,LOOM/BTC,LTC/BTC,LTC/USDT,LUC/ETH,LUX/BTC,LVTC/ETH,MDC/USDT,MGC/USDT,MIB/BTC,MINX/BTC,MINX/ETH,MOAC/USDT,MPL/BTC,MTC/BTC,MT/ETH,MTN/ETH,MT/USDT,MVL/ETH,MVPT/ETH,MWT/USDT,NANO/BTC,NBAI/ETH,NCASH/BTC,NEO/BTC,NEO/USDT,NOBS/BTC,NPXS/ETH,NPXS/USDT,NTY/ETH,ODC/USDT,OMG/BTC,OMX/ETH,OVC/ETH,OZX/ETH,PAL/ETH,PAT/ETH,PAX/USDT,PKX/BTC,PLAY/BTC,PMA/ETH,POLL/BTC,POLY/BTC,PPT/BTC,PSM/BTC,QKC/BTC,QTUM/BTC,QTUM/USDT,RBG/BTC,RBG/ETH,RBG/USDT,RBTC/BTC,RBZ/USDT,RCOIN/BTC,RCOIN/USDT,REP/BTC,REV/BTC,RIF/BTC,SALT/BTC,SCC/BTC,SCO/BTC,SEN/BTC,SENC/ETH,SHE/BTC,SHVR/BTC,SIM/BTC,SKB/BTC,SKM/ETH,SKYM/USDT,SLT/ETH,SMARTUP/ETH,SMARTUP/USDT,SMART/USDT,SORO/USDT,SRCOIN/BTC,SRCOIN/ETH,STORJ/BTC,STQ/BTC,SWET/BTC,SWTC/USDT,TCT/BTC,TEMCO/USDT,TEN/BTC,TEN/ETH,THM/ETH,TIB/BTC,TIMO/USDT,TMTG/BTC,TOC/ETH,TOSC/BTC,TRUE/ETH,TRX/BTC,TRX/USDT,TSL/BTC,TVB/USDT,UTNP/BTC,VBT/USDT,VEEN/BTC,VME/BTC,VME/ETH,VOLLAR/USDT,VSC/ETH,W12/BTC,W12/ETH,WBL/BTC,WFX/BTC,XEM/BTC,XLM/BTC,XMCT/ETH,XMCT/USDT,XMR/BTC,XNK/ETH,XRP/BTC,XRP/USDT,XSR/USDT,YTA/USDT,ZAT/ETH,ZDC/BTC,ZEC/BTC,ZGC/BTC,ZRX/BTC", - "enabledPairs": "BTC/USDT", - "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "/" - }, - "requestCurrencyPairFormat": { + "name": "Coinbene", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { "uppercase": true, "delimiter": "/" }, - "bankAccounts": [ - { - "bankName": "", - "bankAddress": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" + "configFormat": { + "uppercase": true, + "delimiter": "/" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC/USDT", + "available": "ABBC/BTC,ABBC/USDT,ABT/ETH,ABT/USDT,ABYSS/ETH,ACDC/BTC,ACDC/USDT,ADI/ETH,ADK/BTC,ADN/BTC,AE/BTC,AE/USDT,AIDOC/BTC,AION/BTC,AIPE/USDT,AIT/USDT,ALGO/USDT,ALI/ETH,ALX/ETH,APL/ETH,ATX/BTC,B2G/BTC,BAAS/BTC,BAT/BTC,BCH/USDT,BEAUTY/ETH,BETHER/ETH,BEZ/BTC,BGC/USDT,BKG/BTC,BNB/USDT,BNT/BTC,BOA/USDT,BSTN/ETH,BSV/USDT,BTC/USDT,BTFM/USDT,BTNT/BTC,BTSC/BTC,BTT/USDT,BU/ETH,BVT/ETH,C3W/ETH,CAN/ETH,CCC/ETH,CCE/USDT,CC/USDT,CEDEX/ETH,CENT/BTC,CFT/USDT,CLO/BTC,CMT/ETH,CMT/USDT,CNN/BTC,CNN/ETH,CNN/USDT,CONI/USDT,COSM/BTC,COSM/ETH,COZP/BTC,CPC/BTC,CREDO/ETH,CRN/BTC,CSCC/USDT,CS/ETH,CS/USDT,CTXC/ETH,CUST/USDT,CVC/BTC,CXP/BTC,DCA/ETH,DCT/BTC,DDAM/ETH,DDAM/USDT,DENT/BTC,DGD/BTC,DOCK/ETH,DSCB/USDT,DTA/ETH,DUC/BTC,DVC/ETH,EBC/BTC,EBC/ETH,EBC/USDT,ECA/BTC,ECP/BTC,EDC/BTC,EDR/ETH,ELF/BTC,EMT/USDT,EOS/BTC,EOS/USDT,EQUAD/BTC,ETC/BTC,ETC/USDT,ETH/BTC,ETH/USDT,ETK/BTC,ETN/BTC,FAB/ETH,FCC/BTC,FND/ETH,FNKOS/ETH,FTN/BTC,FTN/USDT,FTT/BTC,FXT/ETH,GDC/BTC,GDC/ETH,GDC/USDT,GETX/ETH,GLDR/ETH,GMTK/ETH,GOM2/USDT,GRAM/USDT,GRIN/BTC,GRN/BTC,GUSD/USDT,GVT/BTC,HAPPY/BTC,HDAC/BTC,HMB/USDT,HNB/USDT,HPT/ETH,HT/USDT,HUP/USDT,INCX/ETH,IOST/BTC,IOTE/USDT,ISR/BTC,ISR/ETH,IVY/ETH,JOB/BTC,KBC/BTC,KBC/USDT,KMD/BTC,KNT/ETH,KST/BTC,KUE/BTC,KUE/ETH,LAMB/USDT,LATX/BTC,LBK/BTC,LINK/BTC,LOOM/BTC,LTC/BTC,LTC/USDT,LUC/ETH,LUX/BTC,LVTC/ETH,MC/USDT,MDC/USDT,MIB/BTC,MINX/BTC,MINX/ETH,MOAC/USDT,MPL/BTC,MTC/BTC,MT/ETH,MTN/ETH,MT/USDT,MVL/ETH,MVPT/ETH,MXM/ETH,MXM/USDT,MZG/USDT,NANO/BTC,NBAI/ETH,NCASH/BTC,NEO/BTC,NEO/USDT,NFT/USDT,NOBS/BTC,NPXS/ETH,NPXS/USDT,NTY/ETH,ODC/USDT,OMG/BTC,OMX/ETH,OVC/ETH,OZX/ETH,PAL/ETH,PAT/ETH,PAX/USDT,PKX/BTC,PLAY/BTC,PMA/ETH,POLL/BTC,POLY/BTC,PPT/BTC,PSM/BTC,QKC/BTC,QTUM/BTC,QTUM/USDT,RBTC/BTC,RCOIN/BTC,RCOIN/USDT,REP/BTC,REV/BTC,RIF/BTC,SBT/USDT,SCC/BTC,SCO/BTC,SEN/BTC,SENC/ETH,SHE/BTC,SHVR/BTC,SIM/BTC,SKB/BTC,SKM/ETH,SKYM/USDT,SLT/ETH,SMARTUP/ETH,SMARTUP/USDT,SMART/USDT,SORO/USDT,SRCOIN/BTC,SRCOIN/ETH,STORJ/BTC,STQ/BTC,SWET/BTC,SWTC/USDT,TCT/BTC,TEMCO/USDT,TEN/BTC,TEN/ETH,TIB/BTC,TMTG/BTC,TOC/ETH,TOOS/USDT,TOSC/BTC,TRUE/ETH,TRX/BTC,TRX/USDT,TSL/BTC,TVB/USDT,UNI/USDT,UTNP/BTC,VBT/USDT,VEEN/BTC,VME/BTC,VME/ETH,VSC/ETH,VSF/BTC,W12/BTC,W12/ETH,WBL/BTC,WFX/BTC,XEM/BTC,XLM/BTC,XMCT/ETH,XMCT/USDT,XMR/BTC,XNK/ETH,XRP/BTC,XRP/USDT,XSR/USDT,YAP/BTC,YAP/USDT,YTA/USDT,ZAT/ETH,ZDC/BTC,ZEC/BTC,ZGC/BTC,ZRX/BTC" } - ] + } }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, { "name": "GateIO", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "USDT_CNYX,BTC_CNYX,ETH_CNYX,EOS_CNYX,BCH_CNYX,XRP_CNYX,DOGE_CNYX,TIPS_CNYX,BTC_USDC,BTC_PAX,BTC_USDT,BCH_USDT,ETH_USDT,ETC_USDT,QTUM_USDT,LTC_USDT,DASH_USDT,ZEC_USDT,BTM_USDT,EOS_USDT,REQ_USDT,SNT_USDT,OMG_USDT,PAY_USDT,CVC_USDT,ZRX_USDT,TNT_USDT,XMR_USDT,XRP_USDT,DOGE_USDT,BAT_USDT,PST_USDT,BTG_USDT,DPY_USDT,LRC_USDT,STORJ_USDT,RDN_USDT,STX_USDT,KNC_USDT,LINK_USDT,CDT_USDT,AE_USDT,AE_ETH,AE_BTC,CDT_ETH,RDN_ETH,STX_ETH,KNC_ETH,LINK_ETH,REQ_ETH,RCN_ETH,TRX_ETH,ARN_ETH,KICK_ETH,BNT_ETH,VET_ETH,MCO_ETH,FUN_ETH,DATA_ETH,RLC_ETH,RLC_USDT,ZSC_ETH,WINGS_ETH,MDA_ETH,RCN_USDT,TRX_USDT,KICK_USDT,VET_USDT,MCO_USDT,FUN_USDT,DATA_USDT,ZSC_USDT,MDA_USDT,XTZ_USDT,XTZ_BTC,XTZ_ETH,GNT_USDT,GNT_ETH,GEM_USDT,GEM_ETH,RFR_USDT,RFR_ETH,DADI_USDT,DADI_ETH,ABT_USDT,ABT_ETH,LEDU_BTC,LEDU_ETH,OST_USDT,OST_ETH,XLM_USDT,XLM_ETH,XLM_BTC,MOBI_USDT,MOBI_ETH,MOBI_BTC,OCN_USDT,OCN_ETH,OCN_BTC,ZPT_USDT,ZPT_ETH,ZPT_BTC,COFI_USDT,COFI_ETH,JNT_USDT,JNT_ETH,JNT_BTC,BLZ_USDT,BLZ_ETH,GXS_USDT,GXS_BTC,MTN_USDT,MTN_ETH,RUFF_USDT,RUFF_ETH,RUFF_BTC,TNC_USDT,TNC_ETH,TNC_BTC,ZIL_USDT,ZIL_ETH,BTO_USDT,BTO_ETH,THETA_USDT,THETA_ETH,DDD_USDT,DDD_ETH,DDD_BTC,MKR_USDT,MKR_ETH,DAI_USDT,SMT_USDT,SMT_ETH,MDT_USDT,MDT_ETH,MDT_BTC,MANA_USDT,MANA_ETH,LUN_USDT,LUN_ETH,SALT_USDT,SALT_ETH,FUEL_USDT,FUEL_ETH,ELF_USDT,ELF_ETH,DRGN_USDT,DRGN_ETH,GTC_USDT,GTC_ETH,GTC_BTC,QLC_USDT,QLC_BTC,QLC_ETH,DBC_USDT,DBC_BTC,DBC_ETH,BNTY_USDT,BNTY_ETH,LEND_USDT,LEND_ETH,ICX_USDT,ICX_ETH,BTF_USDT,BTF_BTC,ADA_USDT,ADA_BTC,LSK_USDT,LSK_BTC,WAVES_USDT,WAVES_BTC,BIFI_USDT,BIFI_BTC,MDS_ETH,MDS_USDT,DGD_USDT,DGD_ETH,QASH_USDT,QASH_ETH,QASH_BTC,POWR_USDT,POWR_ETH,POWR_BTC,FIL_USDT,BCD_USDT,BCD_BTC,SBTC_USDT,SBTC_BTC,GOD_USDT,GOD_BTC,BCX_USDT,BCX_BTC,QSP_USDT,QSP_ETH,INK_BTC,INK_USDT,INK_ETH,INK_QTUM,MED_QTUM,MED_ETH,MED_USDT,QBT_QTUM,QBT_ETH,QBT_USDT,TSL_QTUM,TSL_USDT,GNX_USDT,GNX_ETH,NEO_USDT,GAS_USDT,NEO_BTC,GAS_BTC,IOTA_USDT,IOTA_BTC,NAS_USDT,NAS_ETH,NAS_BTC,ETH_BTC,ETC_BTC,ETC_ETH,ZEC_BTC,DASH_BTC,LTC_BTC,BCH_BTC,BTG_BTC,QTUM_BTC,QTUM_ETH,XRP_BTC,DOGE_BTC,XMR_BTC,ZRX_BTC,ZRX_ETH,DNT_ETH,DPY_ETH,OAX_BTC,OAX_USDT,OAX_ETH,REP_ETH,LRC_ETH,LRC_BTC,PST_ETH,BCDN_ETH,BCDN_USDT,TNT_ETH,SNT_ETH,SNT_BTC,BTM_ETH,BTM_BTC,SNET_ETH,SNET_USDT,LLT_SNET,OMG_ETH,OMG_BTC,PAY_ETH,PAY_BTC,BAT_ETH,BAT_BTC,CVC_ETH,STORJ_ETH,STORJ_BTC,EOS_ETH,EOS_BTC,BTS_USDT,BTS_BTC,TIPS_ETH,GT_BTC,GT_USDT,ATOM_BTC,ATOM_USDT,XEM_ETH,XEM_USDT,XEM_BTC,BU_USDT,BU_ETH,BU_BTC,BCHSV_USDT,BCHSV_CNYX,BCHSV_BTC,DCR_USDT,DCR_BTC,BCN_USDT,BCN_BTC,XMC_USDT,XMC_BTC,ATP_USDT,ATP_ETH,NBOT_ETH,NBOT_USDT,MEDX_USDT,MEDX_ETH,GRIN_USDT,GRIN_ETH,GRIN_BTC,BEAM_USDT,BEAM_ETH,BEAM_BTC,VTHO_ETH,BTT_USDT,BTT_ETH,BTT_TRX,TFUEL_ETH,TFUEL_USDT,CELR_ETH,CELR_USDT,CS_ETH,CS_USDT,MAN_ETH,MAN_USDT,REM_ETH,REM_USDT,LYM_ETH,LYM_BTC,LYM_USDT,ONG_ETH,ONG_USDT,ONT_ETH,ONT_USDT,BFT_ETH,BFT_USDT,IHT_ETH,IHT_USDT,SENC_ETH,SENC_USDT,TOMO_ETH,TOMO_USDT,ELEC_ETH,ELEC_USDT,HAV_ETH,HAV_USDT,SWTH_ETH,SWTH_USDT,NKN_ETH,NKN_USDT,SOUL_ETH,SOUL_USDT,LRN_ETH,LRN_USDT,EOSDAC_ETH,EOSDAC_USDT,DOCK_USDT,DOCK_ETH,GSE_USDT,GSE_ETH,RATING_USDT,RATING_ETH,HSC_USDT,HSC_ETH,HIT_USDT,HIT_ETH,DX_USDT,DX_ETH,CNNS_ETH,CNNS_USDT,DREP_ETH,DREP_USDT,MBL_USDT,MBL_ETH,GMAT_USDT,GMAT_ETH,MIX_USDT,MIX_ETH,LAMB_USDT,LAMB_ETH,LEO_USDT,LEO_BTC,WICC_USDT,WICC_ETH,SERO_USDT,SERO_ETH,VIDY_USDT,VIDY_ETH,KGC_USDT,FTM_USDT,FTM_ETH,ONE_USDT,ARPA_USDT,ARPA_ETH,ALGO_USDT,BKC_USDT,BXC_USDT,BXC_ETH,PAX_USDT,PAX_CNYX,USDC_CNYX,USDC_USDT,TUSD_CNYX,TUSD_USDT,HC_USDT,HC_BTC,HC_ETH,GARD_USDT,GARD_ETH,FTI_USDT,FTI_ETH,SOP_ETH,SOP_USDT,LEMO_USDT,LEMO_ETH,QKC_USDT,QKC_ETH,IOTX_USDT,IOTX_ETH,RED_USDT,RED_ETH,LBA_USDT,LBA_ETH,OPEN_USDT,OPEN_ETH,MITH_USDT,MITH_ETH,SKM_USDT,SKM_ETH,XVG_USDT,XVG_BTC,NANO_USDT,NANO_BTC,HT_USDT,BNB_USDT,MET_ETH,MET_USDT,TCT_ETH,TCT_USDT,MXC_USDT,MXC_BTC,MXC_ETH", - "enabledPairs": "BTC_USDT", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_USDT", + "available": "USDT_CNYX,BTC_CNYX,ETH_CNYX,EOS_CNYX,BCH_CNYX,XRP_CNYX,DOGE_CNYX,TIPS_CNYX,BTC_USDC,BTC_PAX,BTC_USDT,BCH_USDT,ETH_USDT,ETC_USDT,QTUM_USDT,LTC_USDT,DASH_USDT,ZEC_USDT,BTM_USDT,EOS_USDT,REQ_USDT,SNT_USDT,OMG_USDT,PAY_USDT,CVC_USDT,ZRX_USDT,TNT_USDT,XMR_USDT,XRP_USDT,DOGE_USDT,BAT_USDT,PST_USDT,BTG_USDT,DPY_USDT,LRC_USDT,STORJ_USDT,RDN_USDT,STX_USDT,KNC_USDT,LINK_USDT,CDT_USDT,AE_USDT,AE_ETH,AE_BTC,CDT_ETH,RDN_ETH,STX_ETH,KNC_ETH,LINK_ETH,REQ_ETH,RCN_ETH,TRX_ETH,ARN_ETH,KICK_ETH,BNT_ETH,VET_ETH,MCO_ETH,FUN_ETH,DATA_ETH,RLC_ETH,RLC_USDT,ZSC_ETH,WINGS_ETH,MDA_ETH,RCN_USDT,TRX_USDT,KICK_USDT,VET_USDT,MCO_USDT,FUN_USDT,DATA_USDT,ZSC_USDT,MDA_USDT,XTZ_USDT,XTZ_BTC,XTZ_ETH,GNT_USDT,GNT_ETH,GEM_USDT,GEM_ETH,RFR_USDT,RFR_ETH,DADI_USDT,DADI_ETH,ABT_USDT,ABT_ETH,LEDU_BTC,LEDU_ETH,OST_USDT,OST_ETH,XLM_USDT,XLM_ETH,XLM_BTC,MOBI_USDT,MOBI_ETH,MOBI_BTC,OCN_USDT,OCN_ETH,OCN_BTC,ZPT_USDT,ZPT_ETH,ZPT_BTC,COFI_USDT,COFI_ETH,JNT_USDT,JNT_ETH,JNT_BTC,BLZ_USDT,BLZ_ETH,GXS_USDT,GXS_BTC,MTN_USDT,MTN_ETH,RUFF_USDT,RUFF_ETH,RUFF_BTC,TNC_USDT,TNC_ETH,TNC_BTC,ZIL_USDT,ZIL_ETH,BTO_USDT,BTO_ETH,THETA_USDT,THETA_ETH,DDD_USDT,DDD_ETH,DDD_BTC,MKR_USDT,MKR_ETH,DAI_USDT,SMT_USDT,SMT_ETH,MDT_USDT,MDT_ETH,MDT_BTC,MANA_USDT,MANA_ETH,LUN_USDT,LUN_ETH,SALT_USDT,SALT_ETH,FUEL_USDT,FUEL_ETH,ELF_USDT,ELF_ETH,DRGN_USDT,DRGN_ETH,GTC_USDT,GTC_ETH,GTC_BTC,QLC_USDT,QLC_BTC,QLC_ETH,DBC_USDT,DBC_BTC,DBC_ETH,BNTY_USDT,BNTY_ETH,LEND_USDT,LEND_ETH,ICX_USDT,ICX_ETH,BTF_USDT,BTF_BTC,ADA_USDT,ADA_BTC,LSK_USDT,LSK_BTC,WAVES_USDT,WAVES_BTC,BIFI_USDT,BIFI_BTC,MDS_ETH,MDS_USDT,DGD_USDT,DGD_ETH,QASH_USDT,QASH_ETH,QASH_BTC,POWR_USDT,POWR_ETH,POWR_BTC,FIL_USDT,BCD_USDT,BCD_BTC,SBTC_USDT,SBTC_BTC,GOD_USDT,GOD_BTC,BCX_USDT,BCX_BTC,QSP_USDT,QSP_ETH,INK_BTC,INK_USDT,INK_ETH,INK_QTUM,QBT_QTUM,QBT_ETH,QBT_USDT,TSL_QTUM,TSL_USDT,GNX_USDT,GNX_ETH,NEO_USDT,GAS_USDT,NEO_BTC,GAS_BTC,IOTA_USDT,IOTA_BTC,NAS_USDT,NAS_ETH,NAS_BTC,ETH_BTC,ETC_BTC,ETC_ETH,ZEC_BTC,DASH_BTC,LTC_BTC,BCH_BTC,BTG_BTC,QTUM_BTC,QTUM_ETH,XRP_BTC,DOGE_BTC,XMR_BTC,ZRX_BTC,ZRX_ETH,DNT_ETH,DPY_ETH,OAX_BTC,OAX_USDT,OAX_ETH,REP_ETH,LRC_ETH,LRC_BTC,PST_ETH,BCDN_ETH,BCDN_USDT,TNT_ETH,SNT_ETH,SNT_BTC,BTM_ETH,BTM_BTC,SNET_ETH,SNET_USDT,LLT_SNET,OMG_ETH,OMG_BTC,PAY_ETH,PAY_BTC,BAT_ETH,BAT_BTC,CVC_ETH,STORJ_ETH,STORJ_BTC,EOS_ETH,EOS_BTC,BTS_USDT,BTS_BTC,TIPS_ETH,GT_BTC,GT_USDT,ATOM_BTC,ATOM_USDT,XEM_ETH,XEM_USDT,XEM_BTC,BU_USDT,BU_ETH,BU_BTC,BCHSV_USDT,BCHSV_CNYX,BCHSV_BTC,DCR_USDT,DCR_BTC,BCN_USDT,BCN_BTC,XMC_USDT,XMC_BTC,ATP_USDT,ATP_ETH,NAX_ETH,NBOT_ETH,NBOT_USDT,MED_USDT,MED_ETH,GRIN_USDT,GRIN_ETH,GRIN_BTC,BEAM_USDT,BEAM_ETH,BEAM_BTC,VTHO_ETH,BTT_USDT,BTT_ETH,BTT_TRX,TFUEL_ETH,TFUEL_USDT,CELR_ETH,CELR_USDT,CS_ETH,CS_USDT,MAN_ETH,MAN_USDT,REM_ETH,REM_USDT,LYM_ETH,LYM_BTC,LYM_USDT,ONG_ETH,ONG_USDT,ONT_ETH,ONT_USDT,BFT_ETH,BFT_USDT,IHT_ETH,IHT_USDT,SENC_ETH,SENC_USDT,TOMO_ETH,TOMO_USDT,ELEC_ETH,ELEC_USDT,HAV_ETH,HAV_USDT,SWTH_ETH,SWTH_USDT,NKN_ETH,NKN_USDT,SOUL_ETH,SOUL_USDT,LRN_ETH,LRN_USDT,EOSDAC_ETH,EOSDAC_USDT,DOCK_USDT,DOCK_ETH,GSE_USDT,GSE_ETH,RATING_USDT,RATING_ETH,HSC_USDT,HSC_ETH,HIT_USDT,HIT_ETH,DX_USDT,DX_ETH,CNNS_ETH,CNNS_USDT,DREP_ETH,DREP_USDT,MBL_USDT,MBL_ETH,GMAT_USDT,GMAT_ETH,MIX_USDT,MIX_ETH,LAMB_USDT,LAMB_ETH,LEO_USDT,LEO_BTC,WICC_USDT,WICC_ETH,SERO_USDT,SERO_ETH,VIDY_USDT,VIDY_ETH,KGC_USDT,FTM_USDT,FTM_ETH,COS_USDT,CRO_USDT,ALY_USDT,WIN_USDT,MTV_USDT,ONE_USDT,ARPA_USDT,ARPA_ETH,DILI_USDT,ALGO_USDT,PI_USDT,CKB_USDT,CKB_BTC,CKB_ETH,BKC_USDT,BXC_USDT,BXC_ETH,PAX_USDT,PAX_CNYX,USDC_CNYX,USDC_USDT,TUSD_CNYX,TUSD_USDT,HC_USDT,HC_BTC,HC_ETH,GARD_USDT,GARD_ETH,FTI_USDT,FTI_ETH,SOP_ETH,SOP_USDT,LEMO_USDT,LEMO_ETH,QKC_USDT,QKC_ETH,QKC_BTC,IOTX_USDT,IOTX_ETH,RED_USDT,RED_ETH,LBA_USDT,LBA_ETH,OPEN_USDT,OPEN_ETH,MITH_USDT,MITH_ETH,SKM_USDT,SKM_ETH,XVG_USDT,XVG_BTC,NANO_USDT,NANO_BTC,HT_USDT,BNB_USDT,MET_ETH,MET_USDT,TCT_ETH,TCT_USDT,MXC_USDT,MXC_BTC,MXC_ETH" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -849,38 +1417,69 @@ "name": "Gemini", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTCUSD,ETHBTC,ETHUSD,BCHUSD,BCHBTC,BCHETH,LTCUSD,LTCBTC,LTCETH,LTCBCH,ZECUSD,ZECBTC,ZECETH,ZECBCH,ZECLTC", - "enabledPairs": "BTCUSD", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCUSD", + "available": "BTCUSD,ETHBTC,ETHUSD,BCHUSD,BCHBTC,BCHETH,LTCUSD,LTCBTC,LTCETH,LTCBCH,ZECUSD,ZECBTC,ZECETH,ZECBCH,ZECLTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -893,39 +1492,71 @@ "name": "HitBTC", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BCN-BTC,BTC-USD,DASH-BTC,DOGE-BTC,DOGE-USD,EMC-BTC,ETH-BTC,LSK-BTC,LTC-BTC,LTC-USD,NXT-BTC,SBD-BTC,SC-BTC,STEEM-BTC,XDN-BTC,XEM-BTC,XMR-BTC,ARDR-BTC,ZEC-BTC,WAVES-BTC,MAID-BTC,AMP-BTC,DGD-BTC,SNGLS-BTC,1ST-BTC,TRST-BTC,TIME-BTC,GNO-BTC,REP-BTC,XMR-USD,DASH-USD,ETH-USD,NXT-USD,ZRC-BTC,BOS-BTC,DCT-BTC,ANT-BTC,AEON-BTC,GUP-BTC,PLU-BTC,LUN-BTC,EDG-BTC,RLC-BTC,SWT-BTC,TKN-BTC,WINGS-BTC,XAUR-BTC,AE-BTC,PTOY-BTC,ZEC-USD,XEM-USD,BCN-USD,XDN-USD,MAID-USD,ETC-BTC,ETC-USD,PLBT-BTC,BNT-BTC,SNT-ETH,CVC-USD,PAY-ETH,OAX-ETH,OMG-ETH,BQX-ETH,XTZ-BTC,DICE-BTC,PTOY-ETH,1ST-ETH,XAUR-ETH,TIME-ETH,DICE-ETH,SWT-ETH,XMR-ETH,ETC-ETH,DASH-ETH,ZEC-ETH,PLU-ETH,GNO-ETH,XRP-BTC,STRAT-USD,STRAT-BTC,SNC-ETH,ADX-ETH,BET-ETH,EOS-ETH,DENT-ETH,SAN-ETH,EOS-BTC,EOS-USD,XTZ-ETH,XTZ-USD,MYB-ETH,SUR-ETH,IXT-ETH,PLR-ETH,TIX-ETH,PRO-ETH,AVT-ETH,EVX-USD,DLT-BTC,BNT-ETH,BNT-USD,MANA-USD,DNT-BTC,FYP-BTC,OPT-BTC,TNT-ETH,STX-BTC,STX-ETH,STX-USD,TNT-USD,TNT-BTC,ENG-ETH,XUC-USD,SNC-BTC,SNC-USD,OAX-USD,OAX-BTC,ZRX-BTC,ZRX-ETH,ZRX-USD,RVT-BTC,PPC-BTC,PPC-USD,QTUM-ETH,IGNIS-ETH,BMC-BTC,BMC-ETH,BMC-USD,CND-BTC,CND-ETH,CND-USD,CDT-ETH,CDT-USD,FUN-BTC,FUN-ETH,FUN-USD,HVN-BTC,HVN-ETH,POE-BTC,POE-ETH,AMB-USD,AMB-ETH,AMB-BTC,HPC-BTC,PPT-ETH,MTH-BTC,MTH-ETH,LRC-BTC,LRC-ETH,ICX-BTC,ICX-ETH,NEO-BTC,NEO-ETH,NEO-USD,CSNO-BTC,ICX-USD,PIX-BTC,PIX-ETH,IND-ETH,KICK-BTC,YOYOW-BTC,CDT-BTC,XVG-BTC,XVG-ETH,XVG-USD,DGB-BTC,DGB-ETH,DGB-USD,DCN-BTC,DCN-ETH,DCN-USD,VIBE-BTC,ENJ-BTC,ENJ-ETH,ENJ-USD,ZSC-BTC,ZSC-ETH,ZSC-USD,TRX-BTC,TRX-ETH,TRX-USD,ART-BTC,EVX-BTC,EVX-ETH,SUB-BTC,SUB-ETH,SUB-USD,WTC-BTC,BTM-BTC,BTM-ETH,BTM-USD,LIFE-BTC,VIB-BTC,VIB-ETH,VIB-USD,DRT-ETH,STU-USD,OMG-BTC,PAY-BTC,PPT-BTC,SNT-BTC,BTG-BTC,BTG-ETH,BTG-USD,SMART-BTC,SMART-ETH,SMART-USD,XUC-ETH,XUC-BTC,LA-ETH,EDO-BTC,EDO-ETH,EDO-USD,HGT-ETH,IXT-BTC,SCL-BTC,ETP-BTC,ETP-ETH,ETP-USD,DRPU-BTC,NEBL-BTC,NEBL-ETH,ARN-BTC,ARN-ETH,STU-BTC,STU-ETH,GVT-ETH,BTX-BTC,LTC-ETH,BCN-ETH,MAID-ETH,NXT-ETH,STRAT-ETH,XDN-ETH,XEM-ETH,PLR-BTC,SUR-BTC,BQX-BTC,DOGE-ETH,AMM-BTC,AMM-ETH,AMM-USD,DBIX-BTC,PRE-BTC,ZAP-BTC,DOV-BTC,DOV-ETH,DRPU-ETH,XRP-ETH,XRP-USD,HSR-BTC,LEND-BTC,LEND-ETH,SPF-ETH,SBTC-BTC,SBTC-ETH,LOC-BTC,LOC-ETH,LOC-USD,SWFTC-BTC,SWFTC-ETH,SWFTC-USD,STAR-ETH,SBTC-USD,STORM-BTC,DIM-ETH,DIM-USD,DIM-BTC,NGC-BTC,NGC-ETH,NGC-USD,EMC-ETH,EMC-USD,MCO-BTC,MCO-ETH,MCO-USD,MANA-ETH,MANA-BTC,CPAY-ETH,DATA-BTC,DATA-ETH,DATA-USD,UTT-BTC,UTT-ETH,UTT-USD,KMD-BTC,KMD-ETH,KMD-USD,QTUM-USD,QTUM-BTC,SNT-USD,OMG-USD,EKO-BTC,EKO-ETH,ADX-BTC,ADX-USD,LSK-ETH,LSK-USD,PLR-USD,SUR-USD,BQX-USD,DRT-USD,REP-ETH,REP-USD,WAX-BTC,WAX-ETH,WAX-USD,C20-BTC,C20-ETH,IDH-BTC,IDH-ETH,IPL-BTC,COV-BTC,COV-ETH,SENT-BTC,SENT-ETH,SENT-USD,SMT-BTC,SMT-ETH,SMT-USD,CHAT-BTC,CHAT-ETH,CHAT-USD,TRAC-ETH,JNT-ETH,UTK-BTC,UTK-ETH,UTK-USD,GNX-ETH,CHSB-BTC,CHSB-ETH,DAY-BTC,DAY-ETH,DAY-USD,NEU-BTC,NEU-ETH,NEU-USD,TAU-BTC,FLP-BTC,FLP-ETH,FLP-USD,R-BTC,R-ETH,EKO-USD,BCPT-ETH,BCPT-USD,PKT-BTC,PKT-ETH,BETR-BTC,BETR-ETH,HAND-ETH,HAND-USD,CHP-ETH,BCPT-BTC,ACT-BTC,ACT-ETH,ACT-USD,ADA-BTC,ADA-ETH,ADA-USD,MTX-BTC,MTX-ETH,MTX-USD,WIZ-BTC,WIZ-ETH,WIZ-USD,DADI-BTC,DADI-ETH,BDG-ETH,DATX-BTC,DATX-ETH,TRUE-BTC,DRG-BTC,DRG-ETH,BANCA-BTC,BANCA-ETH,ZAP-ETH,ZAP-USD,AUTO-BTC,NOAH-BTC,SOC-BTC,OCN-BTC,OCN-ETH,STQ-BTC,STQ-ETH,XLM-BTC,XLM-ETH,XLM-USD,IOTA-BTC,IOTA-ETH,IOTA-USD,DRT-BTC,BETR-USD,ERT-BTC,CRPT-BTC,CRPT-USD,MESH-BTC,MESH-ETH,MESH-USD,IHT-BTC,IHT-ETH,IHT-USD,SCC-BTC,YCC-BTC,DAN-BTC,TEL-BTC,TEL-ETH,NCT-BTC,NCT-ETH,NCT-USD,BMH-BTC,BANCA-USD,NOAH-ETH,NOAH-USD,BERRY-BTC,BERRY-ETH,BERRY-USD,GBX-BTC,GBX-ETH,GBX-USD,SHIP-BTC,SHIP-ETH,NANO-BTC,NANO-ETH,NANO-USD,LNC-BTC,KIN-ETH,ARDR-USD,FOTA-ETH,FOTA-BTC,CVT-BTC,CVT-ETH,CVT-USD,STQ-USD,GNT-BTC,GNT-ETH,GNT-USD,GET-BTC,MITH-BTC,MITH-ETH,MITH-USD,SUNC-ETH,DADI-USD,TKY-BTC,ACAT-BTC,ACAT-ETH,ACAT-USD,BTX-USD,WIKI-BTC,WIKI-ETH,WIKI-USD,ONT-BTC,ONT-ETH,ONT-USD,FTX-BTC,FTX-ETH,FREC-BTC,NAVI-BTC,FREC-ETH,FREC-USD,VME-ETH,NAVI-ETH,LND-ETH,CSM-BTC,NANJ-BTC,NTK-BTC,NTK-ETH,NTK-USD,AUC-BTC,AUC-ETH,CMCT-BTC,CMCT-ETH,CMCT-USD,MAN-BTC,MAN-ETH,MAN-USD,PNT-BTC,PNT-ETH,FXT-BTC,NEXO-BTC,PAT-BTC,PAT-ETH,XMC-BTC,FXT-ETH,HERO-BTC,HERO-ETH,XMC-ETH,XMC-USD,FDZ-BTC,FDZ-ETH,FDZ-USD,SPD-BTC,SPD-ETH,MITX-BTC,TIV-BTC,B2G-BTC,B2G-USD,ZPT-BTC,ZPT-ETH,HBZ-BTC,FACE-BTC,FACE-ETH,HBZ-ETH,HBZ-USD,ZPT-USD,CPT-BTC,PAT-USD,HTML-BTC,HTML-ETH,MITX-ETH,JOT-BTC,JBC-BTC,JBC-ETH,BTS-BTC,BNK-BTC,KBC-BTC,KBC-ETH,BNK-ETH,BNK-USD,TIV-ETH,TIV-USD,CSM-ETH,CSM-USD,INK-BTC,IOST-BTC,INK-ETH,INK-USD,CBC-BTC,IOST-USD,ZIL-BTC,ABYSS-BTC,ABYSS-ETH,ZIL-USD,BCI-BTC,CBC-ETH,CBC-USD,PITCH-BTC,PITCH-ETH,HTML-USD,TDS-BTC,TDS-ETH,TDS-USD,SBD-ETH,SBD-USD,DPN-BTC,UUU-BTC,UUU-ETH,XBP-BTC,CLN-BTC,CLN-ETH,ELEC-BTC,ELEC-ETH,ELEC-USD,QNTU-BTC,QNTU-ETH,QNTU-USD,IPL-ETH,IPL-USD,CENNZ-BTC,CENNZ-ETH,SWM-BTC,SPF-USD,SPF-BTC,LCC-BTC,HGT-BTC,ETH-TUSD,BTC-TUSD,LTC-TUSD,XMR-TUSD,ZRX-TUSD,NEO-TUSD,USD-TUSD,BTC-DAI,ETH-DAI,MKR-DAI,EOS-DAI,USD-DAI,MKR-BTC,MKR-ETH,MKR-USD,TUSD-DAI,NEO-DAI,LTC-DAI,XMR-DAI,XRP-DAI,NEXO-ETH,NEXO-USD,DWS-BTC,DWS-ETH,DWS-USD,APPC-BTC,APPC-ETH,APPC-USD,BIT-ETH,SPC-BTC,SPC-ETH,SPC-USD,REX-BTC,REX-ETH,REX-USD,ELF-BTC,ELF-USD,BCD-BTC,BCD-USD,CVCOIN-BTC,CVCOIN-ETH,CVCOIN-USD,EDG-ETH,EDG-USD,NLC2-BTC,COSM-BTC,COSM-ETH,DASH-EURS,ZEC-EURS,BTC-EURS,EOS-EURS,ETH-EURS,LTC-EURS,NEO-EURS,XMR-EURS,XRP-EURS,EURS-USD,EURS-TUSD,EURS-DAI,MNX-USD,ROX-ETH,ZPR-ETH,MNX-BTC,MNX-ETH,KIND-BTC,KIND-ETH,ENGT-BTC,ENGT-ETH,PMA-BTC,PMA-ETH,TV-BTC,TV-ETH,TV-USD,XCLR-BTC,BAT-BTC,BAT-ETH,BAT-USD,SRN-BTC,SRN-ETH,SRN-USD,SVD-BTC,SVD-ETH,SVD-USD,GST-BTC,GST-ETH,GST-USD,BNB-BTC,BNB-ETH,BNB-USD,DIT-BTC,DIT-ETH,POA20-BTC,CCL-USD,PROC-BTC,POA20-ETH,POA20-USD,POA20-DAI,NIM-BTC,USE-BTC,USE-ETH,DAV-BTC,DAV-ETH,ABTC-BTC,NIM-ETH,ABA-BTC,ABA-ETH,ABA-USD,BCN-EOS,LTC-EOS,XMR-EOS,DASH-EOS,TRX-EOS,NEO-EOS,ZEC-EOS,LSK-EOS,XEM-EOS,XRP-EOS,MESSE-BTC,MESSE-ETH,MESSE-USD,CCL-ETH,RCN-BTC,RCN-ETH,RCN-USD,HMQ-BTC,HMQ-ETH,MYST-BTC,MYST-ETH,USD-GUSD,BTC-GUSD,ETH-GUSD,EOS-GUSD,AXPR-BTC,AXPR-ETH,DAG-BTC,DAG-ETH,BITS-BTC,BITS-ETH,BITS-USD,CDCC-BTC,CDCC-ETH,CDCC-USD,VET-BTC,VET-ETH,VET-USD,SILK-ETH,BOX-BTC,BOX-ETH,BOX-EURS,BOX-EOS,VOCO-BTC,VOCO-ETH,VOCO-USD,PASS-BTC,PASS-ETH,SLX-BTC,SLX-USD,PBTT-BTC,PMA-USD,TRAD-BTC,DGTX-BTC,DGTX-ETH,DGTX-USD,MRK-BTC,MRK-ETH,DGB-TUSD,MESSE-EOS,MESSE-EURS,SNBL-BTC,BCH-BTC,BCH-USD,BSV-BTC,BSV-USD,BKX-BTC,NPLC-BTC,NPLC-ETH,ETN-BTC,ETN-ETH,ETN-USD,MRS-BTC,MRS-ETH,MRS-USD,DTR-BTC,DTR-ETH,TDP-BTC,HBT-ETH,PXG-BTC,PXG-USD,BTC-PAX,ETH-PAX,USD-PAX,BTC-USDC,ETH-USDC,USD-USDC,TUSD-USDC,DAI-USDC,EOS-PAX,CLO-BTC,CLO-ETH,CLO-USD,PETH-BTC,PETH-ETH,PETH-USD,BRD-BTC,BRD-ETH,NMR-BTC,SALT-BTC,SALT-ETH,POLY-BTC,POLY-ETH,POWR-BTC,POWR-ETH,STORJ-BTC,STORJ-ETH,STORJ-USD,MLN-BTC,MLN-ETH,BDG-BTC,POA-ETH,POA-BTC,POA-USD,POA-DAI,KIN-BTC,VEO-BTC,PLA-BTC,PLA-ETH,PLA-USD,BTT-BTC,BTT-USD,BTT-ETH,ZEN-BTC,ZEN-ETH,ZEN-USD,GRIN-BTC,GRIN-ETH,GRIN-USD,FET-BTC,HT-BTC,HT-USD,XZC-BTC,XZC-ETH,XZC-USD,VRA-BTC,VRA-ETH,BTC-KRWB,USD-KRWB,WBTC-ETH,CRO-BTC,CRO-ETH,CRO-USD,GAS-BTC,GAS-ETH,GAS-USD,ORMEUS-BTC,ORMEUS-ETH,SWM-ETH,SWM-USD,PRE-ETH,PHX-BTC,PHX-ETH,PHX-USD,BET-BTC,USD-EOSDT,BTC-EOSDT,ETH-EOSDT,EOS-EOSDT,DAI-EOSDT,NUT-BTC,NUT-EOS,NUT-USD,CUTE-BTC,CUTE-ETH,CUTE-USD,CUTE-EOS,XCON-BTC,DCR-BTC,DCR-ETH,DCR-USD,MG-BTC,MG-ETH,MG-EOS,MG-USD,GNX-BTC,PRO-BTC,EURS-EOSDT,TUSD-EOSDT,ECOIN-BTC,ECOIN-ETH,ECOIN-USD,AGI-BTC,LOOM-BTC,LOOM-ETH,BLZ-BTC,QKC-BTC,QKC-ETH,KNC-BTC,KNC-ETH,KNC-USD,KEY-BTC,KEY-ETH,ATOM-BTC,ATOM-USD,ATOM-ETH,BRDG-BTC,BRDG-ETH,BRDG-USD,MTL-BTC,MTL-ETH,EXP-BTC,BTCB-BTC,PBT-BTC,PBT-ETH,LINK-BTC,LINK-ETH,LINK-USD,USD-USDT20,PHB-BTC,BCH-ETH,BCH-DAI,BCH-TUSD,BCH-EURS,DAPP-BTC,DAPP-EOS,BTC-USDT20,DENT-BTC,DENT-USD", - "enabledPairs": "BTC-USD", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USD", + "available": "BCN-BTC,BTC-USD,DASH-BTC,DOGE-BTC,DOGE-USD,EMC-BTC,ETH-BTC,LSK-BTC,LTC-BTC,LTC-USD,NXT-BTC,SBD-BTC,SC-BTC,STEEM-BTC,XDN-BTC,XEM-BTC,XMR-BTC,ARDR-BTC,ZEC-BTC,WAVES-BTC,MAID-BTC,DGD-BTC,SNGLS-BTC,1ST-BTC,TRST-BTC,TIME-BTC,GNO-BTC,REP-BTC,XMR-USD,DASH-USD,ETH-USD,NXT-USD,ZRC-BTC,BOS-BTC,DCT-BTC,ANT-BTC,AEON-BTC,GUP-BTC,PLU-BTC,LUN-BTC,EDG-BTC,RLC-BTC,SWT-BTC,TKN-BTC,WINGS-BTC,XAUR-BTC,AE-BTC,PTOY-BTC,ZEC-USD,XEM-USD,BCN-USD,XDN-USD,MAID-USD,ETC-BTC,ETC-USD,PLBT-BTC,BNT-BTC,SNT-ETH,CVC-USD,PAY-ETH,OAX-ETH,OMG-ETH,BQX-ETH,XTZ-BTC,DICE-BTC,PTOY-ETH,1ST-ETH,XAUR-ETH,TIME-ETH,DICE-ETH,SWT-ETH,XMR-ETH,ETC-ETH,DASH-ETH,ZEC-ETH,PLU-ETH,GNO-ETH,XRP-BTC,STRAT-USD,STRAT-BTC,SNC-ETH,ADX-ETH,BET-ETH,EOS-ETH,DENT-ETH,SAN-ETH,EOS-BTC,EOS-USD,XTZ-ETH,XTZ-USD,MYB-ETH,SUR-ETH,IXT-ETH,PLR-ETH,TIX-ETH,PRO-ETH,AVT-ETH,EVX-USD,DLT-BTC,BNT-ETH,BNT-USD,MANA-USD,DNT-BTC,FYP-BTC,OPT-BTC,TNT-ETH,STX-BTC,STX-ETH,STX-USD,TNT-USD,TNT-BTC,ENG-ETH,XUC-USD,SNC-BTC,SNC-USD,OAX-USD,OAX-BTC,ZRX-BTC,ZRX-ETH,ZRX-USD,RVT-BTC,PPC-BTC,PPC-USD,QTUM-ETH,IGNIS-ETH,BMC-BTC,BMC-ETH,BMC-USD,CND-BTC,CND-ETH,CND-USD,CDT-ETH,CDT-USD,FUN-BTC,FUN-ETH,FUN-USD,HVN-BTC,HVN-ETH,POE-BTC,POE-ETH,AMB-USD,AMB-ETH,AMB-BTC,HPC-BTC,PPT-ETH,MTH-BTC,MTH-ETH,LRC-BTC,LRC-ETH,ICX-BTC,ICX-ETH,NEO-BTC,NEO-ETH,NEO-USD,CSNO-BTC,ICX-USD,IND-ETH,KICK-BTC,YOYOW-BTC,CDT-BTC,XVG-BTC,XVG-ETH,XVG-USD,DGB-BTC,DGB-ETH,DGB-USD,DCN-ETH,DCN-USD,VIBE-BTC,ENJ-BTC,ENJ-ETH,ENJ-USD,ZSC-BTC,ZSC-ETH,ZSC-USD,TRX-BTC,TRX-ETH,TRX-USD,ART-BTC,EVX-BTC,EVX-ETH,SUB-BTC,SUB-ETH,SUB-USD,WTC-BTC,BTM-BTC,BTM-ETH,BTM-USD,LIFE-BTC,VIB-BTC,VIB-ETH,VIB-USD,DRT-ETH,STU-USD,OMG-BTC,PAY-BTC,PPT-BTC,SNT-BTC,BTG-BTC,BTG-ETH,BTG-USD,SMART-BTC,SMART-ETH,SMART-USD,XUC-ETH,XUC-BTC,LA-ETH,EDO-BTC,EDO-ETH,EDO-USD,HGT-ETH,IXT-BTC,SCL-BTC,ETP-BTC,ETP-ETH,ETP-USD,NEBL-BTC,NEBL-ETH,ARN-BTC,ARN-ETH,STU-BTC,STU-ETH,GVT-ETH,BTX-BTC,LTC-ETH,BCN-ETH,MAID-ETH,NXT-ETH,STRAT-ETH,XDN-ETH,XEM-ETH,PLR-BTC,SUR-BTC,BQX-BTC,DOGE-ETH,AMM-BTC,AMM-ETH,AMM-USD,DBIX-BTC,PRE-BTC,ZAP-BTC,DOV-BTC,DOV-ETH,XRP-ETH,XRP-USD,HSR-BTC,LEND-BTC,LEND-ETH,SPF-ETH,SBTC-BTC,SBTC-ETH,LOC-BTC,LOC-ETH,LOC-USD,SWFTC-BTC,SWFTC-ETH,SWFTC-USD,STAR-ETH,SBTC-USD,STORM-BTC,DIM-ETH,DIM-USD,DIM-BTC,NGC-BTC,NGC-ETH,NGC-USD,EMC-ETH,EMC-USD,MCO-BTC,MCO-ETH,MCO-USD,MANA-ETH,MANA-BTC,CPAY-ETH,DATA-BTC,DATA-ETH,DATA-USD,UTT-BTC,UTT-ETH,UTT-USD,KMD-BTC,KMD-ETH,KMD-USD,QTUM-USD,QTUM-BTC,SNT-USD,OMG-USD,EKO-BTC,EKO-ETH,ADX-BTC,ADX-USD,LSK-ETH,LSK-USD,PLR-USD,SUR-USD,BQX-USD,DRT-USD,REP-ETH,REP-USD,WAXP-BTC,WAXP-ETH,WAXP-USD,C20-BTC,C20-ETH,IDH-BTC,IDH-ETH,IPL-BTC,COV-BTC,COV-ETH,SENT-BTC,SENT-ETH,SENT-USD,SMT-BTC,SMT-ETH,SMT-USD,CHAT-BTC,CHAT-ETH,CHAT-USD,TRAC-ETH,JNT-ETH,UTK-BTC,UTK-ETH,UTK-USD,GNX-ETH,CHSB-BTC,CHSB-ETH,DAY-BTC,DAY-ETH,DAY-USD,NEU-BTC,NEU-ETH,NEU-USD,TAU-BTC,FLP-BTC,FLP-ETH,FLP-USD,R-BTC,R-ETH,EKO-USD,BCPT-ETH,BCPT-USD,PKT-BTC,PKT-ETH,BETR-BTC,BETR-ETH,HAND-ETH,HAND-USD,CHP-ETH,BCPT-BTC,ACT-BTC,ACT-ETH,ACT-USD,ADA-BTC,ADA-ETH,ADA-USD,SIG-BTC,MTX-BTC,MTX-ETH,MTX-USD,WIZ-BTC,WIZ-ETH,WIZ-USD,DADI-BTC,DADI-ETH,BDG-ETH,DATX-BTC,DATX-ETH,TRUE-BTC,DRG-BTC,DRG-ETH,BANCA-BTC,BANCA-ETH,ZAP-ETH,ZAP-USD,AUTO-BTC,SOC-BTC,OCN-BTC,OCN-ETH,STQ-BTC,STQ-ETH,XLM-BTC,XLM-ETH,XLM-USD,IOTA-BTC,IOTA-ETH,IOTA-USD,DRT-BTC,BETR-USD,ERT-BTC,CRPT-BTC,CRPT-USD,MESH-BTC,MESH-ETH,MESH-USD,IHT-BTC,IHT-ETH,IHT-USD,SCC-BTC,YCC-BTC,DAN-BTC,TEL-BTC,TEL-ETH,NCT-BTC,NCT-ETH,NCT-USD,BMH-BTC,BANCA-USD,BERRY-BTC,BERRY-ETH,BERRY-USD,GBX-BTC,GBX-ETH,GBX-USD,SHIP-BTC,SHIP-ETH,NANO-BTC,NANO-ETH,NANO-USD,LNC-BTC,KIN-ETH,ARDR-USD,FOTA-ETH,FOTA-BTC,CVT-BTC,CVT-ETH,CVT-USD,STQ-USD,GNT-BTC,GNT-ETH,GNT-USD,GET-BTC,MITH-BTC,MITH-ETH,MITH-USD,DADI-USD,TKY-BTC,ACAT-BTC,ACAT-ETH,ACAT-USD,BTX-USD,WIKI-BTC,WIKI-ETH,WIKI-USD,ONT-BTC,ONT-ETH,ONT-USD,FTX-BTC,FTX-ETH,NAVI-BTC,VME-ETH,NAVI-ETH,LND-ETH,CSM-BTC,NANJ-BTC,NTK-BTC,NTK-ETH,NTK-USD,AUC-BTC,AUC-ETH,CMCT-BTC,CMCT-ETH,CMCT-USD,MAN-BTC,MAN-ETH,MAN-USD,PNT-BTC,PNT-ETH,FXT-BTC,NEXO-BTC,PAT-BTC,PAT-ETH,XMC-BTC,FXT-ETH,HERO-BTC,HERO-ETH,XMC-ETH,XMC-USD,FDZ-BTC,FDZ-ETH,FDZ-USD,SPD-BTC,SPD-ETH,MITX-BTC,TIV-BTC,B2G-BTC,B2G-USD,HBZ-BTC,FACE-BTC,FACE-ETH,HBZ-ETH,HBZ-USD,CPT-BTC,PAT-USD,HTML-BTC,HTML-ETH,MITX-ETH,BTS-BTC,BNK-BTC,BNK-ETH,BNK-USD,TIV-ETH,TIV-USD,CSM-ETH,CSM-USD,INK-BTC,IOST-BTC,INK-ETH,INK-USD,CBC-BTC,IOST-USD,ZIL-BTC,ABYSS-BTC,ABYSS-ETH,ZIL-USD,BCI-BTC,CBC-ETH,CBC-USD,PITCH-BTC,PITCH-ETH,HTML-USD,TDS-BTC,TDS-ETH,TDS-USD,SBD-ETH,SBD-USD,DPN-BTC,UUU-BTC,UUU-ETH,XBP-BTC,ELEC-BTC,ELEC-ETH,ELEC-USD,QNTU-BTC,QNTU-ETH,QNTU-USD,IPL-ETH,IPL-USD,CENNZ-BTC,CENNZ-ETH,SWM-BTC,SPF-USD,SPF-BTC,LCC-BTC,HGT-BTC,ETH-TUSD,BTC-TUSD,LTC-TUSD,XMR-TUSD,ZRX-TUSD,NEO-TUSD,USD-TUSD,BTC-DAI,ETH-DAI,MKR-DAI,EOS-DAI,USD-DAI,MKR-BTC,MKR-ETH,MKR-USD,TUSD-DAI,NEO-DAI,LTC-DAI,XMR-DAI,XRP-DAI,NEXO-ETH,NEXO-USD,DWS-BTC,DWS-ETH,DWS-USD,APPC-BTC,APPC-ETH,APPC-USD,BIT-ETH,SPC-BTC,SPC-ETH,SPC-USD,REX-BTC,REX-ETH,REX-USD,ELF-BTC,ELF-USD,BCD-BTC,BCD-USD,CVCOIN-BTC,CVCOIN-ETH,CVCOIN-USD,EDG-ETH,EDG-USD,NLC2-BTC,DASH-EURS,ZEC-EURS,BTC-EURS,EOS-EURS,ETH-EURS,LTC-EURS,NEO-EURS,XMR-EURS,XRP-EURS,EURS-USD,EURS-TUSD,EURS-DAI,MNX-USD,ROX-ETH,ZPR-ETH,MNX-BTC,MNX-ETH,KIND-BTC,KIND-ETH,ENGT-BTC,ENGT-ETH,PMA-BTC,PMA-ETH,TV-BTC,TV-ETH,TV-USD,BAT-BTC,BAT-ETH,BAT-USD,SRN-BTC,SRN-ETH,SRN-USD,SVD-BTC,SVD-ETH,SVD-USD,GST-BTC,GST-ETH,GST-USD,BNB-BTC,BNB-ETH,BNB-USD,DIT-BTC,DIT-ETH,POA20-BTC,PROC-BTC,POA20-ETH,POA20-USD,POA20-DAI,NIM-BTC,USE-BTC,USE-ETH,DAV-BTC,DAV-ETH,ABTC-BTC,NIM-ETH,ABA-BTC,ABA-ETH,ABA-USD,BCN-EOS,LTC-EOS,XMR-EOS,DASH-EOS,TRX-EOS,NEO-EOS,ZEC-EOS,LSK-EOS,XEM-EOS,XRP-EOS,RCN-BTC,RCN-ETH,RCN-USD,HMQ-BTC,HMQ-ETH,MYST-BTC,MYST-ETH,USD-GUSD,BTC-GUSD,ETH-GUSD,EOS-GUSD,AXPR-BTC,AXPR-ETH,DAG-BTC,DAG-ETH,BITS-BTC,BITS-ETH,BITS-USD,CDCC-BTC,CDCC-ETH,CDCC-USD,VET-BTC,VET-ETH,VET-USD,SILK-ETH,BOX-BTC,BOX-ETH,BOX-EURS,BOX-EOS,VOCO-BTC,VOCO-ETH,VOCO-USD,PASS-BTC,PASS-ETH,SLX-BTC,SLX-USD,PBTT-BTC,PMA-USD,TRAD-BTC,DGTX-BTC,DGTX-ETH,DGTX-USD,MRK-BTC,MRK-ETH,DGB-TUSD,SNBL-BTC,BCH-BTC,BCH-USD,BSV-BTC,BSV-USD,BKX-BTC,NPLC-BTC,NPLC-ETH,ETN-BTC,ETN-ETH,ETN-USD,DTR-BTC,DTR-ETH,TDP-BTC,HBT-ETH,PXG-BTC,PXG-USD,BTC-PAX,ETH-PAX,USD-PAX,BTC-USDC,ETH-USDC,USD-USDC,TUSD-USDC,DAI-USDC,EOS-PAX,CLO-BTC,CLO-ETH,CLO-USD,PETH-BTC,PETH-ETH,PETH-USD,BRD-BTC,BRD-ETH,NMR-BTC,SALT-BTC,SALT-ETH,POLY-BTC,POLY-ETH,POWR-BTC,POWR-ETH,STORJ-BTC,STORJ-ETH,STORJ-USD,MLN-BTC,MLN-ETH,BDG-BTC,POA-ETH,POA-BTC,POA-USD,POA-DAI,KIN-BTC,VEO-BTC,PLA-BTC,PLA-ETH,PLA-USD,BTT-BTC,BTT-USD,BTT-ETH,ZEN-BTC,ZEN-ETH,ZEN-USD,GRIN-BTC,GRIN-ETH,GRIN-USD,FET-BTC,HT-BTC,HT-USD,XZC-BTC,XZC-ETH,XZC-USD,VRA-BTC,VRA-ETH,BTC-KRWB,USD-KRWB,WBTC-ETH,CRO-BTC,CRO-ETH,CRO-USD,GAS-BTC,GAS-ETH,GAS-USD,ORMEUS-BTC,ORMEUS-ETH,SWM-ETH,SWM-USD,PRE-ETH,PHX-BTC,PHX-ETH,PHX-USD,BET-BTC,USD-EOSDT,BTC-EOSDT,ETH-EOSDT,EOS-EOSDT,DAI-EOSDT,NUT-BTC,NUT-EOS,NUT-USD,CUTE-BTC,CUTE-ETH,CUTE-USD,CUTE-EOS,XCON-BTC,DCR-BTC,DCR-ETH,DCR-USD,MG-BTC,MG-ETH,MG-EOS,MG-USD,GNX-BTC,PRO-BTC,EURS-EOSDT,TUSD-EOSDT,ECOIN-BTC,ECOIN-ETH,ECOIN-USD,AGI-BTC,LOOM-BTC,LOOM-ETH,BLZ-BTC,QKC-BTC,QKC-ETH,KNC-BTC,KNC-ETH,KNC-USD,KEY-BTC,KEY-ETH,ATOM-BTC,ATOM-USD,ATOM-ETH,BRDG-BTC,BRDG-ETH,BRDG-USD,MTL-BTC,MTL-ETH,EXP-BTC,BTCB-BTC,PBT-BTC,PBT-ETH,LINK-BTC,LINK-ETH,LINK-USD,USD-USDT20,PHB-BTC,BCH-ETH,BCH-DAI,BCH-TUSD,BCH-EURS,DAPP-BTC,DAPP-EOS,BTC-USDT20,DENT-BTC,DENT-USD,NJBC-BTC,NJBC-ETH,XRC-BTC,EOS-BCH,LTC-BCH,XRP-BCH,TRX-BCH,XLM-BCH,ETC-BCH,DASH-BCH,ZEC-BCH,BKX-USD,LAMB-BTC,NPXS-BTC,HBAR-BTC,HBAR-USD,ONE-BTC,RFR-BTC,RFR-USD,BUSD-USD,PAXG-BTC,PAXG-USD,REN-BTC,IGNIS-BTC,CEL-BTC,CEL-ETH,WIN-USD,ADK-BTC,PART-BTC,SOZ-BTC,SOZ-ETH,SOZ-USD,WAVES-USD,ADA-BCH,ONT-BCH,XMR-BCH,ATOM-BCH,LINK-BCH,OMG-BCH,WAVES-BCH,IOTX-BTC,HOT-BTC,SLV-BTC,HEDG-BTC,CHZ-BTC,CHZ-USD,COCOS-BTC,COCOS-USD,SEELE-BTC,SEELE-USD,MDA-BTC,LEO-USD,REM-BTC,REM-ETH,REM-USD,SCD-DAI,BTC-BUSD,RVN-BTC,BST-BTC,ERD-BTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -938,40 +1569,72 @@ "name": "Huobi", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiAuthPemKey": "-----BEGIN EC PRIVATE KEY-----\nJUSTADUMMY\n-----END EC PRIVATE KEY-----\n", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "HT-USDT,BAT-ETH,AST-ETH,TRX-BTC,NEW-BTC,AE-BTC,IIC-BTC,NEW-USDT,CDC-BTC,AE-USDT,DGB-BTC,NAS-ETH,QSP-BTC,LYM-ETH,YCC-BTC,BCH-HT,BIX-ETH,WXT-BTC,XRP-BTC,IOST-BTC,CHAT-BTC,BTC-USDT,XTZ-BTC,PVT-BTC,PVT-USDT,WAVES-ETH,ACT-BTC,RSR-BTC,ACT-USDT,WXT-USDT,XLM-ETH,HT-BTC,UUU-USDT,XRP-USDT,UGAS-BTC,BTS-ETH,IRIS-ETH,LUN-BTC,IOST-HT,DOCK-BTC,ABT-ETH,CRO-BTC,MAN-ETH,ENG-ETH,QUN-BTC,APPC-BTC,KAN-ETH,VET-USDT,SOC-ETH,RSR-HT,RUFF-ETH,RCCC-ETH,AAC-ETH,MCO-BTC,RSR-USDT,TNB-ETH,UTK-ETH,ADX-BTC,WAX-ETH,IOST-USDT,HOT-ETH,WTC-USDT,CVCOIN-BTC,NCASH-ETH,ATP-BTC,SWFTC-ETH,GTC-BTC,PNT-BTC,GT-HT,NEO-BTC,OMG-BTC,EOS-HUSD,WPR-ETH,ARPA-BTC,BTM-BTC,BTM-USDT,KCASH-ETH,SSP-ETH,ARPA-USDT,CNN-BTC,NKN-BTC,NPXS-BTC,OMG-USDT,TOPC-ETH,XEM-BTC,BCH-USDT,SNC-BTC,POLY-ETH,CMT-ETH,PAI-USDT,ZEC-USDT,LSK-ETH,SMT-ETH,DASH-USDT,GAS-ETH,DASH-BTC,GXC-ETH,FTT-HT,IOTA-ETH,FTI-BTC,TRIO-ETH,LET-BTC,ZRX-ETH,ETN-ETH,EVX-ETH,BFT-ETH,GRS-BTC,XRP-HT,DASH-HT,QTUM-ETH,HIT-ETH,NEXO-BTC,QASH-BTC,EOS-ETH,ARDR-ETH,ADA-BTC,NEO-USDT,BTT-TRX,COVA-ETH,REN-BTC,LOOM-BTC,CVC-ETH,NANO-ETH,ARPA-HT,NEW-HT,BLZ-ETH,LINK-ETH,XTZ-USDT,PAY-BTC,GNT-USDT,YEE-ETH,XZC-ETH,EGCC-ETH,PROPY-ETH,ZEC-BTC,EDU-ETH,RTE-BTC,DCR-USDT,FTT-BTC,DCR-BTC,EKO-BTC,SBTC-BTC,ZLA-ETH,TOP-HT,ALGO-BTC,DTA-ETH,EKT-ETH,ATOM-USDT,LXT-USDT,ZEN-ETH,LOL-USDT,LTC-USDT,DAT-BTC,REQ-ETH,ELA-ETH,NKN-HT,PC-BTC,HIT-BTC,EKO-ETH,STK-ETH,LAMB-USDT,LAMB-HT,DOGE-ETH,ATOM-BTC,THETA-USDT,LOL-BTC,THETA-BTC,LSK-BTC,ADA-USDT,RDN-BTC,OGO-HT,UIP-USDT,WICC-BTC,OCN-BTC,ELF-BTC,AKRO-USDT,USDC-HUSD,LAMB-BTC,DBC-ETH,BTT-ETH,FAIR-BTC,POWR-ETH,MUSK-ETH,MT-BTC,STEEM-USDT,RBTC-BTC,CTXC-BTC,MANA-USDT,ICX-ETH,GET-BTC,LTC-BTC,ITC-ETH,BCV-BTC,ZJLT-BTC,AKRO-HT,TNT-ETH,TOP-BTC,MEX-BTC,DATX-BTC,ALGO-USDT,LXT-BTC,GT-USDT,FSN-HT,FSN-USDT,MTX-ETH,LET-ETH,OGO-USDT,PHX-BTC,KCASH-HT,HC-USDT,LOL-HT,NKN-USDT,HOT-BTC,LBA-BTC,XMX-BTC,OST-ETH,VEN-USDT,LTC-HT,LBA-USDT,VEN-BTC,CRE-HT,BIFI-BTC,BT1-BTC,HPT-BTC,NULS-BTC,WAN-BTC,ZIL-BTC,ETC-HT,TOS-BTC,MANA-BTC,SHE-BTC,GT-BTC,FSN-BTC,MCO-ETH,MTN-BTC,MDS-BTC,SRN-ETH,GVE-BTC,XMR-ETH,MEET-ETH,NULS-USDT,BCH-BTC,PAI-BTC,NCC-ETH,BSV-BTC,AKRO-BTC,ELF-USDT,DGD-ETH,PVT-HT,UIP-BTC,ATP-USDT,SEELE-ETH,GSC-BTC,ETC-USDT,SOC-BTC,GNX-BTC,WICC-USDT,QSP-ETH,RUFF-BTC,KNC-ETH,ATP-HT,CTXC-USDT,KMD-ETH,OGO-BTC,BKBT-BTC,DGB-ETH,WAVES-USDT,BCD-BTC,HPT-HT,ZIL-USDT,BUT-ETH,CVNT-BTC,OCN-USDT,SALT-ETH,XLM-BTC,TRX-USDT,RCN-BTC,DAC-ETH,MT-HT,ETH-HUSD,HPT-USDT,XTZ-ETH,USDT-HUSD,CHAT-ETH,ONT-USDT,SKM-USDT,MAN-BTC,ARDR-BTC,BCX-BTC,SKM-BTC,EOS-USDT,GNX-ETH,CRE-USDT,PORTAL-ETH,COVA-BTC,BIX-BTC,UUU-ETH,AAC-BTC,TRX-ETH,NEXO-ETH,NAS-BTC,ENG-BTC,AST-BTC,TT-HT,QUN-ETH,EOS-BTC,18C-ETH,WTC-ETH,CVCOIN-ETH,CRE-BTC,CNNS-USDT,WAX-BTC,AIDOC-BTC,VET-ETH,CMT-USDT,BSV-USDT,IDT-ETH,IOST-ETH,BTC-HUSD,IOTA-BTC,TNB-BTC,LINK-BTC,TOPC-BTC,RCCC-BTC,ZRX-USDT,CNNS-BTC,BOX-BTC,MDS-USDT,XLM-USDT,BAT-BTC,LYM-BTC,UC-ETH,RUFF-USDT,LUN-ETH,BIX-USDT,CDC-ETH,BTS-USDT,YCC-ETH,KAN-USDT,MTL-BTC,WAVES-BTC,ONT-BTC,HT-HUSD,IRIS-USDT,SOC-USDT,WPR-BTC,ETC-BTC,TUSD-HUSD,CVC-USDT,PROPY-BTC,TRIO-BTC,CVC-BTC,BTT-USDT,NANO-BTC,GXC-BTC,NCASH-BTC,XRP-HUSD,TT-USDT,SHE-ETH,NANO-USDT,LOOM-ETH,POWR-BTC,QTUM-BTC,SSP-BTC,BTM-ETH,QTUM-USDT,XZC-BTC,GNT-ETH,OMG-ETH,NPXS-ETH,SNT-USDT,ETH-USDT,ABT-BTC,BTS-BTC,STEEM-BTC,VSYS-USDT,BLZ-BTC,CNNS-HT,ADX-ETH,SMT-USDT,IOTA-USDT,PAY-ETH,CMT-BTC,UTK-BTC,SWFTC-BTC,GTC-ETH,LINK-USDT,SNC-ETH,SNT-BTC,EOS-HT,REN-ETH,PAX-HUSD,KCASH-BTC,HC-BTC,IIC-ETH,QASH-ETH,GRS-ETH,EDU-BTC,HIT-USDT,TOP-USDT,XZC-USDT,KAN-BTC,SC-BTC,SKM-HT,AE-ETH,STORJ-USDT,XVG-ETH,ZRX-BTC,EVX-BTC,ETN-BTC,BFT-BTC,FTI-ETH,DAT-ETH,UGAS-ETH,BAT-USDT,GXC-USDT,GAS-BTC,TNT-BTC,HB10-USDT,MUSK-BTC,FTT-USDT,STK-BTC,ELF-ETH,KNC-BTC,CTXC-ETH,DBC-BTC,HC-ETH,EKT-BTC,DTA-USDT,ZLA-BTC,EKT-USDT,DTA-BTC,OCN-ETH,DGD-BTC,BHT-USDT,MTX-BTC,BCV-ETH,YEE-BTC,VSYS-HT,MEX-ETH,DATX-ETH,EGCC-BTC,LXT-ETH,ITC-USDT,TOS-ETH,ITC-BTC,RCN-ETH,XVG-BTC,SC-ETH,BT2-BTC,REQ-BTC,ELA-USDT,LET-USDT,STORJ-BTC,ALGO-ETH,POLY-BTC,LAMB-ETH,DCR-ETH,EGT-BTC,RTE-ETH,FAIR-ETH,CNN-ETH,BHT-BTC,GSC-ETH,GNT-BTC,PAI-ETH,PC-ETH,ADA-ETH,DOGE-BTC,ZEN-BTC,STEEM-ETH,XMR-BTC,XMR-USDT,MDS-ETH,TT-BTC,BTT-BTC,BHT-HT,ZJLT-ETH,UC-BTC,GVE-ETH,MXC-BTC,MANA-ETH,VSYS-BTC,THETA-ETH,NCC-BTC,APPC-ETH,SMT-BTC,IDT-BTC,UIP-ETH,ETH-BTC,BOX-ETH,LBA-ETH,NULS-ETH,PNT-ETH,BTG-BTC,CVNT-ETH,SALT-BTC,XEM-USDT,WXT-HT,BUT-BTC,DAC-BTC,DOCK-ETH,GET-ETH,AIDOC-ETH,EGT-USDT,WAN-ETH,KMD-BTC,MTN-ETH,CRO-USDT,ONT-ETH,BKBT-ETH,MEET-BTC,VEN-ETH,MT-ETH,SRN-BTC,UUU-BTC,SEELE-BTC,ICX-BTC,RDN-ETH,EGT-HT,ZIL-ETH,IRIS-BTC,CRO-HT,ACT-ETH,DOGE-USDT,NAS-USDT,PORTAL-BTC,ELA-BTC,OST-BTC,WICC-ETH,VET-BTC,XMX-ETH,WTC-BTC,HT-ETH,ATOM-ETH,18C-BTC", - "enabledPairs": "BTC-USDT", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": false + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USDT", + "available": "NKN-HT,HC-BTC,ZEC-USDT,KCASH-HT,CRE-USDT,SRN-BTC,LOOM-BTC,WAN-ETH,DAC-ETH,EDU-BTC,KCASH-BTC,BHD-BTC,UTK-ETH,LINK-USDT,EDU-ETH,WAN-BTC,DAC-BTC,OST-ETH,UTK-BTC,BCX-BTC,NODE-BTC,OST-BTC,BTM-USDT,NEXO-ETH,NPXS-BTC,GNT-USDT,FOR-USDT,LOOM-ETH,KCASH-ETH,DCR-USDT,NPXS-ETH,NKN-BTC,DASH-HT,EGT-USDT,NODE-HT,OGO-USDT,HC-ETH,ATOM-BTC,ZIL-USDT,ATOM-ETH,XMR-USDT,BKBT-BTC,STK-ETH,AST-ETH,NEXO-BTC,STK-BTC,STEEM-ETH,MTX-ETH,EVX-ETH,AKRO-BTC,IDT-ETH,DATX-ETH,AKRO-HT,GNX-BTC,WICC-ETH,DASH-BTC,YEE-BTC,WAVES-ETH,IOST-HT,SRN-ETH,DOCK-USDT,ZRX-ETH,CNNS-HT,TT-HT,AE-USDT,TT-BTC,RDN-BTC,IOST-BTC,ITC-ETH,COVA-BTC,AAC-ETH,PAI-ETH,CNNS-BTC,BHD-HT,DTA-BTC,REN-ETH,FTI-ETH,ALGO-ETH,IIC-BTC,PC-BTC,STORJ-BTC,ELA-BTC,AE-BTC,YCC-BTC,ETH-USDT,KMD-BTC,HT-USDT,BTT-BTC,QASH-BTC,MTN-BTC,MX-HT,AE-ETH,ETH-HUSD,FTI-BTC,LUN-BTC,BAT-ETH,TNB-BTC,PC-ETH,LAMB-BTC,PAY-BTC,18C-BTC,MUSK-BTC,QSP-BTC,DOGE-USDT,MANA-ETH,TOPC-ETH,QSP-ETH,HB10-USDT,HT-BTC,BCH-HT,MUSK-ETH,LYM-ETH,PORTAL-BTC,YCC-ETH,TOPC-BTC,ELA-ETH,ARDR-BTC,BSV-HUSD,CVNT-ETH,NCC-ETH,LOL-BTC,AST-BTC,HIT-ETH,KAN-USDT,BKBT-ETH,BHT-HT,BTS-BTC,ZRX-BTC,FOR-HT,AIDOC-BTC,NEO-BTC,THETA-ETH,GNX-ETH,BSV-BTC,WICC-BTC,EVX-BTC,ZIL-BTC,WTC-ETH,KNC-BTC,CNN-BTC,XRP-HT,TOP-BTC,DTA-ETH,REN-BTC,HT-HUSD,AAC-BTC,BTT-USDT,PAI-BTC,LAMB-USDT,YEE-ETH,RSR-HT,BTM-ETH,NKN-USDT,CRE-HT,WXT-BTC,NAS-ETH,SKM-HT,CRE-BTC,DAT-BTC,BIFI-BTC,DGD-ETH,TNT-ETH,HC-USDT,OCN-ETH,XTZ-ETH,PROPY-ETH,BCD-BTC,EKO-BTC,MEET-BTC,SMT-BTC,SALT-BTC,GXC-BTC,HOT-BTC,SBTC-BTC,SALT-ETH,CKB-BTC,UC-BTC,NANO-BTC,PAX-HUSD,FAIR-ETH,SWFTC-ETH,GSC-ETH,ONT-ETH,SKM-BTC,USDT-HUSD,EM-USDT,CKB-HT,XVG-BTC,CVCOIN-ETH,RTE-ETH,BHD-USDT,NANO-ETH,BFT-BTC,ICX-BTC,FTT-HT,XZC-BTC,CVNT-BTC,WAVES-USDT,GNT-ETH,HIT-BTC,LSK-ETH,NCC-BTC,CMT-BTC,STEEM-USDT,ATOM-USDT,LOL-USDT,DOGE-ETH,SMT-ETH,PROPY-BTC,KAN-BTC,DCR-BTC,GXC-ETH,CNN-ETH,REN-USDT,AIDOC-ETH,ITC-USDT,MCO-BTC,ETC-USDT,VSYS-USDT,BSV-USDT,FTT-BTC,TRIO-ETH,GT-USDT,ARPA-USDT,LET-USDT,LTC-BTC,XLM-BTC,NAS-BTC,REQ-BTC,NCASH-BTC,ARPA-BTC,ELA-USDT,ARDR-ETH,RSR-BTC,LAMB-HT,IIC-ETH,MANA-USDT,IOTA-ETH,ONE-BTC,DTA-USDT,MTN-ETH,MX-USDT,MEX-ETH,BTG-BTC,MAN-ETH,VIDY-BTC,RSR-USDT,MX-BTC,UUU-BTC,APPC-BTC,MANA-BTC,PAY-ETH,EGCC-ETH,GTC-BTC,SEELE-BTC,DBC-ETH,MDS-BTC,RCCC-ETH,NEW-USDT,SHE-BTC,STEEM-BTC,BLZ-ETH,ATP-HT,ITC-BTC,FOR-BTC,XMR-ETH,LTC-USDT,CTXC-USDT,XMX-BTC,BUT-BTC,VIDY-USDT,UIP-ETH,18C-ETH,NEW-BTC,NAS-USDT,BCV-BTC,ELF-ETH,BIX-BTC,GT-BTC,ETC-BTC,ONE-USDT,ADX-BTC,LINK-ETH,WAVES-BTC,RDN-ETH,PHX-BTC,ENG-BTC,MDS-USDT,HIT-USDT,IRIS-ETH,ACT-ETH,RCN-BTC,IOST-ETH,EOS-ETH,BOX-ETH,ADA-USDT,SKM-USDT,SMT-USDT,SNT-USDT,RUFF-BTC,POWR-BTC,XTZ-USDT,CKB-USDT,RUFF-ETH,EM-HT,LET-ETH,OCN-USDT,WXT-USDT,LET-BTC,POWR-ETH,PVT-HT,BOX-BTC,CRO-BTC,VSYS-BTC,EM-BTC,QTUM-BTC,LBA-ETH,XRP-USDT,CTXC-ETH,LBA-BTC,QTUM-ETH,GT-HT,CTXC-BTC,GAS-ETH,CRO-HT,PVT-BTC,VSYS-HT,BTS-USDT,NEO-USDT,QUN-BTC,NANO-USDT,BTC-USDT,BTC-HUSD,NULS-USDT,GRS-ETH,ETN-BTC,TOS-BTC,SC-BTC,LXT-USDT,ZEN-BTC,CVC-BTC,SSP-BTC,SNC-BTC,ELF-BTC,ZJLT-ETH,ACT-BTC,OMG-BTC,VET-BTC,EOS-BTC,ATP-USDT,BHT-USDT,GXC-USDT,IRIS-BTC,BIX-USDT,RCN-ETH,REQ-ETH,POLY-BTC,UUU-USDT,EOS-HUSD,CHAT-ETH,MEX-BTC,TOP-USDT,CVC-USDT,EOS-USDT,XLM-USDT,HPT-BTC,SC-ETH,ZEN-ETH,PNT-BTC,UGAS-BTC,DGB-BTC,ATP-BTC,SEELE-USDT,IOTA-BTC,UIP-USDT,ETC-HT,SHE-ETH,MT-BTC,WPR-ETH,PNT-ETH,HPT-USDT,XZC-USDT,VIDY-HT,ELF-USDT,BLZ-BTC,UGAS-ETH,MT-ETH,NULS-ETH,POLY-ETH,ARPA-HT,USDC-HUSD,ONE-HT,RCCC-BTC,ETN-ETH,TRX-ETH,MDS-ETH,DOGE-BTC,EKT-ETH,TUSD-HUSD,FTT-USDT,GAS-BTC,GRS-BTC,SOC-ETH,APPC-ETH,IOST-USDT,LSK-BTC,ONT-USDT,CMT-USDT,ABT-ETH,GET-ETH,WAXP-ETH,DOCK-BTC,OMG-USDT,TRIO-BTC,IOTA-USDT,SNT-BTC,NEW-HT,MCO-ETH,VET-ETH,OGO-HT,FSN-HT,NODE-USDT,ADA-BTC,TOS-ETH,EKO-ETH,EGT-BTC,OCN-BTC,DGD-BTC,ZEC-BTC,BTM-BTC,XTZ-BTC,ACT-USDT,IRIS-USDT,SWFTC-BTC,QTUM-USDT,ZLA-BTC,DCR-ETH,ABT-BTC,MEET-ETH,ZLA-ETH,WXT-HT,BFT-ETH,XVG-ETH,TT-USDT,CVCOIN-BTC,PVT-USDT,CNNS-USDT,HOT-ETH,GVE-ETH,RTE-BTC,GSC-BTC,DAT-ETH,GVE-BTC,RUFF-USDT,ONT-BTC,FAIR-BTC,UC-ETH,LXT-BTC,MTL-BTC,WICC-USDT,LXT-ETH,LBA-USDT,NULS-BTC,TRX-BTC,XEM-BTC,ZIL-ETH,LTC-HT,ZRX-USDT,EKT-BTC,CRO-USDT,XMX-ETH,ENG-ETH,BUT-ETH,BCV-ETH,BIX-ETH,THETA-BTC,XMR-BTC,ADX-ETH,UIP-BTC,WAXP-BTC,ADA-ETH,BTS-ETH,PAI-USDT,SOC-BTC,GET-BTC,DOCK-ETH,KNC-ETH,EGT-HT,LINK-BTC,ETH-BTC,XLM-ETH,BTT-ETH,STORJ-USDT,FSN-BTC,QASH-ETH,SOC-USDT,BTT-TRX,LAMB-ETH,LUN-ETH,TNB-ETH,MXC-BTC,RBTC-BTC,TOP-HT,EOS-HT,ALGO-USDT,MAN-BTC,MT-HT,BCH-BTC,EGCC-BTC,LYM-BTC,BAT-BTC,CHAT-BTC,EKT-USDT,WTC-BTC,DBC-BTC,UUU-ETH,HT-ETH,ALGO-BTC,NCASH-ETH,FSN-USDT,GTC-ETH,SEELE-ETH,THETA-USDT,DGB-ETH,WPR-BTC,DATX-BTC,PORTAL-ETH,XEM-USDT,MTX-BTC,XZC-ETH,CMT-ETH,QUN-ETH,BCH-USDT,LOL-HT,ICX-ETH,GNT-BTC,AKRO-USDT,BCH-HUSD,BHT-BTC,XRP-HUSD,XRP-BTC,OGO-BTC,VET-USDT,TRX-USDT,SSP-ETH,KAN-ETH,SNC-ETH,KMD-ETH,HPT-HT,COVA-ETH,OMG-ETH,WTC-USDT,TNT-BTC,DASH-USDT,BAT-USDT,CVC-ETH,IDT-BTC,ZJLT-BTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "pemKey": "-----BEGIN EC PRIVATE KEY-----\nJUSTADUMMY\n-----END EC PRIVATE KEY-----\n" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -984,40 +1647,69 @@ "name": "ITBIT", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "clientId": "ClientID", - "availablePairs": "XBTUSD,XBTSGD", - "enabledPairs": "XBTUSD,XBTSGD", "baseCurrencies": "USD,SGD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": false, - "pairsLastUpdated": 1566798411, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "lastUpdated": 1566798411, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "XBTUSD,XBTSGD", + "available": "XBTUSD,XBTSGD" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresSecret": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": {}, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": false, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1030,40 +1722,73 @@ "name": "Kraken", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "ATOM-ETH,QTUM-EUR,QTUM-USD,LTC-XBT,XTZ-ETH,XMR-XBT,ADA-EUR,BAT-ETH,BAT-EUR,BAT-XBT,QTUM-CAD,WAVES-USD,ETC-EUR,MLN-ETH,XLM-USD,XRP-CAD,ADA-USD,DASH-XBT,REP-XBT,XBT-CAD,XBT-EUR,XBT-GBP,USDT-USD,ETH-JPY,XBT-USD,ZEC-USD,ETH-EUR,ETH-USD,XTZ-XBT,ZEC-EUR,ZEC-JPY,ADA-ETH,EOS-ETH,QTUM-ETH,ETH-CAD,XTZ-EUR,EOS-EUR,REP-USD,XMR-USD,BCH-XBT,EOS-XBT,ETC-ETH,XLM-XBT,ADA-CAD,ADA-XBT,ATOM-EUR,ATOM-XBT,DASH-EUR,GNO-USD,GNO-XBT,WAVES-XBT,ETH-GBP,XBT-JPY,ZEC-XBT,QTUM-XBT,WAVES-ETH,XDG-XBT,XRP-XBT,EOS-USD,XMR-EUR,XRP-EUR,ATOM-CAD,DASH-USD,ETC-USD,ETH-XBT,GNO-EUR,ETC-XBT,LTC-EUR,REP-ETH,XTZ-USD,XLM-EUR,GNO-ETH,LTC-USD,REP-EUR,XRP-JPY,XRP-USD,ATOM-USD,BCH-USD,WAVES-EUR,BAT-USD,BCH-EUR,MLN-XBT,XTZ-CAD", - "enabledPairs": "XBT-USD", "baseCurrencies": "EUR,USD,CAD,GBP,JPY", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "separator": "," + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "XBT-USD", + "available": "ATOM-CAD,ATOM-XBT,XBT-CAD,XMR-USD,XRP-USD,BAT-XBT,OMG-USD,XBT-EUR,XTZ-CAD,ETH-GBP,ETH-JPY,XBT-JPY,ZEC-XBT,DASH-XBT,LSK-EUR,PAXG-EUR,XRP-EUR,BCH-XBT,GNO-USD,REP-EUR,XTZ-USD,ADA-CAD,ETH-XBT,LTC-USD,XLM-EUR,DASH-EUR,ICX-XBT,LSK-ETH,ZEC-EUR,REP-XBT,ATOM-EUR,EOS-EUR,EOS-USD,ETC-ETH,NANO-XBT,XTZ-ETH,EOS-XBT,ICX-ETH,LINK-ETH,ETH-EUR,ICX-EUR,ICX-USD,REP-ETH,ADA-EUR,WAVES-EUR,XRP-XBT,MLN-ETH,BAT-EUR,LSK-USD,NANO-USD,OMG-ETH,XLM-USD,XMR-XBT,ZEC-USD,BAT-USD,DAI-USD,PAXG-ETH,WAVES-USD,ADA-ETH,GNO-ETH,XTZ-XBT,ETH-CAD,ETH-USD,MLN-XBT,XBT-GBP,PAXG-USD,NANO-ETH,LTC-XBT,XRP-JPY,ATOM-ETH,LSK-XBT,XLM-XBT,ADA-XBT,GNO-EUR,LINK-XBT,ETC-USD,XTZ-EUR,XBT-USD,QTUM-ETH,QTUM-XBT,ETC-XBT,EOS-ETH,OMG-EUR,XDG-XBT,USDT-USD,ATOM-USD,GNO-XBT,LINK-EUR,SC-EUR,LTC-EUR,BCH-EUR,BCH-USD,QTUM-EUR,SC-XBT,DAI-USDT,NANO-EUR,SC-ETH,WAVES-XBT,ETC-EUR,REP-USD,ADA-USD,ETH-DAI,PAXG-XBT,QTUM-CAD,LINK-USD,OMG-XBT,SC-USD,WAVES-ETH,XMR-EUR,XRP-CAD,ZEC-JPY,BAT-ETH,DAI-EUR,DASH-USD,QTUM-USD" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "separator": "," + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresBase64DecodeSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1076,38 +1801,70 @@ "name": "LakeBTC", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BACETH,BTCUSD,USDSGD,USDJPY,LTCBTC,BTCCHF,BTCNZD,BTCJPY,USDNGN,BCHBTC,BTCAUD,NZDUSD,EURUSD,USDHKD,BTCEUR,USDCHF,GBPUSD,XRPBTC,AUDUSD,BTCHKD,BTCGBP,BTCCAD,BTCNGN,BTCSGD,USDCAD,ETHBTC", - "enabledPairs": "BTCUSD,BTCAUD", "baseCurrencies": "USD,EUR,HKD,AUD,GBP,NZD,JPY,SGD,NGN,CHF,CAD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCUSD,BTCAUD", + "available": "BACETH,BTCUSD,USDSGD,USDJPY,LTCBTC,BTCCHF,BTCNZD,BTCJPY,USDNGN,BCHBTC,BTCAUD,NZDUSD,EURUSD,USDHKD,BTCEUR,USDCHF,GBPUSD,XRPBTC,AUDUSD,BTCHKD,BTCGBP,BTCCAD,BTCNGN,BTCSGD,USDCAD,ETHBTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1120,40 +1877,72 @@ "name": "LBank", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "FBC_USDT,HDS_USDT,GALT_USDT,IOG_USDT,IOEX_USDT,VOLLAR_USDT,OATH_USDT,BLOC_USDT,BTC_USDT,ETH_USDT,ETH_BTC,ABBC_BTC,BZKY_ETH,ONOT_ETH,KISC_ETH,BXA_USDT,ATP_USDT,MAT_USDT,SKY_BTC,RNT_USDT,VENA_USDT,GRIN_USDT,IDA_USDT,PNT_USDT,BSV_USDT,OPX_USDT,TENA_ETH,VTHO_BTC,VNX_BTC,AMO_ETH,UBEX_BTC,EOS_BTC,UBEX_USDT,TNS_BTC,ALI_ETH,SDC_ETH,SAIT_ETH,ARTCN_USDT,DAX_BTC,DAX_ETH,DALI_USDT,VET_USDT,BCH_BTC,BCH_USDT,NEO_USDT,QTUM_USDT,ZEC_USDT,VET_BTC,PAI_BTC,PNT_BTC,NEO_BTC,DASH_BTC,LTC_BTC,ETC_BTC,QTUM_BTC,ZEC_BTC,SC_BTC,BTS_BTC,CPX_BTC,XWC_BTC,FIL6_BTC,FIL12_BTC,FIL36_BTC,EOS_USDT,UT_ETH,ELA_ETH,VET_ETH,VTHO_ETH,PAI_ETH,BFDT_ETH,HER_ETH,PTT_ETH,TAC_ETH,IDHUB_ETH,SSC_ETH,SKM_ETH,IIC_ETH,PLY_ETH,EXT_ETH,EOS_ETH,YOYOW_ETH,TRX_ETH,QTUM_ETH,ZEC_ETH,BTS_ETH,BTM_ETH,MITH_ETH,NAS_ETH,MAN_ETH,DBC_ETH,BTO_ETH,DDD_ETH,CPX_ETH,CS_ETH,IHT_ETH,TKY_ETH,OCN_ETH,DCT_ETH,ZPT_ETH,EKO_ETH,MDA_ETH,PST_ETH,XWC_ETH,PUT_ETH,PNT_ETH,AAC_ETH,FIL6_ETH,FIL12_ETH,FIL36_ETH,UIP_ETH,SEER_ETH,BSB_ETH,CDC_ETH,GRAMS_ETH,DDMX_ETH,EAI_ETH,INC_ETH,BNB_USDT,HT_USDT,KBC_BTC,KBC_USDT,MAI_USDT,PHV_USDT,GT_USDT,B91_USDT,VOKEN_USDT,CYE_USDT,BRC_USDT,BTC_AUSD,CXC_BTC,CXC_USDT,DDMX_USDT,SEAL_USDT,SEOS_BTC,BTY_USDT,FO_USDT,VCC_ETH,DLX_USDT,KDS_USDT,BFC_USDT,LBK_USDT,SERO_USDT,MTV_USDT,CKB_USDT,ARPA_USDT,ZIP_USDT,AT_USDT", - "enabledPairs": "btc_usdt", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_" + }, + "configFormat": { + "uppercase": false, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "btc_usdt", + "available": "FBC_USDT,GALT_USDT,IOEX_USDT,OATH_USDT,BLOC_USDT,BTC_USDT,ETH_USDT,ETH_BTC,ABBC_BTC,KISC_ETH,BXA_USDT,ATP_USDT,MAT_USDT,SKY_BTC,RNT_USDT,VENA_USDT,GRIN_USDT,IDA_USDT,PNT_USDT,OPX_USDT,VTHO_BTC,AMO_ETH,UBEX_BTC,EOS_BTC,UBEX_USDT,TNS_BTC,SAIT_ETH,DAX_BTC,DAX_ETH,DALI_USDT,VET_USDT,BCH_BTC,BCH_USDT,NEO_USDT,QTUM_USDT,ZEC_USDT,VET_BTC,PAI_BTC,PNT_BTC,NEO_BTC,DASH_BTC,LTC_BTC,ETC_BTC,QTUM_BTC,ZEC_BTC,SC_BTC,BTS_BTC,CPX_BTC,XWC_BTC,FIL6_BTC,FIL12_BTC,FIL36_BTC,EOS_USDT,UT_ETH,ELA_ETH,VET_ETH,VTHO_ETH,PAI_ETH,HER_ETH,PTT_ETH,TAC_ETH,IDHUB_ETH,SSC_ETH,SKM_ETH,PLY_ETH,EXT_ETH,EOS_ETH,YOYOW_ETH,TRX_ETH,QTUM_ETH,ZEC_ETH,BTS_ETH,BTM_ETH,MITH_ETH,NAS_ETH,MAN_ETH,DBC_ETH,BTO_ETH,DDD_ETH,CPX_ETH,CS_ETH,IHT_ETH,OCN_ETH,EKO_ETH,XWC_ETH,PUT_ETH,PNT_ETH,AAC_ETH,FIL6_ETH,FIL12_ETH,FIL36_ETH,SEER_ETH,BSB_ETH,CDC_ETH,GRAMS_ETH,DDMX_ETH,EAI_ETH,BNB_USDT,HT_USDT,KBC_BTC,KBC_USDT,MAI_USDT,PHV_USDT,GT_USDT,VOKEN_USDT,CYE_USDT,BRC_USDT,BTC_AUSD,DDMX_USDT,SEAL_USDT,SEOS_BTC,BTY_USDT,FO_USDT,DLX_USDT,BFC_USDT,LBK_USDT,SERO_USDT,MTV_USDT,CKB_USDT,ARPA_USDT,ZIP_USDT,AT_USDT,DOT_USDT,DILI_USDT,DUO_USDT,TEP_USDT,BIKI_USDT,MX_USDT,DNS_USDT,OKB_USDT,FLDT_USDT,CCTC_USDT,WIN_USDT,BTT_USDT,TRX_USDT,GRS_BTC,GST_USDT,GST_ETH,ABBC_USDT,UTK_USDT,GKI_USDT,BPX_USDT,SUTER_USDT,LT_USDT,LM_USDT" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1166,38 +1955,70 @@ "name": "LocalBitcoins", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTCXAF,BTCHKD,BTCBRL,BTCPLN,BTCGHS,BTCPEN,BTCSAR,BTCCAD,BTCJOD,BTCVES,BTCXOF,BTCRWF,BTCEUR,BTCNOK,BTCLTC,BTCZMW,BTCXRP,BTCPAB,BTCUSD,BTCCRC,BTCTTD,BTCLBP,BTCOMR,BTCRON,BTCGEL,BTCKRW,BTCCLP,BTCSZL,BTCNGN,BTCILS,BTCDKK,BTCMYR,BTCRUB,BTCKES,BTCINR,BTCJPY,BTCKHR,BTCCOP,BTCIRR,BTCARS,BTCKZT,BTCTZS,BTCVND,BTCEGP,BTCGBP,BTCTHB,BTCAED,BTCGTQ,BTCCHF,BTCIDR,BTCAUD,BTCNZD,BTCKWD,BTCBOB,BTCUGX,BTCETH,BTCUAH,BTCSGD,BTCCNY,BTCPHP,BTCTWD,BTCLKR,BTCNAD,BTCMXN,BTCBYN,BTCBDT,BTCDOP,BTCTRY,BTCPYG,BTCPKR,BTCQAR,BTCSEK,BTCMAD,BTCZAR", - "enabledPairs": "BTCAUD,BTCUSD", "baseCurrencies": "ARS,AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,ILS,INR,MXN,NOK,NZD,PLN,RUB,SEK,SGD,THB,USD,ZAR", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCAUD,BTCUSD", + "available": "BTCXAF,BTCRUB,BTCXRP,BTCIDR,BTCTZS,BTCKWD,BTCTHB,BTCPKR,BTCBWP,BTCTRY,BTCCLP,BTCMXN,BTCCZK,BTCLTC,BTCARS,BTCMAD,BTCMUR,BTCGEL,BTCPEN,BTCSAR,BTCKES,BTCBGN,BTCGBP,BTCUAH,BTCCNY,BTCHKD,BTCRON,BTCSGD,BTCPLN,BTCINR,BTCDKK,BTCCRC,BTCUGX,BTCSEK,BTCPYG,BTCZMW,BTCPHP,BTCNZD,BTCNOK,BTCZAR,BTCBDT,BTCUSD,BTCEGP,BTCBOB,BTCRSD,BTCCHF,BTCKZT,BTCPAB,BTCTWD,BTCAED,BTCVND,BTCETH,BTCDOP,BTCCAD,BTCJPY,BTCAUD,BTCBRL,BTCJOD,BTCGHS,BTCQAR,BTCGTQ,BTCKRW,BTCBYN,BTCEUR,BTCBAM,BTCLKR,BTCHUF,BTCTTD,BTCVES,BTCILS,BTCCOP,BTCSZL,BTCMWK,BTCMYR,BTCUYU,BTCNGN,BTCJMD,BTCXOF,BTCRWF" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1210,40 +2031,74 @@ "name": "OKCOIN International", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC_USD,LTC_USD,ETH_USD,ETC_USD,TUSD_USD,BCH_USD,EOS_USD,XRP_USD,TRX_USD,BSV_USD,USDT_USD,USDK_USD,XLM_USD,ADA_USD,BAT_USD,DCR_USD,EURS_USD,GRIN_USD,GUSD_USD,PAX_USD,USDC_USD,ZEC_USD,ZRX_USD,BTC_USDT,BTC_GUSD,BTC_PAX,BTC_TUSD,BTC_EUR,BTC_EURS,BTC_USDC,ETH_EUR,BCH_EUR,EURS_EUR", - "enabledPairs": "BTC_USD", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot", + "margin" + ], + "pairs": { + "spot": { + "enabled": "BTC-USD", + "available": "BTC-USD,LTC-USD,ETH-USD,ETC-USD,TUSD-USD,BCH-USD,EOS-USD,XRP-USD,TRX-USD,BSV-USD,USDT-USD,USDK-USD,XLM-USD,ADA-USD,BAT-USD,DCR-USD,EURS-USD,HBAR-USD,PAX-USD,USDC-USD,ZEC-USD,BTC-USDT,BTC-EUR,BTC-EURS,ETH-EUR,BCH-EUR,EURS-EUR" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1256,40 +2111,108 @@ "name": "OKEX", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BCH_BTC,BSV_BTC,DASH_BTC,ADA_BTC,ABL_BTC,AE_BTC,ALGO_BTC,ARDR_BTC,ATOM_BTC,BLOC_BTC,BTT_BTC,CAI_BTC,CTXC_BTC,CVT_BTC,DCR_BTC,EGT_BTC,GUSD_BTC,HPB_BTC,HYC_BTC,KAN_BTC,LBA_BTC,LEO_BTC,LET_BTC,LSK_BTC,NXT_BTC,ORS_BTC,PAX_BTC,SC_BTC,TUSD_BTC,USDC_BTC,VITE_BTC,WAVES_BTC,WIN_BTC,WXT_BTC,XAS_BTC,YOU_BTC,ZCO_BTC,ZIL_BTC,XRP_BTC,ELF_BTC,LRC_BTC,MCO_BTC,NULS_BTC,BCX_BTC,CMT_BTC,EDO_BTC,ITC_BTC,SBTC_BTC,ZEC_BTC,NEO_BTC,GAS_BTC,HC_BTC,QTUM_BTC,IOTA_BTC,XUC_BTC,EOS_BTC,SNT_BTC,OMG_BTC,LTC_BTC,ETH_BTC,ETC_BTC,BCD_BTC,BTG_BTC,ACT_BTC,PAY_BTC,BTM_BTC,DGD_BTC,GNT_BTC,LINK_BTC,WTC_BTC,ZRX_BTC,BNT_BTC,CVC_BTC,MANA_BTC,KNC_BTC,GNX_BTC,ICX_BTC,XEM_BTC,ARK_BTC,YOYO_BTC,FUN_BTC,ACE_BTC,TRX_BTC,DGB_BTC,SWFTC_BTC,XMR_BTC,XLM_BTC,KCASH_BTC,MDT_BTC,NAS_BTC,UGC_BTC,DPY_BTC,SSC_BTC,AAC_BTC,VIB_BTC,QUN_BTC,INT_BTC,IOST_BTC,INS_BTC,MOF_BTC,TCT_BTC,STC_BTC,THETA_BTC,PST_BTC,SNC_BTC,MKR_BTC,LIGHT_BTC,OF_BTC,TRUE_BTC,SOC_BTC,ZEN_BTC,HMC_BTC,ZIP_BTC,NANO_BTC,CIC_BTC,GTO_BTC,CHAT_BTC,INSUR_BTC,R_BTC,BEC_BTC,MITH_BTC,ABT_BTC,BKX_BTC,RFR_BTC,TRIO_BTC,DADI_BTC,ONT_BTC,OKB_BTC,ADA_ETH,ABL_ETH,AE_ETH,ALGO_ETH,ATOM_ETH,BTT_ETH,CAI_ETH,CTXC_ETH,DCR_ETH,EGT_ETH,HPB_ETH,HYC_ETH,KAN_ETH,LEO_ETH,LSK_ETH,MVP_ETH,ORS_ETH,SC_ETH,SDA_ETH,WAVES_ETH,WIN_ETH,YOU_ETH,ZIL_ETH,ELF_ETH,LTC_ETH,CMT_ETH,PRA_ETH,LRC_ETH,MCO_ETH,NULS_ETH,DGD_ETH,SNT_ETH,STORJ_ETH,ACT_ETH,BTM_ETH,EOS_ETH,OMG_ETH,DASH_ETH,XRP_ETH,ZEC_ETH,NEO_ETH,GAS_ETH,HC_ETH,QTUM_ETH,IOTA_ETH,ETC_ETH,LINK_ETH,WTC_ETH,ZRX_ETH,BNT_ETH,CVC_ETH,MANA_ETH,GNX_ETH,ICX_ETH,XEM_ETH,YOYO_ETH,TRX_ETH,DGB_ETH,SWFTC_ETH,XMR_ETH,XLM_ETH,KCASH_ETH,MDT_ETH,NAS_ETH,SSC_ETH,AAC_ETH,FAIR_ETH,RCT_ETH,TOPC_ETH,QUN_ETH,INT_ETH,IOST_ETH,INS_ETH,MOF_ETH,REF_ETH,SNC_ETH,MKR_ETH,LIGHT_ETH,OF_ETH,TRUE_ETH,ZEN_ETH,HMC_ETH,ZIP_ETH,NANO_ETH,CIC_ETH,GTO_ETH,INSUR_ETH,UCT_ETH,MITH_ETH,ABT_ETH,AUTO_ETH,TRIO_ETH,TRA_ETH,ONT_ETH,OKB_ETH,BTC_USDK,LTC_USDK,ETH_USDK,OKB_USDK,ETC_USDK,BCH_USDT,BCH_USDK,EOS_USDK,XRP_USDK,TRX_USDK,BSV_USDT,BSV_USDK,USDT_USDK,ADA_USDT,AE_USDT,ALGO_USDT,ALGO_USDK,ALV_USDT,ATOM_USDT,BLOC_USDT,BTT_USDT,CAI_USDT,CRO_USDT,CRO_USDK,CTXC_USDT,CVT_USDT,DCR_USDT,DOGE_USDT,DOGE_USDK,EC_USDT,EC_USDK,EGT_USDT,EM_USDT,EM_USDK,ETM_USDT,ETM_USDK,FSN_USDT,FSN_USDK,FTM_USDT,FTM_USDK,GUSD_USDT,HPB_USDT,HYC_USDT,KAN_USDT,LAMB_USDT,LAMB_USDK,LBA_USDT,LEO_USDT,LEO_USDK,LET_USDT,LSK_USDT,MVP_USDT,ORBS_USDT,ORBS_USDK,ORS_USDT,PAX_USDT,PLG_USDT,PLG_USDK,SC_USDT,TUSD_USDT,USDC_USDT,VNT_USDT,VNT_USDK,WAVES_USDT,WIN_USDT,WXT_USDT,WXT_USDK,XAS_USDT,YOU_USDT,ZIL_USDT,TRX_OKB,ADA_OKB,AE_OKB,BLOC_OKB,DCR_OKB,EGT_OKB,SC_OKB,WAVES_OKB,WXT_OKB,ELF_USDT,DASH_USDT,BTG_USDT,LRC_USDT,MCO_USDT,NULS_USDT,DASH_OKB,XRP_USDT,ZEC_USDT,NEO_USDT,GAS_USDT,HC_USDT,QTUM_USDT,IOTA_USDT,BTC_USDT,BCD_USDT,XUC_USDT,CMT_USDT,EDO_USDT,ITC_USDT,PRA_USDT,ETH_USDT,LTC_USDT,ETC_USDT,EOS_USDT,OMG_USDT,ACT_USDT,BTM_USDT,DGD_USDT,GNT_USDT,PAY_USDT,STORJ_USDT,SNT_USDT,LINK_USDT,WTC_USDT,ZRX_USDT,BNT_USDT,CVC_USDT,MANA_USDT,KNC_USDT,ICX_USDT,XEM_USDT,ARK_USDT,YOYO_USDT,AST_USDT,TRX_USDT,MDA_USDT,DGB_USDT,PPT_USDT,SWFTC_USDT,XMR_USDT,XLM_USDT,KCASH_USDT,MDT_USDT,NAS_USDT,RNT_USDT,UGC_USDT,DPY_USDT,SSC_USDT,AAC_USDT,FAIR_USDT,UBTC_USDT,SHOW_USDT,VIB_USDT,MOT_USDT,UTK_USDT,TOPC_USDT,QUN_USDT,INT_USDT,IPC_USDT,IOST_USDT,INS_USDT,YEE_USDT,MOF_USDT,TCT_USDT,STC_USDT,THETA_USDT,PST_USDT,MKR_USDT,LIGHT_USDT,OF_USDT,TRUE_USDT,SOC_USDT,ZEN_USDT,HMC_USDT,ZIP_USDT,NANO_USDT,CIC_USDT,GTO_USDT,CHAT_USDT,INSUR_USDT,R_USDT,BEC_USDT,MITH_USDT,ABT_USDT,BKX_USDT,RFR_USDT,TRIO_USDT,DADI_USDT,ONT_USDT,OKB_USDT,NEO_OKB,LTC_OKB,ETC_OKB,XRP_OKB,ZEC_OKB,QTUM_OKB,IOTA_OKB,EOS_OKB", - "enabledPairs": "eos_usdt", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "assetTypes": [ + "spot", + "futures", + "perpetualswap", + "index" + ], + "pairs": { + "futures": { + "available": "XRP-USD_191206,XRP-USD_191213,XRP-USD_191227,BTC-USD_191206,BTC-USD_191213,BTC-USD_191227,BTC-USDT_191206,BTC-USDT_191213,BTC-USDT_191227,LTC-USD_191206,LTC-USD_191213,LTC-USD_191227,LTC-USDT_191206,LTC-USDT_191213,LTC-USDT_191227,ETH-USD_191206,ETH-USD_191213,ETH-USD_191227,ETH-USDT_191206,ETH-USDT_191213,ETH-USDT_191227,ETC-USD_191206,ETC-USD_191213,ETC-USD_191227,BCH-USD_191206,BCH-USD_191213,BCH-USD_191227,BCH-USDT_191206,BCH-USDT_191213,BCH-USDT_191227,BSV-USD_191206,BSV-USD_191213,BSV-USD_191227,EOS-USDT_191206,EOS-USDT_191213,EOS-USDT_191227,EOS-USD_191206,EOS-USD_191213,EOS-USD_191227,TRX-USD_191206,TRX-USD_191213,TRX-USD_191227", + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + } + }, + "index": { + "available": "XRP-USD,XRP-USD,XRP-USD,BTC-USD,BTC-USD,BTC-USD,BTC-USDT,BTC-USDT,BTC-USDT,LTC-USD,LTC-USD,LTC-USD,LTC-USDT,LTC-USDT,LTC-USDT,ETH-USD,ETH-USD,ETH-USD,ETH-USDT,ETH-USDT,ETH-USDT,ETC-USD,ETC-USD,ETC-USD,BCH-USD,BCH-USD,BCH-USD,BCH-USDT,BCH-USDT,BCH-USDT,BSV-USD,BSV-USD,BSV-USD,EOS-USDT,EOS-USDT,EOS-USDT,EOS-USD,EOS-USD,EOS-USD,TRX-USD,TRX-USD,TRX-USD", + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + } + }, + "perpetualswap": { + "available": "BTC-USD_SWAP,LTC-USD_SWAP,ETH-USD_SWAP,TRX-USD_SWAP,BCH-USD_SWAP,BSV-USD_SWAP,EOS-USD_SWAP,XRP-USD_SWAP,ETC-USD_SWAP", + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + } + }, + "spot": { + "enabled": "EOS-USDT", + "available": "XPO-USDT,SPND-USDK,SPND-BTC,ROAD-USDK,BCH-BTC,BSV-BTC,DASH-BTC,ADA-BTC,ABL-BTC,AE-BTC,ALGO-BTC,ARDR-BTC,ATOM-BTC,BLOC-BTC,BTT-BTC,CAI-BTC,CRO-BTC,CTXC-BTC,CVT-BTC,DCR-BTC,EGT-BTC,GUSD-BTC,HBAR-BTC,HPB-BTC,HYC-BTC,KAN-BTC,LBA-BTC,LEO-BTC,LET-BTC,LSK-BTC,NXT-BTC,ORS-BTC,PAX-BTC,PMA-BTC,SC-BTC,TUSD-BTC,USDC-BTC,VITE-BTC,VSYS-BTC,WAVES-BTC,WIN-BTC,WXT-BTC,XAS-BTC,XTZ-BTC,YOU-BTC,ZIL-BTC,XRP-BTC,ELF-BTC,LRC-BTC,MCO-BTC,NULS-BTC,BCX-BTC,CMT-BTC,EDO-BTC,ITC-BTC,SBTC-BTC,ZEC-BTC,NEO-BTC,GAS-BTC,HC-BTC,QTUM-BTC,IOTA-BTC,XUC-BTC,EOS-BTC,SNT-BTC,OMG-BTC,LTC-BTC,ETH-BTC,ETC-BTC,BCD-BTC,BTG-BTC,ACT-BTC,PAY-BTC,BTM-BTC,DGD-BTC,GNT-BTC,LINK-BTC,WTC-BTC,ZRX-BTC,BNT-BTC,CVC-BTC,MANA-BTC,KNC-BTC,GNX-BTC,ICX-BTC,XEM-BTC,ARK-BTC,YOYO-BTC,FUN-BTC,ACE-BTC,TRX-BTC,DGB-BTC,SWFTC-BTC,XMR-BTC,XLM-BTC,KCASH-BTC,MDT-BTC,NAS-BTC,UGC-BTC,DPY-BTC,SSC-BTC,AAC-BTC,VIB-BTC,QUN-BTC,INT-BTC,IOST-BTC,INS-BTC,MOF-BTC,TCT-BTC,STC-BTC,THETA-BTC,PST-BTC,SNC-BTC,MKR-BTC,LIGHT-BTC,OF-BTC,TRUE-BTC,SOC-BTC,ZEN-BTC,HMC-BTC,ZIP-BTC,NANO-BTC,CIC-BTC,GTO-BTC,CHAT-BTC,INSUR-BTC,R-BTC,BEC-BTC,MITH-BTC,ABT-BTC,BKX-BTC,RFR-BTC,TRIO-BTC,EDGE-BTC,ONT-BTC,OKB-BTC,ADA-ETH,ABL-ETH,AE-ETH,ALGO-ETH,ATOM-ETH,BTT-ETH,CAI-ETH,CTXC-ETH,DCR-ETH,EGT-ETH,HPB-ETH,HYC-ETH,KAN-ETH,LEO-ETH,MVP-ETH,ORS-ETH,SC-ETH,SDA-ETH,WAVES-ETH,WIN-ETH,YOU-ETH,ZIL-ETH,ELF-ETH,LTC-ETH,CMT-ETH,PRA-ETH,LRC-ETH,MCO-ETH,NULS-ETH,DGD-ETH,STORJ-ETH,BTM-ETH,EOS-ETH,OMG-ETH,DASH-ETH,XRP-ETH,ZEC-ETH,NEO-ETH,GAS-ETH,HC-ETH,QTUM-ETH,IOTA-ETH,ETC-ETH,LINK-ETH,WTC-ETH,ZRX-ETH,CVC-ETH,MANA-ETH,GNX-ETH,XEM-ETH,TRX-ETH,SWFTC-ETH,XMR-ETH,XLM-ETH,KCASH-ETH,MDT-ETH,NAS-ETH,SSC-ETH,AAC-ETH,FAIR-ETH,RCT-ETH,TOPC-ETH,INT-ETH,IOST-ETH,INS-ETH,MOF-ETH,REF-ETH,MKR-ETH,LIGHT-ETH,OF-ETH,TRUE-ETH,ZEN-ETH,NANO-ETH,CIC-ETH,GTO-ETH,UCT-ETH,MITH-ETH,ABT-ETH,AUTO-ETH,TRIO-ETH,ONT-ETH,OKB-ETH,BTC-USDK,LTC-USDK,ETH-USDK,OKB-USDK,ETC-USDK,BCH-USDT,BCH-USDK,EOS-USDK,XRP-USDK,TRX-USDK,BSV-USDT,BSV-USDK,USDT-USDK,ADA-USDT,AE-USDT,ALGO-USDT,ALGO-USDK,ALV-USDT,ATOM-USDT,BLOC-USDT,BTT-USDT,CAI-USDT,CRO-USDT,CRO-USDK,CTXC-USDT,CVT-USDT,DCR-USDT,DOGE-USDT,DOGE-USDK,EC-USDT,EC-USDK,EGT-USDT,EM-USDT,EM-USDK,ETM-USDT,ETM-USDK,FSN-USDT,FSN-USDK,FTM-USDT,FTM-USDK,GUSD-USDT,HBAR-USDT,HBAR-USDK,HPB-USDT,HYC-USDT,KAN-USDT,LAMB-USDT,LAMB-USDK,LBA-USDT,LEO-USDT,LEO-USDK,LET-USDT,LSK-USDT,MVP-USDT,ORBS-USDT,ORBS-USDK,ORS-USDT,PAX-USDT,PLG-USDT,PLG-USDK,PMA-USDK,ROAD-USDT,SC-USDT,TUSD-USDT,USDC-USDT,VNT-USDT,VNT-USDK,VSYS-USDT,VSYS-USDK,WAVES-USDT,WIN-USDT,WXT-USDT,WXT-USDK,XAS-USDT,XPO-USDK,XTZ-USDT,YOU-USDT,ZIL-USDT,TRX-OKB,AE-OKB,BLOC-OKB,EGT-OKB,SC-OKB,WXT-OKB,ELF-USDT,DASH-USDT,BTG-USDT,LRC-USDT,MCO-USDT,NULS-USDT,DASH-OKB,XRP-USDT,ZEC-USDT,NEO-USDT,GAS-USDT,HC-USDT,QTUM-USDT,IOTA-USDT,BTC-USDT,BCD-USDT,XUC-USDT,CMT-USDT,EDO-USDT,ITC-USDT,PRA-USDT,ETH-USDT,LTC-USDT,ETC-USDT,EOS-USDT,OMG-USDT,ACT-USDT,BTM-USDT,DGD-USDT,GNT-USDT,PAY-USDT,STORJ-USDT,SNT-USDT,LINK-USDT,WTC-USDT,ZRX-USDT,BNT-USDT,CVC-USDT,MANA-USDT,KNC-USDT,ICX-USDT,XEM-USDT,ARK-USDT,YOYO-USDT,AST-USDT,TRX-USDT,MDA-USDT,DGB-USDT,PPT-USDT,SWFTC-USDT,XMR-USDT,XLM-USDT,KCASH-USDT,MDT-USDT,NAS-USDT,RNT-USDT,UGC-USDT,DPY-USDT,SSC-USDT,AAC-USDT,FAIR-USDT,UBTC-USDT,SHOW-USDT,VIB-USDT,MOT-USDT,UTK-USDT,TOPC-USDT,QUN-USDT,INT-USDT,IPC-USDT,IOST-USDT,INS-USDT,YEE-USDT,MOF-USDT,TCT-USDT,STC-USDT,THETA-USDT,PST-USDT,MKR-USDT,LIGHT-USDT,OF-USDT,TRUE-USDT,SOC-USDT,ZEN-USDT,HMC-USDT,ZIP-USDT,NANO-USDT,CIC-USDT,GTO-USDT,CHAT-USDT,INSUR-USDT,R-USDT,BEC-USDT,MITH-USDT,ABT-USDT,BKX-USDT,RFR-USDT,TRIO-USDT,EDGE-USDT,ONT-USDT,OKB-USDT,NEO-OKB,LTC-OKB,ETC-OKB,XRP-OKB,ZEC-OKB,IOTA-OKB,EOS-OKB", + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + } + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1302,40 +2225,72 @@ "name": "Poloniex", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC_DASH,BTC_VIA,USDC_ZEC,USDT_BCHSV,BTC_XEM,USDT_STR,ETH_REP,BTC_MANA,USDC_STR,USDC_ETH,USDT_BTC,BTC_REP,BTC_PASC,BTC_GNT,ETH_ZRX,BTC_SNT,ETH_BAT,USDC_DOGE,BTC_POLY,BTC_ATOM,BTC_BAT,BTC_DGB,BTC_NXT,BTC_STR,BTC_STRAT,BTC_EOS,ETH_EOS,BTC_KNC,USDT_SC,BTC_BCHSV,BTC_XMR,BTC_STEEM,BTC_ZEC,BTC_GAS,USDT_BCHABC,USDC_ATOM,BTC_XRP,USDT_DASH,USDT_ETH,USDT_DOGE,BTC_QTUM,USDC_BTC,BTC_LPT,USDT_DGB,BTC_DOGE,USDT_BAT,USDC_BCHSV,BTC_BTS,BTC_GAME,BTC_SC,BTC_OMG,USDT_MANA,BTC_GRIN,BTC_LTC,BTC_ETH,USDT_EOS,USDT_LSK,USDC_BCHABC,USDC_XMR,BTC_NMR,USDT_REP,BTC_ZRX,USDT_GNT,USDT_QTUM,BTC_BNT,USDC_EOS,BTC_BCN,USDT_ATOM,USDC_DASH,BTC_OMNI,BTC_FCT,BTC_LSK,USDC_XRP,BTC_FOAM,BTC_CVC,BTC_NAV,USDT_LTC,USDT_NXT,USDT_XMR,USDT_XRP,BTC_LBC,USDT_ETC,BTC_LOOM,USDT_GRIN,BTC_DCR,BTC_ETC,ETH_ETC,BTC_ARDR,USDT_ZEC,BTC_BCHABC,USDC_GRIN,BTC_STORJ,USDT_ZRX,USDC_USDT,USDC_ETC,BTC_CLAM,BTC_MAID,BTC_VTC,BTC_XPM,ETH_ZEC,USDC_LTC", - "enabledPairs": "BTC_LTC,BTC_ETH,BTC_DOGE,BTC_DASH,BTC_XRP", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_LTC,BTC_ETH,BTC_DOGE,BTC_DASH,BTC_XRP", + "available": "BTC_ATOM,USDT_BCHABC,BTC_STR,USDT_DASH,ETH_BAT,BTC_MANA,USDT_MANA,USDT_ETC,USDT_REP,BTC_STORJ,USDT_BCHSV,USDC_GRIN,BTC_DOGE,BTC_XRP,USDT_ZEC,USDT_DOGE,USDC_ETH,BTC_LSK,ETH_ETC,BTC_ZRX,USDC_ZEC,USDT_XMR,USDT_EOS,BTC_QTUM,USDC_STR,USDC_USDT,BTC_BNT,BTC_BCHSV,BTC_LTC,BTC_ETC,BTC_REP,BTC_ARDR,BTC_KNC,BTC_LPT,BTC_TRX,USDT_ETH,USDT_ATOM,USDC_ETC,USDC_BCHABC,USDC_ATOM,USDT_GRIN,BTC_XPM,USDT_BAT,BTC_LOOM,USDT_QTUM,BTC_BCHABC,BTC_GAS,USDT_GNT,USDC_LTC,BTC_BTS,BTC_DASH,USDT_STR,BTC_ETH,ETH_ZEC,USDC_DASH,BTC_XMR,USDT_BTC,BTC_SC,BTC_ZEC,BTC_MAID,BTC_NXT,ETH_REP,BTC_STRAT,BTC_NMR,USDC_XMR,USDC_EOS,BTC_OMNI,BTC_OMG,ETH_EOS,USDT_ZRX,USDC_BCHSV,BTC_BCN,BTC_EOS,BTC_BAT,USDT_DGB,USDC_BTC,USDC_DOGE,USDT_TRX,BTC_DGB,BTC_FCT,ETH_ZRX,USDT_LSK,USDT_SC,BTC_VIA,USDT_LTC,BTC_GNT,USDC_XRP,BTC_CVC,BTC_SNT,BTC_FOAM,BTC_VTC,BTC_XEM,USDT_NXT,USDT_XRP,BTC_DCR,BTC_POLY,BTC_GRIN,USDC_TRX" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1348,42 +2303,74 @@ "name": "Yobit", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "DASH_BTC,WAVES_BTC,LSK_BTC,LIZA_BTC,BCC_BTC,ETH_BTC,LTC_BTC,TRX_BTC,DOGE_BTC,VNTX_BTC,SW_BTC,ZEC_BTC,DASH_ETH,WAVES_ETH,LSK_ETH,LIZA_ETH,BCC_ETH,LTC_ETH,TRX_ETH,DOGE_ETH,VNTX_ETH,SW_ETH,ZEC_ETH,DASH_DOGE,WAVES_DOGE,LSK_DOGE,LIZA_DOGE,BCC_DOGE,LTC_DOGE,TRX_DOGE,VNTX_DOGE,SW_DOGE,ZEC_DOGE,DASH_USD,WAVES_USD,LSK_USD,LIZA_USD,BCC_USD,LTC_USD,TRX_USD,VNTX_USD,SW_USD,ZEC_USD,ETH_USD,BTC_USD,DASH_RUR,WAVES_BTC,WAVES_RUR,LSK_RUR,LIZA_RUR,BCC_RUR,LTC_RUR,TRX_RUR,VNTX_RUR,SW_RUR,ETH_RUR,ZEC_RUR", - "enabledPairs": "LTC_BTC,ETH_BTC,BTC_USD,DASH_BTC", "baseCurrencies": "USD,RUR", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": false, - "pairsLastUpdated": 1566798411, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_", + "separator": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "lastUpdated": 1566798411, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "LTC_BTC,ETH_BTC,BTC_USD,DASH_BTC", + "available": "DASH_BTC,WAVES_BTC,LSK_BTC,LIZA_BTC,BCC_BTC,ETH_BTC,LTC_BTC,TRX_BTC,DOGE_BTC,VNTX_BTC,SW_BTC,ZEC_BTC,DASH_ETH,WAVES_ETH,LSK_ETH,LIZA_ETH,BCC_ETH,LTC_ETH,TRX_ETH,DOGE_ETH,VNTX_ETH,SW_ETH,ZEC_ETH,DASH_DOGE,WAVES_DOGE,LSK_DOGE,LIZA_DOGE,BCC_DOGE,LTC_DOGE,TRX_DOGE,VNTX_DOGE,SW_DOGE,ZEC_DOGE,DASH_USD,WAVES_USD,LSK_USD,LIZA_USD,BCC_USD,LTC_USD,TRX_USD,VNTX_USD,SW_USD,ZEC_USD,ETH_USD,BTC_USD,DASH_RUR,WAVES_BTC,WAVES_RUR,LSK_RUR,LIZA_RUR,BCC_RUR,LTC_RUR,TRX_RUR,VNTX_RUR,SW_RUR,ETH_RUR,ZEC_RUR" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_", - "separator": "-" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": false, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1396,40 +2383,72 @@ "name": "ZB", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "DASH_USDT,XLM_QC,DOGE_QC,SBTC_USDT,SNT_USDT,BRC_BTC,BCHSV_QC,TUSD_USDT,ZB_BTC,GRIN_USDT,BAT_USDT,HPY_USDT,ADA_BTC,XTZ_USDT,XWC_USDT,YTNB_USDT,QTUM_USDT,EDO_USDT,BTC_QC,ETC_PAX,TV_BTC,HSR_BTC,XWC_QC,TRX_USDT,VSYS_ZB,LTC_PAX,OMG_QC,ETH_BTC,NEO_BTC,HPY_QC,TOPC_USDT,ICX_USDT,BCX_USDT,GNT_QC,B91_QC,EOS_QC,PAX_QC,BTC_PAX,XRP_QC,LTC_USDT,MANA_BTC,BITE_BTC,EOS_BTC,XUC_QC,HOTC_QC,BAR_USDT,ETZ_QC,XRP_USDT,HOTC_USDT,DOGE_BTC,ZRX_BTC,TRUE_USDT,GRAM_USDT,BTH_QC,HLC_QC,SLT_QC,BCD_USDT,ETC_USDT,GNT_BTC,BTP_QC,ZRX_USDT,BCW_QC,PDX_QC,QTUM_BTC,LTC_QC,BRC_USDT,EPC_QC,GRAM_QC,CHAT_USDT,KNC_QC,DASH_BTC,XMR_QC,XEM_QC,BTP_USDT,HSR_QC,BCD_QC,EOSDAC_USDT,MTL_USDT,ENTC_USDT,KNC_USDT,MITH_QC,SAFE_USDT,1ST_USDT,TRX_QC,OMG_BTC,BRC_QC,MCO_QC,LBTC_BTC,KAN_BTC,1ST_QC,BTM_QC,INK_USDT,GRIN_QC,UBTC_QC,EPC_BTC,XEM_BTC,TV_USDT,ETC_BTC,XEM_USDT,UBTC_USDT,TRUE_BTC,HSR_USDT,BCHSV_USDT,AE_BTC,BCX_QC,ETH_PAX,ACC_USDT,OMG_USDT,ETZ_USDT,DDM_QC,KAN_QC,INK_QC,DOGE_USDT,BCHABC_QC,BITCNY_QC,TRUE_QC,DASH_QC,QUN_USDT,ZRX_QC,BTM_BTC,BTM_USDT,HLC_USDT,SLT_USDT,BTC_USDT,CDC_QC,AE_QC,LBTC_USDT,MCO_USDT,XLM_BTC,LEO_USDT,BTN_QC,SAFE_QC,XRP_BTC,BTS_BTC,BCX_BTC,DDM_USDT,TRX_BTC,QUN_QC,BTS_USDT,PDX_BTC,ETC_QC,BCHABC_USDT,QTUM_QC,ADA_USDT,EOSDAC_QC,BDS_QC,BTN_USDT,SLT_BTC,PDX_USDT,SUB_QC,USDT_QC,TOPC_QC,XMR_USDT,BAT_QC,SNT_QC,B91_USDT,GNT_USDT,PAX_USDT,AE_USDT,ZB_USDT,NWT_USDT,CDC_USDT,RCN_USDT,NEO_QC,MANA_USDT,TV_QC,VSYS_BTC,ZB_QC,GRAM_BTC,BTH_USDT,AAA_QC,ICX_QC,LTC_BTC,ETH_QC,CHAT_QC,BCW_USDT,SNT_BTC,ADA_QC,VSYS_QC,XLM_USDT,BAT_BTC,ETH_USDT,EOS_USDT,ICX_BTC,LBTC_QC,NEO_USDT,MANA_QC,BTS_QC", - "enabledPairs": "BTC_USDT,ETH_USDT", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_USDT,ETH_USDT", + "available": "TOPC_QC,CDC_QC,1ST_USDT,TSR_USDT,XRP_QC,ZRX_USDT,INK_QC,PAX_USDT,BTC_USDT,B91_QC,ETZ_USDT,ETH_USDT,SLT_QC,BRC_QC,XWC_QC,ZB_BTC,EOS_BTC,BTH_QC,BCD_USDT,MTL_USDT,SLT_BTC,EOS_USDT,HLC_QC,EPC_QC,QUN_USDT,BTP_USDT,NEO_BTC,LTC_QC,BRC_USDT,SNT_BTC,BCW_QC,BAT_QC,ADA_QC,HSR_BTC,ZRX_QC,SLT_USDT,SNT_QC,MANA_QC,MITH_QC,XEM_USDT,ZB_USDT,RCN_USDT,LVN_QC,ADA_BTC,PDX_USDT,CDC_USDT,PAX_QC,XLM_QC,LEO_USDT,BCHSV_QC,BTM_BTC,ETH_QC,DOGE_USDT,MANA_BTC,GRAM_QC,TRX_USDT,AAA_QC,TOPC_USDT,TRX_QC,YTNB_USDT,EDO_USDT,HSR_USDT,XRP_BTC,LTC_USDT,TUSD_USDT,EOSDAC_QC,BTM_USDT,HPY_USDT,DOGE_BTC,XEM_QC,QUN_QC,DASH_BTC,BCW_USDT,SUB_QC,SAFE_QC,ZB_QC,OMG_QC,ICX_QC,DDM_USDT,BCX_USDT,BAT_BTC,EOS_QC,BTS_USDT,GNT_QC,LBTC_USDT,MANA_USDT,TRUE_QC,UBTC_QC,BRC_BTC,CHAT_USDT,ICX_BTC,BITCNY_QC,MCO_USDT,BCHSV_USDT,EOSDAC_USDT,TRUE_BTC,VSYS_BTC,CRO_QC,BCHABC_QC,TV_QC,ETC_BTC,EPC_BTC,HPY_QC,XMR_QC,GRAM_BTC,VSYS_QC,QTUM_USDT,NWT_USDT,ACC_USDT,NEO_QC,OMG_BTC,GRIN_QC,HSR_QC,INK_USDT,AE_QC,BTH_USDT,BCX_QC,ADA_USDT,KNC_USDT,B91_USDT,ENTC_USDT,TV_USDT,XRP_USDT,ETZ_QC,BCX_BTC,TRUE_USDT,USDT_QC,XLM_USDT,DASH_USDT,XWC_USDT,HOTC_USDT,1ST_QC,MCO_QC,GRAM_USDT,QTUM_QC,UBTC_USDT,SBTC_USDT,KNC_QC,XTZ_USDT,NEO_USDT,KAN_BTC,BTP_QC,DASH_QC,TV_BTC,QTUM_BTC,BAT_USDT,BCHABC_USDT,FN_QC,BAR_USDT,ZRX_BTC,PDX_BTC,GNT_USDT,BTN_QC,BTS_QC,GRIN_USDT,TRX_BTC,ETH_BTC,LBTC_QC,BTN_USDT,VSYS_ZB,AE_USDT,BITE_BTC,ICX_USDT,DDM_QC,PDX_QC,LTC_BTC,KAN_QC,BTS_BTC,GNT_BTC,BCD_QC,HOTC_QC,BDS_QC,LBTC_BTC,CHAT_QC,ETC_USDT,SNT_USDT,BTC_QC,XEM_BTC,XMR_USDT,ETC_QC,BTM_QC,OMG_USDT,DOGE_QC,XLM_BTC,HLC_USDT,AE_BTC,SAFE_USDT,XUC_QC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1441,8 +2460,12 @@ ], "bankAccounts": [ { + "enabled": false, "bankName": "test", "bankAddress": "test", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "TestAccount", "accountNumber": "0234", "swiftCode": "91272837", @@ -1450,20 +2473,5 @@ "supportedCurrencies": "USD", "supportedExchanges": "ANX,Kraken" } - ], - "connectionMonitor": { - "preferredDNSList": [ - "8.8.8.8", - "8.8.4.4", - "1.1.1.1", - "1.0.0.1" - ], - "preferredDomainList": [ - "www.google.com", - "www.cloudflare.com", - "www.facebook.com" - ], - "checkInterval": 1000000000 - }, - "fiatDispayCurrency": "" + ] } \ No newline at end of file diff --git a/connchecker/connchecker.go b/connchecker/connchecker.go index c3f4b70b..1eb7cdc9 100644 --- a/connchecker/connchecker.go +++ b/connchecker/connchecker.go @@ -53,9 +53,9 @@ func New(dnsList, domainList []string, checkInterval time.Duration) (*Checker, e } if c.connected { - log.Debug(ConnFound) + log.Debugln(log.Global, ConnFound) } else { - log.Warnf(ConnNotFound) + log.Warnln(log.Global, ConnNotFound) } c.shutdown = make(chan struct{}, 1) @@ -86,7 +86,7 @@ func (c *Checker) Shutdown() { // Monitor determines internet connectivity via a DNS lookup func (c *Checker) Monitor(wg *sync.WaitGroup) { c.wg.Add(1) - tick := time.NewTicker(time.Second) + tick := time.NewTicker(c.CheckInterval) defer func() { tick.Stop(); c.wg.Done() }() wg.Done() for { @@ -137,7 +137,7 @@ func (c *Checker) connectionTest() { if err == nil { c.Lock() if !c.connected { - log.Debug(ConnRe) + log.Debugln(log.Global, ConnRe) c.connected = true } c.Unlock() @@ -150,7 +150,7 @@ func (c *Checker) connectionTest() { if err == nil { c.Lock() if !c.connected { - log.Debug(ConnRe) + log.Debugln(log.Global, ConnRe) c.connected = true } c.Unlock() @@ -160,7 +160,7 @@ func (c *Checker) connectionTest() { c.Lock() if c.connected { - log.Warn(ConnLost) + log.Warnln(log.Global, ConnLost) c.connected = false } c.Unlock() diff --git a/connchecker/connchecker_test.go b/connchecker/connchecker_test.go index 7b1b27b6..1ff3dfd7 100644 --- a/connchecker/connchecker_test.go +++ b/connchecker/connchecker_test.go @@ -9,22 +9,22 @@ func TestConnection(t *testing.T) { faultyHost := []string{"faultyHost"} _, err := New(faultyDomain, nil, 100000) if err == nil { - t.Fatal("Test Failed - New error cannot be nil") + t.Fatal("New error cannot be nil") } _, err = New(DefaultDNSList, nil, 100000) if err != nil { - t.Fatal("Test Failed - New error", err) + t.Fatal("New error", err) } _, err = New(nil, faultyHost, 100000) if err != nil { - t.Fatal("Test Failed - New error cannot be nil", err) + t.Fatal("New error cannot be nil", err) } c, err := New(nil, nil, 0) if err != nil { - t.Fatal("Test Failed - New error", err) + t.Fatal("New error", err) } if !c.IsConnected() { diff --git a/contrib/bash_autocomplete b/contrib/bash_autocomplete new file mode 100644 index 00000000..6d6c7d9a --- /dev/null +++ b/contrib/bash_autocomplete @@ -0,0 +1,23 @@ +#! /bin/bash +# bash programmable completion for gctcli +# copy to /etc/bash_completion.d/gctcli and source it or restart your shell + +: ${PROG:=$(basename ${BASH_SOURCE})} + +_gctcli() { + if [[ "${COMP_WORDS[0]}" != "source" ]]; then + local cur opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + if [[ "$cur" == "-"* ]]; then + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) + else + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) + fi + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi +} + +complete -o bashdefault -o default -o nospace -F _gctcli $PROG +unset PROG \ No newline at end of file diff --git a/contrib/sqlboiler.cmd b/contrib/sqlboiler.cmd new file mode 100644 index 00000000..95f5d29c --- /dev/null +++ b/contrib/sqlboiler.cmd @@ -0,0 +1,20 @@ +@echo off +title GoCryptoTrader Database Model Generation +IF NOT DEFINED GOPATH ( + echo "GOPATH not set" + exit +) + +IF NOT DEFINED DRIVER ( + SET DRIVER=psql +) + +IF %DRIVER%==psql ( + IF NOT DEFINED MODEL (SET MODEL=postgres) +) ELSE ( + IF NOT DEFINED MODEL (SET MODEL=sqlite3) +) +cd ..\ +start %GOPATH%\\bin\\sqlboiler -o database\\models\\%MODEL% -p %MODEL% --no-auto-timestamps --wipe %DRIVER% + +pause \ No newline at end of file diff --git a/contrib/zsh_autocomplete b/contrib/zsh_autocomplete new file mode 100644 index 00000000..ed4951b2 --- /dev/null +++ b/contrib/zsh_autocomplete @@ -0,0 +1,14 @@ +# zsh programmable completion for gctcli +# source zsh_autocomplete + +_gctcli() { + + local -a opts + opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}") + + _describe 'values' opts + + return +} + +compdef _gctcli gctcli \ No newline at end of file diff --git a/core/banner.go b/core/banner.go new file mode 100644 index 00000000..d22f59f2 --- /dev/null +++ b/core/banner.go @@ -0,0 +1,11 @@ +package core + +// Banner features the GoCryptoTrader banner +const Banner = ` + ______ ______ __ ______ __ + / ____/____ / ____/_____ __ __ ____ / /_ ____ /_ __/_____ ______ ____/ /___ _____ + / / __ / __ \ / / / ___// / / // __ \ / __// __ \ / / / ___// __ // __ // _ \ / ___/ +/ /_/ // /_/ // /___ / / / /_/ // /_/ // /_ / /_/ // / / / / /_/ // /_/ // __// / +\____/ \____/ \____//_/ \__, // .___/ \__/ \____//_/ /_/ \__,_/ \__,_/ \___//_/ + /____//_/ +` diff --git a/version.go b/core/version.go similarity index 72% rename from version.go rename to core/version.go index ab2da9ef..d2722d7b 100644 --- a/version.go +++ b/core/version.go @@ -1,6 +1,10 @@ -package main +package core -import "fmt" +import ( + "fmt" + "runtime" + "time" +) // const vars related to the app version const ( @@ -9,17 +13,22 @@ const ( PrereleaseBlurb = "This version is pre-release and is not inteded to be used as a production ready trading framework or bot - use at your own risk." IsRelease = false - Copyright = "Copyright (c) 2019 The GoCryptoTrader Developers." GitHub = "GitHub: https://github.com/thrasher-corp/gocryptotrader" Trello = "Trello: https://trello.com/b/ZAhMhpOy/gocryptotrader" Slack = "Slack: https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk" Issues = "Issues: https://github.com/thrasher-corp/gocryptotrader/issues" ) -// BuildVersion returns the version string -func BuildVersion(short bool) string { - versionStr := fmt.Sprintf("GoCryptoTrader v%s.%s", - MajorVersion, MinorVersion) +// vars related to the app version +var ( + Copyright = fmt.Sprintf("Copyright (c) 2014-%d The GoCryptoTrader Developers.", + time.Now().Year()) +) + +// Version returns the version string +func Version(short bool) string { + versionStr := fmt.Sprintf("GoCryptoTrader v%s.%s %s %s", + MajorVersion, MinorVersion, runtime.GOARCH, runtime.Version()) if !IsRelease { versionStr += " pre-release.\n" if !short { diff --git a/currency/README.md b/currency/README.md index 90d66ea3..a7a75f08 100644 --- a/currency/README.md +++ b/currency/README.md @@ -46,4 +46,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/currency/code.go b/currency/code.go index eb3913a1..eadae293 100644 --- a/currency/code.go +++ b/currency/code.go @@ -1,37 +1,18 @@ package currency import ( + "encoding/json" "errors" "fmt" - "sync" - "time" + "strings" "github.com/thrasher-corp/gocryptotrader/common" ) -// Bitmasks const for currency rolls -const ( - Unset Role = 0 - Fiat Role = 1 << (iota - 1) - Cryptocurrency - Token - Contract - - UnsetRollString = "roleUnset" - FiatCurrencyString = "fiatCurrency" - CryptocurrencyString = "cryptocurrency" - TokenString = "token" - ContractString = "contract" -) - -// Role defines a bitmask for the full currency rolls either; fiat, -// cryptocurrency, token, or contract -type Role uint8 - func (r Role) String() string { switch r { case Unset: - return UnsetRollString + return UnsetRoleString case Fiat: return FiatCurrencyString case Cryptocurrency: @@ -45,21 +26,21 @@ func (r Role) String() string { } } -// MarshalJSON conforms Roll to the marshaler interface +// MarshalJSON conforms Role to the marshaller interface func (r Role) MarshalJSON() ([]byte, error) { - return common.JSONEncode(r.String()) + return json.Marshal(r.String()) } -// UnmarshalJSON conforms Roll to the unmarshaller interface +// UnmarshalJSON conforms Role to the unmarshaller interface func (r *Role) UnmarshalJSON(d []byte) error { var incoming string - err := common.JSONDecode(d, &incoming) + err := json.Unmarshal(d, &incoming) if err != nil { return err } switch incoming { - case UnsetRollString: + case UnsetRoleString: *r = Unset case FiatCurrencyString: *r = Fiat @@ -76,13 +57,6 @@ func (r *Role) UnmarshalJSON(d []byte) error { return nil } -// BaseCodes defines a basket of bare currency codes -type BaseCodes struct { - Items []*Item - LastMainUpdate time.Time - mtx sync.Mutex -} - // HasData returns true if the type contains data func (b *BaseCodes) HasData() bool { b.mtx.Lock() @@ -106,7 +80,7 @@ func (b *BaseCodes) GetFullCurrencyData() (File, error) { case Contract: file.Contracts = append(file.Contracts, *i) default: - return file, errors.New("roll undefined") + return file, errors.New("role undefined") } } @@ -294,8 +268,8 @@ func (b *BaseCodes) UpdateContract(fullName, symbol, assocExchange string) error // Register registers a currency from a string and returns a currency code func (b *BaseCodes) Register(c string) Code { - NewUpperCode := common.StringToUpper(c) - format := common.StringContains(c, NewUpperCode) + NewUpperCode := strings.ToUpper(c) + format := strings.Contains(c, NewUpperCode) b.mtx.Lock() defer b.mtx.Unlock() @@ -321,7 +295,7 @@ func (b *BaseCodes) Register(c string) Code { // RegisterFiat registers a fiat currency from a string and returns a currency // code func (b *BaseCodes) RegisterFiat(c string) (Code, error) { - c = common.StringToUpper(c) + c = strings.ToUpper(c) b.mtx.Lock() defer b.mtx.Unlock() @@ -395,24 +369,6 @@ func NewCode(c string) Code { return storage.ValidateCode(c) } -// Code defines an ISO 4217 fiat currency or unofficial cryptocurrency code -// string -type Code struct { - Item *Item - UpperCase bool -} - -// Item defines a sub type containing the main attributes of a designated -// currency code pointer -type Item struct { - ID int `json:"id"` - FullName string `json:"fullName"` - Symbol string `json:"symbol"` - Role Role `json:"role"` - AssocChain string `json:"associatedBlockchain"` - AssocExchange []string `json:"associatedExchanges"` -} - // String conforms to the stringer interface func (i *Item) String() string { return i.FullName @@ -427,7 +383,7 @@ func (c Code) String() string { if c.UpperCase { return c.Item.Symbol } - return common.StringToLower(c.Item.Symbol) + return strings.ToLower(c.Item.Symbol) } // Lower converts the code to lowercase formatting @@ -445,7 +401,7 @@ func (c Code) Upper() Code { // UnmarshalJSON comforms type to the umarshaler interface func (c *Code) UnmarshalJSON(d []byte) error { var newcode string - err := common.JSONDecode(d, &newcode) + err := json.Unmarshal(d, &newcode) if err != nil { return err } @@ -456,9 +412,9 @@ func (c *Code) UnmarshalJSON(d []byte) error { // MarshalJSON conforms type to the marshaler interface func (c Code) MarshalJSON() ([]byte, error) { if c.Item == nil { - return common.JSONEncode("") + return json.Marshal("") } - return common.JSONEncode(c.String()) + return json.Marshal(c.String()) } // IsEmpty returns true if the code is empty @@ -495,1608 +451,3 @@ func (c Code) IsFiatCurrency() bool { func (c Code) IsCryptocurrency() bool { return storage.IsCryptocurrency(c) } - -// Const declarations for individual currencies/tokens/fiat -// An ever growing list. Cares not for equivalence, just is -var ( - BTC = NewCode("BTC") - LTC = NewCode("LTC") - ETH = NewCode("ETH") - XRP = NewCode("XRP") - BCH = NewCode("BCH") - EOS = NewCode("EOS") - XLM = NewCode("XLM") - USDT = NewCode("USDT") - ADA = NewCode("ADA") - XMR = NewCode("XMR") - TRX = NewCode("TRX") - MIOTA = NewCode("MIOTA") - DASH = NewCode("DASH") - BNB = NewCode("BNB") - NEO = NewCode("NEO") - ETC = NewCode("ETC") - XEM = NewCode("XEM") - XTZ = NewCode("XTZ") - VET = NewCode("VET") - DOGE = NewCode("DOGE") - ZEC = NewCode("ZEC") - OMG = NewCode("OMG") - BTG = NewCode("BTG") - MKR = NewCode("MKR") - BCN = NewCode("BCN") - ONT = NewCode("ONT") - ZRX = NewCode("ZRX") - LSK = NewCode("LSK") - DCR = NewCode("DCR") - QTUM = NewCode("QTUM") - BCD = NewCode("BCD") - BTS = NewCode("BTS") - NANO = NewCode("NANO") - ZIL = NewCode("ZIL") - SC = NewCode("SC") - DGB = NewCode("DGB") - ICX = NewCode("ICX") - STEEM = NewCode("STEEM") - AE = NewCode("AE") - XVG = NewCode("XVG") - WAVES = NewCode("WAVES") - NPXS = NewCode("NPXS") - ETN = NewCode("ETN") - BTM = NewCode("BTM") - BAT = NewCode("BAT") - ETP = NewCode("ETP") - HOT = NewCode("HOT") - STRAT = NewCode("STRAT") // nolint: misspell - GNT = NewCode("GNT") - REP = NewCode("REP") - SNT = NewCode("SNT") - PPT = NewCode("PPT") - KMD = NewCode("KMD") - TUSD = NewCode("TUSD") - CNX = NewCode("CNX") - LINK = NewCode("LINK") - WTC = NewCode("WTC") - ARDR = NewCode("ARDR") - WAN = NewCode("WAN") - MITH = NewCode("MITH") - RDD = NewCode("RDD") - IOST = NewCode("IOST") - IOT = NewCode("IOT") - KCS = NewCode("KCS") - MAID = NewCode("MAID") - XET = NewCode("XET") - MOAC = NewCode("MOAC") - HC = NewCode("HC") - AION = NewCode("AION") - AOA = NewCode("AOA") - HT = NewCode("HT") - ELF = NewCode("ELF") - LRC = NewCode("LRC") - BNT = NewCode("BNT") - CMT = NewCode("CMT") - DGD = NewCode("DGD") - DCN = NewCode("DCN") - FUN = NewCode("FUN") - GXS = NewCode("GXS") - DROP = NewCode("DROP") - MANA = NewCode("MANA") - PAY = NewCode("PAY") - MCO = NewCode("MCO") - THETA = NewCode("THETA") - NXT = NewCode("NXT") - NOAH = NewCode("NOAH") - LOOM = NewCode("LOOM") - POWR = NewCode("POWR") - WAX = NewCode("WAX") - ELA = NewCode("ELA") - PIVX = NewCode("PIVX") - XIN = NewCode("XIN") - DAI = NewCode("DAI") - BTCP = NewCode("BTCP") - NEXO = NewCode("NEXO") - XBT = NewCode("XBT") - SAN = NewCode("SAN") - GAS = NewCode("GAS") - BCC = NewCode("BCC") - HCC = NewCode("HCC") - OAX = NewCode("OAX") - DNT = NewCode("DNT") - ICN = NewCode("ICN") - LLT = NewCode("LLT") - YOYO = NewCode("YOYO") - SNGLS = NewCode("SNGLS") - BQX = NewCode("BQX") - KNC = NewCode("KNC") - SNM = NewCode("SNM") - CTR = NewCode("CTR") - SALT = NewCode("SALT") - MDA = NewCode("MDA") - IOTA = NewCode("IOTA") - SUB = NewCode("SUB") - MTL = NewCode("MTL") - MTH = NewCode("MTH") - ENG = NewCode("ENG") - AST = NewCode("AST") - CLN = NewCode("CLN") - EDG = NewCode("EDG") - FIRST = NewCode("1ST") - GOLOS = NewCode("GOLOS") - ANT = NewCode("ANT") - GBG = NewCode("GBG") - HMQ = NewCode("HMQ") - INCNT = NewCode("INCNT") - ACE = NewCode("ACE") - ACT = NewCode("ACT") - AAC = NewCode("AAC") - AIDOC = NewCode("AIDOC") - SOC = NewCode("SOC") - ATL = NewCode("ATL") - AVT = NewCode("AVT") - BKX = NewCode("BKX") - BEC = NewCode("BEC") - VEE = NewCode("VEE") - PTOY = NewCode("PTOY") - CAG = NewCode("CAG") - CIC = NewCode("CIC") - CBT = NewCode("CBT") - CAN = NewCode("CAN") - DAT = NewCode("DAT") - DNA = NewCode("DNA") - INT = NewCode("INT") - IPC = NewCode("IPC") - ILA = NewCode("ILA") - LIGHT = NewCode("LIGHT") - MAG = NewCode("MAG") - AMM = NewCode("AMM") - MOF = NewCode("MOF") - MGC = NewCode("MGC") - OF = NewCode("OF") - LA = NewCode("LA") - LEV = NewCode("LEV") - NGC = NewCode("NGC") - OKB = NewCode("OKB") - MOT = NewCode("MOT") - PRA = NewCode("PRA") - R = NewCode("R") - SSC = NewCode("SSC") - SHOW = NewCode("SHOW") - SPF = NewCode("SPF") - SNC = NewCode("SNC") - SWFTC = NewCode("SWFTC") - TRA = NewCode("TRA") - TOPC = NewCode("TOPC") - TRIO = NewCode("TRIO") - QVT = NewCode("QVT") - UCT = NewCode("UCT") - UKG = NewCode("UKG") - UTK = NewCode("UTK") - VIU = NewCode("VIU") - WFEE = NewCode("WFEE") - WRC = NewCode("WRC") - UGC = NewCode("UGC") - YEE = NewCode("YEE") - YOYOW = NewCode("YOYOW") - ZIP = NewCode("ZIP") - READ = NewCode("READ") - RCT = NewCode("RCT") - REF = NewCode("REF") - XUC = NewCode("XUC") - FAIR = NewCode("FAIR") - GSC = NewCode("GSC") - HMC = NewCode("HMC") - PLU = NewCode("PLU") - PRO = NewCode("PRO") - QRL = NewCode("QRL") - REN = NewCode("REN") - ROUND = NewCode("ROUND") - SRN = NewCode("SRN") - XID = NewCode("XID") - SBD = NewCode("SBD") - TAAS = NewCode("TAAS") - TKN = NewCode("TKN") - VEN = NewCode("VEN") - VSL = NewCode("VSL") - TRST = NewCode("TRST") - XXX = NewCode("XXX") - IND = NewCode("IND") - LDC = NewCode("LDC") - GUP = NewCode("GUP") - MGO = NewCode("MGO") - MYST = NewCode("MYST") - NEU = NewCode("NEU") - NET = NewCode("NET") - BMC = NewCode("BMC") - BCAP = NewCode("BCAP") - TIME = NewCode("TIME") - CFI = NewCode("CFI") - EVX = NewCode("EVX") - REQ = NewCode("REQ") - VIB = NewCode("VIB") - ARK = NewCode("ARK") - MOD = NewCode("MOD") - ENJ = NewCode("ENJ") - STORJ = NewCode("STORJ") - RCN = NewCode("RCN") - NULS = NewCode("NULS") - RDN = NewCode("RDN") - DLT = NewCode("DLT") - AMB = NewCode("AMB") - BCPT = NewCode("BCPT") - ARN = NewCode("ARN") - GVT = NewCode("GVT") - CDT = NewCode("CDT") - POE = NewCode("POE") - QSP = NewCode("QSP") - XZC = NewCode("XZC") - TNT = NewCode("TNT") - FUEL = NewCode("FUEL") - ADX = NewCode("ADX") - CND = NewCode("CND") - LEND = NewCode("LEND") - WABI = NewCode("WABI") - SBTC = NewCode("SBTC") - BCX = NewCode("BCX") - TNB = NewCode("TNB") - GTO = NewCode("GTO") - OST = NewCode("OST") - CVC = NewCode("CVC") - DATA = NewCode("DATA") - ETF = NewCode("ETF") - BRD = NewCode("BRD") - NEBL = NewCode("NEBL") - VIBE = NewCode("VIBE") - LUN = NewCode("LUN") - CHAT = NewCode("CHAT") - RLC = NewCode("RLC") - INS = NewCode("INS") - VIA = NewCode("VIA") - BLZ = NewCode("BLZ") - SYS = NewCode("SYS") - NCASH = NewCode("NCASH") - POA = NewCode("POA") - STORM = NewCode("STORM") - WPR = NewCode("WPR") - QLC = NewCode("QLC") - GRS = NewCode("GRS") - CLOAK = NewCode("CLOAK") - ZEN = NewCode("ZEN") - SKY = NewCode("SKY") - IOTX = NewCode("IOTX") - QKC = NewCode("QKC") - AGI = NewCode("AGI") - NXS = NewCode("NXS") - EON = NewCode("EON") - KEY = NewCode("KEY") - NAS = NewCode("NAS") - ADD = NewCode("ADD") - MEETONE = NewCode("MEETONE") - ATD = NewCode("ATD") - MFT = NewCode("MFT") - EOP = NewCode("EOP") - DENT = NewCode("DENT") - IQ = NewCode("IQ") - DOCK = NewCode("DOCK") - POLY = NewCode("POLY") - VTHO = NewCode("VTHO") - ONG = NewCode("ONG") - PHX = NewCode("PHX") - GO = NewCode("GO") - PAX = NewCode("PAX") - EDO = NewCode("EDO") - WINGS = NewCode("WINGS") - NAV = NewCode("NAV") - TRIG = NewCode("TRIG") - APPC = NewCode("APPC") - KRW = NewCode("KRW") - HSR = NewCode("HSR") - ETHOS = NewCode("ETHOS") - CTXC = NewCode("CTXC") - ITC = NewCode("ITC") - TRUE = NewCode("TRUE") - ABT = NewCode("ABT") - RNT = NewCode("RNT") - PLY = NewCode("PLY") - PST = NewCode("PST") - KICK = NewCode("KICK") - BTCZ = NewCode("BTCZ") - DXT = NewCode("DXT") - STQ = NewCode("STQ") - INK = NewCode("INK") - HBZ = NewCode("HBZ") - USDT_ETH = NewCode("USDT_ETH") // nolint: stylecheck, golint - QTUM_ETH = NewCode("QTUM_ETH") // nolint: stylecheck - BTM_ETH = NewCode("BTM_ETH") // nolint: stylecheck, golint - FIL = NewCode("FIL") - STX = NewCode("STX") - BOT = NewCode("BOT") - VERI = NewCode("VERI") - ZSC = NewCode("ZSC") - QBT = NewCode("QBT") - MED = NewCode("MED") - QASH = NewCode("QASH") - MDS = NewCode("MDS") - GOD = NewCode("GOD") - SMT = NewCode("SMT") - BTF = NewCode("BTF") - NAS_ETH = NewCode("NAS_ETH") // nolint: stylecheck, golint - TSL = NewCode("TSL") - BIFI = NewCode("BIFI") - BNTY = NewCode("BNTY") - DRGN = NewCode("DRGN") - GTC = NewCode("GTC") - MDT = NewCode("MDT") - QUN = NewCode("QUN") - GNX = NewCode("GNX") - DDD = NewCode("DDD") - BTO = NewCode("BTO") - TIO = NewCode("TIO") - OCN = NewCode("OCN") - RUFF = NewCode("RUFF") - TNC = NewCode("TNC") - SNET = NewCode("SNET") - COFI = NewCode("COFI") - ZPT = NewCode("ZPT") - JNT = NewCode("JNT") - MTN = NewCode("MTN") - GEM = NewCode("GEM") - DADI = NewCode("DADI") - RFR = NewCode("RFR") - MOBI = NewCode("MOBI") - LEDU = NewCode("LEDU") - DBC = NewCode("DBC") - MKR_OLD = NewCode("MKR_OLD") // nolint: stylecheck, golint - DPY = NewCode("DPY") - BCDN = NewCode("BCDN") - EOSDAC = NewCode("EOSDAC") // nolint: stylecheck - TIPS = NewCode("TIPS") - XMC = NewCode("XMC") - PPS = NewCode("PPS") - BOE = NewCode("BOE") - MEDX = NewCode("MEDX") - SMT_ETH = NewCode("SMT_ETH") // nolint: stylecheck, golint - CS = NewCode("CS") - MAN = NewCode("MAN") - REM = NewCode("REM") - LYM = NewCode("LYM") - INSTAR = NewCode("INSTAR") // nolint: stylecheck - BFT = NewCode("BFT") - IHT = NewCode("IHT") - SENC = NewCode("SENC") - TOMO = NewCode("TOMO") - ELEC = NewCode("ELEC") - SHIP = NewCode("SHIP") - TFD = NewCode("TFD") - HAV = NewCode("HAV") - HUR = NewCode("HUR") - LST = NewCode("LST") - LINO = NewCode("LINO") - SWTH = NewCode("SWTH") - NKN = NewCode("NKN") - SOUL = NewCode("SOUL") - GALA_NEO = NewCode("GALA_NEO") // nolint: stylecheck, golint - LRN = NewCode("LRN") - GSE = NewCode("GSE") - RATING = NewCode("RATING") - HSC = NewCode("HSC") - HIT = NewCode("HIT") - DX = NewCode("DX") - BXC = NewCode("BXC") - GARD = NewCode("GARD") - FTI = NewCode("FTI") - SOP = NewCode("SOP") - LEMO = NewCode("LEMO") - RED = NewCode("RED") - LBA = NewCode("LBA") - KAN = NewCode("KAN") - OPEN = NewCode("OPEN") - SKM = NewCode("SKM") - NBAI = NewCode("NBAI") - UPP = NewCode("UPP") - ATMI = NewCode("ATMI") - TMT = NewCode("TMT") - BBK = NewCode("BBK") - EDR = NewCode("EDR") - MET = NewCode("MET") - TCT = NewCode("TCT") - EXC = NewCode("EXC") - CNC = NewCode("CNC") - TIX = NewCode("TIX") - XTC = NewCode("XTC") - BU = NewCode("BU") - GNO = NewCode("GNO") - MLN = NewCode("MLN") - XBC = NewCode("XBC") - BTCD = NewCode("BTCD") - BURST = NewCode("BURST") - CLAM = NewCode("CLAM") - XCP = NewCode("XCP") - EMC2 = NewCode("EMC2") - EXP = NewCode("EXP") - FCT = NewCode("FCT") - GAME = NewCode("GAME") - GRC = NewCode("GRC") - HUC = NewCode("HUC") - LBC = NewCode("LBC") - NMC = NewCode("NMC") - NEOS = NewCode("NEOS") - OMNI = NewCode("OMNI") - PASC = NewCode("PASC") - PPC = NewCode("PPC") - DSH = NewCode("DSH") - GML = NewCode("GML") - GSY = NewCode("GSY") - POT = NewCode("POT") - XPM = NewCode("XPM") - AMP = NewCode("AMP") - VRC = NewCode("VRC") - VTC = NewCode("VTC") - ZERO07 = NewCode("007") - BIT16 = NewCode("BIT16") - TWO015 = NewCode("2015") - TWO56 = NewCode("256") - TWOBACCO = NewCode("2BACCO") - TWOGIVE = NewCode("2GIVE") - THIRTY2BIT = NewCode("32BIT") - THREE65 = NewCode("365") - FOUR04 = NewCode("404") - SEVEN00 = NewCode("700") - EIGHTBIT = NewCode("8BIT") - ACLR = NewCode("ACLR") - ACES = NewCode("ACES") - ACPR = NewCode("ACPR") - ACID = NewCode("ACID") - ACOIN = NewCode("ACOIN") - ACRN = NewCode("ACRN") - ADAM = NewCode("ADAM") - ADT = NewCode("ADT") - AIB = NewCode("AIB") - ADZ = NewCode("ADZ") - AECC = NewCode("AECC") - AM = NewCode("AM") - AGRI = NewCode("AGRI") - AGT = NewCode("AGT") - AIR = NewCode("AIR") - ALEX = NewCode("ALEX") - AUM = NewCode("AUM") - ALIEN = NewCode("ALIEN") - ALIS = NewCode("ALIS") - ALL = NewCode("ALL") - ASAFE = NewCode("ASAFE") - AMBER = NewCode("AMBER") - AMS = NewCode("AMS") - ANAL = NewCode("ANAL") - ACP = NewCode("ACP") - ANI = NewCode("ANI") - ANTI = NewCode("ANTI") - ALTC = NewCode("ALTC") - APT = NewCode("APT") - ARCO = NewCode("ARCO") - ALC = NewCode("ALC") - ARB = NewCode("ARB") - ARCT = NewCode("ARCT") - ARCX = NewCode("ARCX") - ARGUS = NewCode("ARGUS") - ARH = NewCode("ARH") - ARM = NewCode("ARM") - ARNA = NewCode("ARNA") - ARPA = NewCode("ARPA") - ARTA = NewCode("ARTA") - ABY = NewCode("ABY") - ARTC = NewCode("ARTC") - AL = NewCode("AL") - ASN = NewCode("ASN") - ADCN = NewCode("ADCN") - ATB = NewCode("ATB") - ATM = NewCode("ATM") - ATMCHA = NewCode("ATMCHA") - ATOM = NewCode("ATOM") - ADC = NewCode("ADC") - ARE = NewCode("ARE") - AUR = NewCode("AUR") - AV = NewCode("AV") - AXIOM = NewCode("AXIOM") - B2B = NewCode("B2B") - B2 = NewCode("B2") - B3 = NewCode("B3") - BAB = NewCode("BAB") - BAN = NewCode("BAN") - BamitCoin = NewCode("BamitCoin") - NANAS = NewCode("NANAS") - BBCC = NewCode("BBCC") - BTA = NewCode("BTA") - BSTK = NewCode("BSTK") - BATL = NewCode("BATL") - BBH = NewCode("BBH") - BITB = NewCode("BITB") - BRDD = NewCode("BRDD") - XBTS = NewCode("XBTS") - BVC = NewCode("BVC") - CHATX = NewCode("CHATX") - BEEP = NewCode("BEEP") - BEEZ = NewCode("BEEZ") - BENJI = NewCode("BENJI") - BERN = NewCode("BERN") - PROFIT = NewCode("PROFIT") - BEST = NewCode("BEST") - BGF = NewCode("BGF") - BIGUP = NewCode("BIGUP") - BLRY = NewCode("BLRY") - BILL = NewCode("BILL") - BIOB = NewCode("BIOB") - BIO = NewCode("BIO") - BIOS = NewCode("BIOS") - BPTN = NewCode("BPTN") - BTCA = NewCode("BTCA") - BA = NewCode("BA") - BAC = NewCode("BAC") - BBT = NewCode("BBT") - BOSS = NewCode("BOSS") - BRONZ = NewCode("BRONZ") - CAT = NewCode("CAT") - BTD = NewCode("BTD") - XBTC21 = NewCode("XBTC21") - BCA = NewCode("BCA") - BCP = NewCode("BCP") - BTDOLL = NewCode("BTDOLL") - LIZA = NewCode("LIZA") - BTCRED = NewCode("BTCRED") - BTCS = NewCode("BTCS") - BTU = NewCode("BTU") - BUM = NewCode("BUM") - LITE = NewCode("LITE") - BCM = NewCode("BCM") - BCS = NewCode("BCS") - BTCU = NewCode("BTCU") - BM = NewCode("BM") - BTCRY = NewCode("BTCRY") - BTCR = NewCode("BTCR") - HIRE = NewCode("HIRE") - STU = NewCode("STU") - BITOK = NewCode("BITOK") - BITON = NewCode("BITON") - BPC = NewCode("BPC") - BPOK = NewCode("BPOK") - BTP = NewCode("BTP") - BITCNY = NewCode("bitCNY") - RNTB = NewCode("RNTB") - BSH = NewCode("BSH") - XBS = NewCode("XBS") - BITS = NewCode("BITS") - BST = NewCode("BST") - BXT = NewCode("BXT") - VEG = NewCode("VEG") - VOLT = NewCode("VOLT") - BTV = NewCode("BTV") - BITZ = NewCode("BITZ") - BTZ = NewCode("BTZ") - BHC = NewCode("BHC") - BDC = NewCode("BDC") - JACK = NewCode("JACK") - BS = NewCode("BS") - BSTAR = NewCode("BSTAR") - BLAZR = NewCode("BLAZR") - BOD = NewCode("BOD") - BLUE = NewCode("BLUE") - BLU = NewCode("BLU") - BLUS = NewCode("BLUS") - BMT = NewCode("BMT") - BOLI = NewCode("BOLI") - BOMB = NewCode("BOMB") - BON = NewCode("BON") - BOOM = NewCode("BOOM") - BOSON = NewCode("BOSON") - BSC = NewCode("BSC") - BRH = NewCode("BRH") - BRAIN = NewCode("BRAIN") - BRE = NewCode("BRE") - BTCM = NewCode("BTCM") - BTCO = NewCode("BTCO") - TALK = NewCode("TALK") - BUB = NewCode("BUB") - BUY = NewCode("BUY") - BUZZ = NewCode("BUZZ") - BTH = NewCode("BTH") - C0C0 = NewCode("C0C0") - CAB = NewCode("CAB") - CF = NewCode("CF") - CLO = NewCode("CLO") - CAM = NewCode("CAM") - CD = NewCode("CD") - CANN = NewCode("CANN") - CNNC = NewCode("CNNC") - CPC = NewCode("CPC") - CST = NewCode("CST") - CAPT = NewCode("CAPT") - CARBON = NewCode("CARBON") - CME = NewCode("CME") - CTK = NewCode("CTK") - CBD = NewCode("CBD") - CCC = NewCode("CCC") - CNT = NewCode("CNT") - XCE = NewCode("XCE") - CHRG = NewCode("CHRG") - CHEMX = NewCode("CHEMX") - CHESS = NewCode("CHESS") - CKS = NewCode("CKS") - CHILL = NewCode("CHILL") - CHIP = NewCode("CHIP") - CHOOF = NewCode("CHOOF") - CRX = NewCode("CRX") - CIN = NewCode("CIN") - POLL = NewCode("POLL") - CLICK = NewCode("CLICK") - CLINT = NewCode("CLINT") - CLUB = NewCode("CLUB") - CLUD = NewCode("CLUD") - COX = NewCode("COX") - COXST = NewCode("COXST") - CFC = NewCode("CFC") - CTIC2 = NewCode("CTIC2") - COIN = NewCode("COIN") - BTTF = NewCode("BTTF") - C2 = NewCode("C2") - CAID = NewCode("CAID") - CL = NewCode("CL") - CTIC = NewCode("CTIC") - CXT = NewCode("CXT") - CHP = NewCode("CHP") - CV2 = NewCode("CV2") - COC = NewCode("COC") - COMP = NewCode("COMP") - CMS = NewCode("CMS") - CONX = NewCode("CONX") - CCX = NewCode("CCX") - CLR = NewCode("CLR") - CORAL = NewCode("CORAL") - CORG = NewCode("CORG") - CSMIC = NewCode("CSMIC") - CMC = NewCode("CMC") - COV = NewCode("COV") - COVX = NewCode("COVX") - CRAB = NewCode("CRAB") - CRAFT = NewCode("CRAFT") - CRNK = NewCode("CRNK") - CRAVE = NewCode("CRAVE") - CRM = NewCode("CRM") - XCRE = NewCode("XCRE") - CREDIT = NewCode("CREDIT") - CREVA = NewCode("CREVA") - CRIME = NewCode("CRIME") - CROC = NewCode("CROC") - CRC = NewCode("CRC") - CRW = NewCode("CRW") - CRY = NewCode("CRY") - CBX = NewCode("CBX") - TKTX = NewCode("TKTX") - CB = NewCode("CB") - CIRC = NewCode("CIRC") - CCB = NewCode("CCB") - CDO = NewCode("CDO") - CG = NewCode("CG") - CJ = NewCode("CJ") - CJC = NewCode("CJC") - CYT = NewCode("CYT") - CRPS = NewCode("CRPS") - PING = NewCode("PING") - CWXT = NewCode("CWXT") - CCT = NewCode("CCT") - CTL = NewCode("CTL") - CURVES = NewCode("CURVES") - CC = NewCode("CC") - CYC = NewCode("CYC") - CYG = NewCode("CYG") - CYP = NewCode("CYP") - FUNK = NewCode("FUNK") - CZECO = NewCode("CZECO") - DALC = NewCode("DALC") - DLISK = NewCode("DLISK") - MOOND = NewCode("MOOND") - DB = NewCode("DB") - DCC = NewCode("DCC") - DCYP = NewCode("DCYP") - DETH = NewCode("DETH") - DKC = NewCode("DKC") - DISK = NewCode("DISK") - DRKT = NewCode("DRKT") - DTT = NewCode("DTT") - DASHS = NewCode("DASHS") - DBTC = NewCode("DBTC") - DCT = NewCode("DCT") - DBET = NewCode("DBET") - DEC = NewCode("DEC") - DECR = NewCode("DECR") - DEA = NewCode("DEA") - DPAY = NewCode("DPAY") - DCRE = NewCode("DCRE") - DC = NewCode("DC") - DES = NewCode("DES") - DEM = NewCode("DEM") - DXC = NewCode("DXC") - DCK = NewCode("DCK") - CUBE = NewCode("CUBE") - DGMS = NewCode("DGMS") - DBG = NewCode("DBG") - DGCS = NewCode("DGCS") - DBLK = NewCode("DBLK") - DIME = NewCode("DIME") - DIRT = NewCode("DIRT") - DVD = NewCode("DVD") - DMT = NewCode("DMT") - NOTE = NewCode("NOTE") - DGORE = NewCode("DGORE") - DLC = NewCode("DLC") - DRT = NewCode("DRT") - DOTA = NewCode("DOTA") - DOX = NewCode("DOX") - DRA = NewCode("DRA") - DFT = NewCode("DFT") - XDB = NewCode("XDB") - DRM = NewCode("DRM") - DRZ = NewCode("DRZ") - DRACO = NewCode("DRACO") - DBIC = NewCode("DBIC") - DUB = NewCode("DUB") - GUM = NewCode("GUM") - DUR = NewCode("DUR") - DUST = NewCode("DUST") - DUX = NewCode("DUX") - DXO = NewCode("DXO") - ECN = NewCode("ECN") - EDR2 = NewCode("EDR2") - EA = NewCode("EA") - EAGS = NewCode("EAGS") - EMT = NewCode("EMT") - EBONUS = NewCode("EBONUS") - ECCHI = NewCode("ECCHI") - EKO = NewCode("EKO") - ECLI = NewCode("ECLI") - ECOB = NewCode("ECOB") - ECO = NewCode("ECO") - EDIT = NewCode("EDIT") - EDRC = NewCode("EDRC") - EDC = NewCode("EDC") - EGAME = NewCode("EGAME") - EGG = NewCode("EGG") - EGO = NewCode("EGO") - ELC = NewCode("ELC") - ELCO = NewCode("ELCO") - ECA = NewCode("ECA") - EPC = NewCode("EPC") - ELE = NewCode("ELE") - ONE337 = NewCode("1337") - EMB = NewCode("EMB") - EMC = NewCode("EMC") - EPY = NewCode("EPY") - EMPC = NewCode("EMPC") - EMP = NewCode("EMP") - ENE = NewCode("ENE") - EET = NewCode("EET") - XNG = NewCode("XNG") - EGMA = NewCode("EGMA") - ENTER = NewCode("ENTER") - ETRUST = NewCode("ETRUST") - EQL = NewCode("EQL") - EQM = NewCode("EQM") - EQT = NewCode("EQT") - ERR = NewCode("ERR") - ESC = NewCode("ESC") - ESP = NewCode("ESP") - ENT = NewCode("ENT") - ETCO = NewCode("ETCO") - DOGETH = NewCode("DOGETH") - ECASH = NewCode("ECASH") - ELITE = NewCode("ELITE") - ETHS = NewCode("ETHS") - ETL = NewCode("ETL") - ETZ = NewCode("ETZ") - EUC = NewCode("EUC") - EURC = NewCode("EURC") - EUROPE = NewCode("EUROPE") - EVA = NewCode("EVA") - EGC = NewCode("EGC") - EOC = NewCode("EOC") - EVIL = NewCode("EVIL") - EVO = NewCode("EVO") - EXB = NewCode("EXB") - EXIT = NewCode("EXIT") - XT = NewCode("XT") - F16 = NewCode("F16") - FADE = NewCode("FADE") - FAZZ = NewCode("FAZZ") - FX = NewCode("FX") - FIDEL = NewCode("FIDEL") - FIDGT = NewCode("FIDGT") - FIND = NewCode("FIND") - FPC = NewCode("FPC") - FIRE = NewCode("FIRE") - FFC = NewCode("FFC") - FRST = NewCode("FRST") - FIST = NewCode("FIST") - FIT = NewCode("FIT") - FLX = NewCode("FLX") - FLVR = NewCode("FLVR") - FLY = NewCode("FLY") - FONZ = NewCode("FONZ") - XFCX = NewCode("XFCX") - FOREX = NewCode("FOREX") - FRN = NewCode("FRN") - FRK = NewCode("FRK") - FRWC = NewCode("FRWC") - FGZ = NewCode("FGZ") - FRE = NewCode("FRE") - FRDC = NewCode("FRDC") - FJC = NewCode("FJC") - FURY = NewCode("FURY") - FSN = NewCode("FSN") - FCASH = NewCode("FCASH") - FTO = NewCode("FTO") - FUZZ = NewCode("FUZZ") - GAKH = NewCode("GAKH") - GBT = NewCode("GBT") - UNITS = NewCode("UNITS") - FOUR20G = NewCode("420G") - GENIUS = NewCode("GENIUS") - GEN = NewCode("GEN") - GEO = NewCode("GEO") - GER = NewCode("GER") - GSR = NewCode("GSR") - SPKTR = NewCode("SPKTR") - GIFT = NewCode("GIFT") - WTT = NewCode("WTT") - GHS = NewCode("GHS") - GIG = NewCode("GIG") - GOT = NewCode("GOT") - XGTC = NewCode("XGTC") - GIZ = NewCode("GIZ") - GLO = NewCode("GLO") - GCR = NewCode("GCR") - BSTY = NewCode("BSTY") - GLC = NewCode("GLC") - GSX = NewCode("GSX") - GOAT = NewCode("GOAT") - GB = NewCode("GB") - GFL = NewCode("GFL") - MNTP = NewCode("MNTP") - GP = NewCode("GP") - GLUCK = NewCode("GLUCK") - GOON = NewCode("GOON") - GTFO = NewCode("GTFO") - GOTX = NewCode("GOTX") - GPU = NewCode("GPU") - GRF = NewCode("GRF") - GRAM = NewCode("GRAM") - GRAV = NewCode("GRAV") - GBIT = NewCode("GBIT") - GREED = NewCode("GREED") - GE = NewCode("GE") - GREENF = NewCode("GREENF") - GRE = NewCode("GRE") - GREXIT = NewCode("GREXIT") - GMCX = NewCode("GMCX") - GROW = NewCode("GROW") - GSM = NewCode("GSM") - GT = NewCode("GT") - NLG = NewCode("NLG") - HKN = NewCode("HKN") - HAC = NewCode("HAC") - HALLO = NewCode("HALLO") - HAMS = NewCode("HAMS") - HPC = NewCode("HPC") - HAWK = NewCode("HAWK") - HAZE = NewCode("HAZE") - HZT = NewCode("HZT") - HDG = NewCode("HDG") - HEDG = NewCode("HEDG") - HEEL = NewCode("HEEL") - HMP = NewCode("HMP") - PLAY = NewCode("PLAY") - HXX = NewCode("HXX") - XHI = NewCode("XHI") - HVCO = NewCode("HVCO") - HTC = NewCode("HTC") - MINH = NewCode("MINH") - HODL = NewCode("HODL") - HON = NewCode("HON") - HOPE = NewCode("HOPE") - HQX = NewCode("HQX") - HSP = NewCode("HSP") - HTML5 = NewCode("HTML5") - HYPERX = NewCode("HYPERX") - HPS = NewCode("HPS") - IOC = NewCode("IOC") - IBANK = NewCode("IBANK") - IBITS = NewCode("IBITS") - ICASH = NewCode("ICASH") - ICOB = NewCode("ICOB") - ICON = NewCode("ICON") - IETH = NewCode("IETH") - ILM = NewCode("ILM") - IMPS = NewCode("IMPS") - NKA = NewCode("NKA") - INCP = NewCode("INCP") - IN = NewCode("IN") - INC = NewCode("INC") - IMS = NewCode("IMS") - IFLT = NewCode("IFLT") - INFX = NewCode("INFX") - INGT = NewCode("INGT") - INPAY = NewCode("INPAY") - INSANE = NewCode("INSANE") - INXT = NewCode("INXT") - IFT = NewCode("IFT") - INV = NewCode("INV") - IVZ = NewCode("IVZ") - ILT = NewCode("ILT") - IONX = NewCode("IONX") - ISL = NewCode("ISL") - ITI = NewCode("ITI") - ING = NewCode("ING") - IEC = NewCode("IEC") - IW = NewCode("IW") - IXC = NewCode("IXC") - IXT = NewCode("IXT") - JPC = NewCode("JPC") - JANE = NewCode("JANE") - JWL = NewCode("JWL") - JIF = NewCode("JIF") - JOBS = NewCode("JOBS") - JOCKER = NewCode("JOCKER") - JW = NewCode("JW") - JOK = NewCode("JOK") - XJO = NewCode("XJO") - KGB = NewCode("KGB") - KARMC = NewCode("KARMC") - KARMA = NewCode("KARMA") - KASHH = NewCode("KASHH") - KAT = NewCode("KAT") - KC = NewCode("KC") - KIDS = NewCode("KIDS") - KIN = NewCode("KIN") - KISS = NewCode("KISS") - KOBO = NewCode("KOBO") - TP1 = NewCode("TP1") - KRAK = NewCode("KRAK") - KGC = NewCode("KGC") - KTK = NewCode("KTK") - KR = NewCode("KR") - KUBO = NewCode("KUBO") - KURT = NewCode("KURT") - KUSH = NewCode("KUSH") - LANA = NewCode("LANA") - LTH = NewCode("LTH") - LAZ = NewCode("LAZ") - LEA = NewCode("LEA") - LEAF = NewCode("LEAF") - LENIN = NewCode("LENIN") - LEPEN = NewCode("LEPEN") - LIR = NewCode("LIR") - LVG = NewCode("LVG") - LGBTQ = NewCode("LGBTQ") - LHC = NewCode("LHC") - EXT = NewCode("EXT") - LBTC = NewCode("LBTC") - LSD = NewCode("LSD") - LIMX = NewCode("LIMX") - LTD = NewCode("LTD") - LINDA = NewCode("LINDA") - LKC = NewCode("LKC") - LBTCX = NewCode("LBTCX") - LCC = NewCode("LCC") - LTCU = NewCode("LTCU") - LTCR = NewCode("LTCR") - LDOGE = NewCode("LDOGE") - LTS = NewCode("LTS") - LIV = NewCode("LIV") - LIZI = NewCode("LIZI") - LOC = NewCode("LOC") - LOCX = NewCode("LOCX") - LOOK = NewCode("LOOK") - LOOT = NewCode("LOOT") - XLTCG = NewCode("XLTCG") - BASH = NewCode("BASH") - LUCKY = NewCode("LUCKY") - L7S = NewCode("L7S") - LDM = NewCode("LDM") - LUMI = NewCode("LUMI") - LUNA = NewCode("LUNA") - LC = NewCode("LC") - LUX = NewCode("LUX") - MCRN = NewCode("MCRN") - XMG = NewCode("XMG") - MMXIV = NewCode("MMXIV") - MAT = NewCode("MAT") - MAO = NewCode("MAO") - MAPC = NewCode("MAPC") - MRB = NewCode("MRB") - MXT = NewCode("MXT") - MARV = NewCode("MARV") - MARX = NewCode("MARX") - MCAR = NewCode("MCAR") - MM = NewCode("MM") - MVC = NewCode("MVC") - MAVRO = NewCode("MAVRO") - MAX = NewCode("MAX") - MAZE = NewCode("MAZE") - MBIT = NewCode("MBIT") - MCOIN = NewCode("MCOIN") - MPRO = NewCode("MPRO") - XMS = NewCode("XMS") - MLITE = NewCode("MLITE") - MLNC = NewCode("MLNC") - MENTAL = NewCode("MENTAL") - MERGEC = NewCode("MERGEC") - MTLMC3 = NewCode("MTLMC3") - METAL = NewCode("METAL") - MUU = NewCode("MUU") - MILO = NewCode("MILO") - MND = NewCode("MND") - XMINE = NewCode("XMINE") - MNM = NewCode("MNM") - XNM = NewCode("XNM") - MIRO = NewCode("MIRO") - MIS = NewCode("MIS") - MMXVI = NewCode("MMXVI") - MOIN = NewCode("MOIN") - MOJO = NewCode("MOJO") - TAB = NewCode("TAB") - MONETA = NewCode("MONETA") - MUE = NewCode("MUE") - MONEY = NewCode("MONEY") - MRP = NewCode("MRP") - MOTO = NewCode("MOTO") - MULTI = NewCode("MULTI") - MST = NewCode("MST") - MVR = NewCode("MVR") - MYSTIC = NewCode("MYSTIC") - WISH = NewCode("WISH") - NKT = NewCode("NKT") - NAT = NewCode("NAT") - ENAU = NewCode("ENAU") - NEBU = NewCode("NEBU") - NEF = NewCode("NEF") - NBIT = NewCode("NBIT") - NETKO = NewCode("NETKO") - NTM = NewCode("NTM") - NETC = NewCode("NETC") - NRC = NewCode("NRC") - NTK = NewCode("NTK") - NTRN = NewCode("NTRN") - NEVA = NewCode("NEVA") - NIC = NewCode("NIC") - NKC = NewCode("NKC") - NYC = NewCode("NYC") - NZC = NewCode("NZC") - NICE = NewCode("NICE") - NDOGE = NewCode("NDOGE") - XTR = NewCode("XTR") - N2O = NewCode("N2O") - NIXON = NewCode("NIXON") - NOC = NewCode("NOC") - NODC = NewCode("NODC") - NODES = NewCode("NODES") - NODX = NewCode("NODX") - NLC = NewCode("NLC") - NLC2 = NewCode("NLC2") - NOO = NewCode("NOO") - NVC = NewCode("NVC") - NPC = NewCode("NPC") - NUBIS = NewCode("NUBIS") - NUKE = NewCode("NUKE") - N7 = NewCode("N7") - NUM = NewCode("NUM") - NMR = NewCode("NMR") - NXE = NewCode("NXE") - OBS = NewCode("OBS") - OCEAN = NewCode("OCEAN") - OCOW = NewCode("OCOW") - EIGHT88 = NewCode("888") - OCC = NewCode("OCC") - OK = NewCode("OK") - ODNT = NewCode("ODNT") - FLAV = NewCode("FLAV") - OLIT = NewCode("OLIT") - OLYMP = NewCode("OLYMP") - OMA = NewCode("OMA") - OMC = NewCode("OMC") - ONEK = NewCode("ONEK") - ONX = NewCode("ONX") - XPO = NewCode("XPO") - OPAL = NewCode("OPAL") - OTN = NewCode("OTN") - OP = NewCode("OP") - OPES = NewCode("OPES") - OPTION = NewCode("OPTION") - ORLY = NewCode("ORLY") - OS76 = NewCode("OS76") - OZC = NewCode("OZC") - P7C = NewCode("P7C") - PAC = NewCode("PAC") - PAK = NewCode("PAK") - PAL = NewCode("PAL") - PND = NewCode("PND") - PINKX = NewCode("PINKX") - POPPY = NewCode("POPPY") - DUO = NewCode("DUO") - PARA = NewCode("PARA") - PKB = NewCode("PKB") - GENE = NewCode("GENE") - PARTY = NewCode("PARTY") - PYN = NewCode("PYN") - XPY = NewCode("XPY") - CON = NewCode("CON") - PAYP = NewCode("PAYP") - GUESS = NewCode("GUESS") - PEN = NewCode("PEN") - PTA = NewCode("PTA") - PEO = NewCode("PEO") - PSB = NewCode("PSB") - XPD = NewCode("XPD") - PXL = NewCode("PXL") - PHR = NewCode("PHR") - PIE = NewCode("PIE") - PIO = NewCode("PIO") - PIPR = NewCode("PIPR") - SKULL = NewCode("SKULL") - PLANET = NewCode("PLANET") - PNC = NewCode("PNC") - XPTX = NewCode("XPTX") - PLNC = NewCode("PLNC") - XPS = NewCode("XPS") - POKE = NewCode("POKE") - PLBT = NewCode("PLBT") - POM = NewCode("POM") - PONZ2 = NewCode("PONZ2") - PONZI = NewCode("PONZI") - XSP = NewCode("XSP") - XPC = NewCode("XPC") - PEX = NewCode("PEX") - TRON = NewCode("TRON") - POST = NewCode("POST") - POSW = NewCode("POSW") - PWR = NewCode("PWR") - POWER = NewCode("POWER") - PRE = NewCode("PRE") - PRS = NewCode("PRS") - PXI = NewCode("PXI") - PEXT = NewCode("PEXT") - PRIMU = NewCode("PRIMU") - PRX = NewCode("PRX") - PRM = NewCode("PRM") - PRIX = NewCode("PRIX") - XPRO = NewCode("XPRO") - PCM = NewCode("PCM") - PROC = NewCode("PROC") - NANOX = NewCode("NANOX") - VRP = NewCode("VRP") - PTY = NewCode("PTY") - PSI = NewCode("PSI") - PSY = NewCode("PSY") - PULSE = NewCode("PULSE") - PUPA = NewCode("PUPA") - PURE = NewCode("PURE") - VIDZ = NewCode("VIDZ") - PUTIN = NewCode("PUTIN") - PX = NewCode("PX") - QTM = NewCode("QTM") - QTZ = NewCode("QTZ") - QBC = NewCode("QBC") - XQN = NewCode("XQN") - RBBT = NewCode("RBBT") - RAC = NewCode("RAC") - RADI = NewCode("RADI") - RAD = NewCode("RAD") - RAI = NewCode("RAI") - XRA = NewCode("XRA") - RATIO = NewCode("RATIO") - REA = NewCode("REA") - RCX = NewCode("RCX") - REE = NewCode("REE") - REC = NewCode("REC") - RMS = NewCode("RMS") - RBIT = NewCode("RBIT") - RNC = NewCode("RNC") - REV = NewCode("REV") - RH = NewCode("RH") - XRL = NewCode("XRL") - RICE = NewCode("RICE") - RICHX = NewCode("RICHX") - RID = NewCode("RID") - RIDE = NewCode("RIDE") - RBT = NewCode("RBT") - RING = NewCode("RING") - RIO = NewCode("RIO") - RISE = NewCode("RISE") - ROCKET = NewCode("ROCKET") - RPC = NewCode("RPC") - ROS = NewCode("ROS") - ROYAL = NewCode("ROYAL") - RSGP = NewCode("RSGP") - RBIES = NewCode("RBIES") - RUBIT = NewCode("RUBIT") - RBY = NewCode("RBY") - RUC = NewCode("RUC") - RUPX = NewCode("RUPX") - RUP = NewCode("RUP") - RUST = NewCode("RUST") - SFE = NewCode("SFE") - SLS = NewCode("SLS") - SMSR = NewCode("SMSR") - RONIN = NewCode("RONIN") - STV = NewCode("STV") - HIFUN = NewCode("HIFUN") - MAD = NewCode("MAD") - SANDG = NewCode("SANDG") - STO = NewCode("STO") - SCAN = NewCode("SCAN") - SCITW = NewCode("SCITW") - SCRPT = NewCode("SCRPT") - SCRT = NewCode("SCRT") - SED = NewCode("SED") - SEEDS = NewCode("SEEDS") - B2X = NewCode("B2X") - SEL = NewCode("SEL") - SLFI = NewCode("SLFI") - SMBR = NewCode("SMBR") - SEN = NewCode("SEN") - SENT = NewCode("SENT") - SRNT = NewCode("SRNT") - SEV = NewCode("SEV") - SP = NewCode("SP") - SXC = NewCode("SXC") - GELD = NewCode("GELD") - SHDW = NewCode("SHDW") - SDC = NewCode("SDC") - SAK = NewCode("SAK") - SHRP = NewCode("SHRP") - SHELL = NewCode("SHELL") - SH = NewCode("SH") - SHORTY = NewCode("SHORTY") - SHREK = NewCode("SHREK") - SHRM = NewCode("SHRM") - SIB = NewCode("SIB") - SIGT = NewCode("SIGT") - SLCO = NewCode("SLCO") - SIGU = NewCode("SIGU") - SIX = NewCode("SIX") - SJW = NewCode("SJW") - SKB = NewCode("SKB") - SW = NewCode("SW") - SLEEP = NewCode("SLEEP") - SLING = NewCode("SLING") - SMART = NewCode("SMART") - SMC = NewCode("SMC") - SMF = NewCode("SMF") - SOCC = NewCode("SOCC") - SCL = NewCode("SCL") - SDAO = NewCode("SDAO") - SOLAR = NewCode("SOLAR") - SOLO = NewCode("SOLO") - SCT = NewCode("SCT") - SONG = NewCode("SONG") - ALTCOM = NewCode("ALTCOM") - SPHTX = NewCode("SPHTX") - SPC = NewCode("SPC") - SPACE = NewCode("SPACE") - SBT = NewCode("SBT") - SPEC = NewCode("SPEC") - SPX = NewCode("SPX") - SCS = NewCode("SCS") - SPORT = NewCode("SPORT") - SPT = NewCode("SPT") - SPR = NewCode("SPR") - SPEX = NewCode("SPEX") - SQL = NewCode("SQL") - SBIT = NewCode("SBIT") - STHR = NewCode("STHR") - STALIN = NewCode("STALIN") - STAR = NewCode("STAR") - STA = NewCode("STA") - START = NewCode("START") - STP = NewCode("STP") - PNK = NewCode("PNK") - STEPS = NewCode("STEPS") - STK = NewCode("STK") - STONK = NewCode("STONK") - STS = NewCode("STS") - STRP = NewCode("STRP") - STY = NewCode("STY") - XMT = NewCode("XMT") - SSTC = NewCode("SSTC") - SUPER = NewCode("SUPER") - SRND = NewCode("SRND") - STRB = NewCode("STRB") - M1 = NewCode("M1") - SPM = NewCode("SPM") - BUCKS = NewCode("BUCKS") - TOKEN = NewCode("TOKEN") - SWT = NewCode("SWT") - SWEET = NewCode("SWEET") - SWING = NewCode("SWING") - CHSB = NewCode("CHSB") - SIC = NewCode("SIC") - SDP = NewCode("SDP") - XSY = NewCode("XSY") - SYNX = NewCode("SYNX") - SNRG = NewCode("SNRG") - TAG = NewCode("TAG") - TAGR = NewCode("TAGR") - TAJ = NewCode("TAJ") - TAK = NewCode("TAK") - TAKE = NewCode("TAKE") - TAM = NewCode("TAM") - XTO = NewCode("XTO") - TAP = NewCode("TAP") - TLE = NewCode("TLE") - TSE = NewCode("TSE") - TLEX = NewCode("TLEX") - TAXI = NewCode("TAXI") - TCN = NewCode("TCN") - TDFB = NewCode("TDFB") - TEAM = NewCode("TEAM") - TECH = NewCode("TECH") - TEC = NewCode("TEC") - TEK = NewCode("TEK") - TB = NewCode("TB") - TLX = NewCode("TLX") - TELL = NewCode("TELL") - TENNET = NewCode("TENNET") - TES = NewCode("TES") - TGS = NewCode("TGS") - XVE = NewCode("XVE") - TCR = NewCode("TCR") - GCC = NewCode("GCC") - MAY = NewCode("MAY") - THOM = NewCode("THOM") - TIA = NewCode("TIA") - TIDE = NewCode("TIDE") - TIE = NewCode("TIE") - TIT = NewCode("TIT") - TTC = NewCode("TTC") - TODAY = NewCode("TODAY") - TBX = NewCode("TBX") - TDS = NewCode("TDS") - TLOSH = NewCode("TLOSH") - TOKC = NewCode("TOKC") - TMRW = NewCode("TMRW") - TOOL = NewCode("TOOL") - TCX = NewCode("TCX") - TOT = NewCode("TOT") - TX = NewCode("TX") - TRANSF = NewCode("TRANSF") - TRAP = NewCode("TRAP") - TBCX = NewCode("TBCX") - TRICK = NewCode("TRICK") - TPG = NewCode("TPG") - TFL = NewCode("TFL") - TRUMP = NewCode("TRUMP") - TNG = NewCode("TNG") - TUR = NewCode("TUR") - TWERK = NewCode("TWERK") - TWIST = NewCode("TWIST") - TWO = NewCode("TWO") - UCASH = NewCode("UCASH") - UAE = NewCode("UAE") - XBU = NewCode("XBU") - UBQ = NewCode("UBQ") - U = NewCode("U") - UDOWN = NewCode("UDOWN") - GAIN = NewCode("GAIN") - USC = NewCode("USC") - UMC = NewCode("UMC") - UNF = NewCode("UNF") - UNIFY = NewCode("UNIFY") - USDE = NewCode("USDE") - UBTC = NewCode("UBTC") - UIS = NewCode("UIS") - UNIT = NewCode("UNIT") - UNI = NewCode("UNI") - UXC = NewCode("UXC") - URC = NewCode("URC") - XUP = NewCode("XUP") - UFR = NewCode("UFR") - URO = NewCode("URO") - UTLE = NewCode("UTLE") - VAL = NewCode("VAL") - VPRC = NewCode("VPRC") - VAPOR = NewCode("VAPOR") - VCOIN = NewCode("VCOIN") - VEC = NewCode("VEC") - VEC2 = NewCode("VEC2") - VLT = NewCode("VLT") - VENE = NewCode("VENE") - VNTX = NewCode("VNTX") - VTN = NewCode("VTN") - CRED = NewCode("CRED") - VERS = NewCode("VERS") - VTX = NewCode("VTX") - VTY = NewCode("VTY") - VIP = NewCode("VIP") - VISIO = NewCode("VISIO") - VK = NewCode("VK") - VOL = NewCode("VOL") - VOYA = NewCode("VOYA") - VPN = NewCode("VPN") - XVS = NewCode("XVS") - VTL = NewCode("VTL") - VULC = NewCode("VULC") - VVI = NewCode("VVI") - WGR = NewCode("WGR") - WAM = NewCode("WAM") - WARP = NewCode("WARP") - WASH = NewCode("WASH") - WGO = NewCode("WGO") - WAY = NewCode("WAY") - WCASH = NewCode("WCASH") - WEALTH = NewCode("WEALTH") - WEEK = NewCode("WEEK") - WHO = NewCode("WHO") - WIC = NewCode("WIC") - WBB = NewCode("WBB") - WINE = NewCode("WINE") - WINK = NewCode("WINK") - WISC = NewCode("WISC") - WITCH = NewCode("WITCH") - WMC = NewCode("WMC") - WOMEN = NewCode("WOMEN") - WOK = NewCode("WOK") - WRT = NewCode("WRT") - XCO = NewCode("XCO") - X2 = NewCode("X2") - XNX = NewCode("XNX") - XAU = NewCode("XAU") - XAV = NewCode("XAV") - XDE2 = NewCode("XDE2") - XDE = NewCode("XDE") - XIOS = NewCode("XIOS") - XOC = NewCode("XOC") - XSSX = NewCode("XSSX") - XBY = NewCode("XBY") - YAC = NewCode("YAC") - YMC = NewCode("YMC") - YAY = NewCode("YAY") - YBC = NewCode("YBC") - YES = NewCode("YES") - YOB2X = NewCode("YOB2X") - YOVI = NewCode("YOVI") - ZYD = NewCode("ZYD") - ZECD = NewCode("ZECD") - ZEIT = NewCode("ZEIT") - ZENI = NewCode("ZENI") - ZET2 = NewCode("ZET2") - ZET = NewCode("ZET") - ZMC = NewCode("ZMC") - ZIRK = NewCode("ZIRK") - ZLQ = NewCode("ZLQ") - ZNE = NewCode("ZNE") - ZONTO = NewCode("ZONTO") - ZOOM = NewCode("ZOOM") - ZRC = NewCode("ZRC") - ZUR = NewCode("ZUR") - ZB = NewCode("ZB") - QC = NewCode("QC") - HLC = NewCode("HLC") - SAFE = NewCode("SAFE") - BTN = NewCode("BTN") - CDC = NewCode("CDC") - DDM = NewCode("DDM") - HOTC = NewCode("HOTC") - BDS = NewCode("BDS") - AAA = NewCode("AAA") - XWC = NewCode("XWC") - PDX = NewCode("PDX") - SLT = NewCode("SLT") - HPY = NewCode("HPY") - XXBT = NewCode("XXBT") // BTC, but XXBT instead - XDG = NewCode("XDG") // DOGE - HKD = NewCode("HKD") // Hong Kong Dollar - AUD = NewCode("AUD") // Australian Dollar - USD = NewCode("USD") // United States Dollar - ZUSD = NewCode("ZUSD") // United States Dollar, but with a Z in front of it - EUR = NewCode("EUR") // Euro - ZEUR = NewCode("ZEUR") // Euro, but with a Z in front of it - CAD = NewCode("CAD") // Canadaian Dollar - ZCAD = NewCode("ZCAD") // Canadaian Dollar, but with a Z in front of it - SGD = NewCode("SGD") // Singapore Dollar - RUB = NewCode("RUB") // RUssian ruBle - RUR = NewCode("RUR") // RUssian Ruble - PLN = NewCode("PLN") // Polish złoty - TRY = NewCode("TRY") // Turkish lira - UAH = NewCode("UAH") // Ukrainian hryvnia - JPY = NewCode("JPY") // Japanese yen - ZJPY = NewCode("ZJPY") // Japanese yen, but with a Z in front of it - LCH = NewCode("LCH") - MYR = NewCode("MYR") - AFN = NewCode("AFN") - ARS = NewCode("ARS") - AWG = NewCode("AWG") - AZN = NewCode("AZN") - BSD = NewCode("BSD") - BBD = NewCode("BBD") - BYN = NewCode("BYN") - BZD = NewCode("BZD") - BMD = NewCode("BMD") - BOB = NewCode("BOB") - BAM = NewCode("BAM") - BWP = NewCode("BWP") - BGN = NewCode("BGN") - BRL = NewCode("BRL") - BND = NewCode("BND") - KHR = NewCode("KHR") - KYD = NewCode("KYD") - CLP = NewCode("CLP") - CNY = NewCode("CNY") - COP = NewCode("COP") - HRK = NewCode("HRK") - CUP = NewCode("CUP") - CZK = NewCode("CZK") - DKK = NewCode("DKK") - DOP = NewCode("DOP") - XCD = NewCode("XCD") - EGP = NewCode("EGP") - SVC = NewCode("SVC") - FKP = NewCode("FKP") - FJD = NewCode("FJD") - GIP = NewCode("GIP") - GTQ = NewCode("GTQ") - GGP = NewCode("GGP") - GYD = NewCode("GYD") - HNL = NewCode("HNL") - HUF = NewCode("HUF") - ISK = NewCode("ISK") - INR = NewCode("INR") - IDR = NewCode("IDR") - IRR = NewCode("IRR") - IMP = NewCode("IMP") - ILS = NewCode("ILS") - JMD = NewCode("JMD") - JEP = NewCode("JEP") - KZT = NewCode("KZT") - KPW = NewCode("KPW") - KGS = NewCode("KGS") - LAK = NewCode("LAK") - LBP = NewCode("LBP") - LRD = NewCode("LRD") - MKD = NewCode("MKD") - MUR = NewCode("MUR") - MXN = NewCode("MXN") - MNT = NewCode("MNT") - MZN = NewCode("MZN") - NAD = NewCode("NAD") - NPR = NewCode("NPR") - ANG = NewCode("ANG") - NZD = NewCode("NZD") - NIO = NewCode("NIO") - NGN = NewCode("NGN") - NOK = NewCode("NOK") - OMR = NewCode("OMR") - PKR = NewCode("PKR") - PAB = NewCode("PAB") - PYG = NewCode("PYG") - PHP = NewCode("PHP") - QAR = NewCode("QAR") - RON = NewCode("RON") - SHP = NewCode("SHP") - SAR = NewCode("SAR") - RSD = NewCode("RSD") - SCR = NewCode("SCR") - SOS = NewCode("SOS") - ZAR = NewCode("ZAR") - LKR = NewCode("LKR") - SEK = NewCode("SEK") - CHF = NewCode("CHF") - SRD = NewCode("SRD") - SYP = NewCode("SYP") - TWD = NewCode("TWD") - THB = NewCode("THB") - TTD = NewCode("TTD") - TVD = NewCode("TVD") - GBP = NewCode("GBP") - UYU = NewCode("UYU") - UZS = NewCode("UZS") - VEF = NewCode("VEF") - VND = NewCode("VND") - YER = NewCode("YER") - ZWD = NewCode("ZWD") - XETH = NewCode("XETH") - FX_BTC = NewCode("FX_BTC") // nolint: stylecheck, golint -) diff --git a/currency/code_test.go b/currency/code_test.go index b5d7c599..1568a7e4 100644 --- a/currency/code_test.go +++ b/currency/code_test.go @@ -1,38 +1,37 @@ package currency import ( + "encoding/json" "testing" - - "github.com/thrasher-corp/gocryptotrader/common" ) func TestRoleString(t *testing.T) { - if Unset.String() != UnsetRollString { - t.Errorf("Test Failed - Role String() error expected %s but received %s", - UnsetRollString, + if Unset.String() != UnsetRoleString { + t.Errorf("Role String() error expected %s but received %s", + UnsetRoleString, Unset) } if Fiat.String() != FiatCurrencyString { - t.Errorf("Test Failed - Role String() error expected %s but received %s", + t.Errorf("Role String() error expected %s but received %s", FiatCurrencyString, Fiat) } if Cryptocurrency.String() != CryptocurrencyString { - t.Errorf("Test Failed - Role String() error expected %s but received %s", + t.Errorf("Role String() error expected %s but received %s", CryptocurrencyString, Cryptocurrency) } if Token.String() != TokenString { - t.Errorf("Test Failed - Role String() error expected %s but received %s", + t.Errorf("Role String() error expected %s but received %s", TokenString, Token) } if Contract.String() != ContractString { - t.Errorf("Test Failed - Role String() error expected %s but received %s", + t.Errorf("Role String() error expected %s but received %s", ContractString, Contract) } @@ -40,26 +39,27 @@ func TestRoleString(t *testing.T) { var random Role = 1 << 7 if random.String() != "UNKNOWN" { - t.Errorf("Test Failed - Role String() error expected %s but received %s", + t.Errorf("Role String() error expected %s but received %s", "UNKNOWN", random) } } func TestRoleMarshalJSON(t *testing.T) { - d, err := common.JSONEncode(Fiat) + d, err := json.Marshal(Fiat) if err != nil { - t.Error("Test Failed - Role MarshalJSON() error", err) + t.Error("Role MarshalJSON() error", err) } expected := `"fiatCurrency"` if string(d) != expected { - t.Errorf("Test Failed - Role MarshalJSON() error expected %s but received %s", + t.Errorf("Role MarshalJSON() error expected %s but received %s", expected, string(d)) } } +// TestRoleUnmarshalJSON logic test func TestRoleUnmarshalJSON(t *testing.T) { type AllTheRoles struct { RoleOne Role `json:"RoleOne"` @@ -78,138 +78,160 @@ func TestRoleUnmarshalJSON(t *testing.T) { RoleFive: Contract, } - e, err := common.JSONEncode(1337) + e, err := json.Marshal(1337) if err != nil { - t.Fatal("Test Failed - Role UnmarshalJSON() error", err) + t.Fatal("Role UnmarshalJSON() error", err) } var incoming AllTheRoles - err = common.JSONDecode(e, &incoming) + err = json.Unmarshal(e, &incoming) if err == nil { - t.Fatal("Test Failed - Role UnmarshalJSON() error", err) + t.Fatal("Role UnmarshalJSON() Expected error") } - e, err = common.JSONEncode(outgoing) + e, err = json.Marshal(outgoing) if err != nil { - t.Fatal("Test Failed - Role UnmarshalJSON() error", err) + t.Fatal("Role UnmarshalJSON() error", err) } - err = common.JSONDecode(e, &incoming) + err = json.Unmarshal(e, &incoming) if err != nil { - t.Fatal("Test Failed - Role UnmarshalJSON() error", err) + t.Fatal("Role UnmarshalJSON() error", err) } if incoming.RoleOne != Unset { - t.Errorf("Test Failed - Role String() error expected %s but received %s", + t.Errorf("Role String() error expected %s but received %s", Unset, incoming.RoleOne) } if incoming.RoleTwo != Cryptocurrency { - t.Errorf("Test Failed - Role String() error expected %s but received %s", + t.Errorf("Role String() error expected %s but received %s", Cryptocurrency, incoming.RoleTwo) } if incoming.RoleThree != Fiat { - t.Errorf("Test Failed - Role String() error expected %s but received %s", + t.Errorf("Role String() error expected %s but received %s", Fiat, incoming.RoleThree) } if incoming.RoleFour != Token { - t.Errorf("Test Failed - Role String() error expected %s but received %s", + t.Errorf("Role String() error expected %s but received %s", Token, incoming.RoleFour) } if incoming.RoleFive != Contract { - t.Errorf("Test Failed - Role String() error expected %s but received %s", + t.Errorf("Role String() error expected %s but received %s", Contract, incoming.RoleFive) } if incoming.RoleUnknown != Unset { - t.Errorf("Test Failed - Role String() error expected %s but received %s", + t.Errorf("Role String() error expected %s but received %s", incoming.RoleFive, incoming.RoleUnknown) } + var unhandled Role + err = unhandled.UnmarshalJSON([]byte("\"ThisIsntReal\"")) + if err == nil { + t.Error("Expected unmarshall error") + } } func TestBaseCode(t *testing.T) { var main BaseCodes if main.HasData() { - t.Errorf("Test Failed - BaseCode HasData() error expected false but received %v", + t.Errorf("BaseCode HasData() error expected false but received %v", main.HasData()) } catsCode := main.Register("CATS") if !main.HasData() { - t.Errorf("Test Failed - BaseCode HasData() error expected true but received %v", + t.Errorf("BaseCode HasData() error expected true but received %v", main.HasData()) } if !main.Register("CATS").Match(catsCode) { - t.Errorf("Test Failed - BaseCode Match() error expected true but received %v", + t.Errorf("BaseCode Match() error expected true but received %v", false) } if main.Register("DOGS").Match(catsCode) { - t.Errorf("Test Failed - BaseCode Match() error expected false but received %v", + t.Errorf("BaseCode Match() error expected false but received %v", true) } loadedCurrencies := main.GetCurrencies() if loadedCurrencies.Contains(main.Register("OWLS")) { - t.Errorf("Test Failed - BaseCode Contains() error expected false but received %v", + t.Errorf("BaseCode Contains() error expected false but received %v", true) } if !loadedCurrencies.Contains(catsCode) { - t.Errorf("Test Failed - BaseCode Contains() error expected true but received %v", + t.Errorf("BaseCode Contains() error expected true but received %v", false) } err := main.UpdateContract("Bitcoin Perpetual", "XBTUSD", "Bitmex") if err != nil { - t.Error("Test Failed - BaseCode UpdateContract error", err) + t.Error("BaseCode UpdateContract error", err) } err = main.UpdateCryptocurrency("Bitcoin", "BTC", 1337) if err != nil { - t.Error("Test Failed - BaseCode UpdateContract error", err) + t.Error("BaseCode UpdateContract error", err) + } + + err = main.UpdateFiatCurrency("Unreal Dollar", "AUD", 1111) + if err != nil { + t.Error("BaseCode UpdateContract error", err) + } + if main.Items[5].FullName != "Unreal Dollar" { + t.Error("Expected fullname to update for AUD") } err = main.UpdateFiatCurrency("Australian Dollar", "AUD", 1336) if err != nil { - t.Error("Test Failed - BaseCode UpdateContract error", err) + t.Error("BaseCode UpdateContract error", err) + } + + main.Items[5].Role = Unset + err = main.UpdateFiatCurrency("Australian Dollar", "AUD", 1336) + if err != nil { + t.Error("BaseCode UpdateContract error", err) + } + if main.Items[5].Role != Fiat { + t.Error("Expected role to change to Fiat") } err = main.UpdateToken("Populous", "PPT", "ETH", 1335) if err != nil { - t.Error("Test Failed - BaseCode UpdateContract error", err) + t.Error("BaseCode UpdateContract error", err) } contract := main.Register("XBTUSD") if contract.IsFiatCurrency() { - t.Errorf("Test Failed - BaseCode IsFiatCurrency() error expected false but received %v", + t.Errorf("BaseCode IsFiatCurrency() error expected false but received %v", true) } if contract.IsCryptocurrency() { - t.Errorf("Test Failed - BaseCode IsFiatCurrency() error expected false but received %v", + t.Errorf("BaseCode IsFiatCurrency() error expected false but received %v", true) } if contract.IsDefaultFiatCurrency() { - t.Errorf("Test Failed - BaseCode IsDefaultFiatCurrency() error expected false but received %v", + t.Errorf("BaseCode IsDefaultFiatCurrency() error expected false but received %v", true) } if contract.IsDefaultFiatCurrency() { - t.Errorf("Test Failed - BaseCode IsFiatCurrency() error expected false but received %v", + t.Errorf("BaseCode IsFiatCurrency() error expected false but received %v", true) } @@ -220,50 +242,101 @@ func TestBaseCode(t *testing.T) { Symbol: "ADA", }) if err != nil { - t.Error("Test Failed - BaseCode LoadItem() error", err) + t.Error("BaseCode LoadItem() error", err) } full, err := main.GetFullCurrencyData() if err != nil { - t.Error("Test Failed - BaseCode GetFullCurrencyData error", err) + t.Error("BaseCode GetFullCurrencyData error", err) } if len(full.Contracts) != 1 { - t.Errorf("Test Failed - BaseCode GetFullCurrencyData() error expected 1 but received %v", + t.Errorf("BaseCode GetFullCurrencyData() error expected 1 but received %v", len(full.Contracts)) } if len(full.Cryptocurrency) != 2 { - t.Errorf("Test Failed - BaseCode GetFullCurrencyData() error expected 1 but received %v", + t.Errorf("BaseCode GetFullCurrencyData() error expected 1 but received %v", len(full.Cryptocurrency)) } if len(full.FiatCurrency) != 1 { - t.Errorf("Test Failed - BaseCode GetFullCurrencyData() error expected 1 but received %v", + t.Errorf("BaseCode GetFullCurrencyData() error expected 1 but received %v", len(full.FiatCurrency)) } if len(full.Token) != 1 { - t.Errorf("Test Failed - BaseCode GetFullCurrencyData() error expected 1 but received %v", + t.Errorf("BaseCode GetFullCurrencyData() error expected 1 but received %v", len(full.Token)) } if len(full.UnsetCurrency) != 3 { - t.Errorf("Test Failed - BaseCode GetFullCurrencyData() error expected 3 but received %v", + t.Errorf("BaseCode GetFullCurrencyData() error expected 3 but received %v", len(full.UnsetCurrency)) } if !full.LastMainUpdate.IsZero() { - t.Errorf("Test Failed - BaseCode GetFullCurrencyData() error expected 0 but received %s", + t.Errorf("BaseCode GetFullCurrencyData() error expected 0 but received %s", full.LastMainUpdate) } + + err = main.LoadItem(&Item{ + ID: 0, + FullName: "Cardano", + Role: Role(99), + Symbol: "ADA", + }) + if err != nil { + t.Error("BaseCode LoadItem() error", err) + } + _, err = main.GetFullCurrencyData() + if err == nil { + t.Error("Expected 'Role undefined'") + } + + main.Items[0].FullName = "Hello" + err = main.UpdateCryptocurrency("MEWOW", "CATS", 1338) + if err != nil { + t.Error("BaseCode UpdateContract error", err) + } + if main.Items[0].FullName != "MEWOW" { + t.Error("Fullname not updated") + } + err = main.UpdateCryptocurrency("MEWOW", "CATS", 1338) + if err != nil { + t.Error("BaseCode UpdateContract error", err) + } + + main.Items[0].Role = Cryptocurrency + err = main.UpdateCryptocurrency("MEWOW", "CATS", 3) + if err != nil { + t.Error("BaseCode UpdateContract error", err) + } + if main.Items[0].ID != 3 { + t.Error("ID not updated") + } + + main.Items[0].Role = Unset + err = main.UpdateCryptocurrency("MEWOW", "CATS", 1338) + if err != nil { + t.Error("BaseCode UpdateContract error", err) + } + if main.Items[0].ID != 1338 { + t.Error("ID not updated") + } + + main.Items[0].Role = Token + err = main.UpdateCryptocurrency("MEWOW", "CATS", 3) + if err == nil { + t.Error("Expecting cryptocurrency to already exist") + } } func TestCodeString(t *testing.T) { expected := "TEST" cc := NewCode("TEST") if cc.String() != expected { - t.Errorf("Test Failed - Currency Code String() error expected %s but received %s", + t.Errorf("Currency Code String() error expected %s but received %s", expected, cc) } } @@ -272,7 +345,7 @@ func TestCodeLower(t *testing.T) { expected := "test" cc := NewCode("TEST") if cc.Lower().String() != expected { - t.Errorf("Test Failed - Currency Code Lower() error expected %s but received %s", + t.Errorf("Currency Code Lower() error expected %s but received %s", expected, cc.Lower()) } @@ -282,7 +355,7 @@ func TestCodeUpper(t *testing.T) { expected := "TEST" cc := NewCode("test") if cc.Upper().String() != expected { - t.Errorf("Test Failed - Currency Code Upper() error expected %s but received %s", + t.Errorf("Currency Code Upper() error expected %s but received %s", expected, cc.Upper()) } @@ -291,23 +364,23 @@ func TestCodeUpper(t *testing.T) { func TestCodeUnmarshalJSON(t *testing.T) { var unmarshalHere Code expected := "BRO" - encoded, err := common.JSONEncode(expected) + encoded, err := json.Marshal(expected) if err != nil { - t.Fatal("Test Failed - Currency Code UnmarshalJSON error", err) + t.Fatal("Currency Code UnmarshalJSON error", err) } - err = common.JSONDecode(encoded, &unmarshalHere) + err = json.Unmarshal(encoded, &unmarshalHere) if err != nil { - t.Fatal("Test Failed - Currency Code UnmarshalJSON error", err) + t.Fatal("Currency Code UnmarshalJSON error", err) } - err = common.JSONDecode(encoded, &unmarshalHere) + err = json.Unmarshal(encoded, &unmarshalHere) if err != nil { - t.Fatal("Test Failed - Currency Code UnmarshalJSON error", err) + t.Fatal("Currency Code UnmarshalJSON error", err) } if unmarshalHere.String() != expected { - t.Errorf("Test Failed - Currency Code Upper() error expected %s but received %s", + t.Errorf("Currency Code Upper() error expected %s but received %s", expected, unmarshalHere) } @@ -322,13 +395,13 @@ func TestCodeMarshalJSON(t *testing.T) { expectedJSON := `{"sweetCodes":"BRO"}` - encoded, err := common.JSONEncode(quickstruct) + encoded, err := json.Marshal(quickstruct) if err != nil { - t.Fatal("Test Failed - Currency Code UnmarshalJSON error", err) + t.Fatal("Currency Code UnmarshalJSON error", err) } if string(encoded) != expectedJSON { - t.Errorf("Test Failed - Currency Code Upper() error expected %s but received %s", + t.Errorf("Currency Code Upper() error expected %s but received %s", expectedJSON, string(encoded)) } @@ -339,44 +412,44 @@ func TestCodeMarshalJSON(t *testing.T) { Codey: Code{}, // nil code } - encoded, err = common.JSONEncode(quickstruct) + encoded, err = json.Marshal(quickstruct) if err != nil { - t.Fatal("Test Failed - Currency Code UnmarshalJSON error", err) + t.Fatal("Currency Code UnmarshalJSON error", err) } newExpectedJSON := `{"sweetCodes":""}` if string(encoded) != newExpectedJSON { - t.Errorf("Test Failed - Currency Code Upper() error expected %s but received %s", + t.Errorf("Currency Code Upper() error expected %s but received %s", newExpectedJSON, string(encoded)) } } func TestIsDefaultCurrency(t *testing.T) { if !USD.IsDefaultFiatCurrency() { - t.Errorf("Test Failed. TestIsDefaultCurrency Cannot match currency %s.", + t.Errorf("TestIsDefaultCurrency Cannot match currency %s.", USD) } if !AUD.IsDefaultFiatCurrency() { - t.Errorf("Test Failed. TestIsDefaultCurrency Cannot match currency, %s.", + t.Errorf("TestIsDefaultCurrency Cannot match currency, %s.", AUD) } if LTC.IsDefaultFiatCurrency() { - t.Errorf("Test Failed. TestIsDefaultCurrency Function return is incorrect with, %s.", + t.Errorf("TestIsDefaultCurrency Function return is incorrect with, %s.", LTC) } } func TestIsDefaultCryptocurrency(t *testing.T) { if !BTC.IsDefaultCryptocurrency() { - t.Errorf("Test Failed. TestIsDefaultCryptocurrency cannot match currency, %s.", + t.Errorf("TestIsDefaultCryptocurrency cannot match currency, %s.", BTC) } if !LTC.IsDefaultCryptocurrency() { - t.Errorf("Test Failed. TestIsDefaultCryptocurrency cannot match currency, %s.", + t.Errorf("TestIsDefaultCryptocurrency cannot match currency, %s.", LTC) } if AUD.IsDefaultCryptocurrency() { - t.Errorf("Test Failed. TestIsDefaultCryptocurrency function return is incorrect with, %s.", + t.Errorf("TestIsDefaultCryptocurrency function return is incorrect with, %s.", AUD) } } @@ -384,30 +457,30 @@ func TestIsDefaultCryptocurrency(t *testing.T) { func TestIsFiatCurrency(t *testing.T) { if !USD.IsFiatCurrency() { t.Errorf( - "Test Failed. TestIsFiatCurrency cannot match currency, %s.", USD) + "TestIsFiatCurrency cannot match currency, %s.", USD) } if !CNY.IsFiatCurrency() { t.Errorf( - "Test Failed. TestIsFiatCurrency cannot match currency, %s.", CNY) + "TestIsFiatCurrency cannot match currency, %s.", CNY) } if LINO.IsFiatCurrency() { t.Errorf( - "Test Failed. TestIsFiatCurrency cannot match currency, %s.", LINO, + "TestIsFiatCurrency cannot match currency, %s.", LINO, ) } } func TestIsCryptocurrency(t *testing.T) { if !BTC.IsCryptocurrency() { - t.Errorf("Test Failed. TestIsFiatCurrency cannot match currency, %s.", + t.Errorf("TestIsFiatCurrency cannot match currency, %s.", BTC) } if !LTC.IsCryptocurrency() { - t.Errorf("Test Failed. TestIsFiatCurrency cannot match currency, %s.", + t.Errorf("TestIsFiatCurrency cannot match currency, %s.", LTC) } if AUD.IsCryptocurrency() { - t.Errorf("Test Failed. TestIsFiatCurrency cannot match currency, %s.", + t.Errorf("TestIsFiatCurrency cannot match currency, %s.", AUD) } } @@ -419,7 +492,7 @@ func TestItemString(t *testing.T) { } if newItem.String() != expected { - t.Errorf("Test Failed - Item String() error expected %s but received %s", + t.Errorf("Item String() error expected %s but received %s", expected, &newItem) } diff --git a/currency/code_types.go b/currency/code_types.go new file mode 100644 index 00000000..d387a076 --- /dev/null +++ b/currency/code_types.go @@ -0,0 +1,1655 @@ +package currency + +import ( + "sync" + "time" +) + +// Bitmasks const for currency roles +const ( + Unset Role = 0 + Fiat Role = 1 << (iota - 1) + Cryptocurrency + Token + Contract + + UnsetRoleString = "roleUnset" + FiatCurrencyString = "fiatCurrency" + CryptocurrencyString = "cryptocurrency" + TokenString = "token" + ContractString = "contract" +) + +// Role defines a bitmask for the full currency roles either; fiat, +// cryptocurrency, token, or contract +type Role uint8 + +// BaseCodes defines a basket of bare currency codes +type BaseCodes struct { + Items []*Item + LastMainUpdate time.Time + mtx sync.Mutex +} + +// Code defines an ISO 4217 fiat currency or unofficial cryptocurrency code +// string +type Code struct { + Item *Item + UpperCase bool +} + +// Item defines a sub type containing the main attributes of a designated +// currency code pointer +type Item struct { + ID int `json:"id"` + FullName string `json:"fullName"` + Symbol string `json:"symbol"` + Role Role `json:"role"` + AssocChain string `json:"associatedBlockchain"` + AssocExchange []string `json:"associatedExchanges"` +} + +// Const declarations for individual currencies/tokens/fiat +// An ever growing list. Cares not for equivalence, just is +var ( + BTC = NewCode("BTC") + LTC = NewCode("LTC") + ETH = NewCode("ETH") + XRP = NewCode("XRP") + BCH = NewCode("BCH") + EOS = NewCode("EOS") + XLM = NewCode("XLM") + USDT = NewCode("USDT") + ADA = NewCode("ADA") + XMR = NewCode("XMR") + TRX = NewCode("TRX") + MIOTA = NewCode("MIOTA") + DASH = NewCode("DASH") + BNB = NewCode("BNB") + NEO = NewCode("NEO") + ETC = NewCode("ETC") + XEM = NewCode("XEM") + XTZ = NewCode("XTZ") + VET = NewCode("VET") + DOGE = NewCode("DOGE") + ZEC = NewCode("ZEC") + OMG = NewCode("OMG") + BTG = NewCode("BTG") + MKR = NewCode("MKR") + BCN = NewCode("BCN") + ONT = NewCode("ONT") + ZRX = NewCode("ZRX") + LSK = NewCode("LSK") + DCR = NewCode("DCR") + QTUM = NewCode("QTUM") + BCD = NewCode("BCD") + BTS = NewCode("BTS") + NANO = NewCode("NANO") + ZIL = NewCode("ZIL") + SC = NewCode("SC") + DGB = NewCode("DGB") + ICX = NewCode("ICX") + STEEM = NewCode("STEEM") + AE = NewCode("AE") + XVG = NewCode("XVG") + WAVES = NewCode("WAVES") + NPXS = NewCode("NPXS") + ETN = NewCode("ETN") + BTM = NewCode("BTM") + BAT = NewCode("BAT") + ETP = NewCode("ETP") + HOT = NewCode("HOT") + STRAT = NewCode("STRAT") // nolint: misspell + GNT = NewCode("GNT") + REP = NewCode("REP") + SNT = NewCode("SNT") + PPT = NewCode("PPT") + KMD = NewCode("KMD") + TUSD = NewCode("TUSD") + CNX = NewCode("CNX") + LINK = NewCode("LINK") + WTC = NewCode("WTC") + ARDR = NewCode("ARDR") + WAN = NewCode("WAN") + MITH = NewCode("MITH") + RDD = NewCode("RDD") + IOST = NewCode("IOST") + IOT = NewCode("IOT") + KCS = NewCode("KCS") + MAID = NewCode("MAID") + XET = NewCode("XET") + MOAC = NewCode("MOAC") + HC = NewCode("HC") + AION = NewCode("AION") + AOA = NewCode("AOA") + HT = NewCode("HT") + ELF = NewCode("ELF") + LRC = NewCode("LRC") + BNT = NewCode("BNT") + CMT = NewCode("CMT") + DGD = NewCode("DGD") + DCN = NewCode("DCN") + FUN = NewCode("FUN") + GXS = NewCode("GXS") + DROP = NewCode("DROP") + MANA = NewCode("MANA") + PAY = NewCode("PAY") + MCO = NewCode("MCO") + THETA = NewCode("THETA") + NXT = NewCode("NXT") + NOAH = NewCode("NOAH") + LOOM = NewCode("LOOM") + POWR = NewCode("POWR") + WAX = NewCode("WAX") + ELA = NewCode("ELA") + PIVX = NewCode("PIVX") + XIN = NewCode("XIN") + DAI = NewCode("DAI") + BTCP = NewCode("BTCP") + NEXO = NewCode("NEXO") + XBT = NewCode("XBT") + SAN = NewCode("SAN") + GAS = NewCode("GAS") + BCC = NewCode("BCC") + HCC = NewCode("HCC") + OAX = NewCode("OAX") + DNT = NewCode("DNT") + ICN = NewCode("ICN") + LLT = NewCode("LLT") + YOYO = NewCode("YOYO") + SNGLS = NewCode("SNGLS") + BQX = NewCode("BQX") + KNC = NewCode("KNC") + SNM = NewCode("SNM") + CTR = NewCode("CTR") + SALT = NewCode("SALT") + MDA = NewCode("MDA") + IOTA = NewCode("IOTA") + SUB = NewCode("SUB") + MTL = NewCode("MTL") + MTH = NewCode("MTH") + ENG = NewCode("ENG") + AST = NewCode("AST") + CLN = NewCode("CLN") + EDG = NewCode("EDG") + FIRST = NewCode("1ST") + GOLOS = NewCode("GOLOS") + ANT = NewCode("ANT") + GBG = NewCode("GBG") + HMQ = NewCode("HMQ") + INCNT = NewCode("INCNT") + ACE = NewCode("ACE") + ACT = NewCode("ACT") + AAC = NewCode("AAC") + AIDOC = NewCode("AIDOC") + SOC = NewCode("SOC") + ATL = NewCode("ATL") + AVT = NewCode("AVT") + BKX = NewCode("BKX") + BEC = NewCode("BEC") + VEE = NewCode("VEE") + PTOY = NewCode("PTOY") + CAG = NewCode("CAG") + CIC = NewCode("CIC") + CBT = NewCode("CBT") + CAN = NewCode("CAN") + DAT = NewCode("DAT") + DNA = NewCode("DNA") + INT = NewCode("INT") + IPC = NewCode("IPC") + ILA = NewCode("ILA") + LIGHT = NewCode("LIGHT") + MAG = NewCode("MAG") + AMM = NewCode("AMM") + MOF = NewCode("MOF") + MGC = NewCode("MGC") + OF = NewCode("OF") + LA = NewCode("LA") + LEV = NewCode("LEV") + NGC = NewCode("NGC") + OKB = NewCode("OKB") + MOT = NewCode("MOT") + PRA = NewCode("PRA") + R = NewCode("R") + SSC = NewCode("SSC") + SHOW = NewCode("SHOW") + SPF = NewCode("SPF") + SNC = NewCode("SNC") + SWFTC = NewCode("SWFTC") + TRA = NewCode("TRA") + TOPC = NewCode("TOPC") + TRIO = NewCode("TRIO") + QVT = NewCode("QVT") + UCT = NewCode("UCT") + UKG = NewCode("UKG") + UTK = NewCode("UTK") + VIU = NewCode("VIU") + WFEE = NewCode("WFEE") + WRC = NewCode("WRC") + UGC = NewCode("UGC") + YEE = NewCode("YEE") + YOYOW = NewCode("YOYOW") + ZIP = NewCode("ZIP") + READ = NewCode("READ") + RCT = NewCode("RCT") + REF = NewCode("REF") + XUC = NewCode("XUC") + FAIR = NewCode("FAIR") + GSC = NewCode("GSC") + HMC = NewCode("HMC") + PLU = NewCode("PLU") + PRO = NewCode("PRO") + QRL = NewCode("QRL") + REN = NewCode("REN") + ROUND = NewCode("ROUND") + SRN = NewCode("SRN") + XID = NewCode("XID") + SBD = NewCode("SBD") + TAAS = NewCode("TAAS") + TKN = NewCode("TKN") + VEN = NewCode("VEN") + VSL = NewCode("VSL") + TRST = NewCode("TRST") + XXX = NewCode("XXX") + IND = NewCode("IND") + LDC = NewCode("LDC") + GUP = NewCode("GUP") + MGO = NewCode("MGO") + MYST = NewCode("MYST") + NEU = NewCode("NEU") + NET = NewCode("NET") + BMC = NewCode("BMC") + BCAP = NewCode("BCAP") + TIME = NewCode("TIME") + CFI = NewCode("CFI") + EVX = NewCode("EVX") + REQ = NewCode("REQ") + VIB = NewCode("VIB") + ARK = NewCode("ARK") + MOD = NewCode("MOD") + ENJ = NewCode("ENJ") + STORJ = NewCode("STORJ") + RCN = NewCode("RCN") + NULS = NewCode("NULS") + RDN = NewCode("RDN") + DLT = NewCode("DLT") + AMB = NewCode("AMB") + BCPT = NewCode("BCPT") + ARN = NewCode("ARN") + GVT = NewCode("GVT") + CDT = NewCode("CDT") + POE = NewCode("POE") + QSP = NewCode("QSP") + XZC = NewCode("XZC") + TNT = NewCode("TNT") + FUEL = NewCode("FUEL") + ADX = NewCode("ADX") + CND = NewCode("CND") + LEND = NewCode("LEND") + WABI = NewCode("WABI") + SBTC = NewCode("SBTC") + BCX = NewCode("BCX") + TNB = NewCode("TNB") + GTO = NewCode("GTO") + OST = NewCode("OST") + CVC = NewCode("CVC") + DATA = NewCode("DATA") + ETF = NewCode("ETF") + BRD = NewCode("BRD") + NEBL = NewCode("NEBL") + VIBE = NewCode("VIBE") + LUN = NewCode("LUN") + CHAT = NewCode("CHAT") + RLC = NewCode("RLC") + INS = NewCode("INS") + VIA = NewCode("VIA") + BLZ = NewCode("BLZ") + SYS = NewCode("SYS") + NCASH = NewCode("NCASH") + POA = NewCode("POA") + STORM = NewCode("STORM") + WPR = NewCode("WPR") + QLC = NewCode("QLC") + GRS = NewCode("GRS") + CLOAK = NewCode("CLOAK") + ZEN = NewCode("ZEN") + SKY = NewCode("SKY") + IOTX = NewCode("IOTX") + QKC = NewCode("QKC") + AGI = NewCode("AGI") + NXS = NewCode("NXS") + EON = NewCode("EON") + KEY = NewCode("KEY") + NAS = NewCode("NAS") + ADD = NewCode("ADD") + MEETONE = NewCode("MEETONE") + ATD = NewCode("ATD") + MFT = NewCode("MFT") + EOP = NewCode("EOP") + DENT = NewCode("DENT") + IQ = NewCode("IQ") + DOCK = NewCode("DOCK") + POLY = NewCode("POLY") + VTHO = NewCode("VTHO") + ONG = NewCode("ONG") + PHX = NewCode("PHX") + GO = NewCode("GO") + PAX = NewCode("PAX") + EDO = NewCode("EDO") + WINGS = NewCode("WINGS") + NAV = NewCode("NAV") + TRIG = NewCode("TRIG") + APPC = NewCode("APPC") + KRW = NewCode("KRW") + HSR = NewCode("HSR") + ETHOS = NewCode("ETHOS") + CTXC = NewCode("CTXC") + ITC = NewCode("ITC") + TRUE = NewCode("TRUE") + ABT = NewCode("ABT") + RNT = NewCode("RNT") + PLY = NewCode("PLY") + PST = NewCode("PST") + KICK = NewCode("KICK") + BTCZ = NewCode("BTCZ") + DXT = NewCode("DXT") + STQ = NewCode("STQ") + INK = NewCode("INK") + HBZ = NewCode("HBZ") + USDT_ETH = NewCode("USDT_ETH") // nolint: golint,stylecheck + QTUM_ETH = NewCode("QTUM_ETH") // nolint: golint + BTM_ETH = NewCode("BTM_ETH") // nolint: golint,stylecheck + FIL = NewCode("FIL") + STX = NewCode("STX") + BOT = NewCode("BOT") + VERI = NewCode("VERI") + ZSC = NewCode("ZSC") + QBT = NewCode("QBT") + MED = NewCode("MED") + QASH = NewCode("QASH") + MDS = NewCode("MDS") + GOD = NewCode("GOD") + SMT = NewCode("SMT") + BTF = NewCode("BTF") + NAS_ETH = NewCode("NAS_ETH") // nolint: golint,stylecheck + TSL = NewCode("TSL") + BIFI = NewCode("BIFI") + BNTY = NewCode("BNTY") + DRGN = NewCode("DRGN") + GTC = NewCode("GTC") + MDT = NewCode("MDT") + QUN = NewCode("QUN") + GNX = NewCode("GNX") + DDD = NewCode("DDD") + BTO = NewCode("BTO") + TIO = NewCode("TIO") + OCN = NewCode("OCN") + RUFF = NewCode("RUFF") + TNC = NewCode("TNC") + SNET = NewCode("SNET") + COFI = NewCode("COFI") + ZPT = NewCode("ZPT") + JNT = NewCode("JNT") + MTN = NewCode("MTN") + GEM = NewCode("GEM") + DADI = NewCode("DADI") + RFR = NewCode("RFR") + MOBI = NewCode("MOBI") + LEDU = NewCode("LEDU") + DBC = NewCode("DBC") + MKR_OLD = NewCode("MKR_OLD") // nolint: golint,stylecheck + DPY = NewCode("DPY") + BCDN = NewCode("BCDN") + EOSDAC = NewCode("EOSDAC") // nolint: golint + TIPS = NewCode("TIPS") + XMC = NewCode("XMC") + PPS = NewCode("PPS") + BOE = NewCode("BOE") + MEDX = NewCode("MEDX") + SMT_ETH = NewCode("SMT_ETH") // nolint: golint,stylecheck + CS = NewCode("CS") + MAN = NewCode("MAN") + REM = NewCode("REM") + LYM = NewCode("LYM") + INSTAR = NewCode("INSTAR") // nolint: golint + BFT = NewCode("BFT") + IHT = NewCode("IHT") + SENC = NewCode("SENC") + TOMO = NewCode("TOMO") + ELEC = NewCode("ELEC") + SHIP = NewCode("SHIP") + TFD = NewCode("TFD") + HAV = NewCode("HAV") + HUR = NewCode("HUR") + LST = NewCode("LST") + LINO = NewCode("LINO") + SWTH = NewCode("SWTH") + NKN = NewCode("NKN") + SOUL = NewCode("SOUL") + GALA_NEO = NewCode("GALA_NEO") // nolint: golint,stylecheck + LRN = NewCode("LRN") + GSE = NewCode("GSE") + RATING = NewCode("RATING") + HSC = NewCode("HSC") + HIT = NewCode("HIT") + DX = NewCode("DX") + BXC = NewCode("BXC") + GARD = NewCode("GARD") + FTI = NewCode("FTI") + SOP = NewCode("SOP") + LEMO = NewCode("LEMO") + RED = NewCode("RED") + LBA = NewCode("LBA") + KAN = NewCode("KAN") + OPEN = NewCode("OPEN") + SKM = NewCode("SKM") + NBAI = NewCode("NBAI") + UPP = NewCode("UPP") + ATMI = NewCode("ATMI") + TMT = NewCode("TMT") + BBK = NewCode("BBK") + EDR = NewCode("EDR") + MET = NewCode("MET") + TCT = NewCode("TCT") + EXC = NewCode("EXC") + CNC = NewCode("CNC") + TIX = NewCode("TIX") + XTC = NewCode("XTC") + BU = NewCode("BU") + GNO = NewCode("GNO") + MLN = NewCode("MLN") + XBC = NewCode("XBC") + BTCD = NewCode("BTCD") + BURST = NewCode("BURST") + CLAM = NewCode("CLAM") + XCP = NewCode("XCP") + EMC2 = NewCode("EMC2") + EXP = NewCode("EXP") + FCT = NewCode("FCT") + GAME = NewCode("GAME") + GRC = NewCode("GRC") + HUC = NewCode("HUC") + LBC = NewCode("LBC") + NMC = NewCode("NMC") + NEOS = NewCode("NEOS") + OMNI = NewCode("OMNI") + PASC = NewCode("PASC") + PPC = NewCode("PPC") + DSH = NewCode("DSH") + GML = NewCode("GML") + GSY = NewCode("GSY") + POT = NewCode("POT") + XPM = NewCode("XPM") + AMP = NewCode("AMP") + VRC = NewCode("VRC") + VTC = NewCode("VTC") + ZERO07 = NewCode("007") + BIT16 = NewCode("BIT16") + TWO015 = NewCode("2015") + TWO56 = NewCode("256") + TWOBACCO = NewCode("2BACCO") + TWOGIVE = NewCode("2GIVE") + THIRTY2BIT = NewCode("32BIT") + THREE65 = NewCode("365") + FOUR04 = NewCode("404") + SEVEN00 = NewCode("700") + EIGHTBIT = NewCode("8BIT") + ACLR = NewCode("ACLR") + ACES = NewCode("ACES") + ACPR = NewCode("ACPR") + ACID = NewCode("ACID") + ACOIN = NewCode("ACOIN") + ACRN = NewCode("ACRN") + ADAM = NewCode("ADAM") + ADT = NewCode("ADT") + AIB = NewCode("AIB") + ADZ = NewCode("ADZ") + AECC = NewCode("AECC") + AM = NewCode("AM") + AGRI = NewCode("AGRI") + AGT = NewCode("AGT") + AIR = NewCode("AIR") + ALEX = NewCode("ALEX") + AUM = NewCode("AUM") + ALIEN = NewCode("ALIEN") + ALIS = NewCode("ALIS") + ALL = NewCode("ALL") + ASAFE = NewCode("ASAFE") + AMBER = NewCode("AMBER") + AMS = NewCode("AMS") + ANAL = NewCode("ANAL") + ACP = NewCode("ACP") + ANI = NewCode("ANI") + ANTI = NewCode("ANTI") + ALTC = NewCode("ALTC") + APT = NewCode("APT") + ARCO = NewCode("ARCO") + ALC = NewCode("ALC") + ARB = NewCode("ARB") + ARCT = NewCode("ARCT") + ARCX = NewCode("ARCX") + ARGUS = NewCode("ARGUS") + ARH = NewCode("ARH") + ARM = NewCode("ARM") + ARNA = NewCode("ARNA") + ARPA = NewCode("ARPA") + ARTA = NewCode("ARTA") + ABY = NewCode("ABY") + ARTC = NewCode("ARTC") + AL = NewCode("AL") + ASN = NewCode("ASN") + ADCN = NewCode("ADCN") + ATB = NewCode("ATB") + ATM = NewCode("ATM") + ATMCHA = NewCode("ATMCHA") + ATOM = NewCode("ATOM") + ADC = NewCode("ADC") + ARE = NewCode("ARE") + AUR = NewCode("AUR") + AV = NewCode("AV") + AXIOM = NewCode("AXIOM") + B2B = NewCode("B2B") + B2 = NewCode("B2") + B3 = NewCode("B3") + BAB = NewCode("BAB") + BAN = NewCode("BAN") + BamitCoin = NewCode("BamitCoin") + NANAS = NewCode("NANAS") + BBCC = NewCode("BBCC") + BTA = NewCode("BTA") + BSTK = NewCode("BSTK") + BATL = NewCode("BATL") + BBH = NewCode("BBH") + BITB = NewCode("BITB") + BRDD = NewCode("BRDD") + XBTS = NewCode("XBTS") + BVC = NewCode("BVC") + CHATX = NewCode("CHATX") + BEEP = NewCode("BEEP") + BEEZ = NewCode("BEEZ") + BENJI = NewCode("BENJI") + BERN = NewCode("BERN") + PROFIT = NewCode("PROFIT") + BEST = NewCode("BEST") + BGF = NewCode("BGF") + BIGUP = NewCode("BIGUP") + BLRY = NewCode("BLRY") + BILL = NewCode("BILL") + BIOB = NewCode("BIOB") + BIO = NewCode("BIO") + BIOS = NewCode("BIOS") + BPTN = NewCode("BPTN") + BTCA = NewCode("BTCA") + BA = NewCode("BA") + BAC = NewCode("BAC") + BBT = NewCode("BBT") + BOSS = NewCode("BOSS") + BRONZ = NewCode("BRONZ") + CAT = NewCode("CAT") + BTD = NewCode("BTD") + XBTC21 = NewCode("XBTC21") + BCA = NewCode("BCA") + BCP = NewCode("BCP") + BTDOLL = NewCode("BTDOLL") + LIZA = NewCode("LIZA") + BTCRED = NewCode("BTCRED") + BTCS = NewCode("BTCS") + BTU = NewCode("BTU") + BUM = NewCode("BUM") + LITE = NewCode("LITE") + BCM = NewCode("BCM") + BCS = NewCode("BCS") + BTCU = NewCode("BTCU") + BM = NewCode("BM") + BTCRY = NewCode("BTCRY") + BTCR = NewCode("BTCR") + HIRE = NewCode("HIRE") + STU = NewCode("STU") + BITOK = NewCode("BITOK") + BITON = NewCode("BITON") + BPC = NewCode("BPC") + BPOK = NewCode("BPOK") + BTP = NewCode("BTP") + BITCNY = NewCode("bitCNY") + RNTB = NewCode("RNTB") + BSH = NewCode("BSH") + XBS = NewCode("XBS") + BITS = NewCode("BITS") + BST = NewCode("BST") + BXT = NewCode("BXT") + VEG = NewCode("VEG") + VOLT = NewCode("VOLT") + BTV = NewCode("BTV") + BITZ = NewCode("BITZ") + BTZ = NewCode("BTZ") + BHC = NewCode("BHC") + BDC = NewCode("BDC") + JACK = NewCode("JACK") + BS = NewCode("BS") + BSTAR = NewCode("BSTAR") + BLAZR = NewCode("BLAZR") + BOD = NewCode("BOD") + BLUE = NewCode("BLUE") + BLU = NewCode("BLU") + BLUS = NewCode("BLUS") + BMT = NewCode("BMT") + BOLI = NewCode("BOLI") + BOMB = NewCode("BOMB") + BON = NewCode("BON") + BOOM = NewCode("BOOM") + BOSON = NewCode("BOSON") + BSC = NewCode("BSC") + BRH = NewCode("BRH") + BRAIN = NewCode("BRAIN") + BRE = NewCode("BRE") + BTCM = NewCode("BTCM") + BTCO = NewCode("BTCO") + TALK = NewCode("TALK") + BUB = NewCode("BUB") + BUY = NewCode("BUY") + BUZZ = NewCode("BUZZ") + BTH = NewCode("BTH") + C0C0 = NewCode("C0C0") + CAB = NewCode("CAB") + CF = NewCode("CF") + CLO = NewCode("CLO") + CAM = NewCode("CAM") + CD = NewCode("CD") + CANN = NewCode("CANN") + CNNC = NewCode("CNNC") + CPC = NewCode("CPC") + CST = NewCode("CST") + CAPT = NewCode("CAPT") + CARBON = NewCode("CARBON") + CME = NewCode("CME") + CTK = NewCode("CTK") + CBD = NewCode("CBD") + CCC = NewCode("CCC") + CNT = NewCode("CNT") + XCE = NewCode("XCE") + CHRG = NewCode("CHRG") + CHEMX = NewCode("CHEMX") + CHESS = NewCode("CHESS") + CKS = NewCode("CKS") + CHILL = NewCode("CHILL") + CHIP = NewCode("CHIP") + CHOOF = NewCode("CHOOF") + CRX = NewCode("CRX") + CIN = NewCode("CIN") + POLL = NewCode("POLL") + CLICK = NewCode("CLICK") + CLINT = NewCode("CLINT") + CLUB = NewCode("CLUB") + CLUD = NewCode("CLUD") + COX = NewCode("COX") + COXST = NewCode("COXST") + CFC = NewCode("CFC") + CTIC2 = NewCode("CTIC2") + COIN = NewCode("COIN") + BTTF = NewCode("BTTF") + C2 = NewCode("C2") + CAID = NewCode("CAID") + CL = NewCode("CL") + CTIC = NewCode("CTIC") + CXT = NewCode("CXT") + CHP = NewCode("CHP") + CV2 = NewCode("CV2") + COC = NewCode("COC") + COMP = NewCode("COMP") + CMS = NewCode("CMS") + CONX = NewCode("CONX") + CCX = NewCode("CCX") + CLR = NewCode("CLR") + CORAL = NewCode("CORAL") + CORG = NewCode("CORG") + CSMIC = NewCode("CSMIC") + CMC = NewCode("CMC") + COV = NewCode("COV") + COVX = NewCode("COVX") + CRAB = NewCode("CRAB") + CRAFT = NewCode("CRAFT") + CRNK = NewCode("CRNK") + CRAVE = NewCode("CRAVE") + CRM = NewCode("CRM") + XCRE = NewCode("XCRE") + CREDIT = NewCode("CREDIT") + CREVA = NewCode("CREVA") + CRIME = NewCode("CRIME") + CROC = NewCode("CROC") + CRC = NewCode("CRC") + CRW = NewCode("CRW") + CRY = NewCode("CRY") + CBX = NewCode("CBX") + TKTX = NewCode("TKTX") + CB = NewCode("CB") + CIRC = NewCode("CIRC") + CCB = NewCode("CCB") + CDO = NewCode("CDO") + CG = NewCode("CG") + CJ = NewCode("CJ") + CJC = NewCode("CJC") + CYT = NewCode("CYT") + CRPS = NewCode("CRPS") + PING = NewCode("PING") + CWXT = NewCode("CWXT") + CCT = NewCode("CCT") + CTL = NewCode("CTL") + CURVES = NewCode("CURVES") + CC = NewCode("CC") + CYC = NewCode("CYC") + CYG = NewCode("CYG") + CYP = NewCode("CYP") + FUNK = NewCode("FUNK") + CZECO = NewCode("CZECO") + DALC = NewCode("DALC") + DLISK = NewCode("DLISK") + MOOND = NewCode("MOOND") + DB = NewCode("DB") + DCC = NewCode("DCC") + DCYP = NewCode("DCYP") + DETH = NewCode("DETH") + DKC = NewCode("DKC") + DISK = NewCode("DISK") + DRKT = NewCode("DRKT") + DTT = NewCode("DTT") + DASHS = NewCode("DASHS") + DBTC = NewCode("DBTC") + DCT = NewCode("DCT") + DBET = NewCode("DBET") + DEC = NewCode("DEC") + DECR = NewCode("DECR") + DEA = NewCode("DEA") + DPAY = NewCode("DPAY") + DCRE = NewCode("DCRE") + DC = NewCode("DC") + DES = NewCode("DES") + DEM = NewCode("DEM") + DXC = NewCode("DXC") + DCK = NewCode("DCK") + CUBE = NewCode("CUBE") + DGMS = NewCode("DGMS") + DBG = NewCode("DBG") + DGCS = NewCode("DGCS") + DBLK = NewCode("DBLK") + DIME = NewCode("DIME") + DIRT = NewCode("DIRT") + DVD = NewCode("DVD") + DMT = NewCode("DMT") + NOTE = NewCode("NOTE") + DGORE = NewCode("DGORE") + DLC = NewCode("DLC") + DRT = NewCode("DRT") + DOTA = NewCode("DOTA") + DOX = NewCode("DOX") + DRA = NewCode("DRA") + DFT = NewCode("DFT") + XDB = NewCode("XDB") + DRM = NewCode("DRM") + DRZ = NewCode("DRZ") + DRACO = NewCode("DRACO") + DBIC = NewCode("DBIC") + DUB = NewCode("DUB") + GUM = NewCode("GUM") + DUR = NewCode("DUR") + DUST = NewCode("DUST") + DUX = NewCode("DUX") + DXO = NewCode("DXO") + ECN = NewCode("ECN") + EDR2 = NewCode("EDR2") + EA = NewCode("EA") + EAGS = NewCode("EAGS") + EMT = NewCode("EMT") + EBONUS = NewCode("EBONUS") + ECCHI = NewCode("ECCHI") + EKO = NewCode("EKO") + ECLI = NewCode("ECLI") + ECOB = NewCode("ECOB") + ECO = NewCode("ECO") + EDIT = NewCode("EDIT") + EDRC = NewCode("EDRC") + EDC = NewCode("EDC") + EGAME = NewCode("EGAME") + EGG = NewCode("EGG") + EGO = NewCode("EGO") + ELC = NewCode("ELC") + ELCO = NewCode("ELCO") + ECA = NewCode("ECA") + EPC = NewCode("EPC") + ELE = NewCode("ELE") + ONE337 = NewCode("1337") + EMB = NewCode("EMB") + EMC = NewCode("EMC") + EPY = NewCode("EPY") + EMPC = NewCode("EMPC") + EMP = NewCode("EMP") + ENE = NewCode("ENE") + EET = NewCode("EET") + XNG = NewCode("XNG") + EGMA = NewCode("EGMA") + ENTER = NewCode("ENTER") + ETRUST = NewCode("ETRUST") + EQL = NewCode("EQL") + EQM = NewCode("EQM") + EQT = NewCode("EQT") + ERR = NewCode("ERR") + ESC = NewCode("ESC") + ESP = NewCode("ESP") + ENT = NewCode("ENT") + ETCO = NewCode("ETCO") + DOGETH = NewCode("DOGETH") + ECASH = NewCode("ECASH") + ELITE = NewCode("ELITE") + ETHS = NewCode("ETHS") + ETL = NewCode("ETL") + ETZ = NewCode("ETZ") + EUC = NewCode("EUC") + EURC = NewCode("EURC") + EUROPE = NewCode("EUROPE") + EVA = NewCode("EVA") + EGC = NewCode("EGC") + EOC = NewCode("EOC") + EVIL = NewCode("EVIL") + EVO = NewCode("EVO") + EXB = NewCode("EXB") + EXIT = NewCode("EXIT") + XT = NewCode("XT") + F16 = NewCode("F16") + FADE = NewCode("FADE") + FAZZ = NewCode("FAZZ") + FX = NewCode("FX") + FIDEL = NewCode("FIDEL") + FIDGT = NewCode("FIDGT") + FIND = NewCode("FIND") + FPC = NewCode("FPC") + FIRE = NewCode("FIRE") + FFC = NewCode("FFC") + FRST = NewCode("FRST") + FIST = NewCode("FIST") + FIT = NewCode("FIT") + FLX = NewCode("FLX") + FLVR = NewCode("FLVR") + FLY = NewCode("FLY") + FONZ = NewCode("FONZ") + XFCX = NewCode("XFCX") + FOREX = NewCode("FOREX") + FRN = NewCode("FRN") + FRK = NewCode("FRK") + FRWC = NewCode("FRWC") + FGZ = NewCode("FGZ") + FRE = NewCode("FRE") + FRDC = NewCode("FRDC") + FJC = NewCode("FJC") + FURY = NewCode("FURY") + FSN = NewCode("FSN") + FCASH = NewCode("FCASH") + FTO = NewCode("FTO") + FUZZ = NewCode("FUZZ") + GAKH = NewCode("GAKH") + GBT = NewCode("GBT") + UNITS = NewCode("UNITS") + FOUR20G = NewCode("420G") + GENIUS = NewCode("GENIUS") + GEN = NewCode("GEN") + GEO = NewCode("GEO") + GER = NewCode("GER") + GSR = NewCode("GSR") + SPKTR = NewCode("SPKTR") + GIFT = NewCode("GIFT") + WTT = NewCode("WTT") + GHS = NewCode("GHS") + GIG = NewCode("GIG") + GOT = NewCode("GOT") + XGTC = NewCode("XGTC") + GIZ = NewCode("GIZ") + GLO = NewCode("GLO") + GCR = NewCode("GCR") + BSTY = NewCode("BSTY") + GLC = NewCode("GLC") + GSX = NewCode("GSX") + GOAT = NewCode("GOAT") + GB = NewCode("GB") + GFL = NewCode("GFL") + MNTP = NewCode("MNTP") + GP = NewCode("GP") + GLUCK = NewCode("GLUCK") + GOON = NewCode("GOON") + GTFO = NewCode("GTFO") + GOTX = NewCode("GOTX") + GPU = NewCode("GPU") + GRF = NewCode("GRF") + GRAM = NewCode("GRAM") + GRAV = NewCode("GRAV") + GBIT = NewCode("GBIT") + GREED = NewCode("GREED") + GE = NewCode("GE") + GREENF = NewCode("GREENF") + GRE = NewCode("GRE") + GREXIT = NewCode("GREXIT") + GMCX = NewCode("GMCX") + GROW = NewCode("GROW") + GSM = NewCode("GSM") + GT = NewCode("GT") + NLG = NewCode("NLG") + HKN = NewCode("HKN") + HAC = NewCode("HAC") + HALLO = NewCode("HALLO") + HAMS = NewCode("HAMS") + HPC = NewCode("HPC") + HAWK = NewCode("HAWK") + HAZE = NewCode("HAZE") + HZT = NewCode("HZT") + HDG = NewCode("HDG") + HEDG = NewCode("HEDG") + HEEL = NewCode("HEEL") + HMP = NewCode("HMP") + PLAY = NewCode("PLAY") + HXX = NewCode("HXX") + XHI = NewCode("XHI") + HVCO = NewCode("HVCO") + HTC = NewCode("HTC") + MINH = NewCode("MINH") + HODL = NewCode("HODL") + HON = NewCode("HON") + HOPE = NewCode("HOPE") + HQX = NewCode("HQX") + HSP = NewCode("HSP") + HTML5 = NewCode("HTML5") + HYPERX = NewCode("HYPERX") + HPS = NewCode("HPS") + IOC = NewCode("IOC") + IBANK = NewCode("IBANK") + IBITS = NewCode("IBITS") + ICASH = NewCode("ICASH") + ICOB = NewCode("ICOB") + ICON = NewCode("ICON") + IETH = NewCode("IETH") + ILM = NewCode("ILM") + IMPS = NewCode("IMPS") + NKA = NewCode("NKA") + INCP = NewCode("INCP") + IN = NewCode("IN") + INC = NewCode("INC") + IMS = NewCode("IMS") + IFLT = NewCode("IFLT") + INFX = NewCode("INFX") + INGT = NewCode("INGT") + INPAY = NewCode("INPAY") + INSANE = NewCode("INSANE") + INXT = NewCode("INXT") + IFT = NewCode("IFT") + INV = NewCode("INV") + IVZ = NewCode("IVZ") + ILT = NewCode("ILT") + IONX = NewCode("IONX") + ISL = NewCode("ISL") + ITI = NewCode("ITI") + ING = NewCode("ING") + IEC = NewCode("IEC") + IW = NewCode("IW") + IXC = NewCode("IXC") + IXT = NewCode("IXT") + JPC = NewCode("JPC") + JANE = NewCode("JANE") + JWL = NewCode("JWL") + JIF = NewCode("JIF") + JOBS = NewCode("JOBS") + JOCKER = NewCode("JOCKER") + JW = NewCode("JW") + JOK = NewCode("JOK") + XJO = NewCode("XJO") + KGB = NewCode("KGB") + KARMC = NewCode("KARMC") + KARMA = NewCode("KARMA") + KASHH = NewCode("KASHH") + KAT = NewCode("KAT") + KC = NewCode("KC") + KIDS = NewCode("KIDS") + KIN = NewCode("KIN") + KISS = NewCode("KISS") + KOBO = NewCode("KOBO") + TP1 = NewCode("TP1") + KRAK = NewCode("KRAK") + KGC = NewCode("KGC") + KTK = NewCode("KTK") + KR = NewCode("KR") + KUBO = NewCode("KUBO") + KURT = NewCode("KURT") + KUSH = NewCode("KUSH") + LANA = NewCode("LANA") + LTH = NewCode("LTH") + LAZ = NewCode("LAZ") + LEA = NewCode("LEA") + LEAF = NewCode("LEAF") + LENIN = NewCode("LENIN") + LEPEN = NewCode("LEPEN") + LIR = NewCode("LIR") + LVG = NewCode("LVG") + LGBTQ = NewCode("LGBTQ") + LHC = NewCode("LHC") + EXT = NewCode("EXT") + LBTC = NewCode("LBTC") + LSD = NewCode("LSD") + LIMX = NewCode("LIMX") + LTD = NewCode("LTD") + LINDA = NewCode("LINDA") + LKC = NewCode("LKC") + LBTCX = NewCode("LBTCX") + LCC = NewCode("LCC") + LTCU = NewCode("LTCU") + LTCR = NewCode("LTCR") + LDOGE = NewCode("LDOGE") + LTS = NewCode("LTS") + LIV = NewCode("LIV") + LIZI = NewCode("LIZI") + LOC = NewCode("LOC") + LOCX = NewCode("LOCX") + LOOK = NewCode("LOOK") + LOOT = NewCode("LOOT") + XLTCG = NewCode("XLTCG") + BASH = NewCode("BASH") + LUCKY = NewCode("LUCKY") + L7S = NewCode("L7S") + LDM = NewCode("LDM") + LUMI = NewCode("LUMI") + LUNA = NewCode("LUNA") + LC = NewCode("LC") + LUX = NewCode("LUX") + MCRN = NewCode("MCRN") + XMG = NewCode("XMG") + MMXIV = NewCode("MMXIV") + MAT = NewCode("MAT") + MAO = NewCode("MAO") + MAPC = NewCode("MAPC") + MRB = NewCode("MRB") + MXT = NewCode("MXT") + MARV = NewCode("MARV") + MARX = NewCode("MARX") + MCAR = NewCode("MCAR") + MM = NewCode("MM") + MVC = NewCode("MVC") + MAVRO = NewCode("MAVRO") + MAX = NewCode("MAX") + MAZE = NewCode("MAZE") + MBIT = NewCode("MBIT") + MCOIN = NewCode("MCOIN") + MPRO = NewCode("MPRO") + XMS = NewCode("XMS") + MLITE = NewCode("MLITE") + MLNC = NewCode("MLNC") + MENTAL = NewCode("MENTAL") + MERGEC = NewCode("MERGEC") + MTLMC3 = NewCode("MTLMC3") + METAL = NewCode("METAL") + MUU = NewCode("MUU") + MILO = NewCode("MILO") + MND = NewCode("MND") + XMINE = NewCode("XMINE") + MNM = NewCode("MNM") + XNM = NewCode("XNM") + MIRO = NewCode("MIRO") + MIS = NewCode("MIS") + MMXVI = NewCode("MMXVI") + MOIN = NewCode("MOIN") + MOJO = NewCode("MOJO") + TAB = NewCode("TAB") + MONETA = NewCode("MONETA") + MUE = NewCode("MUE") + MONEY = NewCode("MONEY") + MRP = NewCode("MRP") + MOTO = NewCode("MOTO") + MULTI = NewCode("MULTI") + MST = NewCode("MST") + MVR = NewCode("MVR") + MYSTIC = NewCode("MYSTIC") + WISH = NewCode("WISH") + NKT = NewCode("NKT") + NAT = NewCode("NAT") + ENAU = NewCode("ENAU") + NEBU = NewCode("NEBU") + NEF = NewCode("NEF") + NBIT = NewCode("NBIT") + NETKO = NewCode("NETKO") + NTM = NewCode("NTM") + NETC = NewCode("NETC") + NRC = NewCode("NRC") + NTK = NewCode("NTK") + NTRN = NewCode("NTRN") + NEVA = NewCode("NEVA") + NIC = NewCode("NIC") + NKC = NewCode("NKC") + NYC = NewCode("NYC") + NZC = NewCode("NZC") + NICE = NewCode("NICE") + NDOGE = NewCode("NDOGE") + XTR = NewCode("XTR") + N2O = NewCode("N2O") + NIXON = NewCode("NIXON") + NOC = NewCode("NOC") + NODC = NewCode("NODC") + NODES = NewCode("NODES") + NODX = NewCode("NODX") + NLC = NewCode("NLC") + NLC2 = NewCode("NLC2") + NOO = NewCode("NOO") + NVC = NewCode("NVC") + NPC = NewCode("NPC") + NUBIS = NewCode("NUBIS") + NUKE = NewCode("NUKE") + N7 = NewCode("N7") + NUM = NewCode("NUM") + NMR = NewCode("NMR") + NXE = NewCode("NXE") + OBS = NewCode("OBS") + OCEAN = NewCode("OCEAN") + OCOW = NewCode("OCOW") + EIGHT88 = NewCode("888") + OCC = NewCode("OCC") + OK = NewCode("OK") + ODNT = NewCode("ODNT") + FLAV = NewCode("FLAV") + OLIT = NewCode("OLIT") + OLYMP = NewCode("OLYMP") + OMA = NewCode("OMA") + OMC = NewCode("OMC") + ONEK = NewCode("ONEK") + ONX = NewCode("ONX") + XPO = NewCode("XPO") + OPAL = NewCode("OPAL") + OTN = NewCode("OTN") + OP = NewCode("OP") + OPES = NewCode("OPES") + OPTION = NewCode("OPTION") + ORLY = NewCode("ORLY") + OS76 = NewCode("OS76") + OZC = NewCode("OZC") + P7C = NewCode("P7C") + PAC = NewCode("PAC") + PAK = NewCode("PAK") + PAL = NewCode("PAL") + PND = NewCode("PND") + PINKX = NewCode("PINKX") + POPPY = NewCode("POPPY") + DUO = NewCode("DUO") + PARA = NewCode("PARA") + PKB = NewCode("PKB") + GENE = NewCode("GENE") + PARTY = NewCode("PARTY") + PYN = NewCode("PYN") + XPY = NewCode("XPY") + CON = NewCode("CON") + PAYP = NewCode("PAYP") + GUESS = NewCode("GUESS") + PEN = NewCode("PEN") + PTA = NewCode("PTA") + PEO = NewCode("PEO") + PSB = NewCode("PSB") + XPD = NewCode("XPD") + PXL = NewCode("PXL") + PHR = NewCode("PHR") + PIE = NewCode("PIE") + PIO = NewCode("PIO") + PIPR = NewCode("PIPR") + SKULL = NewCode("SKULL") + PLANET = NewCode("PLANET") + PNC = NewCode("PNC") + XPTX = NewCode("XPTX") + PLNC = NewCode("PLNC") + XPS = NewCode("XPS") + POKE = NewCode("POKE") + PLBT = NewCode("PLBT") + POM = NewCode("POM") + PONZ2 = NewCode("PONZ2") + PONZI = NewCode("PONZI") + XSP = NewCode("XSP") + XPC = NewCode("XPC") + PEX = NewCode("PEX") + TRON = NewCode("TRON") + POST = NewCode("POST") + POSW = NewCode("POSW") + PWR = NewCode("PWR") + POWER = NewCode("POWER") + PRE = NewCode("PRE") + PRS = NewCode("PRS") + PXI = NewCode("PXI") + PEXT = NewCode("PEXT") + PRIMU = NewCode("PRIMU") + PRX = NewCode("PRX") + PRM = NewCode("PRM") + PRIX = NewCode("PRIX") + XPRO = NewCode("XPRO") + PCM = NewCode("PCM") + PROC = NewCode("PROC") + NANOX = NewCode("NANOX") + VRP = NewCode("VRP") + PTY = NewCode("PTY") + PSI = NewCode("PSI") + PSY = NewCode("PSY") + PULSE = NewCode("PULSE") + PUPA = NewCode("PUPA") + PURE = NewCode("PURE") + VIDZ = NewCode("VIDZ") + PUTIN = NewCode("PUTIN") + PX = NewCode("PX") + QTM = NewCode("QTM") + QTZ = NewCode("QTZ") + QBC = NewCode("QBC") + XQN = NewCode("XQN") + RBBT = NewCode("RBBT") + RAC = NewCode("RAC") + RADI = NewCode("RADI") + RAD = NewCode("RAD") + RAI = NewCode("RAI") + XRA = NewCode("XRA") + RATIO = NewCode("RATIO") + REA = NewCode("REA") + RCX = NewCode("RCX") + REE = NewCode("REE") + REC = NewCode("REC") + RMS = NewCode("RMS") + RBIT = NewCode("RBIT") + RNC = NewCode("RNC") + REV = NewCode("REV") + RH = NewCode("RH") + XRL = NewCode("XRL") + RICE = NewCode("RICE") + RICHX = NewCode("RICHX") + RID = NewCode("RID") + RIDE = NewCode("RIDE") + RBT = NewCode("RBT") + RING = NewCode("RING") + RIO = NewCode("RIO") + RISE = NewCode("RISE") + ROCKET = NewCode("ROCKET") + RPC = NewCode("RPC") + ROS = NewCode("ROS") + ROYAL = NewCode("ROYAL") + RSGP = NewCode("RSGP") + RBIES = NewCode("RBIES") + RUBIT = NewCode("RUBIT") + RBY = NewCode("RBY") + RUC = NewCode("RUC") + RUPX = NewCode("RUPX") + RUP = NewCode("RUP") + RUST = NewCode("RUST") + SFE = NewCode("SFE") + SLS = NewCode("SLS") + SMSR = NewCode("SMSR") + RONIN = NewCode("RONIN") + STV = NewCode("STV") + HIFUN = NewCode("HIFUN") + MAD = NewCode("MAD") + SANDG = NewCode("SANDG") + STO = NewCode("STO") + SCAN = NewCode("SCAN") + SCITW = NewCode("SCITW") + SCRPT = NewCode("SCRPT") + SCRT = NewCode("SCRT") + SED = NewCode("SED") + SEEDS = NewCode("SEEDS") + B2X = NewCode("B2X") + SEL = NewCode("SEL") + SLFI = NewCode("SLFI") + SMBR = NewCode("SMBR") + SEN = NewCode("SEN") + SENT = NewCode("SENT") + SRNT = NewCode("SRNT") + SEV = NewCode("SEV") + SP = NewCode("SP") + SXC = NewCode("SXC") + GELD = NewCode("GELD") + SHDW = NewCode("SHDW") + SDC = NewCode("SDC") + SAK = NewCode("SAK") + SHRP = NewCode("SHRP") + SHELL = NewCode("SHELL") + SH = NewCode("SH") + SHORTY = NewCode("SHORTY") + SHREK = NewCode("SHREK") + SHRM = NewCode("SHRM") + SIB = NewCode("SIB") + SIGT = NewCode("SIGT") + SLCO = NewCode("SLCO") + SIGU = NewCode("SIGU") + SIX = NewCode("SIX") + SJW = NewCode("SJW") + SKB = NewCode("SKB") + SW = NewCode("SW") + SLEEP = NewCode("SLEEP") + SLING = NewCode("SLING") + SMART = NewCode("SMART") + SMC = NewCode("SMC") + SMF = NewCode("SMF") + SOCC = NewCode("SOCC") + SCL = NewCode("SCL") + SDAO = NewCode("SDAO") + SOLAR = NewCode("SOLAR") + SOLO = NewCode("SOLO") + SCT = NewCode("SCT") + SONG = NewCode("SONG") + ALTCOM = NewCode("ALTCOM") + SPHTX = NewCode("SPHTX") + SPC = NewCode("SPC") + SPACE = NewCode("SPACE") + SBT = NewCode("SBT") + SPEC = NewCode("SPEC") + SPX = NewCode("SPX") + SCS = NewCode("SCS") + SPORT = NewCode("SPORT") + SPT = NewCode("SPT") + SPR = NewCode("SPR") + SPEX = NewCode("SPEX") + SQL = NewCode("SQL") + SBIT = NewCode("SBIT") + STHR = NewCode("STHR") + STALIN = NewCode("STALIN") + STAR = NewCode("STAR") + STA = NewCode("STA") + START = NewCode("START") + STP = NewCode("STP") + PNK = NewCode("PNK") + STEPS = NewCode("STEPS") + STK = NewCode("STK") + STONK = NewCode("STONK") + STS = NewCode("STS") + STRP = NewCode("STRP") + STY = NewCode("STY") + XMT = NewCode("XMT") + SSTC = NewCode("SSTC") + SUPER = NewCode("SUPER") + SRND = NewCode("SRND") + STRB = NewCode("STRB") + M1 = NewCode("M1") + SPM = NewCode("SPM") + BUCKS = NewCode("BUCKS") + TOKEN = NewCode("TOKEN") + SWT = NewCode("SWT") + SWEET = NewCode("SWEET") + SWING = NewCode("SWING") + CHSB = NewCode("CHSB") + SIC = NewCode("SIC") + SDP = NewCode("SDP") + XSY = NewCode("XSY") + SYNX = NewCode("SYNX") + SNRG = NewCode("SNRG") + TAG = NewCode("TAG") + TAGR = NewCode("TAGR") + TAJ = NewCode("TAJ") + TAK = NewCode("TAK") + TAKE = NewCode("TAKE") + TAM = NewCode("TAM") + XTO = NewCode("XTO") + TAP = NewCode("TAP") + TLE = NewCode("TLE") + TSE = NewCode("TSE") + TLEX = NewCode("TLEX") + TAXI = NewCode("TAXI") + TCN = NewCode("TCN") + TDFB = NewCode("TDFB") + TEAM = NewCode("TEAM") + TECH = NewCode("TECH") + TEC = NewCode("TEC") + TEK = NewCode("TEK") + TB = NewCode("TB") + TLX = NewCode("TLX") + TELL = NewCode("TELL") + TENNET = NewCode("TENNET") + TES = NewCode("TES") + TGS = NewCode("TGS") + XVE = NewCode("XVE") + TCR = NewCode("TCR") + GCC = NewCode("GCC") + MAY = NewCode("MAY") + THOM = NewCode("THOM") + TIA = NewCode("TIA") + TIDE = NewCode("TIDE") + TIE = NewCode("TIE") + TIT = NewCode("TIT") + TTC = NewCode("TTC") + TODAY = NewCode("TODAY") + TBX = NewCode("TBX") + TDS = NewCode("TDS") + TLOSH = NewCode("TLOSH") + TOKC = NewCode("TOKC") + TMRW = NewCode("TMRW") + TOOL = NewCode("TOOL") + TCX = NewCode("TCX") + TOT = NewCode("TOT") + TX = NewCode("TX") + TRANSF = NewCode("TRANSF") + TRAP = NewCode("TRAP") + TBCX = NewCode("TBCX") + TRICK = NewCode("TRICK") + TPG = NewCode("TPG") + TFL = NewCode("TFL") + TRUMP = NewCode("TRUMP") + TNG = NewCode("TNG") + TUR = NewCode("TUR") + TWERK = NewCode("TWERK") + TWIST = NewCode("TWIST") + TWO = NewCode("TWO") + UCASH = NewCode("UCASH") + UAE = NewCode("UAE") + XBU = NewCode("XBU") + UBQ = NewCode("UBQ") + U = NewCode("U") + UDOWN = NewCode("UDOWN") + GAIN = NewCode("GAIN") + USC = NewCode("USC") + UMC = NewCode("UMC") + UNF = NewCode("UNF") + UNIFY = NewCode("UNIFY") + USDE = NewCode("USDE") + UBTC = NewCode("UBTC") + UIS = NewCode("UIS") + UNIT = NewCode("UNIT") + UNI = NewCode("UNI") + UXC = NewCode("UXC") + URC = NewCode("URC") + XUP = NewCode("XUP") + UFR = NewCode("UFR") + URO = NewCode("URO") + UTLE = NewCode("UTLE") + VAL = NewCode("VAL") + VPRC = NewCode("VPRC") + VAPOR = NewCode("VAPOR") + VCOIN = NewCode("VCOIN") + VEC = NewCode("VEC") + VEC2 = NewCode("VEC2") + VLT = NewCode("VLT") + VENE = NewCode("VENE") + VNTX = NewCode("VNTX") + VTN = NewCode("VTN") + CRED = NewCode("CRED") + VERS = NewCode("VERS") + VTX = NewCode("VTX") + VTY = NewCode("VTY") + VIP = NewCode("VIP") + VISIO = NewCode("VISIO") + VK = NewCode("VK") + VOL = NewCode("VOL") + VOYA = NewCode("VOYA") + VPN = NewCode("VPN") + XVS = NewCode("XVS") + VTL = NewCode("VTL") + VULC = NewCode("VULC") + VVI = NewCode("VVI") + WGR = NewCode("WGR") + WAM = NewCode("WAM") + WARP = NewCode("WARP") + WASH = NewCode("WASH") + WGO = NewCode("WGO") + WAY = NewCode("WAY") + WCASH = NewCode("WCASH") + WEALTH = NewCode("WEALTH") + WEEK = NewCode("WEEK") + WHO = NewCode("WHO") + WIC = NewCode("WIC") + WBB = NewCode("WBB") + WINE = NewCode("WINE") + WINK = NewCode("WINK") + WISC = NewCode("WISC") + WITCH = NewCode("WITCH") + WMC = NewCode("WMC") + WOMEN = NewCode("WOMEN") + WOK = NewCode("WOK") + WRT = NewCode("WRT") + XCO = NewCode("XCO") + X2 = NewCode("X2") + XNX = NewCode("XNX") + XAU = NewCode("XAU") + XAV = NewCode("XAV") + XDE2 = NewCode("XDE2") + XDE = NewCode("XDE") + XIOS = NewCode("XIOS") + XOC = NewCode("XOC") + XSSX = NewCode("XSSX") + XBY = NewCode("XBY") + YAC = NewCode("YAC") + YMC = NewCode("YMC") + YAY = NewCode("YAY") + YBC = NewCode("YBC") + YES = NewCode("YES") + YOB2X = NewCode("YOB2X") + YOVI = NewCode("YOVI") + ZYD = NewCode("ZYD") + ZECD = NewCode("ZECD") + ZEIT = NewCode("ZEIT") + ZENI = NewCode("ZENI") + ZET2 = NewCode("ZET2") + ZET = NewCode("ZET") + ZMC = NewCode("ZMC") + ZIRK = NewCode("ZIRK") + ZLQ = NewCode("ZLQ") + ZNE = NewCode("ZNE") + ZONTO = NewCode("ZONTO") + ZOOM = NewCode("ZOOM") + ZRC = NewCode("ZRC") + ZUR = NewCode("ZUR") + ZB = NewCode("ZB") + QC = NewCode("QC") + HLC = NewCode("HLC") + SAFE = NewCode("SAFE") + BTN = NewCode("BTN") + CDC = NewCode("CDC") + DDM = NewCode("DDM") + HOTC = NewCode("HOTC") + BDS = NewCode("BDS") + AAA = NewCode("AAA") + XWC = NewCode("XWC") + PDX = NewCode("PDX") + SLT = NewCode("SLT") + HPY = NewCode("HPY") + XXBT = NewCode("XXBT") // BTC, but XXBT instead + XDG = NewCode("XDG") // DOGE + HKD = NewCode("HKD") // Hong Kong Dollar + AUD = NewCode("AUD") // Australian Dollar + USD = NewCode("USD") // United States Dollar + ZUSD = NewCode("ZUSD") // United States Dollar, but with a Z in front of it + EUR = NewCode("EUR") // Euro + ZEUR = NewCode("ZEUR") // Euro, but with a Z in front of it + CAD = NewCode("CAD") // Canadaian Dollar + ZCAD = NewCode("ZCAD") // Canadaian Dollar, but with a Z in front of it + SGD = NewCode("SGD") // Singapore Dollar + RUB = NewCode("RUB") // RUssian ruBle + RUR = NewCode("RUR") // RUssian Ruble + PLN = NewCode("PLN") // Polish złoty + TRY = NewCode("TRY") // Turkish lira + UAH = NewCode("UAH") // Ukrainian hryvnia + JPY = NewCode("JPY") // Japanese yen + ZJPY = NewCode("ZJPY") // Japanese yen, but with a Z in front of it + LCH = NewCode("LCH") + MYR = NewCode("MYR") + AFN = NewCode("AFN") + ARS = NewCode("ARS") + AWG = NewCode("AWG") + AZN = NewCode("AZN") + BSD = NewCode("BSD") + BBD = NewCode("BBD") + BYN = NewCode("BYN") + BZD = NewCode("BZD") + BMD = NewCode("BMD") + BOB = NewCode("BOB") + BAM = NewCode("BAM") + BWP = NewCode("BWP") + BGN = NewCode("BGN") + BRL = NewCode("BRL") + BND = NewCode("BND") + KHR = NewCode("KHR") + KYD = NewCode("KYD") + CLP = NewCode("CLP") + CNY = NewCode("CNY") + COP = NewCode("COP") + HRK = NewCode("HRK") + CUP = NewCode("CUP") + CZK = NewCode("CZK") + DKK = NewCode("DKK") + DOP = NewCode("DOP") + XCD = NewCode("XCD") + EGP = NewCode("EGP") + SVC = NewCode("SVC") + FKP = NewCode("FKP") + FJD = NewCode("FJD") + GIP = NewCode("GIP") + GTQ = NewCode("GTQ") + GGP = NewCode("GGP") + GYD = NewCode("GYD") + HNL = NewCode("HNL") + HUF = NewCode("HUF") + ISK = NewCode("ISK") + INR = NewCode("INR") + IDR = NewCode("IDR") + IRR = NewCode("IRR") + IMP = NewCode("IMP") + ILS = NewCode("ILS") + JMD = NewCode("JMD") + JEP = NewCode("JEP") + KZT = NewCode("KZT") + KPW = NewCode("KPW") + KGS = NewCode("KGS") + LAK = NewCode("LAK") + LBP = NewCode("LBP") + LRD = NewCode("LRD") + MKD = NewCode("MKD") + MUR = NewCode("MUR") + MXN = NewCode("MXN") + MNT = NewCode("MNT") + MZN = NewCode("MZN") + NAD = NewCode("NAD") + NPR = NewCode("NPR") + ANG = NewCode("ANG") + NZD = NewCode("NZD") + NIO = NewCode("NIO") + NGN = NewCode("NGN") + NOK = NewCode("NOK") + OMR = NewCode("OMR") + PKR = NewCode("PKR") + PAB = NewCode("PAB") + PYG = NewCode("PYG") + PHP = NewCode("PHP") + QAR = NewCode("QAR") + RON = NewCode("RON") + SHP = NewCode("SHP") + SAR = NewCode("SAR") + RSD = NewCode("RSD") + SCR = NewCode("SCR") + SOS = NewCode("SOS") + ZAR = NewCode("ZAR") + LKR = NewCode("LKR") + SEK = NewCode("SEK") + CHF = NewCode("CHF") + SRD = NewCode("SRD") + SYP = NewCode("SYP") + TWD = NewCode("TWD") + THB = NewCode("THB") + TTD = NewCode("TTD") + TVD = NewCode("TVD") + GBP = NewCode("GBP") + UYU = NewCode("UYU") + UZS = NewCode("UZS") + VEF = NewCode("VEF") + VND = NewCode("VND") + YER = NewCode("YER") + ZWD = NewCode("ZWD") + XETH = NewCode("XETH") + FX_BTC = NewCode("FX_BTC") // nolint: golint,stylecheck +) diff --git a/currency/coinmarketcap/coinmarketcap.go b/currency/coinmarketcap/coinmarketcap.go index a6bf2541..5e562989 100644 --- a/currency/coinmarketcap/coinmarketcap.go +++ b/currency/coinmarketcap/coinmarketcap.go @@ -18,53 +18,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/request" ) -// Coinmarketcap account plan bitmasks, url and enpoint consts -const ( - Basic uint8 = 1 << iota - Hobbyist - Startup - Standard - Professional - Enterprise - - baseURL = "https://pro-api.coinmarketcap.com" - sandboxURL = "https://sandbox-api.coinmarketcap.com" - version = "/v1/" - - endpointCryptocurrencyInfo = "cryptocurrency/info" - endpointCryptocurrencyMap = "cryptocurrency/map" - endpointCryptocurrencyHistoricalListings = "cryptocurrency/listings/historical" - endpointCryptocurrencyLatestListings = "cryptocurrency/listings/latest" - endpointCryptocurrencyMarketPairs = "cryptocurrency/market-pairs/latest" - endpointOHLCVHistorical = "cryptocurrency/ohlcv/historical" - endpointOHLCVLatest = "cryptocurrency/ohlcv/latest" - endpointGetMarketQuotesHistorical = "cryptocurrency/quotes/historical" - endpointGetMarketQuotesLatest = "cryptocurrency/quotes/latest" - endpointExchangeInfo = "exchange/info" - endpointExchangeMap = "exchange/map" - endpointExchangeMarketPairsLatest = "exchange/market-pairs/latest" - endpointExchangeMarketQuoteHistorical = "exchange/quotes/historical" - endpointExchangeMarketQuoteLatest = "exchange/quotes/latest" - endpointGlobalQuoteHistorical = "global-metrics/quotes/historical" - endpointGlobalQuoteLatest = "global-metrics/quotes/latest" - endpointPriceConversion = "tools/price-conversion" - - authrate = 0 - defaultTimeOut = time.Second * 15 -) - -// Coinmarketcap is the overarching type across this package -type Coinmarketcap struct { - Verbose bool - Enabled bool - Name string - APIkey string - APIUrl string - APIVersion string - Plan uint8 - Requester *request.Requester -} - // SetDefaults sets default values for the exchange func (c *Coinmarketcap) SetDefaults() { c.Name = "CoinMarketCap" @@ -82,6 +35,7 @@ func (c *Coinmarketcap) SetDefaults() { func (c *Coinmarketcap) Setup(conf Settings) error { if !conf.Enabled { c.Enabled = false + return nil } c.Enabled = true diff --git a/currency/coinmarketcap/coinmarketcap_test.go b/currency/coinmarketcap/coinmarketcap_test.go index 470d5ba7..e81f107f 100644 --- a/currency/coinmarketcap/coinmarketcap_test.go +++ b/currency/coinmarketcap/coinmarketcap_test.go @@ -20,7 +20,7 @@ const ( func areAPICredtionalsSet(minAllowable uint8) bool { if apiAccountPlanLevel != "" && apikey != "" { if err := c.CheckAccountPlan(minAllowable); err != nil { - log.Warn("coinmarketpcap test suite - account plan not allowed for function, please review or upgrade plan to test") + log.Warn(log.Global, "coinmarketpcap test suite - account plan not allowed for function, please review or upgrade plan to test") return false } return true @@ -58,32 +58,32 @@ func TestCheckAccountPlan(t *testing.T) { if areAPICredtionalsSet(Basic) { err := c.CheckAccountPlan(Enterprise) if err == nil { - t.Error("Test Failed - CheckAccountPlan() error cannot be nil") + t.Error("CheckAccountPlan() error cannot be nil") } err = c.CheckAccountPlan(Professional) if err == nil { - t.Error("Test Failed - CheckAccountPlan() error cannot be nil") + t.Error("CheckAccountPlan() error cannot be nil") } err = c.CheckAccountPlan(Standard) if err == nil { - t.Error("Test Failed - CheckAccountPlan() error cannot be nil") + t.Error("CheckAccountPlan() error cannot be nil") } err = c.CheckAccountPlan(Hobbyist) if err == nil { - t.Error("Test Failed - CheckAccountPlan() error cannot be nil") + t.Error("CheckAccountPlan() error cannot be nil") } err = c.CheckAccountPlan(Startup) if err == nil { - t.Error("Test Failed - CheckAccountPlan() error cannot be nil") + t.Error("CheckAccountPlan() error cannot be nil") } err = c.CheckAccountPlan(Basic) if err != nil { - t.Error("Test Failed - CheckAccountPlan() error", err) + t.Error("CheckAccountPlan() error", err) } } } @@ -94,11 +94,11 @@ func TestGetCryptocurrencyInfo(t *testing.T) { _, err := c.GetCryptocurrencyInfo(1) if areAPICredtionalsSet(Basic) { if err != nil { - t.Error("Test Failed - GetCryptocurrencyInfo() error", err) + t.Error("GetCryptocurrencyInfo() error", err) } } else { if err == nil { - t.Error("Test Failed - GetCryptocurrencyInfo() error cannot be nil") + t.Error("GetCryptocurrencyInfo() error cannot be nil") } } } @@ -109,11 +109,11 @@ func TestGetCryptocurrencyIDMap(t *testing.T) { _, err := c.GetCryptocurrencyIDMap() if areAPICredtionalsSet(Basic) { if err != nil { - t.Error("Test Failed - GetCryptocurrencyIDMap() error", err) + t.Error("GetCryptocurrencyIDMap() error", err) } } else { if err == nil { - t.Error("Test Failed - GetCryptocurrencyIDMap() error cannot be nil") + t.Error("GetCryptocurrencyIDMap() error cannot be nil") } } } @@ -123,7 +123,7 @@ func TestGetCryptocurrencyHistoricalListings(t *testing.T) { TestSetup(t) _, err := c.GetCryptocurrencyHistoricalListings() if err == nil { - t.Error("Test Failed - GetCryptocurrencyHistoricalListings() error cannot be nil") + t.Error("GetCryptocurrencyHistoricalListings() error cannot be nil") } } @@ -133,11 +133,11 @@ func TestGetCryptocurrencyLatestListing(t *testing.T) { _, err := c.GetCryptocurrencyLatestListing(0, 0) if areAPICredtionalsSet(Basic) { if err != nil { - t.Error("Test Failed - GetCryptocurrencyLatestListing() error", err) + t.Error("GetCryptocurrencyLatestListing() error", err) } } else { if err == nil { - t.Error("Test Failed - GetCryptocurrencyLatestListing() error cannot be nil") + t.Error("GetCryptocurrencyLatestListing() error cannot be nil") } } } @@ -148,12 +148,12 @@ func TestGetCryptocurrencyLatestMarketPairs(t *testing.T) { _, err := c.GetCryptocurrencyLatestMarketPairs(1, 0, 0) if areAPICredtionalsSet(Standard) { if err != nil { - t.Error("Test Failed - GetCryptocurrencyLatestMarketPairs() error", + t.Error("GetCryptocurrencyLatestMarketPairs() error", err) } } else { if err == nil { - t.Error("Test Failed - GetCryptocurrencyLatestMarketPairs() error cannot be nil") + t.Error("GetCryptocurrencyLatestMarketPairs() error cannot be nil") } } } @@ -164,12 +164,12 @@ func TestGetCryptocurrencyOHLCHistorical(t *testing.T) { _, err := c.GetCryptocurrencyOHLCHistorical(1, time.Now(), time.Now()) if areAPICredtionalsSet(Standard) { if err != nil { - t.Error("Test Failed - GetCryptocurrencyOHLCHistorical() error", + t.Error("GetCryptocurrencyOHLCHistorical() error", err) } } else { if err == nil { - t.Error("Test Failed - GetCryptocurrencyOHLCHistorical() error cannot be nil") + t.Error("GetCryptocurrencyOHLCHistorical() error cannot be nil") } } } @@ -180,12 +180,12 @@ func TestGetCryptocurrencyOHLCLatest(t *testing.T) { _, err := c.GetCryptocurrencyOHLCLatest(1) if areAPICredtionalsSet(Startup) { if err != nil { - t.Error("Test Failed - GetCryptocurrencyOHLCLatest() error", + t.Error("GetCryptocurrencyOHLCLatest() error", err) } } else { if err == nil { - t.Error("Test Failed - GetCryptocurrencyOHLCLatest() error cannot be nil") + t.Error("GetCryptocurrencyOHLCLatest() error cannot be nil") } } } @@ -196,12 +196,12 @@ func TestGetCryptocurrencyLatestQuotes(t *testing.T) { _, err := c.GetCryptocurrencyLatestQuotes(1) if areAPICredtionalsSet(Basic) { if err != nil { - t.Error("Test Failed - GetCryptocurrencyLatestQuotes() error", + t.Error("GetCryptocurrencyLatestQuotes() error", err) } } else { if err == nil { - t.Error("Test Failed - GetCryptocurrencyLatestQuotes() error cannot be nil") + t.Error("GetCryptocurrencyLatestQuotes() error cannot be nil") } } } @@ -212,12 +212,12 @@ func TestGetCryptocurrencyHistoricalQuotes(t *testing.T) { _, err := c.GetCryptocurrencyHistoricalQuotes(1, time.Now(), time.Now()) if areAPICredtionalsSet(Standard) { if err != nil { - t.Error("Test Failed - GetCryptocurrencyHistoricalQuotes() error", + t.Error("GetCryptocurrencyHistoricalQuotes() error", err) } } else { if err == nil { - t.Error("Test Failed - GetCryptocurrencyHistoricalQuotes() error cannot be nil") + t.Error("GetCryptocurrencyHistoricalQuotes() error cannot be nil") } } } @@ -228,12 +228,12 @@ func TestGetExchangeInfo(t *testing.T) { _, err := c.GetExchangeInfo(1) if areAPICredtionalsSet(Startup) { if err != nil { - t.Error("Test Failed - GetExchangeInfo() error", + t.Error("GetExchangeInfo() error", err) } } else { if err == nil { - t.Error("Test Failed - GetExchangeInfo() error cannot be nil") + t.Error("GetExchangeInfo() error cannot be nil") } } } @@ -244,12 +244,12 @@ func TestGetExchangeMap(t *testing.T) { _, err := c.GetExchangeMap(0, 0) if areAPICredtionalsSet(Startup) { if err != nil { - t.Error("Test Failed - GetExchangeMap() error", + t.Error("GetExchangeMap() error", err) } } else { if err == nil { - t.Error("Test Failed - GetExchangeMap() error cannot be nil") + t.Error("GetExchangeMap() error cannot be nil") } } } @@ -260,7 +260,7 @@ func TestGetExchangeHistoricalListings(t *testing.T) { _, err := c.GetExchangeHistoricalListings() if err == nil { // TODO: update this once the feature above is implemented - t.Error("Test Failed - GetExchangeHistoricalListings() error cannot be nil") + t.Error("GetExchangeHistoricalListings() error cannot be nil") } } @@ -270,7 +270,7 @@ func TestGetExchangeLatestListings(t *testing.T) { _, err := c.GetExchangeLatestListings() if err == nil { // TODO: update this once the feature above is implemented - t.Error("Test Failed - GetExchangeHistoricalListings() error cannot be nil") + t.Error("GetExchangeHistoricalListings() error cannot be nil") } } @@ -280,12 +280,12 @@ func TestGetExchangeLatestMarketPairs(t *testing.T) { _, err := c.GetExchangeLatestMarketPairs(1, 0, 0) if areAPICredtionalsSet(Standard) { if err != nil { - t.Error("Test Failed - GetExchangeLatestMarketPairs() error", + t.Error("GetExchangeLatestMarketPairs() error", err) } } else { if err == nil { - t.Error("Test Failed - GetExchangeLatestMarketPairs() error cannot be nil") + t.Error("GetExchangeLatestMarketPairs() error cannot be nil") } } } @@ -296,12 +296,12 @@ func TestGetExchangeLatestQuotes(t *testing.T) { _, err := c.GetExchangeLatestQuotes(1) if areAPICredtionalsSet(Standard) { if err != nil { - t.Error("Test Failed - GetExchangeLatestQuotes() error", + t.Error("GetExchangeLatestQuotes() error", err) } } else { if err == nil { - t.Error("Test Failed - GetExchangeLatestQuotes() error cannot be nil") + t.Error("GetExchangeLatestQuotes() error cannot be nil") } } } @@ -312,12 +312,12 @@ func TestGetExchangeHistoricalQuotes(t *testing.T) { _, err := c.GetExchangeHistoricalQuotes(1, time.Now(), time.Now()) if areAPICredtionalsSet(Standard) { if err != nil { - t.Error("Test Failed - GetExchangeHistoricalQuotes() error", + t.Error("GetExchangeHistoricalQuotes() error", err) } } else { if err == nil { - t.Error("Test Failed - GetExchangeHistoricalQuotes() error cannot be nil") + t.Error("GetExchangeHistoricalQuotes() error cannot be nil") } } } @@ -328,12 +328,12 @@ func TestGetGlobalMeticLatestQuotes(t *testing.T) { _, err := c.GetGlobalMeticLatestQuotes() if areAPICredtionalsSet(Basic) { if err != nil { - t.Error("Test Failed - GetGlobalMeticLatestQuotes() error", + t.Error("GetGlobalMeticLatestQuotes() error", err) } } else { if err == nil { - t.Error("Test Failed - GetGlobalMeticLatestQuotes() error cannot be nil") + t.Error("GetGlobalMeticLatestQuotes() error cannot be nil") } } } @@ -344,12 +344,12 @@ func TestGetGlobalMeticHistoricalQuotes(t *testing.T) { _, err := c.GetGlobalMeticHistoricalQuotes(time.Now(), time.Now()) if areAPICredtionalsSet(Standard) { if err != nil { - t.Error("Test Failed - GetGlobalMeticHistoricalQuotes() error", + t.Error("GetGlobalMeticHistoricalQuotes() error", err) } } else { if err == nil { - t.Error("Test Failed - GetGlobalMeticHistoricalQuotes() error cannot be nil") + t.Error("GetGlobalMeticHistoricalQuotes() error cannot be nil") } } } @@ -360,12 +360,12 @@ func TestGetPriceConversion(t *testing.T) { _, err := c.GetPriceConversion(0, 1, time.Now()) if areAPICredtionalsSet(Hobbyist) { if err != nil { - t.Error("Test Failed - GetPriceConversion() error", + t.Error("GetPriceConversion() error", err) } } else { if err == nil { - t.Error("Test Failed - GetPriceConversion() error cannot be nil") + t.Error("GetPriceConversion() error cannot be nil") } } } @@ -375,38 +375,38 @@ func TestSetAccountPlan(t *testing.T) { for _, plan := range accPlans { err := c.SetAccountPlan(plan) if err != nil { - t.Error("Test Failed - SetAccountPlan() error", err) + t.Error("SetAccountPlan() error", err) } switch plan { case "basic": if c.Plan != Basic { - t.Error("Test Failed - SetAccountPlan() error basic plan not set correctly") + t.Error("SetAccountPlan() error basic plan not set correctly") } case "startup": if c.Plan != Startup { - t.Error("Test Failed - SetAccountPlan() error startup plan not set correctly") + t.Error("SetAccountPlan() error startup plan not set correctly") } case "hobbyist": if c.Plan != Hobbyist { - t.Error("Test Failed - SetAccountPlan() error hobbyist plan not set correctly") + t.Error("SetAccountPlan() error hobbyist plan not set correctly") } case "standard": if c.Plan != Standard { - t.Error("Test Failed - SetAccountPlan() error standard plan not set correctly") + t.Error("SetAccountPlan() error standard plan not set correctly") } case "professional": if c.Plan != Professional { - t.Error("Test Failed - SetAccountPlan() error professional plan not set correctly") + t.Error("SetAccountPlan() error professional plan not set correctly") } case "enterprise": if c.Plan != Enterprise { - t.Error("Test Failed - SetAccountPlan() error enterprise plan not set correctly") + t.Error("SetAccountPlan() error enterprise plan not set correctly") } } } if err := c.SetAccountPlan("bra"); err == nil { - t.Error("Test Failed - SetAccountPlan() error cannot be nil") + t.Error("SetAccountPlan() error cannot be nil") } } diff --git a/currency/coinmarketcap/coinmarketcap_types.go b/currency/coinmarketcap/coinmarketcap_types.go index 95890a9f..7481165e 100644 --- a/currency/coinmarketcap/coinmarketcap_types.go +++ b/currency/coinmarketcap/coinmarketcap_types.go @@ -1,6 +1,57 @@ package coinmarketcap -import "time" +import ( + "time" + + "github.com/thrasher-corp/gocryptotrader/exchanges/request" +) + +// Coinmarketcap account plan bitmasks, url and enpoint consts +const ( + Basic uint8 = 1 << iota + Hobbyist + Startup + Standard + Professional + Enterprise + + baseURL = "https://pro-api.coinmarketcap.com" + sandboxURL = "https://sandbox-api.coinmarketcap.com" + version = "/v1/" + + endpointCryptocurrencyInfo = "cryptocurrency/info" + endpointCryptocurrencyMap = "cryptocurrency/map" + endpointCryptocurrencyHistoricalListings = "cryptocurrency/listings/historical" + endpointCryptocurrencyLatestListings = "cryptocurrency/listings/latest" + endpointCryptocurrencyMarketPairs = "cryptocurrency/market-pairs/latest" + endpointOHLCVHistorical = "cryptocurrency/ohlcv/historical" + endpointOHLCVLatest = "cryptocurrency/ohlcv/latest" + endpointGetMarketQuotesHistorical = "cryptocurrency/quotes/historical" + endpointGetMarketQuotesLatest = "cryptocurrency/quotes/latest" + endpointExchangeInfo = "exchange/info" + endpointExchangeMap = "exchange/map" + endpointExchangeMarketPairsLatest = "exchange/market-pairs/latest" + endpointExchangeMarketQuoteHistorical = "exchange/quotes/historical" + endpointExchangeMarketQuoteLatest = "exchange/quotes/latest" + endpointGlobalQuoteHistorical = "global-metrics/quotes/historical" + endpointGlobalQuoteLatest = "global-metrics/quotes/latest" + endpointPriceConversion = "tools/price-conversion" + + authrate = 0 + defaultTimeOut = time.Second * 15 +) + +// Coinmarketcap is the overarching type across this package +type Coinmarketcap struct { + Verbose bool + Enabled bool + Name string + APIkey string + APIUrl string + APIVersion string + Plan uint8 + Requester *request.Requester +} // Settings defines the current settings from configuration file type Settings struct { diff --git a/currency/conversion.go b/currency/conversion.go index 6df2afd0..5032e244 100644 --- a/currency/conversion.go +++ b/currency/conversion.go @@ -76,13 +76,15 @@ func (c *ConversionRates) Register(from, to Code) (Conversion, error) { p, ok := c.m[from.Item][to.Item] if !ok { - log.Errorf("currency conversion rate not found from %s to %s", from, to) + log.Errorf(log.Global, + "currency conversion rate not found from %s to %s\n", from, to) return Conversion{}, errors.New("no rate found") } i, ok := c.m[to.Item][from.Item] if !ok { - log.Errorf("currency conversion inversion rate not found from %s to %s", + log.Errorf(log.Global, + "currency conversion inversion rate not found from %s to %s\n", to, from) return Conversion{}, errors.New("no rate found") @@ -100,7 +102,7 @@ func (c *ConversionRates) Update(m map[string]float64) error { } if storage.IsVerbose() { - log.Debug("Conversion rates are being updated.") + log.Debugln(log.Global, "Conversion rates are being updated.") } solidvalues := make(map[Code]map[Code]float64) @@ -197,7 +199,8 @@ func (c *ConversionRates) Update(m map[string]float64) error { crossRate = 1 / v } if storage.IsVerbose() { - log.Debugf("Conversion from %s to %s deriving cross rate value %f", + log.Debugf(log.Global, + "Conversion from %s to %s deriving cross rate value %f\n", base, term, crossRate) @@ -251,11 +254,6 @@ func (c *ConversionRates) GetFullRates() Conversions { // Conversions define a list of conversion data type Conversions []Conversion -// Slice exposes the underlying Conversion slice type -func (c Conversions) Slice() []Conversion { - return c -} - // NewConversionFromString splits a string from a foreign exchange provider func NewConversionFromString(p string) (Conversion, error) { return NewConversionFromStrings(p[:3], p[3:]) @@ -299,7 +297,7 @@ func (c Conversion) String() string { return c.From.String() + c.To.String() } -// GetRate returns system rate if availabled +// GetRate returns system rate if available func (c Conversion) GetRate() (float64, error) { c.mtx.Lock() defer c.mtx.Unlock() diff --git a/currency/conversion_test.go b/currency/conversion_test.go index 18db2b71..83b859af 100644 --- a/currency/conversion_test.go +++ b/currency/conversion_test.go @@ -1,30 +1,30 @@ package currency import ( + "fmt" + "strings" "testing" - - "github.com/thrasher-corp/gocryptotrader/common" ) func TestNewConversionFromString(t *testing.T) { expected := "AUDUSD" conv, err := NewConversionFromString(expected) if err != nil { - t.Error("Test Failed - NewConversionFromString() error", err) + t.Error(err) } if conv.String() != expected { - t.Errorf("Test Failed - NewConversion() error expected %s but received %s", + t.Errorf("NewConversion() error expected %s but received %s", expected, conv) } - newexpected := common.StringToLower(expected) + newexpected := strings.ToLower(expected) conv, err = NewConversionFromString(newexpected) if err != nil { - t.Error("Test Failed - NewConversionFromString() error", err) + t.Error(err) } if conv.String() != newexpected { - t.Errorf("Test Failed - NewConversion() error expected %s but received %s", + t.Errorf("NewConversion() error expected %s but received %s", newexpected, conv) } @@ -37,11 +37,11 @@ func TestNewConversionFromStrings(t *testing.T) { conv, err := NewConversionFromStrings(from, to) if err != nil { - t.Error("Test Failed - NewConversionFromString() error", err) + t.Error(err) } if conv.String() != expected { - t.Errorf("Test Failed - NewConversion() error expected %s but received %s", + t.Errorf("NewConversion() error expected %s but received %s", expected, conv) } @@ -54,11 +54,11 @@ func TestNewConversion(t *testing.T) { conv, err := NewConversion(from, to) if err != nil { - t.Error("Test Failed - NewConversionFromCode() error", err) + t.Error(err) } if conv.String() != expected { - t.Errorf("Test Failed - NewConversion() error expected %s but received %s", + t.Errorf("NewConversion() error expected %s but received %s", expected, conv) } @@ -70,18 +70,18 @@ func TestConversionIsInvalid(t *testing.T) { conv, err := NewConversion(from, to) if err != nil { - t.Fatal("Test Failed - NewConversion() error", err) + t.Fatal(err) } if conv.IsInvalid() { - t.Errorf("Test Failed - IsInvalid() error expected false but received %v", + t.Errorf("IsInvalid() error expected false but received %v", conv.IsInvalid()) } to = AUD conv, err = NewConversion(from, to) if err == nil { - t.Fatal("Test Failed - NewConversion() error", err) + t.Error("Expected error") } } @@ -91,18 +91,18 @@ func TestConversionIsFiatPair(t *testing.T) { conv, err := NewConversion(from, to) if err != nil { - t.Fatal("Test Failed - NewConversion() error", err) + t.Fatal(err) } if !conv.IsFiat() { - t.Errorf("Test Failed - IsFiatPair() error expected true but received %v", + t.Errorf("IsFiatPair() error expected true but received %v", conv.IsFiat()) } to = LTC conv, err = NewConversion(from, to) if err == nil { - t.Fatal("Test Failed - NewConversion() error", err) + t.Error("Expected error") } } @@ -110,7 +110,7 @@ func TestConversionsRatesSystem(t *testing.T) { var SuperDuperConversionSystem ConversionRates if SuperDuperConversionSystem.HasData() { - t.Fatalf("Test Failed - HasData() error expected false but received %v", + t.Fatalf("HasData() error expected false but received %v", SuperDuperConversionSystem.HasData()) } @@ -142,16 +142,16 @@ func TestConversionsRatesSystem(t *testing.T) { err := SuperDuperConversionSystem.Update(testmap) if err != nil { - t.Fatal("Test Failed - Update() error", err) + t.Fatal(err) } err = SuperDuperConversionSystem.Update(nil) if err == nil { - t.Fatal("Test Failed - Update() error cannot be nil") + t.Fatal("Update() error cannot be nil") } if !SuperDuperConversionSystem.HasData() { - t.Fatalf("Test Failed - HasData() error expected true but received %v", + t.Fatalf("HasData() error expected true but received %v", SuperDuperConversionSystem.HasData()) } @@ -162,7 +162,7 @@ func TestConversionsRatesSystem(t *testing.T) { r := *p * 1000 expectedRate := 1396.9317581 if r != expectedRate { - t.Errorf("Test Failed - Convert() error expected %.13f but received %.13f", + t.Errorf("Convert() error expected %.13f but received %.13f", expectedRate, r) } @@ -170,8 +170,78 @@ func TestConversionsRatesSystem(t *testing.T) { inverseR := *pi * expectedRate expectedInverseRate := float64(1000) if inverseR != expectedInverseRate { - t.Errorf("Test Failed - Convert() error expected %.13f but received %.13f", + t.Errorf("Convert() error expected %.13f but received %.13f", expectedInverseRate, inverseR) } } + +func TestGetRate(t *testing.T) { + from := NewCode("AUD") + to := NewCode("USD") + + c, err := NewConversion(from, to) + if err != nil { + t.Error(err) + } + rate, err := c.GetRate() + if err != nil { + t.Error(err) + } + if rate == 0 { + t.Error("Rate not set") + } + inv, err := c.GetInversionRate() + if err != nil { + t.Error(err) + } + if inv == 0 { + t.Error("Inverted rate not set") + } + conv, err := c.Convert(1) + if err != nil { + t.Error(err) + } + if rate != conv { + t.Errorf("Incorrect rate %v %v", rate, conv) + } + invConv, err := c.ConvertInverse(1) + if err != nil { + t.Error(err) + } + if inv != invConv { + t.Errorf("Incorrect rate %v %v", conv, invConv) + } + + var convs ConversionRates + var convRate float64 + _, err = convs.GetRate(BTC, USDT) + if err == nil { + t.Errorf("Expected %s", fmt.Errorf("rate not found for from %s to %s conversion", + BTC, + USD)) + } + convRate, err = convs.GetRate(USDT, USD) + if err != nil { + t.Error(err) + } + if convRate != 1 { + t.Errorf("Expected rate to be 1") + } + + convRate, err = convs.GetRate(RUR, RUB) + if err != nil { + t.Error(err) + } + if convRate != 1 { + t.Errorf("Expected rate to be 1") + } + + convRate, err = convs.GetRate(RUB, RUR) + if err != nil { + t.Error(err) + } + if convRate != 1 { + t.Errorf("Expected rate to be 1") + } +} diff --git a/currency/currencies.go b/currency/currencies.go index eb3d5bd2..2647fbc1 100644 --- a/currency/currencies.go +++ b/currency/currencies.go @@ -1,6 +1,9 @@ package currency -import "github.com/thrasher-corp/gocryptotrader/common" +import ( + "encoding/json" + "strings" +) // NewCurrenciesFromStringArray returns a Currencies object from strings func NewCurrenciesFromStringArray(currencies []string) Currencies { @@ -38,19 +41,19 @@ func (c Currencies) Contains(cc Code) bool { // Join returns a comma serparated string func (c Currencies) Join() string { - return common.JoinStrings(c.Strings(), ",") + return strings.Join(c.Strings(), ",") } // UnmarshalJSON comforms type to the umarshaler interface func (c *Currencies) UnmarshalJSON(d []byte) error { var configCurrencies string - err := common.JSONDecode(d, &configCurrencies) + err := json.Unmarshal(d, &configCurrencies) if err != nil { return err } var allTheCurrencies Currencies - for _, data := range common.SplitStrings(configCurrencies, ",") { + for _, data := range strings.Split(configCurrencies, ",") { allTheCurrencies = append(allTheCurrencies, NewCode(data)) } @@ -60,7 +63,7 @@ func (c *Currencies) UnmarshalJSON(d []byte) error { // MarshalJSON conforms type to the marshaler interface func (c Currencies) MarshalJSON() ([]byte, error) { - return common.JSONEncode(c.Join()) + return json.Marshal(c.Join()) } // Match returns if the full list equals the supplied list @@ -84,11 +87,6 @@ func (c Currencies) Match(other Currencies) bool { return true } -// Slice exposes the underlying type -func (c Currencies) Slice() []Code { - return c -} - // HasData checks to see if Currencies type has actual currencies func (c Currencies) HasData() bool { return len(c) != 0 diff --git a/currency/currencies_test.go b/currency/currencies_test.go index 981f97d5..d97df832 100644 --- a/currency/currencies_test.go +++ b/currency/currencies_test.go @@ -1,31 +1,30 @@ package currency import ( + "encoding/json" "testing" - - "github.com/thrasher-corp/gocryptotrader/common" ) func TestCurrenciesUnmarshalJSON(t *testing.T) { var unmarshalHere Currencies expected := "btc,usd,ltc,bro,things" - encoded, err := common.JSONEncode(expected) + encoded, err := json.Marshal(expected) if err != nil { - t.Fatal("Test Failed - Currencies UnmarshalJSON() error", err) + t.Fatal("Currencies UnmarshalJSON() error", err) } - err = common.JSONDecode(encoded, &unmarshalHere) + err = json.Unmarshal(encoded, &unmarshalHere) if err != nil { - t.Fatal("Test Failed - Currencies UnmarshalJSON() error", err) + t.Fatal("Currencies UnmarshalJSON() error", err) } - err = common.JSONDecode(encoded, &unmarshalHere) + err = json.Unmarshal(encoded, &unmarshalHere) if err != nil { - t.Fatal("Test Failed - Currencies UnmarshalJSON() error", err) + t.Fatal("Currencies UnmarshalJSON() error", err) } if unmarshalHere.Join() != expected { - t.Errorf("Test Failed - Currencies UnmarshalJSON() error expected %s but received %s", + t.Errorf("Currencies UnmarshalJSON() error expected %s but received %s", expected, unmarshalHere.Join()) } } @@ -37,14 +36,14 @@ func TestCurrenciesMarshalJSON(t *testing.T) { C: NewCurrenciesFromStringArray([]string{"btc", "usd", "ltc", "bro", "things"}), } - encoded, err := common.JSONEncode(quickStruct) + encoded, err := json.Marshal(quickStruct) if err != nil { - t.Fatal("Test Failed - Currencies MarshalJSON() error", err) + t.Fatal("Currencies MarshalJSON() error", err) } expected := `{"amazingCurrencies":"btc,usd,ltc,bro,things"}` if string(encoded) != expected { - t.Errorf("Test Failed - Currencies MarshalJSON() error expected %s but received %s", + t.Errorf("Currencies MarshalJSON() error expected %s but received %s", expected, string(encoded)) } } diff --git a/currency/currency_test.go b/currency/currency_test.go index f6e3dae7..73568ba5 100644 --- a/currency/currency_test.go +++ b/currency/currency_test.go @@ -7,12 +7,12 @@ import ( func TestGetDefaultExchangeRates(t *testing.T) { rates, err := GetDefaultExchangeRates() if err != nil { - t.Error("Test failed - GetDefaultExchangeRates() err", err) + t.Error("GetDefaultExchangeRates() err", err) } for _, val := range rates { if !val.IsFiat() { - t.Errorf("Test failed - GetDefaultExchangeRates() %s is not fiat pair", + t.Errorf("GetDefaultExchangeRates() %s is not fiat pair", val) } } @@ -21,12 +21,12 @@ func TestGetDefaultExchangeRates(t *testing.T) { func TestGetExchangeRates(t *testing.T) { rates, err := GetExchangeRates() if err != nil { - t.Error("Test failed - GetExchangeRates() err", err) + t.Error("GetExchangeRates() err", err) } for _, val := range rates { if !val.IsFiat() { - t.Errorf("Test failed - GetExchangeRates() %s is not fiat pair", + t.Errorf("GetExchangeRates() %s is not fiat pair", val) } } @@ -35,23 +35,23 @@ func TestGetExchangeRates(t *testing.T) { func TestUpdateBaseCurrency(t *testing.T) { err := UpdateBaseCurrency(AUD) if err != nil { - t.Error("Test failed - UpdateBaseCurrency() err", err) + t.Error("UpdateBaseCurrency() err", err) } err = UpdateBaseCurrency(LTC) if err == nil { - t.Error("Test failed - UpdateBaseCurrency() cannot be nil") + t.Error("UpdateBaseCurrency() error cannot be nil") } if GetBaseCurrency() != AUD { - t.Errorf("Test failed - GetBaseCurrency() expected %s but received %s", + t.Errorf("GetBaseCurrency() expected %s but received %s", AUD, GetBaseCurrency()) } } func TestGetDefaultBaseCurrency(t *testing.T) { if GetDefaultBaseCurrency() != USD { - t.Errorf("Test failed - GetDefaultBaseCurrency() expected %s but received %s", + t.Errorf("GetDefaultBaseCurrency() expected %s but received %s", USD, GetDefaultBaseCurrency()) } } @@ -59,7 +59,7 @@ func TestGetDefaultBaseCurrency(t *testing.T) { func TestGetDefaulCryptoCurrencies(t *testing.T) { expected := Currencies{BTC, LTC, ETH, DOGE, DASH, XRP, XMR} if !GetDefaultCryptocurrencies().Match(expected) { - t.Errorf("Test failed - GetDefaultCryptocurrencies() expected %s but received %s", + t.Errorf("GetDefaultCryptocurrencies() expected %s but received %s", expected, GetDefaultCryptocurrencies()) } } @@ -67,7 +67,7 @@ func TestGetDefaulCryptoCurrencies(t *testing.T) { func TestGetDefaultFiatCurrencies(t *testing.T) { expected := Currencies{USD, AUD, EUR, CNY} if !GetDefaultFiatCurrencies().Match(expected) { - t.Errorf("Test failed - GetDefaultFiatCurrencies() expected %s but received %s", + t.Errorf("GetDefaultFiatCurrencies() expected %s but received %s", expected, GetDefaultFiatCurrencies()) } } @@ -77,14 +77,14 @@ func TestUpdateCurrencies(t *testing.T) { UpdateCurrencies(fiat, false) rFiat := GetFiatCurrencies() if !rFiat.Contains(HKN) || !rFiat.Contains(JPY) { - t.Error("Test failed - UpdateCurrencies() currencies did not update") + t.Error("UpdateCurrencies() currencies did not update") } crypto := Currencies{ZAR, ZCAD, B2} UpdateCurrencies(crypto, true) rCrypto := GetCryptocurrencies() if !rCrypto.Contains(ZAR) || !rCrypto.Contains(ZCAD) || !rCrypto.Contains(B2) { - t.Error("Test failed - UpdateCurrencies() currencies did not update") + t.Error("UpdateCurrencies() currencies did not update") } } @@ -100,7 +100,7 @@ func TestConvertCurrency(t *testing.T) { } if r != 100 { - t.Errorf("Test Failed - ConvertCurrency error, incorrect rate return %2.f but received %2.f", + t.Errorf("ConvertCurrency error, incorrect rate return %2.f but received %2.f", 100.00, r) } diff --git a/currency/forexprovider/README.md b/currency/forexprovider/README.md index 84874e23..aaad1106 100644 --- a/currency/forexprovider/README.md +++ b/currency/forexprovider/README.md @@ -45,4 +45,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/currency/forexprovider/base/README.md b/currency/forexprovider/base/README.md index 5dfe1e1d..30b5e707 100644 --- a/currency/forexprovider/base/README.md +++ b/currency/forexprovider/base/README.md @@ -43,4 +43,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/currency/forexprovider/base/base.go b/currency/forexprovider/base/base.go index 6d8e3347..ff037934 100644 --- a/currency/forexprovider/base/base.go +++ b/currency/forexprovider/base/base.go @@ -4,6 +4,21 @@ import ( "time" ) +// GetName returns name of provider +func (b *Base) GetName() string { + return b.Name +} + +// IsEnabled returns true if enabled +func (b *Base) IsEnabled() bool { + return b.Enabled +} + +// IsPrimaryProvider returns true if primary provider +func (b *Base) IsPrimaryProvider() bool { + return b.PrimaryProvider +} + // DefaultTimeOut is the default timeout for foreign exchange providers const DefaultTimeOut = time.Second * 15 @@ -22,18 +37,3 @@ type Settings struct { type Base struct { Settings `json:"settings"` } - -// GetName returns name of provider -func (b *Base) GetName() string { - return b.Name -} - -// IsEnabled returns true if enabled -func (b *Base) IsEnabled() bool { - return b.Enabled -} - -// IsPrimaryProvider returns true if primary provider -func (b *Base) IsPrimaryProvider() bool { - return b.PrimaryProvider -} diff --git a/currency/forexprovider/base/base_interface.go b/currency/forexprovider/base/base_interface.go index 775f1bc5..18e9bbf4 100644 --- a/currency/forexprovider/base/base_interface.go +++ b/currency/forexprovider/base/base_interface.go @@ -3,6 +3,7 @@ package base import ( "errors" "fmt" + "strings" "sync" "github.com/thrasher-corp/gocryptotrader/common" @@ -49,7 +50,7 @@ func (p *Provider) GetNewRate(base string, currencies []string) (map[string]floa return p.Provider.GetRates(base, "") // Zero value to get all rates default: - return p.Provider.GetRates(base, common.JoinStrings(currencies, ",")) + return p.Provider.GetRates(base, strings.Join(currencies, ",")) } } diff --git a/currency/forexprovider/currencyconverterapi/README.md b/currency/forexprovider/currencyconverterapi/README.md index 8df13b14..cca5b1d3 100644 --- a/currency/forexprovider/currencyconverterapi/README.md +++ b/currency/forexprovider/currencyconverterapi/README.md @@ -5,7 +5,7 @@ [![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader) [![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) -[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencyconverter) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencyconverterapi) [![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) @@ -29,15 +29,15 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader + Individual package example below: ```go import ( -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencyconverter" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencyconverter" ) c := currencyconverter.CurrencyConverter{} // Define configuration newSettings := base.Settings{ - Name: "CurrencyConverter", + Name: "CurrencyConverter", Enabled: true, Verbose: false, RESTPollingDelay: time.Duration, @@ -72,4 +72,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/currency/forexprovider/currencyconverterapi/currencyconverterapi.go b/currency/forexprovider/currencyconverterapi/currencyconverterapi.go index 856003e7..47e21e11 100644 --- a/currency/forexprovider/currencyconverterapi/currencyconverterapi.go +++ b/currency/forexprovider/currencyconverterapi/currencyconverterapi.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" @@ -13,29 +14,6 @@ import ( log "github.com/thrasher-corp/gocryptotrader/logger" ) -// const declarations consist of endpoints -const ( - APIEndpointURL = "https://currencyconverterapi.com/api/" - APIEndpointFreeURL = "https://free.currencyconverterapi.com/api/" - APIEndpointVersion = "v5" - - APIEndpointConvert = "convert" - APIEndpointCurrencies = "currencies" - APIEndpointCountries = "countries" - APIEndpointUsage = "usage" - - defaultAPIKey = "Key" - - authRate = 0 - unAuthRate = 0 -) - -// CurrencyConverter stores the struct for the CurrencyConverter API -type CurrencyConverter struct { - base.Base - Requester *request.Requester -} - // Setup sets appropriate values for CurrencyLayer func (c *CurrencyConverter) Setup(config base.Settings) error { c.Name = config.Name @@ -54,7 +32,7 @@ func (c *CurrencyConverter) Setup(config base.Settings) error { // GetRates is a wrapper function to return rates func (c *CurrencyConverter) GetRates(baseCurrency, symbols string) (map[string]float64, error) { - splitSymbols := common.SplitStrings(symbols, ",") + splitSymbols := strings.Split(symbols, ",") if len(splitSymbols) == 1 { return c.Convert(baseCurrency, symbols) @@ -75,11 +53,11 @@ func (c *CurrencyConverter) GetRates(baseCurrency, symbols string) (map[string]f batch := completedStrings[i : i+2] result, err := c.ConvertMany(batch) if err != nil { - log.Errorf("Failed to get batch err: %s", err) + log.Errorf(log.Global, "Failed to get batch err: %s\n", err) continue } for k, v := range result { - rates[common.ReplaceString(k, "_", "", -1)] = v + rates[strings.Replace(k, "_", "", -1)] = v } } } @@ -98,7 +76,7 @@ func (c *CurrencyConverter) GetRates(baseCurrency, symbols string) (map[string]f } for k, v := range result { - rates[common.ReplaceString(k, "_", "", -1)] = v + rates[strings.Replace(k, "_", "", -1)] = v } return rates, nil @@ -113,7 +91,7 @@ func (c *CurrencyConverter) ConvertMany(currencies []string) (map[string]float64 result := make(map[string]float64) v := url.Values{} - joined := common.JoinStrings(currencies, ",") + joined := strings.Join(currencies, ",") v.Set("q", joined) v.Set("compact", "ultra") diff --git a/currency/forexprovider/currencyconverterapi/currencyconverterapi_test.go b/currency/forexprovider/currencyconverterapi/currencyconverterapi_test.go index a15491c7..b322ef07 100644 --- a/currency/forexprovider/currencyconverterapi/currencyconverterapi_test.go +++ b/currency/forexprovider/currencyconverterapi/currencyconverterapi_test.go @@ -65,7 +65,7 @@ func TestConvertMany(t *testing.T) { currencies = []string{"USD_AUD", "USD_EUR", "USD_GBP"} _, err = c.ConvertMany(currencies) if err == nil { - t.Fatal("non error on supplying 3 or more currencies using the free API") + t.Fatal("Expected error from on supplying 3 or more currencies using the free API") } } diff --git a/currency/forexprovider/currencyconverterapi/currencyconverterapi_types.go b/currency/forexprovider/currencyconverterapi/currencyconverterapi_types.go index 6c310176..f44f2053 100644 --- a/currency/forexprovider/currencyconverterapi/currencyconverterapi_types.go +++ b/currency/forexprovider/currencyconverterapi/currencyconverterapi_types.go @@ -1,5 +1,33 @@ package currencyconverter +import ( + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" +) + +// const declarations consist of endpoints +const ( + APIEndpointURL = "https://currencyconverterapi.com/api/" + APIEndpointFreeURL = "https://free.currencyconverterapi.com/api/" + APIEndpointVersion = "v5" + + APIEndpointConvert = "convert" + APIEndpointCurrencies = "currencies" + APIEndpointCountries = "countries" + APIEndpointUsage = "usage" + + defaultAPIKey = "Key" + + authRate = 0 + unAuthRate = 0 +) + +// CurrencyConverter stores the struct for the CurrencyConverter API +type CurrencyConverter struct { + base.Base + Requester *request.Requester +} + // Error stores the error message type Error struct { Status int `json:"status"` diff --git a/currency/forexprovider/currencylayer/README.md b/currency/forexprovider/currencylayer/README.md index b18bfb9b..37d76211 100644 --- a/currency/forexprovider/currencylayer/README.md +++ b/currency/forexprovider/currencylayer/README.md @@ -29,15 +29,15 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader + Individual package example below: ```go import ( -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencylayer" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencylayer" ) c := currencylayer.CurrencyLayer{} // Define configuration newSettings := base.Settings{ - Name: "CurrencyLayer", + Name: "CurrencyLayer", Enabled: true, Verbose: false, RESTPollingDelay: time.Duration, @@ -72,4 +72,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/currency/forexprovider/currencylayer/currencylayer.go b/currency/forexprovider/currencylayer/currencylayer.go index add3ff43..b0a36fc9 100644 --- a/currency/forexprovider/currencylayer/currencylayer.go +++ b/currency/forexprovider/currencylayer/currencylayer.go @@ -13,10 +13,10 @@ package currencylayer import ( "errors" - "fmt" "net/http" "net/url" "strconv" + "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" @@ -25,38 +25,11 @@ import ( log "github.com/thrasher-corp/gocryptotrader/logger" ) -// const declarations consist of endpoints and APIKey privileges -const ( - AccountFree = iota - AccountBasic - AccountPro - AccountEnterprise - - APIEndpointURL = "http://apilayer.net/api/" - APIEndpointURLSSL = "https://apilayer.net/api/" - APIEndpointList = "list" - APIEndpointLive = "live" - APIEndpointHistorical = "historical" - APIEndpointConversion = "convert" - APIEndpointTimeframe = "timeframe" - APIEndpointChange = "change" - - authRate = 0 - unAuthRate = 0 -) - -// CurrencyLayer is a foreign exchange rate provider at -// https://currencylayer.com NOTE default base currency is USD when using a free -// account. Has automatic upgrade to a SSL connection. -type CurrencyLayer struct { - base.Base - Requester *request.Requester -} - // Setup sets appropriate values for CurrencyLayer func (c *CurrencyLayer) Setup(config base.Settings) error { if config.APIKeyLvl < 0 || config.APIKeyLvl > 3 { - log.Errorf("apikey incorrectly set in config.json for %s, please set appropriate account levels", + log.Errorf(log.Global, + "apikey incorrectly set in config.json for %s, please set appropriate account levels\n", config.Name) return errors.New("apikey set failure") } @@ -125,7 +98,7 @@ func (c *CurrencyLayer) GetliveData(currencies, source string) (map[string]float func (c *CurrencyLayer) GetHistoricalData(date string, currencies []string, source string) (map[string]float64, error) { var resp HistoricalRates v := url.Values{} - v.Set("currencies", common.JoinStrings(currencies, ",")) + v.Set("currencies", strings.Join(currencies, ",")) v.Set("source", source) v.Set("date", date) @@ -179,7 +152,7 @@ func (c *CurrencyLayer) QueryTimeFrame(startDate, endDate, baseCurrency string, v.Set("start_date", startDate) v.Set("end_date", endDate) v.Set("base", baseCurrency) - v.Set("currencies", common.JoinStrings(currencies, ",")) + v.Set("currencies", strings.Join(currencies, ",")) err := c.SendHTTPRequest(APIEndpointTimeframe, v, &resp) if err != nil { @@ -205,7 +178,7 @@ func (c *CurrencyLayer) QueryCurrencyChange(startDate, endDate, baseCurrency str v.Set("start_date", startDate) v.Set("end_date", endDate) v.Set("base", baseCurrency) - v.Set("currencies", common.JoinStrings(currencies, ",")) + v.Set("currencies", strings.Join(currencies, ",")) err := c.SendHTTPRequest(APIEndpointChange, v, &resp) if err != nil { @@ -226,10 +199,10 @@ func (c *CurrencyLayer) SendHTTPRequest(endPoint string, values url.Values, resu var auth bool if c.APIKeyLvl == AccountFree { - path = fmt.Sprintf("%s%s%s", APIEndpointURL, endPoint, "?") + path = APIEndpointURL + endPoint + "?" } else { auth = true - path = fmt.Sprintf("%s%s%s", APIEndpointURLSSL, endPoint, "?") + path = APIEndpointURLSSL + endPoint + "?" } path += values.Encode() diff --git a/currency/forexprovider/currencylayer/currencylayer_test.go b/currency/forexprovider/currencylayer/currencylayer_test.go index 3c6186f0..1105a306 100644 --- a/currency/forexprovider/currencylayer/currencylayer_test.go +++ b/currency/forexprovider/currencylayer/currencylayer_test.go @@ -48,7 +48,7 @@ func areAPIKeysSet() bool { func TestGetRates(t *testing.T) { err := setup() if err != nil { - t.Skip("Test Failed - CurrencyLayer GetRates error", err) + t.Skip("CurrencyLayer GetRates error", err) } _, err = c.GetRates("USD", "AUD") if areAPIKeysSet() && err != nil { @@ -61,7 +61,7 @@ func TestGetRates(t *testing.T) { func TestGetSupportedCurrencies(t *testing.T) { err := setup() if err != nil { - t.Fatal("Test Failed - CurrencyLayer GetSupportedCurrencies error", err) + t.Fatal("CurrencyLayer GetSupportedCurrencies error", err) } _, err = c.GetSupportedCurrencies() if areAPIKeysSet() && err != nil { @@ -74,7 +74,7 @@ func TestGetSupportedCurrencies(t *testing.T) { func TestGetliveData(t *testing.T) { err := setup() if err != nil { - t.Fatal("Test Failed - CurrencyLayer GetliveData error", err) + t.Fatal("CurrencyLayer GetliveData error", err) } _, err = c.GetliveData("AUD", "USD") if areAPIKeysSet() && err != nil { @@ -87,7 +87,7 @@ func TestGetliveData(t *testing.T) { func TestGetHistoricalData(t *testing.T) { err := setup() if err != nil { - t.Fatal("Test Failed - CurrencyLayer GetHistoricalData error", err) + t.Fatal("CurrencyLayer GetHistoricalData error", err) } _, err = c.GetHistoricalData("2016-12-15", []string{"AUD"}, "USD") if areAPIKeysSet() && err != nil { @@ -100,7 +100,7 @@ func TestGetHistoricalData(t *testing.T) { func TestConvert(t *testing.T) { err := setup() if err != nil { - t.Fatal("Test Failed - CurrencyLayer Convert error", err) + t.Fatal("CurrencyLayer Convert error", err) } _, err = c.Convert("USD", "AUD", "", 1) if areAPIKeysSet() && err != nil && c.APIKeyLvl >= AccountBasic { @@ -113,7 +113,7 @@ func TestConvert(t *testing.T) { func TestQueryTimeFrame(t *testing.T) { err := setup() if err != nil { - t.Fatal("Test Failed - CurrencyLayer QueryTimeFrame error", err) + t.Fatal("CurrencyLayer QueryTimeFrame error", err) } _, err = c.QueryTimeFrame("2010-12-0", "2010-12-5", "USD", []string{"AUD"}) if areAPIKeysSet() && err != nil && c.APIKeyLvl >= AccountPro { @@ -126,7 +126,7 @@ func TestQueryTimeFrame(t *testing.T) { func TestQueryCurrencyChange(t *testing.T) { err := setup() if err != nil { - t.Fatal("Test Failed - CurrencyLayer QueryCurrencyChange() error", err) + t.Fatal("CurrencyLayer QueryCurrencyChange() error", err) } _, err = c.QueryCurrencyChange("2010-12-0", "2010-12-5", "USD", []string{"AUD"}) if areAPIKeysSet() && err != nil && c.APIKeyLvl == AccountEnterprise { diff --git a/currency/forexprovider/currencylayer/currencylayer_types.go b/currency/forexprovider/currencylayer/currencylayer_types.go index 3d8fbd86..16753123 100644 --- a/currency/forexprovider/currencylayer/currencylayer_types.go +++ b/currency/forexprovider/currencylayer/currencylayer_types.go @@ -1,5 +1,38 @@ package currencylayer +import ( + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" +) + +// const declarations consist of endpoints and APIKey privileges +const ( + AccountFree = iota + AccountBasic + AccountPro + AccountEnterprise + + APIEndpointURL = "http://apilayer.net/api/" + APIEndpointURLSSL = "https://apilayer.net/api/" + APIEndpointList = "list" + APIEndpointLive = "live" + APIEndpointHistorical = "historical" + APIEndpointConversion = "convert" + APIEndpointTimeframe = "timeframe" + APIEndpointChange = "change" + + authRate = 0 + unAuthRate = 0 +) + +// CurrencyLayer is a foreign exchange rate provider at +// https://currencylayer.com NOTE default base currency is USD when using a free +// account. Has automatic upgrade to a SSL connection. +type CurrencyLayer struct { + base.Base + Requester *request.Requester +} + // Error Defines the response error if an error occurred type Error struct { Code int `json:"code"` diff --git a/events/README.md b/currency/forexprovider/exchangeratesapi.io/README.md similarity index 70% rename from events/README.md rename to currency/forexprovider/exchangeratesapi.io/README.md index cd07f6e5..793f5466 100644 --- a/events/README.md +++ b/currency/forexprovider/exchangeratesapi.io/README.md @@ -1,16 +1,16 @@ -# GoCryptoTrader package Events +# GoCryptoTrader package Forexprovider [![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader) [![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) -[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/events) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangeratesapi.io) [![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) -This events package is part of the GoCryptoTrader codebase. +This forexprovider package is part of the GoCryptoTrader codebase. ## This is still in active development @@ -18,9 +18,39 @@ You can track ideas, planned features and what's in progresss on this Trello boa Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) -## Current Features for events +## Current Features for forexprovider -+ The events package handles events from GoCryptoTrader bot. ++ Fetches up to date curency data from [Exchange rates API]("http://exchangeratesapi.io") + +### How to enable + ++ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-currency-via-config-example) + ++ Individual package example below: +```go +import ( + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerates" +) + +c := exchangerates.ExchangeRates{} + +// Define configuration +newSettings := base.Settings{ + Name: "ExchangeRates", + Enabled: true, + Verbose: false, + RESTPollingDelay: time.Duration, + APIKey: "key", + APIKeyLvl: "keylvl", + PrimaryProvider: true, +} + +c.Setup(newSettings) + +mapstringfloat, err := c.GetRates("USD", "EUR,CHY") +// Handle error +``` ### Please click GoDocs chevron above to view current GoDoc information for this package @@ -41,5 +71,4 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: -***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - +***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** \ No newline at end of file diff --git a/currency/forexprovider/exchangeratesapi.io/exchangeratesapi.go b/currency/forexprovider/exchangeratesapi.io/exchangeratesapi.go index 661db588..6c7b2815 100644 --- a/currency/forexprovider/exchangeratesapi.io/exchangeratesapi.go +++ b/currency/forexprovider/exchangeratesapi.io/exchangeratesapi.go @@ -14,24 +14,6 @@ import ( log "github.com/thrasher-corp/gocryptotrader/logger" ) -const ( - exchangeRatesAPI = "https://api.exchangeratesapi.io" - exchangeRatesLatest = "latest" - exchangeRatesHistory = "history" - exchangeRatesSupportedCurrencies = "EUR,CHF,USD,BRL,ISK,PHP,KRW,BGN,MXN," + - "RON,CAD,SGD,NZD,THB,HKD,JPY,NOK,HRK,ILS,GBP,DKK,HUF,MYR,RUB,TRY,IDR," + - "ZAR,INR,AUD,CZK,SEK,CNY,PLN" - - authRate = 0 - unAuthRate = 0 -) - -// ExchangeRates stores the struct for the ExchangeRatesAPI API -type ExchangeRates struct { - base.Base - Requester *request.Requester -} - // Setup sets appropriate values for CurrencyLayer func (e *ExchangeRates) Setup(config base.Settings) error { e.Name = config.Name @@ -66,8 +48,9 @@ func cleanCurrencies(baseCurrency, symbols string) string { } // remove and warn about any unsupported currencies - if !common.StringContains(exchangeRatesSupportedCurrencies, x) { - log.Warnf("Forex provider ExchangeRatesAPI does not support currency %s, removing from forex rates query.", x) + if !strings.Contains(exchangeRatesSupportedCurrencies, x) { // nolint:gocritic + log.Warnf(log.Global, + "Forex provider ExchangeRatesAPI does not support currency %s, removing from forex rates query.\n", x) continue } cleanedCurrencies = append(cleanedCurrencies, x) @@ -164,7 +147,7 @@ func (e *ExchangeRates) GetRates(baseCurrency, symbols string) (map[string]float // GetSupportedCurrencies returns the supported currency list func (e *ExchangeRates) GetSupportedCurrencies() ([]string, error) { - return common.SplitStrings(exchangeRatesSupportedCurrencies, ","), nil + return strings.Split(exchangeRatesSupportedCurrencies, ","), nil } // SendHTTPRequest sends a HTTPS request to the desired endpoint and returns the result diff --git a/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_types.go b/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_types.go index b8c3e3f6..3c589f15 100644 --- a/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_types.go +++ b/currency/forexprovider/exchangeratesapi.io/exchangeratesapi_types.go @@ -1,5 +1,28 @@ package exchangerates +import ( + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" +) + +const ( + exchangeRatesAPI = "https://api.exchangeratesapi.io" + exchangeRatesLatest = "latest" + exchangeRatesHistory = "history" + exchangeRatesSupportedCurrencies = "EUR,CHF,USD,BRL,ISK,PHP,KRW,BGN,MXN," + + "RON,CAD,SGD,NZD,THB,HKD,JPY,NOK,HRK,ILS,GBP,DKK,HUF,MYR,RUB,TRY,IDR," + + "ZAR,INR,AUD,CZK,SEK,CNY,PLN" + + authRate = 0 + unAuthRate = 0 +) + +// ExchangeRates stores the struct for the ExchangeRatesAPI API +type ExchangeRates struct { + base.Base + Requester *request.Requester +} + // Rates holds the latest forex rates info type Rates struct { Base string `json:"base"` diff --git a/currency/forexprovider/fixer.io/README.md b/currency/forexprovider/fixer.io/README.md index 1ec733db..135caff4 100644 --- a/currency/forexprovider/fixer.io/README.md +++ b/currency/forexprovider/fixer.io/README.md @@ -5,7 +5,7 @@ [![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader) [![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) -[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/currency/forexprovider/fixer) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/currency/forexprovider/fixer.io) [![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) @@ -29,15 +29,15 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader + Individual package example below: ```go import ( -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/fixer.io" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/fixer.io" ) c := fixer.Fixer{} // Define configuration newSettings := base.Settings{ - Name: "Fixer", + Name: "Fixer", Enabled: true, Verbose: false, RESTPollingDelay: time.Duration, @@ -72,4 +72,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/currency/forexprovider/fixer.io/fixer.go b/currency/forexprovider/fixer.io/fixer.go index 2ab84c8c..9816b9df 100644 --- a/currency/forexprovider/fixer.io/fixer.go +++ b/currency/forexprovider/fixer.io/fixer.go @@ -13,6 +13,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" @@ -21,36 +22,11 @@ import ( log "github.com/thrasher-corp/gocryptotrader/logger" ) -const ( - fixerAPIFree = iota - fixerAPIBasic - fixerAPIProfessional - fixerAPIProfessionalPlus - fixerAPIEnterprise - - fixerAPI = "http://data.fixer.io/api/" - fixerAPISSL = "https://data.fixer.io/api/" - fixerAPILatest = "latest" - fixerAPIConvert = "convert" - fixerAPITimeSeries = "timeseries" - fixerAPIFluctuation = "fluctuation" - fixerSupportedCurrencies = "symbols" - - authRate = 0 - unAuthRate = 0 -) - -// Fixer is a foreign exchange rate provider at https://fixer.io/ -// NOTE DEFAULT BASE CURRENCY IS EUR upgrade to basic to change -type Fixer struct { - base.Base - Requester *request.Requester -} - // Setup sets appropriate values for fixer object func (f *Fixer) Setup(config base.Settings) error { if config.APIKeyLvl < 0 || config.APIKeyLvl > 4 { - log.Errorf("apikey incorrectly set in config.json for %s, please set appropriate account levels", + log.Errorf(log.Global, + "apikey incorrectly set in config.json for %s, please set appropriate account levels\n", config.Name) return errors.New("apikey set failure") } @@ -142,7 +118,7 @@ func (f *Fixer) GetHistoricalRates(date, baseCurrency string, symbols []string) var resp Rates v := url.Values{} - v.Set("symbols", common.JoinStrings(symbols, ",")) + v.Set("symbols", strings.Join(symbols, ",")) if baseCurrency != "" { v.Set("base", baseCurrency) @@ -205,7 +181,7 @@ func (f *Fixer) GetTimeSeriesData(startDate, endDate, baseCurrency string, symbo v.Set("start_date", startDate) v.Set("end_date", endDate) v.Set("base", baseCurrency) - v.Set("symbols", common.JoinStrings(symbols, ",")) + v.Set("symbols", strings.Join(symbols, ",")) err := f.SendOpenHTTPRequest(fixerAPITimeSeries, v, &resp) if err != nil { @@ -231,7 +207,7 @@ func (f *Fixer) GetFluctuationData(startDate, endDate, baseCurrency string, symb v.Set("start_date", startDate) v.Set("end_date", endDate) v.Set("base", baseCurrency) - v.Set("symbols", common.JoinStrings(symbols, ",")) + v.Set("symbols", strings.Join(symbols, ",")) err := f.SendOpenHTTPRequest(fixerAPIFluctuation, v, &resp) if err != nil { diff --git a/currency/forexprovider/fixer.io/fixer_test.go b/currency/forexprovider/fixer.io/fixer_test.go index c85f7314..45752552 100644 --- a/currency/forexprovider/fixer.io/fixer_test.go +++ b/currency/forexprovider/fixer.io/fixer_test.go @@ -22,7 +22,7 @@ func setup(t *testing.T) { if !isSetup { err := f.Setup(base.Settings{}) if err != nil { - t.Fatal("Test Failed - Setup error", err) + t.Fatal("Setup error", err) } isSetup = true } @@ -32,7 +32,7 @@ func TestGetRates(t *testing.T) { setup(t) _, err := f.GetRates("EUR", "AUD") if err == nil { - t.Error("test failed - fixer GetRates() error", err) + t.Error("fixer GetRates() Expected error") } } @@ -40,7 +40,7 @@ func TestGetLatestRates(t *testing.T) { setup(t) _, err := f.GetLatestRates("EUR", "AUD") if err == nil { - t.Error("test failed - fixer GetLatestRates() error", err) + t.Error("fixer GetLatestRates() Expected error") } } @@ -48,7 +48,7 @@ func TestGetHistoricalRates(t *testing.T) { setup(t) _, err := f.GetHistoricalRates("2013-12-24", "EUR", []string{"AUD,KRW"}) if err == nil { - t.Error("test failed - fixer GetHistoricalRates() error", err) + t.Error("fixer GetHistoricalRates() Expected error") } } @@ -56,7 +56,7 @@ func TestConvertCurrency(t *testing.T) { setup(t) _, err := f.ConvertCurrency("AUD", "EUR", "", 1337) if err == nil { - t.Error("test failed - fixer ConvertCurrency() error", err) + t.Error("fixer ConvertCurrency() Expected error") } } @@ -64,7 +64,7 @@ func TestGetTimeSeriesData(t *testing.T) { setup(t) _, err := f.GetTimeSeriesData("2013-12-24", "2013-12-25", "EUR", []string{"AUD,KRW"}) if err == nil { - t.Error("test failed - fixer GetTimeSeriesData() error", err) + t.Error("fixer GetTimeSeriesData() Expected error") } } @@ -72,6 +72,6 @@ func TestGetFluctuationData(t *testing.T) { setup(t) _, err := f.GetFluctuationData("2013-12-24", "2013-12-25", "EUR", []string{"AUD,KRW"}) if err == nil { - t.Error("test failed - fixer GetFluctuationData() error", err) + t.Error("fixer GetFluctuationData() Expected error") } } diff --git a/currency/forexprovider/fixer.io/fixer_types.go b/currency/forexprovider/fixer.io/fixer_types.go index 8238b16d..116750bb 100644 --- a/currency/forexprovider/fixer.io/fixer_types.go +++ b/currency/forexprovider/fixer.io/fixer_types.go @@ -1,5 +1,36 @@ package fixer +import ( + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" +) + +const ( + fixerAPIFree = iota + fixerAPIBasic + fixerAPIProfessional + fixerAPIProfessionalPlus + fixerAPIEnterprise + + fixerAPI = "http://data.fixer.io/api/" + fixerAPISSL = "https://data.fixer.io/api/" + fixerAPILatest = "latest" + fixerAPIConvert = "convert" + fixerAPITimeSeries = "timeseries" + fixerAPIFluctuation = "fluctuation" + fixerSupportedCurrencies = "symbols" + + authRate = 0 + unAuthRate = 0 +) + +// Fixer is a foreign exchange rate provider at https://fixer.io/ +// NOTE DEFAULT BASE CURRENCY IS EUR upgrade to basic to change +type Fixer struct { + base.Base + Requester *request.Requester +} + // Rates contains the data fields for the currencies you have requested. type Rates struct { Success bool `json:"success"` diff --git a/currency/forexprovider/forexprovider.go b/currency/forexprovider/forexprovider.go index 8b11152f..ab415bbc 100644 --- a/currency/forexprovider/forexprovider.go +++ b/currency/forexprovider/forexprovider.go @@ -13,13 +13,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/openexchangerates" ) -// ForexProviders is a foreign exchange handler type -type ForexProviders struct { - base.FXHandler -} - -// GetAvailableForexProviders returns a list of supported forex providers -func GetAvailableForexProviders() []string { +// GetSupportedForexProviders returns a list of supported forex providers +func GetSupportedForexProviders() []string { return []string{"CurrencyConverter", "CurrencyLayer", "ExchangeRates", @@ -135,3 +130,8 @@ func StartFXService(fxProviders []base.Settings) (*ForexProviders, error) { return handler, nil } + +// ForexProviders is a foreign exchange handler type +type ForexProviders struct { + base.FXHandler +} diff --git a/currency/forexprovider/openexchangerates/README.md b/currency/forexprovider/openexchangerates/README.md index a9724ed1..83f7fac3 100644 --- a/currency/forexprovider/openexchangerates/README.md +++ b/currency/forexprovider/openexchangerates/README.md @@ -29,15 +29,15 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader + Individual package example below: ```go import ( -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" -"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/openexchangerates" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/openexchangerates" ) c := openexchangerates.OXR{} // Define configuration newSettings := base.Settings{ - Name: "openexchangerates", + Name: "openexchangerates", Enabled: true, Verbose: false, RESTPollingDelay: time.Duration, @@ -72,4 +72,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/currency/forexprovider/openexchangerates/openexchangerates.go b/currency/forexprovider/openexchangerates/openexchangerates.go index 071279b5..27ef1aac 100644 --- a/currency/forexprovider/openexchangerates/openexchangerates.go +++ b/currency/forexprovider/openexchangerates/openexchangerates.go @@ -14,6 +14,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" @@ -22,49 +23,11 @@ import ( log "github.com/thrasher-corp/gocryptotrader/logger" ) -// These consts contain endpoint information -const ( - APIDeveloperAccess = iota - APIEnterpriseAccess - APIUnlimitedAccess - - APIURL = "https://openexchangerates.org/api/" - APIEndpointLatest = "latest.json" - APIEndpointHistorical = "historical/%s.json" - APIEndpointCurrencies = "currencies.json" - APIEndpointTimeSeries = "time-series.json" - APIEndpointConvert = "convert/%s/%s/%s" - APIEndpointOHLC = "ohlc.json" - APIEndpointUsage = "usage.json" - - oxrSupportedCurrencies = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD," + - "BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTC,BTN,BWP,BYN,BYR,BZD,CAD,CDF," + - "CHF,CLF,CLP,CNH,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EEK,EGP," + - "ERN,ETB,EUR,FJD,FKP,GBP,GEL,GGP,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK," + - "HTG,HUF,IDR,ILS,IMP,INR,IQD,IRR,ISK,JEP,JMD,JOD,JPY,KES,KGS,KHR,KMF," + - "KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT," + - "MOP,MRO,MRU,MTL,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR," + - "PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK," + - "SGD,SHP,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY," + - "TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XAG,XAU,XCD,XDR," + - "XOF,XPD,XPF,XPT,YER,ZAR,ZMK,ZMW" - - authRate = 0 - unAuthRate = 0 -) - -// OXR is a foreign exchange rate provider at https://openexchangerates.org/ -// this is the overarching type across this package -// DOCs : https://docs.openexchangerates.org/docs -type OXR struct { - base.Base - Requester *request.Requester -} - // Setup sets values for the OXR object func (o *OXR) Setup(config base.Settings) error { if config.APIKeyLvl < 0 || config.APIKeyLvl > 2 { - log.Errorf("apikey incorrectly set in config.json for %s, please set appropriate account levels", + log.Errorf(log.Global, + "apikey incorrectly set in config.json for %s, please set appropriate account levels\n", config.Name) return errors.New("apikey set failure") } @@ -126,7 +89,7 @@ func (o *OXR) GetHistoricalRates(date, baseCurrency string, symbols []string, pr v := url.Values{} v.Set("base", baseCurrency) - v.Set("symbols", common.JoinStrings(symbols, ",")) + v.Set("symbols", strings.Join(symbols, ",")) v.Set("prettyprint", strconv.FormatBool(prettyPrint)) v.Set("show_alternative", strconv.FormatBool(showAlternative)) endpoint := fmt.Sprintf(APIEndpointHistorical, date) @@ -156,7 +119,7 @@ func (o *OXR) GetCurrencies(showInactive, prettyPrint, showAlternative bool) (ma // GetSupportedCurrencies returns a list of supported currencies func (o *OXR) GetSupportedCurrencies() ([]string, error) { - return common.SplitStrings(oxrSupportedCurrencies, ","), nil + return strings.Split(oxrSupportedCurrencies, ","), nil } // GetTimeSeries returns historical exchange rates for a given time period, @@ -172,7 +135,7 @@ func (o *OXR) GetTimeSeries(baseCurrency, startDate, endDate string, symbols []s v.Set("base", baseCurrency) v.Set("start", startDate) v.Set("end", endDate) - v.Set("symbols", common.JoinStrings(symbols, ",")) + v.Set("symbols", strings.Join(symbols, ",")) v.Set("prettyprint", strconv.FormatBool(prettyPrint)) v.Set("show_alternative", strconv.FormatBool(showAlternative)) @@ -220,7 +183,7 @@ func (o *OXR) GetOHLC(startTime, period, baseCurrency string, symbols []string, v.Set("start_time", startTime) v.Set("period", period) v.Set("base", baseCurrency) - v.Set("symbols", common.JoinStrings(symbols, ",")) + v.Set("symbols", strings.Join(symbols, ",")) v.Set("prettyprint", strconv.FormatBool(prettyPrint)) if err := o.SendHTTPRequest(APIEndpointOHLC, v, &resp); err != nil { diff --git a/currency/forexprovider/openexchangerates/openexchangerates_test.go b/currency/forexprovider/openexchangerates/openexchangerates_test.go index 69093c33..184c9c88 100644 --- a/currency/forexprovider/openexchangerates/openexchangerates_test.go +++ b/currency/forexprovider/openexchangerates/openexchangerates_test.go @@ -31,7 +31,7 @@ func TestGetRates(t *testing.T) { } _, err := o.GetRates("USD", "AUD") if err == nil { - t.Error("test failed - GetRates() error", err) + t.Error("GetRates() Expected error") } } @@ -41,7 +41,7 @@ func TestGetLatest(t *testing.T) { } _, err := o.GetLatest("USD", "AUD", false, false) if err == nil { - t.Error("test failed - GetLatest() error", err) + t.Error("GetLatest() Expected error") } } @@ -51,7 +51,7 @@ func TestGetHistoricalRates(t *testing.T) { } _, err := o.GetHistoricalRates("2017-12-01", "USD", []string{"CNH", "AUD", "ANG"}, false, false) if err == nil { - t.Error("test failed - GetRates() error", err) + t.Error("GetRates() Expected error") } } @@ -61,7 +61,7 @@ func TestGetCurrencies(t *testing.T) { } _, err := o.GetCurrencies(true, true, true) if err != nil { - t.Error("test failed - GetCurrencies() error", err) + t.Error("GetCurrencies() error", err) } } @@ -71,7 +71,7 @@ func TestGetTimeSeries(t *testing.T) { } _, err := o.GetTimeSeries("USD", "2017-12-01", "2017-12-02", []string{"CNH", "AUD", "ANG"}, false, false) if err == nil { - t.Error("test failed - GetTimeSeries() error", err) + t.Error("GetTimeSeries() Expected error") } } @@ -81,7 +81,7 @@ func TestConvertCurrency(t *testing.T) { } _, err := o.ConvertCurrency(1337, "USD", "AUD") if err == nil { - t.Error("test failed - ConvertCurrency() error", err) + t.Error("ConvertCurrency() Expected error") } } @@ -91,7 +91,7 @@ func TestGetOHLC(t *testing.T) { } _, err := o.GetOHLC("2017-07-17T08:30:00Z", "1m", "USD", []string{"AUD"}, false) if err == nil { - t.Error("test failed - GetOHLC() error", err) + t.Error("GetOHLC() Expected error") } } @@ -101,6 +101,6 @@ func TestGetUsageStats(t *testing.T) { } _, err := o.GetUsageStats(false) if err == nil { - t.Error("test failed - GetUsageStats() error", err) + t.Error("GetUsageStats() Expected error") } } diff --git a/currency/forexprovider/openexchangerates/openexchangerates_types.go b/currency/forexprovider/openexchangerates/openexchangerates_types.go index 8c2d6e18..438fd0a4 100644 --- a/currency/forexprovider/openexchangerates/openexchangerates_types.go +++ b/currency/forexprovider/openexchangerates/openexchangerates_types.go @@ -1,5 +1,49 @@ package openexchangerates +import ( + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" +) + +// These consts contain endpoint information +const ( + APIDeveloperAccess = iota + APIEnterpriseAccess + APIUnlimitedAccess + + APIURL = "https://openexchangerates.org/api/" + APIEndpointLatest = "latest.json" + APIEndpointHistorical = "historical/%s.json" + APIEndpointCurrencies = "currencies.json" + APIEndpointTimeSeries = "time-series.json" + APIEndpointConvert = "convert/%s/%s/%s" + APIEndpointOHLC = "ohlc.json" + APIEndpointUsage = "usage.json" + + oxrSupportedCurrencies = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD," + + "BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTC,BTN,BWP,BYN,BYR,BZD,CAD,CDF," + + "CHF,CLF,CLP,CNH,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EEK,EGP," + + "ERN,ETB,EUR,FJD,FKP,GBP,GEL,GGP,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK," + + "HTG,HUF,IDR,ILS,IMP,INR,IQD,IRR,ISK,JEP,JMD,JOD,JPY,KES,KGS,KHR,KMF," + + "KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT," + + "MOP,MRO,MRU,MTL,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR," + + "PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK," + + "SGD,SHP,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY," + + "TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XAG,XAU,XCD,XDR," + + "XOF,XPD,XPF,XPT,YER,ZAR,ZMK,ZMW" + + authRate = 0 + unAuthRate = 0 +) + +// OXR is a foreign exchange rate provider at https://openexchangerates.org/ +// this is the overarching type across this package +// DOCs : https://docs.openexchangerates.org/docs +type OXR struct { + base.Base + Requester *request.Requester +} + // Latest holds latest rate data type Latest struct { Disclaimer string `json:"disclaimer"` diff --git a/currency/manager.go b/currency/manager.go new file mode 100644 index 00000000..c77aaa36 --- /dev/null +++ b/currency/manager.go @@ -0,0 +1,166 @@ +package currency + +import ( + "errors" + + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +// GetAssetTypes returns a list of stored asset types +func (p *PairsManager) GetAssetTypes() asset.Items { + p.m.Lock() + defer p.m.Unlock() + var assetTypes asset.Items + for k := range p.Pairs { + assetTypes = append(assetTypes, k) + } + return assetTypes +} + +// Get gets the currency pair config based on the asset type +func (p *PairsManager) Get(a asset.Item) *PairStore { + p.m.Lock() + defer p.m.Unlock() + c, ok := p.Pairs[a] + if !ok { + return nil + } + return c +} + +// Store stores a new currency pair config based on its asset type +func (p *PairsManager) Store(a asset.Item, ps PairStore) { + p.m.Lock() + + if p.Pairs == nil { + p.Pairs = make(map[asset.Item]*PairStore) + } + + if !p.AssetTypes.Contains(a) { + p.AssetTypes = append(p.AssetTypes, a) + } + + p.Pairs[a] = &ps + p.m.Unlock() +} + +// Delete deletes a map entry based on the supplied asset type +func (p *PairsManager) Delete(a asset.Item) { + p.m.Lock() + defer p.m.Unlock() + if p.Pairs == nil { + return + } + + _, ok := p.Pairs[a] + if !ok { + return + } + + delete(p.Pairs, a) +} + +// GetPairs gets a list of stored pairs based on the asset type and whether +// they're enabled or not +func (p *PairsManager) GetPairs(a asset.Item, enabled bool) Pairs { + p.m.Lock() + defer p.m.Unlock() + if p.Pairs == nil { + return nil + } + + c, ok := p.Pairs[a] + if !ok { + return nil + } + + var pairs Pairs + if enabled { + pairs = c.Enabled + } else { + pairs = c.Available + } + + return pairs +} + +// StorePairs stores a list of pairs based on the asset type and whether +// they're enabled or not +func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) { + p.m.Lock() + defer p.m.Unlock() + + if p.Pairs == nil { + p.Pairs = make(map[asset.Item]*PairStore) + } + + c, ok := p.Pairs[a] + if !ok { + c = new(PairStore) + } + + if enabled { + c.Enabled = pairs + } else { + c.Available = pairs + } + + p.Pairs[a] = c +} + +// DisablePair removes the pair from the enabled pairs list if found +func (p *PairsManager) DisablePair(a asset.Item, pair Pair) error { + p.m.Lock() + defer p.m.Unlock() + + if p.Pairs == nil { + return errors.New("pair manager not initialised") + } + + c, ok := p.Pairs[a] + if !ok { + return errors.New("asset type not found") + } + + if c == nil { + return errors.New("currency store is nil") + } + + if !c.Enabled.Contains(pair, true) { + return errors.New("specified pair is not enabled") + } + + c.Enabled = c.Enabled.Remove(pair) + return nil +} + +// EnablePair adds a pair to the list of enabled pairs if it exists in the list +// of available pairs and isn't already added +func (p *PairsManager) EnablePair(a asset.Item, pair Pair) error { + p.m.Lock() + defer p.m.Unlock() + + if p.Pairs == nil { + return errors.New("pair manager not initialised") + } + + c, ok := p.Pairs[a] + if !ok { + return errors.New("asset type not found") + } + + if c == nil { + return errors.New("currency store is nil") + } + + if !c.Available.Contains(pair, true) { + return errors.New("specified pair was not found in the list of available pairs") + } + + if c.Enabled.Contains(pair, true) { + return errors.New("specified pair is already enabled") + } + + c.Enabled = c.Enabled.Add(pair) + return nil +} diff --git a/currency/manager_test.go b/currency/manager_test.go new file mode 100644 index 00000000..2d2f0bbd --- /dev/null +++ b/currency/manager_test.go @@ -0,0 +1,206 @@ +package currency + +import ( + "testing" + + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +var p PairsManager + +func initTest() { + p.Store(asset.Spot, + PairStore{ + Available: NewPairsFromStrings([]string{"BTC-USD", "LTC-USD"}), + Enabled: NewPairsFromStrings([]string{"BTC-USD"}), + RequestFormat: &PairFormat{ + Uppercase: true, + }, + ConfigFormat: &PairFormat{ + Uppercase: true, + Delimiter: "-", + }, + }, + ) +} + +func TestGetAssetTypes(t *testing.T) { + initTest() + + a := p.GetAssetTypes() + if len(a) == 0 { + t.Errorf("GetAssetTypes shouldn't be nil") + } + + if !a.Contains(asset.Spot) { + t.Errorf("AssetTypeSpot should be in the assets list") + } +} + +func TestGet(t *testing.T) { + initTest() + + if p.Get(asset.Spot) == nil { + t.Error("Spot assets shouldn't be nil") + } + + if p.Get(asset.Futures) != nil { + t.Error("Futures should be nil") + } +} + +func TestStore(t *testing.T) { + p.Store(asset.Futures, + PairStore{ + Available: NewPairsFromStrings([]string{"BTC-USD", "LTC-USD"}), + Enabled: NewPairsFromStrings([]string{"BTC-USD"}), + RequestFormat: &PairFormat{ + Uppercase: true, + }, + ConfigFormat: &PairFormat{ + Uppercase: true, + Delimiter: "-", + }, + }, + ) + + if p.Get(asset.Futures) == nil { + t.Error("Futures assets shouldn't be nil") + } +} + +func TestDelete(t *testing.T) { + p.Pairs = nil + p.Delete(asset.Spot) + + p.Store(asset.Spot, + PairStore{ + Available: NewPairsFromStrings([]string{"BTC-USD"}), + }, + ) + p.Delete(asset.UpsideProfitContract) + if p.Get(asset.Spot) == nil { + t.Error("AssetTypeSpot should exist") + } + + p.Delete(asset.Spot) + if p.Get(asset.Spot) != nil { + t.Error("Delete should have deleted AssetTypeSpot") + } +} + +func TestGetPairs(t *testing.T) { + p.Pairs = nil + pairs := p.GetPairs(asset.Spot, true) + if pairs != nil { + t.Fatal("pairs shouldn't be populated") + } + + initTest() + pairs = p.GetPairs(asset.Spot, true) + if pairs == nil { + t.Fatal("pairs should be populated") + } + + pairs = p.GetPairs("blah", true) + if pairs != nil { + t.Fatal("pairs shouldn't be populated") + } +} + +func TestStorePairs(t *testing.T) { + p.Pairs = nil + p.StorePairs(asset.Spot, NewPairsFromStrings([]string{"ETH-USD"}), false) + pairs := p.GetPairs(asset.Spot, false) + if !pairs.Contains(NewPairFromString("ETH-USD"), true) { + t.Errorf("TestStorePairs failed, unexpected result") + } + + initTest() + p.StorePairs(asset.Spot, NewPairsFromStrings([]string{"ETH-USD"}), false) + pairs = p.GetPairs(asset.Spot, false) + if pairs == nil { + t.Errorf("pairs should be populated") + } + + if !pairs.Contains(NewPairFromString("ETH-USD"), true) { + t.Errorf("TestStorePairs failed, unexpected result") + } + + p.StorePairs(asset.Futures, NewPairsFromStrings([]string{"ETH-KRW"}), true) + pairs = p.GetPairs(asset.Futures, true) + if pairs == nil { + t.Errorf("pairs futures should be populated") + } + + if !pairs.Contains(NewPairFromString("ETH-KRW"), true) { + t.Errorf("TestStorePairs failed, unexpected result") + } +} + +func TestDisablePair(t *testing.T) { + p.Pairs = nil + // Test disabling a pair when the pair manager is not initialised + if err := p.DisablePair(asset.Spot, NewPair(BTC, USD)); err == nil { + t.Error("unexpected result") + } + + // Test asset type which doesn't exist + initTest() + if err := p.DisablePair(asset.Futures, Pair{}); err == nil { + t.Error("unexpected result") + } + + // Test asset type which has an empty pair store + p.Pairs[asset.Spot] = nil + if err := p.DisablePair(asset.Spot, Pair{}); err == nil { + t.Error("unexpected result") + } + + // Test disabling a pair which isn't enabled + initTest() + if err := p.DisablePair(asset.Spot, NewPair(LTC, USD)); err == nil { + t.Error("unexpected result") + } + + // Test disabling a valid pair and ensure nil is empty + if err := p.DisablePair(asset.Spot, NewPair(BTC, USD)); err != nil { + t.Error("unexpected result") + } +} + +func TestEnablePair(t *testing.T) { + p.Pairs = nil + // Test enabling a pair when the pair manager is not initialised + if err := p.EnablePair(asset.Spot, NewPair(BTC, USD)); err == nil { + t.Error("unexpected result") + } + + // Test asset type which doesn't exist + initTest() + if err := p.EnablePair(asset.Futures, Pair{}); err == nil { + t.Error("unexpected result") + } + + // Test asset type which has an empty pair store + p.Pairs[asset.Spot] = nil + if err := p.EnablePair(asset.Spot, Pair{}); err == nil { + t.Error("unexpected result") + } + + // Test enabling a pair which isn't in the list of available pairs + initTest() + if err := p.EnablePair(asset.Spot, NewPair(ETH, USD)); err == nil { + t.Error("unexpected result") + } + + // Test enabling a pair which already is enabled + if err := p.EnablePair(asset.Spot, NewPair(BTC, USD)); err == nil { + t.Error("unexpected result") + } + + // Test enabling a valid pair + if err := p.EnablePair(asset.Spot, NewPair(LTC, USD)); err != nil { + t.Error("unexpected result") + } +} diff --git a/currency/manager_types.go b/currency/manager_types.go new file mode 100644 index 00000000..67e1a167 --- /dev/null +++ b/currency/manager_types.go @@ -0,0 +1,34 @@ +package currency + +import ( + "sync" + + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +// PairsManager manages asset pairs +type PairsManager struct { + RequestFormat *PairFormat `json:"requestFormat,omitempty"` + ConfigFormat *PairFormat `json:"configFormat,omitempty"` + UseGlobalFormat bool `json:"useGlobalFormat,omitempty"` + LastUpdated int64 `json:"lastUpdated,omitempty"` + AssetTypes asset.Items `json:"assetTypes"` + Pairs map[asset.Item]*PairStore `json:"pairs"` + m sync.Mutex +} + +// PairStore stores a currency pair store +type PairStore struct { + Enabled Pairs `json:"enabled,omitempty"` + Available Pairs `json:"available,omitempty"` + RequestFormat *PairFormat `json:"requestFormat,omitempty"` + ConfigFormat *PairFormat `json:"configFormat,omitempty"` +} + +// PairFormat returns the pair format +type PairFormat struct { + Uppercase bool `json:"uppercase"` + Delimiter string `json:"delimiter,omitempty"` + Separator string `json:"separator,omitempty"` + Index string `json:"index,omitempty"` +} diff --git a/currency/pair.go b/currency/pair.go index 9c46730b..cd699ebb 100644 --- a/currency/pair.go +++ b/currency/pair.go @@ -1,10 +1,9 @@ package currency import ( + "encoding/json" "fmt" "strings" - - "github.com/thrasher-corp/gocryptotrader/common" ) // NewPairDelimiter splits the desired currency string at delimeter, the returns @@ -62,7 +61,7 @@ func NewPairFromIndex(currencyPair, index string) (Pair, error) { // NewPairFromString converts currency string into a new CurrencyPair // with or without delimeter func NewPairFromString(currencyPair string) Pair { - delimiters := []string{"_", "-", "/"} + delimiters := []string{"_", "-", "/", ":"} var delimiter string for _, x := range delimiters { if strings.Contains(currencyPair, x) { @@ -73,11 +72,18 @@ func NewPairFromString(currencyPair string) Pair { return NewPairFromStrings(currencyPair[0:3], currencyPair[3:]) } -// Pair holds currency pair information -type Pair struct { - Delimiter string `json:"delimiter"` - Base Code `json:"base"` - Quote Code `json:"quote"` +// NewPairFromFormattedPairs matches a supplied currency pair to a list of pairs +// with a specific format. This is helpful for exchanges which +// provide currency pairs with no delimiter so we can match it with a list and +// apply the same format +func NewPairFromFormattedPairs(currencyPair string, pairs Pairs, pairFmt PairFormat) Pair { + for x := range pairs { + if strings.EqualFold(pairs[x].Format(pairFmt.Delimiter, + pairFmt.Uppercase).String(), currencyPair) { + return pairs[x] + } + } + return NewPairFromString(currencyPair) } // String returns a currency pair string @@ -106,7 +112,7 @@ func (p Pair) Upper() Pair { // UnmarshalJSON comforms type to the umarshaler interface func (p *Pair) UnmarshalJSON(d []byte) error { var pair string - err := common.JSONDecode(d, &pair) + err := json.Unmarshal(d, &pair) if err != nil { return err } @@ -117,7 +123,7 @@ func (p *Pair) UnmarshalJSON(d []byte) error { // MarshalJSON conforms type to the marshaler interface func (p Pair) MarshalJSON() ([]byte, error) { - return common.JSONEncode(p.String()) + return json.Marshal(p.String()) } // Format changes the currency based on user preferences overriding the default @@ -133,7 +139,8 @@ func (p Pair) Format(delimiter string, uppercase bool) Pair { // Equal compares two currency pairs and returns whether or not they are equal func (p Pair) Equal(cPair Pair) bool { - return p.Base.Item == cPair.Base.Item && p.Quote.Item == cPair.Quote.Item + return strings.EqualFold(p.Base.String(), cPair.Base.String()) && + strings.EqualFold(p.Quote.String(), cPair.Quote.String()) } // EqualIncludeReciprocal compares two currency pairs and returns whether or not @@ -188,3 +195,10 @@ func (p Pair) IsEmpty() bool { func (p Pair) ContainsCurrency(c Code) bool { return p.Base.Item == c.Item || p.Quote.Item == c.Item } + +// Pair holds currency pair information +type Pair struct { + Delimiter string `json:"delimiter"` + Base Code `json:"base"` + Quote Code `json:"quote"` +} diff --git a/currency/pair_test.go b/currency/pair_test.go index 8c46fd44..b3856528 100644 --- a/currency/pair_test.go +++ b/currency/pair_test.go @@ -1,9 +1,8 @@ package currency import ( + "encoding/json" "testing" - - "github.com/thrasher-corp/gocryptotrader/common" ) const ( @@ -17,7 +16,7 @@ func TestLower(t *testing.T) { actual := pair.Lower() expected := NewPairFromString(defaultPair).Lower() if actual != expected { - t.Errorf("Test failed. Lower(): %s was not equal to expected value: %s", + t.Errorf("Lower(): %s was not equal to expected value: %s", actual, expected) } } @@ -28,7 +27,7 @@ func TestUpper(t *testing.T) { actual := pair.Upper() expected := NewPairFromString(defaultPair) if actual != expected { - t.Errorf("Test failed. Upper(): %s was not equal to expected value: %s", + t.Errorf("Upper(): %s was not equal to expected value: %s", actual, expected) } } @@ -37,23 +36,23 @@ func TestPairUnmarshalJSON(t *testing.T) { var unmarshalHere Pair configPair := NewPairDelimiter("btc_usd", "_") - encoded, err := common.JSONEncode(configPair) + encoded, err := json.Marshal(configPair) if err != nil { - t.Fatal("Test Failed - Pair UnmarshalJSON() error", err) + t.Fatal("Pair UnmarshalJSON() error", err) } - err = common.JSONDecode(encoded, &unmarshalHere) + err = json.Unmarshal(encoded, &unmarshalHere) if err != nil { - t.Fatal("Test Failed - Pair UnmarshalJSON() error", err) + t.Fatal("Pair UnmarshalJSON() error", err) } - err = common.JSONDecode(encoded, &unmarshalHere) + err = json.Unmarshal(encoded, &unmarshalHere) if err != nil { - t.Fatal("Test Failed - Pair UnmarshalJSON() error", err) + t.Fatal("Pair UnmarshalJSON() error", err) } if !unmarshalHere.Equal(configPair) { - t.Errorf("Test Failed - Pairs UnmarshalJSON() error expected %s but received %s", + t.Errorf("Pairs UnmarshalJSON() error expected %s but received %s", configPair, unmarshalHere) } } @@ -65,45 +64,45 @@ func TestPairMarshalJSON(t *testing.T) { Pair{Base: BTC, Quote: USD, Delimiter: "-"}, } - encoded, err := common.JSONEncode(quickstruct) + encoded, err := json.Marshal(quickstruct) if err != nil { - t.Fatal("Test Failed - Pair MarshalJSON() error", err) + t.Fatal("Pair MarshalJSON() error", err) } expected := `{"superPair":"BTC-USD"}` if string(encoded) != expected { - t.Errorf("Test Failed - Pair MarshalJSON() error expected %s but received %s", + t.Errorf("Pair MarshalJSON() error expected %s but received %s", expected, string(encoded)) } } func TestIsCryptoPair(t *testing.T) { if !NewPair(BTC, LTC).IsCryptoPair() { - t.Error("Test Failed. TestIsCryptoPair. Expected true result") + t.Error("TestIsCryptoPair. Expected true result") } if NewPair(BTC, USD).IsCryptoPair() { - t.Error("Test Failed. TestIsCryptoPair. Expected false result") + t.Error("TestIsCryptoPair. Expected false result") } } func TestIsCryptoFiatPair(t *testing.T) { if !NewPair(BTC, USD).IsCryptoFiatPair() { - t.Error("Test Failed. TestIsCryptoPair. Expected true result") + t.Error("TestIsCryptoPair. Expected true result") } if NewPair(BTC, LTC).IsCryptoFiatPair() { - t.Error("Test Failed. TestIsCryptoPair. Expected false result") + t.Error("TestIsCryptoPair. Expected false result") } } func TestIsFiatPair(t *testing.T) { if !NewPair(AUD, USD).IsFiatPair() { - t.Error("Test Failed. TestIsFiatPair. Expected true result") + t.Error("TestIsFiatPair. Expected true result") } if NewPair(BTC, AUD).IsFiatPair() { - t.Error("Test Failed. TestIsFiatPair. Expected false result") + t.Error("TestIsFiatPair. Expected false result") } } @@ -113,7 +112,7 @@ func TestString(t *testing.T) { actual := defaultPair expected := pair.String() if actual != expected { - t.Errorf("Test failed. String(): %s was not equal to expected value: %s", + t.Errorf("String(): %s was not equal to expected value: %s", actual, expected) } } @@ -125,7 +124,7 @@ func TestFirstCurrency(t *testing.T) { expected := BTC if actual != expected { t.Errorf( - "Test failed. GetFirstCurrency(): %s was not equal to expected value: %s", + "GetFirstCurrency(): %s was not equal to expected value: %s", actual, expected, ) } @@ -138,7 +137,7 @@ func TestSecondCurrency(t *testing.T) { expected := USD if actual != expected { t.Errorf( - "Test failed. GetSecondCurrency(): %s was not equal to expected value: %s", + "GetSecondCurrency(): %s was not equal to expected value: %s", actual, expected, ) } @@ -151,7 +150,7 @@ func TestPair(t *testing.T) { expected := defaultPair if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } @@ -164,7 +163,7 @@ func TestDisplay(t *testing.T) { expected := defaultPairWDelimiter if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } @@ -173,7 +172,7 @@ func TestDisplay(t *testing.T) { expected = "btcusd" if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } @@ -182,7 +181,7 @@ func TestDisplay(t *testing.T) { expected = "BTC~USD" if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } @@ -196,7 +195,7 @@ func TestEquall(t *testing.T) { expected := true if actual != expected { t.Errorf( - "Test failed. Equal(): %v was not equal to expected value: %v", + "Equal(): %v was not equal to expected value: %v", actual, expected, ) } @@ -206,7 +205,7 @@ func TestEquall(t *testing.T) { expected = false if actual != expected { t.Errorf( - "Test failed. Equal(): %v was not equal to expected value: %v", + "Equal(): %v was not equal to expected value: %v", actual, expected, ) } @@ -216,7 +215,7 @@ func TestEquall(t *testing.T) { expected = false if actual != expected { t.Errorf( - "Test failed. Equal(): %v was not equal to expected value: %v", + "Equal(): %v was not equal to expected value: %v", actual, expected, ) } @@ -230,7 +229,7 @@ func TestEqualIncludeReciprocal(t *testing.T) { expected := true if actual != expected { t.Errorf( - "Test failed. Equal(): %v was not equal to expected value: %v", + "Equal(): %v was not equal to expected value: %v", actual, expected, ) } @@ -240,7 +239,7 @@ func TestEqualIncludeReciprocal(t *testing.T) { expected = false if actual != expected { t.Errorf( - "Test failed. Equal(): %v was not equal to expected value: %v", + "Equal(): %v was not equal to expected value: %v", actual, expected, ) } @@ -250,7 +249,7 @@ func TestEqualIncludeReciprocal(t *testing.T) { expected = true if actual != expected { t.Errorf( - "Test failed. Equal(): %v was not equal to expected value: %v", + "Equal(): %v was not equal to expected value: %v", actual, expected, ) } @@ -263,7 +262,7 @@ func TestSwap(t *testing.T) { expected := "USDBTC" if actual != expected { t.Errorf( - "Test failed. TestSwap: %s was not equal to expected value: %s", + "TestSwap: %s was not equal to expected value: %s", actual, expected, ) } @@ -273,12 +272,12 @@ func TestEmpty(t *testing.T) { t.Parallel() pair := NewPair(BTC, USD) if pair.IsEmpty() { - t.Error("Test failed. Empty() returned true when the pair was initialised") + t.Error("Empty() returned true when the pair was initialised") } p := NewPair(NewCode(""), NewCode("")) if !p.IsEmpty() { - t.Error("Test failed. Empty() returned true when the pair wasn't initialised") + t.Error("Empty() returned true when the pair wasn't initialised") } } @@ -289,7 +288,7 @@ func TestNewPair(t *testing.T) { expected := defaultPair if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } @@ -302,7 +301,7 @@ func TestNewPairWithDelimiter(t *testing.T) { expected := "BTC-test-USD" if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } @@ -312,7 +311,7 @@ func TestNewPairWithDelimiter(t *testing.T) { expected = defaultPair if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } @@ -325,7 +324,7 @@ func TestNewPairDelimiter(t *testing.T) { expected := defaultPairWDelimiter if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } @@ -334,7 +333,7 @@ func TestNewPairDelimiter(t *testing.T) { expected = "-" if actual != expected { t.Errorf( - "Test failed. Delmiter: %s was not equal to expected value: %s", + "Delmiter: %s was not equal to expected value: %s", actual, expected, ) } @@ -344,12 +343,12 @@ func TestNewPairDelimiter(t *testing.T) { // specific index func TestNewPairFromIndex(t *testing.T) { t.Parallel() - currency := defaultPair + curr := defaultPair index := "BTC" - pair, err := NewPairFromIndex(currency, index) + pair, err := NewPairFromIndex(curr, index) if err != nil { - t.Error("test failed - NewPairFromIndex() error", err) + t.Error("NewPairFromIndex() error", err) } pair.Delimiter = "-" @@ -358,16 +357,16 @@ func TestNewPairFromIndex(t *testing.T) { expected := defaultPairWDelimiter if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } - currency = "DOGEBTC" + curr = "DOGEBTC" - pair, err = NewPairFromIndex(currency, index) + pair, err = NewPairFromIndex(curr, index) if err != nil { - t.Error("test failed - NewPairFromIndex() error", err) + t.Error("NewPairFromIndex() error", err) } pair.Delimiter = "-" @@ -376,7 +375,7 @@ func TestNewPairFromIndex(t *testing.T) { expected = "DOGE-BTC" if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } @@ -390,7 +389,7 @@ func TestNewPairFromString(t *testing.T) { expected := defaultPairWDelimiter if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } @@ -401,58 +400,82 @@ func TestNewPairFromString(t *testing.T) { expected = defaultPair if actual != expected { t.Errorf( - "Test failed. Pair(): %s was not equal to expected value: %s", + "Pair(): %s was not equal to expected value: %s", actual, expected, ) } } +func TestNewPairFromFormattedPairs(t *testing.T) { + t.Parallel() + pairs := Pairs{ + NewPairDelimiter("BTC-USDT", "-"), + NewPairDelimiter("LTC-USD", "-"), + } + + p := NewPairFromFormattedPairs("BTCUSDT", pairs, PairFormat{Uppercase: true}) + if p.String() != "BTC-USDT" { + t.Error("TestNewPairFromFormattedPairs: Expected currency was not found") + } + + p = NewPairFromFormattedPairs("btcusdt", pairs, PairFormat{Uppercase: false}) + if p.String() != "BTC-USDT" { + t.Error("TestNewPairFromFormattedPairs: Expected currency was not found") + } + + // Now a wrong one, will default to NewPairFromString + p = NewPairFromFormattedPairs("ethusdt", pairs, PairFormat{}) + if p.String() != "ethusdt" && p.Base.String() != "eth" { + t.Error("TestNewPairFromFormattedPairs: Expected currency was not found") + } +} + func TestContainsCurrency(t *testing.T) { p := NewPair(BTC, USD) if !p.ContainsCurrency(BTC) { - t.Error("Test failed. TestContainsCurrency: Expected currency was not found") + t.Error("TestContainsCurrency: Expected currency was not found") } if p.ContainsCurrency(ETH) { - t.Error("Test failed. TestContainsCurrency: Non-existent currency was found") + t.Error("TestContainsCurrency: Non-existent currency was found") } } func TestFormatPairs(t *testing.T) { newP, err := FormatPairs([]string{""}, "-", "") if err != nil { - t.Error("Test Failed - FormatPairs() error", err) + t.Error("FormatPairs() error", err) } if len(newP) > 0 { - t.Error("Test failed. TestFormatPairs: Empty string returned a valid pair") + t.Error("TestFormatPairs: Empty string returned a valid pair") } newP, err = FormatPairs([]string{defaultPairWDelimiter}, "-", "") if err != nil { - t.Error("Test Failed - FormatPairs() error", err) + t.Error("FormatPairs() error", err) } if newP[0].String() != defaultPairWDelimiter { - t.Error("Test failed. TestFormatPairs: Expected pair was not found") + t.Error("TestFormatPairs: Expected pair was not found") } newP, err = FormatPairs([]string{defaultPair}, "", "BTC") if err != nil { - t.Error("Test Failed - FormatPairs() error", err) + t.Error("FormatPairs() error", err) } if newP[0].String() != defaultPair { - t.Error("Test failed. TestFormatPairs: Expected pair was not found") + t.Error("TestFormatPairs: Expected pair was not found") } newP, err = FormatPairs([]string{"ETHUSD"}, "", "") if err != nil { - t.Error("Test Failed - FormatPairs() error", err) + t.Error("FormatPairs() error", err) } if newP[0].String() != "ETHUSD" { - t.Error("Test failed. TestFormatPairs: Expected pair was not found") + t.Error("TestFormatPairs: Expected pair was not found") } } @@ -468,12 +491,12 @@ func TestCopyPairFormat(t *testing.T) { result := CopyPairFormat(testPair, pairs, false) if result.String() != defaultPairWDelimiter { - t.Error("Test failed. TestCopyPairFormat: Expected pair was not found") + t.Error("TestCopyPairFormat: Expected pair was not found") } result = CopyPairFormat(NewPair(ETH, USD), pairs, true) if result.String() != "" { - t.Error("Test failed. TestCopyPairFormat: Unexpected non empty pair returned") + t.Error("TestCopyPairFormat: Unexpected non empty pair returned") } } @@ -483,26 +506,26 @@ func TestFindPairDifferences(t *testing.T) { // Test new pair update newPairs, removedPairs := pairList.FindDifferences(NewPairsFromStrings([]string{"DASH-USD"})) if len(newPairs) != 1 && len(removedPairs) != 3 { - t.Error("Test failed. TestFindPairDifferences: Unexpected values") + t.Error("TestFindPairDifferences: Unexpected values") } // Test that we don't allow empty strings for new pairs newPairs, removedPairs = pairList.FindDifferences(NewPairsFromStrings([]string{""})) if len(newPairs) != 0 && len(removedPairs) != 3 { - t.Error("Test failed. TestFindPairDifferences: Unexpected values") + t.Error("TestFindPairDifferences: Unexpected values") } // Test that we don't allow empty strings for new pairs newPairs, removedPairs = NewPairsFromStrings([]string{""}).FindDifferences(pairList) if len(newPairs) != 3 && len(removedPairs) != 0 { - t.Error("Test failed. TestFindPairDifferences: Unexpected values") + t.Error("TestFindPairDifferences: Unexpected values") } // Test that the supplied pair lists are the same, so // no newPairs or removedPairs newPairs, removedPairs = pairList.FindDifferences(pairList) if len(newPairs) != 0 && len(removedPairs) != 0 { - t.Error("Test failed. TestFindPairDifferences: Unexpected values") + t.Error("TestFindPairDifferences: Unexpected values") } } @@ -514,7 +537,7 @@ func TestPairsToStringArray(t *testing.T) { actual := pairs.Strings() if actual[0] != expected[0] { - t.Error("Test failed. TestPairsToStringArray: Unexpected values") + t.Error("TestPairsToStringArray: Unexpected values") } } @@ -523,7 +546,7 @@ func TestRandomPairFromPairs(t *testing.T) { var emptyPairs Pairs result := emptyPairs.GetRandomPair() if !result.IsEmpty() { - t.Error("Test failed. TestRandomPairFromPairs: Unexpected values") + t.Error("TestRandomPairFromPairs: Unexpected values") } // Test that a populated pairs array returns a non-empty currency pair @@ -532,7 +555,7 @@ func TestRandomPairFromPairs(t *testing.T) { result = pairs.GetRandomPair() if result.IsEmpty() { - t.Error("Test failed. TestRandomPairFromPairs: Unexpected values") + t.Error("TestRandomPairFromPairs: Unexpected values") } // Test that a populated pairs array over a number of attempts returns ALL @@ -550,7 +573,7 @@ func TestRandomPairFromPairs(t *testing.T) { for x := range pairs { _, ok := expectedResults[pairs[x].String()] if !ok { - t.Error("Test failed. TestRandomPairFromPairs: Unexpected values") + t.Error("TestRandomPairFromPairs: Unexpected values") } } } @@ -558,6 +581,6 @@ func TestRandomPairFromPairs(t *testing.T) { func TestIsInvalid(t *testing.T) { p := NewPair(LTC, LTC) if !p.IsInvalid() { - t.Error("Test Failed - IsInvalid() error expect true but received false") + t.Error("IsInvalid() error expect true but received false") } } diff --git a/currency/pairs.go b/currency/pairs.go index d8a134cb..66dd4617 100644 --- a/currency/pairs.go +++ b/currency/pairs.go @@ -1,9 +1,10 @@ package currency import ( + "encoding/json" "math/rand" + "strings" - "github.com/thrasher-corp/gocryptotrader/common" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -21,9 +22,6 @@ func NewPairsFromStrings(pairs []string) Pairs { return ps } -// Pairs defines a list of pairs -type Pairs []Pair - // Strings returns a slice of strings referring to each currency pair func (p Pairs) Strings() []string { var list []string @@ -35,7 +33,7 @@ func (p Pairs) Strings() []string { // Join returns a comma separated list of currency pairs func (p Pairs) Join() string { - return common.JoinStrings(p.Strings(), ",") + return strings.Join(p.Strings(), ",") } // Format formats the pair list to the exchange format configuration @@ -50,7 +48,8 @@ func (p Pairs) Format(delimiter, index string, uppercase bool) Pairs { if index != "" { newP, err := NewPairFromIndex(p[i].String(), index) if err != nil { - log.Errorf("failed to create NewPairFromIndex. Err: %s", err) + log.Errorf(log.Global, + "failed to create NewPairFromIndex. Err: %s\n", err) continue } formattedPair.Base = newP.Base @@ -69,13 +68,18 @@ func (p Pairs) Format(delimiter, index string, uppercase bool) Pairs { // UnmarshalJSON comforms type to the umarshaler interface func (p *Pairs) UnmarshalJSON(d []byte) error { var pairs string - err := common.JSONDecode(d, &pairs) + err := json.Unmarshal(d, &pairs) if err != nil { return err } + // If no pairs enabled in config just continue + if pairs == "" { + return nil + } + var allThePairs Pairs - for _, data := range common.SplitStrings(pairs, ",") { + for _, data := range strings.Split(pairs, ",") { allThePairs = append(allThePairs, NewPairFromString(data)) } @@ -85,7 +89,7 @@ func (p *Pairs) UnmarshalJSON(d []byte) error { // MarshalJSON conforms type to the marshaler interface func (p Pairs) MarshalJSON() ([]byte, error) { - return common.JSONEncode(p.Join()) + return json.Marshal(p.Join()) } // Upper returns an upper formatted pair list @@ -132,6 +136,27 @@ func (p Pairs) RemovePairsByFilter(filter Code) Pairs { return pairs } +// Remove removes the specified pair from the list of pairs if it exists +func (p Pairs) Remove(pair Pair) Pairs { + var pairs Pairs + for x := range p { + if p[x].Equal(pair) { + continue + } + pairs = append(pairs, p[x]) + } + return pairs +} + +// Add adds a specified pair to the list of pairs if it doesn't exist +func (p Pairs) Add(pair Pair) Pairs { + if p.Contains(pair, true) { + return p + } + p = append(p, pair) + return p +} + // FindDifferences returns pairs which are new or have been removed func (p Pairs) FindDifferences(pairs Pairs) (newPairs, removedPairs Pairs) { for x := range pairs { @@ -163,3 +188,6 @@ func (p Pairs) GetRandomPair() Pair { return p[rand.Intn(pairsLen)] } + +// Pairs defines a list of pairs +type Pairs []Pair diff --git a/currency/pairs_test.go b/currency/pairs_test.go index 1dd69d15..1af69a8f 100644 --- a/currency/pairs_test.go +++ b/currency/pairs_test.go @@ -1,9 +1,8 @@ package currency import ( + "encoding/json" "testing" - - "github.com/thrasher-corp/gocryptotrader/common" ) func TestPairsUpper(t *testing.T) { @@ -11,7 +10,7 @@ func TestPairsUpper(t *testing.T) { expected := "BTC_USD,BTC_AUD,BTC_LTC" if pairs.Upper().Join() != expected { - t.Errorf("Test Failed - Pairs Join() error expected %s but received %s", + t.Errorf("Pairs Join() error expected %s but received %s", expected, pairs.Join()) } } @@ -22,7 +21,7 @@ func TestPairsString(t *testing.T) { for i, p := range pairs { if p.String() != expected[i] { - t.Errorf("Test Failed - Pairs String() error expected %s but received %s", + t.Errorf("Pairs String() error expected %s but received %s", expected, p.String()) } } @@ -33,7 +32,7 @@ func TestPairsJoin(t *testing.T) { expected := "btc_usd,btc_aud,btc_ltc" if pairs.Join() != expected { - t.Errorf("Test Failed - Pairs Join() error expected %s but received %s", + t.Errorf("Pairs Join() error expected %s but received %s", expected, pairs.Join()) } } @@ -43,50 +42,65 @@ func TestPairsFormat(t *testing.T) { expected := "BTC-USD,BTC-AUD,BTC-LTC" if pairs.Format("-", "", true).Join() != expected { - t.Errorf("Test Failed - Pairs Join() error expected %s but received %s", + t.Errorf("Pairs Join() error expected %s but received %s", expected, pairs.Format("-", "", true).Join()) } expected = "btc:usd,btc:aud,btc:ltc" if pairs.Format(":", "", false).Join() != expected { - t.Errorf("Test Failed - Pairs Join() error expected %s but received %s", + t.Errorf("Pairs Join() error expected %s but received %s", expected, pairs.Format(":", "", false).Join()) } if pairs.Format(":", "KRW", false).Join() != "" { - t.Errorf("Test Failed - Pairs Join() error expected %s but received %s", + t.Errorf("Pairs Join() error expected %s but received %s", expected, pairs.Format(":", "KRW", true).Join()) } pairs = NewPairsFromStrings([]string{"DASHKRW", "BTCKRW"}) expected = "dash-krw,btc-krw" if pairs.Format("-", "KRW", false).Join() != expected { - t.Errorf("Test Failed - Pairs Join() error expected %s but received %s", + t.Errorf("Pairs Join() error expected %s but received %s", expected, pairs.Format("-", "KRW", false).Join()) } } func TestPairsUnmarshalJSON(t *testing.T) { var unmarshalHere Pairs - configPairs := "btc_usd,btc_aud,btc_ltc" - - encoded, err := common.JSONEncode(configPairs) + configPairs := "" + encoded, err := json.Marshal(configPairs) if err != nil { - t.Fatal("Test Failed - Pairs UnmarshalJSON() error", err) + t.Fatal("Pairs UnmarshalJSON() error", err) } - err = common.JSONDecode(encoded, &unmarshalHere) - if err != nil { - t.Fatal("Test Failed - Pairs UnmarshalJSON() error", err) + err = json.Unmarshal([]byte{1, 3, 3, 7}, &unmarshalHere) + if err == nil { + t.Fatal("error cannot be nil") } - err = common.JSONDecode(encoded, &unmarshalHere) + err = json.Unmarshal(encoded, &unmarshalHere) if err != nil { - t.Fatal("Test Failed - Pairs UnmarshalJSON() error", err) + t.Fatal("Pairs UnmarshalJSON() error", err) + } + + configPairs = "btc_usd,btc_aud,btc_ltc" + encoded, err = json.Marshal(configPairs) + if err != nil { + t.Fatal("Pairs UnmarshalJSON() error", err) + } + + err = json.Unmarshal(encoded, &unmarshalHere) + if err != nil { + t.Fatal("Pairs UnmarshalJSON() error", err) + } + + err = json.Unmarshal(encoded, &unmarshalHere) + if err != nil { + t.Fatal("Pairs UnmarshalJSON() error", err) } if unmarshalHere.Join() != configPairs { - t.Errorf("Test Failed - Pairs UnmarshalJSON() error expected %s but received %s", + t.Errorf("Pairs UnmarshalJSON() error expected %s but received %s", configPairs, unmarshalHere.Join()) } } @@ -98,14 +112,14 @@ func TestPairsMarshalJSON(t *testing.T) { Pairs: NewPairsFromStrings([]string{"btc_usd", "btc_aud", "btc_ltc"}), } - encoded, err := common.JSONEncode(quickstruct) + encoded, err := json.Marshal(quickstruct) if err != nil { - t.Fatal("Test Failed - Pairs MarshalJSON() error", err) + t.Fatal("Pairs MarshalJSON() error", err) } expected := `{"soManyPairs":"btc_usd,btc_aud,btc_ltc"}` if string(encoded) != expected { - t.Errorf("Test Failed - Pairs MarshalJSON() error expected %s but received %s", + t.Errorf("Pairs MarshalJSON() error expected %s but received %s", expected, string(encoded)) } } @@ -119,7 +133,42 @@ func TestRemovePairsByFilter(t *testing.T) { pairs = pairs.RemovePairsByFilter(USDT) if pairs.Contains(NewPair(LTC, USDT), true) { - t.Error("Test failed. TestRemovePairsByFilter unexpected result") + t.Error("TestRemovePairsByFilter unexpected result") + } +} + +func TestRemove(t *testing.T) { + var pairs = Pairs{ + NewPair(BTC, USD), + NewPair(LTC, USD), + NewPair(LTC, USDT), + } + + p := NewPair(BTC, USD) + pairs = pairs.Remove(p) + if pairs.Contains(p, true) || len(pairs) != 2 { + t.Error("TestRemove unexpected result") + } +} + +func TestAdd(t *testing.T) { + var pairs = Pairs{ + NewPair(BTC, USD), + NewPair(LTC, USD), + NewPair(LTC, USDT), + } + + // Test adding a new pair to the list of pairs + p := NewPair(BTC, USDT) + pairs = pairs.Add(p) + if !pairs.Contains(p, true) || len(pairs) != 4 { + t.Error("TestAdd unexpected result") + } + + // Now test adding a pair which already exists + pairs = pairs.Add(p) + if len(pairs) != 4 { + t.Error("TestAdd unexpected result") } } @@ -130,10 +179,10 @@ func TestContains(t *testing.T) { } if !pairs.Contains(NewPair(BTC, USD), true) { - t.Errorf("Test failed. TestContains: Expected pair was not found") + t.Errorf("TestContains: Expected pair was not found") } if pairs.Contains(NewPair(ETH, USD), false) { - t.Errorf("Test failed. TestContains: Non-existent pair was found") + t.Errorf("TestContains: Non-existent pair was found") } } diff --git a/currency/storage.go b/currency/storage.go index 4beaaf3f..3ed5aec0 100644 --- a/currency/storage.go +++ b/currency/storage.go @@ -4,84 +4,21 @@ import ( "encoding/json" "errors" "fmt" - "sync" + "io/ioutil" + "path/filepath" "time" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/file" "github.com/thrasher-corp/gocryptotrader/currency/coinmarketcap" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" log "github.com/thrasher-corp/gocryptotrader/logger" ) -// CurrencyFileUpdateDelay defines the rate at which the currency.json file is -// updated -const ( - DefaultCurrencyFileDelay = 168 * time.Hour - DefaultForeignExchangeDelay = 1 * time.Minute -) - func init() { storage.SetDefaults() } -// storage is an overarching type that keeps track of and updates currency, -// currency exchange rates and pairs -var storage Storage - -// Storage contains the loaded storage currencies supported on available crypto -// or fiat marketplaces -// NOTE: All internal currencies are upper case -type Storage struct { - // FiatCurrencies defines the running fiat currencies in the currency - // storage - fiatCurrencies Currencies - - // Cryptocurrencies defines the running cryptocurrencies in the currency - // storage - cryptocurrencies Currencies - - // CurrencyCodes is a full basket of currencies either crypto, fiat, ico or - // contract being tracked by the currency storage system - currencyCodes BaseCodes - - // Main converting currency - baseCurrency Code - - // FXRates defines a protected conversion rate map - fxRates ConversionRates - - // DefaultBaseCurrency is the base currency used for conversion - defaultBaseCurrency Code - - // DefaultFiatCurrencies has the default minimum of FIAT values - defaultFiatCurrencies Currencies - - // DefaultCryptoCurrencies has the default minimum of crytpocurrency values - defaultCryptoCurrencies Currencies - - // FiatExchangeMarkets defines an interface to access FX data for fiat - // currency rates - fiatExchangeMarkets *forexprovider.ForexProviders - - // CurrencyAnalysis defines a full market analysis suite to receieve and - // define different fiat currencies, cryptocurrencies and markets - currencyAnalysis *coinmarketcap.Coinmarketcap - - // Path defines the main folder to dump and find currency JSON - path string - - // Update delay variables - currencyFileUpdateDelay time.Duration - foreignExchangeUpdateDelay time.Duration - - mtx sync.Mutex - wg sync.WaitGroup - shutdownC chan struct{} - updaterRunning bool - Verbose bool -} - // SetDefaults sets storage defaults for basic package functionality func (s *Storage) SetDefaults() { s.defaultBaseCurrency = USD @@ -110,10 +47,13 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *MainConfiguration return errors.New("currency storage error, no fiat display currency set in config") } s.baseCurrency = settings.FiatDisplayCurrency - log.Debugf("Fiat display currency: %s.", s.baseCurrency) + log.Debugf(log.Global, + "Fiat display currency: %s.\n", s.baseCurrency) if settings.CryptocurrencyProvider.Enabled { - log.Debugf("Setting up currency analysis system with Coinmarketcap...") + log.Debugln( + log.Global, + "Setting up currency analysis system with Coinmarketcap...") c := &coinmarketcap.Coinmarketcap{} c.SetDefaults() err := c.Setup(coinmarketcap.Settings{ @@ -124,7 +64,8 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *MainConfiguration Verbose: settings.CryptocurrencyProvider.Verbose, }) if err != nil { - log.Errorf("Unable to setup CoinMarketCap analysis. Error: %s", err) + log.Errorf(log.Global, + "Unable to setup CoinMarketCap analysis. Error: %s", err) c = nil settings.CryptocurrencyProvider.Enabled = false } else { @@ -137,7 +78,7 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *MainConfiguration return errors.New("currency package runUpdater error filepath not set") } - s.path = filePath + common.GetOSPathSlash() + "currency.json" + s.path = filepath.Join(filePath, DefaultStorageFile) if settings.CurrencyDelay.Nanoseconds() == 0 { s.currencyFileUpdateDelay = DefaultCurrencyFileDelay @@ -202,11 +143,13 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *MainConfiguration return err } - log.Debugf("Primary foreign exchange conversion provider %s enabled", + log.Debugf(log.Global, + "Primary foreign exchange conversion provider %s enabled\n", s.fiatExchangeMarkets.Primary.Provider.GetName()) for i := range s.fiatExchangeMarkets.Support { - log.Debugf("Support forex conversion provider %s enabled", + log.Debugf(log.Global, + "Support forex conversion provider %s enabled\n", s.fiatExchangeMarkets.Support[i].Provider.GetName()) } @@ -214,7 +157,8 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *MainConfiguration // until this system initially updates go s.ForeignExchangeUpdater() } else { - log.Warnf("No foreign exchange providers enabled in config.json") + log.Warnln(log.Global, + "No foreign exchange providers enabled in config.json") s.mtx.Unlock() } @@ -231,19 +175,15 @@ func (s *Storage) SetupConversionRates() { // SetDefaultFiatCurrencies assigns the default fiat currency list and adds it // to the running list func (s *Storage) SetDefaultFiatCurrencies(c ...Code) { - for _, currency := range c { - s.defaultFiatCurrencies = append(s.defaultFiatCurrencies, currency) - s.fiatCurrencies = append(s.fiatCurrencies, currency) - } + s.defaultFiatCurrencies = append(s.defaultFiatCurrencies, c...) + s.fiatCurrencies = append(s.fiatCurrencies, c...) } // SetDefaultCryptocurrencies assigns the default cryptocurrency list and adds // it to the running list func (s *Storage) SetDefaultCryptocurrencies(c ...Code) { - for _, currency := range c { - s.defaultCryptoCurrencies = append(s.defaultCryptoCurrencies, currency) - s.cryptocurrencies = append(s.cryptocurrencies, currency) - } + s.defaultCryptoCurrencies = append(s.defaultCryptoCurrencies, c...) + s.cryptocurrencies = append(s.cryptocurrencies, c...) } // SetupForexProviders sets up a new instance of the forex providers @@ -260,19 +200,20 @@ func (s *Storage) SetupForexProviders(setting ...base.Settings) error { // ForeignExchangeUpdater is a routine that seeds foreign exchange rate and keeps // updated as fast as possible func (s *Storage) ForeignExchangeUpdater() { - log.Debugf("Foreign exchange updater started, seeding FX rate list..") + log.Debugln(log.Global, + "Foreign exchange updater started, seeding FX rate list..") s.wg.Add(1) defer s.wg.Done() err := s.SeedCurrencyAnalysisData() if err != nil { - log.Error(err) + log.Errorln(log.Global, err) } err = s.SeedForeignExchangeRates() if err != nil { - log.Error(err) + log.Errorln(log.Global, err) } // Unlock main rate retrieval mutex so all routines waiting can get access @@ -293,13 +234,13 @@ func (s *Storage) ForeignExchangeUpdater() { case <-SeedForeignExchangeTick.C: err := s.SeedForeignExchangeRates() if err != nil { - log.Error(err) + log.Errorln(log.Global, err) } case <-SeedCurrencyAnalysisTick.C: err := s.SeedCurrencyAnalysisData() if err != nil { - log.Error(err) + log.Errorln(log.Global, err) } } } @@ -307,7 +248,7 @@ func (s *Storage) ForeignExchangeUpdater() { // SeedCurrencyAnalysisData sets a new instance of a coinmarketcap data. func (s *Storage) SeedCurrencyAnalysisData() error { - b, err := common.ReadFile(s.path) + b, err := ioutil.ReadFile(s.path) if err != nil { err = s.FetchCurrencyAnalysisData() if err != nil { @@ -318,7 +259,7 @@ func (s *Storage) SeedCurrencyAnalysisData() error { } var fromFile File - err = common.JSONDecode(b, &fromFile) + err = json.Unmarshal(b, &fromFile) if err != nil { return err } @@ -346,7 +287,8 @@ func (s *Storage) SeedCurrencyAnalysisData() error { // loads it into memory func (s *Storage) FetchCurrencyAnalysisData() error { if s.currencyAnalysis == nil { - log.Warn("Currency analysis system offline please set api keys for coinmarketcap") + log.Warnln(log.Global, + "Currency analysis system offline, please set api keys for coinmarketcap if you wish to use this feature.") return errors.New("currency analysis system offline") } @@ -372,7 +314,7 @@ func (s *Storage) WriteCurrencyDataToFile(path string, mainUpdate bool) error { return err } - return common.WriteFile(path, encoded) + return file.Write(path, encoded) } // LoadFileCurrencyData loads currencies into the currency codes @@ -417,7 +359,7 @@ func (s *Storage) LoadFileCurrencyData(f *File) error { return nil } -// UpdateCurrencies updates currency roll and information using coin market cap +// UpdateCurrencies updates currency role and information using coin market cap func (s *Storage) UpdateCurrencies() error { m, err := s.currencyAnalysis.GetCryptocurrencyIDMap() if err != nil { @@ -561,8 +503,8 @@ func (s *Storage) GetTotalMarketCryptocurrencies() (Currencies, error) { // IsDefaultCurrency returns if a currency is a default currency func (s *Storage) IsDefaultCurrency(c Code) bool { t, _ := GetTranslation(c) - for _, d := range s.defaultFiatCurrencies { - if d.Match(c) || d.Match(t) { + for i := range s.defaultFiatCurrencies { + if s.defaultFiatCurrencies[i].Match(c) || s.defaultFiatCurrencies[i].Match(t) { return true } } diff --git a/currency/storage_test.go b/currency/storage_test.go index 2ed70a61..b9e562bf 100644 --- a/currency/storage_test.go +++ b/currency/storage_test.go @@ -8,7 +8,7 @@ func TestRunUpdater(t *testing.T) { emptyMainConfig := MainConfiguration{} err := newStorage.RunUpdater(BotOverrides{}, &emptyMainConfig, "", false) if err == nil { - t.Fatal("Test Failed storage RunUpdater() error cannot be nil") + t.Fatal("storage RunUpdater() error cannot be nil") } mainConfig := MainConfiguration{ @@ -18,11 +18,11 @@ func TestRunUpdater(t *testing.T) { err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "", false) if err == nil { - t.Fatal("Test Failed storage RunUpdater() error cannot be nil") + t.Fatal("storage RunUpdater() error cannot be nil") } err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "/bla", false) if err != nil { - t.Fatal("Test Failed storage RunUpdater() error", err) + t.Fatal("storage RunUpdater() error", err) } } diff --git a/currency/storage_types.go b/currency/storage_types.go new file mode 100644 index 00000000..032be5db --- /dev/null +++ b/currency/storage_types.go @@ -0,0 +1,62 @@ +package currency + +import ( + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency/coinmarketcap" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider" +) + +// CurrencyFileUpdateDelay defines the rate at which the currency.json file is +// updated +const ( + DefaultCurrencyFileDelay = 168 * time.Hour + DefaultForeignExchangeDelay = 1 * time.Minute + DefaultStorageFile = "currency.json" +) + +// storage is an overarching type that keeps track of and updates currency, +// currency exchange rates and pairs +var storage Storage + +// Storage contains the loaded storage currencies supported on available crypto +// or fiat marketplaces +// NOTE: All internal currencies are upper case +type Storage struct { + // FiatCurrencies defines the running fiat currencies in the currency + // storage + fiatCurrencies Currencies + // Cryptocurrencies defines the running cryptocurrencies in the currency + // storage + cryptocurrencies Currencies + // CurrencyCodes is a full basket of currencies either crypto, fiat, ico or + // contract being tracked by the currency storage system + currencyCodes BaseCodes + // Main converting currency + baseCurrency Code + // FXRates defines a protected conversion rate map + fxRates ConversionRates + // DefaultBaseCurrency is the base currency used for conversion + defaultBaseCurrency Code + // DefaultFiatCurrencies has the default minimum of FIAT values + defaultFiatCurrencies Currencies + // DefaultCryptoCurrencies has the default minimum of crytpocurrency values + defaultCryptoCurrencies Currencies + // FiatExchangeMarkets defines an interface to access FX data for fiat + // currency rates + fiatExchangeMarkets *forexprovider.ForexProviders + // CurrencyAnalysis defines a full market analysis suite to receieve and + // define different fiat currencies, cryptocurrencies and markets + currencyAnalysis *coinmarketcap.Coinmarketcap + // Path defines the main folder to dump and find currency JSON + path string + // Update delay variables + currencyFileUpdateDelay time.Duration + foreignExchangeUpdateDelay time.Duration + mtx sync.Mutex + wg sync.WaitGroup + shutdownC chan struct{} + updaterRunning bool + Verbose bool +} diff --git a/currency/symbol.go b/currency/symbol.go index 28f8221e..e677384a 100644 --- a/currency/symbol.go +++ b/currency/symbol.go @@ -2,6 +2,15 @@ package currency import "errors" +// GetSymbolByCurrencyName returns a currency symbol +func GetSymbolByCurrencyName(currency Code) (string, error) { + result, ok := symbols[currency.Item] + if !ok { + return "", errors.New("currency symbol not found") + } + return result, nil +} + // symbols map holds the currency name and symbol mappings var symbols = map[*Item]string{ ALL.Item: "Lek", @@ -116,12 +125,3 @@ var symbols = map[*Item]string{ YER.Item: "﷼", ZWD.Item: "Z$", } - -// GetSymbolByCurrencyName returns a currency symbol -func GetSymbolByCurrencyName(currency Code) (string, error) { - result, ok := symbols[currency.Item] - if !ok { - return "", errors.New("currency symbol not found") - } - return result, nil -} diff --git a/currency/symbol_test.go b/currency/symbol_test.go index 5dd6447a..94593ec4 100644 --- a/currency/symbol_test.go +++ b/currency/symbol_test.go @@ -6,15 +6,15 @@ func TestGetSymbolByCurrencyName(t *testing.T) { expected := "₩" actual, err := GetSymbolByCurrencyName(KPW) if err != nil { - t.Errorf("Test failed. TestGetSymbolByCurrencyName error: %s", err) + t.Errorf("TestGetSymbolByCurrencyName error: %s", err) } if actual != expected { - t.Errorf("Test failed. TestGetSymbolByCurrencyName differing values") + t.Errorf("TestGetSymbolByCurrencyName differing values") } _, err = GetSymbolByCurrencyName(Code{}) if err == nil { - t.Errorf("Test failed. TestGetSymbolByCurrencyNam returned nil on non-existent currency") + t.Errorf("TestGetSymbolByCurrencyNam returned nil on non-existent currency") } } diff --git a/currency/translation.go b/currency/translation.go index 3a0632b3..c2db7430 100644 --- a/currency/translation.go +++ b/currency/translation.go @@ -1,5 +1,15 @@ package currency +// GetTranslation returns similar strings for a particular currency if not found +// returns the code back +func GetTranslation(currency Code) (Code, bool) { + val, ok := translations[currency] + if !ok { + return currency, ok + } + return val, ok +} + var translations = map[Code]Code{ BTC: XBT, ETH: XETH, @@ -10,13 +20,3 @@ var translations = map[Code]Code{ XDG: DOGE, USDT: USD, } - -// GetTranslation returns similar strings for a particular currency if not found -// returns the code back -func GetTranslation(currency Code) (Code, bool) { - val, ok := translations[currency] - if !ok { - return currency, ok - } - return val, ok -} diff --git a/database/README.md b/database/README.md new file mode 100644 index 00000000..89b35f48 --- /dev/null +++ b/database/README.md @@ -0,0 +1,165 @@ +# GoCryptoTrader package Database + + + + +[![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/portfolio) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This database package is part of the GoCryptoTrader codebase. + +## This is still in active development + +You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for database package + ++ Establishes & Maintains database connection across program life cycle ++ Migration handed by [Goose](https://github.com/thrasher-corp/goose) ++ Model generation handled by [SQLBoiler](https://github.com/thrasher-corp/sqlboiler) + +## How to use + +##### Prerequisites + +[SQLBoiler](https://github.com/thrasher-corp/sqlboiler) +```shell script +go get -u github.com/thrasher-corp/sqlboiler +``` + +[Postgres Driver](https://github.com/thrasher-corp/sqlboiler/drivers/sqlboiler-psql) +```shell script +go get -u github.com/thrasher-corp/sqlboiler/drivers/sqlboiler-psql +``` + +[SQLite Driver](https://github.com/thrasher-corp/sqlboiler-sqlite3) +```shell script +go get -u github.com/thrasher-corp/sqlboiler-sqlite3 +``` + +##### Configuration + +The database configuration struct is currently: +```shell script +type Config struct { + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + Driver string `json:"driver"` + drivers.ConnectionDetails `json:"connectionDetails"` +} +``` +And Connection Details: +```sh +type ConnectionDetails struct { + Host string `json:"host"` + Port uint16 `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + Database string `json:"database"` + SSLMode string `json:"sslmode"` +} +``` + +With an example configuration being: + +```sh + "database": { + "enabled": true, + "verbose": true, + "driver": "postgres", + "connectionDetails": { + "host": "localhost", + "port": 5432, + "username": "gct-dev", + "password": "gct-dev", + "database": "gct-dev", + "sslmode": "disable" + } + }, +``` + +##### Create and Run migrations + Migrations are created using a modified version of [Goose](https://github.com/thrasher-corp/goose) + + A helper tool sits in the ./cmd/dbmigrate folder that includes the following features: + ++ Check current database version with the "status" command +```shell script +dbmigrate -command status +``` + ++ Create a new migration +```sh +dbmigrate -command "create" -args "model" +``` +_This will create a folder in the ./database/migration folder that contains postgres.sql and sqlite.sql files_ + + Run dbmigrate command with -command up +```shell script +dbmigrate -command "up" +``` + +dbmigrate provides a -migrationdir flag override to tell it what path to look in for migrations + +##### Adding a new model +Model's are generated using [SQLBoiler](https://github.com/thrasher-corp/sqlboiler) +A helper tool has been made located in gen_sqlboiler_config that will parse your GoCryptoTrader config and output a SQLBoiler config + +```sh +gen_sqlboiler_config +``` + +By default this will look in your gocryptotrader data folder and default config, these can be overwritten +along with the location of the sqlboiler generated config + +```shell script +-config "configname.json" +-datadir "~/.gocryptotrader/" +-outdir "~/.gocryptotrader/" +``` + + +Generate a new model that gets placed in ./database/models/ folder + +Linux: +```shell script +sqlboiler -o database/models/postgres -p postgres --no-auto-timestamps --wipe psql +``` +Windows: +```sh +sqlboiler -o database\\models\\postgres -p postgres --no-auto-timestamps --wipe psql +``` + +Helpers have been provided in the Makefile for linux users +``` +make gen_db_models +``` +And in the contrib/sqlboiler.cmd for windows users + +##### Adding a Repository ++ Create Repository directory in github.com/thrasher-corp/gocryptotrader/database/repository/ + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** + diff --git a/database/database_logger.go b/database/database_logger.go new file mode 100644 index 00000000..2d038ce3 --- /dev/null +++ b/database/database_logger.go @@ -0,0 +1,12 @@ +package database + +import log "github.com/thrasher-corp/gocryptotrader/logger" + +// Logger implements io.Writer interface to redirect SQLBoiler debug output to GCT logger +type Logger struct{} + +// Write takes input and sends to GCT logger +func (l Logger) Write(p []byte) (n int, err error) { + log.Debugf(log.DatabaseMgr, "SQL: %s", p) + return 0, nil +} diff --git a/database/database_types.go b/database/database_types.go new file mode 100644 index 00000000..3fce1a44 --- /dev/null +++ b/database/database_types.go @@ -0,0 +1,54 @@ +package database + +import ( + "database/sql" + "errors" + "path/filepath" + "sync" + + "github.com/thrasher-corp/gocryptotrader/database/drivers" +) + +// Db holds all information for a database instance +type Db struct { + SQL *sql.DB + DataPath string + Config *Config + + Connected bool + Mu sync.RWMutex +} + +// Config holds all database configurable options including enable/disabled & DSN settings +type Config struct { + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + Driver string `json:"driver"` + drivers.ConnectionDetails `json:"connectionDetails"` +} + +var ( + // DB Global Database Connection + DB = &Db{} + + // MigrationDir which folder to look in for current migrations + MigrationDir = filepath.Join("..", "..", "database", "migrations") + + // ErrNoDatabaseProvided error to display when no database is provided + ErrNoDatabaseProvided = errors.New("no database provided") + + // SupportedDrivers slice of supported database driver types + SupportedDrivers = []string{DBSQLite, DBSQLite3, DBPostgreSQL} + + // DefaultSQLiteDatabase is the default sqlite3 database name to use + DefaultSQLiteDatabase = "gocryptotrader.db" +) + +const ( + // DBSQLite const string for sqlite across code base + DBSQLite = "sqlite" + // DBSQLite3 const string for sqlite3 across code base + DBSQLite3 = "sqlite3" + // DBPostgreSQL const string for PostgreSQL across code base + DBPostgreSQL = "postgres" +) diff --git a/database/drivers/drivers_type.go b/database/drivers/drivers_type.go new file mode 100644 index 00000000..31656167 --- /dev/null +++ b/database/drivers/drivers_type.go @@ -0,0 +1,11 @@ +package drivers + +// ConnectionDetails holds DSN information +type ConnectionDetails struct { + Host string `json:"host"` + Port uint16 `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + Database string `json:"database"` + SSLMode string `json:"sslmode"` +} diff --git a/database/drivers/postgres/postgres.go b/database/drivers/postgres/postgres.go new file mode 100644 index 00000000..9ca54560 --- /dev/null +++ b/database/drivers/postgres/postgres.go @@ -0,0 +1,43 @@ +package postgres + +import ( + "database/sql" + "fmt" + "time" + + // import go libpq driver package + _ "github.com/lib/pq" + "github.com/thrasher-corp/gocryptotrader/database" +) + +// Connect opens a connection to Postgres database and returns a pointer to database.DB +func Connect() (*database.Db, error) { + if database.DB.Config.SSLMode == "" { + database.DB.Config.SSLMode = "disable" + } + + configDSN := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", + database.DB.Config.Username, + database.DB.Config.Password, + database.DB.Config.Host, + database.DB.Config.Port, + database.DB.Config.Database, + database.DB.Config.SSLMode) + + db, err := sql.Open(database.DBPostgreSQL, configDSN) + if err != nil { + return nil, err + } + + err = db.Ping() + if err != nil { + return nil, err + } + + database.DB.SQL = db + database.DB.SQL.SetMaxOpenConns(2) + database.DB.SQL.SetMaxIdleConns(1) + database.DB.SQL.SetConnMaxLifetime(time.Hour) + + return database.DB, nil +} diff --git a/database/drivers/sqlite3/sqlite3.go b/database/drivers/sqlite3/sqlite3.go new file mode 100644 index 00000000..51354549 --- /dev/null +++ b/database/drivers/sqlite3/sqlite3.go @@ -0,0 +1,29 @@ +package sqlite + +import ( + "database/sql" + "path/filepath" + + // import sqlite3 driver + _ "github.com/mattn/go-sqlite3" + "github.com/thrasher-corp/gocryptotrader/database" +) + +// Connect opens a connection to sqlite database and returns a pointer to database.DB +func Connect() (*database.Db, error) { + if database.DB.Config.Database == "" { + return nil, database.ErrNoDatabaseProvided + } + + databaseFullLocation := filepath.Join(database.DB.DataPath, database.DB.Config.Database) + + dbConn, err := sql.Open("sqlite3", databaseFullLocation) + if err != nil { + return nil, err + } + + database.DB.SQL = dbConn + database.DB.SQL.SetMaxOpenConns(1) + + return database.DB, nil +} diff --git a/database/migrations/20190916104959_audit_event/postgres.sql b/database/migrations/20190916104959_audit_event/postgres.sql new file mode 100644 index 00000000..1f69f4a9 --- /dev/null +++ b/database/migrations/20190916104959_audit_event/postgres.sql @@ -0,0 +1,13 @@ +-- +goose Up +-- SQL in this section is executed when the migration is applied. +CREATE TABLE IF NOT EXISTS audit_event +( + id bigserial PRIMARY KEY NOT NULL, + type varchar(255) NOT NULL, + identifier varchar(255) NOT NULL, + message text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT (now() at time zone 'utc') +); +-- +goose Down +-- SQL in this section is executed when the migration is rolled back. +DROP TABLE audit_event; \ No newline at end of file diff --git a/database/migrations/20190916104959_audit_event/sqlite3.sql b/database/migrations/20190916104959_audit_event/sqlite3.sql new file mode 100644 index 00000000..43988cc3 --- /dev/null +++ b/database/migrations/20190916104959_audit_event/sqlite3.sql @@ -0,0 +1,13 @@ +-- +goose Up +-- SQL in this section is executed when the migration is applied. +CREATE TABLE "audit_event" ( + id integer not null primary key, + type text not null, + identifier text not null, + message text not null, + created_at timestamp not null default CURRENT_TIMESTAMP + +); +-- +goose Down +-- SQL in this section is executed when the migration is rolled back. +DROP TABLE audit_event; \ No newline at end of file diff --git a/database/models/postgres/audit_event.go b/database/models/postgres/audit_event.go new file mode 100644 index 00000000..eafc9193 --- /dev/null +++ b/database/models/postgres/audit_event.go @@ -0,0 +1,925 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package postgres + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/thrasher-corp/sqlboiler/boil" + "github.com/thrasher-corp/sqlboiler/queries" + "github.com/thrasher-corp/sqlboiler/queries/qm" + "github.com/thrasher-corp/sqlboiler/queries/qmhelper" + "github.com/thrasher-corp/sqlboiler/strmangle" +) + +// AuditEvent is an object representing the database table. +type AuditEvent struct { + ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"` + Type string `boil:"type" json:"type" toml:"type" yaml:"type"` + Identifier string `boil:"identifier" json:"identifier" toml:"identifier" yaml:"identifier"` + Message string `boil:"message" json:"message" toml:"message" yaml:"message"` + CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"` + + R *auditEventR `boil:"-" json:"-" toml:"-" yaml:"-"` + L auditEventL `boil:"-" json:"-" toml:"-" yaml:"-"` +} + +var AuditEventColumns = struct { + ID string + Type string + Identifier string + Message string + CreatedAt string +}{ + ID: "id", + Type: "type", + Identifier: "identifier", + Message: "message", + CreatedAt: "created_at", +} + +// Generated where + +type whereHelperint64 struct{ field string } + +func (w whereHelperint64) EQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } +func (w whereHelperint64) NEQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) } +func (w whereHelperint64) LT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) } +func (w whereHelperint64) LTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) } +func (w whereHelperint64) GT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) } +func (w whereHelperint64) GTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } + +type whereHelperstring struct{ field string } + +func (w whereHelperstring) EQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } +func (w whereHelperstring) NEQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) } +func (w whereHelperstring) LT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) } +func (w whereHelperstring) LTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) } +func (w whereHelperstring) GT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) } +func (w whereHelperstring) GTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } +func (w whereHelperstring) IN(slice []string) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) +} + +type whereHelpertime_Time struct{ field string } + +func (w whereHelpertime_Time) EQ(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.EQ, x) +} +func (w whereHelpertime_Time) NEQ(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.NEQ, x) +} +func (w whereHelpertime_Time) LT(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LT, x) +} +func (w whereHelpertime_Time) LTE(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LTE, x) +} +func (w whereHelpertime_Time) GT(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GT, x) +} +func (w whereHelpertime_Time) GTE(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GTE, x) +} + +var AuditEventWhere = struct { + ID whereHelperint64 + Type whereHelperstring + Identifier whereHelperstring + Message whereHelperstring + CreatedAt whereHelpertime_Time +}{ + ID: whereHelperint64{field: "\"audit_event\".\"id\""}, + Type: whereHelperstring{field: "\"audit_event\".\"type\""}, + Identifier: whereHelperstring{field: "\"audit_event\".\"identifier\""}, + Message: whereHelperstring{field: "\"audit_event\".\"message\""}, + CreatedAt: whereHelpertime_Time{field: "\"audit_event\".\"created_at\""}, +} + +// AuditEventRels is where relationship names are stored. +var AuditEventRels = struct { +}{} + +// auditEventR is where relationships are stored. +type auditEventR struct { +} + +// NewStruct creates a new relationship struct +func (*auditEventR) NewStruct() *auditEventR { + return &auditEventR{} +} + +// auditEventL is where Load methods for each relationship are stored. +type auditEventL struct{} + +var ( + auditEventAllColumns = []string{"id", "type", "identifier", "message", "created_at"} + auditEventColumnsWithoutDefault = []string{"type", "identifier", "message"} + auditEventColumnsWithDefault = []string{"id", "created_at"} + auditEventPrimaryKeyColumns = []string{"id"} +) + +type ( + // AuditEventSlice is an alias for a slice of pointers to AuditEvent. + // This should generally be used opposed to []AuditEvent. + AuditEventSlice []*AuditEvent + // AuditEventHook is the signature for custom AuditEvent hook methods + AuditEventHook func(context.Context, boil.ContextExecutor, *AuditEvent) error + + auditEventQuery struct { + *queries.Query + } +) + +// Cache for insert, update and upsert +var ( + auditEventType = reflect.TypeOf(&AuditEvent{}) + auditEventMapping = queries.MakeStructMapping(auditEventType) + auditEventPrimaryKeyMapping, _ = queries.BindMapping(auditEventType, auditEventMapping, auditEventPrimaryKeyColumns) + auditEventInsertCacheMut sync.RWMutex + auditEventInsertCache = make(map[string]insertCache) + auditEventUpdateCacheMut sync.RWMutex + auditEventUpdateCache = make(map[string]updateCache) + auditEventUpsertCacheMut sync.RWMutex + auditEventUpsertCache = make(map[string]insertCache) +) + +var ( + // Force time package dependency for automated UpdatedAt/CreatedAt. + _ = time.Second + // Force qmhelper dependency for where clause generation (which doesn't + // always happen) + _ = qmhelper.Where +) + +var auditEventBeforeInsertHooks []AuditEventHook +var auditEventBeforeUpdateHooks []AuditEventHook +var auditEventBeforeDeleteHooks []AuditEventHook +var auditEventBeforeUpsertHooks []AuditEventHook + +var auditEventAfterInsertHooks []AuditEventHook +var auditEventAfterSelectHooks []AuditEventHook +var auditEventAfterUpdateHooks []AuditEventHook +var auditEventAfterDeleteHooks []AuditEventHook +var auditEventAfterUpsertHooks []AuditEventHook + +// doBeforeInsertHooks executes all "before insert" hooks. +func (o *AuditEvent) doBeforeInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventBeforeInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpdateHooks executes all "before Update" hooks. +func (o *AuditEvent) doBeforeUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventBeforeUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeDeleteHooks executes all "before Delete" hooks. +func (o *AuditEvent) doBeforeDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventBeforeDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpsertHooks executes all "before Upsert" hooks. +func (o *AuditEvent) doBeforeUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventBeforeUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterInsertHooks executes all "after Insert" hooks. +func (o *AuditEvent) doAfterInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventAfterInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterSelectHooks executes all "after Select" hooks. +func (o *AuditEvent) doAfterSelectHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventAfterSelectHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpdateHooks executes all "after Update" hooks. +func (o *AuditEvent) doAfterUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventAfterUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterDeleteHooks executes all "after Delete" hooks. +func (o *AuditEvent) doAfterDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventAfterDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpsertHooks executes all "after Upsert" hooks. +func (o *AuditEvent) doAfterUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventAfterUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// AddAuditEventHook registers your hook function for all future operations. +func AddAuditEventHook(hookPoint boil.HookPoint, auditEventHook AuditEventHook) { + switch hookPoint { + case boil.BeforeInsertHook: + auditEventBeforeInsertHooks = append(auditEventBeforeInsertHooks, auditEventHook) + case boil.BeforeUpdateHook: + auditEventBeforeUpdateHooks = append(auditEventBeforeUpdateHooks, auditEventHook) + case boil.BeforeDeleteHook: + auditEventBeforeDeleteHooks = append(auditEventBeforeDeleteHooks, auditEventHook) + case boil.BeforeUpsertHook: + auditEventBeforeUpsertHooks = append(auditEventBeforeUpsertHooks, auditEventHook) + case boil.AfterInsertHook: + auditEventAfterInsertHooks = append(auditEventAfterInsertHooks, auditEventHook) + case boil.AfterSelectHook: + auditEventAfterSelectHooks = append(auditEventAfterSelectHooks, auditEventHook) + case boil.AfterUpdateHook: + auditEventAfterUpdateHooks = append(auditEventAfterUpdateHooks, auditEventHook) + case boil.AfterDeleteHook: + auditEventAfterDeleteHooks = append(auditEventAfterDeleteHooks, auditEventHook) + case boil.AfterUpsertHook: + auditEventAfterUpsertHooks = append(auditEventAfterUpsertHooks, auditEventHook) + } +} + +// One returns a single auditEvent record from the query. +func (q auditEventQuery) One(ctx context.Context, exec boil.ContextExecutor) (*AuditEvent, error) { + o := &AuditEvent{} + + queries.SetLimit(q.Query, 1) + + err := q.Bind(ctx, exec, o) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "postgres: failed to execute a one query for audit_event") + } + + if err := o.doAfterSelectHooks(ctx, exec); err != nil { + return o, err + } + + return o, nil +} + +// All returns all AuditEvent records from the query. +func (q auditEventQuery) All(ctx context.Context, exec boil.ContextExecutor) (AuditEventSlice, error) { + var o []*AuditEvent + + err := q.Bind(ctx, exec, &o) + if err != nil { + return nil, errors.Wrap(err, "postgres: failed to assign all query results to AuditEvent slice") + } + + if len(auditEventAfterSelectHooks) != 0 { + for _, obj := range o { + if err := obj.doAfterSelectHooks(ctx, exec); err != nil { + return o, err + } + } + } + + return o, nil +} + +// Count returns the count of all AuditEvent records in the query. +func (q auditEventQuery) Count(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return 0, errors.Wrap(err, "postgres: failed to count audit_event rows") + } + + return count, nil +} + +// Exists checks if the row exists in the table. +func (q auditEventQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + queries.SetLimit(q.Query, 1) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return false, errors.Wrap(err, "postgres: failed to check if audit_event exists") + } + + return count > 0, nil +} + +// AuditEvents retrieves all the records using an executor. +func AuditEvents(mods ...qm.QueryMod) auditEventQuery { + mods = append(mods, qm.From("\"audit_event\"")) + return auditEventQuery{NewQuery(mods...)} +} + +// FindAuditEvent retrieves a single record by ID with an executor. +// If selectCols is empty Find will return all columns. +func FindAuditEvent(ctx context.Context, exec boil.ContextExecutor, iD int64, selectCols ...string) (*AuditEvent, error) { + auditEventObj := &AuditEvent{} + + sel := "*" + if len(selectCols) > 0 { + sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",") + } + query := fmt.Sprintf( + "select %s from \"audit_event\" where \"id\"=$1", sel, + ) + + q := queries.Raw(query, iD) + + err := q.Bind(ctx, exec, auditEventObj) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "postgres: unable to select from audit_event") + } + + return auditEventObj, nil +} + +// Insert a single record using an executor. +// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts. +func (o *AuditEvent) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error { + if o == nil { + return errors.New("postgres: no audit_event provided for insertion") + } + + var err error + + if err := o.doBeforeInsertHooks(ctx, exec); err != nil { + return err + } + + nzDefaults := queries.NonZeroDefaultSet(auditEventColumnsWithDefault, o) + + key := makeCacheKey(columns, nzDefaults) + auditEventInsertCacheMut.RLock() + cache, cached := auditEventInsertCache[key] + auditEventInsertCacheMut.RUnlock() + + if !cached { + wl, returnColumns := columns.InsertColumnSet( + auditEventAllColumns, + auditEventColumnsWithDefault, + auditEventColumnsWithoutDefault, + nzDefaults, + ) + + cache.valueMapping, err = queries.BindMapping(auditEventType, auditEventMapping, wl) + if err != nil { + return err + } + cache.retMapping, err = queries.BindMapping(auditEventType, auditEventMapping, returnColumns) + if err != nil { + return err + } + if len(wl) != 0 { + cache.query = fmt.Sprintf("INSERT INTO \"audit_event\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1)) + } else { + cache.query = "INSERT INTO \"audit_event\" %sDEFAULT VALUES%s" + } + + var queryOutput, queryReturning string + + if len(cache.retMapping) != 0 { + queryReturning = fmt.Sprintf(" RETURNING \"%s\"", strings.Join(returnColumns, "\",\"")) + } + + cache.query = fmt.Sprintf(cache.query, queryOutput, queryReturning) + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, cache.query) + fmt.Fprintln(boil.DebugWriter, vals) + } + + if len(cache.retMapping) != 0 { + err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(queries.PtrsFromMapping(value, cache.retMapping)...) + } else { + _, err = exec.ExecContext(ctx, cache.query, vals...) + } + + if err != nil { + return errors.Wrap(err, "postgres: unable to insert into audit_event") + } + + if !cached { + auditEventInsertCacheMut.Lock() + auditEventInsertCache[key] = cache + auditEventInsertCacheMut.Unlock() + } + + return o.doAfterInsertHooks(ctx, exec) +} + +// Update uses an executor to update the AuditEvent. +// See boil.Columns.UpdateColumnSet documentation to understand column list inference for updates. +// Update does not automatically update the record in case of default values. Use .Reload() to refresh the records. +func (o *AuditEvent) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) { + var err error + if err = o.doBeforeUpdateHooks(ctx, exec); err != nil { + return 0, err + } + key := makeCacheKey(columns, nil) + auditEventUpdateCacheMut.RLock() + cache, cached := auditEventUpdateCache[key] + auditEventUpdateCacheMut.RUnlock() + + if !cached { + wl := columns.UpdateColumnSet( + auditEventAllColumns, + auditEventPrimaryKeyColumns, + ) + + if len(wl) == 0 { + return 0, errors.New("postgres: unable to update audit_event, could not build whitelist") + } + + cache.query = fmt.Sprintf("UPDATE \"audit_event\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, wl), + strmangle.WhereClause("\"", "\"", len(wl)+1, auditEventPrimaryKeyColumns), + ) + cache.valueMapping, err = queries.BindMapping(auditEventType, auditEventMapping, append(wl, auditEventPrimaryKeyColumns...)) + if err != nil { + return 0, err + } + } + + values := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), cache.valueMapping) + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, cache.query) + fmt.Fprintln(boil.DebugWriter, values) + } + + var result sql.Result + result, err = exec.ExecContext(ctx, cache.query, values...) + if err != nil { + return 0, errors.Wrap(err, "postgres: unable to update audit_event row") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "postgres: failed to get rows affected by update for audit_event") + } + + if !cached { + auditEventUpdateCacheMut.Lock() + auditEventUpdateCache[key] = cache + auditEventUpdateCacheMut.Unlock() + } + + return rowsAff, o.doAfterUpdateHooks(ctx, exec) +} + +// UpdateAll updates all rows with the specified column values. +func (q auditEventQuery) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + queries.SetUpdate(q.Query, cols) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "postgres: unable to update all for audit_event") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "postgres: unable to retrieve rows affected for audit_event") + } + + return rowsAff, nil +} + +// UpdateAll updates all rows with the specified column values, using an executor. +func (o AuditEventSlice) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + ln := int64(len(o)) + if ln == 0 { + return 0, nil + } + + if len(cols) == 0 { + return 0, errors.New("postgres: update all requires at least one column argument") + } + + colNames := make([]string, len(cols)) + args := make([]interface{}, len(cols)) + + i := 0 + for name, value := range cols { + colNames[i] = name + args[i] = value + i++ + } + + // Append all of the primary key values for each column + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), auditEventPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := fmt.Sprintf("UPDATE \"audit_event\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, colNames), + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), len(colNames)+1, auditEventPrimaryKeyColumns, len(o))) + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, sql) + fmt.Fprintln(boil.DebugWriter, args...) + } + + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "postgres: unable to update all in auditEvent slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "postgres: unable to retrieve rows affected all in update all auditEvent") + } + return rowsAff, nil +} + +// Upsert attempts an insert using an executor, and does an update or ignore on conflict. +// See boil.Columns documentation for how to properly use updateColumns and insertColumns. +func (o *AuditEvent) Upsert(ctx context.Context, exec boil.ContextExecutor, updateOnConflict bool, conflictColumns []string, updateColumns, insertColumns boil.Columns) error { + if o == nil { + return errors.New("postgres: no audit_event provided for upsert") + } + + if err := o.doBeforeUpsertHooks(ctx, exec); err != nil { + return err + } + + nzDefaults := queries.NonZeroDefaultSet(auditEventColumnsWithDefault, o) + + // Build cache key in-line uglily - mysql vs psql problems + buf := strmangle.GetBuffer() + if updateOnConflict { + buf.WriteByte('t') + } else { + buf.WriteByte('f') + } + buf.WriteByte('.') + for _, c := range conflictColumns { + buf.WriteString(c) + } + buf.WriteByte('.') + buf.WriteString(strconv.Itoa(updateColumns.Kind)) + for _, c := range updateColumns.Cols { + buf.WriteString(c) + } + buf.WriteByte('.') + buf.WriteString(strconv.Itoa(insertColumns.Kind)) + for _, c := range insertColumns.Cols { + buf.WriteString(c) + } + buf.WriteByte('.') + for _, c := range nzDefaults { + buf.WriteString(c) + } + key := buf.String() + strmangle.PutBuffer(buf) + + auditEventUpsertCacheMut.RLock() + cache, cached := auditEventUpsertCache[key] + auditEventUpsertCacheMut.RUnlock() + + var err error + + if !cached { + insert, ret := insertColumns.InsertColumnSet( + auditEventAllColumns, + auditEventColumnsWithDefault, + auditEventColumnsWithoutDefault, + nzDefaults, + ) + update := updateColumns.UpdateColumnSet( + auditEventAllColumns, + auditEventPrimaryKeyColumns, + ) + + if updateOnConflict && len(update) == 0 { + return errors.New("postgres: unable to upsert audit_event, could not build update column list") + } + + conflict := conflictColumns + if len(conflict) == 0 { + conflict = make([]string, len(auditEventPrimaryKeyColumns)) + copy(conflict, auditEventPrimaryKeyColumns) + } + cache.query = buildUpsertQueryPostgres(dialect, "\"audit_event\"", updateOnConflict, ret, update, conflict, insert) + + cache.valueMapping, err = queries.BindMapping(auditEventType, auditEventMapping, insert) + if err != nil { + return err + } + if len(ret) != 0 { + cache.retMapping, err = queries.BindMapping(auditEventType, auditEventMapping, ret) + if err != nil { + return err + } + } + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + var returns []interface{} + if len(cache.retMapping) != 0 { + returns = queries.PtrsFromMapping(value, cache.retMapping) + } + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, cache.query) + fmt.Fprintln(boil.DebugWriter, vals) + } + + if len(cache.retMapping) != 0 { + err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(returns...) + if err == sql.ErrNoRows { + err = nil // Postgres doesn't return anything when there's no update + } + } else { + _, err = exec.ExecContext(ctx, cache.query, vals...) + } + if err != nil { + return errors.Wrap(err, "postgres: unable to upsert audit_event") + } + + if !cached { + auditEventUpsertCacheMut.Lock() + auditEventUpsertCache[key] = cache + auditEventUpsertCacheMut.Unlock() + } + + return o.doAfterUpsertHooks(ctx, exec) +} + +// Delete deletes a single AuditEvent record with an executor. +// Delete will match against the primary key column to find the record to delete. +func (o *AuditEvent) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if o == nil { + return 0, errors.New("postgres: no AuditEvent provided for delete") + } + + if err := o.doBeforeDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + args := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), auditEventPrimaryKeyMapping) + sql := "DELETE FROM \"audit_event\" WHERE \"id\"=$1" + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, sql) + fmt.Fprintln(boil.DebugWriter, args...) + } + + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "postgres: unable to delete from audit_event") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "postgres: failed to get rows affected by delete for audit_event") + } + + if err := o.doAfterDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + return rowsAff, nil +} + +// DeleteAll deletes all matching rows. +func (q auditEventQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if q.Query == nil { + return 0, errors.New("postgres: no auditEventQuery provided for delete all") + } + + queries.SetDelete(q.Query) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "postgres: unable to delete all from audit_event") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "postgres: failed to get rows affected by deleteall for audit_event") + } + + return rowsAff, nil +} + +// DeleteAll deletes all rows in the slice, using an executor. +func (o AuditEventSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if len(o) == 0 { + return 0, nil + } + + if len(auditEventBeforeDeleteHooks) != 0 { + for _, obj := range o { + if err := obj.doBeforeDeleteHooks(ctx, exec); err != nil { + return 0, err + } + } + } + + var args []interface{} + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), auditEventPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "DELETE FROM \"audit_event\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 1, auditEventPrimaryKeyColumns, len(o)) + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, sql) + fmt.Fprintln(boil.DebugWriter, args) + } + + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "postgres: unable to delete all from auditEvent slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "postgres: failed to get rows affected by deleteall for audit_event") + } + + if len(auditEventAfterDeleteHooks) != 0 { + for _, obj := range o { + if err := obj.doAfterDeleteHooks(ctx, exec); err != nil { + return 0, err + } + } + } + + return rowsAff, nil +} + +// Reload refetches the object from the database +// using the primary keys with an executor. +func (o *AuditEvent) Reload(ctx context.Context, exec boil.ContextExecutor) error { + ret, err := FindAuditEvent(ctx, exec, o.ID) + if err != nil { + return err + } + + *o = *ret + return nil +} + +// ReloadAll refetches every row with matching primary key column values +// and overwrites the original object slice with the newly updated slice. +func (o *AuditEventSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error { + if o == nil || len(*o) == 0 { + return nil + } + + slice := AuditEventSlice{} + var args []interface{} + for _, obj := range *o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), auditEventPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "SELECT \"audit_event\".* FROM \"audit_event\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 1, auditEventPrimaryKeyColumns, len(*o)) + + q := queries.Raw(sql, args...) + + err := q.Bind(ctx, exec, &slice) + if err != nil { + return errors.Wrap(err, "postgres: unable to reload all in AuditEventSlice") + } + + *o = slice + + return nil +} + +// AuditEventExists checks if the AuditEvent row exists. +func AuditEventExists(ctx context.Context, exec boil.ContextExecutor, iD int64) (bool, error) { + var exists bool + sql := "select exists(select 1 from \"audit_event\" where \"id\"=$1 limit 1)" + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, sql) + fmt.Fprintln(boil.DebugWriter, iD) + } + + row := exec.QueryRowContext(ctx, sql, iD) + + err := row.Scan(&exists) + if err != nil { + return false, errors.Wrap(err, "postgres: unable to check if audit_event exists") + } + + return exists, nil +} diff --git a/database/models/postgres/audit_event_test.go b/database/models/postgres/audit_event_test.go new file mode 100644 index 00000000..c9a403ec --- /dev/null +++ b/database/models/postgres/audit_event_test.go @@ -0,0 +1,732 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package postgres + +import ( + "bytes" + "context" + "reflect" + "testing" + + "github.com/thrasher-corp/sqlboiler/boil" + "github.com/thrasher-corp/sqlboiler/queries" + "github.com/thrasher-corp/sqlboiler/randomize" + "github.com/thrasher-corp/sqlboiler/strmangle" +) + +var ( + // Relationships sometimes use the reflection helper queries.Equal/queries.Assign + // so force a package dependency in case they don't. + _ = queries.Equal +) + +func testAuditEvents(t *testing.T) { + t.Parallel() + + query := AuditEvents() + + if query.Query == nil { + t.Error("expected a query, got nothing") + } +} + +func testAuditEventsDelete(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + if rowsAff, err := o.Delete(ctx, tx); err != nil { + t.Error(err) + } else if rowsAff != 1 { + t.Error("should only have deleted one row, but affected:", rowsAff) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 0 { + t.Error("want zero records, got:", count) + } +} + +func testAuditEventsQueryDeleteAll(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + if rowsAff, err := AuditEvents().DeleteAll(ctx, tx); err != nil { + t.Error(err) + } else if rowsAff != 1 { + t.Error("should only have deleted one row, but affected:", rowsAff) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 0 { + t.Error("want zero records, got:", count) + } +} + +func testAuditEventsSliceDeleteAll(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + slice := AuditEventSlice{o} + + if rowsAff, err := slice.DeleteAll(ctx, tx); err != nil { + t.Error(err) + } else if rowsAff != 1 { + t.Error("should only have deleted one row, but affected:", rowsAff) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 0 { + t.Error("want zero records, got:", count) + } +} + +func testAuditEventsExists(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + e, err := AuditEventExists(ctx, tx, o.ID) + if err != nil { + t.Errorf("Unable to check if AuditEvent exists: %s", err) + } + if !e { + t.Errorf("Expected AuditEventExists to return true, but got false.") + } +} + +func testAuditEventsFind(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + auditEventFound, err := FindAuditEvent(ctx, tx, o.ID) + if err != nil { + t.Error(err) + } + + if auditEventFound == nil { + t.Error("want a record, got nil") + } +} + +func testAuditEventsBind(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + if err = AuditEvents().Bind(ctx, tx, o); err != nil { + t.Error(err) + } +} + +func testAuditEventsOne(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + if x, err := AuditEvents().One(ctx, tx); err != nil { + t.Error(err) + } else if x == nil { + t.Error("expected to get a non nil record") + } +} + +func testAuditEventsAll(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + auditEventOne := &AuditEvent{} + auditEventTwo := &AuditEvent{} + if err = randomize.Struct(seed, auditEventOne, auditEventDBTypes, false, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + if err = randomize.Struct(seed, auditEventTwo, auditEventDBTypes, false, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = auditEventOne.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + if err = auditEventTwo.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + slice, err := AuditEvents().All(ctx, tx) + if err != nil { + t.Error(err) + } + + if len(slice) != 2 { + t.Error("want 2 records, got:", len(slice)) + } +} + +func testAuditEventsCount(t *testing.T) { + t.Parallel() + + var err error + seed := randomize.NewSeed() + auditEventOne := &AuditEvent{} + auditEventTwo := &AuditEvent{} + if err = randomize.Struct(seed, auditEventOne, auditEventDBTypes, false, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + if err = randomize.Struct(seed, auditEventTwo, auditEventDBTypes, false, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = auditEventOne.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + if err = auditEventTwo.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 2 { + t.Error("want 2 records, got:", count) + } +} + +func auditEventBeforeInsertHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventAfterInsertHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventAfterSelectHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventBeforeUpdateHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventAfterUpdateHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventBeforeDeleteHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventAfterDeleteHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventBeforeUpsertHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventAfterUpsertHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func testAuditEventsHooks(t *testing.T) { + t.Parallel() + + var err error + + ctx := context.Background() + empty := &AuditEvent{} + o := &AuditEvent{} + + seed := randomize.NewSeed() + if err = randomize.Struct(seed, o, auditEventDBTypes, false); err != nil { + t.Errorf("Unable to randomize AuditEvent object: %s", err) + } + + AddAuditEventHook(boil.BeforeInsertHook, auditEventBeforeInsertHook) + if err = o.doBeforeInsertHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doBeforeInsertHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected BeforeInsertHook function to empty object, but got: %#v", o) + } + auditEventBeforeInsertHooks = []AuditEventHook{} + + AddAuditEventHook(boil.AfterInsertHook, auditEventAfterInsertHook) + if err = o.doAfterInsertHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doAfterInsertHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected AfterInsertHook function to empty object, but got: %#v", o) + } + auditEventAfterInsertHooks = []AuditEventHook{} + + AddAuditEventHook(boil.AfterSelectHook, auditEventAfterSelectHook) + if err = o.doAfterSelectHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doAfterSelectHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected AfterSelectHook function to empty object, but got: %#v", o) + } + auditEventAfterSelectHooks = []AuditEventHook{} + + AddAuditEventHook(boil.BeforeUpdateHook, auditEventBeforeUpdateHook) + if err = o.doBeforeUpdateHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doBeforeUpdateHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected BeforeUpdateHook function to empty object, but got: %#v", o) + } + auditEventBeforeUpdateHooks = []AuditEventHook{} + + AddAuditEventHook(boil.AfterUpdateHook, auditEventAfterUpdateHook) + if err = o.doAfterUpdateHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doAfterUpdateHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected AfterUpdateHook function to empty object, but got: %#v", o) + } + auditEventAfterUpdateHooks = []AuditEventHook{} + + AddAuditEventHook(boil.BeforeDeleteHook, auditEventBeforeDeleteHook) + if err = o.doBeforeDeleteHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doBeforeDeleteHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected BeforeDeleteHook function to empty object, but got: %#v", o) + } + auditEventBeforeDeleteHooks = []AuditEventHook{} + + AddAuditEventHook(boil.AfterDeleteHook, auditEventAfterDeleteHook) + if err = o.doAfterDeleteHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doAfterDeleteHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected AfterDeleteHook function to empty object, but got: %#v", o) + } + auditEventAfterDeleteHooks = []AuditEventHook{} + + AddAuditEventHook(boil.BeforeUpsertHook, auditEventBeforeUpsertHook) + if err = o.doBeforeUpsertHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doBeforeUpsertHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected BeforeUpsertHook function to empty object, but got: %#v", o) + } + auditEventBeforeUpsertHooks = []AuditEventHook{} + + AddAuditEventHook(boil.AfterUpsertHook, auditEventAfterUpsertHook) + if err = o.doAfterUpsertHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doAfterUpsertHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected AfterUpsertHook function to empty object, but got: %#v", o) + } + auditEventAfterUpsertHooks = []AuditEventHook{} +} + +func testAuditEventsInsert(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 1 { + t.Error("want one record, got:", count) + } +} + +func testAuditEventsInsertWhitelist(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Whitelist(auditEventColumnsWithoutDefault...)); err != nil { + t.Error(err) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 1 { + t.Error("want one record, got:", count) + } +} + +func testAuditEventsReload(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + if err = o.Reload(ctx, tx); err != nil { + t.Error(err) + } +} + +func testAuditEventsReloadAll(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + slice := AuditEventSlice{o} + + if err = slice.ReloadAll(ctx, tx); err != nil { + t.Error(err) + } +} + +func testAuditEventsSelect(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + slice, err := AuditEvents().All(ctx, tx) + if err != nil { + t.Error(err) + } + + if len(slice) != 1 { + t.Error("want one record, got:", len(slice)) + } +} + +var ( + auditEventDBTypes = map[string]string{`ID`: `bigint`, `Type`: `character varying`, `Identifier`: `character varying`, `Message`: `text`, `CreatedAt`: `timestamp with time zone`} + _ = bytes.MinRead +) + +func testAuditEventsUpdate(t *testing.T) { + t.Parallel() + + if 0 == len(auditEventPrimaryKeyColumns) { + t.Skip("Skipping table with no primary key columns") + } + if len(auditEventAllColumns) == len(auditEventPrimaryKeyColumns) { + t.Skip("Skipping table with only primary key columns") + } + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 1 { + t.Error("want one record, got:", count) + } + + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventPrimaryKeyColumns...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + if rowsAff, err := o.Update(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } else if rowsAff != 1 { + t.Error("should only affect one row but affected", rowsAff) + } +} + +func testAuditEventsSliceUpdateAll(t *testing.T) { + t.Parallel() + + if len(auditEventAllColumns) == len(auditEventPrimaryKeyColumns) { + t.Skip("Skipping table with only primary key columns") + } + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 1 { + t.Error("want one record, got:", count) + } + + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventPrimaryKeyColumns...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + // Remove Primary keys and unique columns from what we plan to update + var fields []string + if strmangle.StringSliceMatch(auditEventAllColumns, auditEventPrimaryKeyColumns) { + fields = auditEventAllColumns + } else { + fields = strmangle.SetComplement( + auditEventAllColumns, + auditEventPrimaryKeyColumns, + ) + } + + value := reflect.Indirect(reflect.ValueOf(o)) + typ := reflect.TypeOf(o).Elem() + n := typ.NumField() + + updateMap := M{} + for _, col := range fields { + for i := 0; i < n; i++ { + f := typ.Field(i) + if f.Tag.Get("boil") == col { + updateMap[col] = value.Field(i).Interface() + } + } + } + + slice := AuditEventSlice{o} + if rowsAff, err := slice.UpdateAll(ctx, tx, updateMap); err != nil { + t.Error(err) + } else if rowsAff != 1 { + t.Error("wanted one record updated but got", rowsAff) + } +} + +func testAuditEventsUpsert(t *testing.T) { + t.Parallel() + + if len(auditEventAllColumns) == len(auditEventPrimaryKeyColumns) { + t.Skip("Skipping table with only primary key columns") + } + + seed := randomize.NewSeed() + var err error + // Attempt the INSERT side of an UPSERT + o := AuditEvent{} + if err = randomize.Struct(seed, &o, auditEventDBTypes, true); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Upsert(ctx, tx, false, nil, boil.Infer(), boil.Infer()); err != nil { + t.Errorf("Unable to upsert AuditEvent: %s", err) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + if count != 1 { + t.Error("want one record, got:", count) + } + + // Attempt the UPDATE side of an UPSERT + if err = randomize.Struct(seed, &o, auditEventDBTypes, false, auditEventPrimaryKeyColumns...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + if err = o.Upsert(ctx, tx, true, nil, boil.Infer(), boil.Infer()); err != nil { + t.Errorf("Unable to upsert AuditEvent: %s", err) + } + + count, err = AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + if count != 1 { + t.Error("want one record, got:", count) + } +} diff --git a/database/models/postgres/boil_main_test.go b/database/models/postgres/boil_main_test.go new file mode 100644 index 00000000..e1bbd228 --- /dev/null +++ b/database/models/postgres/boil_main_test.go @@ -0,0 +1,119 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package postgres + +import ( + "database/sql" + "flag" + "fmt" + "math/rand" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/spf13/viper" + "github.com/thrasher-corp/sqlboiler/boil" +) + +var flagDebugMode = flag.Bool("test.sqldebug", false, "Turns on debug mode for SQL statements") +var flagConfigFile = flag.String("test.config", "", "Overrides the default config") + +const outputDirDepth = 3 + +var ( + dbMain tester +) + +type tester interface { + setup() error + conn() (*sql.DB, error) + teardown() error +} + +func TestMain(m *testing.M) { + if dbMain == nil { + fmt.Println("no dbMain tester interface was ready") + os.Exit(-1) + } + + rand.Seed(time.Now().UnixNano()) + + flag.Parse() + + var err error + + // Load configuration + err = initViper() + if err != nil { + fmt.Println("unable to load config file") + os.Exit(-2) + } + + // Set DebugMode so we can see generated sql statements + boil.DebugMode = *flagDebugMode + + if err = dbMain.setup(); err != nil { + fmt.Println("Unable to execute setup:", err) + os.Exit(-4) + } + + conn, err := dbMain.conn() + if err != nil { + fmt.Println("failed to get connection:", err) + } + + var code int + boil.SetDB(conn) + code = m.Run() + + if err = dbMain.teardown(); err != nil { + fmt.Println("Unable to execute teardown:", err) + os.Exit(-5) + } + + os.Exit(code) +} + +func initViper() error { + if flagConfigFile != nil && *flagConfigFile != "" { + viper.SetConfigFile(*flagConfigFile) + if err := viper.ReadInConfig(); err != nil { + return err + } + return nil + } + + var err error + + viper.SetConfigName("sqlboiler") + + configHome := os.Getenv("XDG_CONFIG_HOME") + homePath := os.Getenv("HOME") + wd, err := os.Getwd() + if err != nil { + wd = strings.Repeat("../", outputDirDepth) + } else { + wd = wd + strings.Repeat("/..", outputDirDepth) + } + + configPaths := []string{wd} + if len(configHome) > 0 { + configPaths = append(configPaths, filepath.Join(configHome, "sqlboiler")) + } else { + configPaths = append(configPaths, filepath.Join(homePath, ".config/sqlboiler")) + } + + for _, p := range configPaths { + viper.AddConfigPath(p) + } + + // Ignore errors here, fall back to defaults and validation to provide errs + _ = viper.ReadInConfig() + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() + + return nil +} diff --git a/database/models/postgres/boil_queries.go b/database/models/postgres/boil_queries.go new file mode 100644 index 00000000..00fb6f9b --- /dev/null +++ b/database/models/postgres/boil_queries.go @@ -0,0 +1,33 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package postgres + +import ( + "github.com/thrasher-corp/sqlboiler/drivers" + "github.com/thrasher-corp/sqlboiler/queries" + "github.com/thrasher-corp/sqlboiler/queries/qm" +) + +var dialect = drivers.Dialect{ + LQ: 0x22, + RQ: 0x22, + + UseIndexPlaceholders: true, + UseLastInsertID: false, + UseSchema: false, + UseDefaultKeyword: true, + UseAutoColumns: false, + UseTopClause: false, + UseOutputClause: false, + UseCaseWhenExistsClause: false, +} + +// NewQuery initializes a new Query using the passed in QueryMods +func NewQuery(mods ...qm.QueryMod) *queries.Query { + q := &queries.Query{} + queries.SetDialect(q, &dialect) + qm.Apply(q, mods...) + + return q +} diff --git a/database/models/postgres/boil_queries_test.go b/database/models/postgres/boil_queries_test.go new file mode 100644 index 00000000..1986576b --- /dev/null +++ b/database/models/postgres/boil_queries_test.go @@ -0,0 +1,52 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package postgres + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "math/rand" + "regexp" + + "github.com/thrasher-corp/sqlboiler/boil" +) + +var dbNameRand *rand.Rand + +func MustTx(transactor boil.ContextTransactor, err error) boil.ContextTransactor { + if err != nil { + panic(fmt.Sprintf("Cannot create a transactor: %s", err)) + } + return transactor +} + +func newFKeyDestroyer(regex *regexp.Regexp, reader io.Reader) io.Reader { + return &fKeyDestroyer{ + reader: reader, + rgx: regex, + } +} + +type fKeyDestroyer struct { + reader io.Reader + buf *bytes.Buffer + rgx *regexp.Regexp +} + +func (f *fKeyDestroyer) Read(b []byte) (int, error) { + if f.buf == nil { + all, err := ioutil.ReadAll(f.reader) + if err != nil { + return 0, err + } + + all = bytes.Replace(all, []byte{'\r', '\n'}, []byte{'\n'}, -1) + all = f.rgx.ReplaceAll(all, []byte{}) + f.buf = bytes.NewBuffer(all) + } + + return f.buf.Read(b) +} diff --git a/database/models/postgres/boil_suites_test.go b/database/models/postgres/boil_suites_test.go new file mode 100644 index 00000000..d4e1c166 --- /dev/null +++ b/database/models/postgres/boil_suites_test.go @@ -0,0 +1,121 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package postgres + +import "testing" + +// This test suite runs each operation test in parallel. +// Example, if your database has 3 tables, the suite will run: +// table1, table2 and table3 Delete in parallel +// table1, table2 and table3 Insert in parallel, and so forth. +// It does NOT run each operation group in parallel. +// Separating the tests thusly grants avoidance of Postgres deadlocks. +func TestParent(t *testing.T) { + t.Run("AuditEvents", testAuditEvents) +} + +func TestDelete(t *testing.T) { + t.Run("AuditEvents", testAuditEventsDelete) +} + +func TestQueryDeleteAll(t *testing.T) { + t.Run("AuditEvents", testAuditEventsQueryDeleteAll) +} + +func TestSliceDeleteAll(t *testing.T) { + t.Run("AuditEvents", testAuditEventsSliceDeleteAll) +} + +func TestExists(t *testing.T) { + t.Run("AuditEvents", testAuditEventsExists) +} + +func TestFind(t *testing.T) { + t.Run("AuditEvents", testAuditEventsFind) +} + +func TestBind(t *testing.T) { + t.Run("AuditEvents", testAuditEventsBind) +} + +func TestOne(t *testing.T) { + t.Run("AuditEvents", testAuditEventsOne) +} + +func TestAll(t *testing.T) { + t.Run("AuditEvents", testAuditEventsAll) +} + +func TestCount(t *testing.T) { + t.Run("AuditEvents", testAuditEventsCount) +} + +func TestHooks(t *testing.T) { + t.Run("AuditEvents", testAuditEventsHooks) +} + +func TestInsert(t *testing.T) { + t.Run("AuditEvents", testAuditEventsInsert) + t.Run("AuditEvents", testAuditEventsInsertWhitelist) +} + +// TestToOne tests cannot be run in parallel +// or deadlocks can occur. +func TestToOne(t *testing.T) {} + +// TestOneToOne tests cannot be run in parallel +// or deadlocks can occur. +func TestOneToOne(t *testing.T) {} + +// TestToMany tests cannot be run in parallel +// or deadlocks can occur. +func TestToMany(t *testing.T) {} + +// TestToOneSet tests cannot be run in parallel +// or deadlocks can occur. +func TestToOneSet(t *testing.T) {} + +// TestToOneRemove tests cannot be run in parallel +// or deadlocks can occur. +func TestToOneRemove(t *testing.T) {} + +// TestOneToOneSet tests cannot be run in parallel +// or deadlocks can occur. +func TestOneToOneSet(t *testing.T) {} + +// TestOneToOneRemove tests cannot be run in parallel +// or deadlocks can occur. +func TestOneToOneRemove(t *testing.T) {} + +// TestToManyAdd tests cannot be run in parallel +// or deadlocks can occur. +func TestToManyAdd(t *testing.T) {} + +// TestToManySet tests cannot be run in parallel +// or deadlocks can occur. +func TestToManySet(t *testing.T) {} + +// TestToManyRemove tests cannot be run in parallel +// or deadlocks can occur. +func TestToManyRemove(t *testing.T) {} + +func TestReload(t *testing.T) { + t.Run("AuditEvents", testAuditEventsReload) +} + +func TestReloadAll(t *testing.T) { + t.Run("AuditEvents", testAuditEventsReloadAll) +} + +func TestSelect(t *testing.T) { + t.Run("AuditEvents", testAuditEventsSelect) +} + +func TestUpdate(t *testing.T) { + t.Run("AuditEvents", testAuditEventsUpdate) +} + +func TestSliceUpdateAll(t *testing.T) { + t.Run("AuditEvents", testAuditEventsSliceUpdateAll) +} diff --git a/database/models/postgres/boil_table_names.go b/database/models/postgres/boil_table_names.go new file mode 100644 index 00000000..c506138f --- /dev/null +++ b/database/models/postgres/boil_table_names.go @@ -0,0 +1,10 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package postgres + +var TableNames = struct { + AuditEvent string +}{ + AuditEvent: "audit_event", +} diff --git a/database/models/postgres/boil_types.go b/database/models/postgres/boil_types.go new file mode 100644 index 00000000..7ca491aa --- /dev/null +++ b/database/models/postgres/boil_types.go @@ -0,0 +1,52 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package postgres + +import ( + "strconv" + + "github.com/pkg/errors" + "github.com/thrasher-corp/sqlboiler/boil" + "github.com/thrasher-corp/sqlboiler/strmangle" +) + +// M type is for providing columns and column values to UpdateAll. +type M map[string]interface{} + +// ErrSyncFail occurs during insert when the record could not be retrieved in +// order to populate default value information. This usually happens when LastInsertId +// fails or there was a primary key configuration that was not resolvable. +var ErrSyncFail = errors.New("postgres: failed to synchronize data after insert") + +type insertCache struct { + query string + retQuery string + valueMapping []uint64 + retMapping []uint64 +} + +type updateCache struct { + query string + valueMapping []uint64 +} + +func makeCacheKey(cols boil.Columns, nzDefaults []string) string { + buf := strmangle.GetBuffer() + + buf.WriteString(strconv.Itoa(cols.Kind)) + for _, w := range cols.Cols { + buf.WriteString(w) + } + + if len(nzDefaults) != 0 { + buf.WriteByte('.') + } + for _, nz := range nzDefaults { + buf.WriteString(nz) + } + + str := buf.String() + strmangle.PutBuffer(buf) + return str +} diff --git a/database/models/postgres/psql_main_test.go b/database/models/postgres/psql_main_test.go new file mode 100644 index 00000000..5149dc31 --- /dev/null +++ b/database/models/postgres/psql_main_test.go @@ -0,0 +1,243 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package postgres + +import ( + "bytes" + "database/sql" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/kat-co/vala" + _ "github.com/lib/pq" + "github.com/pkg/errors" + "github.com/spf13/viper" + "github.com/thrasher-corp/goose" + "github.com/thrasher-corp/sqlboiler/drivers/sqlboiler-psql/driver" + "github.com/thrasher-corp/sqlboiler/randomize" +) + +var rgxPGFkey = regexp.MustCompile(`(?m)^ALTER TABLE ONLY .*\n\s+ADD CONSTRAINT .*? FOREIGN KEY .*?;\n`) + +type pgTester struct { + dbConn *sql.DB + + dbName string + host string + user string + pass string + sslmode string + port int + + pgPassFile string + + testDBName string + skipSQLCmd bool +} + +func init() { + dbMain = &pgTester{} +} + +// setup dumps the database schema and imports it into a temporary randomly +// generated test database so that tests can be run against it using the +// generated sqlboiler ORM package. +func (p *pgTester) setup() error { + var err error + + viper.SetDefault("psql.schema", "public") + viper.SetDefault("psql.port", 5432) + viper.SetDefault("psql.sslmode", "require") + + p.dbName = viper.GetString("psql.dbname") + p.host = viper.GetString("psql.host") + p.user = viper.GetString("psql.user") + p.pass = viper.GetString("psql.pass") + p.port = viper.GetInt("psql.port") + p.sslmode = viper.GetString("psql.sslmode") + p.testDBName = viper.GetString("psql.testdbname") + p.skipSQLCmd = viper.GetBool("psql.skipsqlcmd") + + err = vala.BeginValidation().Validate( + vala.StringNotEmpty(p.user, "psql.user"), + vala.StringNotEmpty(p.host, "psql.host"), + vala.Not(vala.Equals(p.port, 0, "psql.port")), + vala.StringNotEmpty(p.dbName, "psql.dbname"), + vala.StringNotEmpty(p.sslmode, "psql.sslmode"), + ).Check() + + if err != nil { + return err + } + + // if no testing DB passed + if len(p.testDBName) == 0 { + // Create a randomized db name. + p.testDBName = randomize.StableDBName(p.dbName) + } + + if err = p.makePGPassFile(); err != nil { + return err + } + + if !p.skipSQLCmd { + if err = p.dropTestDB(); err != nil { + return err + } + if err = p.createTestDB(); err != nil { + return err + } + + dumpCmd := exec.Command("pg_dump", "--schema-only", p.dbName) + dumpCmd.Env = append(os.Environ(), p.pgEnv()...) + createCmd := exec.Command("psql", p.testDBName) + createCmd.Env = append(os.Environ(), p.pgEnv()...) + + r, w := io.Pipe() + dumpCmdStderr := &bytes.Buffer{} + createCmdStderr := &bytes.Buffer{} + + dumpCmd.Stdout = w + dumpCmd.Stderr = dumpCmdStderr + + createCmd.Stdin = newFKeyDestroyer(rgxPGFkey, r) + createCmd.Stderr = createCmdStderr + + if err = dumpCmd.Start(); err != nil { + return errors.Wrap(err, "failed to start pg_dump command") + } + if err = createCmd.Start(); err != nil { + return errors.Wrap(err, "failed to start psql command") + } + + if err = dumpCmd.Wait(); err != nil { + fmt.Println(err) + fmt.Println(dumpCmdStderr.String()) + return errors.Wrap(err, "failed to wait for pg_dump command") + } + + _ = w.Close() // After dumpCmd is done, close the write end of the pipe + + if err = createCmd.Wait(); err != nil { + fmt.Println(err) + fmt.Println(createCmdStderr.String()) + return errors.Wrap(err, "failed to wait for psql command") + } + } + + return nil +} + +func (p *pgTester) runCmd(stdin, command string, args ...string) error { + cmd := exec.Command(command, args...) + cmd.Env = append(os.Environ(), p.pgEnv()...) + + if len(stdin) != 0 { + cmd.Stdin = strings.NewReader(stdin) + } + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + cmd.Stdout = stdout + cmd.Stderr = stderr + if err := cmd.Run(); err != nil { + fmt.Println("failed running:", command, args) + fmt.Println(stdout.String()) + fmt.Println(stderr.String()) + return err + } + + return nil +} + +func (p *pgTester) pgEnv() []string { + return []string{ + fmt.Sprintf("PGHOST=%s", p.host), + fmt.Sprintf("PGPORT=%d", p.port), + fmt.Sprintf("PGUSER=%s", p.user), + fmt.Sprintf("PGPASSFILE=%s", p.pgPassFile), + } +} + +func (p *pgTester) makePGPassFile() error { + tmp, err := ioutil.TempFile("", "pgpass") + if err != nil { + return errors.Wrap(err, "failed to create option file") + } + + fmt.Fprintf(tmp, "%s:%d:postgres:%s", p.host, p.port, p.user) + if len(p.pass) != 0 { + fmt.Fprintf(tmp, ":%s", p.pass) + } + fmt.Fprintln(tmp) + + fmt.Fprintf(tmp, "%s:%d:%s:%s", p.host, p.port, p.dbName, p.user) + if len(p.pass) != 0 { + fmt.Fprintf(tmp, ":%s", p.pass) + } + fmt.Fprintln(tmp) + + fmt.Fprintf(tmp, "%s:%d:%s:%s", p.host, p.port, p.testDBName, p.user) + if len(p.pass) != 0 { + fmt.Fprintf(tmp, ":%s", p.pass) + } + fmt.Fprintln(tmp) + + p.pgPassFile = tmp.Name() + return tmp.Close() +} + +func (p *pgTester) createTestDB() error { + return p.runCmd("", "createdb", p.testDBName) +} + +func (p *pgTester) dropTestDB() error { + return p.runCmd("", "dropdb", "--if-exists", p.testDBName) +} + +// teardown executes cleanup tasks when the tests finish running +func (p *pgTester) teardown() error { + var err error + if err = p.dbConn.Close(); err != nil { + return err + } + p.dbConn = nil + + if !p.skipSQLCmd { + if err = p.dropTestDB(); err != nil { + return err + } + } + + return os.Remove(p.pgPassFile) +} + +func (p *pgTester) conn() (*sql.DB, error) { + if p.dbConn != nil { + return p.dbConn, nil + } + + var err error + p.dbConn, err = sql.Open("postgres", driver.PSQLBuildQueryString(p.user, p.pass, p.testDBName, p.host, p.port, p.sslmode)) + if err != nil { + return nil, err + } + + path := filepath.Join("..", "..", "migrations") + err = goose.Run("up", p.dbConn, "postgres", path, "") + if err != nil { + if err == goose.ErrNoNextVersion { + return p.dbConn, nil + } + return nil, err + } + + return p.dbConn, nil +} diff --git a/database/models/postgres/psql_suites_test.go b/database/models/postgres/psql_suites_test.go new file mode 100644 index 00000000..0dac7a27 --- /dev/null +++ b/database/models/postgres/psql_suites_test.go @@ -0,0 +1,10 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package postgres + +import "testing" + +func TestUpsert(t *testing.T) { + t.Run("AuditEvents", testAuditEventsUpsert) +} diff --git a/database/models/postgres/psql_upsert.go b/database/models/postgres/psql_upsert.go new file mode 100644 index 00000000..2d362318 --- /dev/null +++ b/database/models/postgres/psql_upsert.go @@ -0,0 +1,61 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package postgres + +import ( + "fmt" + "strings" + + "github.com/thrasher-corp/sqlboiler/drivers" + "github.com/thrasher-corp/sqlboiler/strmangle" +) + +// buildUpsertQueryPostgres builds a SQL statement string using the upsertData provided. +func buildUpsertQueryPostgres(dia drivers.Dialect, tableName string, updateOnConflict bool, ret, update, conflict, whitelist []string) string { + conflict = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, conflict) + whitelist = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, whitelist) + ret = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, ret) + + buf := strmangle.GetBuffer() + defer strmangle.PutBuffer(buf) + + columns := "DEFAULT VALUES" + if len(whitelist) != 0 { + columns = fmt.Sprintf("(%s) VALUES (%s)", + strings.Join(whitelist, ", "), + strmangle.Placeholders(dia.UseIndexPlaceholders, len(whitelist), 1, 1)) + } + + fmt.Fprintf( + buf, + "INSERT INTO %s %s ON CONFLICT ", + tableName, + columns, + ) + + if !updateOnConflict || len(update) == 0 { + buf.WriteString("DO NOTHING") + } else { + buf.WriteByte('(') + buf.WriteString(strings.Join(conflict, ", ")) + buf.WriteString(") DO UPDATE SET ") + + for i, v := range update { + if i != 0 { + buf.WriteByte(',') + } + quoted := strmangle.IdentQuote(dia.LQ, dia.RQ, v) + buf.WriteString(quoted) + buf.WriteString(" = EXCLUDED.") + buf.WriteString(quoted) + } + } + + if len(ret) != 0 { + buf.WriteString(" RETURNING ") + buf.WriteString(strings.Join(ret, ", ")) + } + + return buf.String() +} diff --git a/database/models/sqlite3/audit_event.go b/database/models/sqlite3/audit_event.go new file mode 100644 index 00000000..428c7cc6 --- /dev/null +++ b/database/models/sqlite3/audit_event.go @@ -0,0 +1,816 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sqlite3 + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "strings" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/thrasher-corp/sqlboiler/boil" + "github.com/thrasher-corp/sqlboiler/queries" + "github.com/thrasher-corp/sqlboiler/queries/qm" + "github.com/thrasher-corp/sqlboiler/queries/qmhelper" + "github.com/thrasher-corp/sqlboiler/strmangle" +) + +// AuditEvent is an object representing the database table. +type AuditEvent struct { + ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"` + Type string `boil:"type" json:"type" toml:"type" yaml:"type"` + Identifier string `boil:"identifier" json:"identifier" toml:"identifier" yaml:"identifier"` + Message string `boil:"message" json:"message" toml:"message" yaml:"message"` + CreatedAt string `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"` + + R *auditEventR `boil:"-" json:"-" toml:"-" yaml:"-"` + L auditEventL `boil:"-" json:"-" toml:"-" yaml:"-"` +} + +var AuditEventColumns = struct { + ID string + Type string + Identifier string + Message string + CreatedAt string +}{ + ID: "id", + Type: "type", + Identifier: "identifier", + Message: "message", + CreatedAt: "created_at", +} + +// Generated where + +type whereHelperint64 struct{ field string } + +func (w whereHelperint64) EQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } +func (w whereHelperint64) NEQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) } +func (w whereHelperint64) LT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) } +func (w whereHelperint64) LTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) } +func (w whereHelperint64) GT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) } +func (w whereHelperint64) GTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } + +type whereHelperstring struct{ field string } + +func (w whereHelperstring) EQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } +func (w whereHelperstring) NEQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) } +func (w whereHelperstring) LT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) } +func (w whereHelperstring) LTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) } +func (w whereHelperstring) GT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) } +func (w whereHelperstring) GTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } +func (w whereHelperstring) IN(slice []string) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) +} + +var AuditEventWhere = struct { + ID whereHelperint64 + Type whereHelperstring + Identifier whereHelperstring + Message whereHelperstring + CreatedAt whereHelperstring +}{ + ID: whereHelperint64{field: "\"audit_event\".\"id\""}, + Type: whereHelperstring{field: "\"audit_event\".\"type\""}, + Identifier: whereHelperstring{field: "\"audit_event\".\"identifier\""}, + Message: whereHelperstring{field: "\"audit_event\".\"message\""}, + CreatedAt: whereHelperstring{field: "\"audit_event\".\"created_at\""}, +} + +// AuditEventRels is where relationship names are stored. +var AuditEventRels = struct { +}{} + +// auditEventR is where relationships are stored. +type auditEventR struct { +} + +// NewStruct creates a new relationship struct +func (*auditEventR) NewStruct() *auditEventR { + return &auditEventR{} +} + +// auditEventL is where Load methods for each relationship are stored. +type auditEventL struct{} + +var ( + auditEventAllColumns = []string{"id", "type", "identifier", "message", "created_at"} + auditEventColumnsWithoutDefault = []string{"type", "identifier", "message"} + auditEventColumnsWithDefault = []string{"id", "created_at"} + auditEventPrimaryKeyColumns = []string{"id"} +) + +type ( + // AuditEventSlice is an alias for a slice of pointers to AuditEvent. + // This should generally be used opposed to []AuditEvent. + AuditEventSlice []*AuditEvent + // AuditEventHook is the signature for custom AuditEvent hook methods + AuditEventHook func(context.Context, boil.ContextExecutor, *AuditEvent) error + + auditEventQuery struct { + *queries.Query + } +) + +// Cache for insert, update and upsert +var ( + auditEventType = reflect.TypeOf(&AuditEvent{}) + auditEventMapping = queries.MakeStructMapping(auditEventType) + auditEventPrimaryKeyMapping, _ = queries.BindMapping(auditEventType, auditEventMapping, auditEventPrimaryKeyColumns) + auditEventInsertCacheMut sync.RWMutex + auditEventInsertCache = make(map[string]insertCache) + auditEventUpdateCacheMut sync.RWMutex + auditEventUpdateCache = make(map[string]updateCache) + auditEventUpsertCacheMut sync.RWMutex + auditEventUpsertCache = make(map[string]insertCache) +) + +var ( + // Force time package dependency for automated UpdatedAt/CreatedAt. + _ = time.Second + // Force qmhelper dependency for where clause generation (which doesn't + // always happen) + _ = qmhelper.Where +) + +var auditEventBeforeInsertHooks []AuditEventHook +var auditEventBeforeUpdateHooks []AuditEventHook +var auditEventBeforeDeleteHooks []AuditEventHook +var auditEventBeforeUpsertHooks []AuditEventHook + +var auditEventAfterInsertHooks []AuditEventHook +var auditEventAfterSelectHooks []AuditEventHook +var auditEventAfterUpdateHooks []AuditEventHook +var auditEventAfterDeleteHooks []AuditEventHook +var auditEventAfterUpsertHooks []AuditEventHook + +// doBeforeInsertHooks executes all "before insert" hooks. +func (o *AuditEvent) doBeforeInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventBeforeInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpdateHooks executes all "before Update" hooks. +func (o *AuditEvent) doBeforeUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventBeforeUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeDeleteHooks executes all "before Delete" hooks. +func (o *AuditEvent) doBeforeDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventBeforeDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpsertHooks executes all "before Upsert" hooks. +func (o *AuditEvent) doBeforeUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventBeforeUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterInsertHooks executes all "after Insert" hooks. +func (o *AuditEvent) doAfterInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventAfterInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterSelectHooks executes all "after Select" hooks. +func (o *AuditEvent) doAfterSelectHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventAfterSelectHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpdateHooks executes all "after Update" hooks. +func (o *AuditEvent) doAfterUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventAfterUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterDeleteHooks executes all "after Delete" hooks. +func (o *AuditEvent) doAfterDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventAfterDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpsertHooks executes all "after Upsert" hooks. +func (o *AuditEvent) doAfterUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range auditEventAfterUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// AddAuditEventHook registers your hook function for all future operations. +func AddAuditEventHook(hookPoint boil.HookPoint, auditEventHook AuditEventHook) { + switch hookPoint { + case boil.BeforeInsertHook: + auditEventBeforeInsertHooks = append(auditEventBeforeInsertHooks, auditEventHook) + case boil.BeforeUpdateHook: + auditEventBeforeUpdateHooks = append(auditEventBeforeUpdateHooks, auditEventHook) + case boil.BeforeDeleteHook: + auditEventBeforeDeleteHooks = append(auditEventBeforeDeleteHooks, auditEventHook) + case boil.BeforeUpsertHook: + auditEventBeforeUpsertHooks = append(auditEventBeforeUpsertHooks, auditEventHook) + case boil.AfterInsertHook: + auditEventAfterInsertHooks = append(auditEventAfterInsertHooks, auditEventHook) + case boil.AfterSelectHook: + auditEventAfterSelectHooks = append(auditEventAfterSelectHooks, auditEventHook) + case boil.AfterUpdateHook: + auditEventAfterUpdateHooks = append(auditEventAfterUpdateHooks, auditEventHook) + case boil.AfterDeleteHook: + auditEventAfterDeleteHooks = append(auditEventAfterDeleteHooks, auditEventHook) + case boil.AfterUpsertHook: + auditEventAfterUpsertHooks = append(auditEventAfterUpsertHooks, auditEventHook) + } +} + +// One returns a single auditEvent record from the query. +func (q auditEventQuery) One(ctx context.Context, exec boil.ContextExecutor) (*AuditEvent, error) { + o := &AuditEvent{} + + queries.SetLimit(q.Query, 1) + + err := q.Bind(ctx, exec, o) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "sqlite3: failed to execute a one query for audit_event") + } + + if err := o.doAfterSelectHooks(ctx, exec); err != nil { + return o, err + } + + return o, nil +} + +// All returns all AuditEvent records from the query. +func (q auditEventQuery) All(ctx context.Context, exec boil.ContextExecutor) (AuditEventSlice, error) { + var o []*AuditEvent + + err := q.Bind(ctx, exec, &o) + if err != nil { + return nil, errors.Wrap(err, "sqlite3: failed to assign all query results to AuditEvent slice") + } + + if len(auditEventAfterSelectHooks) != 0 { + for _, obj := range o { + if err := obj.doAfterSelectHooks(ctx, exec); err != nil { + return o, err + } + } + } + + return o, nil +} + +// Count returns the count of all AuditEvent records in the query. +func (q auditEventQuery) Count(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return 0, errors.Wrap(err, "sqlite3: failed to count audit_event rows") + } + + return count, nil +} + +// Exists checks if the row exists in the table. +func (q auditEventQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + queries.SetLimit(q.Query, 1) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return false, errors.Wrap(err, "sqlite3: failed to check if audit_event exists") + } + + return count > 0, nil +} + +// AuditEvents retrieves all the records using an executor. +func AuditEvents(mods ...qm.QueryMod) auditEventQuery { + mods = append(mods, qm.From("\"audit_event\"")) + return auditEventQuery{NewQuery(mods...)} +} + +// FindAuditEvent retrieves a single record by ID with an executor. +// If selectCols is empty Find will return all columns. +func FindAuditEvent(ctx context.Context, exec boil.ContextExecutor, iD int64, selectCols ...string) (*AuditEvent, error) { + auditEventObj := &AuditEvent{} + + sel := "*" + if len(selectCols) > 0 { + sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",") + } + query := fmt.Sprintf( + "select %s from \"audit_event\" where \"id\"=?", sel, + ) + + q := queries.Raw(query, iD) + + err := q.Bind(ctx, exec, auditEventObj) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "sqlite3: unable to select from audit_event") + } + + return auditEventObj, nil +} + +// Insert a single record using an executor. +// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts. +func (o *AuditEvent) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error { + if o == nil { + return errors.New("sqlite3: no audit_event provided for insertion") + } + + var err error + + if err := o.doBeforeInsertHooks(ctx, exec); err != nil { + return err + } + + nzDefaults := queries.NonZeroDefaultSet(auditEventColumnsWithDefault, o) + + key := makeCacheKey(columns, nzDefaults) + auditEventInsertCacheMut.RLock() + cache, cached := auditEventInsertCache[key] + auditEventInsertCacheMut.RUnlock() + + if !cached { + wl, returnColumns := columns.InsertColumnSet( + auditEventAllColumns, + auditEventColumnsWithDefault, + auditEventColumnsWithoutDefault, + nzDefaults, + ) + + cache.valueMapping, err = queries.BindMapping(auditEventType, auditEventMapping, wl) + if err != nil { + return err + } + cache.retMapping, err = queries.BindMapping(auditEventType, auditEventMapping, returnColumns) + if err != nil { + return err + } + if len(wl) != 0 { + cache.query = fmt.Sprintf("INSERT INTO \"audit_event\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1)) + } else { + cache.query = "INSERT INTO \"audit_event\" () VALUES ()%s%s" + } + + var queryOutput, queryReturning string + + if len(cache.retMapping) != 0 { + cache.retQuery = fmt.Sprintf("SELECT \"%s\" FROM \"audit_event\" WHERE %s", strings.Join(returnColumns, "\",\""), strmangle.WhereClause("\"", "\"", 0, auditEventPrimaryKeyColumns)) + } + + cache.query = fmt.Sprintf(cache.query, queryOutput, queryReturning) + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, cache.query) + fmt.Fprintln(boil.DebugWriter, vals) + } + + result, err := exec.ExecContext(ctx, cache.query, vals...) + + if err != nil { + return errors.Wrap(err, "sqlite3: unable to insert into audit_event") + } + + var lastID int64 + var identifierCols []interface{} + + if len(cache.retMapping) == 0 { + goto CacheNoHooks + } + + lastID, err = result.LastInsertId() + if err != nil { + return ErrSyncFail + } + + o.ID = int64(lastID) + if lastID != 0 && len(cache.retMapping) == 1 && cache.retMapping[0] == auditEventMapping["ID"] { + goto CacheNoHooks + } + + identifierCols = []interface{}{ + o.ID, + } + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, cache.retQuery) + fmt.Fprintln(boil.DebugWriter, identifierCols...) + } + + err = exec.QueryRowContext(ctx, cache.retQuery, identifierCols...).Scan(queries.PtrsFromMapping(value, cache.retMapping)...) + if err != nil { + return errors.Wrap(err, "sqlite3: unable to populate default values for audit_event") + } + +CacheNoHooks: + if !cached { + auditEventInsertCacheMut.Lock() + auditEventInsertCache[key] = cache + auditEventInsertCacheMut.Unlock() + } + + return o.doAfterInsertHooks(ctx, exec) +} + +// Update uses an executor to update the AuditEvent. +// See boil.Columns.UpdateColumnSet documentation to understand column list inference for updates. +// Update does not automatically update the record in case of default values. Use .Reload() to refresh the records. +func (o *AuditEvent) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) { + var err error + if err = o.doBeforeUpdateHooks(ctx, exec); err != nil { + return 0, err + } + key := makeCacheKey(columns, nil) + auditEventUpdateCacheMut.RLock() + cache, cached := auditEventUpdateCache[key] + auditEventUpdateCacheMut.RUnlock() + + if !cached { + wl := columns.UpdateColumnSet( + auditEventAllColumns, + auditEventPrimaryKeyColumns, + ) + + if len(wl) == 0 { + return 0, errors.New("sqlite3: unable to update audit_event, could not build whitelist") + } + + cache.query = fmt.Sprintf("UPDATE \"audit_event\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, wl), + strmangle.WhereClause("\"", "\"", 0, auditEventPrimaryKeyColumns), + ) + cache.valueMapping, err = queries.BindMapping(auditEventType, auditEventMapping, append(wl, auditEventPrimaryKeyColumns...)) + if err != nil { + return 0, err + } + } + + values := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), cache.valueMapping) + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, cache.query) + fmt.Fprintln(boil.DebugWriter, values) + } + + var result sql.Result + result, err = exec.ExecContext(ctx, cache.query, values...) + if err != nil { + return 0, errors.Wrap(err, "sqlite3: unable to update audit_event row") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "sqlite3: failed to get rows affected by update for audit_event") + } + + if !cached { + auditEventUpdateCacheMut.Lock() + auditEventUpdateCache[key] = cache + auditEventUpdateCacheMut.Unlock() + } + + return rowsAff, o.doAfterUpdateHooks(ctx, exec) +} + +// UpdateAll updates all rows with the specified column values. +func (q auditEventQuery) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + queries.SetUpdate(q.Query, cols) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "sqlite3: unable to update all for audit_event") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "sqlite3: unable to retrieve rows affected for audit_event") + } + + return rowsAff, nil +} + +// UpdateAll updates all rows with the specified column values, using an executor. +func (o AuditEventSlice) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + ln := int64(len(o)) + if ln == 0 { + return 0, nil + } + + if len(cols) == 0 { + return 0, errors.New("sqlite3: update all requires at least one column argument") + } + + colNames := make([]string, len(cols)) + args := make([]interface{}, len(cols)) + + i := 0 + for name, value := range cols { + colNames[i] = name + args[i] = value + i++ + } + + // Append all of the primary key values for each column + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), auditEventPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := fmt.Sprintf("UPDATE \"audit_event\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, colNames), + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, auditEventPrimaryKeyColumns, len(o))) + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, sql) + fmt.Fprintln(boil.DebugWriter, args...) + } + + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "sqlite3: unable to update all in auditEvent slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "sqlite3: unable to retrieve rows affected all in update all auditEvent") + } + return rowsAff, nil +} + +// Delete deletes a single AuditEvent record with an executor. +// Delete will match against the primary key column to find the record to delete. +func (o *AuditEvent) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if o == nil { + return 0, errors.New("sqlite3: no AuditEvent provided for delete") + } + + if err := o.doBeforeDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + args := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), auditEventPrimaryKeyMapping) + sql := "DELETE FROM \"audit_event\" WHERE \"id\"=?" + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, sql) + fmt.Fprintln(boil.DebugWriter, args...) + } + + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "sqlite3: unable to delete from audit_event") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "sqlite3: failed to get rows affected by delete for audit_event") + } + + if err := o.doAfterDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + return rowsAff, nil +} + +// DeleteAll deletes all matching rows. +func (q auditEventQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if q.Query == nil { + return 0, errors.New("sqlite3: no auditEventQuery provided for delete all") + } + + queries.SetDelete(q.Query) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "sqlite3: unable to delete all from audit_event") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "sqlite3: failed to get rows affected by deleteall for audit_event") + } + + return rowsAff, nil +} + +// DeleteAll deletes all rows in the slice, using an executor. +func (o AuditEventSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if len(o) == 0 { + return 0, nil + } + + if len(auditEventBeforeDeleteHooks) != 0 { + for _, obj := range o { + if err := obj.doBeforeDeleteHooks(ctx, exec); err != nil { + return 0, err + } + } + } + + var args []interface{} + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), auditEventPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "DELETE FROM \"audit_event\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, auditEventPrimaryKeyColumns, len(o)) + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, sql) + fmt.Fprintln(boil.DebugWriter, args) + } + + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "sqlite3: unable to delete all from auditEvent slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "sqlite3: failed to get rows affected by deleteall for audit_event") + } + + if len(auditEventAfterDeleteHooks) != 0 { + for _, obj := range o { + if err := obj.doAfterDeleteHooks(ctx, exec); err != nil { + return 0, err + } + } + } + + return rowsAff, nil +} + +// Reload refetches the object from the database +// using the primary keys with an executor. +func (o *AuditEvent) Reload(ctx context.Context, exec boil.ContextExecutor) error { + ret, err := FindAuditEvent(ctx, exec, o.ID) + if err != nil { + return err + } + + *o = *ret + return nil +} + +// ReloadAll refetches every row with matching primary key column values +// and overwrites the original object slice with the newly updated slice. +func (o *AuditEventSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error { + if o == nil || len(*o) == 0 { + return nil + } + + slice := AuditEventSlice{} + var args []interface{} + for _, obj := range *o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), auditEventPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "SELECT \"audit_event\".* FROM \"audit_event\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, auditEventPrimaryKeyColumns, len(*o)) + + q := queries.Raw(sql, args...) + + err := q.Bind(ctx, exec, &slice) + if err != nil { + return errors.Wrap(err, "sqlite3: unable to reload all in AuditEventSlice") + } + + *o = slice + + return nil +} + +// AuditEventExists checks if the AuditEvent row exists. +func AuditEventExists(ctx context.Context, exec boil.ContextExecutor, iD int64) (bool, error) { + var exists bool + sql := "select exists(select 1 from \"audit_event\" where \"id\"=? limit 1)" + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, sql) + fmt.Fprintln(boil.DebugWriter, iD) + } + + row := exec.QueryRowContext(ctx, sql, iD) + + err := row.Scan(&exists) + if err != nil { + return false, errors.Wrap(err, "sqlite3: unable to check if audit_event exists") + } + + return exists, nil +} diff --git a/database/models/sqlite3/audit_event_test.go b/database/models/sqlite3/audit_event_test.go new file mode 100644 index 00000000..25e1e473 --- /dev/null +++ b/database/models/sqlite3/audit_event_test.go @@ -0,0 +1,684 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sqlite3 + +import ( + "bytes" + "context" + "reflect" + "testing" + + "github.com/thrasher-corp/sqlboiler/boil" + "github.com/thrasher-corp/sqlboiler/queries" + "github.com/thrasher-corp/sqlboiler/randomize" + "github.com/thrasher-corp/sqlboiler/strmangle" +) + +var ( + // Relationships sometimes use the reflection helper queries.Equal/queries.Assign + // so force a package dependency in case they don't. + _ = queries.Equal +) + +func testAuditEvents(t *testing.T) { + t.Parallel() + + query := AuditEvents() + + if query.Query == nil { + t.Error("expected a query, got nothing") + } +} + +func testAuditEventsDelete(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + if rowsAff, err := o.Delete(ctx, tx); err != nil { + t.Error(err) + } else if rowsAff != 1 { + t.Error("should only have deleted one row, but affected:", rowsAff) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 0 { + t.Error("want zero records, got:", count) + } +} + +func testAuditEventsQueryDeleteAll(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + if rowsAff, err := AuditEvents().DeleteAll(ctx, tx); err != nil { + t.Error(err) + } else if rowsAff != 1 { + t.Error("should only have deleted one row, but affected:", rowsAff) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 0 { + t.Error("want zero records, got:", count) + } +} + +func testAuditEventsSliceDeleteAll(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + slice := AuditEventSlice{o} + + if rowsAff, err := slice.DeleteAll(ctx, tx); err != nil { + t.Error(err) + } else if rowsAff != 1 { + t.Error("should only have deleted one row, but affected:", rowsAff) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 0 { + t.Error("want zero records, got:", count) + } +} + +func testAuditEventsExists(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + e, err := AuditEventExists(ctx, tx, o.ID) + if err != nil { + t.Errorf("Unable to check if AuditEvent exists: %s", err) + } + if !e { + t.Errorf("Expected AuditEventExists to return true, but got false.") + } +} + +func testAuditEventsFind(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + auditEventFound, err := FindAuditEvent(ctx, tx, o.ID) + if err != nil { + t.Error(err) + } + + if auditEventFound == nil { + t.Error("want a record, got nil") + } +} + +func testAuditEventsBind(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + if err = AuditEvents().Bind(ctx, tx, o); err != nil { + t.Error(err) + } +} + +func testAuditEventsOne(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + if x, err := AuditEvents().One(ctx, tx); err != nil { + t.Error(err) + } else if x == nil { + t.Error("expected to get a non nil record") + } +} + +func testAuditEventsAll(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + auditEventOne := &AuditEvent{} + auditEventTwo := &AuditEvent{} + if err = randomize.Struct(seed, auditEventOne, auditEventDBTypes, false, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + if err = randomize.Struct(seed, auditEventTwo, auditEventDBTypes, false, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = auditEventOne.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + if err = auditEventTwo.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + slice, err := AuditEvents().All(ctx, tx) + if err != nil { + t.Error(err) + } + + if len(slice) != 2 { + t.Error("want 2 records, got:", len(slice)) + } +} + +func testAuditEventsCount(t *testing.T) { + t.Parallel() + + var err error + seed := randomize.NewSeed() + auditEventOne := &AuditEvent{} + auditEventTwo := &AuditEvent{} + if err = randomize.Struct(seed, auditEventOne, auditEventDBTypes, false, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + if err = randomize.Struct(seed, auditEventTwo, auditEventDBTypes, false, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = auditEventOne.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + if err = auditEventTwo.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 2 { + t.Error("want 2 records, got:", count) + } +} + +func auditEventBeforeInsertHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventAfterInsertHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventAfterSelectHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventBeforeUpdateHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventAfterUpdateHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventBeforeDeleteHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventAfterDeleteHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventBeforeUpsertHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func auditEventAfterUpsertHook(ctx context.Context, e boil.ContextExecutor, o *AuditEvent) error { + *o = AuditEvent{} + return nil +} + +func testAuditEventsHooks(t *testing.T) { + t.Parallel() + + var err error + + ctx := context.Background() + empty := &AuditEvent{} + o := &AuditEvent{} + + seed := randomize.NewSeed() + if err = randomize.Struct(seed, o, auditEventDBTypes, false); err != nil { + t.Errorf("Unable to randomize AuditEvent object: %s", err) + } + + AddAuditEventHook(boil.BeforeInsertHook, auditEventBeforeInsertHook) + if err = o.doBeforeInsertHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doBeforeInsertHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected BeforeInsertHook function to empty object, but got: %#v", o) + } + auditEventBeforeInsertHooks = []AuditEventHook{} + + AddAuditEventHook(boil.AfterInsertHook, auditEventAfterInsertHook) + if err = o.doAfterInsertHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doAfterInsertHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected AfterInsertHook function to empty object, but got: %#v", o) + } + auditEventAfterInsertHooks = []AuditEventHook{} + + AddAuditEventHook(boil.AfterSelectHook, auditEventAfterSelectHook) + if err = o.doAfterSelectHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doAfterSelectHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected AfterSelectHook function to empty object, but got: %#v", o) + } + auditEventAfterSelectHooks = []AuditEventHook{} + + AddAuditEventHook(boil.BeforeUpdateHook, auditEventBeforeUpdateHook) + if err = o.doBeforeUpdateHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doBeforeUpdateHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected BeforeUpdateHook function to empty object, but got: %#v", o) + } + auditEventBeforeUpdateHooks = []AuditEventHook{} + + AddAuditEventHook(boil.AfterUpdateHook, auditEventAfterUpdateHook) + if err = o.doAfterUpdateHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doAfterUpdateHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected AfterUpdateHook function to empty object, but got: %#v", o) + } + auditEventAfterUpdateHooks = []AuditEventHook{} + + AddAuditEventHook(boil.BeforeDeleteHook, auditEventBeforeDeleteHook) + if err = o.doBeforeDeleteHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doBeforeDeleteHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected BeforeDeleteHook function to empty object, but got: %#v", o) + } + auditEventBeforeDeleteHooks = []AuditEventHook{} + + AddAuditEventHook(boil.AfterDeleteHook, auditEventAfterDeleteHook) + if err = o.doAfterDeleteHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doAfterDeleteHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected AfterDeleteHook function to empty object, but got: %#v", o) + } + auditEventAfterDeleteHooks = []AuditEventHook{} + + AddAuditEventHook(boil.BeforeUpsertHook, auditEventBeforeUpsertHook) + if err = o.doBeforeUpsertHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doBeforeUpsertHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected BeforeUpsertHook function to empty object, but got: %#v", o) + } + auditEventBeforeUpsertHooks = []AuditEventHook{} + + AddAuditEventHook(boil.AfterUpsertHook, auditEventAfterUpsertHook) + if err = o.doAfterUpsertHooks(ctx, nil); err != nil { + t.Errorf("Unable to execute doAfterUpsertHooks: %s", err) + } + if !reflect.DeepEqual(o, empty) { + t.Errorf("Expected AfterUpsertHook function to empty object, but got: %#v", o) + } + auditEventAfterUpsertHooks = []AuditEventHook{} +} + +func testAuditEventsInsert(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 1 { + t.Error("want one record, got:", count) + } +} + +func testAuditEventsInsertWhitelist(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Whitelist(auditEventColumnsWithoutDefault...)); err != nil { + t.Error(err) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 1 { + t.Error("want one record, got:", count) + } +} + +func testAuditEventsReload(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + if err = o.Reload(ctx, tx); err != nil { + t.Error(err) + } +} + +func testAuditEventsReloadAll(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + slice := AuditEventSlice{o} + + if err = slice.ReloadAll(ctx, tx); err != nil { + t.Error(err) + } +} + +func testAuditEventsSelect(t *testing.T) { + t.Parallel() + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + slice, err := AuditEvents().All(ctx, tx) + if err != nil { + t.Error(err) + } + + if len(slice) != 1 { + t.Error("want one record, got:", len(slice)) + } +} + +var ( + auditEventDBTypes = map[string]string{`ID`: `INTEGER`, `Type`: `TEXT`, `Identifier`: `TEXT`, `Message`: `TEXT`, `CreatedAt`: `TIMESTAMP`} + _ = bytes.MinRead +) + +func testAuditEventsUpdate(t *testing.T) { + t.Parallel() + + if 0 == len(auditEventPrimaryKeyColumns) { + t.Skip("Skipping table with no primary key columns") + } + if len(auditEventAllColumns) == len(auditEventPrimaryKeyColumns) { + t.Skip("Skipping table with only primary key columns") + } + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 1 { + t.Error("want one record, got:", count) + } + + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventPrimaryKeyColumns...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + if rowsAff, err := o.Update(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } else if rowsAff != 1 { + t.Error("should only affect one row but affected", rowsAff) + } +} + +func testAuditEventsSliceUpdateAll(t *testing.T) { + t.Parallel() + + if len(auditEventAllColumns) == len(auditEventPrimaryKeyColumns) { + t.Skip("Skipping table with only primary key columns") + } + + seed := randomize.NewSeed() + var err error + o := &AuditEvent{} + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + ctx := context.Background() + tx := MustTx(boil.BeginTx(ctx, nil)) + defer func() { _ = tx.Rollback() }() + if err = o.Insert(ctx, tx, boil.Infer()); err != nil { + t.Error(err) + } + + count, err := AuditEvents().Count(ctx, tx) + if err != nil { + t.Error(err) + } + + if count != 1 { + t.Error("want one record, got:", count) + } + + if err = randomize.Struct(seed, o, auditEventDBTypes, true, auditEventPrimaryKeyColumns...); err != nil { + t.Errorf("Unable to randomize AuditEvent struct: %s", err) + } + + // Remove Primary keys and unique columns from what we plan to update + var fields []string + if strmangle.StringSliceMatch(auditEventAllColumns, auditEventPrimaryKeyColumns) { + fields = auditEventAllColumns + } else { + fields = strmangle.SetComplement( + auditEventAllColumns, + auditEventPrimaryKeyColumns, + ) + } + + value := reflect.Indirect(reflect.ValueOf(o)) + typ := reflect.TypeOf(o).Elem() + n := typ.NumField() + + updateMap := M{} + for _, col := range fields { + for i := 0; i < n; i++ { + f := typ.Field(i) + if f.Tag.Get("boil") == col { + updateMap[col] = value.Field(i).Interface() + } + } + } + + slice := AuditEventSlice{o} + if rowsAff, err := slice.UpdateAll(ctx, tx, updateMap); err != nil { + t.Error(err) + } else if rowsAff != 1 { + t.Error("wanted one record updated but got", rowsAff) + } +} diff --git a/database/models/sqlite3/boil_main_test.go b/database/models/sqlite3/boil_main_test.go new file mode 100644 index 00000000..57ff6348 --- /dev/null +++ b/database/models/sqlite3/boil_main_test.go @@ -0,0 +1,119 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sqlite3 + +import ( + "database/sql" + "flag" + "fmt" + "math/rand" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/spf13/viper" + "github.com/thrasher-corp/sqlboiler/boil" +) + +var flagDebugMode = flag.Bool("test.sqldebug", false, "Turns on debug mode for SQL statements") +var flagConfigFile = flag.String("test.config", "", "Overrides the default config") + +const outputDirDepth = 3 + +var ( + dbMain tester +) + +type tester interface { + setup() error + conn() (*sql.DB, error) + teardown() error +} + +func TestMain(m *testing.M) { + if dbMain == nil { + fmt.Println("no dbMain tester interface was ready") + os.Exit(-1) + } + + rand.Seed(time.Now().UnixNano()) + + flag.Parse() + + var err error + + // Load configuration + err = initViper() + if err != nil { + fmt.Println("unable to load config file") + os.Exit(-2) + } + + // Set DebugMode so we can see generated sql statements + boil.DebugMode = *flagDebugMode + + if err = dbMain.setup(); err != nil { + fmt.Println("Unable to execute setup:", err) + os.Exit(-4) + } + + conn, err := dbMain.conn() + if err != nil { + fmt.Println("failed to get connection:", err) + } + + var code int + boil.SetDB(conn) + code = m.Run() + + if err = dbMain.teardown(); err != nil { + fmt.Println("Unable to execute teardown:", err) + os.Exit(-5) + } + + os.Exit(code) +} + +func initViper() error { + if flagConfigFile != nil && *flagConfigFile != "" { + viper.SetConfigFile(*flagConfigFile) + if err := viper.ReadInConfig(); err != nil { + return err + } + return nil + } + + var err error + + viper.SetConfigName("sqlboiler") + + configHome := os.Getenv("XDG_CONFIG_HOME") + homePath := os.Getenv("HOME") + wd, err := os.Getwd() + if err != nil { + wd = strings.Repeat("../", outputDirDepth) + } else { + wd = wd + strings.Repeat("/..", outputDirDepth) + } + + configPaths := []string{wd} + if len(configHome) > 0 { + configPaths = append(configPaths, filepath.Join(configHome, "sqlboiler")) + } else { + configPaths = append(configPaths, filepath.Join(homePath, ".config/sqlboiler")) + } + + for _, p := range configPaths { + viper.AddConfigPath(p) + } + + // Ignore errors here, fall back to defaults and validation to provide errs + _ = viper.ReadInConfig() + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() + + return nil +} diff --git a/database/models/sqlite3/boil_queries.go b/database/models/sqlite3/boil_queries.go new file mode 100644 index 00000000..daec99d8 --- /dev/null +++ b/database/models/sqlite3/boil_queries.go @@ -0,0 +1,33 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sqlite3 + +import ( + "github.com/thrasher-corp/sqlboiler/drivers" + "github.com/thrasher-corp/sqlboiler/queries" + "github.com/thrasher-corp/sqlboiler/queries/qm" +) + +var dialect = drivers.Dialect{ + LQ: 0x22, + RQ: 0x22, + + UseIndexPlaceholders: false, + UseLastInsertID: true, + UseSchema: false, + UseDefaultKeyword: false, + UseAutoColumns: false, + UseTopClause: false, + UseOutputClause: false, + UseCaseWhenExistsClause: false, +} + +// NewQuery initializes a new Query using the passed in QueryMods +func NewQuery(mods ...qm.QueryMod) *queries.Query { + q := &queries.Query{} + queries.SetDialect(q, &dialect) + qm.Apply(q, mods...) + + return q +} diff --git a/database/models/sqlite3/boil_queries_test.go b/database/models/sqlite3/boil_queries_test.go new file mode 100644 index 00000000..d10c3425 --- /dev/null +++ b/database/models/sqlite3/boil_queries_test.go @@ -0,0 +1,52 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sqlite3 + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "math/rand" + "regexp" + + "github.com/thrasher-corp/sqlboiler/boil" +) + +var dbNameRand *rand.Rand + +func MustTx(transactor boil.ContextTransactor, err error) boil.ContextTransactor { + if err != nil { + panic(fmt.Sprintf("Cannot create a transactor: %s", err)) + } + return transactor +} + +func newFKeyDestroyer(regex *regexp.Regexp, reader io.Reader) io.Reader { + return &fKeyDestroyer{ + reader: reader, + rgx: regex, + } +} + +type fKeyDestroyer struct { + reader io.Reader + buf *bytes.Buffer + rgx *regexp.Regexp +} + +func (f *fKeyDestroyer) Read(b []byte) (int, error) { + if f.buf == nil { + all, err := ioutil.ReadAll(f.reader) + if err != nil { + return 0, err + } + + all = bytes.Replace(all, []byte{'\r', '\n'}, []byte{'\n'}, -1) + all = f.rgx.ReplaceAll(all, []byte{}) + f.buf = bytes.NewBuffer(all) + } + + return f.buf.Read(b) +} diff --git a/database/models/sqlite3/boil_suites_test.go b/database/models/sqlite3/boil_suites_test.go new file mode 100644 index 00000000..5b53a446 --- /dev/null +++ b/database/models/sqlite3/boil_suites_test.go @@ -0,0 +1,121 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sqlite3 + +import "testing" + +// This test suite runs each operation test in parallel. +// Example, if your database has 3 tables, the suite will run: +// table1, table2 and table3 Delete in parallel +// table1, table2 and table3 Insert in parallel, and so forth. +// It does NOT run each operation group in parallel. +// Separating the tests thusly grants avoidance of Postgres deadlocks. +func TestParent(t *testing.T) { + t.Run("AuditEvents", testAuditEvents) +} + +func TestDelete(t *testing.T) { + t.Run("AuditEvents", testAuditEventsDelete) +} + +func TestQueryDeleteAll(t *testing.T) { + t.Run("AuditEvents", testAuditEventsQueryDeleteAll) +} + +func TestSliceDeleteAll(t *testing.T) { + t.Run("AuditEvents", testAuditEventsSliceDeleteAll) +} + +func TestExists(t *testing.T) { + t.Run("AuditEvents", testAuditEventsExists) +} + +func TestFind(t *testing.T) { + t.Run("AuditEvents", testAuditEventsFind) +} + +func TestBind(t *testing.T) { + t.Run("AuditEvents", testAuditEventsBind) +} + +func TestOne(t *testing.T) { + t.Run("AuditEvents", testAuditEventsOne) +} + +func TestAll(t *testing.T) { + t.Run("AuditEvents", testAuditEventsAll) +} + +func TestCount(t *testing.T) { + t.Run("AuditEvents", testAuditEventsCount) +} + +func TestHooks(t *testing.T) { + t.Run("AuditEvents", testAuditEventsHooks) +} + +func TestInsert(t *testing.T) { + t.Run("AuditEvents", testAuditEventsInsert) + t.Run("AuditEvents", testAuditEventsInsertWhitelist) +} + +// TestToOne tests cannot be run in parallel +// or deadlocks can occur. +func TestToOne(t *testing.T) {} + +// TestOneToOne tests cannot be run in parallel +// or deadlocks can occur. +func TestOneToOne(t *testing.T) {} + +// TestToMany tests cannot be run in parallel +// or deadlocks can occur. +func TestToMany(t *testing.T) {} + +// TestToOneSet tests cannot be run in parallel +// or deadlocks can occur. +func TestToOneSet(t *testing.T) {} + +// TestToOneRemove tests cannot be run in parallel +// or deadlocks can occur. +func TestToOneRemove(t *testing.T) {} + +// TestOneToOneSet tests cannot be run in parallel +// or deadlocks can occur. +func TestOneToOneSet(t *testing.T) {} + +// TestOneToOneRemove tests cannot be run in parallel +// or deadlocks can occur. +func TestOneToOneRemove(t *testing.T) {} + +// TestToManyAdd tests cannot be run in parallel +// or deadlocks can occur. +func TestToManyAdd(t *testing.T) {} + +// TestToManySet tests cannot be run in parallel +// or deadlocks can occur. +func TestToManySet(t *testing.T) {} + +// TestToManyRemove tests cannot be run in parallel +// or deadlocks can occur. +func TestToManyRemove(t *testing.T) {} + +func TestReload(t *testing.T) { + t.Run("AuditEvents", testAuditEventsReload) +} + +func TestReloadAll(t *testing.T) { + t.Run("AuditEvents", testAuditEventsReloadAll) +} + +func TestSelect(t *testing.T) { + t.Run("AuditEvents", testAuditEventsSelect) +} + +func TestUpdate(t *testing.T) { + t.Run("AuditEvents", testAuditEventsUpdate) +} + +func TestSliceUpdateAll(t *testing.T) { + t.Run("AuditEvents", testAuditEventsSliceUpdateAll) +} diff --git a/database/models/sqlite3/boil_table_names.go b/database/models/sqlite3/boil_table_names.go new file mode 100644 index 00000000..17af4b3c --- /dev/null +++ b/database/models/sqlite3/boil_table_names.go @@ -0,0 +1,10 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sqlite3 + +var TableNames = struct { + AuditEvent string +}{ + AuditEvent: "audit_event", +} diff --git a/database/models/sqlite3/boil_types.go b/database/models/sqlite3/boil_types.go new file mode 100644 index 00000000..68f4ae1c --- /dev/null +++ b/database/models/sqlite3/boil_types.go @@ -0,0 +1,52 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sqlite3 + +import ( + "strconv" + + "github.com/pkg/errors" + "github.com/thrasher-corp/sqlboiler/boil" + "github.com/thrasher-corp/sqlboiler/strmangle" +) + +// M type is for providing columns and column values to UpdateAll. +type M map[string]interface{} + +// ErrSyncFail occurs during insert when the record could not be retrieved in +// order to populate default value information. This usually happens when LastInsertId +// fails or there was a primary key configuration that was not resolvable. +var ErrSyncFail = errors.New("sqlite3: failed to synchronize data after insert") + +type insertCache struct { + query string + retQuery string + valueMapping []uint64 + retMapping []uint64 +} + +type updateCache struct { + query string + valueMapping []uint64 +} + +func makeCacheKey(cols boil.Columns, nzDefaults []string) string { + buf := strmangle.GetBuffer() + + buf.WriteString(strconv.Itoa(cols.Kind)) + for _, w := range cols.Cols { + buf.WriteString(w) + } + + if len(nzDefaults) != 0 { + buf.WriteByte('.') + } + for _, nz := range nzDefaults { + buf.WriteString(nz) + } + + str := buf.String() + strmangle.PutBuffer(buf) + return str +} diff --git a/database/models/sqlite3/sqlite3_main_test.go b/database/models/sqlite3/sqlite3_main_test.go new file mode 100644 index 00000000..96605f7a --- /dev/null +++ b/database/models/sqlite3/sqlite3_main_test.go @@ -0,0 +1,63 @@ +// Code generated by SQLBoiler 3.5.0-gct (https://github.com/thrasher-corp/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sqlite3 + +import ( + "database/sql" + "fmt" + "math/rand" + "os" + "path/filepath" + "regexp" + + _ "github.com/mattn/go-sqlite3" + "github.com/thrasher-corp/goose" +) + +var rgxSQLitekey = regexp.MustCompile(`(?mi)((,\n)?\s+foreign key.*?\n)+`) + +type sqliteTester struct { + dbConn *sql.DB + + dbName string + testDBName string +} + +func init() { + dbMain = &sqliteTester{} +} + +func (s *sqliteTester) setup() error { + s.testDBName = filepath.Join(os.TempDir(), fmt.Sprintf("boil-sqlite3-%d.sql", rand.Int())) + + return nil +} + +func (s *sqliteTester) teardown() error { + if s.dbConn != nil { + s.dbConn.Close() + } + + return os.Remove(s.testDBName) +} + +func (s *sqliteTester) conn() (*sql.DB, error) { + if s.dbConn != nil { + return s.dbConn, nil + } + + var err error + s.dbConn, err = sql.Open("sqlite3", fmt.Sprintf("file:%s?_loc=UTC", s.testDBName)) + if err != nil { + return nil, err + } + + path := filepath.Join("..", "..", "migrations") + err = goose.Run("up", s.dbConn, "sqlite3", path, "") + if err != nil { + return nil, err + } + + return s.dbConn, nil +} diff --git a/database/repository/audit/audit.go b/database/repository/audit/audit.go new file mode 100644 index 00000000..b78cbfd4 --- /dev/null +++ b/database/repository/audit/audit.go @@ -0,0 +1,93 @@ +package audit + +import ( + "context" + "errors" + "time" + + "github.com/thrasher-corp/gocryptotrader/database" + modelPSQL "github.com/thrasher-corp/gocryptotrader/database/models/postgres" + modelSQLite "github.com/thrasher-corp/gocryptotrader/database/models/sqlite3" + "github.com/thrasher-corp/gocryptotrader/database/repository" + log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/sqlboiler/boil" + "github.com/thrasher-corp/sqlboiler/queries/qm" +) + +// TableTimeFormat Go Time format conversion +const TableTimeFormat = "2006-01-02 15:04:05" + +// Event inserts a new audit event to database +func Event(id, msgtype, message string) { + if database.DB.SQL == nil { + return + } + + ctx := context.Background() + ctx = boil.SkipTimestamps(ctx) + + tx, err := database.DB.SQL.BeginTx(ctx, nil) + if err != nil { + log.Errorf(log.Global, "Event transaction begin failed: %v", err) + return + } + + if repository.GetSQLDialect() == database.DBSQLite3 { + var tempEvent = modelSQLite.AuditEvent{ + Type: msgtype, + Identifier: id, + Message: message, + } + err = tempEvent.Insert(ctx, tx, boil.Blacklist("created_at")) + } else { + var tempEvent = modelPSQL.AuditEvent{ + Type: msgtype, + Identifier: id, + Message: message, + } + err = tempEvent.Insert(ctx, tx, boil.Blacklist("created_at")) + } + + if err != nil { + log.Errorf(log.Global, "Event insert failed: %v", err) + err = tx.Rollback() + if err != nil { + log.Errorf(log.Global, "Event Transaction rollback failed: %v", err) + } + return + } + + err = tx.Commit() + if err != nil { + log.Errorf(log.Global, "Event Transaction commit failed: %v", err) + err = tx.Rollback() + if err != nil { + log.Errorf(log.Global, "Event Transaction rollback failed: %v", err) + } + return + } +} + +// GetEvent () returns list of order events matching query +func GetEvent(startTime, endTime time.Time, order string, limit int) (interface{}, error) { + if database.DB.SQL == nil { + return nil, errors.New("database is nil") + } + + query := qm.Where("created_at BETWEEN ? AND ?", startTime, endTime) + + orderByQueryString := "id" + if order == "desc" { + orderByQueryString += " desc" + } + + orderByQuery := qm.OrderBy(orderByQueryString) + limitQuery := qm.Limit(limit) + + ctx := context.Background() + if repository.GetSQLDialect() == database.DBSQLite3 { + return modelSQLite.AuditEvents(query, orderByQuery, limitQuery).All(ctx, database.DB.SQL) + } + + return modelPSQL.AuditEvents(query, orderByQuery, limitQuery).All(ctx, database.DB.SQL) +} diff --git a/database/repository/repository.go b/database/repository/repository.go new file mode 100644 index 00000000..ed2fab36 --- /dev/null +++ b/database/repository/repository.go @@ -0,0 +1,16 @@ +package repository + +import ( + "github.com/thrasher-corp/gocryptotrader/database" +) + +// GetSQLDialect returns current SQL Dialect based on enabled driver +func GetSQLDialect() string { + switch database.DB.Config.Driver { + case "sqlite", "sqlite3": + return database.DBSQLite3 + case "psql", "postgres", "postgresql": + return database.DBPostgreSQL + } + return "invalid driver" +} diff --git a/database/tests/audit_test.go b/database/tests/audit_test.go new file mode 100644 index 00000000..59c4d38b --- /dev/null +++ b/database/tests/audit_test.go @@ -0,0 +1,120 @@ +package tests + +import ( + "fmt" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/database" + "github.com/thrasher-corp/gocryptotrader/database/drivers" + "github.com/thrasher-corp/gocryptotrader/database/repository" + "github.com/thrasher-corp/gocryptotrader/database/repository/audit" + "github.com/thrasher-corp/goose" +) + +func TestAudit(t *testing.T) { + testCases := []struct { + name string + config *database.Config + runner func(t *testing.T) + closer func(t *testing.T, dbConn *database.Db) error + output interface{} + }{ + { + "SQLite-Write", + &database.Config{ + Driver: database.DBSQLite3, + ConnectionDetails: drivers.ConnectionDetails{Database: "./testdb"}, + }, + + writeAudit, + closeDatabase, + nil, + }, + { + "SQLite-Read", + &database.Config{ + Driver: database.DBSQLite3, + ConnectionDetails: drivers.ConnectionDetails{Database: "./testdb"}, + }, + + readHelper, + closeDatabase, + nil, + }, + { + "Postgres-Write", + postgresTestDatabase, + writeAudit, + nil, + nil, + }, + { + "Postgres-Read", + postgresTestDatabase, + readHelper, + nil, + nil, + }, + } + + for _, tests := range testCases { + test := tests + + t.Run(test.name, func(t *testing.T) { + if !checkValidConfig(t, &test.config.ConnectionDetails) { + t.Skip("database not configured skipping test") + } + + dbConn, err := connectToDatabase(t, test.config) + + if err != nil { + t.Fatal(err) + } + path := filepath.Join("..", "migrations") + err = goose.Run("up", dbConn.SQL, repository.GetSQLDialect(), path, "") + if err != nil { + t.Fatalf("failed to run migrations %v", err) + } + + if test.runner != nil { + test.runner(t) + } + + if test.closer != nil { + err = test.closer(t, dbConn) + if err != nil { + t.Log(err) + } + } + }) + } +} + +func writeAudit(t *testing.T) { + t.Helper() + var wg sync.WaitGroup + + for x := 0; x < 20; x++ { + wg.Add(1) + + go func(x int) { + defer wg.Done() + test := fmt.Sprintf("test-%v", x) + audit.Event(test, test, test) + }(x) + } + + wg.Wait() +} + +func readHelper(t *testing.T) { + t.Helper() + + _, err := audit.GetEvent(time.Now().Add(-time.Hour*60), time.Now(), "asc", 1) + if err != nil { + t.Error(err) + } +} diff --git a/database/tests/database_test.go b/database/tests/database_test.go new file mode 100644 index 00000000..6c35a64d --- /dev/null +++ b/database/tests/database_test.go @@ -0,0 +1,186 @@ +package tests + +import ( + "fmt" + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/thrasher-corp/gocryptotrader/database" + "github.com/thrasher-corp/gocryptotrader/database/drivers" + psqlConn "github.com/thrasher-corp/gocryptotrader/database/drivers/postgres" + sqliteConn "github.com/thrasher-corp/gocryptotrader/database/drivers/sqlite3" +) + +var ( + tempDir string + postgresTestDatabase *database.Config +) + +func getConnectionDetails() *database.Config { + _, exists := os.LookupEnv("TRAVIS") + if exists { + return &database.Config{ + Enabled: true, + Driver: "postgres", + ConnectionDetails: drivers.ConnectionDetails{ + Host: "localhost", + Port: 5432, + Username: "postgres", + Password: "", + Database: "gct_dev_ci", + SSLMode: "", + }, + } + } + + _, exists = os.LookupEnv("APPVEYOR") + if exists { + return &database.Config{ + Enabled: true, + Driver: "postgres", + ConnectionDetails: drivers.ConnectionDetails{ + Host: "localhost", + Port: 5432, + Username: "postgres", + Password: "Password12!", + Database: "gct_dev_ci", + SSLMode: "", + }, + } + } + + return &database.Config{ + Enabled: true, + Driver: "postgres", + ConnectionDetails: drivers.ConnectionDetails{ + //Host: "", + //Port: 5432, + //Username: "", + //Password: "", + //Database: "", + //SSLMode: "", + }, + } +} + +func TestMain(m *testing.M) { + var err error + postgresTestDatabase = getConnectionDetails() + + tempDir, err = ioutil.TempDir("", "gct-temp") + if err != nil { + fmt.Printf("failed to create temp file: %v", err) + os.Exit(1) + } + + t := m.Run() + + err = os.RemoveAll(tempDir) + if err != nil { + fmt.Printf("Failed to remove temp db file: %v", err) + } + + os.Exit(t) +} + +func TestDatabaseConnect(t *testing.T) { + testCases := []struct { + name string + config *database.Config + closer func(t *testing.T, dbConn *database.Db) error + output interface{} + }{ + { + "SQLite", + &database.Config{ + Driver: database.DBSQLite3, + ConnectionDetails: drivers.ConnectionDetails{Database: "./testdb.db"}, + }, + closeDatabase, + nil, + }, + { + "SQliteNoDatabase", + &database.Config{ + Driver: database.DBSQLite3, + ConnectionDetails: drivers.ConnectionDetails{ + Host: "localhost", + }, + }, + nil, + database.ErrNoDatabaseProvided, + }, + { + name: "Postgres", + config: postgresTestDatabase, + output: nil, + }, + } + + for _, tests := range testCases { + test := tests + t.Run(test.name, func(t *testing.T) { + if !checkValidConfig(t, &test.config.ConnectionDetails) { + t.Skip("database not configured skipping test") + } + + dbConn, err := connectToDatabase(t, test.config) + if err != nil { + switch v := test.output.(type) { + case error: + if v.Error() != err.Error() { + t.Fatal(err) + } + return + default: + break + } + } + + if test.closer != nil { + err = test.closer(t, dbConn) + if err != nil { + t.Log(err) + } + } + }) + } +} + +func connectToDatabase(t *testing.T, conn *database.Config) (dbConn *database.Db, err error) { + t.Helper() + database.DB.Config = conn + + if conn.Driver == database.DBPostgreSQL { + dbConn, err = psqlConn.Connect() + if err != nil { + return nil, err + } + } else if conn.Driver == database.DBSQLite3 || conn.Driver == database.DBSQLite { + database.DB.DataPath = tempDir + dbConn, err = sqliteConn.Connect() + + if err != nil { + return nil, err + } + } + database.DB.Connected = true + return +} + +func closeDatabase(t *testing.T, conn *database.Db) (err error) { + t.Helper() + + if conn != nil { + return conn.SQL.Close() + } + return nil +} + +func checkValidConfig(t *testing.T, config *drivers.ConnectionDetails) bool { + t.Helper() + + return !reflect.DeepEqual(drivers.ConnectionDetails{}, *config) +} diff --git a/dispatch/dispatch.go b/dispatch/dispatch.go new file mode 100644 index 00000000..63ee5cfe --- /dev/null +++ b/dispatch/dispatch.go @@ -0,0 +1,362 @@ +package dispatch + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/gofrs/uuid" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +func init() { + dispatcher = &Dispatcher{ + routes: make(map[uuid.UUID][]chan interface{}), + outbound: sync.Pool{ + New: func() interface{} { + // Create unbuffered channel for data pass + return make(chan interface{}) + }, + }, + } +} + +// Start starts the dispatch system by spawning workers and allocating memory +func Start(workers, jobsLimit int) error { + if dispatcher == nil { + return errors.New(errNotInitialised) + } + + mtx.Lock() + defer mtx.Unlock() + return dispatcher.start(workers, jobsLimit) +} + +// Stop attempts to stop the dispatch service, this will close all pipe channels +// flush job list and drop all workers +func Stop() error { + if dispatcher == nil { + return errors.New(errNotInitialised) + } + + log.Debugln(log.DispatchMgr, "Dispatch manager shutting down...") + + mtx.Lock() + defer mtx.Unlock() + return dispatcher.stop() +} + +// IsRunning checks to see if the dispatch service is running +func IsRunning() bool { + if dispatcher == nil { + return false + } + + return dispatcher.isRunning() +} + +// DropWorker drops a worker routine +func DropWorker() error { + if dispatcher == nil { + return errors.New(errNotInitialised) + } + + dispatcher.dropWorker() + return nil +} + +// SpawnWorker starts a new worker routine +func SpawnWorker() error { + if dispatcher == nil { + return errors.New(errNotInitialised) + } + return dispatcher.spawnWorker() +} + +// start compares atomic running value, sets defaults, overides with +// configuration, then spawns workers +func (d *Dispatcher) start(workers, channelCapacity int) error { + if atomic.LoadUint32(&d.running) == 1 { + return errors.New(errAlreadyStarted) + } + + if workers < 1 { + log.Warn(log.DispatchMgr, + "Dispatcher: workers cannot be zero, using default values") + workers = DefaultMaxWorkers + } + if channelCapacity < 1 { + log.Warn(log.DispatchMgr, + "Dispatcher: jobs limit cannot be zero, using default values") + channelCapacity = DefaultJobsLimit + } + d.jobs = make(chan *job, channelCapacity) + d.maxWorkers = int32(workers) + d.shutdown = make(chan *sync.WaitGroup) + + if atomic.LoadInt32(&d.count) != 0 { + return errors.New("dispatcher leaked workers found") + } + + for i := int32(0); i < d.maxWorkers; i++ { + err := d.spawnWorker() + if err != nil { + return err + } + } + + atomic.SwapUint32(&d.running, 1) + return nil +} + +// stop stops the service and shuts down all worker routines +func (d *Dispatcher) stop() error { + if !atomic.CompareAndSwapUint32(&d.running, 1, 0) { + return errors.New(errCannotShutdown) + } + close(d.shutdown) + ch := make(chan struct{}) + timer := time.NewTimer(1 * time.Second) + defer func() { + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + }() + go func(ch chan struct{}) { d.wg.Wait(); ch <- struct{}{} }(ch) + select { + case <-ch: + // close all routes + for key := range d.routes { + for i := range d.routes[key] { + close(d.routes[key][i]) + } + + d.routes[key] = nil + } + + for len(d.jobs) != 0 { // drain jobs channel for old data + <-d.jobs + } + + log.Debugln(log.DispatchMgr, "Dispatch manager shutdown.") + + return nil + case <-timer.C: + return errors.New(errShutdownRoutines) + } +} + +// isRunning returns if the dispatch system is running +func (d *Dispatcher) isRunning() bool { + return atomic.LoadUint32(&d.running) == 1 +} + +// dropWorker deallocates a worker routine +func (d *Dispatcher) dropWorker() { + wg := sync.WaitGroup{} + wg.Add(1) + d.shutdown <- &wg + wg.Wait() +} + +// spawnWorker allocates a new worker for job processing +func (d *Dispatcher) spawnWorker() error { + if atomic.LoadInt32(&d.count) >= d.maxWorkers { + return errors.New("dispatcher cannot spawn more workers; ceiling reached") + } + var spawnWg sync.WaitGroup + spawnWg.Add(1) + go d.relayer(&spawnWg) + spawnWg.Wait() + return nil +} + +// Relayer routine relays communications across the defined routes +func (d *Dispatcher) relayer(i *sync.WaitGroup) { + atomic.AddInt32(&d.count, 1) + d.wg.Add(1) + timeout := time.NewTimer(0) + i.Done() + for { + select { + case j := <-d.jobs: + d.rMtx.RLock() + if _, ok := d.routes[j.ID]; !ok { + d.rMtx.RUnlock() + continue + } + // Channel handshake timeout feature if a channel is blocked for any + // period of time due to an issue with the receiving routine. + // This will wait on channel then fall over to the next route when + // the timer actuates and continue over the route list. Have to + // iterate across full length of routes so every routine can get + // their new info, cannot be buffered as we dont want to have an old + // orderbook etc contained in a buffered channel when a routine + // actually is ready for a receive. + // TODO: Need to consider optimal timer length + for i := range d.routes[j.ID] { + if !timeout.Stop() { // Stop timer before reset + // Drain channel if timer has already actuated + select { + case <-timeout.C: + default: + } + } + + timeout.Reset(DefaultHandshakeTimeout) + select { + case d.routes[j.ID][i] <- j.Data: + case <-timeout.C: + } + } + d.rMtx.RUnlock() + + case v := <-d.shutdown: + if !timeout.Stop() { + select { + case <-timeout.C: + default: + } + } + atomic.AddInt32(&d.count, -1) + if v != nil { + v.Done() + } + d.wg.Done() + return + } + } +} + +// publish relays data to the subscribed subsystems +func (d *Dispatcher) publish(id uuid.UUID, data interface{}) error { + if data == nil { + return errors.New("dispatcher data cannot be nil") + } + + if id == (uuid.UUID{}) { + return errors.New("dispatcher uuid not set") + } + + if atomic.LoadUint32(&d.running) == 0 { + return nil + } + + // Create a new job to publish + newJob := &job{ + Data: data, + ID: id, + } + + // Push job on stack here + select { + case d.jobs <- newJob: + default: + return fmt.Errorf("dispatcher jobs at limit [%d] current worker count [%d]. Spawn more workers via --dispatchworkers=x"+ + ", or increase the jobs limit via --dispatchjobslimit=x", + len(d.jobs), + atomic.LoadInt32(&d.count)) + } + + return nil +} + +// Subscribe subscribes a system and returns a communication chan, this does not +// ensure initial push. If your routine is out of sync with heartbeat and the +// system does not get a change, its up to you to in turn get initial state. +func (d *Dispatcher) subscribe(id uuid.UUID) (chan interface{}, error) { + if atomic.LoadUint32(&d.running) == 0 { + return nil, errors.New(errNotInitialised) + } + + // Read lock to read route list + d.rMtx.RLock() + _, ok := d.routes[id] + d.rMtx.RUnlock() + if !ok { + return nil, errors.New("dispatcher uuid not found in route list") + } + + // Get an unused channel from the channel pool + unusedChan := d.outbound.Get().(chan interface{}) + + // Lock for writing to the route list + d.rMtx.Lock() + d.routes[id] = append(d.routes[id], unusedChan) + d.rMtx.Unlock() + + return unusedChan, nil +} + +// Unsubscribe unsubs a routine from the dispatcher +func (d *Dispatcher) unsubscribe(id uuid.UUID, usedChan chan interface{}) error { + if atomic.LoadUint32(&d.running) == 0 { + // reference will already be released in the stop function + return nil + } + + // Read lock to read route list + d.rMtx.RLock() + _, ok := d.routes[id] + d.rMtx.RUnlock() + if !ok { + return errors.New("dispatcher uuid does not reference any channels") + } + + // Lock for write to delete references + d.rMtx.Lock() + for i := range d.routes[id] { + if d.routes[id][i] != usedChan { + continue + } + // Delete individual reference + d.routes[id][i] = d.routes[id][len(d.routes[id])-1] + d.routes[id][len(d.routes[id])-1] = nil + d.routes[id] = d.routes[id][:len(d.routes[id])-1] + + d.rMtx.Unlock() + + // Drain and put the used chan back in pool; only if it is not closed. + select { + case _, ok := <-usedChan: + if !ok { + return nil + } + default: + } + + d.outbound.Put(usedChan) + return nil + } + d.rMtx.Unlock() + return errors.New("dispatcher channel not found in uuid reference slice") +} + +// GetNewID returns a new ID +func (d *Dispatcher) getNewID() (uuid.UUID, error) { + // Generate new uuid + newID, err := uuid.NewV4() + if err != nil { + return uuid.UUID{}, err + } + + // Check to see if it already exists + d.rMtx.RLock() + _, ok := d.routes[newID] + d.rMtx.RUnlock() + if ok { + return newID, errors.New("dispatcher collision detected, uuid already exists") + } + + // Write the key into system + d.rMtx.Lock() + d.routes[newID] = nil + d.rMtx.Unlock() + + return newID, nil +} diff --git a/dispatch/dispatch_test.go b/dispatch/dispatch_test.go new file mode 100644 index 00000000..ee39af81 --- /dev/null +++ b/dispatch/dispatch_test.go @@ -0,0 +1,310 @@ +package dispatch + +import ( + "fmt" + "os" + "sync" + "testing" + + "github.com/gofrs/uuid" +) + +var mux *Mux + +func TestMain(m *testing.M) { + err := Start(DefaultMaxWorkers, 0) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + cpyDispatch = dispatcher + mux = GetNewMux() + cpyMux = mux + os.Exit(m.Run()) +} + +var cpyDispatch *Dispatcher +var cpyMux *Mux + +func TestDispatcher(t *testing.T) { + dispatcher = nil + err := Stop() + if err == nil { + t.Error("error cannot be nil") + } + + err = Start(10, 0) + if err == nil { + t.Error("error cannot be nil") + } + if IsRunning() { + t.Error("should be false") + } + + err = DropWorker() + if err == nil { + t.Error("error cannot be nil") + } + + err = SpawnWorker() + if err == nil { + t.Error("error cannot be nil") + } + + dispatcher = cpyDispatch + + if !IsRunning() { + t.Error("should be true") + } + + err = Start(10, 0) + if err == nil { + t.Error("error cannot be nil") + } + + err = DropWorker() + if err != nil { + t.Error(err) + } + + err = DropWorker() + if err != nil { + t.Error(err) + } + + err = SpawnWorker() + if err != nil { + t.Error(err) + } + + err = SpawnWorker() + if err != nil { + t.Error(err) + } + + err = SpawnWorker() + if err == nil { + t.Error("error cannot be nil") + } + + err = Stop() + if err != nil { + t.Error(err) + } + + err = Stop() + if err == nil { + t.Error("error cannot be nil") + } + + err = Start(0, 20) + if err != nil { + t.Error(err) + } + if cap(dispatcher.jobs) != 20 { + t.Errorf("Expected jobs limit to be %v, is %v", 20, cap(dispatcher.jobs)) + } + payload := "something" + + err = dispatcher.publish(uuid.UUID{}, &payload) + if err == nil { + t.Error("error cannot be nil") + } + + err = dispatcher.publish(uuid.UUID{}, nil) + if err == nil { + t.Error("error cannot be nil") + } + + id, err := dispatcher.getNewID() + if err != nil { + t.Error(err) + } + + err = dispatcher.publish(id, &payload) + if err != nil { + t.Error(err) + } + + err = dispatcher.stop() + if err != nil { + t.Error(err) + } + + err = dispatcher.publish(id, &payload) + if err != nil { + t.Error(err) + } + + _, err = dispatcher.subscribe(id) + if err == nil { + t.Error("error cannot be nil") + } + + err = dispatcher.start(10, -1) + if err != nil { + t.Error(err) + } + if cap(dispatcher.jobs) != DefaultJobsLimit { + t.Errorf("Expected jobs limit to be %v, is %v", DefaultJobsLimit, cap(dispatcher.jobs)) + } + someID, err := uuid.NewV4() + if err != nil { + t.Error(err) + } + + _, err = dispatcher.subscribe(someID) + if err == nil { + t.Error("error cannot be nil") + } + + randomChan := make(chan interface{}) + err = dispatcher.unsubscribe(someID, randomChan) + if err == nil { + t.Error("Expected error") + } + + err = dispatcher.unsubscribe(id, randomChan) + if err == nil { + t.Error("Expected error") + } + + close(randomChan) + err = dispatcher.unsubscribe(id, randomChan) + if err == nil { + t.Error("Expected error") + } +} + +func TestMux(t *testing.T) { + mux = nil + _, err := mux.Subscribe(uuid.UUID{}) + if err == nil { + t.Error("error cannot be nil") + } + + err = mux.Unsubscribe(uuid.UUID{}, nil) + if err == nil { + t.Error("error cannot be nil") + } + + err = mux.Publish(nil, nil) + if err == nil { + t.Error("error cannot be nil") + } + + _, err = mux.GetID() + if err == nil { + t.Error("error cannot be nil") + } + mux = cpyMux + + err = mux.Publish(nil, nil) + if err == nil { + t.Error("error cannot be nil") + } + + payload := "string" + id, err := uuid.NewV4() + if err != nil { + t.Error(err) + } + + err = mux.Publish([]uuid.UUID{id}, &payload) + if err != nil { + t.Error(err) + } + + _, err = mux.Subscribe(uuid.UUID{}) + if err == nil { + t.Error("error cannot be nil") + } + + _, err = mux.Subscribe(id) + if err == nil { + t.Error("error cannot be nil") + } +} + +func TestSubscribe(t *testing.T) { + itemID, err := mux.GetID() + if err != nil { + t.Fatal(err) + } + + var pipes []Pipe + for i := 0; i < 1000; i++ { + newPipe, err := mux.Subscribe(itemID) + if err != nil { + t.Error(err) + } + pipes = append(pipes, newPipe) + } + + for i := range pipes { + err := pipes[i].Release() + if err != nil { + t.Error(err) + } + } +} + +func TestPublish(t *testing.T) { + itemID, err := mux.GetID() + if err != nil { + t.Fatal(err) + } + + pipe, err := mux.Subscribe(itemID) + if err != nil { + t.Error(err) + } + + var wg sync.WaitGroup + wg.Add(1) + go func(wg *sync.WaitGroup) { + wg.Done() + for { + _, ok := <-pipe.C + if !ok { + pErr := pipe.Release() + if pErr != nil { + t.Error(pErr) + } + wg.Done() + return + } + } + }(&wg) + wg.Wait() + wg.Add(1) + mainPayload := "PAYLOAD" + for i := 0; i < 100; i++ { + errMux := mux.Publish([]uuid.UUID{itemID}, &mainPayload) + if errMux != nil { + t.Error(errMux) + } + } + + // Shut down dispatch system + err = Stop() + if err != nil { + t.Fatal(err) + } + wg.Wait() +} + +func BenchmarkSubscribe(b *testing.B) { + newID, err := mux.GetID() + if err != nil { + b.Error(err) + } + + for n := 0; n < b.N; n++ { + _, err := mux.Subscribe(newID) + if err != nil { + b.Error(err) + } + } +} diff --git a/dispatch/dispatch_types.go b/dispatch/dispatch_types.go new file mode 100644 index 00000000..1b9d7d99 --- /dev/null +++ b/dispatch/dispatch_types.go @@ -0,0 +1,87 @@ +package dispatch + +import ( + "sync" + "time" + + "github.com/gofrs/uuid" +) + +const ( + // DefaultJobsLimit defines a maxiumum amount of jobs allowed in channel + DefaultJobsLimit = 100 + + // DefaultMaxWorkers is the package default worker ceiling amount + DefaultMaxWorkers = 10 + + // DefaultHandshakeTimeout defines a workers max length of time to wait on a + // an unbuffered channel for a receiver before moving on to next route + DefaultHandshakeTimeout = 200 * time.Nanosecond + + errNotInitialised = "dispatcher not initialised" + errAlreadyStarted = "dispatcher already started" + errCannotShutdown = "dispatcher cannot shutdown, already stopped" + errShutdownRoutines = "dispatcher did not shutdown properly, routines failed to close" +) + +// dispatcher is our main in memory instance with a stop/start mtx below +var dispatcher *Dispatcher +var mtx sync.Mutex + +// Dispatcher defines an internal subsystem communication/change state publisher +type Dispatcher struct { + // routes refers to a subystem uuid ticket map with associated publish + // channels, a relayer will be given a unique id through its job channel, + // then publish the data across the full registered channels for that uuid. + // See relayer() method below. + routes map[uuid.UUID][]chan interface{} + + // rMtx protects the routes variable ensuring acceptable read/write access + rMtx sync.RWMutex + + // Persistent buffered job queue for relayers + jobs chan *job + + // Dynamic channel pool; returns an unbuffered channel for routes map + outbound sync.Pool + + // MaxWorkers defines max worker ceiling + maxWorkers int32 + // Atomic values ----------------------- + // Worker counter + count int32 + // Dispatch status + running uint32 + + // Unbufferd shutdown chan, sync wg for ensuring concurrency when only + // dropping a single relayer routine + shutdown chan *sync.WaitGroup + + // Relayer shutdown tracking + wg sync.WaitGroup +} + +// job defines a relaying job associated with a ticket which allows routing to +// routines that require specific data +type job struct { + Data interface{} + ID uuid.UUID +} + +// Mux defines a new multiplexor for the dispatch system, these a generated +// per subsystem +type Mux struct { + // Reference to the main running dispatch service + d *Dispatcher + sync.RWMutex +} + +// Pipe defines an outbound object to the desired routine +type Pipe struct { + // Channel to get all our lovely informations + C chan interface{} + // ID to tracked system + id uuid.UUID + // Reference to multiplexor + m *Mux +} diff --git a/dispatch/mux.go b/dispatch/mux.go new file mode 100644 index 00000000..38037403 --- /dev/null +++ b/dispatch/mux.go @@ -0,0 +1,76 @@ +package dispatch + +import ( + "errors" + "reflect" + + "github.com/gofrs/uuid" +) + +// GetNewMux returns a new multiplexor to track subsystem updates +func GetNewMux() *Mux { + return &Mux{d: dispatcher} +} + +// Subscribe takes in a package defined signature element pointing to an ID set +// and returns the associated pipe +func (m *Mux) Subscribe(id uuid.UUID) (Pipe, error) { + if m == nil { + return Pipe{}, errors.New("mux is nil") + } + + if id == (uuid.UUID{}) { + return Pipe{}, errors.New("id not set") + } + + ch, err := m.d.subscribe(id) + if err != nil { + return Pipe{}, err + } + + return Pipe{C: ch, id: id, m: m}, nil +} + +// Unsubscribe returns channel to the pool for the full signature set +func (m *Mux) Unsubscribe(id uuid.UUID, ch chan interface{}) error { + if m == nil { + return errors.New("mux is nil") + } + return m.d.unsubscribe(id, ch) +} + +// Publish takes in a persistent memory address and dispatches changes to +// required pipes. Data should be of *type. +func (m *Mux) Publish(ids []uuid.UUID, data interface{}) error { + if m == nil { + return errors.New("mux is nil") + } + + if data == nil { + return errors.New("data payload is nil") + } + + cpy := reflect.ValueOf(data).Elem().Interface() + + for i := range ids { + // Create copy to not interfere with stored value + err := m.d.publish(ids[i], &cpy) + if err != nil { + return err + } + } + return nil +} + +// GetID a new unique ID to track routing information in the dispatch system +func (m *Mux) GetID() (uuid.UUID, error) { + if m == nil { + return uuid.UUID{}, errors.New("mux is nil") + } + return m.d.getNewID() +} + +// Release returns the channel to the communications pool to be reused +func (p *Pipe) Release() error { + return p.m.Unsubscribe(p.id, p.C) +} diff --git a/docker-compose.yml b/docker-compose.yml index 65aac43b..87c18d76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,9 +7,12 @@ services: depends_on: - daemon ports: - - "9051:80" + - "9054:80" daemon: build: . ports: - "9050:9050" + - "9051:9051" + - "9052:9052" + - "9053:9053" diff --git a/docs/EXCHANGE_API.md b/docs/EXCHANGE_API.md new file mode 100644 index 00000000..2af91388 --- /dev/null +++ b/docs/EXCHANGE_API.md @@ -0,0 +1,72 @@ +# GoCryptoTrader Unified API + + + +[![Build Status](https://travis-ci.com/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.com/thrasher-corp/gocryptotrader) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + +A cryptocurrency trading bot supporting multiple exchanges written in Golang. + +**Please note that this bot is under development and is not ready for production!** + +## Community + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Unified API + +GoCryptoTrader supports a unified API for dealing with exchanges. Each exchange +has its own wrapper file which maps the exchanges own RESTful endpoints into a +standardised way for bot and standalone application usage. + +A full breakdown of all the supported wrapper funcs can be found [here.](https://github.com/thrasher-corp/gocryptotrader/blob/engine/exchanges/interfaces.go#L16) +Please note that these change on a regular basis as GoCryptoTrader is undergoing +rapid development. + +Each exchange supports public API endpoints which don't require any authentication +(fetching ticker, orderbook, trade data) and also private API endpoints (which +require authentication). Some examples include submitting, cancelling and fetching +open orders). To use the authenticated API endpoints, you'll need to set your API +credentials in either the `config.json` file or when you initialise an exchange in +your application, and also have the appropriate key permissions set for the exchange. +Each exchange has a credentials validator which ensures that the API credentials +supplied meet the requirements to make an authenticated request. + +## Public API Ticker Example + +```go + var b bitstamp.Bitstamp + b.SetDefaults() + ticker, err := b.FetchTicker(currency.NewPair(currency.BTC, currency.USD), asset.Spot) + if err != nil { + // Handle error + } + fmt.Println(ticker.Last) +``` + +## Private API Submit Order Example + +```go + var b bitstamp.Bitstamp + b.SetDefaults() + + b.API.Credentials.Key = "your_key" + b.API.Credentials.Secret = "your_secret" + b.API.Credentials.ClientID = "your_clientid" + + o := &order.Submit{ + Pair: currency.NewPair(currency.BTC, currency.USD), + OrderSide: order.Sell, + OrderType: order.Limit, + Price: 1000000, + Amount: 0.1, + } + resp, err := b.SubmitOrder(o) + if err != nil { + // Handle error + } + fmt.Println(resp.OrderID) +``` diff --git a/docs/FILES.md b/docs/FILES.md new file mode 100644 index 00000000..f456f7f7 --- /dev/null +++ b/docs/FILES.md @@ -0,0 +1,48 @@ +# GoCryptoTrader File Hierarchy + + + +[![Build Status](https://travis-ci.com/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.com/thrasher-corp/gocryptotrader) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + +A cryptocurrency trading bot supporting multiple exchanges written in Golang. + +**Please note that this bot is under development and is not ready for production!** + +## Community + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Default data directory + +By default, GoCryptoTrader uses the following data directores: + +Operating System | Path | Translated +--- | --- | ---- +| Windows | %APPDATA%\GoCryptoTrader | C:\Users\User\AppData\Roaming\GoCryptoTrader +| Linux | ~/.gocryptotrader | /home/user/.gocryptotrader +| macOS | ~/.gocryptotrader | /Users/User/.gocryptotrader + +This can be overridden by running GoCryptoTrader with the `-datadir` command line +parameter. + +## Subdirectories + +Depending on the features enabled, you'll see the following directories created +inside the data directory: + +Directory | Reason +--- | --- +| database | Used to store the database file (if using SQLite3) and sqlboiler config files +| logs | Used to store the debug log file (`log.txt` by default), if file output and logging is enabled +| tls | Used to store the generated self-signed certificate and key for gRPC authentication + +## Files + +File | Reason +--- | --- +config.json or config.dat (encrypted config) | Config file which GoCryptoTrader loads from (can be overridden by the `-config` command line parameter). +currency.json | Cached list of fiat and digital currencies diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..c45f6c7e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,31 @@ +# GoCryptoTrader Documentation + + + +[![Build Status](https://travis-ci.com/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.com/thrasher-corp/gocryptotrader) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + +A cryptocurrency trading bot supporting multiple exchanges written in Golang. + +**Please note that this bot is under development and is not ready for production!** + +## Community + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Documentation + +See below for feature documentation: + ++ [Exchange unified API documentation](EXCHANGE_API.md) ++ [File hierarchy documentation](FILES.md) ++ [Config documentation](/config/README.md) ++ [gRPC service documentation](/gctrpc/README.md) ++ [gctcli documentation](/cmd/gctcli/README.md) ++ [Database documentation](/database/README.md) ++ [Currency documentation](/currency/README.md) ++ [Exchange documentation](/exchanges/README.md) ++ [Portfolio documentation](/portfolio/README.md) diff --git a/engine/addr_helpers.go b/engine/addr_helpers.go new file mode 100644 index 00000000..d3afee63 --- /dev/null +++ b/engine/addr_helpers.go @@ -0,0 +1,100 @@ +package engine + +import ( + "errors" + "strings" + "sync" + + "github.com/thrasher-corp/gocryptotrader/currency" +) + +// DepositAddressStore stores a list of exchange deposit addresses +type DepositAddressStore struct { + m sync.Mutex + Store map[string]map[string]string +} + +// DepositAddressManager manages the exchange deposit address store +type DepositAddressManager struct { + Store DepositAddressStore +} + +// vars related to the deposit address helpers +var ( + ErrDepositAddressStoreIsNil = errors.New("deposit address store is nil") + ErrDepositAddressNotFound = errors.New("deposit address does not exist") +) + +// Seed seeds the deposit address store +func (d *DepositAddressStore) Seed(coinData map[string]map[string]string) { + d.m.Lock() + defer d.m.Unlock() + if d.Store == nil { + d.Store = make(map[string]map[string]string) + } + + for k, v := range coinData { + r := make(map[string]string) + for w, x := range v { + r[strings.ToUpper(w)] = x + } + d.Store[strings.ToUpper(k)] = r + } +} + +// GetDepositAddress returns a deposit address based on the specified item +func (d *DepositAddressStore) GetDepositAddress(exchName string, item currency.Code) (string, error) { + d.m.Lock() + defer d.m.Unlock() + + if len(d.Store) == 0 { + return "", ErrDepositAddressStoreIsNil + } + + r, ok := d.Store[strings.ToUpper(exchName)] + if !ok { + return "", ErrExchangeNotFound + } + + addr, ok := r[strings.ToUpper(item.String())] + if !ok { + return "", ErrDepositAddressNotFound + } + + return addr, nil +} + +// GetDepositAddresses returns a list of stored deposit addresses +func (d *DepositAddressStore) GetDepositAddresses(exchName string) (map[string]string, error) { + d.m.Lock() + defer d.m.Unlock() + + if len(d.Store) == 0 { + return nil, ErrDepositAddressStoreIsNil + } + + r, ok := d.Store[strings.ToUpper(exchName)] + if !ok { + return nil, ErrDepositAddressNotFound + } + + return r, nil +} + +// GetDepositAddressByExchange returns a deposit address for the specified exchange and cryptocurrency +// if it exists +func (d *DepositAddressManager) GetDepositAddressByExchange(exchName string, currencyItem currency.Code) (string, error) { + return d.Store.GetDepositAddress(exchName, currencyItem) +} + +// GetDepositAddressesByExchange returns a list of cryptocurrency addresses for the specified +// exchange if they exist +func (d *DepositAddressManager) GetDepositAddressesByExchange(exchName string) (map[string]string, error) { + return d.Store.GetDepositAddresses(exchName) +} + +// Sync synchronises all deposit addresses +func (d *DepositAddressManager) Sync() { + result := GetExchangeCryptocurrencyDepositAddresses() + d.Store.Seed(result) +} diff --git a/engine/addr_helpers_test.go b/engine/addr_helpers_test.go new file mode 100644 index 00000000..d3405e4d --- /dev/null +++ b/engine/addr_helpers_test.go @@ -0,0 +1,64 @@ +package engine + +import ( + "testing" + + "github.com/thrasher-corp/gocryptotrader/currency" +) + +const ( + testBTCAddress = "1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX" +) + +func TestSeed(t *testing.T) { + var d DepositAddressStore + u := map[string]map[string]string{ + "BITSTAMP": { + "BTC": testBTCAddress, + }, + } + + d.Seed(u) + r, err := d.GetDepositAddress("BITSTAMP", currency.BTC) + if err != nil { + t.Error("unexpected result") + } + + if r != testBTCAddress { + t.Error("unexpected result") + } +} + +func TestGetDepositAddress(t *testing.T) { + var d DepositAddressStore + _, err := d.GetDepositAddress("", currency.BTC) + if err != ErrDepositAddressStoreIsNil { + t.Error("non-error on non-existent exchange") + } + + d.Store = map[string]map[string]string{ + "BITSTAMP": { + "BTC": testBTCAddress, + }, + } + + _, err = d.GetDepositAddress("", currency.BTC) + if err != ErrExchangeNotFound { + t.Error("non-error on non-existent exchange") + } + + var r string + r, err = d.GetDepositAddress("BiTStAmP", currency.NewCode("bTC")) + if err != nil { + t.Error("unexpected err: ", err) + } + + if r != testBTCAddress { + t.Error("unexpected BTC address: ", r) + } + + _, err = d.GetDepositAddress("BiTStAmP", currency.LTC) + if err != ErrDepositAddressNotFound { + t.Error("unexpected err: ", err) + } +} diff --git a/engine/comms_relayer.go b/engine/comms_relayer.go new file mode 100644 index 00000000..190f43c6 --- /dev/null +++ b/engine/comms_relayer.go @@ -0,0 +1,94 @@ +package engine + +import ( + "errors" + "sync/atomic" + + "github.com/thrasher-corp/gocryptotrader/communications" + "github.com/thrasher-corp/gocryptotrader/communications/base" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// commsManager starts the NTP manager +type commsManager struct { + started int32 + stopped int32 + shutdown chan struct{} + relayMsg chan base.Event + comms *communications.Communications +} + +func (c *commsManager) Started() bool { + return atomic.LoadInt32(&c.started) == 1 +} + +func (c *commsManager) Start() (err error) { + if atomic.AddInt32(&c.started, 1) != 1 { + return errors.New("communications manager already started") + } + + defer func() { + if err != nil { + atomic.CompareAndSwapInt32(&c.started, 1, 0) + } + }() + + log.Debugln(log.CommunicationMgr, "Communications manager starting...") + commsCfg := Bot.Config.GetCommunicationsConfig() + c.comms, err = communications.NewComm(&commsCfg) + if err != nil { + return err + } + + c.shutdown = make(chan struct{}) + c.relayMsg = make(chan base.Event) + go c.run() + log.Debugln(log.CommunicationMgr, "Communications manager started.") + return nil +} + +func (c *commsManager) GetStatus() (map[string]base.CommsStatus, error) { + if !c.Started() { + return nil, errors.New("communications manager not started") + } + return c.comms.GetStatus(), nil +} + +func (c *commsManager) Stop() error { + if atomic.LoadInt32(&c.started) == 0 { + return errors.New("communications manager not started") + } + + if atomic.AddInt32(&c.stopped, 1) != 1 { + return errors.New("communications manager is already stopped") + } + + close(c.shutdown) + log.Debugln(log.CommunicationMgr, "Communications manager shutting down...") + return nil +} + +func (c *commsManager) PushEvent(evt base.Event) { + if !c.Started() { + return + } + c.relayMsg <- evt +} + +func (c *commsManager) run() { + defer func() { + // TO-DO shutdown comms connections for connected services (Slack etc) + atomic.CompareAndSwapInt32(&c.stopped, 1, 0) + atomic.CompareAndSwapInt32(&c.started, 1, 0) + log.Debugln(log.CommunicationMgr, "Communications manager shutdown.") + }() + + for { + select { + case msg := <-c.relayMsg: + c.comms.PushEvent(msg) + case <-c.shutdown: + return + } + } +} diff --git a/engine/connection.go b/engine/connection.go new file mode 100644 index 00000000..01f46db6 --- /dev/null +++ b/engine/connection.go @@ -0,0 +1,69 @@ +package engine + +import ( + "errors" + "sync/atomic" + + "github.com/thrasher-corp/gocryptotrader/connchecker" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// connectionManager manages the connchecker +type connectionManager struct { + started int32 + stopped int32 + conn *connchecker.Checker +} + +// Started returns if the connection manager has started +func (c *connectionManager) Started() bool { + return atomic.LoadInt32(&c.started) == 1 +} + +// Start starts an instance of the connection manager +func (c *connectionManager) Start() error { + if atomic.AddInt32(&c.started, 1) != 1 { + return errors.New("connection manager already started") + } + + log.Debugln(log.ConnectionMgr, "Connection manager starting...") + var err error + c.conn, err = connchecker.New(Bot.Config.ConnectionMonitor.DNSList, + Bot.Config.ConnectionMonitor.PublicDomainList, + Bot.Config.ConnectionMonitor.CheckInterval) + if err != nil { + atomic.CompareAndSwapInt32(&c.started, 1, 0) + return err + } + + log.Debugln(log.ConnectionMgr, "Connection manager started.") + return nil +} + +// Stop stops the connection manager +func (c *connectionManager) Stop() error { + if atomic.LoadInt32(&c.started) == 0 { + return errors.New("connection manager not started") + } + + if atomic.AddInt32(&c.stopped, 1) != 1 { + return errors.New("connection manager is already stopped") + } + + log.Debugln(log.ConnectionMgr, "Connection manager shutting down...") + c.conn.Shutdown() + atomic.CompareAndSwapInt32(&c.stopped, 1, 0) + atomic.CompareAndSwapInt32(&c.started, 1, 0) + log.Debugln(log.ConnectionMgr, "Connection manager stopped.") + return nil +} + +// IsOnline returns if the connection manager is online +func (c *connectionManager) IsOnline() bool { + if c.conn == nil { + log.Warnln(log.ConnectionMgr, "Connection manager: IsOnline called but conn is nil") + return false + } + + return c.conn.IsConnected() +} diff --git a/engine/database.go b/engine/database.go new file mode 100644 index 00000000..c6811f0b --- /dev/null +++ b/engine/database.go @@ -0,0 +1,130 @@ +package engine + +import ( + "errors" + "fmt" + "sync/atomic" + "time" + + "github.com/thrasher-corp/gocryptotrader/database" + dbpsql "github.com/thrasher-corp/gocryptotrader/database/drivers/postgres" + dbsqlite3 "github.com/thrasher-corp/gocryptotrader/database/drivers/sqlite3" + log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/sqlboiler/boil" +) + +var ( + dbConn *database.Db +) + +type databaseManager struct { + running atomic.Value + shutdown chan struct{} +} + +func (a *databaseManager) Started() bool { + return a.running.Load() == true +} + +func (a *databaseManager) Start() (err error) { + if a.Started() { + return errors.New("database manager already started") + } + + log.Debugln(log.DatabaseMgr, "Database manager starting...") + + a.shutdown = make(chan struct{}) + + if Bot.Config.Database.Enabled { + if Bot.Config.Database.Driver == database.DBPostgreSQL { + log.Debugf(log.DatabaseMgr, + "Attempting to establish database connection to host %s/%s utilising %s driver\n", + Bot.Config.Database.Host, + Bot.Config.Database.Database, + Bot.Config.Database.Driver) + dbConn, err = dbpsql.Connect() + } else if Bot.Config.Database.Driver == database.DBSQLite || + Bot.Config.Database.Driver == database.DBSQLite3 { + log.Debugf(log.DatabaseMgr, + "Attempting to establish database connection to %s utilising %s driver\n", + Bot.Config.Database.Database, + Bot.Config.Database.Driver) + dbConn, err = dbsqlite3.Connect() + } + if err != nil { + return fmt.Errorf("database failed to connect: %v Some features that utilise a database will be unavailable", err) + } + dbConn.Connected = true + + DBLogger := database.Logger{} + if Bot.Config.Database.Verbose { + boil.DebugMode = true + boil.DebugWriter = DBLogger + } + + go a.run() + return nil + } + + return errors.New("database support disabled") +} + +func (a *databaseManager) Stop() error { + if !a.Started() { + return errors.New("database manager already stopped") + } + + log.Debugln(log.DatabaseMgr, "Database manager shutting down...") + + err := dbConn.SQL.Close() + if err != nil { + log.Errorf(log.DatabaseMgr, "Failed to close database: %v", err) + } + + close(a.shutdown) + return nil +} + +func (a *databaseManager) run() { + log.Debugln(log.DatabaseMgr, "Database manager started.") + Bot.ServicesWG.Add(1) + + t := time.NewTicker(time.Second * 2) + + a.running.Store(true) + + defer func() { + t.Stop() + a.running.Store(false) + + Bot.ServicesWG.Done() + + log.Debugln(log.DatabaseMgr, "Database manager shutdown.") + }() + + for { + select { + case <-a.shutdown: + return + case <-t.C: + a.checkConnection() + } + } +} + +func (a *databaseManager) checkConnection() { + dbConn.Mu.Lock() + defer dbConn.Mu.Unlock() + + err := dbConn.SQL.Ping() + if err != nil { + log.Errorf(log.DatabaseMgr, "Database connection error: %v\n", err) + dbConn.Connected = false + return + } + + if !dbConn.Connected { + log.Info(log.DatabaseMgr, "Database connection reestablished") + dbConn.Connected = true + } +} diff --git a/engine/engine.go b/engine/engine.go new file mode 100644 index 00000000..0b16523c --- /dev/null +++ b/engine/engine.go @@ -0,0 +1,494 @@ +package engine + +import ( + "errors" + "flag" + "fmt" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/currency/coinmarketcap" + "github.com/thrasher-corp/gocryptotrader/dispatch" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/gocryptotrader/portfolio" + "github.com/thrasher-corp/gocryptotrader/utils" +) + +// Engine contains configuration, portfolio, exchange & ticker data and is the +// overarching type across this code base. +type Engine struct { + Config *config.Config + Portfolio *portfolio.Base + Exchanges []exchange.IBotExchange + ExchangeCurrencyPairManager *ExchangeCurrencyPairSyncer + NTPManager ntpManager + ConnectionManager connectionManager + DatabaseManager databaseManager + OrderManager orderManager + PortfolioManager portfolioManager + CommsManager commsManager + DepositAddressManager *DepositAddressManager + Settings Settings + Uptime time.Time + ServicesWG sync.WaitGroup +} + +// Vars for engine +var ( + Bot *Engine + + // Stores the set flags + flagSet = make(map[string]bool) +) + +// New starts a new engine +func New() (*Engine, error) { + var b Engine + b.Config = &config.Cfg + + err := b.Config.LoadConfig("", false) + if err != nil { + return nil, fmt.Errorf("failed to load config. Err: %s", err) + } + + return &b, nil +} + +// NewFromSettings starts a new engine based on supplied settings +func NewFromSettings(settings *Settings) (*Engine, error) { + if settings == nil { + return nil, errors.New("engine: settings is nil") + } + + var b Engine + b.Config = &config.Cfg + filePath, err := config.GetFilePath(settings.ConfigFile) + if err != nil { + return nil, err + } + + log.Debugf(log.Global, "Loading config file %s..\n", filePath) + err = b.Config.LoadConfig(filePath, settings.EnableDryRun) + if err != nil { + return nil, fmt.Errorf("failed to load config. Err: %s", err) + } + + err = common.CreateDir(settings.DataDir) + if err != nil { + return nil, fmt.Errorf("failed to open/create data directory: %s. Err: %s", settings.DataDir, err) + } + + if *b.Config.Logging.Enabled { + log.SetupGlobalLogger() + log.SetupSubLoggers(b.Config.Logging.SubLoggers) + } + + b.Settings.ConfigFile = filePath + b.Settings.DataDir = settings.DataDir + b.Settings.CheckParamInteraction = settings.CheckParamInteraction + + err = utils.AdjustGoMaxProcs(settings.GoMaxProcs) + if err != nil { + return nil, fmt.Errorf("unable to adjust runtime GOMAXPROCS value. Err: %s", err) + } + + ValidateSettings(&b, settings) + return &b, nil +} + +// ValidateSettings validates and sets all bot settings +func ValidateSettings(b *Engine, s *Settings) { + flag.Visit(func(f *flag.Flag) { flagSet[f.Name] = true }) + + b.Settings.Verbose = s.Verbose + b.Settings.EnableDryRun = s.EnableDryRun + b.Settings.EnableAllExchanges = s.EnableAllExchanges + b.Settings.EnableAllPairs = s.EnableAllPairs + b.Settings.EnablePortfolioManager = s.EnablePortfolioManager + b.Settings.EnableCoinmarketcapAnalysis = s.EnableCoinmarketcapAnalysis + b.Settings.EnableDatabaseManager = s.EnableDatabaseManager + b.Settings.EnableDispatcher = s.EnableDispatcher + + if flagSet["grpc"] { + b.Settings.EnableGRPC = s.EnableGRPC + } else { + b.Settings.EnableGRPC = b.Config.RemoteControl.GRPC.Enabled + } + + if flagSet["grpcproxy"] { + b.Settings.EnableGRPCProxy = s.EnableGRPCProxy + } else { + b.Settings.EnableGRPCProxy = b.Config.RemoteControl.GRPC.GRPCProxyEnabled + } + + if flagSet["websocketrpc"] { + b.Settings.EnableWebsocketRPC = s.EnableWebsocketRPC + } else { + b.Settings.EnableWebsocketRPC = b.Config.RemoteControl.WebsocketRPC.Enabled + } + + if flagSet["deprecatedrpc"] { + b.Settings.EnableDeprecatedRPC = s.EnableDeprecatedRPC + } else { + b.Settings.EnableDeprecatedRPC = b.Config.RemoteControl.DeprecatedRPC.Enabled + } + + b.Settings.EnableCommsRelayer = s.EnableCommsRelayer + b.Settings.EnableEventManager = s.EnableEventManager + + if b.Settings.EnableEventManager { + if b.Settings.EventManagerDelay != time.Duration(0) && s.EventManagerDelay > 0 { + b.Settings.EventManagerDelay = s.EventManagerDelay + } else { + b.Settings.EventManagerDelay = EventSleepDelay + } + } + + b.Settings.EnableConnectivityMonitor = s.EnableConnectivityMonitor + b.Settings.EnableNTPClient = s.EnableNTPClient + b.Settings.EnableOrderManager = s.EnableOrderManager + b.Settings.EnableExchangeSyncManager = s.EnableExchangeSyncManager + b.Settings.EnableTickerSyncing = s.EnableTickerSyncing + b.Settings.EnableOrderbookSyncing = s.EnableOrderbookSyncing + b.Settings.EnableTradeSyncing = s.EnableTradeSyncing + b.Settings.SyncWorkers = s.SyncWorkers + b.Settings.SyncTimeout = s.SyncTimeout + b.Settings.SyncContinuously = s.SyncContinuously + b.Settings.EnableDepositAddressManager = s.EnableDepositAddressManager + b.Settings.EnableExchangeAutoPairUpdates = s.EnableExchangeAutoPairUpdates + b.Settings.EnableExchangeWebsocketSupport = s.EnableExchangeWebsocketSupport + b.Settings.EnableExchangeRESTSupport = s.EnableExchangeRESTSupport + b.Settings.EnableExchangeVerbose = s.EnableExchangeVerbose + b.Settings.EnableExchangeHTTPRateLimiter = s.EnableExchangeHTTPDebugging + b.Settings.EnableExchangeHTTPDebugging = s.EnableExchangeHTTPDebugging + b.Settings.DisableExchangeAutoPairUpdates = s.DisableExchangeAutoPairUpdates + b.Settings.ExchangePurgeCredentials = s.ExchangePurgeCredentials + b.Settings.EnableWebsocketRoutine = s.EnableWebsocketRoutine + + if !b.Settings.EnableExchangeHTTPRateLimiter { + request.DisableRateLimiter = true + } + + // Checks if the flag values are different from the defaults + b.Settings.MaxHTTPRequestJobsLimit = s.MaxHTTPRequestJobsLimit + if b.Settings.MaxHTTPRequestJobsLimit != request.DefaultMaxRequestJobs && s.MaxHTTPRequestJobsLimit > 0 { + request.MaxRequestJobs = b.Settings.MaxHTTPRequestJobsLimit + } + + b.Settings.RequestTimeoutRetryAttempts = s.RequestTimeoutRetryAttempts + if b.Settings.RequestTimeoutRetryAttempts != request.DefaultTimeoutRetryAttempts && s.RequestTimeoutRetryAttempts > 0 { + request.TimeoutRetryAttempts = b.Settings.RequestTimeoutRetryAttempts + } + + b.Settings.ExchangeHTTPTimeout = s.ExchangeHTTPTimeout + if s.ExchangeHTTPTimeout != time.Duration(0) && s.ExchangeHTTPTimeout > 0 { + b.Settings.ExchangeHTTPTimeout = s.ExchangeHTTPTimeout + } else { + b.Settings.ExchangeHTTPTimeout = b.Config.GlobalHTTPTimeout + } + + b.Settings.ExchangeHTTPUserAgent = s.ExchangeHTTPUserAgent + b.Settings.ExchangeHTTPProxy = s.ExchangeHTTPProxy + + if s.GlobalHTTPTimeout != time.Duration(0) && s.GlobalHTTPTimeout > 0 { + b.Settings.GlobalHTTPTimeout = s.GlobalHTTPTimeout + } else { + b.Settings.GlobalHTTPTimeout = b.Config.GlobalHTTPTimeout + } + common.HTTPClient = common.NewHTTPClientWithTimeout(b.Settings.GlobalHTTPTimeout) + + b.Settings.GlobalHTTPUserAgent = s.GlobalHTTPUserAgent + if b.Settings.GlobalHTTPUserAgent != "" { + common.HTTPUserAgent = b.Settings.GlobalHTTPUserAgent + } + + b.Settings.GlobalHTTPProxy = s.GlobalHTTPProxy + b.Settings.DispatchMaxWorkerAmount = s.DispatchMaxWorkerAmount + b.Settings.DispatchJobsLimit = s.DispatchJobsLimit +} + +// PrintSettings returns the engine settings +func PrintSettings(s *Settings) { + log.Debugln(log.Global) + log.Debugf(log.Global, "ENGINE SETTINGS") + log.Debugf(log.Global, "- CORE SETTINGS:") + log.Debugf(log.Global, "\t Verbose mode: %v", s.Verbose) + log.Debugf(log.Global, "\t Enable dry run mode: %v", s.EnableDryRun) + log.Debugf(log.Global, "\t Enable all exchanges: %v", s.EnableAllExchanges) + log.Debugf(log.Global, "\t Enable all pairs: %v", s.EnableAllPairs) + log.Debugf(log.Global, "\t Enable coinmarketcap analaysis: %v", s.EnableCoinmarketcapAnalysis) + log.Debugf(log.Global, "\t Enable portfolio manager: %v", s.EnablePortfolioManager) + log.Debugf(log.Global, "\t Enable gPRC: %v", s.EnableGRPC) + log.Debugf(log.Global, "\t Enable gRPC Proxy: %v", s.EnableGRPCProxy) + log.Debugf(log.Global, "\t Enable websocket RPC: %v", s.EnableWebsocketRPC) + log.Debugf(log.Global, "\t Enable deprecated RPC: %v", s.EnableDeprecatedRPC) + log.Debugf(log.Global, "\t Enable comms relayer: %v", s.EnableCommsRelayer) + log.Debugf(log.Global, "\t Enable event manager: %v", s.EnableEventManager) + log.Debugf(log.Global, "\t Event manager sleep delay: %v", s.EventManagerDelay) + log.Debugf(log.Global, "\t Enable order manager: %v", s.EnableOrderManager) + log.Debugf(log.Global, "\t Enable exchange sync manager: %v", s.EnableExchangeSyncManager) + log.Debugf(log.Global, "\t Enable deposit address manager: %v\n", s.EnableDepositAddressManager) + log.Debugf(log.Global, "\t Enable websocket routine: %v\n", s.EnableWebsocketRoutine) + log.Debugf(log.Global, "\t Enable NTP client: %v", s.EnableNTPClient) + log.Debugf(log.Global, "\t Enable Database manager: %v", s.EnableDatabaseManager) + log.Debugf(log.Global, "\t Enable dispatcher: %v", s.EnableDispatcher) + log.Debugf(log.Global, "\t Dispatch package max worker amount: %d", s.DispatchMaxWorkerAmount) + log.Debugf(log.Global, "\t Dispatch package jobs limit: %d", s.DispatchJobsLimit) + log.Debugf(log.Global, "- EXCHANGE SYNCER SETTINGS:\n") + log.Debugf(log.Global, "\t Exchange sync continuously: %v\n", s.SyncContinuously) + log.Debugf(log.Global, "\t Exchange sync workers: %v\n", s.SyncWorkers) + log.Debugf(log.Global, "\t Enable ticker syncing: %v\n", s.EnableTickerSyncing) + log.Debugf(log.Global, "\t Enable orderbook syncing: %v\n", s.EnableOrderbookSyncing) + log.Debugf(log.Global, "\t Enable trade syncing: %v\n", s.EnableTradeSyncing) + log.Debugf(log.Global, "\t Exchange sync timeout: %v\n", s.SyncTimeout) + log.Debugf(log.Global, "- FOREX SETTINGS:") + log.Debugf(log.Global, "\t Enable currency conveter: %v", s.EnableCurrencyConverter) + log.Debugf(log.Global, "\t Enable currency layer: %v", s.EnableCurrencyLayer) + log.Debugf(log.Global, "\t Enable fixer: %v", s.EnableFixer) + log.Debugf(log.Global, "\t Enable OpenExchangeRates: %v", s.EnableOpenExchangeRates) + log.Debugf(log.Global, "- EXCHANGE SETTINGS:") + log.Debugf(log.Global, "\t Enable exchange auto pair updates: %v", s.EnableExchangeAutoPairUpdates) + log.Debugf(log.Global, "\t Disable all exchange auto pair updates: %v", s.DisableExchangeAutoPairUpdates) + log.Debugf(log.Global, "\t Enable exchange websocket support: %v", s.EnableExchangeWebsocketSupport) + log.Debugf(log.Global, "\t Enable exchange verbose mode: %v", s.EnableExchangeVerbose) + log.Debugf(log.Global, "\t Enable exchange HTTP rate limiter: %v", s.EnableExchangeHTTPRateLimiter) + log.Debugf(log.Global, "\t Enable exchange HTTP debugging: %v", s.EnableExchangeHTTPDebugging) + log.Debugf(log.Global, "\t Exchange max HTTP request jobs: %v", s.MaxHTTPRequestJobsLimit) + log.Debugf(log.Global, "\t Exchange HTTP request timeout retry amount: %v", s.RequestTimeoutRetryAttempts) + log.Debugf(log.Global, "\t Exchange HTTP timeout: %v", s.ExchangeHTTPTimeout) + log.Debugf(log.Global, "\t Exchange HTTP user agent: %v", s.ExchangeHTTPUserAgent) + log.Debugf(log.Global, "\t Exchange HTTP proxy: %v\n", s.ExchangeHTTPProxy) + log.Debugf(log.Global, "- COMMON SETTINGS:") + log.Debugf(log.Global, "\t Global HTTP timeout: %v", s.GlobalHTTPTimeout) + log.Debugf(log.Global, "\t Global HTTP user agent: %v", s.GlobalHTTPUserAgent) + log.Debugf(log.Global, "\t Global HTTP proxy: %v", s.ExchangeHTTPProxy) + log.Debugln(log.Global) +} + +// Start starts the engine +func (e *Engine) Start() error { + if e == nil { + return errors.New("engine instance is nil") + } + + if e.Settings.EnableDatabaseManager { + if err := e.DatabaseManager.Start(); err != nil { + log.Errorf(log.Global, "Database manager unable to start: %v", err) + } + } + + if e.Settings.EnableDispatcher { + if err := dispatch.Start(e.Settings.DispatchMaxWorkerAmount, e.Settings.DispatchJobsLimit); err != nil { + log.Errorf(log.DispatchMgr, "Dispatcher unable to start: %v", err) + } + } + + // Sets up internet connectivity monitor + if e.Settings.EnableConnectivityMonitor { + if err := e.ConnectionManager.Start(); err != nil { + log.Errorf(log.Global, "Connection manager unable to start: %v", err) + } + } + + if e.Settings.EnableNTPClient { + if err := e.NTPManager.Start(); err != nil { + log.Errorf(log.Global, "NTP manager unable to start: %v", err) + } + } + + e.Uptime = time.Now() + log.Debugf(log.Global, "Bot '%s' started.\n", e.Config.Name) + log.Debugf(log.Global, "Using data dir: %s\n", e.Settings.DataDir) + if *e.Config.Logging.Enabled && strings.Contains(e.Config.Logging.Output, "file") { + log.Debugf(log.Global, "Using log file: %s\n", + filepath.Join(log.LogPath, e.Config.Logging.LoggerFileConfig.FileName)) + } + log.Debugf(log.Global, + "Using %d out of %d logical processors for runtime performance\n", + runtime.GOMAXPROCS(-1), runtime.NumCPU()) + + enabledExchanges := e.Config.CountEnabledExchanges() + if e.Settings.EnableAllExchanges { + enabledExchanges = len(e.Config.Exchanges) + } + + log.Debugln(log.Global, "EXCHANGE COVERAGE") + log.Debugf(log.Global, "\t Available Exchanges: %d. Enabled Exchanges: %d.\n", + len(e.Config.Exchanges), enabledExchanges) + + if e.Settings.ExchangePurgeCredentials { + log.Debugln(log.Global, "Purging exchange API credentials.") + e.Config.PurgeExchangeAPICredentials() + } + + log.Debugln(log.Global, "Setting up exchanges..") + SetupExchanges() + if len(Bot.Exchanges) == 0 { + return errors.New("no exchanges are loaded") + } + + if e.Settings.EnableCommsRelayer { + if err := e.CommsManager.Start(); err != nil { + log.Errorf(log.Global, "Communications manager unable to start: %v\n", err) + } + } + + var newFxSettings []currency.FXSettings + for _, d := range e.Config.Currency.ForexProviders { + newFxSettings = append(newFxSettings, currency.FXSettings(d)) + } + + err := currency.RunStorageUpdater(currency.BotOverrides{ + Coinmarketcap: e.Settings.EnableCoinmarketcapAnalysis, + FxCurrencyConverter: e.Settings.EnableCurrencyConverter, + FxCurrencyLayer: e.Settings.EnableCurrencyLayer, + FxFixer: e.Settings.EnableFixer, + FxOpenExchangeRates: e.Settings.EnableOpenExchangeRates, + }, + ¤cy.MainConfiguration{ + ForexProviders: newFxSettings, + CryptocurrencyProvider: coinmarketcap.Settings(e.Config.Currency.CryptocurrencyProvider), + Cryptocurrencies: e.Config.Currency.Cryptocurrencies, + FiatDisplayCurrency: e.Config.Currency.FiatDisplayCurrency, + CurrencyDelay: e.Config.Currency.CurrencyFileUpdateDuration, + FxRateDelay: e.Config.Currency.ForeignExchangeUpdateDuration, + }, + e.Settings.DataDir, + e.Settings.Verbose) + if err != nil { + log.Errorf(log.Global, "currency updater system failed to start %v", err) + } + + if e.Settings.EnableGRPC { + go StartRPCServer() + } + + if e.Settings.EnableDeprecatedRPC { + go StartRESTServer() + } + + if e.Settings.EnableWebsocketRPC { + go StartWebsocketServer() + StartWebsocketHandler() + } + + if e.Settings.EnablePortfolioManager { + if err = e.PortfolioManager.Start(); err != nil { + log.Errorf(log.Global, "Fund manager unable to start: %v", err) + } + } + + if e.Settings.EnableDepositAddressManager { + e.DepositAddressManager = new(DepositAddressManager) + e.DepositAddressManager.Sync() + } + + if e.Settings.EnableOrderManager { + if err = e.OrderManager.Start(); err != nil { + log.Errorf(log.Global, "Order manager unable to start: %v", err) + } + } + + if e.Settings.EnableExchangeSyncManager { + exchangeSyncCfg := CurrencyPairSyncerConfig{ + SyncTicker: e.Settings.EnableTickerSyncing, + SyncOrderbook: e.Settings.EnableOrderbookSyncing, + SyncTrades: e.Settings.EnableTradeSyncing, + SyncContinuously: e.Settings.SyncContinuously, + NumWorkers: e.Settings.SyncWorkers, + Verbose: e.Settings.Verbose, + } + + e.ExchangeCurrencyPairManager, err = NewCurrencyPairSyncer(exchangeSyncCfg) + if err != nil { + log.Warnf(log.Global, "Unable to initialise exchange currency pair syncer. Err: %s", err) + } else { + go e.ExchangeCurrencyPairManager.Start() + } + } + + if e.Settings.EnableEventManager { + go EventManger() + } + + if e.Settings.EnableWebsocketRoutine { + go WebsocketRoutine() + } + + return nil +} + +// Stop correctly shuts down engine saving configuration files +func (e *Engine) Stop() { + log.Debugln(log.Global, "Engine shutting down..") + + if len(portfolio.Portfolio.Addresses) != 0 { + e.Config.Portfolio = portfolio.Portfolio + } + + if e.OrderManager.Started() { + if err := e.OrderManager.Stop(); err != nil { + log.Errorf(log.Global, "Order manager unable to stop. Error: %v", err) + } + } + + if e.NTPManager.Started() { + if err := e.NTPManager.Stop(); err != nil { + log.Errorf(log.Global, "NTP manager unable to stop. Error: %v", err) + } + } + + if e.CommsManager.Started() { + if err := e.CommsManager.Stop(); err != nil { + log.Errorf(log.Global, "Communication manager unable to stop. Error: %v", err) + } + } + + if e.PortfolioManager.Started() { + if err := e.PortfolioManager.Stop(); err != nil { + log.Errorf(log.Global, "Fund manager unable to stop. Error: %v", err) + } + } + + if e.ConnectionManager.Started() { + if err := e.ConnectionManager.Stop(); err != nil { + log.Errorf(log.Global, "Connection manager unable to stop. Error: %v", err) + } + } + + if e.DatabaseManager.Started() { + if err := e.DatabaseManager.Stop(); err != nil { + log.Errorf(log.Global, "Database manager unable to stop. Error: %v", err) + } + } + + if dispatch.IsRunning() { + if err := dispatch.Stop(); err != nil { + log.Errorf(log.DispatchMgr, "Dispatch system unable to stop. Error: %v", err) + } + } + + if !e.Settings.EnableDryRun { + err := e.Config.SaveConfig(e.Settings.ConfigFile, false) + if err != nil { + log.Errorln(log.Global, "Unable to save config.") + } else { + log.Debugln(log.Global, "Config file saved successfully.") + } + } + + // Wait for services to gracefully shutdown + e.ServicesWG.Wait() + err := log.CloseLogger() + if err != nil { + fmt.Printf("Failed to close logger %v", err) + } +} diff --git a/engine/engine_types.go b/engine/engine_types.go new file mode 100644 index 00000000..9f0bbc95 --- /dev/null +++ b/engine/engine_types.go @@ -0,0 +1,76 @@ +package engine + +import "time" + +// Settings stores engine params +type Settings struct { + ConfigFile string + DataDir string + MigrationDir string + LogFile string + GoMaxProcs int + CheckParamInteraction bool + + // Core Settings + EnableDryRun bool + EnableAllExchanges bool + EnableAllPairs bool + EnableCoinmarketcapAnalysis bool + EnablePortfolioManager bool + EnableGRPC bool + EnableGRPCProxy bool + EnableWebsocketRPC bool + EnableDeprecatedRPC bool + EnableCommsRelayer bool + EnableExchangeSyncManager bool + EnableDepositAddressManager bool + EnableEventManager bool + EnableOrderManager bool + EnableConnectivityMonitor bool + EnableDatabaseManager bool + EnableNTPClient bool + EnableWebsocketRoutine bool + EventManagerDelay time.Duration + Verbose bool + + // Exchange syncer settings + EnableTickerSyncing bool + EnableOrderbookSyncing bool + EnableTradeSyncing bool + SyncWorkers int + SyncContinuously bool + SyncTimeout time.Duration + + // Forex settings + EnableCurrencyConverter bool + EnableCurrencyLayer bool + EnableFixer bool + EnableOpenExchangeRates bool + + // Exchange tuning settings + EnableExchangeHTTPRateLimiter bool + EnableExchangeHTTPDebugging bool + EnableExchangeVerbose bool + ExchangePurgeCredentials bool + EnableExchangeAutoPairUpdates bool + DisableExchangeAutoPairUpdates bool + EnableExchangeRESTSupport bool + EnableExchangeWebsocketSupport bool + MaxHTTPRequestJobsLimit int + RequestTimeoutRetryAttempts int + + // Global HTTP related settings + GlobalHTTPTimeout time.Duration + GlobalHTTPUserAgent string + GlobalHTTPProxy string + + // Exchange HTTP related settings + ExchangeHTTPTimeout time.Duration + ExchangeHTTPUserAgent string + ExchangeHTTPProxy string + + // Dispatch system settings + EnableDispatcher bool + DispatchMaxWorkerAmount int + DispatchJobsLimit int +} diff --git a/engine/events.go b/engine/events.go new file mode 100644 index 00000000..bf87a4c3 --- /dev/null +++ b/engine/events.go @@ -0,0 +1,349 @@ +package engine + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/thrasher-corp/gocryptotrader/communications/base" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// TO-DO MAKE THIS A SERVICE SUBSYSTEM + +// Event const vars +const ( + ItemPrice = "PRICE" + ItemOrderbook = "ORDERBOOK" + + ConditionGreaterThan = ">" + ConditionGreaterThanOrEqual = ">=" + ConditionLessThan = "<" + ConditionLessThanOrEqual = "<=" + ConditionIsEqual = "==" + + ActionSMSNotify = "SMS" + ActionConsolePrint = "CONSOLE_PRINT" + ActionTest = "ACTION_TEST" + + defaultSleepDelay = time.Millisecond * 500 +) + +// vars related to events package +var ( + errInvalidItem = errors.New("invalid item") + errInvalidCondition = errors.New("invalid conditional option") + errInvalidAction = errors.New("invalid action") + errExchangeDisabled = errors.New("desired exchange is disabled") + EventSleepDelay = defaultSleepDelay +) + +// EventConditionParams holds the event condition variables +type EventConditionParams struct { + Condition string + Price float64 + + CheckBids bool + CheckBidsAndAsks bool + OrderbookAmount float64 +} + +// Event struct holds the event variables +type Event struct { + ID int64 + Exchange string + Item string + Condition EventConditionParams + Pair currency.Pair + Asset asset.Item + Action string + Executed bool +} + +// Events variable is a pointer array to the event structures that will be +// appended +var Events []*Event + +// Add adds an event to the Events chain and returns an index/eventID +// and an error +func Add(exchange, item string, condition EventConditionParams, currencyPair currency.Pair, asset asset.Item, action string) (int64, error) { + err := IsValidEvent(exchange, item, condition, action) + if err != nil { + return 0, err + } + + evt := &Event{} + + if len(Events) == 0 { + evt.ID = 0 + } else { + evt.ID = int64(len(Events) + 1) + } + + evt.Exchange = exchange + evt.Item = item + evt.Condition = condition + evt.Pair = currencyPair + evt.Asset = asset + evt.Action = action + evt.Executed = false + Events = append(Events, evt) + return evt.ID, nil +} + +// Remove deletes and event by its ID +func Remove(eventID int64) bool { + for i, x := range Events { + if x.ID == eventID { + Events = append(Events[:i], Events[i+1:]...) + return true + } + } + return false +} + +// GetEventCounter displays the emount of total events on the chain and the +// events that have been executed. +func GetEventCounter() (total, executed int) { + total = len(Events) + + for _, x := range Events { + if x.Executed { + executed++ + } + } + return total, executed +} + +// ExecuteAction will execute the action pending on the chain +func (e *Event) ExecuteAction() bool { + if strings.Contains(e.Action, ",") { + action := strings.Split(e.Action, ",") + if action[0] == ActionSMSNotify { + message := fmt.Sprintf("Event triggered: %s\n", e.String()) + if action[1] == "ALL" { + Bot.CommsManager.PushEvent(base.Event{ + Type: "event", + Message: message, + }) + } + } + } else { + log.Debugf(log.EventMgr, "Event triggered: %s\n", e.String()) + } + return true +} + +// String turns the structure event into a string +func (e *Event) String() string { + return fmt.Sprintf( + "If the %s [%s] %s on %s meets the following %v then %s.", e.Pair.String(), + strings.ToUpper(e.Asset.String()), e.Item, e.Exchange, e.Condition, e.Action, + ) +} + +func (e *Event) processTicker() bool { + t, err := ticker.GetTicker(e.Exchange, e.Pair, e.Asset) + if err != nil { + if Bot.Settings.Verbose { + log.Debugf(log.EventMgr, "Events: failed to get ticker. Err: %s\n", err) + } + return false + } + + if t.Last == 0 { + if Bot.Settings.Verbose { + log.Debugln(log.EventMgr, "Events: ticker last price is 0") + } + return false + } + return e.processCondition(t.Last, e.Condition.Price) +} + +func (e *Event) processCondition(actual, threshold float64) bool { + switch e.Condition.Condition { + case ConditionGreaterThan: + if actual > threshold { + return e.ExecuteAction() + } + case ConditionGreaterThanOrEqual: + if actual >= threshold { + return e.ExecuteAction() + } + case ConditionLessThan: + if actual < threshold { + return e.ExecuteAction() + } + case ConditionLessThanOrEqual: + if actual <= threshold { + return e.ExecuteAction() + } + case ConditionIsEqual: + if actual == threshold { + return e.ExecuteAction() + } + } + return false +} + +func (e *Event) processOrderbook() bool { + ob, err := orderbook.Get(e.Exchange, e.Pair, e.Asset) + if err != nil { + if Bot.Settings.Verbose { + log.Debugf(log.EventMgr, "Events: Failed to get orderbook. Err: %s\n", err) + } + return false + } + + success := false + if e.Condition.CheckBids || e.Condition.CheckBidsAndAsks { + for x := range ob.Bids { + subtotal := ob.Bids[x].Amount * ob.Bids[x].Price + result := e.processCondition(subtotal, e.Condition.OrderbookAmount) + if result { + success = true + log.Debugf(log.EventMgr, "Events: Bid Amount: %f Price: %v Subtotal: %v\n", ob.Bids[x].Amount, ob.Bids[x].Price, subtotal) + } + } + } + + if !e.Condition.CheckBids || e.Condition.CheckBidsAndAsks { + for x := range ob.Asks { + subtotal := ob.Asks[x].Amount * ob.Asks[x].Price + result := e.processCondition(subtotal, e.Condition.OrderbookAmount) + if result { + success = true + log.Debugf(log.EventMgr, "Events: Ask Amount: %f Price: %v Subtotal: %v\n", ob.Asks[x].Amount, ob.Asks[x].Price, subtotal) + } + } + } + return success +} + +// CheckEventCondition will check the event structure to see if there is a condition +// met +func (e *Event) CheckEventCondition() bool { + if e.Item == ItemPrice { + return e.processTicker() + } + return e.processOrderbook() +} + +// IsValidEvent checks the actions to be taken and returns an error if incorrect +func IsValidEvent(exchange, item string, condition EventConditionParams, action string) error { + exchange = strings.ToUpper(exchange) + item = strings.ToUpper(item) + action = strings.ToUpper(action) + + if !IsValidExchange(exchange) { + return errExchangeDisabled + } + + if !IsValidItem(item) { + return errInvalidItem + } + + if !IsValidCondition(condition.Condition) { + return errInvalidCondition + } + + if item == ItemPrice { + if condition.Price <= 0 { + return errInvalidCondition + } + } + + if item == ItemOrderbook { + if condition.OrderbookAmount <= 0 { + return errInvalidCondition + } + } + + if strings.Contains(action, ",") { + a := strings.Split(action, ",") + + if a[0] != ActionSMSNotify { + return errInvalidAction + } + } else if action != ActionConsolePrint && action != ActionTest { + return errInvalidAction + } + + return nil +} + +// EventManger is the overarching routine that will iterate through the Events +// chain +func EventManger() { + log.Debugf(log.EventMgr, "EventManager started. SleepDelay: %v\n", EventSleepDelay.String()) + + for { + total, executed := GetEventCounter() + if total > 0 && executed != total { + for _, event := range Events { + if !event.Executed { + if Bot.Settings.Verbose { + log.Debugf(log.EventMgr, "Events: Processing event %s.\n", event.String()) + } + success := event.CheckEventCondition() + if success { + msg := fmt.Sprintf( + "Events: ID: %d triggered on %s successfully [%v]\n", event.ID, + event.Exchange, event.String(), + ) + log.Infoln(log.EventMgr, msg) + Bot.CommsManager.PushEvent(base.Event{Type: "event", Message: msg}) + event.Executed = true + } + } + } + } + time.Sleep(EventSleepDelay) + } +} + +// IsValidExchange validates the exchange +func IsValidExchange(exchangeName string) bool { + cfg := config.GetConfig() + for x := range cfg.Exchanges { + if strings.EqualFold(cfg.Exchanges[x].Name, exchangeName) && cfg.Exchanges[x].Enabled { + return true + } + } + return false +} + +// IsValidCondition validates passed in condition +func IsValidCondition(condition string) bool { + switch condition { + case ConditionGreaterThan, ConditionGreaterThanOrEqual, ConditionLessThan, ConditionLessThanOrEqual, ConditionIsEqual: + return true + } + return false +} + +// IsValidAction validates passed in action +func IsValidAction(action string) bool { + action = strings.ToUpper(action) + switch action { + case ActionSMSNotify, ActionConsolePrint, ActionTest: + return true + } + return false +} + +// IsValidItem validates passed in Item +func IsValidItem(item string) bool { + item = strings.ToUpper(item) + switch item { + case ItemPrice, ItemOrderbook: + return true + } + return false +} diff --git a/engine/events_test.go b/engine/events_test.go new file mode 100644 index 00000000..cc771af4 --- /dev/null +++ b/engine/events_test.go @@ -0,0 +1,346 @@ +package engine + +import ( + "testing" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" +) + +const ( + testExchange = "Bitstamp" +) + +var ( + configLoaded = false +) + +func addValidEvent() (int64, error) { + return Add(testExchange, + ItemPrice, + EventConditionParams{Condition: ConditionGreaterThan, Price: 1}, + currency.NewPair(currency.BTC, currency.USD), + asset.Spot, + "SMS,test") +} + +func TestAdd(t *testing.T) { + if !configLoaded { + loadConfig(t) + } + + _, err := Add("", "", EventConditionParams{}, currency.Pair{}, "", "") + if err == nil { + t.Error("should err on invalid params") + } + + _, err = addValidEvent() + if err != nil { + t.Error("unexpected result", err) + } + + _, err = addValidEvent() + if err != nil { + t.Error("unexpected result", err) + } + + if len(Events) != 2 { + t.Error("2 events should be stored") + } +} + +func TestRemove(t *testing.T) { + if !configLoaded { + loadConfig(t) + } + + id, err := addValidEvent() + if err != nil { + t.Error("unexpected result", err) + } + + if s := Remove(id); !s { + t.Error("unexpected result") + } + + if s := Remove(id); s { + t.Error("unexpected result") + } +} + +func TestGetEventCounter(t *testing.T) { + if !configLoaded { + loadConfig(t) + } + + _, err := addValidEvent() + if err != nil { + t.Error("unexpected result", err) + } + + n, e := GetEventCounter() + if n == 0 || e > 0 { + t.Error("unexpected result") + } + + Events[0].Executed = true + n, e = GetEventCounter() + if n == 0 || e == 0 { + t.Error("unexpected result") + } +} + +func TestExecuteAction(t *testing.T) { + t.Parallel() + if Bot == nil { + Bot = new(Engine) + } + + var e Event + if r := e.ExecuteAction(); !r { + t.Error("unexpected result") + } + + e.Action = "SMS,test" + if r := e.ExecuteAction(); !r { + t.Error("unexpected result") + } + + e.Action = "SMS,ALL" + if r := e.ExecuteAction(); !r { + t.Error("unexpected result") + } +} + +func TestString(t *testing.T) { + t.Parallel() + e := Event{ + Exchange: testExchange, + Item: ItemPrice, + Condition: EventConditionParams{ + Condition: ConditionGreaterThan, + Price: 1, + }, + Pair: currency.NewPair(currency.BTC, currency.USD), + Asset: asset.Spot, + Action: "SMS,ALL", + } + + if r := e.String(); r != "If the BTCUSD [SPOT] PRICE on Bitstamp meets the following {> 1 false false 0} then SMS,ALL." { + t.Error("unexpected result") + } +} + +func TestProcessTicker(t *testing.T) { + if Bot == nil { + Bot = new(Engine) + } + Bot.Settings.Verbose = true + + e := Event{ + Exchange: testExchange, + Pair: currency.NewPair(currency.BTC, currency.USD), + Asset: asset.Spot, + Condition: EventConditionParams{ + Condition: ConditionGreaterThan, + Price: 1, + }, + } + + // now populate it with a 0 entry + tick := ticker.Price{ + Pair: currency.NewPair(currency.BTC, currency.USD), + Last: 0, + } + if err := ticker.ProcessTicker(e.Exchange, &tick, e.Asset); err != nil { + t.Fatal("unexpected result:", err) + } + if r := e.processTicker(); r { + t.Error("unexpected result") + } + + // now populate it with a number > 0 + tick.Last = 1337 + if err := ticker.ProcessTicker(e.Exchange, &tick, e.Asset); err != nil { + t.Fatal("unexpected result:", err) + } + if r := e.processTicker(); !r { + t.Error("unexpected result") + } +} + +func TestProcessCondition(t *testing.T) { + t.Parallel() + var e Event + tester := []struct { + Condition string + Actual float64 + Threshold float64 + ExpectedResult bool + }{ + {ConditionGreaterThan, 1, 2, false}, + {ConditionGreaterThan, 2, 1, true}, + {ConditionGreaterThanOrEqual, 1, 2, false}, + {ConditionGreaterThanOrEqual, 2, 1, true}, + {ConditionIsEqual, 1, 1, true}, + {ConditionIsEqual, 1, 2, false}, + {ConditionLessThan, 1, 2, true}, + {ConditionLessThan, 2, 1, false}, + {ConditionLessThanOrEqual, 1, 2, true}, + {ConditionLessThanOrEqual, 2, 1, false}, + } + for x := range tester { + e.Condition.Condition = tester[x].Condition + if r := e.processCondition(tester[x].Actual, tester[x].Threshold); r != tester[x].ExpectedResult { + t.Error("unexpected result") + } + } +} + +func TestProcessOrderbook(t *testing.T) { + if Bot == nil { + Bot = new(Engine) + } + Bot.Settings.Verbose = true + + e := Event{ + Exchange: testExchange, + Pair: currency.NewPair(currency.BTC, currency.USD), + Asset: asset.Spot, + Condition: EventConditionParams{ + Condition: ConditionGreaterThan, + CheckBidsAndAsks: true, + OrderbookAmount: 100, + }, + } + + // now populate it with a 0 entry + o := orderbook.Base{ + Pair: currency.NewPair(currency.BTC, currency.USD), + Bids: []orderbook.Item{{Amount: 24, Price: 23}}, + Asks: []orderbook.Item{{Amount: 24, Price: 23}}, + ExchangeName: e.Exchange, + AssetType: e.Asset, + } + if err := o.Process(); err != nil { + t.Fatal("unexpected result:", err) + } + + if r := e.processOrderbook(); !r { + t.Error("unexpected result") + } +} + +func TestCheckEventCondition(t *testing.T) { + t.Parallel() + if Bot == nil { + Bot = new(Engine) + } + Bot.Settings.Verbose = true + + e := Event{ + Item: ItemPrice, + } + if r := e.CheckEventCondition(); r { + t.Error("unexpected result") + } + + e.Item = ItemOrderbook + if r := e.CheckEventCondition(); r { + t.Error("unexpected result") + } +} + +func TestIsValidEvent(t *testing.T) { + if !configLoaded { + loadConfig(t) + } + + // invalid exchange name + if err := IsValidEvent("meow", "", EventConditionParams{}, ""); err != errExchangeDisabled { + t.Error("unexpected result:", err) + } + + // invalid item + if err := IsValidEvent(testExchange, "", EventConditionParams{}, ""); err != errInvalidItem { + t.Error("unexpected result:", err) + } + + // invalid condition + if err := IsValidEvent(testExchange, ItemPrice, EventConditionParams{}, ""); err != errInvalidCondition { + t.Error("unexpected result:", err) + } + + // valid condition but empty price which will still throw an errInvalidCondition + c := EventConditionParams{ + Condition: ConditionGreaterThan, + } + if err := IsValidEvent(testExchange, ItemPrice, c, ""); err != errInvalidCondition { + t.Error("unexpected result:", err) + } + + // valid condition but empty orderbook amount will still still throw an errInvalidCondition + if err := IsValidEvent(testExchange, ItemOrderbook, c, ""); err != errInvalidCondition { + t.Error("unexpected result:", err) + } + + // test action splitting, but invalid + c.OrderbookAmount = 1337 + if err := IsValidEvent(testExchange, ItemOrderbook, c, "a,meow"); err != errInvalidAction { + t.Error("unexpected result:", err) + } + + // check for invalid action without splitting + if err := IsValidEvent(testExchange, ItemOrderbook, c, "hi"); err != errInvalidAction { + t.Error("unexpected result:", err) + } + + // valid event + if err := IsValidEvent(testExchange, ItemOrderbook, c, "SMS,test"); err != nil { + t.Error("unexpected result:", err) + } +} + +func TestIsValidExchange(t *testing.T) { + t.Parallel() + if s := IsValidExchange("invalidexchangerino"); s { + t.Error("unexpected result") + } + + loadConfig(t) + if s := IsValidExchange(testExchange); !s { + t.Error("unexpected result") + } +} + +func TestIsValidCondition(t *testing.T) { + t.Parallel() + if s := IsValidCondition("invalidconditionerino"); s { + t.Error("unexpected result") + } + if s := IsValidCondition(ConditionGreaterThan); !s { + t.Error("unexpected result") + } +} + +func TestIsValidAction(t *testing.T) { + t.Parallel() + if s := IsValidAction("invalidactionerino"); s { + t.Error("unexpected result") + } + if s := IsValidAction(ActionSMSNotify); !s { + t.Error("unexpected result") + } +} + +func TestIsValidItem(t *testing.T) { + t.Parallel() + if s := IsValidItem("invaliditemerino"); s { + t.Error("unexpected result") + } + if s := IsValidItem(ItemPrice); !s { + t.Error("unexpected result") + } +} diff --git a/exchange.go b/engine/exchange.go similarity index 59% rename from exchange.go rename to engine/exchange.go index 7e9b0185..1cc5141a 100644 --- a/exchange.go +++ b/engine/exchange.go @@ -1,4 +1,4 @@ -package main +package engine import ( "errors" @@ -46,11 +46,25 @@ var ( ErrExchangeFailedToLoad = errors.New("exchange failed to load") ) +func dryrunParamInteraction(param string) { + if !Bot.Settings.CheckParamInteraction { + return + } + + if !Bot.Settings.EnableDryRun && !flagSet["dryrun"] { + log.Warnf(log.Global, + "Command line argument '-%s' induces dry run mode."+ + " Set -dryrun=false if you wish to override this.", + param) + Bot.Settings.EnableDryRun = true + } +} + // CheckExchangeExists returns true whether or not an exchange has already // been loaded func CheckExchangeExists(exchName string) bool { - for x := range bot.exchanges { - if strings.EqualFold(bot.exchanges[x].GetName(), exchName) { + for x := range Bot.Exchanges { + if strings.EqualFold(Bot.Exchanges[x].GetName(), exchName) { return true } } @@ -59,9 +73,9 @@ func CheckExchangeExists(exchName string) bool { // GetExchangeByName returns an exchange given an exchange name func GetExchangeByName(exchName string) exchange.IBotExchange { - for x := range bot.exchanges { - if strings.EqualFold(bot.exchanges[x].GetName(), exchName) { - return bot.exchanges[x] + for x := range Bot.Exchanges { + if strings.EqualFold(Bot.Exchanges[x].GetName(), exchName) { + return Bot.Exchanges[x] } } return nil @@ -69,7 +83,7 @@ func GetExchangeByName(exchName string) exchange.IBotExchange { // ReloadExchange loads an exchange config by name func ReloadExchange(name string) error { - if len(bot.exchanges) == 0 { + if len(Bot.Exchanges) == 0 { return ErrNoExchangesLoaded } @@ -77,20 +91,20 @@ func ReloadExchange(name string) error { return ErrExchangeNotFound } - exchCfg, err := bot.config.GetExchangeConfig(name) + exchCfg, err := Bot.Config.GetExchangeConfig(name) if err != nil { return err } e := GetExchangeByName(name) - e.Setup(&exchCfg) - log.Debugf("%s exchange reloaded successfully.\n", name) + e.Setup(exchCfg) + log.Debugf(log.ExchangeSys, "%s exchange reloaded successfully.\n", name) return nil } // UnloadExchange unloads an exchange by name func UnloadExchange(name string) error { - if len(bot.exchanges) == 0 { + if len(Bot.Exchanges) == 0 { return ErrNoExchangesLoaded } @@ -98,21 +112,21 @@ func UnloadExchange(name string) error { return ErrExchangeNotFound } - exchCfg, err := bot.config.GetExchangeConfig(name) + exchCfg, err := Bot.Config.GetExchangeConfig(name) if err != nil { return err } exchCfg.Enabled = false - err = bot.config.UpdateExchangeConfig(&exchCfg) + err = Bot.Config.UpdateExchangeConfig(exchCfg) if err != nil { return err } - for x := range bot.exchanges { - if strings.EqualFold(bot.exchanges[x].GetName(), name) { - bot.exchanges[x].SetEnabled(false) - bot.exchanges = append(bot.exchanges[:x], bot.exchanges[x+1:]...) + for x := range Bot.Exchanges { + if strings.EqualFold(Bot.Exchanges[x].GetName(), name) { + Bot.Exchanges[x].SetEnabled(false) + Bot.Exchanges = append(Bot.Exchanges[:x], Bot.Exchanges[x+1:]...) return nil } } @@ -122,10 +136,10 @@ func UnloadExchange(name string) error { // LoadExchange loads an exchange by name func LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error { - nameLower := common.StringToLower(name) + nameLower := strings.ToLower(name) var exch exchange.IBotExchange - if len(bot.exchanges) > 0 { + if len(Bot.Exchanges) > 0 { if CheckExchangeExists(name) { return ErrExchangeAlreadyLoaded } @@ -197,14 +211,85 @@ func LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error { } exch.SetDefaults() - bot.exchanges = append(bot.exchanges, exch) - exchCfg, err := bot.config.GetExchangeConfig(name) + exchCfg, err := Bot.Config.GetExchangeConfig(name) if err != nil { return err } + if Bot.Settings.EnableAllPairs { + if exchCfg.CurrencyPairs != nil { + dryrunParamInteraction("enableallpairs") + assets := exchCfg.CurrencyPairs.GetAssetTypes() + for x := range assets { + pairs := exchCfg.CurrencyPairs.GetPairs(assets[x], false) + exchCfg.CurrencyPairs.StorePairs(assets[x], pairs, true) + } + } + } + + if Bot.Settings.EnableExchangeVerbose { + dryrunParamInteraction("exchangeverbose") + exchCfg.Verbose = true + } + + if Bot.Settings.EnableExchangeWebsocketSupport { + dryrunParamInteraction("exchangewebsocketsupport") + if exchCfg.Features != nil { + if exchCfg.Features.Supports.Websocket { + exchCfg.Features.Enabled.Websocket = true + } + } + } + + if Bot.Settings.EnableExchangeAutoPairUpdates { + dryrunParamInteraction("exchangeautopairupdates") + if exchCfg.Features != nil { + if exchCfg.Features.Supports.RESTCapabilities.AutoPairUpdates { + exchCfg.Features.Enabled.AutoPairUpdates = true + } + } + } + + if Bot.Settings.DisableExchangeAutoPairUpdates { + dryrunParamInteraction("exchangedisableautopairupdates") + if exchCfg.Features != nil { + if exchCfg.Features.Supports.RESTCapabilities.AutoPairUpdates { + exchCfg.Features.Enabled.AutoPairUpdates = false + } + } + } + + if Bot.Settings.ExchangeHTTPUserAgent != "" { + dryrunParamInteraction("exchangehttpuseragent") + exchCfg.HTTPUserAgent = Bot.Settings.ExchangeHTTPUserAgent + } + + if Bot.Settings.ExchangeHTTPProxy != "" { + dryrunParamInteraction("exchangehttpproxy") + exchCfg.ProxyAddress = Bot.Settings.ExchangeHTTPProxy + } + + if Bot.Settings.ExchangeHTTPTimeout != exchange.DefaultHTTPTimeout { + dryrunParamInteraction("exchangehttptimeout") + exchCfg.HTTPTimeout = Bot.Settings.ExchangeHTTPTimeout + } + + if Bot.Settings.EnableExchangeHTTPDebugging { + dryrunParamInteraction("exchangehttpdebugging") + exchCfg.HTTPDebugging = Bot.Settings.EnableExchangeHTTPDebugging + } + + if Bot.Settings.EnableAllExchanges { + dryrunParamInteraction("enableallexchanges") + } + exchCfg.Enabled = true - exch.Setup(&exchCfg) + err = exch.Setup(exchCfg) + if err != nil { + return err + } + + Bot.Exchanges = append(Bot.Exchanges, exch) if useWG { exch.Start(wg) @@ -216,21 +301,22 @@ func LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error { return nil } -// SetupExchanges sets up the exchanges used by the bot +// SetupExchanges sets up the exchanges used by the Bot func SetupExchanges() { var wg sync.WaitGroup - for x := range bot.config.Exchanges { - exch := &bot.config.Exchanges[x] + exchanges := Bot.Config.GetAllExchangeConfigs() + for x := range exchanges { + exch := exchanges[x] if CheckExchangeExists(exch.Name) { e := GetExchangeByName(exch.Name) if e == nil { - log.Errorf("%s", ErrExchangeNotFound) + log.Errorln(log.ExchangeSys, ErrExchangeNotFound) continue } err := ReloadExchange(exch.Name) if err != nil { - log.Errorf("ReloadExchange %s failed: %s", exch.Name, err) + log.Errorf(log.ExchangeSys, "ReloadExchange %s failed: %s\n", exch.Name, err) continue } @@ -240,19 +326,19 @@ func SetupExchanges() { } return } - if !exch.Enabled { - log.Debugf("%s: Exchange support: Disabled", exch.Name) + if !exch.Enabled && !Bot.Settings.EnableAllExchanges { + log.Debugf(log.ExchangeSys, "%s: Exchange support: Disabled\n", exch.Name) continue } err := LoadExchange(exch.Name, true, &wg) if err != nil { - log.Errorf("LoadExchange %s failed: %s", exch.Name, err) + log.Errorf(log.ExchangeSys, "LoadExchange %s failed: %s\n", exch.Name, err) continue } - log.Debugf( + log.Debugf(log.ExchangeSys, "%s: Exchange support: Enabled (Authenticated API support: %s - Verbose mode: %s).\n", exch.Name, - common.IsEnabled(exch.AuthenticatedAPISupport), + common.IsEnabled(exch.API.AuthenticatedSupport), common.IsEnabled(exch.Verbose), ) } diff --git a/engine/exchange_test.go b/engine/exchange_test.go new file mode 100644 index 00000000..18f08032 --- /dev/null +++ b/engine/exchange_test.go @@ -0,0 +1,198 @@ +package engine + +import ( + "testing" + + "github.com/thrasher-corp/gocryptotrader/config" +) + +var testSetup = false + +func SetupTest(t *testing.T) { + if !testSetup { + if Bot == nil { + Bot = new(Engine) + } + Bot.Config = &config.Cfg + err := Bot.Config.LoadConfig("", true) + if err != nil { + t.Fatalf("SetupTest: Failed to load config: %s", err) + } + testSetup = true + } + + if CheckExchangeExists(testExchange) { + return + } + err := LoadExchange(testExchange, false, nil) + if err != nil { + t.Errorf("SetupTest: Failed to load exchange: %s", err) + } +} + +func CleanupTest(t *testing.T) { + if !CheckExchangeExists(testExchange) { + return + } + + err := UnloadExchange(testExchange) + if err != nil { + t.Fatalf("CleanupTest: Failed to unload exchange: %s", + err) + } +} + +func TestCheckExchangeExists(t *testing.T) { + SetupTest(t) + + if !CheckExchangeExists(testExchange) { + t.Errorf("TestGetExchangeExists: Unable to find exchange") + } + + if CheckExchangeExists("Asdsad") { + t.Errorf("TestGetExchangeExists: Non-existent exchange found") + } + + CleanupTest(t) +} + +func TestGetExchangeByName(t *testing.T) { + SetupTest(t) + + exch := GetExchangeByName(testExchange) + if exch == nil { + t.Errorf("TestGetExchangeByName: Failed to get exchange") + } + + if !exch.IsEnabled() { + t.Errorf("TestGetExchangeByName: Unexpected result") + } + + exch.SetEnabled(false) + bfx := GetExchangeByName(testExchange) + if bfx.IsEnabled() { + t.Errorf("TestGetExchangeByName: Unexpected result") + } + + if exch.GetName() != testExchange { + t.Errorf("TestGetExchangeByName: Unexpected result") + } + + exch = GetExchangeByName("Asdasd") + if exch != nil { + t.Errorf("TestGetExchangeByName: Non-existent exchange found") + } + + CleanupTest(t) +} + +func TestReloadExchange(t *testing.T) { + SetupTest(t) + + err := ReloadExchange("asdf") + if err != ErrExchangeNotFound { + t.Errorf("TestReloadExchange: Incorrect result: %s", + err) + } + + err = ReloadExchange(testExchange) + if err != nil { + t.Errorf("TestReloadExchange: Incorrect result: %s", + err) + } + + CleanupTest(t) + + err = ReloadExchange("asdf") + if err != ErrNoExchangesLoaded { + t.Errorf("TestReloadExchange: Incorrect result: %s", + err) + } +} + +func TestUnloadExchange(t *testing.T) { + SetupTest(t) + + err := UnloadExchange("asdf") + if err != ErrExchangeNotFound { + t.Errorf("TestUnloadExchange: Incorrect result: %s", + err) + } + + err = UnloadExchange(testExchange) + if err != nil { + t.Errorf("TestUnloadExchange: Failed to get exchange. %s", + err) + } + + err = UnloadExchange("asdf") + if err != ErrNoExchangesLoaded { + t.Errorf("TestUnloadExchange: Incorrect result: %s", + err) + } + + CleanupTest(t) +} + +func TestDryRunParamInteraction(t *testing.T) { + SetupTest(t) + + // Load bot as per normal, dry run and verbose for Bitfinex should be + // disabled + exchCfg, err := Bot.Config.GetExchangeConfig(testExchange) + if err != nil { + t.Error(err) + } + + if Bot.Settings.EnableDryRun || + exchCfg.Verbose { + t.Error("dryrun and verbose should have been disabled") + } + + // Simulate overiding default settings and ensure that enabling exchange + // verbose mode will be set on Bitfinex + if err = UnloadExchange(testExchange); err != nil { + t.Error(err) + } + + Bot.Settings.CheckParamInteraction = true + Bot.Settings.EnableExchangeVerbose = true + if err = LoadExchange(testExchange, false, nil); err != nil { + t.Error(err) + } + + exchCfg, err = Bot.Config.GetExchangeConfig(testExchange) + if err != nil { + t.Error(err) + } + + if !Bot.Settings.EnableDryRun || + !exchCfg.Verbose { + t.Error("dryrun and verbose should have been enabled") + } + + if err = UnloadExchange(testExchange); err != nil { + t.Error(err) + } + + // Now set dryrun mode to false (via flagset and the previously enabled + // setting), enable exchange verbose mode and verify that verbose mode + // will be set on Bitfinex + Bot.Settings.EnableDryRun = false + Bot.Settings.CheckParamInteraction = true + Bot.Settings.EnableExchangeVerbose = true + flagSet["dryrun"] = true + if err = LoadExchange(testExchange, false, nil); err != nil { + t.Error(err) + } + + exchCfg, err = Bot.Config.GetExchangeConfig(testExchange) + if err != nil { + t.Error(err) + } + + if Bot.Settings.EnableDryRun || + !exchCfg.Verbose { + t.Error("dryrun should be false and verbose should be true") + } +} diff --git a/engine/helpers.go b/engine/helpers.go new file mode 100644 index 00000000..0db45287 --- /dev/null +++ b/engine/helpers.go @@ -0,0 +1,883 @@ +package engine + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "math/big" + "net" + "os" + "path/filepath" + "strings" + "time" + + "github.com/pquerna/otp/totp" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/file" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/dispatch" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/stats" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/gocryptotrader/portfolio" + "github.com/thrasher-corp/gocryptotrader/utils" +) + +// GetSubsystemsStatus returns the status of various subsystems +func GetSubsystemsStatus() map[string]bool { + systems := make(map[string]bool) + systems["communications"] = Bot.CommsManager.Started() + systems["internet_monitor"] = Bot.ConnectionManager.Started() + systems["orders"] = Bot.OrderManager.Started() + systems["portfolio"] = Bot.PortfolioManager.Started() + systems["ntp_timekeeper"] = Bot.NTPManager.Started() + systems["database"] = Bot.DatabaseManager.Started() + systems["exchange_syncer"] = Bot.Settings.EnableExchangeSyncManager + systems["grpc"] = Bot.Settings.EnableGRPC + systems["grpc_proxy"] = Bot.Settings.EnableGRPCProxy + systems["deprecated_rpc"] = Bot.Settings.EnableDeprecatedRPC + systems["websocket_rpc"] = Bot.Settings.EnableWebsocketRPC + systems["dispatch"] = dispatch.IsRunning() + return systems +} + +// RPCEndpoint stores an RPC endpoint status and addr +type RPCEndpoint struct { + Started bool + ListenAddr string +} + +// GetRPCEndpoints returns a list of RPC endpoints and their listen addrs +func GetRPCEndpoints() map[string]RPCEndpoint { + endpoints := make(map[string]RPCEndpoint) + endpoints["grpc"] = RPCEndpoint{ + Started: Bot.Settings.EnableGRPC, + ListenAddr: "grpc://" + Bot.Config.RemoteControl.GRPC.ListenAddress, + } + endpoints["grpc_proxy"] = RPCEndpoint{ + Started: Bot.Settings.EnableGRPCProxy, + ListenAddr: "http://" + Bot.Config.RemoteControl.GRPC.GRPCProxyListenAddress, + } + endpoints["deprecated_rpc"] = RPCEndpoint{ + Started: Bot.Settings.EnableDeprecatedRPC, + ListenAddr: "http://" + Bot.Config.RemoteControl.DeprecatedRPC.ListenAddress, + } + endpoints["websocket_rpc"] = RPCEndpoint{ + Started: Bot.Settings.EnableWebsocketRPC, + ListenAddr: "ws://" + Bot.Config.RemoteControl.WebsocketRPC.ListenAddress, + } + return endpoints +} + +// SetSubsystem enables or disables an engine subsystem +func SetSubsystem(subsys string, enable bool) error { + switch strings.ToLower(subsys) { + case "communications": + if enable { + return Bot.CommsManager.Start() + } + return Bot.CommsManager.Stop() + case "internet_monitor": + if enable { + return Bot.ConnectionManager.Start() + } + return Bot.CommsManager.Stop() + case "orders": + if enable { + return Bot.OrderManager.Start() + } + return Bot.OrderManager.Stop() + case "portfolio": + if enable { + return Bot.PortfolioManager.Start() + } + return Bot.OrderManager.Stop() + case "ntp_timekeeper": + if enable { + return Bot.NTPManager.Start() + } + return Bot.NTPManager.Stop() + case "database": + if enable { + return Bot.DatabaseManager.Start() + } + return Bot.DatabaseManager.Stop() + case "exchange_syncer": + if enable { + Bot.ExchangeCurrencyPairManager.Start() + } + Bot.ExchangeCurrencyPairManager.Stop() + case "dispatch": + if enable { + return dispatch.Start(Bot.Settings.DispatchMaxWorkerAmount, Bot.Settings.DispatchJobsLimit) + } + return dispatch.Stop() + } + return errors.New("subsystem not found") +} + +// GetExchangeOTPs returns OTP codes for all exchanges which have a otpsecret +// stored +func GetExchangeOTPs() (map[string]string, error) { + otpCodes := make(map[string]string) + for x := range Bot.Config.Exchanges { + if otpSecret := Bot.Config.Exchanges[x].API.Credentials.OTPSecret; otpSecret != "" { + exchName := Bot.Config.Exchanges[x].Name + o, err := totp.GenerateCode(otpSecret, time.Now()) + if err != nil { + log.Errorf(log.Global, "Unable to generate OTP code for exchange %s. Err: %s\n", + exchName, err) + continue + } + otpCodes[exchName] = o + } + } + + if len(otpCodes) == 0 { + return nil, errors.New("no exchanges found which have a OTP secret stored") + } + + return otpCodes, nil +} + +// GetExchangeoOTPByName returns a OTP code for the desired exchange +// if it exists +func GetExchangeoOTPByName(exchName string) (string, error) { + for x := range Bot.Config.Exchanges { + if !strings.EqualFold(Bot.Config.Exchanges[x].Name, exchName) { + continue + } + + if otpSecret := Bot.Config.Exchanges[x].API.Credentials.OTPSecret; otpSecret != "" { + return totp.GenerateCode(otpSecret, time.Now()) + } + } + return "", errors.New("exchange does not have a OTP secret stored") +} + +// GetAuthAPISupportedExchanges returns a list of auth api enabled exchanges +func GetAuthAPISupportedExchanges() []string { + var exchanges []string + for x := range Bot.Exchanges { + if !Bot.Exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) && + !Bot.Exchanges[x].GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + continue + } + exchanges = append(exchanges, Bot.Exchanges[x].GetName()) + } + return exchanges +} + +// IsOnline returns whether or not the engine has Internet connectivity +func IsOnline() bool { + return Bot.ConnectionManager.IsOnline() +} + +// GetAvailableExchanges returns a list of enabled exchanges +func GetAvailableExchanges() []string { + var enExchanges []string + for x := range Bot.Config.Exchanges { + if Bot.Config.Exchanges[x].Enabled { + enExchanges = append(enExchanges, Bot.Config.Exchanges[x].Name) + } + } + return enExchanges +} + +// GetAllAvailablePairs returns a list of all available pairs on either enabled +// or disabled exchanges +func GetAllAvailablePairs(enabledExchangesOnly bool, assetType asset.Item) currency.Pairs { + var pairList currency.Pairs + for x := range Bot.Config.Exchanges { + if enabledExchangesOnly && !Bot.Config.Exchanges[x].Enabled { + continue + } + + exchName := Bot.Config.Exchanges[x].Name + pairs, err := Bot.Config.GetAvailablePairs(exchName, assetType) + if err != nil { + continue + } + + for y := range pairs { + if pairList.Contains(pairs[y], false) { + continue + } + pairList = append(pairList, pairs[y]) + } + } + return pairList +} + +// GetSpecificAvailablePairs returns a list of supported pairs based on specific +// parameters +func GetSpecificAvailablePairs(enabledExchangesOnly, fiatPairs, includeUSDT, cryptoPairs bool, assetType asset.Item) currency.Pairs { + var pairList currency.Pairs + supportedPairs := GetAllAvailablePairs(enabledExchangesOnly, assetType) + + for x := range supportedPairs { + if fiatPairs { + if supportedPairs[x].IsCryptoFiatPair() && + !supportedPairs[x].ContainsCurrency(currency.USDT) || + (includeUSDT && + supportedPairs[x].ContainsCurrency(currency.USDT) && + supportedPairs[x].IsCryptoPair()) { + if pairList.Contains(supportedPairs[x], false) { + continue + } + pairList = append(pairList, supportedPairs[x]) + } + } + if cryptoPairs { + if supportedPairs[x].IsCryptoPair() { + if pairList.Contains(supportedPairs[x], false) { + continue + } + pairList = append(pairList, supportedPairs[x]) + } + } + } + return pairList +} + +// IsRelatablePairs checks to see if the two pairs are relatable +func IsRelatablePairs(p1, p2 currency.Pair, includeUSDT bool) bool { + if p1.EqualIncludeReciprocal(p2) { + return true + } + + var relatablePairs = GetRelatableCurrencies(p1, true, includeUSDT) + if p1.IsCryptoFiatPair() { + for x := range relatablePairs { + relatablePairs = append(relatablePairs, + GetRelatableFiatCurrencies(relatablePairs[x])...) + } + } + return relatablePairs.Contains(p2, false) +} + +// MapCurrenciesByExchange returns a list of currency pairs mapped to an +// exchange +func MapCurrenciesByExchange(p currency.Pairs, enabledExchangesOnly bool, assetType asset.Item) map[string]currency.Pairs { + currencyExchange := make(map[string]currency.Pairs) + for x := range p { + for y := range Bot.Config.Exchanges { + if enabledExchangesOnly && !Bot.Config.Exchanges[y].Enabled { + continue + } + exchName := Bot.Config.Exchanges[y].Name + success, err := Bot.Config.SupportsPair(exchName, p[x], assetType) + if err != nil || !success { + continue + } + + result, ok := currencyExchange[exchName] + if !ok { + var pairs []currency.Pair + pairs = append(pairs, p[x]) + currencyExchange[exchName] = pairs + } else { + if result.Contains(p[x], false) { + continue + } + result = append(result, p[x]) + currencyExchange[exchName] = result + } + } + } + return currencyExchange +} + +// GetExchangeNamesByCurrency returns a list of exchanges supporting +// a currency pair based on whether the exchange is enabled or not +func GetExchangeNamesByCurrency(p currency.Pair, enabled bool, assetType asset.Item) []string { + var exchanges []string + for x := range Bot.Config.Exchanges { + if enabled != Bot.Config.Exchanges[x].Enabled { + continue + } + + exchName := Bot.Config.Exchanges[x].Name + success, err := Bot.Config.SupportsPair(exchName, p, assetType) + if err != nil { + continue + } + + if success { + exchanges = append(exchanges, exchName) + } + } + return exchanges +} + +// GetRelatableCryptocurrencies returns a list of currency pairs if it can find +// any relatable currencies (e.g ETHBTC -> ETHLTC -> ETHUSDT -> ETHREP) +func GetRelatableCryptocurrencies(p currency.Pair) currency.Pairs { + var pairs currency.Pairs + cryptocurrencies := currency.GetCryptocurrencies() + + for x := range cryptocurrencies { + newPair := currency.NewPair(p.Base, cryptocurrencies[x]) + if newPair.IsInvalid() { + continue + } + + if newPair.Base.Upper() == p.Base.Upper() && + newPair.Quote.Upper() == p.Quote.Upper() { + continue + } + + if pairs.Contains(newPair, false) { + continue + } + pairs = append(pairs, newPair) + } + return pairs +} + +// GetRelatableFiatCurrencies returns a list of currency pairs if it can find +// any relatable currencies (e.g ETHUSD -> ETHAUD -> ETHGBP -> ETHJPY) +func GetRelatableFiatCurrencies(p currency.Pair) currency.Pairs { + var pairs currency.Pairs + fiatCurrencies := currency.GetFiatCurrencies() + + for x := range fiatCurrencies { + newPair := currency.NewPair(p.Base, fiatCurrencies[x]) + if newPair.Base.Upper() == newPair.Quote.Upper() { + continue + } + + if newPair.Base.Upper() == p.Base.Upper() && + newPair.Quote.Upper() == p.Quote.Upper() { + continue + } + + if pairs.Contains(newPair, false) { + continue + } + pairs = append(pairs, newPair) + } + return pairs +} + +// GetRelatableCurrencies returns a list of currency pairs if it can find +// any relatable currencies (e.g BTCUSD -> BTC USDT -> XBT USDT -> XBT USD) +// incOrig includes the supplied pair if desired +func GetRelatableCurrencies(p currency.Pair, incOrig, incUSDT bool) currency.Pairs { + var pairs currency.Pairs + + addPair := func(p currency.Pair) { + if pairs.Contains(p, true) { + return + } + pairs = append(pairs, p) + } + + buildPairs := func(p currency.Pair, incOrig bool) { + if incOrig { + addPair(p) + } + + first, ok := currency.GetTranslation(p.Base) + if ok { + addPair(currency.NewPair(first, p.Quote)) + + var second currency.Code + second, ok = currency.GetTranslation(p.Quote) + if ok { + addPair(currency.NewPair(first, second)) + } + } + + second, ok := currency.GetTranslation(p.Quote) + if ok { + addPair(currency.NewPair(p.Base, second)) + } + } + + buildPairs(p, incOrig) + buildPairs(p.Swap(), incOrig) + + if !incUSDT { + pairs = pairs.RemovePairsByFilter(currency.USDT) + } + + return pairs +} + +// GetSpecificOrderbook returns a specific orderbook given the currency, +// exchangeName and assetType +func GetSpecificOrderbook(p currency.Pair, exchangeName string, assetType asset.Item) (orderbook.Base, error) { + var specificOrderbook orderbook.Base + var err error + for x := range Bot.Exchanges { + if Bot.Exchanges[x] != nil { + if Bot.Exchanges[x].GetName() == exchangeName { + specificOrderbook, err = Bot.Exchanges[x].FetchOrderbook( + p, + assetType, + ) + break + } + } + } + return specificOrderbook, err +} + +// GetSpecificTicker returns a specific ticker given the currency, +// exchangeName and assetType +func GetSpecificTicker(p currency.Pair, exchangeName string, assetType asset.Item) (ticker.Price, error) { + var specificTicker ticker.Price + var err error + for x := range Bot.Exchanges { + if Bot.Exchanges[x] != nil { + if Bot.Exchanges[x].GetName() == exchangeName { + specificTicker, err = Bot.Exchanges[x].FetchTicker( + p, + assetType, + ) + break + } + } + } + return specificTicker, err +} + +// GetCollatedExchangeAccountInfoByCoin collates individual exchange account +// information and turns into into a map string of +// exchange.AccountCurrencyInfo +func GetCollatedExchangeAccountInfoByCoin(exchAccounts []exchange.AccountInfo) map[currency.Code]exchange.AccountCurrencyInfo { + result := make(map[currency.Code]exchange.AccountCurrencyInfo) + for _, accounts := range exchAccounts { + for _, account := range accounts.Accounts { + for _, accountCurrencyInfo := range account.Currencies { + currencyName := accountCurrencyInfo.CurrencyName + avail := accountCurrencyInfo.TotalValue + onHold := accountCurrencyInfo.Hold + + info, ok := result[currencyName] + if !ok { + accountInfo := exchange.AccountCurrencyInfo{CurrencyName: currencyName, Hold: onHold, TotalValue: avail} + result[currencyName] = accountInfo + } else { + info.Hold += onHold + info.TotalValue += avail + result[currencyName] = info + } + } + } + } + return result +} + +// GetAccountCurrencyInfoByExchangeName returns info for an exchange +func GetAccountCurrencyInfoByExchangeName(accounts []exchange.AccountInfo, exchangeName string) (exchange.AccountInfo, error) { + for i := 0; i < len(accounts); i++ { + if accounts[i].Exchange == exchangeName { + return accounts[i], nil + } + } + return exchange.AccountInfo{}, ErrExchangeNotFound +} + +// GetExchangeHighestPriceByCurrencyPair returns the exchange with the highest +// price for a given currency pair and asset type +func GetExchangeHighestPriceByCurrencyPair(p currency.Pair, assetType asset.Item) (string, error) { + result := stats.SortExchangesByPrice(p, assetType, true) + if len(result) == 0 { + return "", fmt.Errorf("no stats for supplied currency pair and asset type") + } + + return result[0].Exchange, nil +} + +// GetExchangeLowestPriceByCurrencyPair returns the exchange with the lowest +// price for a given currency pair and asset type +func GetExchangeLowestPriceByCurrencyPair(p currency.Pair, assetType asset.Item) (string, error) { + result := stats.SortExchangesByPrice(p, assetType, false) + if len(result) == 0 { + return "", fmt.Errorf("no stats for supplied currency pair and asset type") + } + + return result[0].Exchange, nil +} + +// SeedExchangeAccountInfo seeds account info +func SeedExchangeAccountInfo(data []exchange.AccountInfo) { + if len(data) == 0 { + return + } + + port := portfolio.GetPortfolio() + + for _, exchangeData := range data { + exchangeName := exchangeData.Exchange + var currencies []exchange.AccountCurrencyInfo + for _, account := range exchangeData.Accounts { + for _, info := range account.Currencies { + var update bool + for i := range currencies { + if info.CurrencyName == currencies[i].CurrencyName { + currencies[i].Hold += info.Hold + currencies[i].TotalValue += info.TotalValue + update = true + } + } + + if update { + continue + } + + currencies = append(currencies, exchange.AccountCurrencyInfo{ + CurrencyName: info.CurrencyName, + TotalValue: info.TotalValue, + Hold: info.Hold, + }) + } + } + + for _, total := range currencies { + currencyName := total.CurrencyName + total := total.TotalValue + + if !port.ExchangeAddressExists(exchangeName, currencyName) { + if total <= 0 { + continue + } + + log.Debugf(log.PortfolioMgr, "Portfolio: Adding new exchange address: %s, %s, %f, %s\n", + exchangeName, + currencyName, + total, + portfolio.PortfolioAddressExchange) + + port.Addresses = append( + port.Addresses, + portfolio.Address{Address: exchangeName, + CoinType: currencyName, + Balance: total, + Description: portfolio.PortfolioAddressExchange}) + } else { + if total <= 0 { + log.Debugf(log.PortfolioMgr, "Portfolio: Removing %s %s entry.\n", + exchangeName, + currencyName) + port.RemoveExchangeAddress(exchangeName, currencyName) + } else { + balance, ok := port.GetAddressBalance(exchangeName, + portfolio.PortfolioAddressExchange, + currencyName) + if !ok { + continue + } + + if balance != total { + log.Debugf(log.PortfolioMgr, "Portfolio: Updating %s %s entry with balance %f.\n", + exchangeName, + currencyName, + total) + port.UpdateExchangeAddressBalance(exchangeName, + currencyName, + total) + } + } + } + } + } +} + +// GetCryptocurrenciesByExchange returns a list of cryptocurrencies the exchange supports +func GetCryptocurrenciesByExchange(exchangeName string, enabledExchangesOnly, enabledPairs bool, assetType asset.Item) ([]string, error) { + var cryptocurrencies []string + for x := range Bot.Config.Exchanges { + if !strings.EqualFold(Bot.Config.Exchanges[x].Name, exchangeName) { + continue + } + if enabledExchangesOnly && !Bot.Config.Exchanges[x].Enabled { + continue + } + + var err error + var pairs []currency.Pair + if enabledPairs { + pairs, err = Bot.Config.GetEnabledPairs(exchangeName, assetType) + if err != nil { + return nil, err + } + } else { + pairs, err = Bot.Config.GetAvailablePairs(exchangeName, assetType) + if err != nil { + return nil, err + } + } + + for y := range pairs { + if pairs[y].Base.IsCryptocurrency() && + !common.StringDataCompareInsensitive(cryptocurrencies, pairs[y].Base.String()) { + cryptocurrencies = append(cryptocurrencies, pairs[y].Base.String()) + } + + if pairs[y].Quote.IsCryptocurrency() && + !common.StringDataCompareInsensitive(cryptocurrencies, pairs[y].Quote.String()) { + cryptocurrencies = append(cryptocurrencies, pairs[y].Quote.String()) + } + } + } + return cryptocurrencies, nil +} + +// GetCryptocurrencyDepositAddressesByExchange returns the cryptocurrency deposit addresses for a particular exchange +func GetCryptocurrencyDepositAddressesByExchange(exchName string) (map[string]string, error) { + if Bot.DepositAddressManager != nil { + return Bot.DepositAddressManager.GetDepositAddressesByExchange(exchName) + } + + result := GetExchangeCryptocurrencyDepositAddresses() + r, ok := result[exchName] + if !ok { + return nil, ErrExchangeNotFound + } + return r, nil +} + +// GetExchangeCryptocurrencyDepositAddress returns the cryptocurrency deposit address for a particular +// exchange +func GetExchangeCryptocurrencyDepositAddress(exchName, accountID string, item currency.Code) (string, error) { + if Bot.DepositAddressManager != nil { + return Bot.DepositAddressManager.GetDepositAddressByExchange(exchName, item) + } + + exch := GetExchangeByName(exchName) + if exch == nil { + return "", ErrExchangeNotFound + } + return exch.GetDepositAddress(item, accountID) +} + +// GetExchangeCryptocurrencyDepositAddresses obtains an exchanges deposit cryptocurrency list +func GetExchangeCryptocurrencyDepositAddresses() map[string]map[string]string { + result := make(map[string]map[string]string) + for x := range Bot.Exchanges { + if !Bot.Exchanges[x].IsEnabled() { + continue + } + exchName := Bot.Exchanges[x].GetName() + if !Bot.Exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) { + if Bot.Settings.Verbose { + log.Debugf(log.ExchangeSys, "GetExchangeCryptocurrencyDepositAddresses: Skippping %s due to disabled authenticated API support.\n", exchName) + } + continue + } + + cryptoCurrencies, err := GetCryptocurrenciesByExchange(exchName, true, true, asset.Spot) + if err != nil { + log.Debugf(log.ExchangeSys, "%s failed to get cryptocurrency deposit addresses. Err: %s\n", exchName, err) + continue + } + + cryptoAddr := make(map[string]string) + for y := range cryptoCurrencies { + cryptocurrency := cryptoCurrencies[y] + depositAddr, err := Bot.Exchanges[x].GetDepositAddress(currency.NewCode(cryptocurrency), "") + if err != nil { + log.Errorf(log.Global, "%s failed to get cryptocurrency deposit addresses. Err: %s\n", exchName, err) + continue + } + cryptoAddr[cryptocurrency] = depositAddr + } + result[exchName] = cryptoAddr + } + return result +} + +// WithdrawCryptocurrencyFundsByExchange withdraws the desired cryptocurrency and amount to a desired cryptocurrency address +func WithdrawCryptocurrencyFundsByExchange(exchName string, req *exchange.CryptoWithdrawRequest) (string, error) { + if req == nil { + return "", errors.New("crypto withdraw request param is nil") + } + + exch := GetExchangeByName(exchName) + if exch == nil { + return "", ErrExchangeNotFound + } + + return exch.WithdrawCryptocurrencyFunds(req) +} + +// FormatCurrency is a method that formats and returns a currency pair +// based on the user currency display preferences +func FormatCurrency(p currency.Pair) currency.Pair { + return p.Format(Bot.Config.Currency.CurrencyPairFormat.Delimiter, + Bot.Config.Currency.CurrencyPairFormat.Uppercase) +} + +// GetExchanges returns a list of loaded exchanges +func GetExchanges(enabled bool) []string { + var exchanges []string + for x := range Bot.Exchanges { + if Bot.Exchanges[x].IsEnabled() && enabled { + exchanges = append(exchanges, Bot.Exchanges[x].GetName()) + continue + } + exchanges = append(exchanges, Bot.Exchanges[x].GetName()) + } + return exchanges +} + +// GetAllActiveTickers returns all enabled exchange tickers +func GetAllActiveTickers() []EnabledExchangeCurrencies { + var tickerData []EnabledExchangeCurrencies + + for _, exch := range Bot.Exchanges { + if !exch.IsEnabled() { + continue + } + + assets := exch.GetAssetTypes() + exchName := exch.GetName() + var exchangeTicker EnabledExchangeCurrencies + exchangeTicker.ExchangeName = exchName + + for y := range assets { + currencies := exch.GetEnabledPairs(assets[y]) + for z := range currencies { + tp, err := exch.FetchTicker(currencies[z], assets[y]) + if err != nil { + log.Errorf(log.ExchangeSys, "Exchange %s failed to retrieve %s ticker. Err: %s\n", exchName, + currencies[z].String(), + err) + continue + } + exchangeTicker.ExchangeValues = append(exchangeTicker.ExchangeValues, tp) + } + tickerData = append(tickerData, exchangeTicker) + } + } + return tickerData +} + +// GetAllEnabledExchangeAccountInfo returns all the current enabled exchanges +func GetAllEnabledExchangeAccountInfo() AllEnabledExchangeAccounts { + var response AllEnabledExchangeAccounts + for _, individualBot := range Bot.Exchanges { + if individualBot != nil && individualBot.IsEnabled() { + if !individualBot.GetAuthenticatedAPISupport(exchange.RestAuthentication) { + if Bot.Settings.Verbose { + log.Debugf(log.ExchangeSys, "GetAllEnabledExchangeAccountInfo: Skippping %s due to disabled authenticated API support.\n", individualBot.GetName()) + } + continue + } + individualExchange, err := individualBot.GetAccountInfo() + if err != nil { + log.Errorf(log.ExchangeSys, "Error encountered retrieving exchange account info for %s. Error %s\n", + individualBot.GetName(), err) + continue + } + response.Data = append(response.Data, individualExchange) + } + } + return response +} + +func checkCerts() error { + targetDir := utils.GetTLSDir(Bot.Settings.DataDir) + _, err := os.Stat(targetDir) + if os.IsNotExist(err) { + err := common.CreateDir(targetDir) + if err != nil { + return err + } + return genCert(targetDir) + } + + log.Debugln(log.Global, "gRPC TLS certs directory already exists, will use them.") + return nil +} + +func genCert(targetDir string) error { + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return fmt.Errorf("failed to generate ecdsa private key: %s", err) + } + + notBefore := time.Now() + notAfter := notBefore.Add(time.Hour * 24 * 365) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return fmt.Errorf("failed to generate serial number: %s", err) + } + + host, err := os.Hostname() + if err != nil { + return fmt.Errorf("failed to get hostname: %s", err) + } + + dnsNames := []string{host} + if host != "localhost" { + dnsNames = append(dnsNames, "localhost") + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"gocryptotrader"}, + CommonName: host, + }, + NotBefore: notBefore, + NotAfter: notAfter, + IsCA: true, + BasicConstraintsValid: true, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + + IPAddresses: []net.IP{ + net.ParseIP("127.0.0.1"), + net.ParseIP("::1"), + }, + DNSNames: dnsNames, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) + if err != nil { + return fmt.Errorf("failed to create certificate: %s", err) + } + + certData := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if certData == nil { + return fmt.Errorf("cert data is nil") + } + + b, err := x509.MarshalECPrivateKey(privKey) + if err != nil { + return fmt.Errorf("failed to marshal ECDSA private key: %s", err) + } + + keyData := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) + if keyData == nil { + return fmt.Errorf("key pem data is nil") + } + + err = file.Write(filepath.Join(targetDir, "key.pem"), keyData) + if err != nil { + return fmt.Errorf("failed to write key.pem file %s", err) + } + + err = file.Write(filepath.Join(targetDir, "cert.pem"), certData) + if err != nil { + return fmt.Errorf("failed to write cert.pem file %s", err) + } + + log.Debugf(log.Global, "TLS key.pem and cert.pem files written to %s\n", targetDir) + return nil +} diff --git a/helpers_test.go b/engine/helpers_test.go similarity index 72% rename from helpers_test.go rename to engine/helpers_test.go index 750b9001..8bf23ab3 100644 --- a/helpers_test.go +++ b/engine/helpers_test.go @@ -1,19 +1,21 @@ -package main +package engine import ( "testing" + "time" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/stats" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" ) const ( - TestConfig = "./testdata/configtest.json" + TestConfig = "../testdata/configtest.json" ) var ( @@ -23,14 +25,17 @@ var ( func SetupTestHelpers(t *testing.T) { if !helperTestLoaded { if !testSetup { - bot.config = &config.Cfg - err := bot.config.LoadConfig("./testdata/configtest.json") + if Bot == nil { + Bot = new(Engine) + } + Bot.Config = &config.Cfg + err := Bot.Config.LoadConfig("../testdata/configtest.json", true) if err != nil { - t.Fatalf("Test failed. SetupTest: Failed to load config: %s", err) + t.Fatalf("SetupTest: Failed to load config: %s", err) } testSetup = true } - err := bot.config.RetrieveConfigCurrencyPairs(true) + err := Bot.Config.RetrieveConfigCurrencyPairs(true, asset.Spot) if err != nil { t.Fatalf("Failed to retrieve config currency pairs. %s", err) } @@ -38,9 +43,116 @@ func SetupTestHelpers(t *testing.T) { } } +func TestGetExchangeOTPs(t *testing.T) { + SetupTestHelpers(t) + _, err := GetExchangeOTPs() + if err == nil { + t.Fatal("Expected err with no exchange OTP secrets set") + } + + bfxCfg, err := Bot.Config.GetExchangeConfig("Bitfinex") + if err != nil { + t.Fatal(err) + } + bCfg, err := Bot.Config.GetExchangeConfig("Bitstamp") + if err != nil { + t.Fatal(err) + } + + bfxCfg.API.Credentials.OTPSecret = "JBSWY3DPEHPK3PXP" + bCfg.API.Credentials.OTPSecret = "JBSWY3DPEHPK3PXP" + result, err := GetExchangeOTPs() + if err != nil { + t.Fatal(err) + } + if len(result) != 2 { + t.Fatal("Expected 2 OTP results") + } + + bfxCfg.API.Credentials.OTPSecret = "°" + result, err = GetExchangeOTPs() + if err != nil { + t.Fatal(err) + } + if len(result) != 1 { + t.Fatal("Expected 1 OTP code with invalid OTP Secret") + } + + // Flush settings + bfxCfg.API.Credentials.OTPSecret = "" + bCfg.API.Credentials.OTPSecret = "" +} + +func TestGetExchangeoOTPByName(t *testing.T) { + SetupTestHelpers(t) + _, err := GetExchangeoOTPByName("Bitstamp") + if err == nil { + t.Fatal("Expected err with no exchange OTP secrets set") + } + + bCfg, err := Bot.Config.GetExchangeConfig("Bitstamp") + if err != nil { + t.Fatal(err) + } + + bCfg.API.Credentials.OTPSecret = "JBSWY3DPEHPK3PXP" + result, err := GetExchangeoOTPByName("Bitstamp") + if err != nil { + t.Fatal(err) + } + if result == "" { + t.Fatal("Expected valid OTP code") + } + + // Flush setting + bCfg.API.Credentials.OTPSecret = "" +} + +func TestGetAuthAPISupportedExchanges(t *testing.T) { + SetupTestHelpers(t) + if result := GetAuthAPISupportedExchanges(); result != nil { + t.Fatal("Unexpected result") + } +} + +func TestIsOnline(t *testing.T) { + SetupTestHelpers(t) + if r := IsOnline(); r { + t.Fatal("Unexpected result") + } + + if err := Bot.ConnectionManager.Start(); err != nil { + t.Fatal(err) + } + + tick := time.NewTicker(time.Second * 5) + defer tick.Stop() + for { + select { + case <-tick.C: + t.Fatal("Test timeout") + default: + if IsOnline() { + if err := Bot.ConnectionManager.Stop(); err != nil { + t.Fatal("unable to shutdown connection manager") + } + return + } + } + } +} + +func TestGetAvailableExchanges(t *testing.T) { + SetupTestHelpers(t) + if r := len(GetAvailableExchanges()); r == 0 { + t.Error("Expected len > 0") + } +} + func TestGetSpecificAvailablePairs(t *testing.T) { SetupTestHelpers(t) - result := GetSpecificAvailablePairs(true, true, true, false) + assetType := asset.Spot + result := GetSpecificAvailablePairs(true, true, true, false, assetType) if !result.Contains(currency.NewPairFromStrings("BTC", "USD"), true) { t.Fatal("Unexpected result") @@ -50,13 +162,13 @@ func TestGetSpecificAvailablePairs(t *testing.T) { t.Fatal("Unexpected result") } - result = GetSpecificAvailablePairs(true, true, false, false) + result = GetSpecificAvailablePairs(true, true, false, false, assetType) if result.Contains(currency.NewPairFromStrings("BTC", "USDT"), false) { t.Fatal("Unexpected result") } - result = GetSpecificAvailablePairs(true, false, false, true) + result = GetSpecificAvailablePairs(true, false, false, true, assetType) if !result.Contains(currency.NewPairFromStrings("LTC", "BTC"), false) { t.Fatal("Unexpected result") } @@ -229,7 +341,7 @@ func TestMapCurrenciesByExchange(t *testing.T) { currency.NewPair(currency.BTC, currency.EUR), } - result := MapCurrenciesByExchange(pairs, true) + result := MapCurrenciesByExchange(pairs, true, asset.Spot) pairs, ok := result["Bitstamp"] if !ok { t.Fatal("Unexpected result") @@ -242,18 +354,25 @@ func TestMapCurrenciesByExchange(t *testing.T) { func TestGetExchangeNamesByCurrency(t *testing.T) { SetupTestHelpers(t) + assetType := asset.Spot - result := GetExchangeNamesByCurrency(currency.NewPairFromStrings("BTC", "USD"), true) + result := GetExchangeNamesByCurrency(currency.NewPairFromStrings("BTC", "USD"), + true, + assetType) if !common.StringDataCompare(result, "Bitstamp") { t.Fatal("Unexpected result") } - result = GetExchangeNamesByCurrency(currency.NewPairFromStrings("BTC", "JPY"), true) + result = GetExchangeNamesByCurrency(currency.NewPairFromStrings("BTC", "JPY"), + true, + assetType) if !common.StringDataCompare(result, "Bitflyer") { t.Fatal("Unexpected result") } - result = GetExchangeNamesByCurrency(currency.NewPairFromStrings("blah", "JPY"), true) + result = GetExchangeNamesByCurrency(currency.NewPairFromStrings("blah", "JPY"), + true, + assetType) if len(result) > 0 { t.Fatal("Unexpected result") } @@ -271,7 +390,7 @@ func TestGetSpecificOrderbook(t *testing.T) { Pair: currency.NewPair(currency.BTC, currency.USD), Bids: bids, ExchangeName: "Bitstamp", - AssetType: orderbook.Spot, + AssetType: asset.Spot, } err := base.Process() @@ -279,7 +398,9 @@ func TestGetSpecificOrderbook(t *testing.T) { t.Fatal("Unexpected result", err) } - ob, err := GetSpecificOrderbook("BTCUSD", "Bitstamp", ticker.Spot) + ob, err := GetSpecificOrderbook(currency.NewPairFromString("BTCUSD"), + "Bitstamp", + asset.Spot) if err != nil { t.Fatal(err) } @@ -288,7 +409,9 @@ func TestGetSpecificOrderbook(t *testing.T) { t.Fatal("Unexpected result") } - ob, err = GetSpecificOrderbook("ETHLTC", "Bitstamp", ticker.Spot) + ob, err = GetSpecificOrderbook(currency.NewPairFromStrings("ETH", "LTC"), + "Bitstamp", + asset.Spot) if err == nil { t.Fatal("Unexpected result") } @@ -301,15 +424,15 @@ func TestGetSpecificTicker(t *testing.T) { LoadExchange("Bitstamp", false, nil) p := currency.NewPairFromStrings("BTC", "USD") - err := ticker.ProcessTicker("Bitstamp", &ticker.Price{Pair: p, Last: 1000}, - ticker.Spot) + asset.Spot) if err != nil { - t.Fatal("Test failed. ProcessTicker error", err) + t.Fatal("ProcessTicker error", err) } - tick, err := GetSpecificTicker("BTCUSD", "Bitstamp", ticker.Spot) + tick, err := GetSpecificTicker(currency.NewPairFromStrings("BTC", "USD"), "Bitstamp", + asset.Spot) if err != nil { t.Fatal(err) } @@ -318,7 +441,8 @@ func TestGetSpecificTicker(t *testing.T) { t.Fatal("Unexpected result") } - tick, err = GetSpecificTicker("ETHLTC", "Bitstamp", ticker.Spot) + tick, err = GetSpecificTicker(currency.NewPairFromStrings("ETH", "LTC"), "Bitstamp", + asset.Spot) if err == nil { t.Fatal("Unexpected result") } @@ -409,7 +533,7 @@ func TestGetAccountCurrencyInfoByExchangeName(t *testing.T) { } _, err = GetAccountCurrencyInfoByExchangeName(exchangeInfo, "ASDF") - if err.Error() != exchange.ErrExchangeNotFound { + if err != ErrExchangeNotFound { t.Fatal("Unexepcted result") } } @@ -418,9 +542,9 @@ func TestGetExchangeHighestPriceByCurrencyPair(t *testing.T) { SetupTestHelpers(t) p := currency.NewPairFromStrings("BTC", "USD") - stats.Add("Bitfinex", p, ticker.Spot, 1000, 10000) - stats.Add("Bitstamp", p, ticker.Spot, 1337, 10000) - exchangeName, err := GetExchangeHighestPriceByCurrencyPair(p, ticker.Spot) + stats.Add("Bitfinex", p, asset.Spot, 1000, 10000) + stats.Add("Bitstamp", p, asset.Spot, 1337, 10000) + exchangeName, err := GetExchangeHighestPriceByCurrencyPair(p, asset.Spot) if err != nil { t.Error(err) } @@ -429,7 +553,8 @@ func TestGetExchangeHighestPriceByCurrencyPair(t *testing.T) { t.Error("Unexpected result") } - _, err = GetExchangeHighestPriceByCurrencyPair(currency.NewPairFromStrings("BTC", "AUD"), ticker.Spot) + _, err = GetExchangeHighestPriceByCurrencyPair(currency.NewPairFromStrings("BTC", "AUD"), + asset.Spot) if err == nil { t.Error("Unexpected result") } @@ -439,9 +564,9 @@ func TestGetExchangeLowestPriceByCurrencyPair(t *testing.T) { SetupTestHelpers(t) p := currency.NewPairFromStrings("BTC", "USD") - stats.Add("Bitfinex", p, ticker.Spot, 1000, 10000) - stats.Add("Bitstamp", p, ticker.Spot, 1337, 10000) - exchangeName, err := GetExchangeLowestPriceByCurrencyPair(p, ticker.Spot) + stats.Add("Bitfinex", p, asset.Spot, 1000, 10000) + stats.Add("Bitstamp", p, asset.Spot, 1337, 10000) + exchangeName, err := GetExchangeLowestPriceByCurrencyPair(p, asset.Spot) if err != nil { t.Error(err) } @@ -450,8 +575,18 @@ func TestGetExchangeLowestPriceByCurrencyPair(t *testing.T) { t.Error("Unexpected result") } - _, err = GetExchangeLowestPriceByCurrencyPair(currency.NewPairFromStrings("BTC", "AUD"), ticker.Spot) + _, err = GetExchangeLowestPriceByCurrencyPair(currency.NewPairFromStrings("BTC", "AUD"), + asset.Spot) if err == nil { t.Error("Unexpected reuslt") } } + +func TestGetCryptocurrenciesByExchange(t *testing.T) { + SetupTestHelpers(t) + + _, err := GetCryptocurrenciesByExchange("Bitfinex", false, false, asset.Spot) + if err != nil { + t.Fatalf("Err %s", err) + } +} diff --git a/engine/orders.go b/engine/orders.go new file mode 100644 index 00000000..aa50406e --- /dev/null +++ b/engine/orders.go @@ -0,0 +1,281 @@ +package engine + +import ( + "errors" + "fmt" + "sync/atomic" + "time" + + "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/communications/base" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// vars for the fund manager package +var ( + OrderManagerDelay = time.Second * 10 + ErrOrdersAlreadyExists = errors.New("order already exists") +) + +func (o *orderStore) Get() map[string][]order.Detail { + o.m.Lock() + defer o.m.Unlock() + return o.Orders +} + +func (o *orderStore) exists(order *order.Detail) bool { + r, ok := o.Orders[order.Exchange] + if !ok { + return false + } + + for x := range r { + if r[x].ID == order.ID { + return true + } + } + + return false +} + +func (o *orderStore) Add(order *order.Detail) error { + o.m.Lock() + defer o.m.Unlock() + + if o.exists(order) { + return ErrOrdersAlreadyExists + } + + orders := o.Orders[order.Exchange] + orders = append(orders, *order) + o.Orders[order.Exchange] = orders + return nil +} + +func (o *orderManager) Started() bool { + return atomic.LoadInt32(&o.started) == 1 +} + +func (o *orderManager) Start() error { + if atomic.AddInt32(&o.started, 1) != 1 { + return errors.New("order manager already started") + } + + log.Debugln(log.OrderBook, "Order manager starting...") + + o.shutdown = make(chan struct{}) + o.orderStore.Orders = make(map[string][]order.Detail) + go o.run() + return nil +} +func (o *orderManager) Stop() error { + if atomic.LoadInt32(&o.started) == 0 { + return errors.New("order manager not started") + } + + if atomic.AddInt32(&o.stopped, 1) != 1 { + return errors.New("order manager is already stopped") + } + defer func() { + atomic.CompareAndSwapInt32(&o.stopped, 1, 0) + atomic.CompareAndSwapInt32(&o.started, 1, 0) + }() + + log.Debugln(log.OrderBook, "Order manager shutting down...") + close(o.shutdown) + return nil +} + +func (o *orderManager) gracefulShutdown() { + if o.cfg.CancelOrdersOnShutdown { + log.Debugln(log.OrderMgr, "Order manager: Cancelling any open orders...") + orders := o.orderStore.Get() + if orders == nil { + return + } + + for k, v := range orders { + log.Debugf(log.OrderMgr, "Order manager: Cancelling order(s) for exchange %s.\n", k) + for y := range v { + log.Debugf(log.OrderMgr, "order manager: Cancelling order ID %v [%v]", + v[y].ID, v[y]) + err := o.Cancel(k, &order.Cancel{ + OrderID: v[y].ID, + }) + if err != nil { + msg := fmt.Sprintf("Order manager: Exchange %s unable to cancel order ID=%v. Err: %s", + k, v[y].ID, err) + log.Debugln(log.OrderBook, msg) + Bot.CommsManager.PushEvent(base.Event{ + Type: "order", + Message: msg, + }) + continue + } + + msg := fmt.Sprintf("Order manager: Exchange %s order ID=%v cancelled.", + k, v[y].ID) + log.Debugln(log.OrderBook, msg) + Bot.CommsManager.PushEvent(base.Event{ + Type: "order", + Message: msg, + }) + } + } + } +} + +func (o *orderManager) run() { + log.Debugln(log.OrderBook, "Order manager started.") + tick := time.NewTicker(OrderManagerDelay) + Bot.ServicesWG.Add(1) + defer func() { + log.Debugln(log.OrderMgr, "Order manager shutdown.") + tick.Stop() + Bot.ServicesWG.Done() + }() + + for { + select { + case <-o.shutdown: + o.gracefulShutdown() + return + case <-tick.C: + o.processOrders() + } + } +} + +func (o *orderManager) CancelAllOrders() {} + +func (o *orderManager) Cancel(exchName string, cancel *order.Cancel) error { + if exchName == "" { + return errors.New("order exchange name is empty") + } + + if cancel == nil { + return errors.New("order cancel param is nil") + } + + if cancel.OrderID == "" { + return errors.New("order id is empty") + } + + exch := GetExchangeByName(exchName) + if exch == nil { + return errors.New("unable to get exchange by name") + } + + if cancel.AssetType.String() != "" && !exch.GetAssetTypes().Contains(cancel.AssetType) { + return errors.New("order asset type not supported by exchange") + } + + return exch.CancelOrder(cancel) +} + +func (o *orderManager) Submit(exchName string, newOrder *order.Submit) (*orderSubmitResponse, error) { + if exchName == "" { + return nil, errors.New("order exchange name must be specified") + } + + if err := newOrder.Validate(); err != nil { + return nil, err + } + + if o.cfg.EnforceLimitConfig { + if !o.cfg.AllowMarketOrders && newOrder.OrderType == order.Market { + return nil, errors.New("order market type is not allowed") + } + + if o.cfg.LimitAmount > 0 && newOrder.Amount > o.cfg.LimitAmount { + return nil, errors.New("order limit exceeds allowed limit") + } + + if len(o.cfg.AllowedExchanges) > 0 && + !common.StringDataCompareInsensitive(o.cfg.AllowedExchanges, exchName) { + return nil, errors.New("order exchange not found in allowed list") + } + + if len(o.cfg.AllowedPairs) > 0 && !o.cfg.AllowedPairs.Contains(newOrder.Pair, true) { + return nil, errors.New("order pair not found in allowed list") + } + } + + exch := GetExchangeByName(exchName) + if exch == nil { + return nil, errors.New("unable to get exchange by name") + } + + id, err := uuid.NewV4() + if err != nil { + log.Warnf(log.OrderMgr, + "Order manager: Unable to generate UUID. Err: %s\n", + err) + } + + result, err := exch.SubmitOrder(newOrder) + if err != nil { + return nil, err + } + + if result.IsOrderPlaced { + return nil, errors.New("order unable to be placed") + } + + msg := fmt.Sprintf("Order manager: Exchange %s submitted order ID=%v [Ours: %v] pair=%v price=%v amount=%v side=%v type=%v.", + exchName, + result.OrderID, + id.String(), + newOrder.Pair, + newOrder.Price, + newOrder.Amount, + newOrder.OrderSide, + newOrder.OrderType) + + log.Debugln(log.OrderMgr, msg) + Bot.CommsManager.PushEvent(base.Event{ + Type: "order", + Message: msg, + }) + + return &orderSubmitResponse{ + SubmitResponse: order.SubmitResponse{ + OrderID: result.OrderID, + }, + OurOrderID: id.String(), + }, nil +} + +func (o *orderManager) processOrders() { + authExchanges := GetAuthAPISupportedExchanges() + for x := range authExchanges { + log.Debugf(log.OrderMgr, "Order manager: Procesing orders for exchange %v.\n", authExchanges[x]) + exch := GetExchangeByName(authExchanges[x]) + req := order.GetOrdersRequest{ + OrderSide: order.AnySide, + OrderType: order.AnyType, + } + result, err := exch.GetActiveOrders(&req) + if err != nil { + log.Warnf(log.OrderMgr, "Order manager: Unable to get active orders: %s\n", err) + continue + } + + for x := range result { + ord := &result[x] + result := o.orderStore.Add(ord) + if result != ErrOrdersAlreadyExists { + msg := fmt.Sprintf("Order manager: Exchange %s added order ID=%v pair=%v price=%v amount=%v side=%v type=%v.", + ord.Exchange, ord.ID, ord.CurrencyPair, ord.Price, ord.Amount, ord.OrderSide, ord.OrderType) + log.Debugf(log.OrderMgr, "%v\n", msg) + Bot.CommsManager.PushEvent(base.Event{ + Type: "order", + Message: msg, + }) + continue + } + } + } +} diff --git a/engine/orders_types.go b/engine/orders_types.go new file mode 100644 index 00000000..c0479299 --- /dev/null +++ b/engine/orders_types.go @@ -0,0 +1,36 @@ +package engine + +import ( + "sync" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) + +type orderManagerConfig struct { + EnforceLimitConfig bool + AllowMarketOrders bool + CancelOrdersOnShutdown bool + LimitAmount float64 + AllowedPairs currency.Pairs + AllowedExchanges []string + OrderSubmissionRetries int64 +} + +type orderStore struct { + m sync.Mutex + Orders map[string][]order.Detail +} + +type orderManager struct { + started int32 + stopped int32 + shutdown chan struct{} + orderStore orderStore + cfg orderManagerConfig +} + +type orderSubmitResponse struct { + order.SubmitResponse + OurOrderID string +} diff --git a/engine/portfolio.go b/engine/portfolio.go new file mode 100644 index 00000000..26e4644b --- /dev/null +++ b/engine/portfolio.go @@ -0,0 +1,85 @@ +package engine + +import ( + "errors" + "sync/atomic" + "time" + + log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/gocryptotrader/portfolio" +) + +// vars for the fund manager package +var ( + PortfolioSleepDelay = time.Minute +) + +type portfolioManager struct { + started int32 + stopped int32 + shutdown chan struct{} +} + +func (p *portfolioManager) Started() bool { + return atomic.LoadInt32(&p.started) == 1 +} + +func (p *portfolioManager) Start() error { + if atomic.AddInt32(&p.started, 1) != 1 { + return errors.New("portfolio manager already started") + } + + log.Debugln(log.PortfolioMgr, "Portfolio manager starting...") + Bot.Portfolio = &portfolio.Portfolio + Bot.Portfolio.Seed(Bot.Config.Portfolio) + p.shutdown = make(chan struct{}) + go p.run() + return nil +} +func (p *portfolioManager) Stop() error { + if atomic.AddInt32(&p.stopped, 1) != 1 { + return errors.New("portfolio manager is already stopped") + } + + log.Debugln(log.PortfolioMgr, "Portfolio manager shutting down...") + close(p.shutdown) + return nil +} + +func (p *portfolioManager) run() { + log.Debugln(log.PortfolioMgr, "Portfolio manager started.") + Bot.ServicesWG.Add(1) + tick := time.NewTicker(PortfolioSleepDelay) + defer func() { + atomic.CompareAndSwapInt32(&p.stopped, 1, 0) + atomic.CompareAndSwapInt32(&p.started, 1, 0) + tick.Stop() + Bot.ServicesWG.Done() + log.Debugf(log.PortfolioMgr, "Portfolio manager shutdown.") + }() + + for { + select { + case <-p.shutdown: + return + + case <-tick.C: + p.processPortfolio() + } + } +} + +func (p *portfolioManager) processPortfolio() { + pf := portfolio.GetPortfolio() + data := pf.GetPortfolioGroupedCoin() + for key, value := range data { + success := pf.UpdatePortfolio(value, key) + if success { + log.Debugf(log.PortfolioMgr, + "Portfolio manager: Successfully updated address balance for %s address(es) %s\n", + key, value, + ) + } + } + SeedExchangeAccountInfo(GetAllEnabledExchangeAccountInfo().Data) +} diff --git a/engine/restful_router.go b/engine/restful_router.go new file mode 100644 index 00000000..cc4d671d --- /dev/null +++ b/engine/restful_router.go @@ -0,0 +1,114 @@ +package engine + +import ( + "fmt" + "net/http" + _ "net/http/pprof" // nolint: gosec + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/thrasher-corp/gocryptotrader/common" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// RESTLogger logs the requests internally +func RESTLogger(inner http.Handler, name string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + inner.ServeHTTP(w, r) + + log.Debugf(log.RESTSys, + "%s\t%s\t%s\t%s", + r.Method, + r.RequestURI, + name, + time.Since(start), + ) + }) +} + +// StartRESTServer starts a REST server +func StartRESTServer() { + listenAddr := Bot.Config.RemoteControl.DeprecatedRPC.ListenAddress + log.Debugf(log.RESTSys, + "Deprecated RPC server support enabled. Listen URL: http://%s:%d\n", + common.ExtractHost(listenAddr), common.ExtractPort(listenAddr)) + err := http.ListenAndServe(listenAddr, newRouter(true)) + if err != nil { + log.Errorf(log.RESTSys, "Failed to start deprecated RPC server. Err: %s", err) + } +} + +// StartWebsocketServer starts a Websocket server +func StartWebsocketServer() { + listenAddr := Bot.Config.RemoteControl.WebsocketRPC.ListenAddress + log.Debugf(log.RESTSys, + "Websocket RPC support enabled. Listen URL: ws://%s:%d/ws\n", + common.ExtractHost(listenAddr), common.ExtractPort(listenAddr)) + err := http.ListenAndServe(listenAddr, newRouter(false)) + if err != nil { + log.Errorf(log.RESTSys, "Failed to start websocket RPC server. Err: %s", err) + } +} + +// newRouter takes in the exchange interfaces and returns a new multiplexor +// router +func newRouter(isREST bool) *mux.Router { + router := mux.NewRouter().StrictSlash(true) + var routes []Route + var listenAddr string + + if isREST { + listenAddr = Bot.Config.RemoteControl.DeprecatedRPC.ListenAddress + } else { + listenAddr = Bot.Config.RemoteControl.WebsocketRPC.ListenAddress + } + + if common.ExtractPort(listenAddr) == 80 { + listenAddr = common.ExtractHost(listenAddr) + } else { + listenAddr = strings.Join([]string{common.ExtractHost(listenAddr), + strconv.Itoa(common.ExtractPort(listenAddr))}, ":") + } + + if isREST { + routes = []Route{ + {"", http.MethodGet, "/", getIndex}, + {"GetAllSettings", http.MethodGet, "/config/all", RESTGetAllSettings}, + {"SaveAllSettings", http.MethodPost, "/config/all/save", RESTSaveAllSettings}, + {"AllEnabledAccountInfo", http.MethodGet, "/exchanges/enabled/accounts/all", RESTGetAllEnabledAccountInfo}, + {"AllActiveExchangesAndCurrencies", http.MethodGet, "/exchanges/enabled/latest/all", RESTGetAllActiveTickers}, + {"GetPortfolio", http.MethodGet, "/portfolio/all", RESTGetPortfolio}, + {"AllActiveExchangesAndOrderbooks", http.MethodGet, "/exchanges/orderbook/latest/all", RESTGetAllActiveOrderbooks}, + } + + if Bot.Config.Profiler.Enabled { + log.Debugf(log.RESTSys, + "HTTP Go performance profiler (pprof) endpoint enabled: http://%s:%d/debug\n", + common.ExtractHost(listenAddr), + common.ExtractPort(listenAddr)) + router.PathPrefix("/debug").Handler(http.DefaultServeMux) + } + } else { + routes = []Route{ + {"ws", http.MethodGet, "/ws", WebsocketClientHandler}, + } + } + + for _, route := range routes { + router. + Methods(route.Method). + Path(route.Pattern). + Name(route.Name). + Handler(RESTLogger(route.HandlerFunc, route.Name)). + Host(listenAddr) + } + return router +} + +func getIndex(w http.ResponseWriter, _ *http.Request) { + fmt.Fprint(w, "GoCryptoTrader RESTful interface. For the web GUI, please visit the web GUI readme.") + w.WriteHeader(http.StatusOK) +} diff --git a/engine/restful_server.go b/engine/restful_server.go new file mode 100644 index 00000000..575178e9 --- /dev/null +++ b/engine/restful_server.go @@ -0,0 +1,132 @@ +package engine + +import ( + "encoding/json" + "net/http" + + "github.com/thrasher-corp/gocryptotrader/config" + log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/gocryptotrader/portfolio" +) + +// RESTfulJSONResponse outputs a JSON response of the response interface +func RESTfulJSONResponse(w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + return json.NewEncoder(w).Encode(response) +} + +// RESTfulError prints the REST method and error +func RESTfulError(method string, err error) { + log.Errorf(log.RESTSys, "RESTful %s: server failed to send JSON response. Error %s\n", + method, err) +} + +// RESTGetAllSettings replies to a request with an encoded JSON response about the +// trading Bots configuration. +func RESTGetAllSettings(w http.ResponseWriter, r *http.Request) { + err := RESTfulJSONResponse(w, Bot.Config) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTSaveAllSettings saves all current settings from request body as a JSON +// document then reloads state and returns the settings +func RESTSaveAllSettings(w http.ResponseWriter, r *http.Request) { + // Get the data from the request + decoder := json.NewDecoder(r.Body) + var responseData config.Post + err := decoder.Decode(&responseData) + if err != nil { + RESTfulError(r.Method, err) + } + // Save change the settings + err = Bot.Config.UpdateConfig(Bot.Settings.ConfigFile, &responseData.Data, false) + if err != nil { + RESTfulError(r.Method, err) + } + + err = RESTfulJSONResponse(w, Bot.Config) + if err != nil { + RESTfulError(r.Method, err) + } + + SetupExchanges() +} + +// GetAllActiveOrderbooks returns all enabled exchanges orderbooks +func GetAllActiveOrderbooks() []EnabledExchangeOrderbooks { + var orderbookData []EnabledExchangeOrderbooks + + for _, exch := range Bot.Exchanges { + if !exch.IsEnabled() { + continue + } + + assets := exch.GetAssetTypes() + exchName := exch.GetName() + var exchangeOB EnabledExchangeOrderbooks + exchangeOB.ExchangeName = exchName + + for y := range assets { + currencies := exch.GetEnabledPairs(assets[y]) + for z := range currencies { + ob, err := exch.FetchOrderbook(currencies[z], assets[y]) + if err != nil { + log.Errorf(log.RESTSys, + "Exchange %s failed to retrieve %s orderbook. Err: %s\n", exchName, + currencies[z].String(), + err) + continue + } + exchangeOB.ExchangeValues = append(exchangeOB.ExchangeValues, ob) + } + orderbookData = append(orderbookData, exchangeOB) + } + orderbookData = append(orderbookData, exchangeOB) + } + return orderbookData +} + +// RESTGetAllActiveOrderbooks returns all enabled exchange orderbooks +func RESTGetAllActiveOrderbooks(w http.ResponseWriter, r *http.Request) { + var response AllEnabledExchangeOrderbooks + response.Data = GetAllActiveOrderbooks() + + err := RESTfulJSONResponse(w, response) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTGetPortfolio returns the Bot portfolio +func RESTGetPortfolio(w http.ResponseWriter, r *http.Request) { + p := portfolio.GetPortfolio() + result := p.GetPortfolioSummary() + err := RESTfulJSONResponse(w, result) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTGetAllActiveTickers returns all active tickers +func RESTGetAllActiveTickers(w http.ResponseWriter, r *http.Request) { + var response AllEnabledExchangeCurrencies + response.Data = GetAllActiveTickers() + + err := RESTfulJSONResponse(w, response) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTGetAllEnabledAccountInfo via get request returns JSON response of account +// info +func RESTGetAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) { + response := GetAllEnabledExchangeAccountInfo() + err := RESTfulJSONResponse(w, response) + if err != nil { + RESTfulError(r.Method, err) + } +} diff --git a/restful_server_test.go b/engine/restful_server_test.go similarity index 67% rename from restful_server_test.go rename to engine/restful_server_test.go index 191a85dc..631ecbe2 100644 --- a/restful_server_test.go +++ b/engine/restful_server_test.go @@ -1,4 +1,4 @@ -package main +package engine import ( "encoding/json" @@ -6,7 +6,6 @@ import ( "net/http" "net/http/httptest" "reflect" - "strings" "testing" "github.com/thrasher-corp/gocryptotrader/config" @@ -14,9 +13,9 @@ import ( func loadConfig(t *testing.T) *config.Config { cfg := config.GetConfig() - err := cfg.LoadConfig(strings.Replace(config.ConfigTestFile, "..", ".", 1)) + err := cfg.LoadConfig("", true) if err != nil { - t.Error("Test failed. GetCurrencyConfig LoadConfig error", err) + t.Error("GetCurrencyConfig LoadConfig error", err) } return cfg } @@ -26,7 +25,7 @@ func makeHTTPGetRequest(t *testing.T, response interface{}) *http.Response { err := RESTfulJSONResponse(w, response) if err != nil { - t.Error("Test failed. Failed to make response.", err) + t.Error("Failed to make response.", err) } return w.Result() } @@ -38,17 +37,17 @@ func TestConfigAllJsonResponse(t *testing.T) { body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { - t.Error("Test failed. Body not readable", err) + t.Error("Body not readable", err) } var responseConfig config.Config jsonErr := json.Unmarshal(body, &responseConfig) if jsonErr != nil { - t.Error("Test failed. Response not parseable as json", err) + t.Error("Response not parseable as json", err) } if reflect.DeepEqual(responseConfig, cfg) { - t.Error("Test failed. Json not equal to config") + t.Error("Json not equal to config") } } @@ -60,10 +59,10 @@ func TestInvalidHostRequest(t *testing.T) { req.Host = "invalidsite.com" resp := httptest.NewRecorder() - NewRouter().ServeHTTP(resp, req) + newRouter(true).ServeHTTP(resp, req) if status := resp.Code; status != http.StatusNotFound { - t.Errorf("Test failed. Response returned wrong status code expected %v got %v", http.StatusNotFound, status) + t.Errorf("Response returned wrong status code expected %v got %v", http.StatusNotFound, status) } } @@ -75,9 +74,9 @@ func TestValidHostRequest(t *testing.T) { req.Host = "localhost:9050" resp := httptest.NewRecorder() - NewRouter().ServeHTTP(resp, req) + newRouter(true).ServeHTTP(resp, req) if status := resp.Code; status != http.StatusOK { - t.Errorf("Test failed. Response returned wrong status code expected %v got %v", http.StatusOK, status) + t.Errorf("Response returned wrong status code expected %v got %v", http.StatusOK, status) } } diff --git a/engine/restful_types.go b/engine/restful_types.go new file mode 100644 index 00000000..ba84a39d --- /dev/null +++ b/engine/restful_types.go @@ -0,0 +1,46 @@ +package engine + +import ( + "net/http" + + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" +) + +// Route is a sub type that holds the request routes +type Route struct { + Name string + Method string + Pattern string + HandlerFunc http.HandlerFunc +} + +// AllEnabledExchangeOrderbooks holds the enabled exchange orderbooks +type AllEnabledExchangeOrderbooks struct { + Data []EnabledExchangeOrderbooks `json:"data"` +} + +// EnabledExchangeOrderbooks is a sub type for singular exchanges and respective +// orderbooks +type EnabledExchangeOrderbooks struct { + ExchangeName string `json:"exchangeName"` + ExchangeValues []orderbook.Base `json:"exchangeValues"` +} + +// AllEnabledExchangeCurrencies holds the enabled exchange currencies +type AllEnabledExchangeCurrencies struct { + Data []EnabledExchangeCurrencies `json:"data"` +} + +// EnabledExchangeCurrencies is a sub type for singular exchanges and respective +// currencies +type EnabledExchangeCurrencies struct { + ExchangeName string `json:"exchangeName"` + ExchangeValues []ticker.Price `json:"exchangeValues"` +} + +// AllEnabledExchangeAccounts holds all enabled accounts info +type AllEnabledExchangeAccounts struct { + Data []exchange.AccountInfo `json:"data"` +} diff --git a/engine/routines.go b/engine/routines.go new file mode 100644 index 00000000..87ded1e9 --- /dev/null +++ b/engine/routines.go @@ -0,0 +1,354 @@ +package engine + +import ( + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/stats" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +func printCurrencyFormat(price float64) string { + displaySymbol, err := currency.GetSymbolByCurrencyName(Bot.Config.Currency.FiatDisplayCurrency) + if err != nil { + log.Errorf(log.Global, "Failed to get display symbol: %s\n", err) + } + + return fmt.Sprintf("%s%.8f", displaySymbol, price) +} + +func printConvertCurrencyFormat(origCurrency currency.Code, origPrice float64) string { + displayCurrency := Bot.Config.Currency.FiatDisplayCurrency + conv, err := currency.ConvertCurrency(origPrice, + origCurrency, + displayCurrency) + if err != nil { + log.Errorf(log.Global, "Failed to convert currency: %s\n", err) + } + + displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency) + if err != nil { + log.Errorf(log.Global, "Failed to get display symbol: %s\n", err) + } + + origSymbol, err := currency.GetSymbolByCurrencyName(origCurrency) + if err != nil { + log.Errorf(log.Global, "Failed to get original currency symbol for %s: %s\n", + origCurrency, + err) + } + + return fmt.Sprintf("%s%.2f %s (%s%.2f %s)", + displaySymbol, + conv, + displayCurrency, + origSymbol, + origPrice, + origCurrency, + ) +} + +func printTickerSummary(result *ticker.Price, p currency.Pair, assetType asset.Item, exchangeName string, err error) { + if err != nil { + log.Errorf(log.Ticker, "Failed to get %s %s ticker. Error: %s\n", + p.String(), + exchangeName, + err) + return + } + + stats.Add(exchangeName, p, assetType, result.Last, result.Volume) + if p.Quote.IsFiatCurrency() && + p.Quote != Bot.Config.Currency.FiatDisplayCurrency { + origCurrency := p.Quote.Upper() + log.Infof(log.Ticker, "%s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f\n", + exchangeName, + FormatCurrency(p).String(), + strings.ToUpper(assetType.String()), + printConvertCurrencyFormat(origCurrency, result.Last), + printConvertCurrencyFormat(origCurrency, result.Ask), + printConvertCurrencyFormat(origCurrency, result.Bid), + printConvertCurrencyFormat(origCurrency, result.High), + printConvertCurrencyFormat(origCurrency, result.Low), + result.Volume) + } else { + if p.Quote.IsFiatCurrency() && + p.Quote == Bot.Config.Currency.FiatDisplayCurrency { + log.Infof(log.Ticker, "%s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f\n", + exchangeName, + FormatCurrency(p).String(), + strings.ToUpper(assetType.String()), + printCurrencyFormat(result.Last), + printCurrencyFormat(result.Ask), + printCurrencyFormat(result.Bid), + printCurrencyFormat(result.High), + printCurrencyFormat(result.Low), + result.Volume) + } else { + log.Infof(log.Ticker, "%s %s %s: TICKER: Last %.8f Ask %.8f Bid %.8f High %.8f Low %.8f Volume %.8f\n", + exchangeName, + FormatCurrency(p).String(), + strings.ToUpper(assetType.String()), + result.Last, + result.Ask, + result.Bid, + result.High, + result.Low, + result.Volume) + } + } +} + +func printOrderbookSummary(result *orderbook.Base, p currency.Pair, assetType asset.Item, exchangeName string, err error) { + if err != nil { + log.Errorf(log.OrderBook, "Failed to get %s %s orderbook of type %s. Error: %s\n", + p, + exchangeName, + assetType, + err) + return + } + + bidsAmount, bidsValue := result.TotalBidsAmount() + asksAmount, asksValue := result.TotalAsksAmount() + + if p.Quote.IsFiatCurrency() && + p.Quote != Bot.Config.Currency.FiatDisplayCurrency { + origCurrency := p.Quote.Upper() + log.Infof(log.OrderBook, "%s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s\n", + exchangeName, + FormatCurrency(p).String(), + strings.ToUpper(assetType.String()), + len(result.Bids), + bidsAmount, + p.Base.String(), + printConvertCurrencyFormat(origCurrency, bidsValue), + len(result.Asks), + asksAmount, + p.Base.String(), + printConvertCurrencyFormat(origCurrency, asksValue), + ) + } else { + if p.Quote.IsFiatCurrency() && + p.Quote == Bot.Config.Currency.FiatDisplayCurrency { + log.Infof(log.OrderBook, "%s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s\n", + exchangeName, + FormatCurrency(p).String(), + strings.ToUpper(assetType.String()), + len(result.Bids), + bidsAmount, + p.Base.String(), + printCurrencyFormat(bidsValue), + len(result.Asks), + asksAmount, + p.Base.String(), + printCurrencyFormat(asksValue), + ) + } else { + log.Infof(log.OrderBook, "%s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %f Asks len: %d Amount: %f %s. Total value: %f\n", + exchangeName, + FormatCurrency(p).String(), + strings.ToUpper(assetType.String()), + len(result.Bids), + bidsAmount, + p.Base.String(), + bidsValue, + len(result.Asks), + asksAmount, + p.Base.String(), + asksValue, + ) + } + } +} + +func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string) { + evt := WebsocketEvent{ + Data: result, + Event: event, + AssetType: assetType, + Exchange: exchangeName, + } + err := BroadcastWebsocketMessage(evt) + if err != nil { + log.Errorf(log.WebsocketMgr, "Failed to broadcast websocket event %v. Error: %s\n", + event, err) + } +} + +// WebsocketRoutine Initial routine management system for websocket +func WebsocketRoutine() { + if Bot.Settings.Verbose { + log.Debugln(log.WebsocketMgr, "Connecting exchange websocket services...") + } + + for i := range Bot.Exchanges { + go func(i int) { + if Bot.Exchanges[i].SupportsWebsocket() { + if Bot.Settings.Verbose { + log.Debugf(log.WebsocketMgr, "Exchange %s websocket support: Yes Enabled: %v\n", Bot.Exchanges[i].GetName(), + common.IsEnabled(Bot.Exchanges[i].IsWebsocketEnabled())) + } + + // TO-DO: expose IsConnected() and IsConnecting so this can be simplified + if Bot.Exchanges[i].IsWebsocketEnabled() { + ws, err := Bot.Exchanges[i].GetWebsocket() + if err != nil { + log.Errorf(log.WebsocketMgr, "Exchange %s GetWebsocket error: %s\n", + Bot.Exchanges[i].GetName(), err) + return + } + + // Exchange sync manager might have already started ws + // service or is in the process of connecting, so check + if ws.IsConnected() || ws.IsConnecting() { + return + } + + // Data handler routine + go WebsocketDataHandler(ws) + + err = ws.Connect() + if err != nil { + log.Errorf(log.WebsocketMgr, "%v\n", err) + } + } + } else if Bot.Settings.Verbose { + log.Debugf(log.WebsocketMgr, "Exchange %s websocket support: No\n", Bot.Exchanges[i].GetName()) + } + }(i) + } +} + +var shutdowner = make(chan struct{}, 1) +var wg sync.WaitGroup + +// Websocketshutdown shuts down the exchange routines and then shuts down +// governing routines +func Websocketshutdown(ws *wshandler.Websocket) error { + err := ws.Shutdown() // shutdown routines on the exchange + if err != nil { + log.Errorf(log.WebsocketMgr, "routines.go error - failed to shutdown %s\n", err) + } + + timer := time.NewTimer(5 * time.Second) + c := make(chan struct{}, 1) + + go func(c chan struct{}) { + close(shutdowner) + wg.Wait() + c <- struct{}{} + }(c) + + select { + case <-timer.C: + return errors.New("routines.go error - failed to shutdown routines") + + case <-c: + return nil + } +} + +// WebsocketDataHandler handles websocket data coming from a websocket feed +// associated with an exchange +func WebsocketDataHandler(ws *wshandler.Websocket) { + wg.Add(1) + defer wg.Done() + + for { + select { + case <-shutdowner: + return + + case data := <-ws.DataHandler: + switch d := data.(type) { + case string: + switch d { + case wshandler.WebsocketNotEnabled: + if Bot.Settings.Verbose { + log.Warnf(log.WebsocketMgr, "routines.go warning - exchange %s websocket not enabled\n", + ws.GetName()) + } + + default: + log.Info(log.WebsocketMgr, d) + } + + case error: + log.Errorf(log.WebsocketMgr, "routines.go exchange %s websocket error - %s", ws.GetName(), data) + case wshandler.TradeData: + // Trade Data + // if Bot.Settings.Verbose { + // log.Println("Websocket trades Updated: ", data.(exchange.TradeData)) + // } + + case wshandler.TickerData: + // Ticker data + // if Bot.Settings.Verbose { + // log.Println("Websocket Ticker Updated: ", data.(exchange.TickerData)) + // } + + tickerNew := ticker.Price{ + Last: d.Last, + High: d.High, + Low: d.Low, + Bid: d.Bid, + Ask: d.Ask, + Volume: d.Volume, + QuoteVolume: d.QuoteVolume, + PriceATH: d.PriceATH, + Open: d.Open, + Close: d.Close, + Pair: d.Pair, + LastUpdated: d.Timestamp, + } + if Bot.Settings.EnableExchangeSyncManager && Bot.ExchangeCurrencyPairManager != nil { + Bot.ExchangeCurrencyPairManager.update(ws.GetName(), + d.Pair, d.AssetType, SyncItemTicker, nil) + } + err := ticker.ProcessTicker(ws.GetName(), &tickerNew, d.AssetType) + if err != nil { + log.Errorf(log.WebsocketMgr, "routines.go exchange %s websocket error - %s", ws.GetName(), err) + } + printTickerSummary(&tickerNew, tickerNew.Pair, d.AssetType, ws.GetName(), nil) + case wshandler.KlineData: + // Kline data + if Bot.Settings.Verbose { + log.Infof(log.WebsocketMgr, "Websocket Kline Updated: %v\n", d) + } + case wshandler.WebsocketOrderbookUpdate: + // Orderbook data + result := data.(wshandler.WebsocketOrderbookUpdate) + if Bot.Settings.EnableExchangeSyncManager && Bot.ExchangeCurrencyPairManager != nil { + Bot.ExchangeCurrencyPairManager.update(ws.GetName(), + result.Pair, result.Asset, SyncItemOrderbook, nil) + } + // TO-DO: printOrderbookSummary + //nolint:gocritic + if Bot.Settings.Verbose { + log.Infof(log.WebsocketMgr, + "Websocket %s %s orderbook updated\n", + ws.GetName(), + FormatCurrency(result.Pair), + ) + } + default: + if Bot.Settings.Verbose { + log.Warnf(log.WebsocketMgr, + "Websocket %s Unknown type: %v\n", + ws.GetName(), + d) + } + } + } + } +} diff --git a/engine/rpcserver.go b/engine/rpcserver.go new file mode 100644 index 00000000..5e862b55 --- /dev/null +++ b/engine/rpcserver.go @@ -0,0 +1,1210 @@ +package engine + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "path/filepath" + "strings" + "time" + + grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + grpcruntime "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/database/models/postgres" + "github.com/thrasher-corp/gocryptotrader/database/models/sqlite3" + "github.com/thrasher-corp/gocryptotrader/database/repository/audit" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/gctrpc" + "github.com/thrasher-corp/gocryptotrader/gctrpc/auth" + log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/gocryptotrader/portfolio" + "github.com/thrasher-corp/gocryptotrader/utils" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" +) + +const ( + errExchangeNameUnset = "exchange name unset" + errCurrencyPairUnset = "currency pair unset" + errAssetTypeUnset = "asset type unset" + errDispatchSystem = "dispatch system offline" +) + +// RPCServer struct +type RPCServer struct{} + +func authenticateClient(ctx context.Context) (context.Context, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return ctx, fmt.Errorf("unable to extract metadata") + } + + authStr, ok := md["authorization"] + if !ok { + return ctx, fmt.Errorf("authorization header missing") + } + + if !strings.Contains(authStr[0], "Basic") { + return ctx, fmt.Errorf("basic not found in authorization header") + } + + decoded, err := crypto.Base64Decode(strings.Split(authStr[0], " ")[1]) + if err != nil { + return ctx, fmt.Errorf("unable to base64 decode authorization header") + } + + username := strings.Split(string(decoded), ":")[0] + password := strings.Split(string(decoded), ":")[1] + + if username != Bot.Config.RemoteControl.Username || password != Bot.Config.RemoteControl.Password { + return ctx, fmt.Errorf("username/password mismatch") + } + + return ctx, nil +} + +// StartRPCServer starts a gRPC server with TLS auth +func StartRPCServer() { + err := checkCerts() + if err != nil { + log.Errorf(log.GRPCSys, "gRPC checkCerts failed. err: %s\n", err) + return + } + + log.Debugf(log.GRPCSys, "gRPC server support enabled. Starting gRPC server on https://%v.\n", Bot.Config.RemoteControl.GRPC.ListenAddress) + lis, err := net.Listen("tcp", Bot.Config.RemoteControl.GRPC.ListenAddress) + if err != nil { + log.Errorf(log.GRPCSys, "gRPC server failed to bind to port: %s", err) + return + } + + targetDir := utils.GetTLSDir(Bot.Settings.DataDir) + creds, err := credentials.NewServerTLSFromFile(filepath.Join(targetDir, "cert.pem"), filepath.Join(targetDir, "key.pem")) + if err != nil { + log.Errorf(log.GRPCSys, "gRPC server could not load TLS keys: %s\n", err) + return + } + + opts := []grpc.ServerOption{ + grpc.Creds(creds), + grpc.UnaryInterceptor(grpcauth.UnaryServerInterceptor(authenticateClient)), + } + server := grpc.NewServer(opts...) + s := RPCServer{} + gctrpc.RegisterGoCryptoTraderServer(server, &s) + + go func() { + if err := server.Serve(lis); err != nil { + log.Errorf(log.GRPCSys, "gRPC server failed to serve: %s\n", err) + return + } + }() + + log.Debugln(log.GRPCSys, "gRPC server started!") + + if Bot.Settings.EnableGRPCProxy { + StartRPCRESTProxy() + } +} + +// StartRPCRESTProxy starts a gRPC proxy +func StartRPCRESTProxy() { + log.Debugf(log.GRPCSys, "gRPC proxy server support enabled. Starting gRPC proxy server on http://%v.\n", Bot.Config.RemoteControl.GRPC.GRPCProxyListenAddress) + + targetDir := utils.GetTLSDir(Bot.Settings.DataDir) + creds, err := credentials.NewClientTLSFromFile(filepath.Join(targetDir, "cert.pem"), "") + if err != nil { + log.Errorf(log.GRPCSys, "Unabled to start gRPC proxy. Err: %s\n", err) + return + } + + mux := grpcruntime.NewServeMux() + opts := []grpc.DialOption{grpc.WithTransportCredentials(creds), + grpc.WithPerRPCCredentials(auth.BasicAuth{ + Username: Bot.Config.RemoteControl.Username, + Password: Bot.Config.RemoteControl.Password, + }), + } + err = gctrpc.RegisterGoCryptoTraderHandlerFromEndpoint(context.Background(), + mux, Bot.Config.RemoteControl.GRPC.ListenAddress, opts) + if err != nil { + log.Errorf(log.GRPCSys, "Failed to register gRPC proxy. Err: %s\n", err) + return + } + + go func() { + if err := http.ListenAndServe(Bot.Config.RemoteControl.GRPC.GRPCProxyListenAddress, mux); err != nil { + log.Errorf(log.GRPCSys, "gRPC proxy failed to server: %s\n", err) + return + } + }() + + log.Debugln(log.GRPCSys, "gRPC proxy server started!") +} + +// GetInfo returns info about the current GoCryptoTrader session +func (s *RPCServer) GetInfo(ctx context.Context, r *gctrpc.GetInfoRequest) (*gctrpc.GetInfoResponse, error) { + d := time.Since(Bot.Uptime) + resp := gctrpc.GetInfoResponse{ + Uptime: d.String(), + EnabledExchanges: int64(Bot.Config.CountEnabledExchanges()), + AvailableExchanges: int64(len(Bot.Config.Exchanges)), + DefaultFiatCurrency: Bot.Config.Currency.FiatDisplayCurrency.String(), + DefaultForexProvider: Bot.Config.GetPrimaryForexProvider(), + SubsystemStatus: GetSubsystemsStatus(), + } + endpoints := GetRPCEndpoints() + resp.RpcEndpoints = make(map[string]*gctrpc.RPCEndpoint) + for k, v := range endpoints { + resp.RpcEndpoints[k] = &gctrpc.RPCEndpoint{ + Started: v.Started, + ListenAddress: v.ListenAddr, + } + } + return &resp, nil +} + +// GetSubsystems returns a list of subsystems and their status +func (s *RPCServer) GetSubsystems(ctx context.Context, r *gctrpc.GetSubsystemsRequest) (*gctrpc.GetSusbsytemsResponse, error) { + return &gctrpc.GetSusbsytemsResponse{SubsystemsStatus: GetSubsystemsStatus()}, nil +} + +// EnableSubsystem enables a engine subsytem +func (s *RPCServer) EnableSubsystem(ctx context.Context, r *gctrpc.GenericSubsystemRequest) (*gctrpc.GenericSubsystemResponse, error) { + err := SetSubsystem(r.Subsystem, true) + return &gctrpc.GenericSubsystemResponse{}, err +} + +// DisableSubsystem disables a engine subsytem +func (s *RPCServer) DisableSubsystem(ctx context.Context, r *gctrpc.GenericSubsystemRequest) (*gctrpc.GenericSubsystemResponse, error) { + err := SetSubsystem(r.Subsystem, false) + return &gctrpc.GenericSubsystemResponse{}, err +} + +// GetRPCEndpoints returns a list of API endpoints +func (s *RPCServer) GetRPCEndpoints(ctx context.Context, r *gctrpc.GetRPCEndpointsRequest) (*gctrpc.GetRPCEndpointsResponse, error) { + endpoints := GetRPCEndpoints() + var resp gctrpc.GetRPCEndpointsResponse + resp.Endpoints = make(map[string]*gctrpc.RPCEndpoint) + for k, v := range endpoints { + resp.Endpoints[k] = &gctrpc.RPCEndpoint{ + Started: v.Started, + ListenAddress: v.ListenAddr, + } + } + return &resp, nil +} + +// GetCommunicationRelayers returns the status of the engines communication relayers +func (s *RPCServer) GetCommunicationRelayers(ctx context.Context, r *gctrpc.GetCommunicationRelayersRequest) (*gctrpc.GetCommunicationRelayersResponse, error) { + relayers, err := Bot.CommsManager.GetStatus() + if err != nil { + return nil, err + } + + var resp gctrpc.GetCommunicationRelayersResponse + resp.CommunicationRelayers = make(map[string]*gctrpc.CommunicationRelayer) + for k, v := range relayers { + resp.CommunicationRelayers[k] = &gctrpc.CommunicationRelayer{ + Enabled: v.Enabled, + Connected: v.Connected, + } + } + return &resp, nil +} + +// GetExchanges returns a list of exchanges +// Param is whether or not you wish to list enabled exchanges +func (s *RPCServer) GetExchanges(ctx context.Context, r *gctrpc.GetExchangesRequest) (*gctrpc.GetExchangesResponse, error) { + exchanges := strings.Join(GetExchanges(r.Enabled), ",") + return &gctrpc.GetExchangesResponse{Exchanges: exchanges}, nil +} + +// DisableExchange disables an exchange +func (s *RPCServer) DisableExchange(ctx context.Context, r *gctrpc.GenericExchangeNameRequest) (*gctrpc.GenericExchangeNameResponse, error) { + err := UnloadExchange(r.Exchange) + return &gctrpc.GenericExchangeNameResponse{}, err +} + +// EnableExchange enables an exchange +func (s *RPCServer) EnableExchange(ctx context.Context, r *gctrpc.GenericExchangeNameRequest) (*gctrpc.GenericExchangeNameResponse, error) { + err := LoadExchange(r.Exchange, false, nil) + return &gctrpc.GenericExchangeNameResponse{}, err +} + +// GetExchangeOTPCode retrieves an exchanges OTP code +func (s *RPCServer) GetExchangeOTPCode(ctx context.Context, r *gctrpc.GenericExchangeNameRequest) (*gctrpc.GetExchangeOTPReponse, error) { + result, err := GetExchangeoOTPByName(r.Exchange) + return &gctrpc.GetExchangeOTPReponse{OtpCode: result}, err +} + +// GetExchangeOTPCodes retrieves OTP codes for all exchanges which have an +// OTP secret installed +func (s *RPCServer) GetExchangeOTPCodes(ctx context.Context, r *gctrpc.GetExchangeOTPsRequest) (*gctrpc.GetExchangeOTPsResponse, error) { + result, err := GetExchangeOTPs() + return &gctrpc.GetExchangeOTPsResponse{OtpCodes: result}, err +} + +// GetExchangeInfo gets info for a specific exchange +func (s *RPCServer) GetExchangeInfo(ctx context.Context, r *gctrpc.GenericExchangeNameRequest) (*gctrpc.GetExchangeInfoResponse, error) { + exchCfg, err := Bot.Config.GetExchangeConfig(r.Exchange) + if err != nil { + return nil, err + } + + resp := &gctrpc.GetExchangeInfoResponse{ + Name: exchCfg.Name, + Enabled: exchCfg.Enabled, + Verbose: exchCfg.Verbose, + UsingSandbox: exchCfg.UseSandbox, + HttpTimeout: exchCfg.HTTPTimeout.String(), + HttpUseragent: exchCfg.HTTPUserAgent, + HttpProxy: exchCfg.ProxyAddress, + BaseCurrencies: strings.Join(exchCfg.BaseCurrencies.Strings(), ","), + } + + resp.SupportedAssets = make(map[string]*gctrpc.PairsSupported) + for x := range exchCfg.CurrencyPairs.AssetTypes { + a := exchCfg.CurrencyPairs.AssetTypes[x] + resp.SupportedAssets[a.String()] = &gctrpc.PairsSupported{ + EnabledPairs: exchCfg.CurrencyPairs.Get(a).Enabled.Join(), + AvailablePairs: exchCfg.CurrencyPairs.Get(a).Available.Join(), + } + } + return resp, nil +} + +// GetTicker returns the ticker for a specified exchange, currency pair and +// asset type +func (s *RPCServer) GetTicker(ctx context.Context, r *gctrpc.GetTickerRequest) (*gctrpc.TickerResponse, error) { + t, err := GetSpecificTicker( + currency.Pair{ + Delimiter: r.Pair.Delimiter, + Base: currency.NewCode(r.Pair.Base), + Quote: currency.NewCode(r.Pair.Quote), + }, + r.Exchange, + asset.Item(r.AssetType), + ) + if err != nil { + return nil, err + } + + resp := &gctrpc.TickerResponse{ + Pair: r.Pair, + LastUpdated: t.LastUpdated.Unix(), + Last: t.Last, + High: t.High, + Low: t.Low, + Bid: t.Bid, + Ask: t.Ask, + Volume: t.Volume, + PriceAth: t.PriceATH, + } + + return resp, nil +} + +// GetTickers returns a list of tickers for all enabled exchanges and all +// enabled currency pairs +func (s *RPCServer) GetTickers(ctx context.Context, r *gctrpc.GetTickersRequest) (*gctrpc.GetTickersResponse, error) { + activeTickers := GetAllActiveTickers() + var tickers []*gctrpc.Tickers + + for x := range activeTickers { + var ticker gctrpc.Tickers + ticker.Exchange = activeTickers[x].ExchangeName + for y := range activeTickers[x].ExchangeValues { + t := activeTickers[x].ExchangeValues[y] + ticker.Tickers = append(ticker.Tickers, &gctrpc.TickerResponse{ + Pair: &gctrpc.CurrencyPair{ + Delimiter: t.Pair.Delimiter, + Base: t.Pair.Base.String(), + Quote: t.Pair.Quote.String(), + }, + LastUpdated: t.LastUpdated.Unix(), + Last: t.Last, + High: t.High, + Low: t.Low, + Bid: t.Bid, + Ask: t.Ask, + Volume: t.Volume, + PriceAth: t.PriceATH, + }) + } + tickers = append(tickers, &ticker) + } + + return &gctrpc.GetTickersResponse{Tickers: tickers}, nil +} + +// GetOrderbook returns an orderbook for a specific exchange, currency pair +// and asset type +func (s *RPCServer) GetOrderbook(ctx context.Context, r *gctrpc.GetOrderbookRequest) (*gctrpc.OrderbookResponse, error) { + ob, err := GetSpecificOrderbook( + currency.Pair{ + Delimiter: r.Pair.Delimiter, + Base: currency.NewCode(r.Pair.Base), + Quote: currency.NewCode(r.Pair.Quote), + }, + r.Exchange, + asset.Item(r.AssetType), + ) + if err != nil { + return nil, err + } + + var bids []*gctrpc.OrderbookItem + for x := range ob.Bids { + bids = append(bids, &gctrpc.OrderbookItem{ + Amount: ob.Bids[x].Amount, + Price: ob.Bids[x].Price, + }) + } + + var asks []*gctrpc.OrderbookItem + for x := range ob.Asks { + asks = append(asks, &gctrpc.OrderbookItem{ + Amount: ob.Asks[x].Amount, + Price: ob.Asks[x].Price, + }) + } + + resp := &gctrpc.OrderbookResponse{ + Pair: r.Pair, + Bids: bids, + Asks: asks, + LastUpdated: ob.LastUpdated.Unix(), + AssetType: r.AssetType, + } + + return resp, nil +} + +// GetOrderbooks returns a list of orderbooks for all enabled exchanges and all +// enabled currency pairs +func (s *RPCServer) GetOrderbooks(ctx context.Context, r *gctrpc.GetOrderbooksRequest) (*gctrpc.GetOrderbooksResponse, error) { + activeOrderbooks := GetAllActiveOrderbooks() + var orderbooks []*gctrpc.Orderbooks + + for x := range activeOrderbooks { + var ob gctrpc.Orderbooks + ob.Exchange = activeOrderbooks[x].ExchangeName + for y := range activeOrderbooks[x].ExchangeValues { + o := activeOrderbooks[x].ExchangeValues[y] + var bids []*gctrpc.OrderbookItem + for z := range o.Bids { + bids = append(bids, &gctrpc.OrderbookItem{ + Amount: o.Bids[z].Amount, + Price: o.Bids[z].Price, + }) + } + + var asks []*gctrpc.OrderbookItem + for z := range o.Asks { + asks = append(asks, &gctrpc.OrderbookItem{ + Amount: o.Asks[z].Amount, + Price: o.Asks[z].Price, + }) + } + + ob.Orderbooks = append(ob.Orderbooks, &gctrpc.OrderbookResponse{ + Pair: &gctrpc.CurrencyPair{ + Delimiter: o.Pair.Delimiter, + Base: o.Pair.Base.String(), + Quote: o.Pair.Quote.String(), + }, + LastUpdated: o.LastUpdated.Unix(), + Bids: bids, + Asks: asks, + }) + } + orderbooks = append(orderbooks, &ob) + } + + return &gctrpc.GetOrderbooksResponse{Orderbooks: orderbooks}, nil +} + +// GetAccountInfo returns an account balance for a specific exchange +func (s *RPCServer) GetAccountInfo(ctx context.Context, r *gctrpc.GetAccountInfoRequest) (*gctrpc.GetAccountInfoResponse, error) { + exch := GetExchangeByName(r.Exchange) + if exch == nil { + return nil, errors.New("exchange is not loaded/doesn't exist") + } + + resp, err := exch.GetAccountInfo() + if err != nil { + return nil, err + } + + var accounts []*gctrpc.Account + for x := range resp.Accounts { + var a gctrpc.Account + a.Id = resp.Accounts[x].ID + for _, y := range resp.Accounts[x].Currencies { + a.Currencies = append(a.Currencies, &gctrpc.AccountCurrencyInfo{ + Currency: y.CurrencyName.String(), + Hold: y.Hold, + TotalValue: y.TotalValue, + }) + } + accounts = append(accounts, &a) + } + + return &gctrpc.GetAccountInfoResponse{Exchange: r.Exchange, Accounts: accounts}, nil +} + +// GetConfig returns the bots config +func (s *RPCServer) GetConfig(ctx context.Context, r *gctrpc.GetConfigRequest) (*gctrpc.GetConfigResponse, error) { + return &gctrpc.GetConfigResponse{}, common.ErrNotYetImplemented +} + +// GetPortfolio returns the portfolio details +func (s *RPCServer) GetPortfolio(ctx context.Context, r *gctrpc.GetPortfolioRequest) (*gctrpc.GetPortfolioResponse, error) { + var addrs []*gctrpc.PortfolioAddress + botAddrs := Bot.Portfolio.Addresses + + for x := range botAddrs { + addrs = append(addrs, &gctrpc.PortfolioAddress{ + Address: botAddrs[x].Address, + CoinType: botAddrs[x].CoinType.String(), + Description: botAddrs[x].Description, + Balance: botAddrs[x].Balance, + }) + } + + resp := &gctrpc.GetPortfolioResponse{ + Portfolio: addrs, + } + + return resp, nil +} + +// GetPortfolioSummary returns the portfolio summary +func (s *RPCServer) GetPortfolioSummary(ctx context.Context, r *gctrpc.GetPortfolioSummaryRequest) (*gctrpc.GetPortfolioSummaryResponse, error) { + result := Bot.Portfolio.GetPortfolioSummary() + var resp gctrpc.GetPortfolioSummaryResponse + + p := func(coins []portfolio.Coin) []*gctrpc.Coin { + var c []*gctrpc.Coin + for x := range coins { + c = append(c, + &gctrpc.Coin{ + Coin: coins[x].Coin.String(), + Balance: coins[x].Balance, + Address: coins[x].Address, + Percentage: coins[x].Percentage, + }, + ) + } + return c + } + + resp.CoinTotals = p(result.Totals) + resp.CoinsOffline = p(result.Offline) + resp.CoinsOfflineSummary = make(map[string]*gctrpc.OfflineCoins) + for k, v := range result.OfflineSummary { + var o []*gctrpc.OfflineCoinSummary + for x := range v { + o = append(o, + &gctrpc.OfflineCoinSummary{ + Address: v[x].Address, + Balance: v[x].Balance, + Percentage: v[x].Percentage, + }, + ) + } + resp.CoinsOfflineSummary[k.String()] = &gctrpc.OfflineCoins{ + Addresses: o, + } + } + resp.CoinsOnline = p(result.Online) + resp.CoinsOnlineSummary = make(map[string]*gctrpc.OnlineCoins) + for k, v := range result.OnlineSummary { + o := make(map[string]*gctrpc.OnlineCoinSummary) + for x, y := range v { + o[x.String()] = &gctrpc.OnlineCoinSummary{ + Balance: y.Balance, + Percentage: y.Percentage, + } + } + resp.CoinsOnlineSummary[k] = &gctrpc.OnlineCoins{ + Coins: o, + } + } + + return &resp, nil +} + +// AddPortfolioAddress adds an address to the portfolio manager +func (s *RPCServer) AddPortfolioAddress(ctx context.Context, r *gctrpc.AddPortfolioAddressRequest) (*gctrpc.AddPortfolioAddressResponse, error) { + err := Bot.Portfolio.AddAddress(r.Address, r.Description, currency.NewCode(r.CoinType), r.Balance) + return &gctrpc.AddPortfolioAddressResponse{}, err +} + +// RemovePortfolioAddress removes an address from the portfolio manager +func (s *RPCServer) RemovePortfolioAddress(ctx context.Context, r *gctrpc.RemovePortfolioAddressRequest) (*gctrpc.RemovePortfolioAddressResponse, error) { + err := Bot.Portfolio.RemoveAddress(r.Address, r.Description, currency.NewCode(r.CoinType)) + return &gctrpc.RemovePortfolioAddressResponse{}, err +} + +// GetForexProviders returns a list of available forex providers +func (s *RPCServer) GetForexProviders(ctx context.Context, r *gctrpc.GetForexProvidersRequest) (*gctrpc.GetForexProvidersResponse, error) { + providers := Bot.Config.GetForexProvidersConfig() + if len(providers) == 0 { + return nil, fmt.Errorf("forex providers is empty") + } + + var forexProviders []*gctrpc.ForexProvider + for x := range providers { + forexProviders = append(forexProviders, &gctrpc.ForexProvider{ + Name: providers[x].Name, + Enabled: providers[x].Enabled, + Verbose: providers[x].Verbose, + RestPollingDelay: providers[x].RESTPollingDelay.String(), + ApiKey: providers[x].APIKey, + ApiKeyLevel: int64(providers[x].APIKeyLvl), + PrimaryProvider: providers[x].PrimaryProvider, + }) + } + return &gctrpc.GetForexProvidersResponse{ForexProviders: forexProviders}, nil +} + +// GetForexRates returns a list of forex rates +func (s *RPCServer) GetForexRates(ctx context.Context, r *gctrpc.GetForexRatesRequest) (*gctrpc.GetForexRatesResponse, error) { + rates, err := currency.GetExchangeRates() + if err != nil { + return nil, err + } + + if len(rates) == 0 { + return nil, fmt.Errorf("forex rates is empty") + } + + var forexRates []*gctrpc.ForexRatesConversion + for x := range rates { + rate, err := rates[x].GetRate() + if err != nil { + continue + } + + // TODO + // inverseRate, err := rates[x].GetInversionRate() + // if err != nil { + // continue + // } + + forexRates = append(forexRates, &gctrpc.ForexRatesConversion{ + From: rates[x].From.String(), + To: rates[x].To.String(), + Rate: rate, + InverseRate: 0, + }) + } + return &gctrpc.GetForexRatesResponse{ForexRates: forexRates}, nil +} + +// GetOrders returns all open orders, filtered by exchange, currency pair or +// asset type +func (s *RPCServer) GetOrders(ctx context.Context, r *gctrpc.GetOrdersRequest) (*gctrpc.GetOrdersResponse, error) { + exch := GetExchangeByName(r.Exchange) + if exch == nil { + return nil, errors.New("exchange is not loaded/doesn't exist") + } + + resp, err := exch.GetActiveOrders(&order.GetOrdersRequest{}) + if err != nil { + return nil, err + } + + var orders []*gctrpc.OrderDetails + for x := range resp { + orders = append(orders, &gctrpc.OrderDetails{ + Exchange: r.Exchange, + Id: resp[x].ID, + BaseCurrency: resp[x].CurrencyPair.Base.String(), + QuoteCurrency: resp[x].CurrencyPair.Quote.String(), + AssetType: asset.Spot.String(), + OrderType: resp[x].OrderType.String(), + OrderSide: resp[x].OrderSide.String(), + CreationTime: resp[x].OrderDate.Unix(), + Status: resp[x].Status.String(), + Price: resp[x].Price, + Amount: resp[x].Amount, + }) + } + + return &gctrpc.GetOrdersResponse{Orders: orders}, nil +} + +// GetOrder returns order information based on exchange and order ID +func (s *RPCServer) GetOrder(ctx context.Context, r *gctrpc.GetOrderRequest) (*gctrpc.OrderDetails, error) { + return &gctrpc.OrderDetails{}, common.ErrNotYetImplemented +} + +// SubmitOrder submits an order specified by exchange, currency pair and asset +// type +func (s *RPCServer) SubmitOrder(ctx context.Context, r *gctrpc.SubmitOrderRequest) (*gctrpc.SubmitOrderResponse, error) { + exch := GetExchangeByName(r.Exchange) + if exch == nil { + return nil, errors.New("exchange is not loaded/doesn't exist") + } + + p := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) + submission := &order.Submit{ + Pair: p, + OrderSide: order.Side(r.Side), + OrderType: order.Type(r.OrderType), + Amount: r.Amount, + Price: r.Price, + ClientID: r.ClientId, + } + result, err := exch.SubmitOrder(submission) + return &gctrpc.SubmitOrderResponse{ + OrderId: result.OrderID, + OrderPlaced: result.IsOrderPlaced, + }, err +} + +// SimulateOrder simulates an order specified by exchange, currency pair and asset +// type +func (s *RPCServer) SimulateOrder(ctx context.Context, r *gctrpc.SimulateOrderRequest) (*gctrpc.SimulateOrderResponse, error) { + exch := GetExchangeByName(r.Exchange) + if exch == nil { + return nil, errors.New("exchange is not loaded/doesn't exist") + } + + p := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) + o, err := exch.FetchOrderbook(p, asset.Spot) + if err != nil { + return nil, err + } + + var buy = true + if !strings.EqualFold(r.Side, order.Buy.String()) && + !strings.EqualFold(r.Side, order.Bid.String()) { + buy = false + } + + result := o.SimulateOrder(r.Amount, buy) + var resp gctrpc.SimulateOrderResponse + for x := range result.Orders { + resp.Orders = append(resp.Orders, &gctrpc.OrderbookItem{ + Price: result.Orders[x].Price, + Amount: result.Orders[x].Amount, + }) + } + + resp.Amount = result.Amount + resp.MaximumPrice = result.MaximumPrice + resp.MinimumPrice = result.MinimumPrice + resp.PercentageGainLoss = result.PercentageGainOrLoss + resp.Status = result.Status + return &resp, nil +} + +// WhaleBomb finds the amount required to reach a specific price target for a given exchange, pair +// and asset type +func (s *RPCServer) WhaleBomb(ctx context.Context, r *gctrpc.WhaleBombRequest) (*gctrpc.SimulateOrderResponse, error) { + exch := GetExchangeByName(r.Exchange) + if exch == nil { + return nil, errors.New("exchange is not loaded/doesn't exist") + } + + p := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) + o, err := exch.FetchOrderbook(p, asset.Spot) + if err != nil { + return nil, err + } + + var buy = true + if !strings.EqualFold(r.Side, order.Buy.String()) && + !strings.EqualFold(r.Side, order.Bid.String()) { + buy = false + } + + result, err := o.WhaleBomb(r.PriceTarget, buy) + var resp gctrpc.SimulateOrderResponse + for x := range result.Orders { + resp.Orders = append(resp.Orders, &gctrpc.OrderbookItem{ + Price: result.Orders[x].Price, + Amount: result.Orders[x].Amount, + }) + } + + resp.Amount = result.Amount + resp.MaximumPrice = result.MaximumPrice + resp.MinimumPrice = result.MinimumPrice + resp.PercentageGainLoss = result.PercentageGainOrLoss + resp.Status = result.Status + return &resp, err +} + +// CancelOrder cancels an order specified by exchange, currency pair and asset +// type +func (s *RPCServer) CancelOrder(ctx context.Context, r *gctrpc.CancelOrderRequest) (*gctrpc.CancelOrderResponse, error) { + exch := GetExchangeByName(r.Exchange) + if exch == nil { + return nil, errors.New("exchange is not loaded/doesn't exist") + } + + err := exch.CancelOrder(&order.Cancel{ + AccountID: r.AccountId, + OrderID: r.OrderId, + Side: order.Side(r.Side), + WalletAddress: r.WalletAddress, + }) + + return &gctrpc.CancelOrderResponse{}, err +} + +// CancelAllOrders cancels all orders, filterable by exchange +func (s *RPCServer) CancelAllOrders(ctx context.Context, r *gctrpc.CancelAllOrdersRequest) (*gctrpc.CancelAllOrdersResponse, error) { + return &gctrpc.CancelAllOrdersResponse{}, common.ErrNotYetImplemented +} + +// GetEvents returns the stored events list +func (s *RPCServer) GetEvents(ctx context.Context, r *gctrpc.GetEventsRequest) (*gctrpc.GetEventsResponse, error) { + return &gctrpc.GetEventsResponse{}, common.ErrNotYetImplemented +} + +// AddEvent adds an event +func (s *RPCServer) AddEvent(ctx context.Context, r *gctrpc.AddEventRequest) (*gctrpc.AddEventResponse, error) { + evtCondition := EventConditionParams{ + CheckBids: r.ConditionParams.CheckBids, + CheckBidsAndAsks: r.ConditionParams.CheckBidsAndAsks, + Condition: r.ConditionParams.Condition, + OrderbookAmount: r.ConditionParams.OrderbookAmount, + Price: r.ConditionParams.Price, + } + + p := currency.NewPairWithDelimiter(r.Pair.Base, + r.Pair.Quote, r.Pair.Delimiter) + + id, err := Add(r.Exchange, r.Item, evtCondition, p, asset.Item(r.AssetType), r.Action) + if err != nil { + return nil, err + } + + return &gctrpc.AddEventResponse{Id: id}, nil +} + +// RemoveEvent removes an event, specified by an event ID +func (s *RPCServer) RemoveEvent(ctx context.Context, r *gctrpc.RemoveEventRequest) (*gctrpc.RemoveEventResponse, error) { + Remove(r.Id) + return &gctrpc.RemoveEventResponse{}, nil +} + +// GetCryptocurrencyDepositAddresses returns a list of cryptocurrency deposit +// addresses specified by an exchange +func (s *RPCServer) GetCryptocurrencyDepositAddresses(ctx context.Context, r *gctrpc.GetCryptocurrencyDepositAddressesRequest) (*gctrpc.GetCryptocurrencyDepositAddressesResponse, error) { + exch := GetExchangeByName(r.Exchange) + if exch == nil { + return nil, errors.New("exchange is not loaded/doesn't exist") + } + + result, err := GetCryptocurrencyDepositAddressesByExchange(r.Exchange) + return &gctrpc.GetCryptocurrencyDepositAddressesResponse{Addresses: result}, err +} + +// GetCryptocurrencyDepositAddress returns a cryptocurrency deposit address +// specified by exchange and cryptocurrency +func (s *RPCServer) GetCryptocurrencyDepositAddress(ctx context.Context, r *gctrpc.GetCryptocurrencyDepositAddressRequest) (*gctrpc.GetCryptocurrencyDepositAddressResponse, error) { + exch := GetExchangeByName(r.Exchange) + if exch == nil { + return nil, errors.New("exchange is not loaded/doesn't exist") + } + + addr, err := GetExchangeCryptocurrencyDepositAddress(r.Exchange, "", currency.NewCode(r.Cryptocurrency)) + return &gctrpc.GetCryptocurrencyDepositAddressResponse{Address: addr}, err +} + +// WithdrawCryptocurrencyFunds withdraws cryptocurrency funds specified by +// exchange +func (s *RPCServer) WithdrawCryptocurrencyFunds(ctx context.Context, r *gctrpc.WithdrawCurrencyRequest) (*gctrpc.WithdrawResponse, error) { + return &gctrpc.WithdrawResponse{}, common.ErrNotYetImplemented +} + +// WithdrawFiatFunds withdraws fiat funds specified by exchange +func (s *RPCServer) WithdrawFiatFunds(ctx context.Context, r *gctrpc.WithdrawCurrencyRequest) (*gctrpc.WithdrawResponse, error) { + return &gctrpc.WithdrawResponse{}, common.ErrNotYetImplemented +} + +// GetLoggerDetails returns a loggers details +func (s *RPCServer) GetLoggerDetails(ctx context.Context, r *gctrpc.GetLoggerDetailsRequest) (*gctrpc.GetLoggerDetailsResponse, error) { + levels, err := log.Level(r.Logger) + if err != nil { + return nil, err + } + + return &gctrpc.GetLoggerDetailsResponse{ + Info: levels.Info, + Debug: levels.Debug, + Warn: levels.Warn, + Error: levels.Error, + }, nil +} + +// SetLoggerDetails sets a loggers details +func (s *RPCServer) SetLoggerDetails(ctx context.Context, r *gctrpc.SetLoggerDetailsRequest) (*gctrpc.GetLoggerDetailsResponse, error) { + levels, err := log.SetLevel(r.Logger, r.Level) + if err != nil { + return nil, err + } + + return &gctrpc.GetLoggerDetailsResponse{ + Info: levels.Info, + Debug: levels.Debug, + Warn: levels.Warn, + Error: levels.Error, + }, nil +} + +// GetExchangePairs returns a list of exchange supported assets and related pairs +func (s *RPCServer) GetExchangePairs(ctx context.Context, r *gctrpc.GetExchangePairsRequest) (*gctrpc.GetExchangePairsResponse, error) { + exchCfg, err := Bot.Config.GetExchangeConfig(r.Exchange) + if err != nil { + return nil, err + } + + if r.Asset != "" && + !exchCfg.CurrencyPairs.GetAssetTypes().Contains(asset.Item(r.Asset)) { + return nil, errors.New("specified asset type does not exist") + } + + var resp gctrpc.GetExchangePairsResponse + resp.SupportedAssets = make(map[string]*gctrpc.PairsSupported) + assetTypes := exchCfg.CurrencyPairs.GetAssetTypes() + for x := range assetTypes { + a := assetTypes[x] + if r.Asset != "" && !strings.EqualFold(a.String(), r.Asset) { + continue + } + resp.SupportedAssets[a.String()] = &gctrpc.PairsSupported{ + AvailablePairs: exchCfg.CurrencyPairs.Get(a).Available.Join(), + EnabledPairs: exchCfg.CurrencyPairs.Get(a).Enabled.Join(), + } + } + return &resp, nil +} + +// EnableExchangePair enables the specified pair on an exchange +func (s *RPCServer) EnableExchangePair(ctx context.Context, r *gctrpc.ExchangePairRequest) (*gctrpc.GenericExchangeNameResponse, error) { + exchCfg, err := Bot.Config.GetExchangeConfig(r.Exchange) + if err != nil { + return nil, err + } + + if r.AssetType != "" && + !exchCfg.CurrencyPairs.GetAssetTypes().Contains(asset.Item(r.AssetType)) { + return nil, errors.New("specified asset type does not exist") + } + + // Default to spot asset type unless set + a := asset.Spot + if r.AssetType != "" { + a = asset.Item(r.AssetType) + } + + pairFmt, err := Bot.Config.GetPairFormat(r.Exchange, a) + if err != nil { + return nil, err + } + + p := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote).Format( + pairFmt.Delimiter, pairFmt.Uppercase) + err = exchCfg.CurrencyPairs.EnablePair(a, p) + if err != nil { + return nil, err + } + err = GetExchangeByName(r.Exchange).GetBase().CurrencyPairs.EnablePair( + asset.Item(r.AssetType), p) + return &gctrpc.GenericExchangeNameResponse{}, err +} + +// DisableExchangePair disables the specified pair on an exchange +func (s *RPCServer) DisableExchangePair(ctx context.Context, r *gctrpc.ExchangePairRequest) (*gctrpc.GenericExchangeNameResponse, error) { + exchCfg, err := Bot.Config.GetExchangeConfig(r.Exchange) + if err != nil { + return nil, err + } + + if r.AssetType != "" && + !exchCfg.CurrencyPairs.GetAssetTypes().Contains(asset.Item(r.AssetType)) { + return nil, errors.New("specified asset type does not exist") + } + + // Default to spot asset type unless set + a := asset.Spot + if r.AssetType != "" { + a = asset.Item(r.AssetType) + } + + pairFmt, err := Bot.Config.GetPairFormat(r.Exchange, a) + if err != nil { + return nil, err + } + + p := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote).Format( + pairFmt.Delimiter, pairFmt.Uppercase) + err = exchCfg.CurrencyPairs.DisablePair(asset.Item(r.AssetType), p) + if err != nil { + return nil, err + } + err = GetExchangeByName(r.Exchange).GetBase().CurrencyPairs.DisablePair( + asset.Item(r.AssetType), p) + return &gctrpc.GenericExchangeNameResponse{}, err +} + +// GetOrderbookStream streams the requested updated orderbook +func (s *RPCServer) GetOrderbookStream(r *gctrpc.GetOrderbookStreamRequest, stream gctrpc.GoCryptoTrader_GetOrderbookStreamServer) error { + if r.Exchange == "" { + return errors.New(errExchangeNameUnset) + } + + if r.Pair.String() == "" { + return errors.New(errCurrencyPairUnset) + } + + if r.AssetType == "" { + return errors.New(errAssetTypeUnset) + } + + p := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) + + pipe, err := orderbook.SubscribeOrderbook(r.Exchange, p, asset.Item(r.AssetType)) + if err != nil { + return err + } + + defer pipe.Release() + + for { + data, ok := <-pipe.C + if !ok { + return errors.New(errDispatchSystem) + } + + ob := (*data.(*interface{})).(orderbook.Base) + var bids, asks []*gctrpc.OrderbookItem + for i := range ob.Bids { + bids = append(bids, &gctrpc.OrderbookItem{ + Amount: ob.Bids[i].Amount, + Price: ob.Bids[i].Price, + Id: ob.Bids[i].ID, + }) + } + for i := range ob.Asks { + asks = append(asks, &gctrpc.OrderbookItem{ + Amount: ob.Asks[i].Amount, + Price: ob.Asks[i].Price, + Id: ob.Asks[i].ID, + }) + } + err := stream.Send(&gctrpc.OrderbookResponse{ + Pair: &gctrpc.CurrencyPair{Base: ob.Pair.Base.String(), + Quote: ob.Pair.Quote.String()}, + Bids: bids, + Asks: asks, + AssetType: ob.AssetType.String(), + }) + if err != nil { + return err + } + } +} + +// GetExchangeOrderbookStream streams all orderbooks associated with an exchange +func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStreamRequest, stream gctrpc.GoCryptoTrader_GetExchangeOrderbookStreamServer) error { + if r.Exchange == "" { + return errors.New(errExchangeNameUnset) + } + + pipe, err := orderbook.SubscribeToExchangeOrderbooks(r.Exchange) + if err != nil { + return err + } + + defer pipe.Release() + + for { + data, ok := <-pipe.C + if !ok { + return errors.New(errDispatchSystem) + } + + ob := (*data.(*interface{})).(orderbook.Base) + var bids, asks []*gctrpc.OrderbookItem + for i := range ob.Bids { + bids = append(bids, &gctrpc.OrderbookItem{ + Amount: ob.Bids[i].Amount, + Price: ob.Bids[i].Price, + Id: ob.Bids[i].ID, + }) + } + for i := range ob.Asks { + asks = append(asks, &gctrpc.OrderbookItem{ + Amount: ob.Asks[i].Amount, + Price: ob.Asks[i].Price, + Id: ob.Asks[i].ID, + }) + } + err := stream.Send(&gctrpc.OrderbookResponse{ + Pair: &gctrpc.CurrencyPair{Base: ob.Pair.Base.String(), + Quote: ob.Pair.Quote.String()}, + Bids: bids, + Asks: asks, + AssetType: ob.AssetType.String(), + }) + if err != nil { + return err + } + } +} + +// GetTickerStream streams the requested updated ticker +func (s *RPCServer) GetTickerStream(r *gctrpc.GetTickerStreamRequest, stream gctrpc.GoCryptoTrader_GetTickerStreamServer) error { + if r.Exchange == "" { + return errors.New(errExchangeNameUnset) + } + + if r.Pair.String() == "" { + return errors.New(errCurrencyPairUnset) + } + + if r.AssetType == "" { + return errors.New(errAssetTypeUnset) + } + + p := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) + + pipe, err := ticker.SubscribeTicker(r.Exchange, p, asset.Item(r.AssetType)) + if err != nil { + return err + } + + defer pipe.Release() + + for { + data, ok := <-pipe.C + if !ok { + return errors.New(errDispatchSystem) + } + t := (*data.(*interface{})).(ticker.Price) + + err := stream.Send(&gctrpc.TickerResponse{ + Pair: &gctrpc.CurrencyPair{ + Base: t.Pair.Base.String(), + Quote: t.Pair.Quote.String(), + Delimiter: t.Pair.Delimiter}, + LastUpdated: t.LastUpdated.Unix(), + Last: t.Last, + High: t.High, + Low: t.Low, + Bid: t.Bid, + Ask: t.Ask, + Volume: t.Volume, + PriceAth: t.PriceATH, + }) + if err != nil { + return err + } + } +} + +// GetExchangeTickerStream streams all tickers associated with an exchange +func (s *RPCServer) GetExchangeTickerStream(r *gctrpc.GetExchangeTickerStreamRequest, stream gctrpc.GoCryptoTrader_GetExchangeTickerStreamServer) error { + if r.Exchange == "" { + return errors.New(errExchangeNameUnset) + } + + pipe, err := ticker.SubscribeToExchangeTickers(r.Exchange) + if err != nil { + return err + } + + defer pipe.Release() + + for { + data, ok := <-pipe.C + if !ok { + return errors.New(errDispatchSystem) + } + t := (*data.(*interface{})).(ticker.Price) + + err := stream.Send(&gctrpc.TickerResponse{ + Pair: &gctrpc.CurrencyPair{ + Base: t.Pair.Base.String(), + Quote: t.Pair.Quote.String(), + Delimiter: t.Pair.Delimiter}, + LastUpdated: t.LastUpdated.Unix(), + Last: t.Last, + High: t.High, + Low: t.Low, + Bid: t.Bid, + Ask: t.Ask, + Volume: t.Volume, + PriceAth: t.PriceATH, + }) + if err != nil { + return err + } + } +} + +// GetAuditEvent returns matching audit events from database +func (s *RPCServer) GetAuditEvent(ctx context.Context, r *gctrpc.GetAuditEventRequest) (*gctrpc.GetAuditEventResponse, error) { + UTCStartTime, err := time.Parse(audit.TableTimeFormat, r.StartDate) + if err != nil { + return nil, err + } + + UTCSEndTime, err := time.Parse(audit.TableTimeFormat, r.EndDate) + if err != nil { + return nil, err + } + + loc := time.FixedZone("", int(r.Offset)) + + events, err := audit.GetEvent(UTCStartTime, UTCSEndTime, r.OrderBy, int(r.Limit)) + if err != nil { + return nil, err + } + + resp := gctrpc.GetAuditEventResponse{} + + switch v := events.(type) { + case postgres.AuditEventSlice: + for x := range v { + tempEvent := &gctrpc.AuditEvent{ + Type: v[x].Type, + Identifier: v[x].Identifier, + Message: v[x].Message, + Timestamp: v[x].CreatedAt.In(loc).Format(audit.TableTimeFormat), + } + + resp.Events = append(resp.Events, tempEvent) + } + case sqlite3.AuditEventSlice: + for x := range v { + tempEvent := &gctrpc.AuditEvent{ + Type: v[x].Type, + Identifier: v[x].Identifier, + Message: v[x].Message, + Timestamp: v[x].CreatedAt, + } + resp.Events = append(resp.Events, tempEvent) + } + } + + return &resp, nil +} diff --git a/engine/syncer.go b/engine/syncer.go new file mode 100644 index 00000000..cdeb418d --- /dev/null +++ b/engine/syncer.go @@ -0,0 +1,600 @@ +package engine + +import ( + "errors" + "sync/atomic" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// const holds the sync item types +const ( + SyncItemTicker = iota + SyncItemOrderbook + SyncItemTrade + + DefaultSyncerWorkers = 15 + DefaultSyncerTimeout = time.Second * 15 +) + +var ( + createdCounter = 0 + removedCounter = 0 +) + +// NewCurrencyPairSyncer starts a new CurrencyPairSyncer +func NewCurrencyPairSyncer(c CurrencyPairSyncerConfig) (*ExchangeCurrencyPairSyncer, error) { + if !c.SyncOrderbook && !c.SyncTicker && !c.SyncTrades { + return nil, errors.New("no sync items enabled") + } + + if c.NumWorkers <= 0 { + c.NumWorkers = DefaultSyncerWorkers + } + + if c.SyncTimeout <= time.Duration(0) { + c.SyncTimeout = DefaultSyncerTimeout + } + + s := ExchangeCurrencyPairSyncer{ + Cfg: CurrencyPairSyncerConfig{ + SyncTicker: c.SyncTicker, + SyncOrderbook: c.SyncOrderbook, + SyncTrades: c.SyncTrades, + SyncContinuously: c.SyncContinuously, + SyncTimeout: c.SyncTimeout, + NumWorkers: c.NumWorkers, + }, + } + + s.tickerBatchLastRequested = make(map[string]time.Time) + + log.Debugf(log.SyncMgr, + "Exchange currency pair syncer config: continuous: %v ticker: %v"+ + " orderbook: %v trades: %v workers: %v verbose: %v timeout: %v\n", + s.Cfg.SyncContinuously, s.Cfg.SyncTicker, s.Cfg.SyncOrderbook, + s.Cfg.SyncTrades, s.Cfg.NumWorkers, s.Cfg.Verbose, s.Cfg.SyncTimeout) + return &s, nil +} + +func (e *ExchangeCurrencyPairSyncer) get(exchangeName string, p currency.Pair, a asset.Item) (*CurrencyPairSyncAgent, error) { + e.mux.Lock() + defer e.mux.Unlock() + + for x := range e.CurrencyPairs { + if e.CurrencyPairs[x].Exchange == exchangeName && + e.CurrencyPairs[x].Pair.Equal(p) && + e.CurrencyPairs[x].AssetType == a { + return &e.CurrencyPairs[x], nil + } + } + + return nil, errors.New("exchange currency pair syncer not found") +} + +func (e *ExchangeCurrencyPairSyncer) exists(exchangeName string, p currency.Pair, a asset.Item) bool { + e.mux.Lock() + defer e.mux.Unlock() + + for x := range e.CurrencyPairs { + if e.CurrencyPairs[x].Exchange == exchangeName && + e.CurrencyPairs[x].Pair.Equal(p) && + e.CurrencyPairs[x].AssetType == a { + return true + } + } + return false +} + +func (e *ExchangeCurrencyPairSyncer) add(c *CurrencyPairSyncAgent) { + e.mux.Lock() + defer e.mux.Unlock() + + if e.Cfg.SyncTicker { + if e.Cfg.Verbose { + log.Debugf(log.SyncMgr, + "%s: Added ticker sync item %v: using websocket: %v using REST: %v\n", + c.Exchange, FormatCurrency(c.Pair).String(), c.Ticker.IsUsingWebsocket, + c.Ticker.IsUsingREST) + } + if atomic.LoadInt32(&e.initSyncCompleted) != 1 { + e.initSyncWG.Add(1) + createdCounter++ + } + } + + if e.Cfg.SyncOrderbook { + if e.Cfg.Verbose { + log.Debugf(log.SyncMgr, + "%s: Added orderbook sync item %v: using websocket: %v using REST: %v\n", + c.Exchange, FormatCurrency(c.Pair).String(), c.Orderbook.IsUsingWebsocket, + c.Orderbook.IsUsingREST) + } + if atomic.LoadInt32(&e.initSyncCompleted) != 1 { + e.initSyncWG.Add(1) + createdCounter++ + } + } + + if e.Cfg.SyncTrades { + if e.Cfg.Verbose { + log.Debugf(log.SyncMgr, + "%s: Added trade sync item %v: using websocket: %v using REST: %v\n", + c.Exchange, FormatCurrency(c.Pair).String(), c.Trade.IsUsingWebsocket, + c.Trade.IsUsingREST) + } + if atomic.LoadInt32(&e.initSyncCompleted) != 1 { + e.initSyncWG.Add(1) + createdCounter++ + } + } + + c.Created = time.Now() + e.CurrencyPairs = append(e.CurrencyPairs, *c) +} + +func (e *ExchangeCurrencyPairSyncer) remove(c *CurrencyPairSyncAgent) { + e.mux.Lock() + defer e.mux.Unlock() + + for x := range e.CurrencyPairs { + if e.CurrencyPairs[x].Exchange == c.Exchange && + e.CurrencyPairs[x].Pair.Equal(c.Pair) && + e.CurrencyPairs[x].AssetType == c.AssetType { + e.CurrencyPairs = append(e.CurrencyPairs[:x], e.CurrencyPairs[x+1:]...) + return + } + } +} + +func (e *ExchangeCurrencyPairSyncer) isProcessing(exchangeName string, p currency.Pair, a asset.Item, syncType int) bool { + e.mux.Lock() + defer e.mux.Unlock() + + for x := range e.CurrencyPairs { + if e.CurrencyPairs[x].Exchange == exchangeName && + e.CurrencyPairs[x].Pair.Equal(p) && + e.CurrencyPairs[x].AssetType == a { + switch syncType { + case SyncItemTicker: + return e.CurrencyPairs[x].Ticker.IsProcessing + case SyncItemOrderbook: + return e.CurrencyPairs[x].Orderbook.IsProcessing + case SyncItemTrade: + return e.CurrencyPairs[x].Trade.IsProcessing + } + } + } + + return false +} + +func (e *ExchangeCurrencyPairSyncer) setProcessing(exchangeName string, p currency.Pair, a asset.Item, syncType int, processing bool) { + e.mux.Lock() + defer e.mux.Unlock() + + for x := range e.CurrencyPairs { + if e.CurrencyPairs[x].Exchange == exchangeName && + e.CurrencyPairs[x].Pair.Equal(p) && + e.CurrencyPairs[x].AssetType == a { + switch syncType { + case SyncItemTicker: + e.CurrencyPairs[x].Ticker.IsProcessing = processing + case SyncItemOrderbook: + e.CurrencyPairs[x].Orderbook.IsProcessing = processing + case SyncItemTrade: + e.CurrencyPairs[x].Trade.IsProcessing = processing + } + } + } +} + +func (e *ExchangeCurrencyPairSyncer) update(exchangeName string, p currency.Pair, a asset.Item, syncType int, err error) { + if atomic.LoadInt32(&e.initSyncStarted) != 1 { + return + } + + switch syncType { + case SyncItemOrderbook, SyncItemTrade, SyncItemTicker: + if !e.Cfg.SyncOrderbook && syncType == SyncItemOrderbook { + return + } + + if !e.Cfg.SyncTicker && syncType == SyncItemTicker { + return + } + + if !e.Cfg.SyncTrades && syncType == SyncItemTrade { + return + } + default: + log.Warnf(log.SyncMgr, "ExchangeCurrencyPairSyncer: unknown sync item %v\n", syncType) + return + } + + e.mux.Lock() + defer e.mux.Unlock() + + for x := range e.CurrencyPairs { + if e.CurrencyPairs[x].Exchange == exchangeName && + e.CurrencyPairs[x].Pair.Equal(p) && + e.CurrencyPairs[x].AssetType == a { + switch syncType { + case SyncItemTicker: + origHadData := e.CurrencyPairs[x].Ticker.HaveData + e.CurrencyPairs[x].Ticker.LastUpdated = time.Now() + if err != nil { + e.CurrencyPairs[x].Ticker.NumErrors++ + } + e.CurrencyPairs[x].Ticker.HaveData = true + e.CurrencyPairs[x].Ticker.IsProcessing = false + if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData { + removedCounter++ + log.Debugf(log.SyncMgr, "%s ticker sync complete %v [%d/%d].\n", + exchangeName, FormatCurrency(p).String(), removedCounter, createdCounter) + e.initSyncWG.Done() + } + + case SyncItemOrderbook: + origHadData := e.CurrencyPairs[x].Orderbook.HaveData + e.CurrencyPairs[x].Orderbook.LastUpdated = time.Now() + if err != nil { + e.CurrencyPairs[x].Orderbook.NumErrors++ + } + e.CurrencyPairs[x].Orderbook.HaveData = true + e.CurrencyPairs[x].Orderbook.IsProcessing = false + if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData { + removedCounter++ + log.Debugf(log.SyncMgr, "%s orderbook sync complete %v [%d/%d].\n", + exchangeName, FormatCurrency(p).String(), removedCounter, createdCounter) + e.initSyncWG.Done() + } + + case SyncItemTrade: + origHadData := e.CurrencyPairs[x].Trade.HaveData + e.CurrencyPairs[x].Trade.LastUpdated = time.Now() + if err != nil { + e.CurrencyPairs[x].Trade.NumErrors++ + } + e.CurrencyPairs[x].Trade.HaveData = true + e.CurrencyPairs[x].Trade.IsProcessing = false + if atomic.LoadInt32(&e.initSyncCompleted) != 1 && !origHadData { + removedCounter++ + log.Debugf(log.SyncMgr, "%s trade sync complete %v [%d/%d].\n", + exchangeName, FormatCurrency(p).String(), removedCounter, createdCounter) + e.initSyncWG.Done() + } + } + } + } +} + +func (e *ExchangeCurrencyPairSyncer) worker() { + cleanup := func() { + log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer worker shutting down.") + } + defer cleanup() + + for atomic.LoadInt32(&e.shutdown) != 1 { + for x := range Bot.Exchanges { + if !Bot.Exchanges[x].IsEnabled() { + continue + } + + exchangeName := Bot.Exchanges[x].GetName() + assetTypes := Bot.Exchanges[x].GetAssetTypes() + supportsREST := Bot.Exchanges[x].SupportsREST() + supportsRESTTickerBatching := Bot.Exchanges[x].SupportsRESTTickerBatchUpdates() + var usingREST bool + var usingWebsocket bool + var switchedToRest bool + if Bot.Exchanges[x].SupportsWebsocket() && Bot.Exchanges[x].IsWebsocketEnabled() { + ws, err := Bot.Exchanges[x].GetWebsocket() + if err != nil { + log.Errorf(log.SyncMgr, "%s unable to get websocket pointer. Err: %s\n", + exchangeName, err) + usingREST = true + } + + if ws.IsConnected() { + usingWebsocket = true + } else { + usingREST = true + } + } else if supportsREST { + usingREST = true + } + + for y := range assetTypes { + enabledPairs := Bot.Exchanges[x].GetEnabledPairs(assetTypes[y]) + for i := range enabledPairs { + if atomic.LoadInt32(&e.shutdown) == 1 { + return + } + + if !e.exists(exchangeName, enabledPairs[i], assetTypes[y]) { + c := CurrencyPairSyncAgent{ + AssetType: assetTypes[y], + Exchange: exchangeName, + Pair: enabledPairs[i], + } + + if e.Cfg.SyncTicker { + c.Ticker = SyncBase{ + IsUsingREST: usingREST, + IsUsingWebsocket: usingWebsocket, + } + } + + if e.Cfg.SyncOrderbook { + c.Orderbook = SyncBase{ + IsUsingREST: usingREST, + IsUsingWebsocket: usingWebsocket, + } + } + + if e.Cfg.SyncTrades { + c.Trade = SyncBase{ + IsUsingREST: usingREST, + IsUsingWebsocket: usingWebsocket, + } + } + + e.add(&c) + } + + c, err := e.get(exchangeName, enabledPairs[i], assetTypes[y]) + if err != nil { + log.Errorf(log.SyncMgr, "failed to get item. Err: %s\n", err) + continue + } + if switchedToRest && usingWebsocket { + log.Infof(log.SyncMgr, + "%s %s: Websocket re-enabled, switching from rest to websocket\n", + c.Exchange, FormatCurrency(enabledPairs[i]).String()) + switchedToRest = false + } + if e.Cfg.SyncTicker { + if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTicker) { + if c.Ticker.LastUpdated.IsZero() || time.Since(c.Ticker.LastUpdated) > e.Cfg.SyncTimeout { + if c.Ticker.IsUsingWebsocket { + if time.Since(c.Created) < e.Cfg.SyncTimeout { + continue + } + + if supportsREST { + e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, true) + c.Ticker.IsUsingWebsocket = false + c.Ticker.IsUsingREST = true + log.Warnf(log.SyncMgr, + "%s %s: No ticker update after %s, switching from websocket to rest\n", + c.Exchange, FormatCurrency(enabledPairs[i]).String(), e.Cfg.SyncTimeout) + switchedToRest = true + e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, false) + } + } + + if c.Ticker.IsUsingREST { + e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, true) + var result ticker.Price + var err error + + if supportsRESTTickerBatching { + e.mux.Lock() + batchLastDone, ok := e.tickerBatchLastRequested[exchangeName] + if !ok { + e.tickerBatchLastRequested[exchangeName] = time.Time{} + } + e.mux.Unlock() + + if batchLastDone.IsZero() || time.Since(batchLastDone) > e.Cfg.SyncTimeout { + e.mux.Lock() + if e.Cfg.Verbose { + log.Debugf(log.SyncMgr, "%s Init'ing REST ticker batching\n", exchangeName) + } + result, err = Bot.Exchanges[x].UpdateTicker(c.Pair, c.AssetType) + e.tickerBatchLastRequested[exchangeName] = time.Now() + e.mux.Unlock() + } else { + if e.Cfg.Verbose { + log.Debugf(log.SyncMgr, "%s Using recent batching cache\n", exchangeName) + } + result, err = Bot.Exchanges[x].FetchTicker(c.Pair, c.AssetType) + } + } else { + result, err = Bot.Exchanges[x].UpdateTicker(c.Pair, c.AssetType) + } + printTickerSummary(&result, c.Pair, c.AssetType, exchangeName, err) + if err == nil { + //nolint:gocritic Bot.CommsRelayer.StageTickerData(exchangeName, c.AssetType, result) + if Bot.Config.RemoteControl.WebsocketRPC.Enabled { + relayWebsocketEvent(result, "ticker_update", c.AssetType.String(), exchangeName) + } + } + e.update(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, err) + } + } else { + time.Sleep(time.Millisecond * 50) + } + } + } + + if e.Cfg.SyncOrderbook { + if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemOrderbook) { + if c.Orderbook.LastUpdated.IsZero() || time.Since(c.Orderbook.LastUpdated) > e.Cfg.SyncTimeout { + if c.Orderbook.IsUsingWebsocket { + if time.Since(c.Created) < e.Cfg.SyncTimeout { + continue + } + if supportsREST { + e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, true) + c.Orderbook.IsUsingWebsocket = false + c.Orderbook.IsUsingREST = true + log.Warnf(log.SyncMgr, + "%s %s: No orderbook update after %s, switching from websocket to rest\n", + c.Exchange, FormatCurrency(c.Pair).String(), e.Cfg.SyncTimeout) + switchedToRest = true + e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, false) + } + } + + e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, true) + result, err := Bot.Exchanges[x].UpdateOrderbook(c.Pair, c.AssetType) + printOrderbookSummary(&result, c.Pair, c.AssetType, exchangeName, err) + if err == nil { + //nolint:gocritic Bot.CommsRelayer.StageOrderbookData(exchangeName, c.AssetType, result) + if Bot.Config.RemoteControl.WebsocketRPC.Enabled { + relayWebsocketEvent(result, "orderbook_update", c.AssetType.String(), exchangeName) + } + } + e.update(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, err) + } else { + time.Sleep(time.Millisecond * 50) + } + } + if e.Cfg.SyncTrades { + if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTrade) { + if c.Trade.LastUpdated.IsZero() || time.Since(c.Trade.LastUpdated) > e.Cfg.SyncTimeout { + e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, true) + e.update(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, nil) + } + } + } + } + } + } + } + } +} + +// Start starts an exchange currency pair syncer +func (e *ExchangeCurrencyPairSyncer) Start() { + log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer started.") + + for x := range Bot.Exchanges { + if !Bot.Exchanges[x].IsEnabled() { + continue + } + + exchangeName := Bot.Exchanges[x].GetName() + supportsWebsocket := Bot.Exchanges[x].SupportsWebsocket() + assetTypes := Bot.Exchanges[x].GetAssetTypes() + supportsREST := Bot.Exchanges[x].SupportsREST() + + if !supportsREST && !supportsWebsocket { + log.Warnf(log.SyncMgr, + "Loaded exchange %s does not support REST or Websocket.\n", + exchangeName) + continue + } + + var usingWebsocket bool + var usingREST bool + + if supportsWebsocket && Bot.Exchanges[x].IsWebsocketEnabled() { + ws, err := Bot.Exchanges[x].GetWebsocket() + if err != nil { + log.Errorf(log.SyncMgr, "%s failed to get websocket. Err: %s\n", + exchangeName, err) + usingREST = true + } + + if !ws.IsConnected() && !ws.IsConnecting() { + go WebsocketDataHandler(ws) + + err = ws.Connect() + if err != nil { + log.Errorf(log.SyncMgr, "%s websocket failed to connect. Err: %s\n", + exchangeName, err) + usingREST = true + } else { + usingWebsocket = true + } + } else { + usingWebsocket = true + } + } else if supportsREST { + usingREST = true + } + + for y := range assetTypes { + enabledPairs := Bot.Exchanges[x].GetEnabledPairs(assetTypes[y]) + for i := range enabledPairs { + if e.exists(exchangeName, enabledPairs[i], assetTypes[y]) { + continue + } + c := CurrencyPairSyncAgent{ + AssetType: assetTypes[y], + Exchange: exchangeName, + Pair: enabledPairs[i], + } + + if e.Cfg.SyncTicker { + c.Ticker = SyncBase{ + IsUsingREST: usingREST, + IsUsingWebsocket: usingWebsocket, + } + } + + if e.Cfg.SyncOrderbook { + c.Orderbook = SyncBase{ + IsUsingREST: usingREST, + IsUsingWebsocket: usingWebsocket, + } + } + + if e.Cfg.SyncTrades { + c.Trade = SyncBase{ + IsUsingREST: usingREST, + IsUsingWebsocket: usingWebsocket, + } + } + + e.add(&c) + } + } + } + + if atomic.CompareAndSwapInt32(&e.initSyncStarted, 0, 1) { + log.Debugf(log.SyncMgr, + "Exchange CurrencyPairSyncer initial sync started. %d items to process.\n", + createdCounter) + e.initSyncStartTime = time.Now() + } + + go func() { + e.initSyncWG.Wait() + if atomic.CompareAndSwapInt32(&e.initSyncCompleted, 0, 1) { + log.Debugf(log.SyncMgr, "Exchange CurrencyPairSyncer initial sync is complete.\n") + completedTime := time.Now() + log.Debugf(log.SyncMgr, "Exchange CurrencyPairSyncer initiial sync took %v [%v sync items].\n", + completedTime.Sub(e.initSyncStartTime), createdCounter) + + if !e.Cfg.SyncContinuously { + log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer stopping.") + e.Stop() + return + } + } + }() + + if atomic.LoadInt32(&e.initSyncCompleted) == 1 && !e.Cfg.SyncContinuously { + return + } + + for i := 0; i < e.Cfg.NumWorkers; i++ { + go e.worker() + } +} + +// Stop shuts down the exchange currency pair syncer +func (e *ExchangeCurrencyPairSyncer) Stop() { + stopped := atomic.CompareAndSwapInt32(&e.shutdown, 0, 1) + if stopped { + log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer stopped.") + } +} diff --git a/engine/syncer_test.go b/engine/syncer_test.go new file mode 100644 index 00000000..8546e21e --- /dev/null +++ b/engine/syncer_test.go @@ -0,0 +1,45 @@ +package engine + +import ( + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/config" +) + +func TestNewCurrencyPairSyncer(t *testing.T) { + t.Skip() + + if Bot == nil { + Bot = new(Engine) + } + Bot.Config = &config.Cfg + err := Bot.Config.LoadConfig("", true) + if err != nil { + t.Fatalf("TestNewExchangeSyncer: Failed to load config: %s", err) + } + + Bot.Settings.DisableExchangeAutoPairUpdates = true + Bot.Settings.Verbose = true + Bot.Settings.EnableExchangeWebsocketSupport = true + + SetupExchanges() + + if err != nil { + t.Log("failed to start exchange syncer") + } + + Bot.ExchangeCurrencyPairManager, err = NewCurrencyPairSyncer(CurrencyPairSyncerConfig{ + SyncTicker: true, + SyncOrderbook: false, + SyncTrades: false, + SyncContinuously: false, + }) + if err != nil { + t.Errorf("NewCurrencyPairSyncer failed: err %s", err) + } + + Bot.ExchangeCurrencyPairManager.Start() + time.Sleep(time.Second * 15) + Bot.ExchangeCurrencyPairManager.Stop() +} diff --git a/engine/syncer_types.go b/engine/syncer_types.go new file mode 100644 index 00000000..4701b503 --- /dev/null +++ b/engine/syncer_types.go @@ -0,0 +1,61 @@ +package engine + +import ( + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +// CurrencyPairSyncerConfig stores the currency pair config +type CurrencyPairSyncerConfig struct { + SyncTicker bool + SyncOrderbook bool + SyncTrades bool + SyncContinuously bool + SyncTimeout time.Duration + NumWorkers int + Verbose bool +} + +// ExchangeSyncerConfig stores the exchange syncer config +type ExchangeSyncerConfig struct { + SyncDepositAddresses bool + SyncOrders bool +} + +// ExchangeCurrencyPairSyncer stores the exchange currency pair syncer object +type ExchangeCurrencyPairSyncer struct { + Cfg CurrencyPairSyncerConfig + CurrencyPairs []CurrencyPairSyncAgent + tickerBatchLastRequested map[string]time.Time + mux sync.Mutex + initSyncWG sync.WaitGroup + + initSyncCompleted int32 + initSyncStarted int32 + initSyncStartTime time.Time + shutdown int32 +} + +// SyncBase stores information +type SyncBase struct { + IsUsingWebsocket bool + IsUsingREST bool + IsProcessing bool + LastUpdated time.Time + HaveData bool + NumErrors int +} + +// CurrencyPairSyncAgent stores the sync agent info +type CurrencyPairSyncAgent struct { + Created time.Time + Exchange string + AssetType asset.Item + Pair currency.Pair + Ticker SyncBase + Orderbook SyncBase + Trade SyncBase +} diff --git a/engine/timekeeper.go b/engine/timekeeper.go new file mode 100644 index 00000000..8956ff6d --- /dev/null +++ b/engine/timekeeper.go @@ -0,0 +1,142 @@ +package engine + +import ( + "errors" + "fmt" + "os" + "sync/atomic" + "time" + + log "github.com/thrasher-corp/gocryptotrader/logger" + ntpclient "github.com/thrasher-corp/gocryptotrader/ntpclient" +) + +// vars related to the NTP manager +var ( + NTPCheckInterval = time.Second * 30 + NTPRetryLimit = 3 + errNTPDisabled = errors.New("ntp client disabled") +) + +// ntpManager starts the NTP manager +type ntpManager struct { + started int32 + stopped int32 + inititalCheck bool + shutdown chan struct{} +} + +func (n *ntpManager) Started() bool { + return atomic.LoadInt32(&n.started) == 1 +} + +func (n *ntpManager) Start() (err error) { + if atomic.AddInt32(&n.started, 1) != 1 { + return errors.New("NTP manager already started") + } + + var disable bool + defer func() { + if err != nil || disable { + atomic.CompareAndSwapInt32(&n.started, 1, 0) + } + }() + + if Bot.Config.NTPClient.Level == -1 { + err = errors.New("NTP client disabled") + return + } + + log.Debugln(log.TimeMgr, "NTP manager starting...") + if Bot.Config.NTPClient.Level == 0 && *Bot.Config.Logging.Enabled { + // Initial NTP check (prompts user on how we should proceed) + n.inititalCheck = true + + // Sometimes the NTP client can have transient issues due to UDP, try + // the default retry limits before giving up + for i := 0; i < NTPRetryLimit; i++ { + err = n.processTime() + switch err { + case nil: + break + case errNTPDisabled: + log.Debugln(log.TimeMgr, "NTP manager: User disabled NTP prompts. Exiting.") + disable = true + err = nil + return + default: + if i == NTPRetryLimit-1 { + return err + } + } + } + } + n.shutdown = make(chan struct{}) + go n.run() + log.Debugln(log.TimeMgr, "NTP manager started.") + return nil +} + +func (n *ntpManager) Stop() error { + if atomic.LoadInt32(&n.started) == 0 { + return errors.New("NTP manager not started") + } + + if atomic.AddInt32(&n.stopped, 1) != 1 { + return errors.New("NTP manager is already stopped") + } + + close(n.shutdown) + log.Debugln(log.TimeMgr, "NTP manager shutting down...") + return nil +} + +func (n *ntpManager) run() { + t := time.NewTicker(NTPCheckInterval) + defer func() { + t.Stop() + atomic.CompareAndSwapInt32(&n.stopped, 1, 0) + atomic.CompareAndSwapInt32(&n.started, 1, 0) + log.Debugln(log.TimeMgr, "NTP manager shutdown.") + }() + + for { + select { + case <-n.shutdown: + return + case <-t.C: + n.processTime() + } + } +} + +func (n *ntpManager) FetchNTPTime() (time.Time, error) { + return ntpclient.NTPClient(Bot.Config.NTPClient.Pool) +} + +func (n *ntpManager) processTime() error { + NTPTime, err := n.FetchNTPTime() + if err != nil { + return err + } + + currentTime := time.Now() + NTPcurrentTimeDifference := NTPTime.Sub(currentTime) + configNTPTime := *Bot.Config.NTPClient.AllowedDifference + configNTPNegativeTime := (*Bot.Config.NTPClient.AllowedNegativeDifference - (*Bot.Config.NTPClient.AllowedNegativeDifference * 2)) + if NTPcurrentTimeDifference > configNTPTime || NTPcurrentTimeDifference < configNTPNegativeTime { + log.Warnf(log.TimeMgr, "NTP manager: Time out of sync (NTP): %v | (time.Now()): %v | (Difference): %v | (Allowed): +%v / %v\n", NTPTime, currentTime, NTPcurrentTimeDifference, configNTPTime, configNTPNegativeTime) + if n.inititalCheck { + n.inititalCheck = false + disable, err := Bot.Config.DisableNTPCheck(os.Stdin) + if err != nil { + return fmt.Errorf("unable to disable NTP check: %s", err) + } + log.Infoln(log.TimeMgr, disable) + if Bot.Config.NTPClient.Level == -1 { + return errNTPDisabled + } + } + } + return nil +} diff --git a/websocket.go b/engine/websocket.go similarity index 66% rename from websocket.go rename to engine/websocket.go index 822e1927..2f3c1431 100644 --- a/websocket.go +++ b/engine/websocket.go @@ -1,13 +1,16 @@ -package main +package engine import ( + "encoding/json" "errors" "net/http" + "strings" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -39,52 +42,6 @@ var wsHandlers = map[string]wsCommandHandler{ "getportfolio": {authRequired: true, handler: wsGetPortfolio}, } -// WebsocketClient stores information related to the websocket client -type WebsocketClient struct { - Hub *WebsocketHub - Conn *websocket.Conn - Authenticated bool - authFailures int - Send chan []byte -} - -// WebsocketHub stores the data for managing websocket clients -type WebsocketHub struct { - Clients map[*WebsocketClient]bool - Broadcast chan []byte - Register chan *WebsocketClient - Unregister chan *WebsocketClient -} - -// WebsocketEvent is the struct used for websocket events -type WebsocketEvent struct { - Exchange string `json:"exchange,omitempty"` - AssetType string `json:"assetType,omitempty"` - Event string - Data interface{} -} - -// WebsocketEventResponse is the struct used for websocket event responses -type WebsocketEventResponse struct { - Event string `json:"event"` - Data interface{} `json:"data"` - Error string `json:"error"` -} - -// WebsocketOrderbookTickerRequest is a struct used for ticker and orderbook -// requests -type WebsocketOrderbookTickerRequest struct { - Exchange string `json:"exchangeName"` - Currency string `json:"currency"` - AssetType string `json:"assetType"` -} - -// WebsocketAuth is a struct used for -type WebsocketAuth struct { - Username string `json:"username"` - Password string `json:"password"` -} - // NewWebsocketHub Creates a new websocket hub func NewWebsocketHub() *WebsocketHub { return &WebsocketHub{ @@ -102,7 +59,7 @@ func (h *WebsocketHub) run() { h.Clients[client] = true case client := <-h.Unregister: if _, ok := h.Clients[client]; ok { - log.Debugln("websocket: disconnected client") + log.Debugln(log.WebsocketMgr, "websocket: disconnected client") delete(h.Clients, client) close(client.Send) } @@ -111,7 +68,7 @@ func (h *WebsocketHub) run() { select { case client.Send <- message: default: - log.Debugln("websocket: disconnected client") + log.Debugln(log.WebsocketMgr, "websocket: disconnected client") close(client.Send) delete(h.Clients, client) } @@ -122,9 +79,9 @@ func (h *WebsocketHub) run() { // SendWebsocketMessage sends a websocket event to the client func (c *WebsocketClient) SendWebsocketMessage(evt interface{}) error { - data, err := common.JSONEncode(evt) + data, err := json.Marshal(evt) if err != nil { - log.Errorf("websocket: failed to send message: %s", err) + log.Errorf(log.WebsocketMgr, "websocket: failed to send message: %s\n", err) return err } @@ -142,48 +99,48 @@ func (c *WebsocketClient) read() { msgType, message, err := c.Conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - log.Errorf("websocket: client disconnected, err: %s", err) + log.Errorf(log.WebsocketMgr, "websocket: client disconnected, err: %s\n", err) } break } if msgType == websocket.TextMessage { var evt WebsocketEvent - err := common.JSONDecode(message, &evt) + err := json.Unmarshal(message, &evt) if err != nil { - log.Errorf("websocket: failed to decode JSON sent from client %s", err) + log.Errorf(log.WebsocketMgr, "websocket: failed to decode JSON sent from client %s\n", err) continue } if evt.Event == "" { - log.Warnf("websocket: client sent a blank event, disconnecting") + log.Warnln(log.WebsocketMgr, "websocket: client sent a blank event, disconnecting") continue } - dataJSON, err := common.JSONEncode(evt.Data) + dataJSON, err := json.Marshal(evt.Data) if err != nil { - log.Errorf("websocket: client sent data we couldn't JSON decode") + log.Errorln(log.WebsocketMgr, "websocket: client sent data we couldn't JSON decode") break } - req := common.StringToLower(evt.Event) - log.Debugf("websocket: request received: %s", req) + req := strings.ToLower(evt.Event) + log.Debugf(log.WebsocketMgr, "websocket: request received: %s\n", req) result, ok := wsHandlers[req] if !ok { - log.Debugln("websocket: unsupported event") + log.Debugln(log.WebsocketMgr, "websocket: unsupported event") continue } if result.authRequired && !c.Authenticated { - log.Warnf("Websocket: request %s failed due to unauthenticated request on an authenticated API", evt.Event) + log.Warnf(log.WebsocketMgr, "Websocket: request %s failed due to unauthenticated request on an authenticated API\n", evt.Event) c.SendWebsocketMessage(WebsocketEventResponse{Event: evt.Event, Error: "unauthorised request on authenticated API"}) continue } err = result.handler(c, dataJSON) if err != nil { - log.Errorf("websocket: request %s failed. Error %s", evt.Event, err) + log.Errorf(log.WebsocketMgr, "websocket: request %s failed. Error %s\n", evt.Event, err) continue } } @@ -199,13 +156,13 @@ func (c *WebsocketClient) write() { case message, ok := <-c.Send: if !ok { c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) - log.Debugln("websocket: hub closed the channel") + log.Debugln(log.WebsocketMgr, "websocket: hub closed the channel") return } w, err := c.Conn.NextWriter(websocket.TextMessage) if err != nil { - log.Errorf("websocket: failed to create new io.writeCloser: %s", err) + log.Errorf(log.WebsocketMgr, "websocket: failed to create new io.writeCloser: %s\n", err) return } w.Write(message) @@ -217,7 +174,7 @@ func (c *WebsocketClient) write() { } if err := w.Close(); err != nil { - log.Errorf("websocket: failed to close io.WriteCloser: %s", err) + log.Errorf(log.WebsocketMgr, "websocket: failed to close io.WriteCloser: %s\n", err) return } } @@ -240,7 +197,7 @@ func BroadcastWebsocketMessage(evt WebsocketEvent) error { return errors.New("websocket service not started") } - data, err := common.JSONEncode(evt) + data, err := json.Marshal(evt) if err != nil { return err } @@ -256,11 +213,12 @@ func WebsocketClientHandler(w http.ResponseWriter, r *http.Request) { StartWebsocketHandler() } - connectionLimit := bot.config.Webserver.WebsocketConnectionLimit + connectionLimit := Bot.Config.RemoteControl.WebsocketRPC.ConnectionLimit numClients := len(wsHub.Clients) if numClients >= connectionLimit { - log.Warnf("websocket: client rejected due to websocket client limit reached. Number of clients %d. Limit %d.", + log.Warnf(log.WebsocketMgr, + "websocket: client rejected due to websocket client limit reached. Number of clients %d. Limit %d.\n", numClients, connectionLimit) w.WriteHeader(http.StatusForbidden) return @@ -273,19 +231,20 @@ func WebsocketClientHandler(w http.ResponseWriter, r *http.Request) { // Allow insecure origin if the Origin request header is present and not // equal to the Host request header. Default to false - if bot.config.Webserver.WebsocketAllowInsecureOrigin { + if Bot.Config.RemoteControl.WebsocketRPC.AllowInsecureOrigin { upgrader.CheckOrigin = func(r *http.Request) bool { return true } } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { - log.Error(err) + log.Error(log.WebsocketMgr, err) return } client := &WebsocketClient{Hub: wsHub, Conn: conn, Send: make(chan []byte, 1024)} client.Hub.Register <- client - log.Debugf("websocket: client connected. Connected clients: %d. Limit %d.", + log.Debugf(log.WebsocketMgr, + "websocket: client connected. Connected clients: %d. Limit %d.\n", numClients+1, connectionLimit) go client.read() @@ -298,41 +257,43 @@ func wsAuth(client *WebsocketClient, data interface{}) error { } var auth WebsocketAuth - err := common.JSONDecode(data.([]byte), &auth) + err := json.Unmarshal(data.([]byte), &auth) if err != nil { wsResp.Error = err.Error() client.SendWebsocketMessage(wsResp) return err } - hashPW := common.HexEncodeToString(common.GetSHA256([]byte(bot.config.Webserver.AdminPassword))) - - if auth.Username == bot.config.Webserver.AdminUsername && auth.Password == hashPW { + hashPW := crypto.HexEncodeToString(crypto.GetSHA256([]byte(Bot.Config.RemoteControl.Password))) + if auth.Username == Bot.Config.RemoteControl.Username && auth.Password == hashPW { client.Authenticated = true wsResp.Data = WebsocketResponseSuccess - log.Debugf("websocket: client authenticated successfully") + log.Debugln(log.WebsocketMgr, + "websocket: client authenticated successfully") return client.SendWebsocketMessage(wsResp) } wsResp.Error = "invalid username/password" client.authFailures++ client.SendWebsocketMessage(wsResp) - if client.authFailures >= bot.config.Webserver.WebsocketMaxAuthFailures { - log.Debugf("websocket: disconnecting client, maximum auth failures threshold reached (failures: %d limit: %d)", - client.authFailures, bot.config.Webserver.WebsocketMaxAuthFailures) + if client.authFailures >= Bot.Config.RemoteControl.WebsocketRPC.MaxAuthFailures { + log.Debugf(log.WebsocketMgr, + "websocket: disconnecting client, maximum auth failures threshold reached (failures: %d limit: %d)\n", + client.authFailures, Bot.Config.RemoteControl.WebsocketRPC.MaxAuthFailures) wsHub.Unregister <- client return nil } - log.Debugf("websocket: client sent wrong username/password (failures: %d limit: %d)", - client.authFailures, bot.config.Webserver.WebsocketMaxAuthFailures) + log.Debugf(log.WebsocketMgr, + "websocket: client sent wrong username/password (failures: %d limit: %d)\n", + client.authFailures, Bot.Config.RemoteControl.WebsocketRPC.MaxAuthFailures) return nil } func wsGetConfig(client *WebsocketClient, data interface{}) error { wsResp := WebsocketEventResponse{ Event: "GetConfig", - Data: bot.config, + Data: Bot.Config, } return client.SendWebsocketMessage(wsResp) } @@ -342,14 +303,14 @@ func wsSaveConfig(client *WebsocketClient, data interface{}) error { Event: "SaveConfig", } var cfg config.Config - err := common.JSONDecode(data.([]byte), &cfg) + err := json.Unmarshal(data.([]byte), &cfg) if err != nil { wsResp.Error = err.Error() client.SendWebsocketMessage(wsResp) return err } - err = bot.config.UpdateConfig(bot.configFile, &cfg) + err = Bot.Config.UpdateConfig(Bot.Settings.ConfigFile, &cfg, Bot.Settings.EnableDryRun) if err != nil { wsResp.Error = err.Error() client.SendWebsocketMessage(wsResp) @@ -383,15 +344,15 @@ func wsGetTicker(client *WebsocketClient, data interface{}) error { Event: "GetTicker", } var tickerReq WebsocketOrderbookTickerRequest - err := common.JSONDecode(data.([]byte), &tickerReq) + err := json.Unmarshal(data.([]byte), &tickerReq) if err != nil { wsResp.Error = err.Error() client.SendWebsocketMessage(wsResp) return err } - result, err := GetSpecificTicker(tickerReq.Currency, - tickerReq.Exchange, tickerReq.AssetType) + result, err := GetSpecificTicker(currency.NewPairFromString(tickerReq.Currency), + tickerReq.Exchange, asset.Item(tickerReq.AssetType)) if err != nil { wsResp.Error = err.Error() @@ -415,15 +376,15 @@ func wsGetOrderbook(client *WebsocketClient, data interface{}) error { Event: "GetOrderbook", } var orderbookReq WebsocketOrderbookTickerRequest - err := common.JSONDecode(data.([]byte), &orderbookReq) + err := json.Unmarshal(data.([]byte), &orderbookReq) if err != nil { wsResp.Error = err.Error() client.SendWebsocketMessage(wsResp) return err } - result, err := GetSpecificOrderbook(orderbookReq.Currency, - orderbookReq.Exchange, orderbookReq.AssetType) + result, err := GetSpecificOrderbook(currency.NewPairFromString(orderbookReq.Currency), + orderbookReq.Exchange, asset.Item(orderbookReq.AssetType)) if err != nil { wsResp.Error = err.Error() @@ -452,6 +413,6 @@ func wsGetPortfolio(client *WebsocketClient, data interface{}) error { wsResp := WebsocketEventResponse{ Event: "GetPortfolio", } - wsResp.Data = bot.portfolio.GetPortfolioSummary() + wsResp.Data = Bot.Portfolio.GetPortfolioSummary() return client.SendWebsocketMessage(wsResp) } diff --git a/engine/websocket_types.go b/engine/websocket_types.go new file mode 100644 index 00000000..c74483fc --- /dev/null +++ b/engine/websocket_types.go @@ -0,0 +1,49 @@ +package engine + +import "github.com/gorilla/websocket" + +// WebsocketClient stores information related to the websocket client +type WebsocketClient struct { + Hub *WebsocketHub + Conn *websocket.Conn + Authenticated bool + authFailures int + Send chan []byte +} + +// WebsocketHub stores the data for managing websocket clients +type WebsocketHub struct { + Clients map[*WebsocketClient]bool + Broadcast chan []byte + Register chan *WebsocketClient + Unregister chan *WebsocketClient +} + +// WebsocketEvent is the struct used for websocket events +type WebsocketEvent struct { + Exchange string `json:"exchange,omitempty"` + AssetType string `json:"assetType,omitempty"` + Event string + Data interface{} +} + +// WebsocketEventResponse is the struct used for websocket event responses +type WebsocketEventResponse struct { + Event string `json:"event"` + Data interface{} `json:"data"` + Error string `json:"error"` +} + +// WebsocketOrderbookTickerRequest is a struct used for ticker and orderbook +// requests +type WebsocketOrderbookTickerRequest struct { + Exchange string `json:"exchangeName"` + Currency string `json:"currency"` + AssetType string `json:"assetType"` +} + +// WebsocketAuth is a struct used for +type WebsocketAuth struct { + Username string `json:"username"` + Password string `json:"password"` +} diff --git a/events/event_test.go b/events/event_test.go deleted file mode 100644 index 69aeb09a..00000000 --- a/events/event_test.go +++ /dev/null @@ -1,369 +0,0 @@ -package events - -// -// import ( -// "testing" -// -// "github.com/thrasher-corp/gocryptotrader/config" -// "github.com/thrasher-corp/gocryptotrader/currency/pair" -// "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" -// "github.com/thrasher-corp/gocryptotrader/smsglobal" -// ) -// -// var ( -// loaded = false -// ) -// -// func testSetup(t *testing.T) { -// if !loaded { -// cfg := config.GetConfig() -// err := cfg.LoadConfig("") -// if err != nil { -// t.Fatalf("Test failed. Failed to load config %s", err) -// } -// smsglobal.New(cfg.SMS.Username, cfg.SMS.Password, cfg.Name, cfg.SMS.Contacts) -// loaded = true -// } -// } -// -// func TestAddEvent(t *testing.T) { -// testSetup(t) -// -// pair := currency.NewPairFromStrings("BTC", "USD") -// eventID, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) -// if err != nil && eventID != 0 { -// t.Errorf("Test Failed. AddEvent: Error, %s", err) -// } -// eventID, err = AddEvent("ANXX", "price", ">,==", pair, "SPOT", actionTest) -// if err == nil && eventID == 0 { -// t.Error("Test Failed. AddEvent: Error, error not captured in Exchange") -// } -// eventID, err = AddEvent("ANX", "prices", ">,==", pair, "SPOT", actionTest) -// if err == nil && eventID == 0 { -// t.Error("Test Failed. AddEvent: Error, error not captured in Item") -// } -// eventID, err = AddEvent("ANX", "price", "3===D", pair, "SPOT", actionTest) -// if err == nil && eventID == 0 { -// t.Error("Test Failed. AddEvent: Error, error not captured in Condition") -// } -// eventID, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", "console_prints") -// if err == nil && eventID == 0 { -// t.Error("Test Failed. AddEvent: Error, error not captured in Action") -// } -// -// if !RemoveEvent(eventID) { -// t.Error("Test Failed. RemoveEvent: Error, error removing event") -// } -// } -// -// func TestRemoveEvent(t *testing.T) { -// testSetup(t) -// -// pair := currency.NewPairFromStrings("BTC", "USD") -// eventID, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) -// if err != nil && eventID != 0 { -// t.Errorf("Test Failed. RemoveEvent: Error, %s", err) -// } -// if !RemoveEvent(eventID) { -// t.Error("Test Failed. RemoveEvent: Error, error removing event") -// } -// if RemoveEvent(1234) { -// t.Error("Test Failed. RemoveEvent: Error, error removing event") -// } -// } -// -// func TestGetEventCounter(t *testing.T) { -// testSetup(t) -// -// pair := currency.NewPairFromStrings("BTC", "USD") -// one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) -// if err != nil { -// t.Errorf("Test Failed. GetEventCounter: Error, %s", err) -// } -// two, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) -// if err != nil { -// t.Errorf("Test Failed. GetEventCounter: Error, %s", err) -// } -// three, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) -// if err != nil { -// t.Errorf("Test Failed. GetEventCounter: Error, %s", err) -// } -// -// Events[three-1].Executed = true -// -// total, _ := GetEventCounter() -// if total <= 0 { -// t.Errorf("Test Failed. GetEventCounter: Total = %d", total) -// } -// if !RemoveEvent(one) { -// t.Error("Test Failed. GetEventCounter: Error, error removing event") -// } -// if !RemoveEvent(two) { -// t.Error("Test Failed. GetEventCounter: Error, error removing event") -// } -// if !RemoveEvent(three) { -// t.Error("Test Failed. GetEventCounter: Error, error removing event") -// } -// -// total2, _ := GetEventCounter() -// if total2 != 0 { -// t.Errorf("Test Failed. GetEventCounter: Total = %d", total2) -// } -// } -// -// func TestExecuteAction(t *testing.T) { -// testSetup(t) -// -// pair := currency.NewPairFromStrings("BTC", "USD") -// one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) -// if err != nil { -// t.Fatalf("Test Failed. ExecuteAction: Error, %s", err) -// } -// isExecuted := Events[one].ExecuteAction() -// if !isExecuted { -// t.Error("Test Failed. ExecuteAction: Error, error removing event") -// } -// if !RemoveEvent(one) { -// t.Error("Test Failed. ExecuteAction: Error, error removing event") -// } -// -// action := actionSMSNotify + "," + "ALL" -// one, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", action) -// if err != nil { -// t.Fatalf("Test Failed. ExecuteAction: Error, %s", err) -// } -// -// isExecuted = Events[one].ExecuteAction() -// if !isExecuted { -// t.Error("Test Failed. ExecuteAction: Error, error removing event") -// } -// if !RemoveEvent(one) { -// t.Error("Test Failed. ExecuteAction: Error, error removing event") -// } -// -// action = actionSMSNotify + "," + "StyleGherkin" -// one, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", action) -// if err != nil { -// t.Fatalf("Test Failed. ExecuteAction: Error, %s", err) -// } -// -// isExecuted = Events[one].ExecuteAction() -// if !isExecuted { -// t.Error("Test Failed. ExecuteAction: Error, error removing event") -// } -// if !RemoveEvent(one) { -// t.Error("Test Failed. ExecuteAction: Error, error removing event") -// } -// // More tests when ExecuteAction is expanded -// } -// -// func TestEventToString(t *testing.T) { -// testSetup(t) -// -// pair := currency.NewPairFromStrings("BTC", "USD") -// one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) -// if err != nil { -// t.Errorf("Test Failed. EventToString: Error, %s", err) -// } -// -// eventString := Events[one].String() -// if eventString != "If the BTCUSD [SPOT] price on ANX is > == then ACTION_TEST." { -// t.Error("Test Failed. EventToString: Error, incorrect return string") -// } -// -// if !RemoveEvent(one) { -// t.Error("Test Failed. EventToString: Error, error removing event") -// } -// } -// -// func TestCheckCondition(t *testing.T) { -// testSetup(t) -// -// // Test invalid currency pair -// newPair := currency.NewPairFromStrings("A", "B") -// one, err := AddEvent("ANX", "price", ">=,10", newPair, "SPOT", actionTest) -// if err != nil { -// t.Errorf("Test Failed. CheckCondition: Error, %s", err) -// } -// conditionBool := Events[one].CheckCondition() -// if conditionBool { -// t.Error("Test Failed. CheckCondition: Error, wrong conditional.") -// } -// -// // Test last price == 0 -// var tickerNew ticker.Price -// tickerNew.Last = 0 -// newPair = currency.NewPairFromStrings("BTC", "USD") -// ticker.ProcessTicker("ANX", newPair, tickerNew, ticker.Spot) -// Events[one].Pair = newPair -// conditionBool = Events[one].CheckCondition() -// if conditionBool { -// t.Error("Test Failed. CheckCondition: Error, wrong conditional.") -// } -// -// // Test last pricce > 0 and conditional logic -// tickerNew.Last = 11 -// ticker.ProcessTicker("ANX", newPair, tickerNew, ticker.Spot) -// Events[one].Condition = ">,10" -// conditionBool = Events[one].CheckCondition() -// if !conditionBool { -// t.Error("Test Failed. CheckCondition: Error, wrong conditional.") -// } -// -// // Test last price >= 10 -// Events[one].Condition = ">=,10" -// conditionBool = Events[one].CheckCondition() -// if !conditionBool { -// t.Error("Test Failed. CheckCondition: Error, wrong conditional.") -// } -// -// // Test last price <= 10 -// Events[one].Condition = "<,100" -// conditionBool = Events[one].CheckCondition() -// if !conditionBool { -// t.Error("Test Failed. CheckCondition: Error, wrong conditional.") -// } -// -// // Test last price <= 10 -// Events[one].Condition = "<=,100" -// conditionBool = Events[one].CheckCondition() -// if !conditionBool { -// t.Error("Test Failed. CheckCondition: Error, wrong conditional.") -// } -// -// Events[one].Condition = "==,11" -// conditionBool = Events[one].CheckCondition() -// if !conditionBool { -// t.Error("Test Failed. CheckCondition: Error, wrong conditional.") -// } -// -// Events[one].Condition = "^,11" -// conditionBool = Events[one].CheckCondition() -// if conditionBool { -// t.Error("Test Failed. CheckCondition: Error, wrong conditional.") -// } -// -// if !RemoveEvent(one) { -// t.Error("Test Failed. CheckCondition: Error, error removing event") -// } -// } -// -// func TestIsValidEvent(t *testing.T) { -// testSetup(t) -// -// err := IsValidEvent("ANX", "price", ">,==", actionTest) -// if err != nil { -// t.Errorf("Test Failed. IsValidEvent: %s", err) -// } -// err = IsValidEvent("ANX", "price", ">,", actionTest) -// if err == nil { -// t.Errorf("Test Failed. IsValidEvent: %s", err) -// } -// err = IsValidEvent("ANX", "Testy", ">,==", actionTest) -// if err == nil { -// t.Errorf("Test Failed. IsValidEvent: %s", err) -// } -// err = IsValidEvent("Testys", "price", ">,==", actionTest) -// if err == nil { -// t.Errorf("Test Failed. IsValidEvent: %s", err) -// } -// -// action := "blah,blah" -// err = IsValidEvent("ANX", "price", ">=,10", action) -// if err == nil { -// t.Errorf("Test Failed. IsValidEvent: %s", err) -// } -// -// action = "SMS,blah" -// err = IsValidEvent("ANX", "price", ">=,10", action) -// if err == nil { -// t.Errorf("Test Failed. IsValidEvent: %s", err) -// } -// -// //Function tests need to appended to this function when more actions are -// //implemented -// } -// -// func TestCheckEvents(t *testing.T) { -// testSetup(t) -// -// pair := currency.NewPairFromStrings("BTC", "USD") -// _, err := AddEvent("ANX", "price", ">=,10", pair, "SPOT", actionTest) -// if err != nil { -// t.Fatal("Test failed. TestChcheckEvents add event") -// } -// -// go CheckEvents() -// } -// -// func TestIsValidExchange(t *testing.T) { -// testSetup(t) -// -// boolean := IsValidExchange("ANX") -// if !boolean { -// t.Error("Test Failed. IsValidExchange: Error, incorrect Exchange") -// } -// boolean = IsValidExchange("OBTUSE") -// if boolean { -// t.Error("Test Failed. IsValidExchange: Error, incorrect return") -// } -// } -// -// func TestIsValidCondition(t *testing.T) { -// testSetup(t) -// -// boolean := IsValidCondition(">") -// if !boolean { -// t.Error("Test Failed. IsValidCondition: Error, incorrect Condition") -// } -// boolean = IsValidCondition(">=") -// if !boolean { -// t.Error("Test Failed. IsValidCondition: Error, incorrect Condition") -// } -// boolean = IsValidCondition("<") -// if !boolean { -// t.Error("Test Failed. IsValidCondition: Error, incorrect Condition") -// } -// boolean = IsValidCondition("<=") -// if !boolean { -// t.Error("Test Failed. IsValidCondition: Error, incorrect Condition") -// } -// boolean = IsValidCondition("==") -// if !boolean { -// t.Error("Test Failed. IsValidCondition: Error, incorrect Condition") -// } -// boolean = IsValidCondition("**********") -// if boolean { -// t.Error("Test Failed. IsValidCondition: Error, incorrect return") -// } -// } -// -// func TestIsValidAction(t *testing.T) { -// testSetup(t) -// -// boolean := IsValidAction("sms") -// if !boolean { -// t.Error("Test Failed. IsValidAction: Error, incorrect Action") -// } -// boolean = IsValidAction(actionTest) -// if !boolean { -// t.Error("Test Failed. IsValidAction: Error, incorrect Action") -// } -// boolean = IsValidAction("randomstring") -// if boolean { -// t.Error("Test Failed. IsValidAction: Error, incorrect return") -// } -// } -// -// func TestIsValidItem(t *testing.T) { -// testSetup(t) -// -// boolean := IsValidItem("price") -// if !boolean { -// t.Error("Test Failed. IsValidItem: Error, incorrect Item") -// } -// boolean = IsValidItem("obtuse") -// if boolean { -// t.Error("Test Failed. IsValidItem: Error, incorrect return") -// } -// } diff --git a/events/events.go b/events/events.go deleted file mode 100644 index 2828efaa..00000000 --- a/events/events.go +++ /dev/null @@ -1,282 +0,0 @@ -package events - -import ( - "errors" - "fmt" - "strconv" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/communications" - "github.com/thrasher-corp/gocryptotrader/communications/base" - "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - log "github.com/thrasher-corp/gocryptotrader/logger" -) - -const ( - itemPrice = "PRICE" - greaterThan = ">" - greaterThanOrEqual = ">=" - lessThan = "<" - lessThanOrEqual = "<=" - isEqual = "==" - actionSMSNotify = "SMS" - actionConsolePrint = "CONSOLE_PRINT" - actionTest = "ACTION_TEST" -) - -var ( - errInvalidItem = errors.New("invalid item") - errInvalidCondition = errors.New("invalid conditional option") - errInvalidAction = errors.New("invalid action") - errExchangeDisabled = errors.New("desired exchange is disabled") - - // NOTE comms is an interim implementation - comms *communications.Communications -) - -// Event struct holds the event variables -type Event struct { - ID int - Exchange string - Item string - Condition string - Pair currency.Pair - Asset string - Action string - Executed bool -} - -// Events variable is a pointer array to the event structures that will be -// appended -var Events []*Event - -// SetComms is an interim function that will support a median integration. This -// sets the current comms package. -func SetComms(commsP *communications.Communications) { - comms = commsP -} - -// AddEvent adds an event to the Events chain and returns an index/eventID -// and an error -func AddEvent(exchange, item, condition string, currencyPair currency.Pair, asset, action string) (int, error) { - err := IsValidEvent(exchange, item, condition, action) - if err != nil { - return 0, err - } - - evt := &Event{} - - if len(Events) == 0 { - evt.ID = 0 - } else { - evt.ID = len(Events) + 1 - } - - evt.Exchange = exchange - evt.Item = item - evt.Condition = condition - evt.Pair = currencyPair - evt.Asset = asset - evt.Action = action - evt.Executed = false - Events = append(Events, evt) - return evt.ID, nil -} - -// RemoveEvent deletes and event by its ID -func RemoveEvent(eventID int) bool { - for i, x := range Events { - if x.ID == eventID { - Events = append(Events[:i], Events[i+1:]...) - return true - } - } - return false -} - -// GetEventCounter displays the emount of total events on the chain and the -// events that have been executed. -func GetEventCounter() (total, executed int) { - total = len(Events) - - for _, x := range Events { - if x.Executed { - executed++ - } - } - return total, executed -} - -// ExecuteAction will execute the action pending on the chain -func (e *Event) ExecuteAction() bool { - if common.StringContains(e.Action, ",") { - action := common.SplitStrings(e.Action, ",") - if action[0] == actionSMSNotify { - message := fmt.Sprintf("Event triggered: %s", e.String()) - if action[1] == "ALL" { - comms.PushEvent(base.Event{TradeDetails: message}) - } - } - } else { - log.Debugf("Event triggered: %s", e.String()) - } - return true -} - -// String turns the structure event into a string -func (e *Event) String() string { - condition := common.SplitStrings(e.Condition, ",") - return fmt.Sprintf( - "If the %s%s [%s] %s on %s is %s then %s.", e.Pair.Base.String(), - e.Pair.Quote.String(), - e.Asset, - e.Item, - e.Exchange, - condition[0]+" "+condition[1], - e.Action, - ) -} - -// CheckCondition will check the event structure to see if there is a condition -// met -func (e *Event) CheckCondition() bool { - condition := common.SplitStrings(e.Condition, ",") - targetPrice, _ := strconv.ParseFloat(condition[1], 64) - - t, err := ticker.GetTicker(e.Exchange, e.Pair, e.Asset) - if err != nil { - return false - } - - lastPrice := t.Last - - if lastPrice == 0 { - return false - } - - switch condition[0] { - case greaterThan: - if lastPrice > targetPrice { - return e.ExecuteAction() - } - case greaterThanOrEqual: - if lastPrice >= targetPrice { - return e.ExecuteAction() - } - case lessThan: - if lastPrice < targetPrice { - return e.ExecuteAction() - } - case lessThanOrEqual: - if lastPrice <= targetPrice { - return e.ExecuteAction() - } - case isEqual: - if lastPrice == targetPrice { - return e.ExecuteAction() - } - } - return false -} - -// IsValidEvent checks the actions to be taken and returns an error if incorrect -func IsValidEvent(exchange, item, condition, action string) error { - exchange = common.StringToUpper(exchange) - item = common.StringToUpper(item) - action = common.StringToUpper(action) - - if !IsValidExchange(exchange) { - return errExchangeDisabled - } - - if !IsValidItem(item) { - return errInvalidItem - } - - if !common.StringContains(condition, ",") { - return errInvalidCondition - } - - c := common.SplitStrings(condition, ",") - - if !IsValidCondition(c[0]) || c[1] == "" { - return errInvalidCondition - } - - if common.StringContains(action, ",") { - a := common.SplitStrings(action, ",") - - if a[0] != actionSMSNotify { - return errInvalidAction - } - - if a[1] != "ALL" { - comms.PushEvent(base.Event{Type: a[1]}) - } - } else if action != actionConsolePrint && action != actionTest { - return errInvalidAction - } - - return nil -} - -// CheckEvents is the overarching routine that will iterate through the Events -// chain -func CheckEvents() { - for { - total, executed := GetEventCounter() - if total > 0 && executed != total { - for _, event := range Events { - if !event.Executed { - success := event.CheckCondition() - if success { - log.Debugf( - "Event %d triggered on %s successfully.\n", event.ID, - event.Exchange, - ) - event.Executed = true - } - } - } - } - } -} - -// IsValidExchange validates the exchange -func IsValidExchange(exchange string) bool { - exchange = common.StringToUpper(exchange) - cfg := config.GetConfig() - for x := range cfg.Exchanges { - if cfg.Exchanges[x].Name == exchange && cfg.Exchanges[x].Enabled { - return true - } - } - return false -} - -// IsValidCondition validates passed in condition -func IsValidCondition(condition string) bool { - switch condition { - case greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, isEqual: - return true - } - return false -} - -// IsValidAction validates passed in action -func IsValidAction(action string) bool { - action = common.StringToUpper(action) - switch action { - case actionSMSNotify, actionConsolePrint, actionTest: - return true - } - return false -} - -// IsValidItem validates passed in Item -func IsValidItem(item string) bool { - item = common.StringToUpper(item) - return (item == itemPrice) -} diff --git a/exchange_test.go b/exchange_test.go deleted file mode 100644 index 6eda4032..00000000 --- a/exchange_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package main - -import ( - "testing" - - "github.com/thrasher-corp/gocryptotrader/config" -) - -var testSetup = false - -func SetupTest(t *testing.T) { - if !testSetup { - bot.config = &config.Cfg - err := bot.config.LoadConfig("./testdata/configtest.json") - if err != nil { - t.Fatalf("Test failed. SetupTest: Failed to load config: %s", err) - } - testSetup = true - } - - if CheckExchangeExists("Bitfinex") { - return - } - err := LoadExchange("Bitfinex", false, nil) - if err != nil { - t.Errorf("Test failed. SetupTest: Failed to load exchange: %s", err) - } -} - -func CleanupTest(t *testing.T) { - if !CheckExchangeExists("Bitfinex") { - return - } - - err := UnloadExchange("Bitfinex") - if err != nil { - t.Fatalf("Test failed. CleanupTest: Failed to unload exchange: %s", - err) - } -} - -func TestCheckExchangeExists(t *testing.T) { - SetupTest(t) - - if !CheckExchangeExists("Bitfinex") { - t.Errorf("Test failed. TestGetExchangeExists: Unable to find exchange") - } - - if CheckExchangeExists("Asdsad") { - t.Errorf("Test failed. TestGetExchangeExists: Non-existent exchange found") - } - - CleanupTest(t) -} - -func TestGetExchangeByName(t *testing.T) { - SetupTest(t) - - exch := GetExchangeByName("Bitfinex") - if exch == nil { - t.Errorf("Test failed. TestGetExchangeByName: Failed to get exchange") - } - - if !exch.IsEnabled() { - t.Errorf("Test failed. TestGetExchangeByName: Unexpected result") - } - - exch.SetEnabled(false) - bfx := GetExchangeByName("Bitfinex") - if bfx.IsEnabled() { - t.Errorf("Test failed. TestGetExchangeByName: Unexpected result") - } - - if exch.GetName() != "Bitfinex" { - t.Errorf("Test failed. TestGetExchangeByName: Unexpected result") - } - - exch = GetExchangeByName("Asdasd") - if exch != nil { - t.Errorf("Test failed. TestGetExchangeByName: Non-existent exchange found") - } - - CleanupTest(t) -} - -func TestReloadExchange(t *testing.T) { - SetupTest(t) - - err := ReloadExchange("asdf") - if err != ErrExchangeNotFound { - t.Errorf("Test failed. TestReloadExchange: Incorrect result: %s", - err) - } - - err = ReloadExchange("Bitfinex") - if err != nil { - t.Errorf("Test failed. TestReloadExchange: Incorrect result: %s", - err) - } - - CleanupTest(t) - - err = ReloadExchange("asdf") - if err != ErrNoExchangesLoaded { - t.Errorf("Test failed. TestReloadExchange: Incorrect result: %s", - err) - } -} - -func TestUnloadExchange(t *testing.T) { - SetupTest(t) - - err := UnloadExchange("asdf") - if err != ErrExchangeNotFound { - t.Errorf("Test failed. TestUnloadExchange: Incorrect result: %s", - err) - } - - err = UnloadExchange("Bitfinex") - if err != nil { - t.Errorf("Test failed. TestUnloadExchange: Failed to get exchange. %s", - err) - } - - err = UnloadExchange("asdf") - if err != ErrNoExchangesLoaded { - t.Errorf("Test failed. TestUnloadExchange: Incorrect result: %s", - err) - } - - CleanupTest(t) -} - -func TestSetupExchanges(t *testing.T) { - SetupTest(t) - SetupExchanges() - CleanupTest(t) -} diff --git a/exchanges/README.md b/exchanges/README.md index bbaf3ca1..9333cc6e 100644 --- a/exchanges/README.md +++ b/exchanges/README.md @@ -45,4 +45,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/alphapoint/README.md b/exchanges/alphapoint/README.md index dd75369b..99c6a8f7 100644 --- a/exchanges/alphapoint/README.md +++ b/exchanges/alphapoint/README.md @@ -46,4 +46,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/alphapoint/alphapoint.go b/exchanges/alphapoint/alphapoint.go index 25bea99f..33400dd2 100644 --- a/exchanges/alphapoint/alphapoint.go +++ b/exchanges/alphapoint/alphapoint.go @@ -2,17 +2,17 @@ package alphapoint import ( "bytes" + "encoding/json" "errors" "fmt" "net/http" "strconv" - "time" + "strings" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) const ( @@ -48,22 +48,6 @@ type Alphapoint struct { WebsocketConn *websocket.Conn } -// SetDefaults sets current default settings -func (a *Alphapoint) SetDefaults() { - a.APIUrl = alphapointDefaultAPIURL - a.WebsocketURL = alphapointDefaultWebsocketURL - a.AssetTypes = []string{ticker.Spot} - a.SupportsAutoPairUpdating = false - a.SupportsRESTTickerBatching = false - a.APIWithdrawPermissions = exchange.WithdrawCryptoWith2FA | - exchange.AutoWithdrawCryptoWithAPIPermission | - exchange.NoFiatWithdrawals - a.Requester = request.New(a.Name, - request.NewRateLimit(time.Minute*10, alphapointAuthRate), - request.NewRateLimit(time.Minute*10, alphapointUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) -} - // GetTicker returns current ticker information from Alphapoint for a selected // currency pair ie "BTCUSD" func (a *Alphapoint) GetTicker(currencyPair string) (Ticker, error) { @@ -362,7 +346,7 @@ func (a *Alphapoint) WithdrawCoins(symbol, product, address string, amount float } func (a *Alphapoint) convertOrderTypeToOrderTypeNumber(orderType string) (orderTypeNumber int64) { - if orderType == exchange.MarketOrderType.ToString() { + if orderType == order.Market.String() { orderTypeNumber = 1 } @@ -527,9 +511,9 @@ func (a *Alphapoint) GetOrderFee(symbol, side string, quantity, price float64) ( func (a *Alphapoint) SendHTTPRequest(method, path string, data map[string]interface{}, result interface{}) error { headers := make(map[string]string) headers["Content-Type"] = "application/json" - path = fmt.Sprintf("%s/ajax/v%s/%s", a.APIUrl, alphapointAPIVersion, path) + path = fmt.Sprintf("%s/ajax/v%s/%s", a.API.Endpoints.URL, alphapointAPIVersion, path) - PayloadJSON, err := common.JSONEncode(data) + PayloadJSON, err := json.Marshal(data) if err != nil { return errors.New("unable to JSON request") } @@ -548,7 +532,7 @@ func (a *Alphapoint) SendHTTPRequest(method, path string, data map[string]interf // SendAuthenticatedHTTPRequest sends an authenticated request func (a *Alphapoint) SendAuthenticatedHTTPRequest(method, path string, data map[string]interface{}, result interface{}) error { - if !a.AuthenticatedAPISupport { + if !a.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, a.Name) } @@ -556,14 +540,15 @@ func (a *Alphapoint) SendAuthenticatedHTTPRequest(method, path string, data map[ headers := make(map[string]string) headers["Content-Type"] = "application/json" - data["apiKey"] = a.APIKey + data["apiKey"] = a.API.Credentials.Key data["apiNonce"] = n - hmac := common.GetHMAC(common.HashSHA256, []byte(n.String()+a.ClientID+a.APIKey), - []byte(a.APISecret)) - data["apiSig"] = common.StringToUpper(common.HexEncodeToString(hmac)) - path = fmt.Sprintf("%s/ajax/v%s/%s", a.APIUrl, alphapointAPIVersion, path) + hmac := crypto.GetHMAC(crypto.HashSHA256, + []byte(n.String()+a.API.Credentials.ClientID+a.API.Credentials.Key), + []byte(a.API.Credentials.Secret)) + data["apiSig"] = strings.ToUpper(crypto.HexEncodeToString(hmac)) + path = fmt.Sprintf("%s/ajax/v%s/%s", a.API.Endpoints.URL, alphapointAPIVersion, path) - PayloadJSON, err := common.JSONEncode(data) + PayloadJSON, err := json.Marshal(data) if err != nil { return errors.New("unable to JSON request") } diff --git a/exchanges/alphapoint/alphapoint_test.go b/exchanges/alphapoint/alphapoint_test.go index 9fe8d764..910e9880 100644 --- a/exchanges/alphapoint/alphapoint_test.go +++ b/exchanges/alphapoint/alphapoint_test.go @@ -1,11 +1,15 @@ package alphapoint import ( + "encoding/json" + "log" + "os" "testing" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) const ( @@ -15,509 +19,445 @@ const ( canManipulateRealOrders = false ) -func TestSetDefaults(t *testing.T) { - t.Parallel() - SetDefaults := Alphapoint{} +var a Alphapoint - SetDefaults.SetDefaults() - if SetDefaults.APIUrl != "https://sim3.alphapoint.com:8400" { - t.Error("Test Failed - SetDefaults: String Incorrect -", SetDefaults.APIUrl) +func TestMain(m *testing.M) { + a.SetDefaults() + a.API.Credentials.Key = apiKey + a.API.Credentials.Secret = apiSecret + a.API.AuthenticatedSupport = true + if a.API.Endpoints.URL != "https://sim3.alphapoint.com:8400" { + log.Fatal("SetDefaults: String Incorrect -", a.API.Endpoints.URL) } - if SetDefaults.WebsocketURL != "wss://sim3.alphapoint.com:8401/v1/GetTicker/" { - t.Error("Test Failed - SetDefaults: String Incorrect -", SetDefaults.WebsocketURL) + if a.API.Endpoints.WebsocketURL != "wss://sim3.alphapoint.com:8401/v1/GetTicker/" { + log.Fatal("SetDefaults: String Incorrect -", a.API.Endpoints.WebsocketURL) } + os.Exit(m.Run()) } -func testSetAPIKey(a *Alphapoint) { - a.APIKey = apiKey - a.APISecret = apiSecret - a.AuthenticatedAPISupport = true +func areTestAPIKeysSet() bool { + return a.ValidateAPICredentials() } -func testIsAPIKeysSet(a *Alphapoint) bool { - if apiKey != "" && apiSecret != "" && a.AuthenticatedAPISupport { - return true - } - return false -} func TestGetTicker(t *testing.T) { - alpha := Alphapoint{} - alpha.SetDefaults() - + t.Parallel() var ticker Ticker var err error - if onlineTest { - ticker, err = alpha.GetTicker("BTCUSD") + ticker, err = a.GetTicker("BTCUSD") if err != nil { - t.Fatal("Test Failed - Alphapoint GetTicker init error: ", err) + t.Fatal("Alphapoint GetTicker init error: ", err) } - _, err = alpha.GetTicker("wigwham") + _, err = a.GetTicker("wigwham") if err == nil { - t.Error("Test Failed - Alphapoint GetTicker error") + t.Error("Alphapoint GetTicker Expected error") } } else { mockResp := []byte( string(`{"high":253.101,"last":249.76,"bid":248.8901,"volume":5.813354,"low":231.21,"ask":248.9012,"Total24HrQtyTraded":52.654968,"Total24HrProduct2Traded":569.05762,"Total24HrNumTrades":4,"sellOrderCount":7,"buyOrderCount":11,"numOfCreateOrders":0,"isAccepted":true}`), ) - err = common.JSONDecode(mockResp, &ticker) + err = json.Unmarshal(mockResp, &ticker) if err != nil { - t.Fatal("Test Failed - Alphapoint GetTicker unmarshalling error: ", err) + t.Fatal("Alphapoint GetTicker unmarshalling error: ", err) } if ticker.Last != 249.76 { - t.Error("Test failed - Alphapoint GetTicker expected last = 249.76") + t.Error("Alphapoint GetTicker expected last = 249.76") } } if ticker.Last < 0 { - t.Error("Test failed - Alphapoint GetTicker last < 0") + t.Error("Alphapoint GetTicker last < 0") } } func TestGetTrades(t *testing.T) { - alpha := Alphapoint{} - alpha.SetDefaults() - + t.Parallel() var trades Trades var err error - if onlineTest { - trades, err = alpha.GetTrades("BTCUSD", 0, 10) + trades, err = a.GetTrades("BTCUSD", 0, 10) if err != nil { - t.Fatalf("Test Failed - Init error: %s", err) + t.Fatalf("Init error: %s", err) } - _, err = alpha.GetTrades("wigwham", 0, 10) + _, err = a.GetTrades("wigwham", 0, 10) if err == nil { - t.Fatal("Test Failed - GetTrades error") + t.Fatal("GetTrades Expected error") } } else { mockResp := []byte( string(`{"isAccepted":true,"dateTimeUtc":635507981548085938,"ins":"BTCUSD","startIndex":0,"count":10,"trades":[{"tid":0,"px":231.8379,"qty":4.913,"unixtime":1399951989,"utcticks":635355487898355234,"incomingOrderSide":0,"incomingServerOrderId":2598,"bookServerOrderId":2588},{"tid":1,"px":7895.1487,"qty":0.25,"unixtime":1403143708,"utcticks":635387405087297421,"incomingOrderSide":0,"incomingServerOrderId":284241,"bookServerOrderId":284235},{"tid":2,"px":7935.058,"qty":0.25,"unixtime":1403195348,"utcticks":635387921488684140,"incomingOrderSide":0,"incomingServerOrderId":575845,"bookServerOrderId":574078},{"tid":3,"px":7935.0448,"qty":0.25,"unixtime":1403195378,"utcticks":635387921780090390,"incomingOrderSide":0,"incomingServerOrderId":576028,"bookServerOrderId":575946},{"tid":4,"px":7933.9566,"qty":0.1168,"unixtime":1403195510,"utcticks":635387923108371640,"incomingOrderSide":0,"incomingServerOrderId":576974,"bookServerOrderId":576947},{"tid":5,"px":7961.0856,"qty":0.25,"unixtime":1403202307,"utcticks":635387991073850156,"incomingOrderSide":0,"incomingServerOrderId":600547,"bookServerOrderId":600338},{"tid":6,"px":7961.1388,"qty":0.011,"unixtime":1403202307,"utcticks":635387991073850156,"incomingOrderSide":0,"incomingServerOrderId":600547,"bookServerOrderId":600418},{"tid":7,"px":7961.2451,"qty":0.02,"unixtime":1403202307,"utcticks":635387991073850156,"incomingOrderSide":0,"incomingServerOrderId":600547,"bookServerOrderId":600428},{"tid":8,"px":7947.1437,"qty":0.09,"unixtime":1403202749,"utcticks":635387995498225156,"incomingOrderSide":0,"incomingServerOrderId":602183,"bookServerOrderId":601745},{"tid":9,"px":7818.5073,"qty":0.25,"unixtime":1403219720,"utcticks":635388165206506406,"incomingOrderSide":0,"incomingServerOrderId":661909,"bookServerOrderId":661620}]}`), ) - err = common.JSONDecode(mockResp, &trades) + err = json.Unmarshal(mockResp, &trades) if err != nil { - t.Fatal("Test Failed - GetTrades unmarshalling error: ", err) + t.Fatal("GetTrades unmarshalling error: ", err) } } if !trades.IsAccepted { - t.Error("Test Failed - GetTrades IsAccepted failed") + t.Error("GetTrades IsAccepted failed") } if trades.Count <= 0 { - t.Error("Test failed - GetTrades trades count is <= 0") + t.Error("GetTrades trades count is <= 0") } if trades.Instrument != "BTCUSD" { - t.Error("Test failed - GetTrades instrument is != BTCUSD") + t.Error("GetTrades instrument is != BTCUSD") } } func TestGetTradesByDate(t *testing.T) { - alpha := Alphapoint{} - alpha.SetDefaults() - + t.Parallel() var trades Trades var err error - if onlineTest { - trades, err = alpha.GetTradesByDate("BTCUSD", 1414799400, 1414800000) + trades, err = a.GetTradesByDate("BTCUSD", 1414799400, 1414800000) if err != nil { - t.Errorf("Test Failed - Init error: %s", err) + t.Errorf("Init error: %s", err) } - _, err = alpha.GetTradesByDate("wigwham", 1414799400, 1414800000) + _, err = a.GetTradesByDate("wigwham", 1414799400, 1414800000) if err == nil { - t.Error("Test Failed - GetTradesByDate error") + t.Error("GetTradesByDate Expected error") } } else { mockResp := []byte( string(`{"isAccepted":true,"dateTimeUtc":635504540880633671,"ins":"BTCUSD","startDate":1414799400,"endDate":1414800000,"trades":[{"tid":11505,"px":334.669,"qty":0.1211,"unixtime":1414799403,"utcticks":635503962032459843,"incomingOrderSide":1,"incomingServerOrderId":5185651,"bookServerOrderId":5162440},{"tid":11506,"px":334.669,"qty":0.1211,"unixtime":1414799405,"utcticks":635503962058446171,"incomingOrderSide":1,"incomingServerOrderId":5186245,"bookServerOrderId":5162440},{"tid":11507,"px":336.498,"qty":0.011,"unixtime":1414799407,"utcticks":635503962072967656,"incomingOrderSide":0,"incomingServerOrderId":5186530,"bookServerOrderId":5178944},{"tid":11508,"px":335.948,"qty":0.011,"unixtime":1414799410,"utcticks":635503962108055546,"incomingOrderSide":0,"incomingServerOrderId":5187260,"bookServerOrderId":5186531}]}`), ) - err = common.JSONDecode(mockResp, &trades) + err = json.Unmarshal(mockResp, &trades) if err != nil { - t.Fatal("Test Failed - GetTradesByDate unmarshalling error: ", err) + t.Fatal("GetTradesByDate unmarshalling error: ", err) } } if trades.DateTimeUTC < 0 { - t.Error("Test Failed - Alphapoint trades.Count value is negative") + t.Error("Alphapoint trades.Count value is negative") } if trades.EndDate < 0 { - t.Error("Test Failed - Alphapoint trades.DateTimeUTC value is negative") + t.Error("Alphapoint trades.DateTimeUTC value is negative") } if trades.Instrument != "BTCUSD" { - t.Error("Test Failed - Alphapoint trades.Instrument value is incorrect") + t.Error("Alphapoint trades.Instrument value is incorrect") } if !trades.IsAccepted { - t.Error("Test Failed - Alphapoint trades.IsAccepted value is true") + t.Error("Alphapoint trades.IsAccepted value is true") } if len(trades.RejectReason) > 0 { - t.Error("Test Failed - Alphapoint trades.IsAccepted value has been returned") + t.Error("Alphapoint trades.IsAccepted value has been returned") } if trades.StartDate < 0 { - t.Error("Test Failed - Alphapoint trades.StartIndex value is negative") + t.Error("Alphapoint trades.StartIndex value is negative") } } func TestGetOrderbook(t *testing.T) { - alpha := Alphapoint{} - alpha.SetDefaults() - + t.Parallel() var orderBook Orderbook var err error - if onlineTest { - orderBook, err = alpha.GetOrderbook("BTCUSD") + orderBook, err = a.GetOrderbook("BTCUSD") if err != nil { - t.Errorf("Test Failed - Init error: %s", err) + t.Errorf("Init error: %s", err) } - _, err = alpha.GetOrderbook("wigwham") + _, err = a.GetOrderbook("wigwham") if err == nil { - t.Error("Test Failed - GetOrderbook() error") + t.Error("GetOrderbook() Expected error") } } else { mockResp := []byte( string(`{"bids":[{"qty":725,"px":66},{"qty":1289,"px":65},{"qty":1266,"px":64}],"asks":[{"qty":1,"px":67},{"qty":1,"px":69},{"qty":2,"px":70}],"isAccepted":true}`), ) - err = common.JSONDecode(mockResp, &orderBook) + err = json.Unmarshal(mockResp, &orderBook) if err != nil { - t.Fatal("Test Failed - TestGetOrderbook unmarshalling error: ", err) + t.Fatal("TestGetOrderbook unmarshalling error: ", err) } if orderBook.Bids[0].Quantity != 725 { - t.Error("Test Failed - TestGetOrderbook Bids[0].Quantity != 725") + t.Error("TestGetOrderbook Bids[0].Quantity != 725") } } if !orderBook.IsAccepted { - t.Error("Test Failed - Alphapoint orderBook.IsAccepted value is negative") + t.Error("Alphapoint orderBook.IsAccepted value is negative") } if len(orderBook.Asks) == 0 { - t.Error("Test Failed - Alphapoint orderBook.Asks has len 0") + t.Error("Alphapoint orderBook.Asks has len 0") } if len(orderBook.Bids) == 0 { - t.Error("Test Failed - Alphapoint orderBook.Bids has len 0") + t.Error("Alphapoint orderBook.Bids has len 0") } } func TestGetProductPairs(t *testing.T) { - alpha := Alphapoint{} - alpha.SetDefaults() - + t.Parallel() var products ProductPairs var err error if onlineTest { - products, err = alpha.GetProductPairs() + products, err = a.GetProductPairs() if err != nil { - t.Errorf("Test Failed - Init error: %s", err) + t.Errorf("Init error: %s", err) } } else { mockResp := []byte( string(`{"productPairs":[{"name":"LTCUSD","productPairCode":100,"product1Label":"LTC","product1DecimalPlaces":8,"product2Label":"USD","product2DecimalPlaces":6}, {"name":"BTCUSD","productPairCode":99,"product1Label":"BTC","product1DecimalPlaces":8,"product2Label":"USD","product2DecimalPlaces":6}],"isAccepted":true}`), ) - err = common.JSONDecode(mockResp, &products) + err = json.Unmarshal(mockResp, &products) if err != nil { - t.Fatal("Test Failed - TestGetProductPairs unmarshalling error: ", err) + t.Fatal("TestGetProductPairs unmarshalling error: ", err) } if products.ProductPairs[0].Name != "LTCUSD" { - t.Error("Test Failed - Alphapoint ProductPairs 0 != LTCUSD") + t.Error("Alphapoint ProductPairs 0 != LTCUSD") } if products.ProductPairs[1].Product1Label != "BTC" { - t.Error("Test Failed - Alphapoint ProductPairs 1 != BTC") + t.Error("Alphapoint ProductPairs 1 != BTC") } } if !products.IsAccepted { - t.Error("Test Failed - Alphapoint ProductPairs.IsAccepted value is negative") + t.Error("Alphapoint ProductPairs.IsAccepted value is negative") } if len(products.ProductPairs) == 0 { - t.Error("Test Failed - Alphapoint ProductPairs len is 0") + t.Error("Alphapoint ProductPairs len is 0") } } func TestGetProducts(t *testing.T) { - alpha := Alphapoint{} - alpha.SetDefaults() - + t.Parallel() var products Products var err error if onlineTest { - products, err = alpha.GetProducts() + products, err = a.GetProducts() if err != nil { - t.Errorf("Test Failed - Init error: %s", err) + t.Errorf("Init error: %s", err) } } else { mockResp := []byte( string(`{"products": [{"name": "USD","isDigital": false,"productCode": 0,"decimalPlaces": 4,"fullName": "US Dollar"},{"name": "BTC","isDigital": true,"productCode": 1,"decimalPlaces": 6,"fullName": "Bitcoin"}],"isAccepted": true}`), ) - err = common.JSONDecode(mockResp, &products) + err = json.Unmarshal(mockResp, &products) if err != nil { - t.Fatal("Test Failed - TestGetProducts unmarshalling error: ", err) + t.Fatal("TestGetProducts unmarshalling error: ", err) } if products.Products[0].Name != "USD" { - t.Error("Test Failed - Alphapoint Products 0 != USD") + t.Error("Alphapoint Products 0 != USD") } if products.Products[1].ProductCode != 1 { - t.Error("Test Failed - Alphapoint Products 1 product code != 1") + t.Error("Alphapoint Products 1 product code != 1") } } if !products.IsAccepted { - t.Error("Test Failed - Alphapoint Products.IsAccepted value is negative") + t.Error("Alphapoint Products.IsAccepted value is negative") } if len(products.Products) == 0 { - t.Error("Test Failed - Alphapoint Products len is 0") + t.Error("Alphapoint Products len is 0") } } func TestCreateAccount(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } err := a.CreateAccount("test", "account", "something@something.com", "0292383745", "lolcat123") if err != nil { - t.Errorf("Test Failed - Init error: %s", err) + t.Errorf("Init error: %s", err) } err = a.CreateAccount("test", "account", "something@something.com", "0292383745", "bla") if err == nil { - t.Errorf("Test Failed - CreateAccount() error") + t.Errorf("CreateAccount() Expected error") } err = a.CreateAccount("", "", "", "", "lolcat123") if err == nil { - t.Errorf("Test Failed - CreateAccount() error") + t.Errorf("CreateAccount() Expected error") } } func TestGetUserInfo(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } _, err := a.GetUserInfo() if err == nil { - t.Error("Test Failed - GetUserInfo() error") + t.Error("GetUserInfo() Expected error") } } func TestSetUserInfo(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } _, err := a.SetUserInfo("bla", "bla", "1", "meh", true, true) if err == nil { - t.Error("Test Failed - GetUserInfo() error") + t.Error("GetUserInfo() Expected error") } } func TestGetAccountInfo(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } _, err := a.GetAccountInfo() if err == nil { - t.Error("Test Failed - GetUserInfo() error") + t.Error("GetUserInfo() Expected error") } } func TestGetAccountTrades(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } _, err := a.GetAccountTrades("", 1, 2) if err == nil { - t.Error("Test Failed - GetUserInfo() error") + t.Error("GetUserInfo() Expected error") } } func TestGetDepositAddresses(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } _, err := a.GetDepositAddresses() if err == nil { - t.Error("Test Failed - GetUserInfo() error") + t.Error("GetUserInfo() Expected error") } } func TestWithdrawCoins(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } err := a.WithdrawCoins("", "", "", 0.01) if err == nil { - t.Error("Test Failed - GetUserInfo() error") + t.Error("GetUserInfo() Expected error") } } func TestCreateOrder(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } - _, err := a.CreateOrder("", "", exchange.MarketOrderType.ToString(), 0.01, 0) + _, err := a.CreateOrder("", "", order.Limit.String(), 0.01, 0) if err == nil { - t.Error("Test Failed - GetUserInfo() error") + t.Error("GetUserInfo() Expected error") } } func TestModifyExistingOrder(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } _, err := a.ModifyExistingOrder("", 1, 1) if err == nil { - t.Error("Test Failed - GetUserInfo() error") + t.Error("GetUserInfo() Expected error") } } func TestCancelAllExistingOrders(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } err := a.CancelAllExistingOrders("") if err == nil { - t.Error("Test Failed - GetUserInfo() error") + t.Error("GetUserInfo() Expected error") } } func TestGetOrders(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } _, err := a.GetOrders() if err == nil { - t.Error("Test Failed - GetUserInfo() error") + t.Error("GetUserInfo() Expected error") } } func TestGetOrderFee(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - testSetAPIKey(a) - - if !testIsAPIKeysSet(a) { - return + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") } _, err := a.GetOrderFee("", "", 1, 1) if err == nil { - t.Error("Test Failed - GetUserInfo() error") + t.Error("GetUserInfo() Expected error") } } func TestFormatWithdrawPermissions(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() + t.Parallel() expectedResult := exchange.AutoWithdrawCryptoWithAPIPermissionText + " & " + exchange.WithdrawCryptoWith2FAText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := a.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + t.Parallel() + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := a.GetActiveOrders(&getOrdersRequest) - if areTestAPIKeysSet(a) && err != nil { + if areTestAPIKeysSet() && err != nil { t.Errorf("Could not get open orders: %s", err) - } else if !areTestAPIKeysSet(a) && err == nil { + } else if !areTestAPIKeysSet() && err == nil { t.Error("Expecting an error when no keys are set") } } func TestGetOrderHistory(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + t.Parallel() + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := a.GetOrderHistory(&getOrdersRequest) - if areTestAPIKeysSet(a) && err != nil { + if areTestAPIKeysSet() && err != nil { t.Errorf("Could not get order history: %s", err) - } else if !areTestAPIKeysSet(a) && err == nil { + } else if !areTestAPIKeysSet() && err == nil { t.Error("Expecting an error when no keys are set") } } @@ -525,31 +465,30 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- -func areTestAPIKeysSet(a *Alphapoint) bool { - if a.APIKey != "" && a.APIKey != "Key" && - a.APISecret != "" && a.APISecret != "Secret" { - return true - } - return false -} - func TestSubmitOrder(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - - if areTestAPIKeysSet(a) && !canManipulateRealOrders { + t.Parallel() + if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "_", - Base: currency.BTC, - Quote: currency.USD, + + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: "_", + Base: currency.BTC, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := a.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 1, "clientId") - if !areTestAPIKeysSet(a) && err == nil { + + response, err := a.SubmitOrder(orderSubmission) + if !areTestAPIKeysSet() && err == nil { t.Error("Expecting an error when no keys are set") } - if areTestAPIKeysSet(a) && err != nil { + if areTestAPIKeysSet() && err != nil { t.Errorf("Withdraw failed to be placed: %v", err) if !response.IsOrderPlaced { @@ -559,16 +498,13 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - - if areTestAPIKeysSet(a) && !canManipulateRealOrders { + t.Parallel() + if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.BTC, currency.LTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -576,25 +512,22 @@ func TestCancelExchangeOrder(t *testing.T) { } err := a.CancelOrder(orderCancellation) - if !areTestAPIKeysSet(a) && err == nil { + if !areTestAPIKeysSet() && err == nil { t.Error("Expecting an error when no keys are set") } - if areTestAPIKeysSet(a) && err != nil { + if areTestAPIKeysSet() && err != nil { t.Errorf("Withdraw failed to be placed: %v", err) } } func TestCancelAllExchangeOrders(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - - if areTestAPIKeysSet(a) && !canManipulateRealOrders { + t.Parallel() + if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.BTC, currency.LTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -602,40 +535,32 @@ func TestCancelAllExchangeOrders(t *testing.T) { } resp, err := a.CancelAllOrders(orderCancellation) - - if !areTestAPIKeysSet(a) && err == nil { + if !areTestAPIKeysSet() && err == nil { t.Error("Expecting an error when no keys are set") } - if areTestAPIKeysSet(a) && err != nil { + if areTestAPIKeysSet() && err != nil { t.Errorf("Withdraw failed to be placed: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestModifyOrder(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - - _, err := a.ModifyOrder(&exchange.ModifyOrder{}) + t.Parallel() + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := a.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", - AddressTag: "0123456789", - } - + t.Parallel() + var withdrawCryptoRequest = exchange.CryptoWithdrawRequest{} _, err := a.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) if err != common.ErrNotYetImplemented { t.Errorf("Expected 'Not implemented', received %v", err) @@ -643,15 +568,12 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - - if areTestAPIKeysSet(a) && !canManipulateRealOrders { + t.Parallel() + if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := a.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrNotYetImplemented { t.Errorf("Expected '%v', received: '%v'", common.ErrNotYetImplemented, err) @@ -659,15 +581,12 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - a := &Alphapoint{} - a.SetDefaults() - - if areTestAPIKeysSet(a) && !canManipulateRealOrders { + t.Parallel() + if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := a.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrNotYetImplemented { t.Errorf("Expected '%v', received: '%v'", common.ErrNotYetImplemented, err) diff --git a/exchanges/alphapoint/alphapoint_types.go b/exchanges/alphapoint/alphapoint_types.go index 795f0426..081896ca 100644 --- a/exchanges/alphapoint/alphapoint_types.go +++ b/exchanges/alphapoint/alphapoint_types.go @@ -1,6 +1,8 @@ package alphapoint -import exchange "github.com/thrasher-corp/gocryptotrader/exchanges" +import ( + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) // Response contains general responses from the exchange type Response struct { @@ -198,15 +200,15 @@ type WebsocketTicker struct { } // orderSideMap holds order type info based on Alphapoint data -var orderSideMap = map[int64]exchange.OrderSide{ - 1: exchange.BuyOrderSide, - 2: exchange.SellOrderSide, +var orderSideMap = map[int64]order.Side{ + 1: order.Buy, + 2: order.Sell, } // orderTypeMap holds order type info based on Alphapoint data -var orderTypeMap = map[int]exchange.OrderType{ - 1: exchange.MarketOrderType, - 2: exchange.LimitOrderType, - 3: exchange.StopOrderType, - 6: exchange.TrailingStopOrderType, +var orderTypeMap = map[int]order.Type{ + 1: order.Market, + 2: order.Limit, + 3: order.Stop, + 6: order.TrailingStop, } diff --git a/exchanges/alphapoint/alphapoint_websocket.go b/exchanges/alphapoint/alphapoint_websocket.go index dc2c87c5..ebb1329d 100644 --- a/exchanges/alphapoint/alphapoint_websocket.go +++ b/exchanges/alphapoint/alphapoint_websocket.go @@ -1,10 +1,10 @@ package alphapoint import ( + "encoding/json" "net/http" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -17,28 +17,29 @@ func (a *Alphapoint) WebsocketClient() { for a.Enabled { var dialer websocket.Dialer var err error - a.WebsocketConn, _, err = dialer.Dial(a.WebsocketURL, http.Header{}) + a.WebsocketConn, _, err = dialer.Dial(a.API.Endpoints.WebsocketURL, http.Header{}) if err != nil { - log.Errorf("%s Unable to connect to Websocket. Error: %s\n", a.Name, err) + log.Errorf(log.ExchangeSys, "%s Unable to connect to Websocket. Error: %s\n", a.Name, err) continue } if a.Verbose { - log.Debugf("%s Connected to Websocket.\n", a.Name) + log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", a.Name) } err = a.WebsocketConn.WriteMessage(websocket.TextMessage, []byte(`{"messageType": "logon"}`)) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) return } for a.Enabled { msgType, resp, err := a.WebsocketConn.ReadMessage() if err != nil { - log.Error(err) + a.Websocket.ReadMessageErrors <- err + log.Error(log.ExchangeSys, err) break } @@ -48,23 +49,23 @@ func (a *Alphapoint) WebsocketClient() { } msgType := MsgType{} - err := common.JSONDecode(resp, &msgType) + err := json.Unmarshal(resp, &msgType) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } if msgType.MessageType == "Ticker" { ticker := WebsocketTicker{} - err = common.JSONDecode(resp, &ticker) + err = json.Unmarshal(resp, &ticker) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } } } } a.WebsocketConn.Close() - log.Debugf("%s Websocket client disconnected.", a.Name) + log.Debugf(log.ExchangeSys, "%s Websocket client disconnected.", a.Name) } } diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index 80e106f8..4df2c5d5 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -2,23 +2,93 @@ package alphapoint import ( "errors" - "fmt" "strconv" "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) +// GetDefaultConfig returns a default exchange config for Alphapoint +func (a *Alphapoint) GetDefaultConfig() (*config.ExchangeConfig, error) { + return nil, common.ErrFunctionNotSupported +} + +// SetDefaults sets current default settings +func (a *Alphapoint) SetDefaults() { + a.Name = "Alphapoint" + a.Enabled = true + a.Verbose = true + a.API.Endpoints.URL = alphapointDefaultAPIURL + a.API.Endpoints.WebsocketURL = alphapointDefaultWebsocketURL + a.API.CredentialsValidator.RequiresKey = true + a.API.CredentialsValidator.RequiresSecret = true + + a.CurrencyPairs.AssetTypes = asset.Items{ + asset.Spot, + } + + a.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + AccountInfo: true, + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + GetOrders: true, + CancelOrder: true, + CancelOrders: true, + SubmitOrder: true, + ModifyOrder: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + }, + + WebsocketCapabilities: protocol.Features{ + AccountInfo: true, + }, + + WithdrawPermissions: exchange.WithdrawCryptoWith2FA | + exchange.AutoWithdrawCryptoWithAPIPermission | + exchange.NoFiatWithdrawals, + }, + } + + a.Requester = request.New(a.Name, + request.NewRateLimit(time.Minute*10, alphapointAuthRate), + request.NewRateLimit(time.Minute*10, alphapointUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (a *Alphapoint) FetchTradablePairs(asset asset.Item) ([]string, error) { + return nil, common.ErrFunctionNotSupported +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (a *Alphapoint) UpdateTradablePairs(forceUpdate bool) error { + return common.ErrFunctionNotSupported +} + // GetAccountInfo retrieves balances for all enabled currencies on the // Alphapoint exchange func (a *Alphapoint) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = a.GetName() + response.Exchange = a.Name account, err := a.GetAccountInformation() if err != nil { return response, err @@ -42,7 +112,7 @@ func (a *Alphapoint) GetAccountInfo() (exchange.AccountInfo, error) { } // UpdateTicker updates and returns the ticker for a currency pair -func (a *Alphapoint) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (a *Alphapoint) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price tick, err := a.GetTicker(p.String()) if err != nil { @@ -57,7 +127,7 @@ func (a *Alphapoint) UpdateTicker(p currency.Pair, assetType string) (ticker.Pri tickerPrice.Volume = tick.Volume tickerPrice.Last = tick.Last - err = ticker.ProcessTicker(a.GetName(), &tickerPrice, assetType) + err = ticker.ProcessTicker(a.Name, &tickerPrice, assetType) if err != nil { return tickerPrice, err } @@ -65,9 +135,9 @@ func (a *Alphapoint) UpdateTicker(p currency.Pair, assetType string) (ticker.Pri return ticker.GetTicker(a.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (a *Alphapoint) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tick, err := ticker.GetTicker(a.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (a *Alphapoint) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tick, err := ticker.GetTicker(a.Name, p, assetType) if err != nil { return a.UpdateTicker(p, assetType) } @@ -75,7 +145,7 @@ func (a *Alphapoint) GetTickerPrice(p currency.Pair, assetType string) (ticker.P } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (a *Alphapoint) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (a *Alphapoint) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base orderbookNew, err := a.GetOrderbook(p.String()) if err != nil { @@ -83,19 +153,21 @@ func (a *Alphapoint) UpdateOrderbook(p currency.Pair, assetType string) (orderbo } for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, - orderbook.Item{Amount: data.Quantity, Price: data.Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[x].Quantity, + Price: orderbookNew.Bids[x].Price, + }) } for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, - orderbook.Item{Amount: data.Quantity, Price: data.Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[x].Quantity, + Price: orderbookNew.Asks[x].Price, + }) } orderBook.Pair = p - orderBook.ExchangeName = a.GetName() + orderBook.ExchangeName = a.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -106,9 +178,9 @@ func (a *Alphapoint) UpdateOrderbook(p currency.Pair, assetType string) (orderbo return orderbook.Get(a.Name, p, assetType) } -// GetOrderbookEx returns the orderbook for a currency pair -func (a *Alphapoint) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(a.GetName(), p, assetType) +// FetchOrderbook returns the orderbook for a currency pair +func (a *Alphapoint) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(a.Name, p, assetType) if err != nil { return a.UpdateOrderbook(p, assetType) } @@ -118,60 +190,61 @@ func (a *Alphapoint) GetOrderbookEx(p currency.Pair, assetType string) (orderboo // GetFundingHistory returns funding history, deposits and // withdrawals func (a *Alphapoint) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory // https://alphapoint.github.io/slate/#generatetreasuryactivityreport - return fundHistory, common.ErrNotYetImplemented + return nil, common.ErrNotYetImplemented } // GetExchangeHistory returns historic trade data since exchange opening. -func (a *Alphapoint) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (a *Alphapoint) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order and returns a true value when // successfully submitted -func (a *Alphapoint) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - - response, err := a.CreateOrder(p.String(), - side.ToString(), - orderType.ToString(), - amount, price) +func (a *Alphapoint) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } + response, err := a.CreateOrder(s.Pair.String(), + s.OrderSide.String(), + s.OrderSide.String(), + s.Amount, + s.Price) + if err != nil { + return submitOrderResponse, err + } if response > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response) + submitOrderResponse.OrderID = strconv.FormatInt(response, 10) } - - if err == nil { - submitOrderResponse.IsOrderPlaced = true + if s.OrderType == order.Market { + submitOrderResponse.FullyMatched = true } + submitOrderResponse.IsOrderPlaced = true - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (a *Alphapoint) ModifyOrder(_ *exchange.ModifyOrder) (string, error) { +func (a *Alphapoint) ModifyOrder(_ *order.Modify) (string, error) { return "", common.ErrNotYetImplemented } // CancelOrder cancels an order by its corresponding ID number -func (a *Alphapoint) CancelOrder(order *exchange.OrderCancellation) error { +func (a *Alphapoint) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) if err != nil { return err } - _, err = a.CancelExistingOrder(orderIDInt, order.AccountID) - return err } // CancelAllOrders cancels all orders for a given account -func (a *Alphapoint) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - return exchange.CancelAllOrdersResponse{}, +func (a *Alphapoint) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) { + return order.CancelAllResponse{}, a.CancelAllExistingOrders(orderCancellation.AccountID) } @@ -209,18 +282,18 @@ func (a *Alphapoint) GetDepositAddress(cryptocurrency currency.Code, _ string) ( // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (a *Alphapoint) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (a *Alphapoint) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { return "", common.ErrNotYetImplemented } // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted -func (a *Alphapoint) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (a *Alphapoint) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrNotYetImplemented } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted -func (a *Alphapoint) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (a *Alphapoint) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrNotYetImplemented } @@ -236,86 +309,84 @@ func (a *Alphapoint) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, err // GetActiveOrders retrieves any orders that are active/open // This function is not concurrency safe due to orderSide/orderType maps -func (a *Alphapoint) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (a *Alphapoint) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := a.GetOrders() if err != nil { return nil, err } - var orders []exchange.OrderDetail + var orders []order.Detail for x := range resp { - for _, order := range resp[x].OpenOrders { - if order.State != 1 { + for y := range resp[x].OpenOrders { + if resp[x].OpenOrders[y].State != 1 { continue } - orderDetail := exchange.OrderDetail{ - Amount: order.QtyTotal, + orderDetail := order.Detail{ + Amount: resp[x].OpenOrders[y].QtyTotal, Exchange: a.Name, - AccountID: fmt.Sprintf("%v", order.AccountID), - ID: fmt.Sprintf("%v", order.ServerOrderID), - Price: order.Price, - RemainingAmount: order.QtyRemaining, + AccountID: strconv.FormatInt(int64(resp[x].OpenOrders[y].AccountID), 10), + ID: strconv.FormatInt(int64(resp[x].OpenOrders[y].ServerOrderID), 10), + Price: resp[x].OpenOrders[y].Price, + RemainingAmount: resp[x].OpenOrders[y].QtyRemaining, } - orderDetail.OrderSide = orderSideMap[order.Side] - orderDetail.OrderDate = time.Unix(order.ReceiveTime, 0) - orderDetail.OrderType = orderTypeMap[order.OrderType] + orderDetail.OrderSide = orderSideMap[resp[x].OpenOrders[y].Side] + orderDetail.OrderDate = time.Unix(resp[x].OpenOrders[y].ReceiveTime, 0) + orderDetail.OrderType = orderTypeMap[resp[x].OpenOrders[y].OrderType] if orderDetail.OrderType == "" { - orderDetail.OrderType = exchange.UnknownOrderType + orderDetail.OrderType = order.Unknown } orders = append(orders, orderDetail) } } - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status // This function is not concurrency safe due to orderSide/orderType maps -func (a *Alphapoint) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (a *Alphapoint) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := a.GetOrders() if err != nil { return nil, err } - var orders []exchange.OrderDetail + var orders []order.Detail for x := range resp { - for _, order := range resp[x].OpenOrders { - if order.State == 1 { + for y := range resp[x].OpenOrders { + if resp[x].OpenOrders[y].State == 1 { continue } - orderDetail := exchange.OrderDetail{ - Amount: order.QtyTotal, - AccountID: fmt.Sprintf("%v", order.AccountID), + orderDetail := order.Detail{ + Amount: resp[x].OpenOrders[y].QtyTotal, + AccountID: strconv.FormatInt(int64(resp[x].OpenOrders[y].AccountID), 10), Exchange: a.Name, - ID: fmt.Sprintf("%v", order.ServerOrderID), - Price: order.Price, - RemainingAmount: order.QtyRemaining, + ID: strconv.FormatInt(int64(resp[x].OpenOrders[y].ServerOrderID), 10), + Price: resp[x].OpenOrders[y].Price, + RemainingAmount: resp[x].OpenOrders[y].QtyRemaining, } - orderDetail.OrderSide = orderSideMap[order.Side] - orderDetail.OrderDate = time.Unix(order.ReceiveTime, 0) - orderDetail.OrderType = orderTypeMap[order.OrderType] + orderDetail.OrderSide = orderSideMap[resp[x].OpenOrders[y].Side] + orderDetail.OrderDate = time.Unix(resp[x].OpenOrders[y].ReceiveTime, 0) + orderDetail.OrderType = orderTypeMap[resp[x].OpenOrders[y].OrderType] if orderDetail.OrderType == "" { - orderDetail.OrderType = exchange.UnknownOrderType + orderDetail.OrderType = order.Unknown } orders = append(orders, orderDetail) } } - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) return orders, nil } diff --git a/exchanges/anx/README.md b/exchanges/anx/README.md index 93b5d13f..1826110a 100644 --- a/exchanges/anx/README.md +++ b/exchanges/anx/README.md @@ -51,22 +51,22 @@ main.go ```go var a exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "ANX" { - a = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "ANX" { + a = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := a.GetTickerPrice() +tick, err := a.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := a.GetOrderbookEx() +ob, err := a.FetchOrderbook() if err != nil { // Handle error } @@ -141,4 +141,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/anx/anx.go b/exchanges/anx/anx.go index 9b04cb60..09bf290a 100644 --- a/exchanges/anx/anx.go +++ b/exchanges/anx/anx.go @@ -2,19 +2,16 @@ package anx import ( "bytes" + "encoding/json" "errors" "fmt" "net/http" "strconv" "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -46,80 +43,11 @@ type ANX struct { exchange.Base } -// SetDefaults sets current default settings -func (a *ANX) SetDefaults() { - a.Name = "ANX" - a.Enabled = false - a.TakerFee = 0.02 - a.MakerFee = 0.01 - a.Verbose = false - a.RESTPollingDelay = 10 - a.RequestCurrencyPairFormat.Delimiter = "" - a.RequestCurrencyPairFormat.Uppercase = true - a.RequestCurrencyPairFormat.Index = "" - a.ConfigCurrencyPairFormat.Delimiter = "_" - a.ConfigCurrencyPairFormat.Uppercase = true - a.ConfigCurrencyPairFormat.Index = "" - a.APIWithdrawPermissions = exchange.WithdrawCryptoWithEmail | - exchange.AutoWithdrawCryptoWithSetup | - exchange.WithdrawCryptoWith2FA | - exchange.WithdrawFiatViaWebsiteOnly - a.AssetTypes = []string{ticker.Spot} - a.SupportsAutoPairUpdating = true - a.SupportsRESTTickerBatching = false - a.Requester = request.New(a.Name, - request.NewRateLimit(time.Second, anxAuthRate), - request.NewRateLimit(time.Second, anxUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - a.APIUrlDefault = anxAPIURL - a.APIUrl = a.APIUrlDefault - a.Websocket = wshandler.New() -} - -// Setup is run on startup to setup exchange with config values -func (a *ANX) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - a.SetEnabled(false) - } else { - a.Enabled = true - a.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - a.SetAPIKeys(exch.APIKey, exch.APISecret, "", true) - a.SetHTTPClientTimeout(exch.HTTPTimeout) - a.SetHTTPClientUserAgent(exch.HTTPUserAgent) - a.RESTPollingDelay = exch.RESTPollingDelay - a.Verbose = exch.Verbose - a.HTTPDebugging = exch.HTTPDebugging - a.BaseCurrencies = exch.BaseCurrencies - a.AvailablePairs = exch.AvailablePairs - a.EnabledPairs = exch.EnabledPairs - err := a.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = a.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = a.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = a.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = a.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - } -} - // GetCurrencies returns a list of supported currencies (both fiat // and cryptocurrencies) func (a *ANX) GetCurrencies() (CurrenciesStore, error) { var result CurrenciesStaticResponse - path := fmt.Sprintf("%sapi/3/%s", a.APIUrl, anxCurrencies) + path := fmt.Sprintf("%sapi/3/%s", a.API.Endpoints.URL, anxCurrencies) err := a.SendHTTPRequest(path, &result) if err != nil { @@ -132,14 +60,14 @@ func (a *ANX) GetCurrencies() (CurrenciesStore, error) { // GetTicker returns the current ticker func (a *ANX) GetTicker(currency string) (Ticker, error) { var t Ticker - path := fmt.Sprintf("%sapi/2/%s/%s", a.APIUrl, currency, anxTicker) + path := fmt.Sprintf("%sapi/2/%s/%s", a.API.Endpoints.URL, currency, anxTicker) return t, a.SendHTTPRequest(path, &t) } // GetDepth returns current orderbook depth. func (a *ANX) GetDepth(currency string) (Depth, error) { var depth Depth - path := fmt.Sprintf("%sapi/2/%s/%s", a.APIUrl, currency, anxDepth) + path := fmt.Sprintf("%sapi/2/%s/%s", a.API.Endpoints.URL, currency, anxDepth) return depth, a.SendHTTPRequest(path, &depth) } @@ -416,7 +344,7 @@ func (a *ANX) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends a authenticated HTTP request func (a *ANX) SendAuthenticatedHTTPRequest(path string, params map[string]interface{}, result interface{}) error { - if !a.AuthenticatedAPISupport { + if !a.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, a.Name) } @@ -429,23 +357,23 @@ func (a *ANX) SendAuthenticatedHTTPRequest(path string, params map[string]interf req[key] = value } - PayloadJSON, err := common.JSONEncode(req) + PayloadJSON, err := json.Marshal(req) if err != nil { return errors.New("unable to JSON request") } if a.Verbose { - log.Debugf("Request JSON: %s\n", PayloadJSON) + log.Debugf(log.ExchangeSys, "Request JSON: %s\n", PayloadJSON) } - hmac := common.GetHMAC(common.HashSHA512, []byte(path+string("\x00")+string(PayloadJSON)), []byte(a.APISecret)) + hmac := crypto.GetHMAC(crypto.HashSHA512, []byte(path+string("\x00")+string(PayloadJSON)), []byte(a.API.Credentials.Secret)) headers := make(map[string]string) - headers["Rest-Key"] = a.APIKey - headers["Rest-Sign"] = common.Base64Encode(hmac) + headers["Rest-Key"] = a.API.Credentials.Key + headers["Rest-Sign"] = crypto.Base64Encode(hmac) headers["Content-Type"] = "application/json" return a.SendPayload(http.MethodPost, - a.APIUrl+path, + a.API.Endpoints.URL+path, headers, bytes.NewBuffer(PayloadJSON), result, @@ -485,9 +413,9 @@ func (a *ANX) calculateTradingFee(purchasePrice, amount float64, isMaker bool) f var fee float64 if isMaker { - fee = a.MakerFee * amount * purchasePrice + fee = 0.01 * amount * purchasePrice } else { - fee = a.TakerFee * amount * purchasePrice + fee = 0.02 * amount * purchasePrice } return fee @@ -516,7 +444,7 @@ func (a *ANX) GetAccountInformation() (AccountInformation, error) { } if response.ResultCode != "OK" { - log.Errorf("Response code is not OK: %s\n", response.ResultCode) + log.Errorf(log.ExchangeSys, "Response code is not OK: %s\n", response.ResultCode) return response, errors.New(response.ResultCode) } return response, nil @@ -539,7 +467,7 @@ func (a *ANX) CheckAPIWithdrawPermission() (bool, error) { } if !apiAllowsWithdraw { - log.Warn("API key is missing withdrawal permissions") + log.Warn(log.ExchangeSys, "API key is missing withdrawal permissions") } return apiAllowsWithdraw, nil diff --git a/exchanges/anx/anx_live_test.go b/exchanges/anx/anx_live_test.go index 8a442e2d..2618cad8 100644 --- a/exchanges/anx/anx_live_test.go +++ b/exchanges/anx/anx_live_test.go @@ -17,16 +17,22 @@ var mockTests = false func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatalf("ANX Setup() load config error: %s", err) + } anxConfig, err := cfg.GetExchangeConfig("ANX") if err != nil { - log.Fatal("Test Failed - ANX Setup() init error ", err) + log.Fatalf("ANX Setup() init error: %s", err) } - anxConfig.AuthenticatedAPISupport = true - anxConfig.APIKey = apiKey - anxConfig.APISecret = apiSecret + anxConfig.API.AuthenticatedSupport = true + anxConfig.API.Credentials.Key = apiKey + anxConfig.API.Credentials.Secret = apiSecret a.SetDefaults() - a.Setup(&anxConfig) - log.Printf(sharedtestvalues.LiveTesting, a.GetName(), a.APIUrl) + err = a.Setup(anxConfig) + if err != nil { + log.Fatal("ANX setup error", err) + } + log.Printf(sharedtestvalues.LiveTesting, a.Name, a.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/anx/anx_mock_test.go b/exchanges/anx/anx_mock_test.go index 4f92879b..8871ced0 100644 --- a/exchanges/anx/anx_mock_test.go +++ b/exchanges/anx/anx_mock_test.go @@ -20,25 +20,32 @@ var mockTests = true func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("ANX load config error", err) + } anxConfig, err := cfg.GetExchangeConfig("ANX") if err != nil { - log.Fatal("Test Failed - Mock server error", err) + log.Fatal("Mock server error", err) } - anxConfig.AuthenticatedAPISupport = true - anxConfig.APIKey = apiKey - anxConfig.APISecret = apiSecret + a.SkipAuthCheck = true + anxConfig.API.AuthenticatedSupport = true + anxConfig.API.Credentials.Key = apiKey + anxConfig.API.Credentials.Secret = apiSecret a.SetDefaults() - a.Setup(&anxConfig) + err = a.Setup(anxConfig) + if err != nil { + log.Fatal("ANX setup error", err) + } serverDetails, newClient, err := mock.NewVCRServer(mockFile) if err != nil { - log.Fatalf("Test Failed - Mock server error %s", err) + log.Fatalf("Mock server error %s", err) } a.HTTPClient = newClient - a.APIUrl = serverDetails + "/" + a.API.Endpoints.URL = serverDetails + "/" - log.Printf(sharedtestvalues.MockTesting, a.GetName(), a.APIUrl) + log.Printf(sharedtestvalues.MockTesting, a.Name, a.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/anx/anx_test.go b/exchanges/anx/anx_test.go index b5f760a3..c817c725 100644 --- a/exchanges/anx/anx_test.go +++ b/exchanges/anx/anx_test.go @@ -6,6 +6,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // Please supply your own keys here for due diligence testing @@ -21,15 +23,15 @@ func TestGetCurrencies(t *testing.T) { t.Parallel() _, err := a.GetCurrencies() if err != nil { - t.Fatalf("Test failed. TestGetCurrencies failed. Err: %s", err) + t.Fatalf("TestGetCurrencies failed. Err: %s", err) } } func TestGetTradablePairs(t *testing.T) { t.Parallel() - _, err := a.GetTradablePairs() + _, err := a.FetchTradablePairs(asset.Spot) if err != nil { - t.Fatalf("Test failed. TestGetTradablePairs failed. Err: %s", err) + t.Fatalf("TestGetTradablePairs failed. Err: %s", err) } } @@ -37,10 +39,10 @@ func TestGetTicker(t *testing.T) { t.Parallel() ticker, err := a.GetTicker("BTCUSD") if err != nil { - t.Errorf("Test Failed - ANX GetTicker() error: %s", err) + t.Errorf("ANX GetTicker() error: %s", err) } if ticker.Result != "success" { - t.Error("Test Failed - ANX GetTicker() unsuccessful") + t.Error("ANX GetTicker() unsuccessful") } } @@ -48,10 +50,10 @@ func TestGetDepth(t *testing.T) { t.Parallel() depth, err := a.GetDepth("BTCUSD") if err != nil { - t.Errorf("Test Failed - ANX GetDepth() error: %s", err) + t.Errorf("ANX GetDepth() error: %s", err) } if depth.Result != "success" { - t.Error("Test Failed - ANX GetDepth() unsuccessful") + t.Error("ANX GetDepth() unsuccessful") } } @@ -59,13 +61,13 @@ func TestGetAPIKey(t *testing.T) { t.Parallel() apiKey, apiSecret, err := a.GetAPIKey("userName", "passWord", "", "1337") if err == nil { - t.Error("Test Failed - ANX GetAPIKey() Incorrect") + t.Error("ANX GetAPIKey() Expected error") } if apiKey != "" { - t.Error("Test Failed - ANX GetAPIKey() Incorrect") + t.Error("ANX GetAPIKey() Expected error") } if apiSecret != "" { - t.Error("Test Failed - ANX GetAPIKey() Incorrect") + t.Error("ANX GetAPIKey() Expected error") } } @@ -84,7 +86,7 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { t.Parallel() var feeBuilder = setFeeBuilder() a.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -102,7 +104,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee Basic if resp, err := a.GetFee(feeBuilder); resp != float64(0.02) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) } // CryptocurrencyTradeFee High quantity @@ -110,7 +112,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := a.GetFee(feeBuilder); resp != float64(20000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(20000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(20000), resp) t.Error(err) } @@ -118,7 +120,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := a.GetFee(feeBuilder); resp != float64(0.01) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.01), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.01), resp) t.Error(err) } @@ -126,7 +128,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := a.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -134,7 +136,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := a.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -142,7 +144,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := a.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -151,7 +153,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD if resp, err := a.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -160,7 +162,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD if resp, err := a.GetFee(feeBuilder); resp != float64(250.01) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(250.01), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(250.01), resp) t.Error(err) } } @@ -169,9 +171,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { t.Parallel() expectedResult := exchange.AutoWithdrawCryptoWithSetupText + " & " + exchange.WithdrawCryptoWith2FAText + " & " + exchange.WithdrawCryptoWithEmailText + " & " + exchange.WithdrawFiatViaWebsiteOnlyText - withdrawPermissions := a.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } @@ -179,8 +179,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := a.GetActiveOrders(&getOrdersRequest) @@ -196,8 +196,8 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := a.GetOrderHistory(&getOrdersRequest) @@ -207,7 +207,7 @@ func TestGetOrderHistory(t *testing.T) { case !areTestAPIKeysSet() && err == nil && !mockTests: t.Error("Expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - GetBalance() error", err) + t.Error("GetBalance() error", err) } } @@ -215,11 +215,7 @@ func TestGetOrderHistory(t *testing.T) { // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if a.APIKey != "" && a.APIKey != "Key" && - a.APISecret != "" && a.APISecret != "Secret" { - return true - } - return false + return a.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { @@ -227,14 +223,22 @@ func TestSubmitOrder(t *testing.T) { if areTestAPIKeysSet() && !canManipulateRealOrders && !mockTests { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "_", - Base: currency.BTC, - Quote: currency.USD, + + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: "_", + Base: currency.BTC, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - // TODO: QA Pass to submit order - response, err := a.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 1, "") + response, err := a.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) && !mockTests { + // TODO: QA Pass to submit order t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { t.Error("Expecting an error when no keys are set") @@ -248,8 +252,7 @@ func TestCancelExchangeOrder(t *testing.T) { } currencyPair := currency.NewPair(currency.BTC, currency.LTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -274,8 +277,7 @@ func TestCancelAllExchangeOrders(t *testing.T) { } currencyPair := currency.NewPair(currency.BTC, currency.LTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -292,8 +294,8 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("QA pass needs to be completed and mock needs to be updated error cannot be nil") } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } @@ -302,19 +304,19 @@ func TestGetAccountInfo(t *testing.T) { _, err := a.GetAccountInfo() switch { case areTestAPIKeysSet() && err != nil && !mockTests: - t.Error("test failed - GetAccountInfo() error:", err) + t.Error("GetAccountInfo() error:", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("test failed - GetAccountInfo() error") + t.Error("GetAccountInfo() error") case mockTests && err != nil: - t.Error("test failed - GetAccountInfo() error:", err) + t.Error("GetAccountInfo() error:", err) } } func TestModifyOrder(t *testing.T) { t.Parallel() - _, err := a.ModifyOrder(&exchange.ModifyOrder{}) + _, err := a.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } @@ -324,12 +326,14 @@ func TestWithdraw(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", - AddressTag: "0123456789", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", + AddressTag: "0123456789", } _, err := a.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) @@ -342,7 +346,7 @@ func TestWithdraw(t *testing.T) { func TestWithdrawFiat(t *testing.T) { t.Parallel() - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := a.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", @@ -353,7 +357,7 @@ func TestWithdrawFiat(t *testing.T) { func TestWithdrawInternationalBank(t *testing.T) { t.Parallel() - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := a.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", @@ -366,9 +370,9 @@ func TestGetDepositAddress(t *testing.T) { t.Parallel() _, err := a.GetDepositAddress(currency.BTC, "") if areTestAPIKeysSet() && err != nil && !mockTests { - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) } else if !areTestAPIKeysSet() && err == nil { - t.Error("Test Failed - GetDepositAddress() error cannot be nil") + t.Error("GetDepositAddress() error cannot be nil") } } @@ -380,7 +384,7 @@ func TestUpdateOrderbook(t *testing.T) { Quote: currency.USD} _, err := a.UpdateOrderbook(q, "spot") - if err != nil { - t.Fatalf("Update for orderbook failed: %v", err) + if err == nil { + t.Fatalf("error cannot be nil as the endpoint returns no orderbook information") } } diff --git a/exchanges/anx/anx_types.go b/exchanges/anx/anx_types.go index 7c590ee8..cfcee857 100644 --- a/exchanges/anx/anx_types.go +++ b/exchanges/anx/anx_types.go @@ -161,16 +161,16 @@ type TickerComponent struct { type Ticker struct { Result string `json:"result"` Data struct { - High TickerComponent `json:"high"` - Low TickerComponent `json:"low"` - Avg TickerComponent `json:"avg"` - Vwap TickerComponent `json:"vwap"` - Vol TickerComponent `json:"vol"` - Last TickerComponent `json:"last"` - Buy TickerComponent `json:"buy"` - Sell TickerComponent `json:"sell"` - Now string `json:"now"` - UpdateTime string `json:"dataUpdateTime"` + High TickerComponent `json:"high"` + Low TickerComponent `json:"low"` + Average TickerComponent `json:"avg"` + VolumeWeightedAveragePrice TickerComponent `json:"vwap"` + Volume TickerComponent `json:"vol"` + Last TickerComponent `json:"last"` + Buy TickerComponent `json:"buy"` + Sell TickerComponent `json:"sell"` + Now int64 `json:"now,string"` + UpdateTime int64 `json:"dataUpdateTime,string"` } `json:"data"` } diff --git a/exchanges/anx/anx_wrapper.go b/exchanges/anx/anx_wrapper.go index fecea716..5c1184bf 100644 --- a/exchanges/anx/anx_wrapper.go +++ b/exchanges/anx/anx_wrapper.go @@ -1,21 +1,135 @@ package anx import ( - "fmt" "strconv" "strings" "sync" "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config for Alphapoint +func (a *ANX) GetDefaultConfig() (*config.ExchangeConfig, error) { + a.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = a.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = a.BaseCurrencies + + err := a.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if a.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = a.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets current default settings +func (a *ANX) SetDefaults() { + a.Name = "ANX" + a.Enabled = true + a.Verbose = true + a.BaseCurrencies = currency.Currencies{ + currency.USD, + currency.HKD, + currency.EUR, + currency.CAD, + currency.AUD, + currency.SGD, + currency.JPY, + currency.GBP, + currency.NZD, + } + a.API.CredentialsValidator.RequiresKey = true + a.API.CredentialsValidator.RequiresSecret = true + a.API.CredentialsValidator.RequiresBase64DecodeSecret = true + + a.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "_", + Uppercase: true, + }, + } + + a.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: false, + RESTCapabilities: protocol.Features{ + TickerFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + TradeFee: true, + FiatWithdrawalFee: true, + CryptoWithdrawalFee: true, + }, + WithdrawPermissions: exchange.WithdrawCryptoWithEmail | + exchange.AutoWithdrawCryptoWithSetup | + exchange.WithdrawCryptoWith2FA | + exchange.WithdrawFiatViaWebsiteOnly, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: false, + }, + } + + a.Requester = request.New(a.Name, + request.NewRateLimit(time.Second, anxAuthRate), + request.NewRateLimit(time.Second, anxUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + a.API.Endpoints.URLDefault = anxAPIURL + a.API.Endpoints.URL = a.API.Endpoints.URLDefault +} + +// Setup is run on startup to setup exchange with config values +func (a *ANX) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + a.SetEnabled(false) + return nil + } + + return a.SetupDefaults(exch) +} + // Start starts the ANX go routine func (a *ANX) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -28,52 +142,47 @@ func (a *ANX) Start(wg *sync.WaitGroup) { // Run implements the ANX wrapper func (a *ANX) Run() { if a.Verbose { - log.Debugf("%s polling delay: %ds.\n", a.GetName(), a.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", a.GetName(), len(a.EnabledPairs), a.EnabledPairs) + a.PrintEnabledPairs() } - tradablePairs, err := a.GetTradablePairs() - if err != nil { - log.Debugf("%s Failed to get available symbols.\n", a.GetName()) - } else { - forceUpgrade := false - if !common.StringDataContains(a.EnabledPairs.Strings(), "_") || - !common.StringDataContains(a.AvailablePairs.Strings(), "_") { - forceUpgrade = true - } + forceUpdate := false + if !common.StringDataContains(a.GetEnabledPairs(asset.Spot).Strings(), "_") || + !common.StringDataContains(a.GetAvailablePairs(asset.Spot).Strings(), "_") { + enabledPairs := currency.NewPairsFromStrings([]string{"BTC_USD,BTC_HKD,BTC_EUR,BTC_CAD,BTC_AUD,BTC_SGD,BTC_JPY,BTC_GBP,BTC_NZD,LTC_BTC,DOG_EBTC,STR_BTC,XRP_BTC"}) + log.Warn(log.ExchangeSys, + "Enabled pairs for ANX reset due to config upgrade, please enable the ones you would like again.") - if forceUpgrade { - newPairs := []string{"BTC_USD,BTC_HKD,BTC_EUR,BTC_CAD,BTC_AUD,BTC_SGD,BTC_JPY,BTC_GBP,BTC_NZD,LTC_BTC,DOG_EBTC,STR_BTC,XRP_BTC"} - - var enabledPairs currency.Pairs - for _, p := range newPairs { - enabledPairs = append(enabledPairs, - currency.NewPairDelimiter(p, "_")) - } - - log.Warn("Enabled pairs for ANX reset due to config upgrade, please enable the ones you would like again.") - - err = a.UpdateCurrencies(enabledPairs, true, true) - if err != nil { - log.Errorf("%s Failed to get config.\n", a.GetName()) - } - } - - var exchangeProducts currency.Pairs - for _, p := range tradablePairs { - exchangeProducts = append(exchangeProducts, - currency.NewPairDelimiter(p, "_")) - } - - err = a.UpdateCurrencies(exchangeProducts, false, forceUpgrade) + forceUpdate = true + err := a.UpdatePairs(enabledPairs, asset.Spot, true, true) if err != nil { - log.Errorf("%s Failed to get config.\n", a.GetName()) + log.Errorf(log.ExchangeSys, "%s failed to update currencies.\n", a.Name) + return } } + + if !a.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { + return + } + + err := a.UpdateTradablePairs(forceUpdate) + if err != nil { + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", a.Name, err) + } } -// GetTradablePairs returns a list of available -func (a *ANX) GetTradablePairs() ([]string, error) { +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (a *ANX) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := a.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return a.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (a *ANX) FetchTradablePairs(asset asset.Item) ([]string, error) { result, err := a.GetCurrencies() if err != nil { return nil, err @@ -81,77 +190,40 @@ func (a *ANX) GetTradablePairs() ([]string, error) { var currencies []string for x := range result.CurrencyPairs { - currencies = append(currencies, result.CurrencyPairs[x].TradedCcy+"_"+result.CurrencyPairs[x].SettlementCcy) + currencies = append(currencies, result.CurrencyPairs[x].TradedCcy+ + a.GetPairFormat(asset, false).Delimiter+ + result.CurrencyPairs[x].SettlementCcy) } return currencies, nil } // UpdateTicker updates and returns the ticker for a currency pair -func (a *ANX) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (a *ANX) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - tick, err := a.GetTicker(exchange.FormatExchangeCurrency(a.GetName(), p).String()) + tick, err := a.GetTicker(a.FormatExchangeCurrency(p, assetType).String()) if err != nil { return tickerPrice, err } + last, _ := convert.FloatFromString(tick.Data.Last.Value) + high, _ := convert.FloatFromString(tick.Data.High.Value) + low, _ := convert.FloatFromString(tick.Data.Low.Value) + bid, _ := convert.FloatFromString(tick.Data.Buy.Value) + ask, _ := convert.FloatFromString(tick.Data.Sell.Value) + volume, _ := convert.FloatFromString(tick.Data.Volume.Value) - tickerPrice.Pair = p - - if tick.Data.Sell.Value != "" { - tickerPrice.Ask, err = strconv.ParseFloat(tick.Data.Sell.Value, 64) - if err != nil { - return tickerPrice, err - } - } else { - tickerPrice.Ask = 0 + tickerPrice = ticker.Price{ + Last: last, + High: high, + Low: low, + Bid: bid, + Ask: ask, + Volume: volume, + Pair: p, + LastUpdated: time.Unix(0, tick.Data.UpdateTime), } - if tick.Data.Buy.Value != "" { - tickerPrice.Bid, err = strconv.ParseFloat(tick.Data.Buy.Value, 64) - if err != nil { - return tickerPrice, err - } - } else { - tickerPrice.Bid = 0 - } - - if tick.Data.Low.Value != "" { - tickerPrice.Low, err = strconv.ParseFloat(tick.Data.Low.Value, 64) - if err != nil { - return tickerPrice, err - } - } else { - tickerPrice.Low = 0 - } - - if tick.Data.Last.Value != "" { - tickerPrice.Last, err = strconv.ParseFloat(tick.Data.Last.Value, 64) - if err != nil { - return tickerPrice, err - } - } else { - tickerPrice.Last = 0 - } - - if tick.Data.Vol.Value != "" { - tickerPrice.Volume, err = strconv.ParseFloat(tick.Data.Vol.Value, 64) - if err != nil { - return tickerPrice, err - } - } else { - tickerPrice.Volume = 0 - } - - if tick.Data.High.Value != "" { - tickerPrice.High, err = strconv.ParseFloat(tick.Data.High.Value, 64) - if err != nil { - return tickerPrice, err - } - } else { - tickerPrice.High = 0 - } - - err = ticker.ProcessTicker(a.GetName(), &tickerPrice, assetType) + err = ticker.ProcessTicker(a.Name, &tickerPrice, assetType) if err != nil { return tickerPrice, err } @@ -159,18 +231,18 @@ func (a *ANX) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, err return ticker.GetTicker(a.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (a *ANX) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(a.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (a *ANX) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(a.Name, p, assetType) if err != nil { return a.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns the orderbook for a currency pair -func (a *ANX) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(a.GetName(), p, assetType) +// FetchOrderbook returns the orderbook for a currency pair +func (a *ANX) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(a.Name, p, assetType) if err != nil { return a.UpdateOrderbook(p, assetType) } @@ -178,9 +250,9 @@ func (a *ANX) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (a *ANX) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (a *ANX) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := a.GetDepth(exchange.FormatExchangeCurrency(a.GetName(), p).String()) + orderbookNew, err := a.GetDepth(a.FormatExchangeCurrency(p, assetType).String()) if err != nil { return orderBook, err } @@ -200,7 +272,7 @@ func (a *ANX) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base } orderBook.Pair = p - orderBook.ExchangeName = a.GetName() + orderBook.ExchangeName = a.Name orderBook.AssetType = assetType err = orderBook.Process() if err != nil { @@ -229,7 +301,7 @@ func (a *ANX) GetAccountInfo() (exchange.AccountInfo, error) { }) } - info.Exchange = a.GetName() + info.Exchange = a.Name info.Accounts = append(info.Accounts, exchange.Account{ Currencies: balance, }) @@ -240,71 +312,73 @@ func (a *ANX) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (a *ANX) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (a *ANX) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (a *ANX) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (a *ANX) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse +func (a *ANX) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } var isBuying bool var limitPriceInSettlementCurrency float64 - if side == exchange.BuyOrderSide { + if s.OrderSide == order.Buy { isBuying = true } - if orderType == exchange.LimitOrderType { - limitPriceInSettlementCurrency = price + if s.OrderType == order.Limit { + limitPriceInSettlementCurrency = s.Price } - response, err := a.NewOrder(orderType.ToString(), + response, err := a.NewOrder(s.OrderType.String(), isBuying, - p.Base.String(), - amount, - p.Quote.String(), - amount, + s.Pair.Base.String(), + s.Amount, + s.Pair.Quote.String(), + s.Amount, limitPriceInSettlementCurrency, false, "", false) - + if err != nil { + return submitOrderResponse, err + } if response != "" { submitOrderResponse.OrderID = response } - - if err == nil { - submitOrderResponse.IsOrderPlaced = true + if s.OrderType == order.Market { + submitOrderResponse.FullyMatched = true } + submitOrderResponse.IsOrderPlaced = true - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (a *ANX) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (a *ANX) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (a *ANX) CancelOrder(order *exchange.OrderCancellation) error { +func (a *ANX) CancelOrder(order *order.Cancel) error { orderIDs := []string{order.OrderID} _, err := a.CancelOrderByIDs(orderIDs) return err } // CancelAllOrders cancels all orders associated with a currency pair -func (a *ANX) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (a *ANX) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } placedOrders, err := a.GetOrderList(true) if err != nil { @@ -321,9 +395,9 @@ func (a *ANX) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAll return cancelAllOrdersResponse, err } - for _, order := range resp.OrderCancellationResponses { - if order.Error != CancelRequestSubmitted { - cancelAllOrdersResponse.OrderStatus[order.UUID] = order.Error + for i := range resp.OrderCancellationResponses { + if resp.OrderCancellationResponses[i].Error != CancelRequestSubmitted { + cancelAllOrdersResponse.Status[resp.OrderCancellationResponses[i].UUID] = resp.OrderCancellationResponses[i].Error } } @@ -331,8 +405,8 @@ func (a *ANX) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAll } // GetOrderInfo returns information on a current open order -func (a *ANX) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (a *ANX) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -343,20 +417,20 @@ func (a *ANX) GetDepositAddress(cryptocurrency currency.Code, _ string) (string, // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (a *ANX) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { - return a.Send(withdrawRequest.Currency.String(), withdrawRequest.Address, "", fmt.Sprintf("%v", withdrawRequest.Amount)) +func (a *ANX) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { + return a.Send(withdrawRequest.Currency.String(), withdrawRequest.Address, "", strconv.FormatFloat(withdrawRequest.Amount, 'f', -1, 64)) } // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is // submitted -func (a *ANX) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (a *ANX) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { // Fiat withdrawals available via website return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted -func (a *ANX) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (a *ANX) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { // Fiat withdrawals available via website return "", common.ErrFunctionNotSupported } @@ -368,7 +442,7 @@ func (a *ANX) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (a *ANX) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (a.APIKey == "" || a.APISecret == "") && // Todo check connection status + if (!a.AllowAuthenticatedRequest() || a.SkipAuthCheck) && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -376,74 +450,72 @@ func (a *ANX) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { } // GetActiveOrders retrieves any orders that are active/open -func (a *ANX) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (a *ANX) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := a.GetOrderList(true) if err != nil { return nil, err } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range resp { orderDate := time.Unix(resp[i].Timestamp, 0) - orderType := exchange.OrderType(strings.ToUpper(resp[i].OrderType)) + orderType := order.Type(strings.ToUpper(resp[i].OrderType)) - orderDetail := exchange.OrderDetail{ + orderDetail := order.Detail{ Amount: resp[i].TradedCurrencyAmount, CurrencyPair: currency.NewPairWithDelimiter(resp[i].TradedCurrency, - resp[i].SettlementCurrency, a.ConfigCurrencyPairFormat.Delimiter), + resp[i].SettlementCurrency, + a.GetPairFormat(asset.Spot, false).Delimiter), OrderDate: orderDate, Exchange: a.Name, ID: resp[i].OrderID, OrderType: orderType, Price: resp[i].SettlementCurrencyAmount, - Status: resp[i].OrderStatus, + Status: order.Status(resp[i].OrderStatus), } orders = append(orders, orderDetail) } - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, + order.FilterOrdersByType(&orders, getOrdersRequest.OrderType) + order.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (a *ANX) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (a *ANX) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := a.GetOrderList(false) if err != nil { return nil, err } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range resp { orderDate := time.Unix(resp[i].Timestamp, 0) - orderType := exchange.OrderType(strings.ToUpper(resp[i].OrderType)) + orderType := order.Type(strings.ToUpper(resp[i].OrderType)) - orderDetail := exchange.OrderDetail{ + orderDetail := order.Detail{ Amount: resp[i].TradedCurrencyAmount, OrderDate: orderDate, Exchange: a.Name, ID: resp[i].OrderID, OrderType: orderType, Price: resp[i].SettlementCurrencyAmount, - Status: resp[i].OrderStatus, + Status: order.Status(resp[i].OrderStatus), CurrencyPair: currency.NewPairWithDelimiter(resp[i].TradedCurrency, resp[i].SettlementCurrency, - a.ConfigCurrencyPairFormat.Delimiter), + a.GetPairFormat(asset.Spot, false).Delimiter), } orders = append(orders, orderDetail) } - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } diff --git a/exchanges/asset/asset.go b/exchanges/asset/asset.go new file mode 100644 index 00000000..43947812 --- /dev/null +++ b/exchanges/asset/asset.go @@ -0,0 +1,112 @@ +package asset + +import ( + "strings" +) + +// Item stores the asset type +type Item string + +// Items stores a list of assets types +type Items []Item + +// Const vars for asset package +const ( + Spot = Item("spot") + Margin = Item("margin") + Index = Item("index") + Binary = Item("binary") + PerpetualContract = Item("perpetualcontract") + PerpetualSwap = Item("perpetualswap") + Futures = Item("futures") + UpsideProfitContract = Item("upsideprofitcontract") + DownsideProfitContract = Item("downsideprofitcontract") +) + +var supported = Items{ + Spot, + Margin, + Index, + Binary, + PerpetualContract, + PerpetualSwap, + Futures, + UpsideProfitContract, + DownsideProfitContract, +} + +// Supported returns a list of supported asset types +func Supported() Items { + return supported +} + +// returns an Item to string +func (a Item) String() string { + return string(a) +} + +// Strings converts an asset type array to a string array +func (a Items) Strings() []string { + var assets []string + for x := range a { + assets = append(assets, string(a[x])) + } + return assets +} + +// Contains returns whether or not the supplied asset exists +// in the list of Items +func (a Items) Contains(i Item) bool { + if !IsValid(i) { + return false + } + + for x := range a { + if strings.EqualFold(a[x].String(), i.String()) { + return true + } + } + + return false +} + +// JoinToString joins an asset type array and converts it to a string +// with the supplied separator +func (a Items) JoinToString(separator string) string { + return strings.Join(a.Strings(), separator) +} + +// IsValid returns whether or not the supplied asset type is valid or +// not +func IsValid(input Item) bool { + a := Supported() + for x := range a { + if strings.EqualFold(a[x].String(), input.String()) { + return true + } + } + return false +} + +// New takes an input of asset types as string and returns an Items +// array +func New(input string) Items { + if !strings.Contains(input, ",") { + if IsValid(Item(input)) { + return Items{ + Item(input), + } + } + return nil + } + + assets := strings.Split(input, ",") + var result Items + for x := range assets { + if !IsValid(Item(assets[x])) { + return nil + } + result = append(result, Item(assets[x])) + } + return result +} diff --git a/exchanges/asset/asset_test.go b/exchanges/asset/asset_test.go new file mode 100644 index 00000000..e1db02ba --- /dev/null +++ b/exchanges/asset/asset_test.go @@ -0,0 +1,85 @@ +package asset + +import ( + "testing" + + "github.com/thrasher-corp/gocryptotrader/common" +) + +func TestString(t *testing.T) { + a := Spot + if a.String() != "spot" { + t.Fatal("TestString returned an unexpected result") + } +} + +func TestToStringArray(t *testing.T) { + a := Items{Spot, Futures} + result := a.Strings() + for x := range a { + if !common.StringDataCompare(result, a[x].String()) { + t.Fatal("TestToStringArray returned an unexpected result") + } + } +} + +func TestContains(t *testing.T) { + a := Items{Spot, Futures} + if a.Contains("meow") { + t.Fatal("TestContains returned an unexpected result") + } + + if !a.Contains(Spot) { + t.Fatal("TestContains returned an unexpected result") + } + + if a.Contains(Binary) { + t.Fatal("TestContains returned an unexpected result") + } + + if !a.Contains("SpOt") { + t.Error("TestContains returned an unexpected result") + } +} + +func TestJoinToString(t *testing.T) { + a := Items{Spot, Futures} + if a.JoinToString(",") != "spot,futures" { + t.Fatal("TestJoinToString returned an unexpected result") + } +} + +func TestIsValid(t *testing.T) { + if IsValid("rawr") { + t.Fatal("TestIsValid returned an unexpected result") + } + + if !IsValid(Spot) { + t.Fatal("TestIsValid returned an unexpected result") + } +} + +func TestNew(t *testing.T) { + a := New("Spota") + if a != nil { + t.Fatal("TestNew returned an unexpected result") + } + + a = New("SpOt") + if a == nil { + t.Fatal("TestNew returned an unexpected result") + } + + a = New("spot,futures") + if a.JoinToString(",") != "spot,futures" { + t.Fatal("TestNew returned an unexpected result") + } + + if a := New("Spot_rawr"); a != nil { + t.Fatal("TestNew returned an unexpected result") + } + + if a := New("Spot,Rawr"); a != nil { + t.Fatal("TestNew returned an unexpected result") + } +} diff --git a/exchanges/binance/README.md b/exchanges/binance/README.md index f1ba24d9..11625cbd 100644 --- a/exchanges/binance/README.md +++ b/exchanges/binance/README.md @@ -48,22 +48,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Binance" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Binance" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } @@ -138,4 +138,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 0a3e214d..fb0f8a16 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -8,28 +8,19 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) -// Binance is the overarching type across the Bithumb package -type Binance struct { - exchange.Base - WebsocketConn *wshandler.WebsocketConnection - - // Valid string list that is required by the exchange - validLimits []int - validIntervals []TimeInterval -} - const ( apiURL = "https://api.binance.com" @@ -71,129 +62,21 @@ const ( binanceUnauthRate = 0 ) -// SetDefaults sets the basic defaults for Binance -func (b *Binance) SetDefaults() { - b.Name = "Binance" - b.Enabled = false - b.Verbose = false - b.RESTPollingDelay = 10 - b.RequestCurrencyPairFormat.Delimiter = "" - b.RequestCurrencyPairFormat.Uppercase = true - b.ConfigCurrencyPairFormat.Delimiter = "-" - b.ConfigCurrencyPairFormat.Uppercase = true - b.AssetTypes = []string{ticker.Spot} - b.SupportsAutoPairUpdating = true - b.SupportsRESTTickerBatching = true - b.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | - exchange.NoFiatWithdrawals - b.SetValues() - b.Requester = request.New(b.Name, - request.NewRateLimit(time.Second, binanceAuthRate), - request.NewRateLimit(time.Second, binanceUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - b.APIUrlDefault = apiURL - b.APIUrl = b.APIUrlDefault - b.Websocket = wshandler.New() - b.WebsocketURL = binanceDefaultWebsocketURL - b.Websocket.Functionality = wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketTickerSupported | - wshandler.WebsocketKlineSupported | - wshandler.WebsocketOrderbookSupported - b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} +// Binance is the overarching type across the Bithumb package +type Binance struct { + exchange.Base + WebsocketConn *wshandler.WebsocketConnection -// Setup takes in the supplied exchange configuration details and sets params -func (b *Binance) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - b.SetEnabled(false) - } else { - b.Enabled = true - b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - b.SetHTTPClientTimeout(exch.HTTPTimeout) - b.SetHTTPClientUserAgent(exch.HTTPUserAgent) - b.RESTPollingDelay = exch.RESTPollingDelay - b.Verbose = exch.Verbose - b.HTTPDebugging = exch.HTTPDebugging - b.Websocket.SetWsStatusAndConnection(exch.Websocket) - b.BaseCurrencies = exch.BaseCurrencies - b.AvailablePairs = exch.AvailablePairs - b.EnabledPairs = exch.EnabledPairs - err := b.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = b.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = b.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = b.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = b.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = b.Websocket.Setup(b.WSConnect, - nil, - nil, - exch.Name, - exch.Websocket, - exch.Verbose, - binanceDefaultWebsocketURL, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - b.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: b.Name, - URL: b.Websocket.GetWebsocketURL(), - ProxyURL: b.Websocket.GetProxyAddress(), - Verbose: b.Verbose, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - b.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - true, - true, - false, - exch.Name) - } -} - -// GetExchangeValidCurrencyPairs returns the full pair list from the exchange -// at the moment do not integrate with config currency pairs automatically -func (b *Binance) GetExchangeValidCurrencyPairs() ([]string, error) { - var validCurrencyPairs []string - - info, err := b.GetExchangeInfo() - if err != nil { - return nil, err - } - - for i := range info.Symbols { - if info.Symbols[i].Status == "TRADING" { - validCurrencyPairs = append(validCurrencyPairs, info.Symbols[i].BaseAsset+"-"+info.Symbols[i].QuoteAsset) - } - } - return validCurrencyPairs, nil + // Valid string list that is required by the exchange + validLimits []int + validIntervals []TimeInterval } // GetExchangeInfo returns exchange information. Check binance_types for more // information func (b *Binance) GetExchangeInfo() (ExchangeInfo, error) { var resp ExchangeInfo - path := b.APIUrl + exchangeInfo + path := b.API.Endpoints.URL + exchangeInfo return resp, b.SendHTTPRequest(path, &resp) } @@ -204,55 +87,53 @@ func (b *Binance) GetExchangeInfo() (ExchangeInfo, error) { // symbol: string of currency pair // limit: returned limit amount func (b *Binance) GetOrderBook(obd OrderBookDataRequestParams) (OrderBook, error) { - orderbook, resp := OrderBook{}, OrderBookData{} - + var orderbook OrderBook if err := b.CheckLimit(obd.Limit); err != nil { return orderbook, err } - if err := b.CheckSymbol(obd.Symbol); err != nil { - return orderbook, err - } params := url.Values{} - params.Set("symbol", common.StringToUpper(obd.Symbol)) + params.Set("symbol", strings.ToUpper(obd.Symbol)) params.Set("limit", fmt.Sprintf("%d", obd.Limit)) - path := fmt.Sprintf("%s%s?%s", b.APIUrl, orderBookDepth, params.Encode()) - + var resp OrderBookData + path := common.EncodeURLValues(b.API.Endpoints.URL+orderBookDepth, params) if err := b.SendHTTPRequest(path, &resp); err != nil { return orderbook, err } - for _, asks := range resp.Asks { - var ASK struct { - Price float64 - Quantity float64 + for x := range resp.Bids { + price, err := strconv.ParseFloat(resp.Bids[x][0], 64) + if err != nil { + return orderbook, err } - for i, ask := range asks.([]interface{}) { - switch i { - case 0: - ASK.Price, _ = strconv.ParseFloat(ask.(string), 64) - case 1: - ASK.Quantity, _ = strconv.ParseFloat(ask.(string), 64) - orderbook.Asks = append(orderbook.Asks, ASK) - } + + amount, err := strconv.ParseFloat(resp.Bids[x][1], 64) + if err != nil { + return orderbook, err } + + orderbook.Bids = append(orderbook.Bids, OrderbookItem{ + Price: price, + Quantity: amount, + }) } - for _, bids := range resp.Bids { - var BID struct { - Price float64 - Quantity float64 + for x := range resp.Asks { + price, err := strconv.ParseFloat(resp.Asks[x][0], 64) + if err != nil { + return orderbook, err } - for i, bid := range bids.([]interface{}) { - switch i { - case 0: - BID.Price, _ = strconv.ParseFloat(bid.(string), 64) - case 1: - BID.Quantity, _ = strconv.ParseFloat(bid.(string), 64) - orderbook.Bids = append(orderbook.Bids, BID) - } + + amount, err := strconv.ParseFloat(resp.Asks[x][1], 64) + if err != nil { + return orderbook, err } + + orderbook.Asks = append(orderbook.Asks, OrderbookItem{ + Price: price, + Quantity: amount, + }) } orderbook.LastUpdateID = resp.LastUpdateID @@ -265,10 +146,10 @@ func (b *Binance) GetRecentTrades(rtr RecentTradeRequestParams) ([]RecentTrade, var resp []RecentTrade params := url.Values{} - params.Set("symbol", common.StringToUpper(rtr.Symbol)) + params.Set("symbol", strings.ToUpper(rtr.Symbol)) params.Set("limit", fmt.Sprintf("%d", rtr.Limit)) - path := fmt.Sprintf("%s%s?%s", b.APIUrl, recentTrades, params.Encode()) + path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, recentTrades, params.Encode()) return resp, b.SendHTTPRequest(path, &resp) } @@ -295,15 +176,12 @@ func (b *Binance) GetAggregatedTrades(symbol string, limit int) ([]AggregatedTra if err := b.CheckLimit(limit); err != nil { return resp, err } - if err := b.CheckSymbol(symbol); err != nil { - return resp, err - } params := url.Values{} - params.Set("symbol", common.StringToUpper(symbol)) + params.Set("symbol", strings.ToUpper(symbol)) params.Set("limit", strconv.Itoa(limit)) - path := fmt.Sprintf("%s%s?%s", b.APIUrl, aggregatedTrades, params.Encode()) + path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, aggregatedTrades, params.Encode()) return resp, b.SendHTTPRequest(path, &resp) } @@ -333,7 +211,7 @@ func (b *Binance) GetSpotKline(arg KlinesRequestParams) ([]CandleStick, error) { params.Set("endTime", strconv.FormatInt(arg.EndTime, 10)) } - path := fmt.Sprintf("%s%s?%s", b.APIUrl, candleStick, params.Encode()) + path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, candleStick, params.Encode()) if err := b.SendHTTPRequest(path, &resp); err != nil { return kline, err @@ -377,15 +255,10 @@ func (b *Binance) GetSpotKline(arg KlinesRequestParams) ([]CandleStick, error) { // symbol: string of currency pair func (b *Binance) GetAveragePrice(symbol string) (AveragePrice, error) { resp := AveragePrice{} - - if err := b.CheckSymbol(symbol); err != nil { - return resp, err - } - params := url.Values{} - params.Set("symbol", common.StringToUpper(symbol)) + params.Set("symbol", strings.ToUpper(symbol)) - path := fmt.Sprintf("%s%s?%s", b.APIUrl, averagePrice, params.Encode()) + path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, averagePrice, params.Encode()) return resp, b.SendHTTPRequest(path, &resp) } @@ -395,15 +268,10 @@ func (b *Binance) GetAveragePrice(symbol string) (AveragePrice, error) { // symbol: string of currency pair func (b *Binance) GetPriceChangeStats(symbol string) (PriceChangeStats, error) { resp := PriceChangeStats{} - - if err := b.CheckSymbol(symbol); err != nil { - return resp, err - } - params := url.Values{} - params.Set("symbol", common.StringToUpper(symbol)) + params.Set("symbol", strings.ToUpper(symbol)) - path := fmt.Sprintf("%s%s?%s", b.APIUrl, priceChange, params.Encode()) + path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, priceChange, params.Encode()) return resp, b.SendHTTPRequest(path, &resp) } @@ -411,7 +279,7 @@ func (b *Binance) GetPriceChangeStats(symbol string) (PriceChangeStats, error) { // GetTickers returns the ticker data for the last 24 hrs func (b *Binance) GetTickers() ([]PriceChangeStats, error) { var resp []PriceChangeStats - path := fmt.Sprintf("%s%s", b.APIUrl, priceChange) + path := b.API.Endpoints.URL + priceChange return resp, b.SendHTTPRequest(path, &resp) } @@ -420,15 +288,10 @@ func (b *Binance) GetTickers() ([]PriceChangeStats, error) { // symbol: string of currency pair func (b *Binance) GetLatestSpotPrice(symbol string) (SymbolPrice, error) { resp := SymbolPrice{} - - if err := b.CheckSymbol(symbol); err != nil { - return resp, err - } - params := url.Values{} - params.Set("symbol", common.StringToUpper(symbol)) + params.Set("symbol", strings.ToUpper(symbol)) - path := fmt.Sprintf("%s%s?%s", b.APIUrl, symbolPrice, params.Encode()) + path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, symbolPrice, params.Encode()) return resp, b.SendHTTPRequest(path, &resp) } @@ -438,15 +301,10 @@ func (b *Binance) GetLatestSpotPrice(symbol string) (SymbolPrice, error) { // symbol: string of currency pair func (b *Binance) GetBestPrice(symbol string) (BestPrice, error) { resp := BestPrice{} - - if err := b.CheckSymbol(symbol); err != nil { - return resp, err - } - params := url.Values{} - params.Set("symbol", common.StringToUpper(symbol)) + params.Set("symbol", strings.ToUpper(symbol)) - path := fmt.Sprintf("%s%s?%s", b.APIUrl, bestPrice, params.Encode()) + path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, bestPrice, params.Encode()) return resp, b.SendHTTPRequest(path, &resp) } @@ -455,14 +313,14 @@ func (b *Binance) GetBestPrice(symbol string) (BestPrice, error) { func (b *Binance) NewOrder(o *NewOrderRequest) (NewOrderResponse, error) { var resp NewOrderResponse - path := fmt.Sprintf("%s%s", b.APIUrl, newOrder) + path := b.API.Endpoints.URL + newOrder params := url.Values{} params.Set("symbol", o.Symbol) - params.Set("side", string(o.Side)) + params.Set("side", o.Side) params.Set("type", string(o.TradeType)) params.Set("quantity", strconv.FormatFloat(o.Quantity, 'f', -1, 64)) - if o.TradeType == "LIMIT" { + if o.TradeType == BinanceRequestParamsOrderLimit { params.Set("price", strconv.FormatFloat(o.Price, 'f', -1, 64)) } if o.TimeInForce != "" { @@ -499,7 +357,7 @@ func (b *Binance) NewOrder(o *NewOrderRequest) (NewOrderResponse, error) { func (b *Binance) CancelExistingOrder(symbol string, orderID int64, origClientOrderID string) (CancelOrderResponse, error) { var resp CancelOrderResponse - path := fmt.Sprintf("%s%s", b.APIUrl, cancelOrder) + path := b.API.Endpoints.URL + cancelOrder params := url.Values{} params.Set("symbol", symbol) @@ -520,11 +378,13 @@ func (b *Binance) CancelExistingOrder(symbol string, orderID int64, origClientOr // is equal to the number of symbols currently trading on the exchange. func (b *Binance) OpenOrders(symbol string) ([]QueryOrderData, error) { var resp []QueryOrderData - path := fmt.Sprintf("%s%s", b.APIUrl, openOrders) + + path := b.API.Endpoints.URL + openOrders + params := url.Values{} if symbol != "" { - params.Set("symbol", common.StringToUpper(symbol)) + params.Set("symbol", strings.ToUpper(symbol)) } if err := b.SendAuthHTTPRequest(http.MethodGet, path, params, &resp); err != nil { @@ -540,10 +400,10 @@ func (b *Binance) OpenOrders(symbol string) ([]QueryOrderData, error) { func (b *Binance) AllOrders(symbol, orderID, limit string) ([]QueryOrderData, error) { var resp []QueryOrderData - path := fmt.Sprintf("%s%s", b.APIUrl, allOrders) + path := b.API.Endpoints.URL + allOrders params := url.Values{} - params.Set("symbol", common.StringToUpper(symbol)) + params.Set("symbol", strings.ToUpper(symbol)) if orderID != "" { params.Set("orderId", orderID) } @@ -561,10 +421,10 @@ func (b *Binance) AllOrders(symbol, orderID, limit string) ([]QueryOrderData, er func (b *Binance) QueryOrder(symbol, origClientOrderID string, orderID int64) (QueryOrderData, error) { var resp QueryOrderData - path := fmt.Sprintf("%s%s", b.APIUrl, queryOrder) + path := b.API.Endpoints.URL + queryOrder params := url.Values{} - params.Set("symbol", common.StringToUpper(symbol)) + params.Set("symbol", strings.ToUpper(symbol)) if origClientOrderID != "" { params.Set("origClientOrderId", origClientOrderID) } @@ -591,7 +451,7 @@ func (b *Binance) GetAccount() (*Account, error) { var resp response - path := fmt.Sprintf("%s%s", b.APIUrl, accountInfo) + path := b.API.Endpoints.URL + accountInfo params := url.Values{} if err := b.SendAuthHTTPRequest(http.MethodGet, path, params, &resp); err != nil { @@ -621,29 +481,29 @@ func (b *Binance) SendHTTPRequest(path string, result interface{}) error { // SendAuthHTTPRequest sends an authenticated HTTP request func (b *Binance) SendAuthHTTPRequest(method, path string, params url.Values, result interface{}) error { - if !b.AuthenticatedAPISupport { + if !b.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } if params == nil { params = url.Values{} } - params.Set("recvWindow", strconv.FormatInt(common.RecvWindow(5*time.Second), 10)) + params.Set("recvWindow", strconv.FormatInt(convert.RecvWindow(5*time.Second), 10)) params.Set("timestamp", strconv.FormatInt(time.Now().Unix()*1000, 10)) signature := params.Encode() - hmacSigned := common.GetHMAC(common.HashSHA256, []byte(signature), []byte(b.APISecret)) - hmacSignedStr := common.HexEncodeToString(hmacSigned) + hmacSigned := crypto.GetHMAC(crypto.HashSHA256, []byte(signature), []byte(b.API.Credentials.Secret)) + hmacSignedStr := crypto.HexEncodeToString(hmacSigned) headers := make(map[string]string) - headers["X-MBX-APIKEY"] = b.APIKey + headers["X-MBX-APIKEY"] = b.API.Credentials.Key if b.Verbose { - log.Debugf("sent path: %s", path) + log.Debugf(log.ExchangeSys, "sent path: %s", path) } path = common.EncodeURLValues(path, params) - path += fmt.Sprintf("&signature=%s", hmacSignedStr) + path += "&signature=" + hmacSignedStr interim := json.RawMessage{} @@ -666,13 +526,13 @@ func (b *Binance) SendAuthHTTPRequest(method, path string, params url.Values, re return err } - if err := common.JSONDecode(interim, &errCap); err == nil { + if err := json.Unmarshal(interim, &errCap); err == nil { if !errCap.Success && errCap.Message != "" { return errors.New(errCap.Message) } } - return common.JSONDecode(interim, result) + return json.Unmarshal(interim, result) } // CheckLimit checks value against a variable list @@ -686,10 +546,10 @@ func (b *Binance) CheckLimit(limit int) error { } // CheckSymbol checks value against a variable list -func (b *Binance) CheckSymbol(symbol string) error { - enPairs := b.GetAvailableCurrencies() +func (b *Binance) CheckSymbol(symbol string, assetType asset.Item) error { + enPairs := b.GetAvailablePairs(assetType) for x := range enPairs { - if exchange.FormatExchangeCurrency(b.Name, enPairs[x]).String() == symbol { + if b.FormatExchangeCurrency(enPairs[x], assetType).String() == symbol { return nil } } @@ -783,7 +643,7 @@ func getCryptocurrencyWithdrawalFee(c currency.Code) float64 { // WithdrawCrypto sends cryptocurrency to the address of your choosing func (b *Binance) WithdrawCrypto(asset, address, addressTag, name, amount string) (string, error) { var resp WithdrawResponse - path := fmt.Sprintf("%s%s", b.APIUrl, withdraw) + path := b.API.Endpoints.URL + withdraw params := url.Values{} params.Set("asset", asset) @@ -809,7 +669,7 @@ func (b *Binance) WithdrawCrypto(asset, address, addressTag, name, amount string // GetDepositAddressForCurrency retrieves the wallet address for a given currency func (b *Binance) GetDepositAddressForCurrency(currency string) (string, error) { - path := fmt.Sprintf("%s%s", b.APIUrl, depositAddress) + path := b.API.Endpoints.URL + depositAddress resp := struct { Address string `json:"address"` diff --git a/exchanges/binance/binance_live_test.go b/exchanges/binance/binance_live_test.go index afb46716..d9745102 100644 --- a/exchanges/binance/binance_live_test.go +++ b/exchanges/binance/binance_live_test.go @@ -17,16 +17,22 @@ var mockTests = false func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Binance load config error", err) + } binanceConfig, err := cfg.GetExchangeConfig("Binance") if err != nil { - log.Fatal("Test Failed - Binance Setup() init error", err) + log.Fatal("Binance Setup() init error", err) } - binanceConfig.AuthenticatedAPISupport = true - binanceConfig.APIKey = apiKey - binanceConfig.APISecret = apiSecret + binanceConfig.API.AuthenticatedSupport = true + binanceConfig.API.Credentials.Key = apiKey + binanceConfig.API.Credentials.Secret = apiSecret b.SetDefaults() - b.Setup(&binanceConfig) - log.Printf(sharedtestvalues.LiveTesting, b.GetName(), b.APIUrl) + err = b.Setup(binanceConfig) + if err != nil { + log.Fatal("Binance setup error", err) + } + log.Printf(sharedtestvalues.LiveTesting, b.Name, b.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/binance/binance_mock_test.go b/exchanges/binance/binance_mock_test.go index 9c986955..28160f03 100644 --- a/exchanges/binance/binance_mock_test.go +++ b/exchanges/binance/binance_mock_test.go @@ -20,25 +20,32 @@ var mockTests = true func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Binance load config error", err) + } binanceConfig, err := cfg.GetExchangeConfig("Binance") if err != nil { - log.Fatal("Test Failed - Binance Setup() init error", err) + log.Fatal("Binance Setup() init error", err) } - binanceConfig.AuthenticatedAPISupport = true - binanceConfig.APIKey = apiKey - binanceConfig.APISecret = apiSecret + b.SkipAuthCheck = true + binanceConfig.API.AuthenticatedSupport = true + binanceConfig.API.Credentials.Key = apiKey + binanceConfig.API.Credentials.Secret = apiSecret b.SetDefaults() - b.Setup(&binanceConfig) + err = b.Setup(binanceConfig) + if err != nil { + log.Fatal("Binance setup error", err) + } serverDetails, newClient, err := mock.NewVCRServer(mockfile) if err != nil { - log.Fatalf("Test Failed - Mock server error %s", err) + log.Fatalf("Mock server error %s", err) } b.HTTPClient = newClient - b.APIUrl = serverDetails + b.API.Endpoints.URL = serverDetails - log.Printf(sharedtestvalues.MockTesting, b.GetName(), b.APIUrl) + log.Printf(sharedtestvalues.MockTesting, b.Name, b.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index f914f35f..0934d278 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -6,6 +6,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // Please supply your own keys here for due diligence testing @@ -18,11 +20,7 @@ const ( var b Binance func areTestAPIKeysSet() bool { - if b.APIKey != "" && b.APIKey != "Key" && - b.APISecret != "" && b.APISecret != "Secret" { - return true - } - return false + return b.ValidateAPICredentials() } func setFeeBuilder() *exchange.FeeBuilder { @@ -34,12 +32,12 @@ func setFeeBuilder() *exchange.FeeBuilder { } } -func TestGetExchangeValidCurrencyPairs(t *testing.T) { +func TestFetchTradablePairs(t *testing.T) { t.Parallel() - _, err := b.GetExchangeValidCurrencyPairs() + _, err := b.FetchTradablePairs(asset.Spot) if err != nil { - t.Error("Test Failed - Binance GetExchangeValidCurrencyPairs() error", err) + t.Error("Binance FetchTradablePairs(asset asets.AssetType) error", err) } } @@ -52,7 +50,7 @@ func TestGetOrderBook(t *testing.T) { }) if err != nil { - t.Error("Test Failed - Binance GetOrderBook() error", err) + t.Error("Binance GetOrderBook() error", err) } } @@ -65,7 +63,7 @@ func TestGetRecentTrades(t *testing.T) { }) if err != nil { - t.Error("Test Failed - Binance GetRecentTrades() error", err) + t.Error("Binance GetRecentTrades() error", err) } } @@ -74,10 +72,10 @@ func TestGetHistoricalTrades(t *testing.T) { _, err := b.GetHistoricalTrades("BTCUSDT", 5, 0) if !mockTests && err == nil { - t.Error("Test Failed - Binance GetHistoricalTrades() expecting error") + t.Error("Binance GetHistoricalTrades() expecting error") } if mockTests && err == nil { - t.Error("Test Failed - Binance GetHistoricalTrades() error", err) + t.Error("Binance GetHistoricalTrades() error", err) } } @@ -86,7 +84,7 @@ func TestGetAggregatedTrades(t *testing.T) { _, err := b.GetAggregatedTrades("BTCUSDT", 5) if err != nil { - t.Error("Test Failed - Binance GetAggregatedTrades() error", err) + t.Error("Binance GetAggregatedTrades() error", err) } } @@ -99,7 +97,7 @@ func TestGetSpotKline(t *testing.T) { Limit: 24, }) if err != nil { - t.Error("Test Failed - Binance GetSpotKline() error", err) + t.Error("Binance GetSpotKline() error", err) } } @@ -108,7 +106,7 @@ func TestGetAveragePrice(t *testing.T) { _, err := b.GetAveragePrice("BTCUSDT") if err != nil { - t.Error("Test Failed - Binance GetAveragePrice() error", err) + t.Error("Binance GetAveragePrice() error", err) } } @@ -117,7 +115,7 @@ func TestGetPriceChangeStats(t *testing.T) { _, err := b.GetPriceChangeStats("BTCUSDT") if err != nil { - t.Error("Test Failed - Binance GetPriceChangeStats() error", err) + t.Error("Binance GetPriceChangeStats() error", err) } } @@ -126,7 +124,7 @@ func TestGetTickers(t *testing.T) { _, err := b.GetTickers() if err != nil { - t.Error("Test Failed - Binance TestGetTickers error", err) + t.Error("Binance TestGetTickers error", err) } } @@ -135,7 +133,7 @@ func TestGetLatestSpotPrice(t *testing.T) { _, err := b.GetLatestSpotPrice("BTCUSDT") if err != nil { - t.Error("Test Failed - Binance GetLatestSpotPrice() error", err) + t.Error("Binance GetLatestSpotPrice() error", err) } } @@ -144,7 +142,7 @@ func TestGetBestPrice(t *testing.T) { _, err := b.GetBestPrice("BTCUSDT") if err != nil { - t.Error("Test Failed - Binance GetBestPrice() error", err) + t.Error("Binance GetBestPrice() error", err) } } @@ -154,11 +152,11 @@ func TestQueryOrder(t *testing.T) { _, err := b.QueryOrder("BTCUSDT", "", 1337) switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - QueryOrder() error", err) + t.Error("QueryOrder() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - QueryOrder() expecting an error when no keys are set") + t.Error("QueryOrder() expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock QueryOrder() error", err) + t.Error("Mock QueryOrder() error", err) } } @@ -168,11 +166,11 @@ func TestOpenOrders(t *testing.T) { _, err := b.OpenOrders("BTCUSDT") switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - OpenOrders() error", err) + t.Error("OpenOrders() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - OpenOrders() expecting an error when no keys are set") + t.Error("OpenOrders() expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock OpenOrders() error", err) + t.Error("Mock OpenOrders() error", err) } } @@ -182,11 +180,11 @@ func TestAllOrders(t *testing.T) { _, err := b.AllOrders("BTCUSDT", "", "") switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - AllOrders() error", err) + t.Error("AllOrders() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - AllOrders() expecting an error when no keys are set") + t.Error("AllOrders() expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock AllOrders() error", err) + t.Error("Mock AllOrders() error", err) } } @@ -216,7 +214,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee Basic if resp, err := b.GetFee(feeBuilder); resp != float64(0.1) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) } // CryptocurrencyTradeFee High quantity @@ -224,7 +222,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := b.GetFee(feeBuilder); resp != float64(100000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(100000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(100000), resp) t.Error(err) } @@ -232,7 +230,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := b.GetFee(feeBuilder); resp != float64(0.1) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.1), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.1), resp) t.Error(err) } @@ -240,7 +238,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } @@ -249,7 +247,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := b.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) t.Error(err) } @@ -257,7 +255,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -266,7 +264,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -275,7 +273,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } @@ -284,7 +282,6 @@ func TestFormatWithdrawPermissions(t *testing.T) { t.Parallel() expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := b.FormatWithdrawPermissions() if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) @@ -294,8 +291,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) if err == nil { @@ -309,19 +306,19 @@ func TestGetActiveOrders(t *testing.T) { _, err = b.GetActiveOrders(&getOrdersRequest) switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - GetActiveOrders() error", err) + t.Error("GetActiveOrders() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - GetActiveOrders() expecting an error when no keys are set") + t.Error("GetActiveOrders() expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock GetActiveOrders() error", err) + t.Error("Mock GetActiveOrders() error", err) } } func TestGetOrderHistory(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -336,11 +333,11 @@ func TestGetOrderHistory(t *testing.T) { _, err = b.GetOrderHistory(&getOrdersRequest) switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - GetOrderHistory() error", err) + t.Error("GetOrderHistory() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - GetOrderHistory() expecting an error when no keys are set") + t.Error("GetOrderHistory() expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock GetOrderHistory() error", err) + t.Error("Mock GetOrderHistory() error", err) } } @@ -354,19 +351,27 @@ func TestSubmitOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - _, err := b.SubmitOrder(currency.NewPair(currency.LTC, currency.BTC), - exchange.BuyOrderSide, - exchange.MarketOrderType, - 1, - 1, - "clientId") + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: "_", + Base: currency.LTC, + Quote: currency.BTC, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1000000000, + ClientID: "meowOrder", + } + + _, err := b.SubmitOrder(orderSubmission) switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - SubmitOrder() error", err) + t.Error("SubmitOrder() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - SubmitOrder() expecting an error when no keys are set") + t.Error("SubmitOrder() expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock SubmitOrder() error", err) + t.Error("Mock SubmitOrder() error", err) } } @@ -376,8 +381,7 @@ func TestCancelExchangeOrder(t *testing.T) { if areTestAPIKeysSet() && !canManipulateRealOrders && !mockTests { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -387,11 +391,11 @@ func TestCancelExchangeOrder(t *testing.T) { err := b.CancelOrder(orderCancellation) switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - CancelExchangeOrder() error", err) + t.Error("CancelExchangeOrder() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - CancelExchangeOrder() expecting an error when no keys are set") + t.Error("CancelExchangeOrder() expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock CancelExchangeOrder() error", err) + t.Error("Mock CancelExchangeOrder() error", err) } } @@ -401,8 +405,7 @@ func TestCancelAllExchangeOrders(t *testing.T) { if areTestAPIKeysSet() && !canManipulateRealOrders && !mockTests { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -412,11 +415,11 @@ func TestCancelAllExchangeOrders(t *testing.T) { _, err := b.CancelAllOrders(orderCancellation) switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - CancelAllExchangeOrders() error", err) + t.Error("CancelAllExchangeOrders() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - CancelAllExchangeOrders() expecting an error when no keys are set") + t.Error("CancelAllExchangeOrders() expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock CancelAllExchangeOrders() error", err) + t.Error("Mock CancelAllExchangeOrders() error", err) } } @@ -426,20 +429,20 @@ func TestGetAccountInfo(t *testing.T) { _, err := b.GetAccountInfo() switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - GetAccountInfo() error", err) + t.Error("GetAccountInfo() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - GetAccountInfo() expecting an error when no keys are set") + t.Error("GetAccountInfo() expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock GetAccountInfo() error", err) + t.Error("Mock GetAccountInfo() error", err) } } func TestModifyOrder(t *testing.T) { t.Parallel() - _, err := b.ModifyOrder(&exchange.ModifyOrder{}) + _, err := b.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error cannot be nil") + t.Error("ModifyOrder() error cannot be nil") } } @@ -450,28 +453,30 @@ func TestWithdraw(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 0, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: 0, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } _, err := b.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - Withdraw() error", err) + t.Error("Withdraw() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - Withdraw() expecting an error when no keys are set") + t.Error("Withdraw() expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock Withdraw() error", err) + t.Error("Mock Withdraw() error", err) } } func TestWithdrawFiat(t *testing.T) { t.Parallel() - var withdrawFiatRequest exchange.WithdrawRequest + var withdrawFiatRequest exchange.FiatWithdrawRequest _, err := b.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -481,7 +486,7 @@ func TestWithdrawFiat(t *testing.T) { func TestWithdrawInternationalBank(t *testing.T) { t.Parallel() - var withdrawFiatRequest exchange.WithdrawRequest + var withdrawFiatRequest exchange.FiatWithdrawRequest _, err := b.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -494,10 +499,10 @@ func TestGetDepositAddress(t *testing.T) { _, err := b.GetDepositAddress(currency.BTC, "") switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - GetDepositAddress() error cannot be nil") + t.Error("GetDepositAddress() error cannot be nil") case mockTests && err != nil: - t.Error("Test Failed - Mock GetDepositAddress() error", err) + t.Error("Mock GetDepositAddress() error", err) } } diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index ee586ad0..48a90381 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -59,13 +59,19 @@ type OrderBookDataRequestParams struct { Limit int `json:"limit"` // Default 100; max 1000. Valid limits:[5, 10, 20, 50, 100, 500, 1000] } +// OrderbookItem stores an individual orderbook item +type OrderbookItem struct { + Price float64 + Quantity float64 +} + // OrderBookData is resp data from orderbook endpoint type OrderBookData struct { - Code int `json:"code"` - Msg string `json:"msg"` - LastUpdateID int64 `json:"lastUpdateId"` - Bids []interface{} `json:"bids"` - Asks []interface{} `json:"asks"` + Code int `json:"code"` + Msg string `json:"msg"` + LastUpdateID int64 `json:"lastUpdateId"` + Bids [][]string `json:"bids"` + Asks [][]string `json:"asks"` } // OrderBook actual structured data that can be used for orderbook @@ -73,14 +79,8 @@ type OrderBook struct { LastUpdateID int64 Code int Msg string - Bids []struct { - Price float64 - Quantity float64 - } - Asks []struct { - Price float64 - Quantity float64 - } + Bids []OrderbookItem + Asks []OrderbookItem } // DepthUpdateParams is used as an embedded type for WebsocketDepthStream @@ -92,13 +92,13 @@ type DepthUpdateParams []struct { // WebsocketDepthStream is the difference for the update depth stream type WebsocketDepthStream struct { - Event string `json:"e"` - Timestamp int64 `json:"E"` - Pair string `json:"s"` - FirstUpdateID int64 `json:"U"` - LastUpdateID int64 `json:"u"` - UpdateBids []interface{} `json:"b"` - UpdateAsks []interface{} `json:"a"` + Event string `json:"e"` + Timestamp int64 `json:"E"` + Pair string `json:"s"` + FirstUpdateID int64 `json:"U"` + LastUpdateID int64 `json:"u"` + UpdateBids [][]interface{} `json:"b"` + UpdateAsks [][]interface{} `json:"a"` } // RecentTradeRequestParams represents Klines request data. @@ -167,29 +167,29 @@ type KlineStream struct { // TickerStream holds the ticker stream data type TickerStream struct { - EventType string `json:"e"` - EventTime int64 `json:"E"` - Symbol string `json:"s"` - PriceChange string `json:"p"` - PriceChangePercent string `json:"P"` - WeightedAvgPrice string `json:"w"` - PrevDayClose string `json:"x"` - CurrDayClose string `json:"c"` - CloseTradeQuantity string `json:"Q"` - BestBidPrice string `json:"b"` - BestBidQuantity string `json:"B"` - BestAskPrice string `json:"a"` - BestAskQuantity string `json:"A"` - OpenPrice string `json:"o"` - HighPrice string `json:"h"` - LowPrice string `json:"l"` - TotalTradedVolume string `json:"v"` - TotalTradedQuoteVolume string `json:"q"` - OpenTime int64 `json:"O"` - CloseTime int64 `json:"C"` - FirstTradeID int64 `json:"F"` - LastTradeID int64 `json:"L"` - NumberOfTrades int64 `json:"n"` + EventType string `json:"e"` + EventTime int64 `json:"E"` + Symbol string `json:"s"` + PriceChange float64 `json:"p,string"` + PriceChangePercent float64 `json:"P,string"` + WeightedAvgPrice float64 `json:"w,string"` + ClosePrice float64 `json:"x,string"` + LastPrice float64 `json:"c,string"` + LastPriceQuantity float64 `json:"Q,string"` + BestBidPrice float64 `json:"b,string"` + BestBidQuantity float64 `json:"B,string"` + BestAskPrice float64 `json:"a,string"` + BestAskQuantity float64 `json:"A,string"` + OpenPrice float64 `json:"o,string"` + HighPrice float64 `json:"h,string"` + LowPrice float64 `json:"l,string"` + TotalTradedVolume float64 `json:"v,string"` + TotalTradedQuoteVolume float64 `json:"q,string"` + OpenTime int64 `json:"O"` + CloseTime int64 `json:"C"` + FirstTradeID int64 `json:"F"` + LastTradeID int64 `json:"L"` + NumberOfTrades int64 `json:"n"` } // HistoricalTrade holds recent trade data @@ -280,7 +280,7 @@ type NewOrderRequest struct { // Symbol (currency pair to trade) Symbol string // Side Buy or Sell - Side RequestParamsSideType + Side string // TradeType (market or limit order) TradeType RequestParamsOrderType // TimeInForce specifies how long the order remains in effect. @@ -366,17 +366,6 @@ type Account struct { Balances []Balance `json:"balances"` } -// RequestParamsSideType trade order side (buy or sell) -type RequestParamsSideType string - -var ( - // BinanceRequestParamsSideBuy buy order type - BinanceRequestParamsSideBuy = RequestParamsSideType("BUY") - - // BinanceRequestParamsSideSell sell order type - BinanceRequestParamsSideSell = RequestParamsSideType("SELL") -) - // RequestParamsTimeForceType Time in force type RequestParamsTimeForceType string diff --git a/exchanges/binance/binance_websocket.go b/exchanges/binance/binance_websocket.go index 71b5e78a..21d40cd3 100644 --- a/exchanges/binance/binance_websocket.go +++ b/exchanges/binance/binance_websocket.go @@ -1,6 +1,7 @@ package binance import ( + "encoding/json" "errors" "fmt" "net/http" @@ -9,11 +10,9 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" ) @@ -22,8 +21,8 @@ const ( binanceDefaultWebsocketURL = "wss://stream.binance.com:9443" ) -// WSConnect intiates a websocket connection -func (b *Binance) WSConnect() error { +// WsConnect intiates a websocket connection +func (b *Binance) WsConnect() error { if !b.Websocket.IsEnabled() || !b.IsEnabled() { return errors.New(wshandler.WebsocketNotEnabled) } @@ -31,18 +30,19 @@ func (b *Binance) WSConnect() error { var dialer websocket.Dialer var err error + pairs := b.GetEnabledPairs(asset.Spot).Strings() tick := strings.ToLower( strings.Replace( - strings.Join(b.EnabledPairs.Strings(), "@ticker/"), "-", "", -1)) + "@ticker" + strings.Join(pairs, "@ticker/"), "-", "", -1)) + "@ticker" trade := strings.ToLower( strings.Replace( - strings.Join(b.EnabledPairs.Strings(), "@trade/"), "-", "", -1)) + "@trade" + strings.Join(pairs, "@trade/"), "-", "", -1)) + "@trade" kline := strings.ToLower( strings.Replace( - strings.Join(b.EnabledPairs.Strings(), "@kline_1m/"), "-", "", -1)) + "@kline_1m" + strings.Join(pairs, "@kline_1m/"), "-", "", -1)) + "@kline_1m" depth := strings.ToLower( strings.Replace( - strings.Join(b.EnabledPairs.Strings(), "@depth/"), "-", "", -1)) + "@depth" + strings.Join(pairs, "@depth/"), "-", "", -1)) + "@depth" wsurl := b.Websocket.GetWebsocketURL() + "/stream?streams=" + @@ -53,8 +53,9 @@ func (b *Binance) WSConnect() error { kline + "/" + depth - for _, ePair := range b.GetEnabledCurrencies() { - err = b.SeedLocalCache(ePair) + enabledPairs := b.GetEnabledPairs(asset.Spot) + for i := range enabledPairs { + err = b.SeedLocalCache(enabledPairs[i]) if err != nil { return err } @@ -87,12 +88,12 @@ func (b *Binance) WsHandleData() { default: read, err := b.WebsocketConn.ReadMessage() if err != nil { - b.Websocket.DataHandler <- err + b.Websocket.ReadMessageErrors <- err return } b.Websocket.TrafficAlert <- struct{}{} var multiStreamData MultiStreamData - err = common.JSONDecode(read.Raw, &multiStreamData) + err = json.Unmarshal(read.Raw, &multiStreamData) if err != nil { b.Websocket.DataHandler <- fmt.Errorf("%v - Could not load multi stream data: %s", b.Name, @@ -103,7 +104,7 @@ func (b *Binance) WsHandleData() { switch streamType[1] { case "trade": trade := TradeStream{} - err := common.JSONDecode(multiStreamData.Data, &trade) + err := json.Unmarshal(multiStreamData.Data, &trade) if err != nil { b.Websocket.DataHandler <- fmt.Errorf("%v - Could not unmarshal trade data: %s", b.Name, @@ -128,18 +129,19 @@ func (b *Binance) WsHandleData() { } b.Websocket.DataHandler <- wshandler.TradeData{ - CurrencyPair: currency.NewPairFromString(trade.Symbol), - Timestamp: time.Unix(0, trade.TimeStamp), - Price: price, - Amount: amount, - Exchange: b.GetName(), - AssetType: orderbook.Spot, - Side: trade.EventType, + CurrencyPair: currency.NewPairFromFormattedPairs(trade.Symbol, b.GetEnabledPairs(asset.Spot), + b.GetPairFormat(asset.Spot, true)), + Timestamp: time.Unix(0, trade.TimeStamp), + Price: price, + Amount: amount, + Exchange: b.Name, + AssetType: asset.Spot, + Side: trade.EventType, } continue case "ticker": t := TickerStream{} - err := common.JSONDecode(multiStreamData.Data, &t) + err := json.Unmarshal(multiStreamData.Data, &t) if err != nil { b.Websocket.DataHandler <- fmt.Errorf("%v - Could not convert to a TickerStream structure %s", b.Name, @@ -147,24 +149,27 @@ func (b *Binance) WsHandleData() { continue } - var wsTicker wshandler.TickerData - - wsTicker.Timestamp = time.Unix(t.EventTime/1000, 0) - wsTicker.Pair = currency.NewPairFromString(t.Symbol) - wsTicker.AssetType = ticker.Spot - wsTicker.Exchange = b.GetName() - wsTicker.ClosePrice, _ = strconv.ParseFloat(t.CurrDayClose, 64) - wsTicker.Quantity, _ = strconv.ParseFloat(t.TotalTradedVolume, 64) - wsTicker.OpenPrice, _ = strconv.ParseFloat(t.OpenPrice, 64) - wsTicker.HighPrice, _ = strconv.ParseFloat(t.HighPrice, 64) - wsTicker.LowPrice, _ = strconv.ParseFloat(t.LowPrice, 64) - - b.Websocket.DataHandler <- wsTicker + b.Websocket.DataHandler <- wshandler.TickerData{ + Exchange: b.Name, + Open: t.OpenPrice, + Close: t.ClosePrice, + Volume: t.TotalTradedVolume, + QuoteVolume: t.TotalTradedQuoteVolume, + High: t.HighPrice, + Low: t.LowPrice, + Bid: t.BestBidPrice, + Ask: t.BestAskPrice, + Last: t.LastPrice, + Timestamp: time.Unix(0, t.EventTime), + AssetType: asset.Spot, + Pair: currency.NewPairFromFormattedPairs(t.Symbol, b.GetEnabledPairs(asset.Spot), + b.GetPairFormat(asset.Spot, true)), + } continue case "kline": kline := KlineStream{} - err := common.JSONDecode(multiStreamData.Data, &kline) + err := json.Unmarshal(multiStreamData.Data, &kline) if err != nil { b.Websocket.DataHandler <- fmt.Errorf("%v - Could not convert to a KlineStream structure %s", b.Name, @@ -174,9 +179,10 @@ func (b *Binance) WsHandleData() { var wsKline wshandler.KlineData wsKline.Timestamp = time.Unix(0, kline.EventTime) - wsKline.Pair = currency.NewPairFromString(kline.Symbol) - wsKline.AssetType = ticker.Spot - wsKline.Exchange = b.GetName() + wsKline.Pair = currency.NewPairFromFormattedPairs(kline.Symbol, b.GetEnabledPairs(asset.Spot), + b.GetPairFormat(asset.Spot, true)) + wsKline.AssetType = asset.Spot + wsKline.Exchange = b.Name wsKline.StartTime = time.Unix(0, kline.Kline.StartTime) wsKline.CloseTime = time.Unix(0, kline.Kline.CloseTime) wsKline.Interval = kline.Kline.Interval @@ -189,7 +195,7 @@ func (b *Binance) WsHandleData() { continue case "depth": depth := WebsocketDepthStream{} - err := common.JSONDecode(multiStreamData.Data, &depth) + err := json.Unmarshal(multiStreamData.Data, &depth) if err != nil { b.Websocket.DataHandler <- fmt.Errorf("%v - Could not convert to depthStream structure %s", b.Name, @@ -205,11 +211,12 @@ func (b *Binance) WsHandleData() { continue } - currencyPair := currency.NewPairFromString(depth.Pair) + currencyPair := currency.NewPairFromFormattedPairs(depth.Pair, b.GetEnabledPairs(asset.Spot), + b.GetPairFormat(asset.Spot, true)) b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: currencyPair, - Asset: orderbook.Spot, - Exchange: b.GetName(), + Asset: asset.Spot, + Exchange: b.Name, } continue } @@ -220,10 +227,9 @@ func (b *Binance) WsHandleData() { // SeedLocalCache seeds depth data func (b *Binance) SeedLocalCache(p currency.Pair) error { var newOrderBook orderbook.Base - formattedPair := exchange.FormatExchangeCurrency(b.Name, p) orderbookNew, err := b.GetOrderBook( OrderBookDataRequestParams{ - Symbol: formattedPair.String(), + Symbol: b.FormatExchangeCurrency(p, asset.Spot).String(), Limit: 1000, }) if err != nil { @@ -231,56 +237,62 @@ func (b *Binance) SeedLocalCache(p currency.Pair) error { } for i := range orderbookNew.Bids { - newOrderBook.Bids = append(newOrderBook.Bids, - orderbook.Item{Amount: orderbookNew.Bids[i].Quantity, Price: orderbookNew.Bids[i].Price}) + newOrderBook.Bids = append(newOrderBook.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[i].Quantity, + Price: orderbookNew.Bids[i].Price, + }) } for i := range orderbookNew.Asks { - newOrderBook.Asks = append(newOrderBook.Asks, - orderbook.Item{Amount: orderbookNew.Asks[i].Quantity, Price: orderbookNew.Asks[i].Price}) + newOrderBook.Asks = append(newOrderBook.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[i].Quantity, + Price: orderbookNew.Asks[i].Price, + }) } newOrderBook.LastUpdated = time.Unix(orderbookNew.LastUpdateID, 0) - newOrderBook.Pair = currency.NewPairFromString(formattedPair.String()) - newOrderBook.AssetType = ticker.Spot + newOrderBook.Pair = p + newOrderBook.AssetType = asset.Spot + newOrderBook.ExchangeName = b.Name - return b.Websocket.Orderbook.LoadSnapshot(&newOrderBook, false) + return b.Websocket.Orderbook.LoadSnapshot(&newOrderBook) } // UpdateLocalCache updates and returns the most recent iteration of the orderbook func (b *Binance) UpdateLocalCache(wsdp *WebsocketDepthStream) error { var updateBid, updateAsk []orderbook.Item for i := range wsdp.UpdateBids { - var priceToBeUpdated orderbook.Item - for i, bids := range wsdp.UpdateBids[i].([]interface{}) { - switch i { - case 0: - priceToBeUpdated.Price, _ = strconv.ParseFloat(bids.(string), 64) - case 1: - priceToBeUpdated.Amount, _ = strconv.ParseFloat(bids.(string), 64) - } + p, err := strconv.ParseFloat(wsdp.UpdateBids[i][0].(string), 64) + if err != nil { + return err } - updateBid = append(updateBid, priceToBeUpdated) + a, err := strconv.ParseFloat(wsdp.UpdateBids[i][1].(string), 64) + if err != nil { + return err + } + + updateBid = append(updateBid, orderbook.Item{Price: p, Amount: a}) } for i := range wsdp.UpdateAsks { - var priceToBeUpdated orderbook.Item - for i, asks := range wsdp.UpdateAsks[i].([]interface{}) { - switch i { - case 0: - priceToBeUpdated.Price, _ = strconv.ParseFloat(asks.(string), 64) - case 1: - priceToBeUpdated.Amount, _ = strconv.ParseFloat(asks.(string), 64) - } + p, err := strconv.ParseFloat(wsdp.UpdateAsks[i][0].(string), 64) + if err != nil { + return err } - updateAsk = append(updateAsk, priceToBeUpdated) + a, err := strconv.ParseFloat(wsdp.UpdateAsks[i][1].(string), 64) + if err != nil { + return err + } + + updateAsk = append(updateAsk, orderbook.Item{Price: p, Amount: a}) } - currencyPair := currency.NewPairFromString(wsdp.Pair) + currencyPair := currency.NewPairFromFormattedPairs(wsdp.Pair, b.GetEnabledPairs(asset.Spot), + b.GetPairFormat(asset.Spot, true)) return b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ - Bids: updateBid, - Asks: updateAsk, - CurrencyPair: currencyPair, - UpdateID: wsdp.LastUpdateID, - AssetType: orderbook.Spot, + Bids: updateBid, + Asks: updateAsk, + Pair: currencyPair, + UpdateID: wsdp.LastUpdateID, + Asset: asset.Spot, }) } diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index 71a0d748..4f79f15c 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -9,15 +9,168 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) -// Start starts the OKEX go routine +// GetDefaultConfig returns a default exchange config +func (b *Binance) GetDefaultConfig() (*config.ExchangeConfig, error) { + b.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = b.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = b.BaseCurrencies + + err := b.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if b.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = b.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for Binance +func (b *Binance) SetDefaults() { + b.Name = "Binance" + b.Enabled = true + b.Verbose = true + b.API.CredentialsValidator.RequiresKey = true + b.API.CredentialsValidator.RequiresSecret = true + b.SetValues() + + b.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + } + + b.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + KlineFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + TradeFetching: true, + UserTradeHistory: true, + TradeFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TradeFetching: true, + TickerFetching: true, + KlineFetching: true, + OrderbookFetching: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + b.Requester = request.New(b.Name, + request.NewRateLimit(time.Second, binanceAuthRate), + request.NewRateLimit(time.Second, binanceUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + b.API.Endpoints.URLDefault = apiURL + b.API.Endpoints.URL = b.API.Endpoints.URLDefault + b.Websocket = wshandler.New() + b.API.Endpoints.WebsocketURL = binanceDefaultWebsocketURL + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup takes in the supplied exchange configuration details and sets params +func (b *Binance) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + b.SetEnabled(false) + return nil + } + + err := b.SetupDefaults(exch) + if err != nil { + return err + } + + err = b.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: binanceDefaultWebsocketURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: b.WsConnect, + Features: &b.Features.Supports.WebsocketCapabilities, + }) + + if err != nil { + return err + } + + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + b.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + false, + true, + true, + false, + exch.Name) + return nil +} + +// Start starts the Binance go routine func (b *Binance) Start(wg *sync.WaitGroup) { wg.Add(1) go func() { @@ -26,123 +179,160 @@ func (b *Binance) Start(wg *sync.WaitGroup) { }() } -// Run implements the OKEX wrapper +// Run implements the Binance wrapper func (b *Binance) Run() { if b.Verbose { - log.Debugf("%s Websocket: %s. (url: %s).\n%s polling delay: %ds.\n%s %d currencies enabled: %s.\n", - b.GetName(), + log.Debugf(log.ExchangeSys, + "%s Websocket: %s. (url: %s).\n", + b.Name, common.IsEnabled(b.Websocket.IsEnabled()), - b.Websocket.GetWebsocketURL(), - b.GetName(), - b.RESTPollingDelay, - b.GetName(), - len(b.EnabledPairs), - b.EnabledPairs) + b.Websocket.GetWebsocketURL()) + b.PrintEnabledPairs() } - symbols, err := b.GetExchangeValidCurrencyPairs() - if err != nil { - log.Errorf("%s Failed to get exchange info.\n", b.GetName()) - } else { - forceUpgrade := false - if !common.StringDataContains(b.EnabledPairs.Strings(), "-") || - !common.StringDataContains(b.AvailablePairs.Strings(), "-") { - forceUpgrade = true - } + forceUpdate := false + if !common.StringDataContains(b.GetEnabledPairs(asset.Spot).Strings(), b.GetPairFormat(asset.Spot, false).Delimiter) || + !common.StringDataContains(b.GetAvailablePairs(asset.Spot).Strings(), b.GetPairFormat(asset.Spot, false).Delimiter) { + enabledPairs := currency.NewPairsFromStrings([]string{fmt.Sprintf("BTC%vUSDT", b.GetPairFormat(asset.Spot, false).Delimiter)}) + log.Warn(log.ExchangeSys, + "Available pairs for Binance reset due to config upgrade, please enable the ones you would like to use again") + forceUpdate = true - if forceUpgrade { - enabledPairs := currency.Pairs{currency.Pair{ - Base: currency.BTC, - Quote: currency.USDT, - Delimiter: "-", - }} - - log.Warn("Available pairs for Binance reset due to config upgrade, please enable the ones you would like again") - - err = b.UpdateCurrencies(enabledPairs, true, true) - if err != nil { - log.Errorf("%s Failed to get config.\n", b.GetName()) - } - } - - var newSymbols currency.Pairs - for _, p := range symbols { - newSymbols = append(newSymbols, - currency.NewPairFromString(p)) - } - - err = b.UpdateCurrencies(newSymbols, false, forceUpgrade) + err := b.UpdatePairs(enabledPairs, asset.Spot, true, true) if err != nil { - log.Errorf("%s Failed to get config.\n", b.GetName()) + log.Errorf(log.ExchangeSys, + "%s failed to update currencies. Err: %s\n", + b.Name, + err) } } + + if !b.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { + return + } + + err := b.UpdateTradablePairs(forceUpdate) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + b.Name, + err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (b *Binance) FetchTradablePairs(asset asset.Item) ([]string, error) { + var validCurrencyPairs []string + + info, err := b.GetExchangeInfo() + if err != nil { + return nil, err + } + + for x := range info.Symbols { + if info.Symbols[x].Status == "TRADING" { + validCurrencyPairs = append(validCurrencyPairs, info.Symbols[x].BaseAsset+ + b.GetPairFormat(asset, false).Delimiter+ + info.Symbols[x].QuoteAsset) + } + } + return validCurrencyPairs, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (b *Binance) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := b.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return b.UpdatePairs(currency.NewPairsFromStrings(pairs), + asset.Spot, + false, + forceUpdate) } // UpdateTicker updates and returns the ticker for a currency pair -func (b *Binance) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (b *Binance) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price tick, err := b.GetTickers() if err != nil { return tickerPrice, err } - - for _, x := range b.GetEnabledCurrencies() { - curr := exchange.FormatExchangeCurrency(b.Name, x) + pairs := b.GetEnabledPairs(assetType) + for i := range pairs { for y := range tick { - if tick[y].Symbol != curr.String() { + pairFmt := b.FormatExchangeCurrency(pairs[i], assetType).String() + if tick[y].Symbol != pairFmt { continue } - tickerPrice.Pair = x - tickerPrice.Ask = tick[y].AskPrice - tickerPrice.Bid = tick[y].BidPrice - tickerPrice.High = tick[y].HighPrice - tickerPrice.Last = tick[y].LastPrice - tickerPrice.Low = tick[y].LowPrice - tickerPrice.Volume = tick[y].Volume - ticker.ProcessTicker(b.Name, &tickerPrice, assetType) + tickerPrice := ticker.Price{ + Last: tick[y].LastPrice, + High: tick[y].HighPrice, + Low: tick[y].LowPrice, + Bid: tick[y].BidPrice, + Ask: tick[y].AskPrice, + Volume: tick[y].Volume, + QuoteVolume: tick[y].QuoteVolume, + Open: tick[y].OpenPrice, + Close: tick[y].PrevClosePrice, + Pair: pairs[i], + } + err = ticker.ProcessTicker(b.Name, &tickerPrice, assetType) + if err != nil { + log.Error(log.Ticker, err) + } } } return ticker.GetTicker(b.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (b *Binance) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (b *Binance) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(b.Name, p, assetType) if err != nil { return b.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (b *Binance) GetOrderbookEx(currency currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(b.GetName(), currency, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (b *Binance) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(b.Name, p, assetType) if err != nil { - return b.UpdateOrderbook(currency, assetType) + return b.UpdateOrderbook(p, assetType) } return ob, nil } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (b *Binance) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (b *Binance) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := b.GetOrderBook(OrderBookDataRequestParams{Symbol: exchange.FormatExchangeCurrency(b.Name, p).String(), Limit: 1000}) + orderbookNew, err := b.GetOrderBook(OrderBookDataRequestParams{Symbol: b.FormatExchangeCurrency(p, + assetType).String(), Limit: 1000}) if err != nil { return orderBook, err } - for _, bids := range orderbookNew.Bids { + for x := range orderbookNew.Bids { orderBook.Bids = append(orderBook.Bids, - orderbook.Item{Amount: bids.Quantity, Price: bids.Price}) + orderbook.Item{ + Amount: orderbookNew.Bids[x].Quantity, + Price: orderbookNew.Bids[x].Price, + }) } - for _, asks := range orderbookNew.Asks { + for x := range orderbookNew.Asks { orderBook.Asks = append(orderBook.Asks, - orderbook.Item{Amount: asks.Quantity, Price: asks.Price}) + orderbook.Item{ + Amount: orderbookNew.Asks[x].Quantity, + Price: orderbookNew.Asks[x].Price, + }) } orderBook.Pair = p - orderBook.ExchangeName = b.GetName() + orderBook.ExchangeName = b.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -163,25 +353,25 @@ func (b *Binance) GetAccountInfo() (exchange.AccountInfo, error) { } var currencyBalance []exchange.AccountCurrencyInfo - for _, balance := range raw.Balances { - freeCurrency, err := strconv.ParseFloat(balance.Free, 64) + for i := range raw.Balances { + freeCurrency, err := strconv.ParseFloat(raw.Balances[i].Free, 64) if err != nil { return info, err } - lockedCurrency, err := strconv.ParseFloat(balance.Locked, 64) + lockedCurrency, err := strconv.ParseFloat(raw.Balances[i].Locked, 64) if err != nil { return info, err } currencyBalance = append(currencyBalance, exchange.AccountCurrencyInfo{ - CurrencyName: currency.NewCode(balance.Asset), + CurrencyName: currency.NewCode(raw.Balances[i].Asset), TotalValue: freeCurrency + lockedCurrency, Hold: freeCurrency, }) } - info.Exchange = b.GetName() + info.Exchange = b.Name info.Accounts = append(info.Accounts, exchange.Account{ Currencies: currencyBalance, }) @@ -192,32 +382,33 @@ func (b *Binance) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (b *Binance) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (b *Binance) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - return resp, common.ErrNotYetImplemented +func (b *Binance) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (b *Binance) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse +func (b *Binance) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } - var sideType RequestParamsSideType - if side == exchange.BuyOrderSide { - sideType = BinanceRequestParamsSideBuy + var sideType string + if s.OrderSide == order.Buy { + sideType = order.Buy.String() } else { - sideType = BinanceRequestParamsSideSell + sideType = order.Sell.String() } var requestParamsOrderType RequestParamsOrderType - switch orderType { - case exchange.MarketOrderType: + switch s.OrderType { + case order.Market: requestParamsOrderType = BinanceRequestParamsOrderMarket - case exchange.LimitOrderType: + case order.Limit: requestParamsOrderType = BinanceRequestParamsOrderLimit default: submitOrderResponse.IsOrderPlaced = false @@ -225,51 +416,53 @@ func (b *Binance) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderTyp } var orderRequest = NewOrderRequest{ - Symbol: p.Base.String() + p.Quote.String(), + Symbol: s.Pair.Base.String() + s.Pair.Quote.String(), Side: sideType, - Price: price, - Quantity: amount, + Price: s.Price, + Quantity: s.Amount, TradeType: requestParamsOrderType, TimeInForce: BinanceRequestParamsTimeGTC, } response, err := b.NewOrder(&orderRequest) - + if err != nil { + return submitOrderResponse, err + } if response.OrderID > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response.OrderID) + submitOrderResponse.OrderID = strconv.FormatInt(response.OrderID, 10) } - - if err == nil { - submitOrderResponse.IsOrderPlaced = true + if response.ExecutedQty == response.OrigQty { + submitOrderResponse.FullyMatched = true } + submitOrderResponse.IsOrderPlaced = true - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (b *Binance) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (b *Binance) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (b *Binance) CancelOrder(order *exchange.OrderCancellation) error { +func (b *Binance) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) if err != nil { return err } - _, err = b.CancelExistingOrder(exchange.FormatExchangeCurrency(b.Name, order.CurrencyPair).String(), + _, err = b.CancelExistingOrder(b.FormatExchangeCurrency(order.CurrencyPair, + order.AssetType).String(), orderIDInt, order.AccountID) - return err } // CancelAllOrders cancels all orders associated with a currency pair -func (b *Binance) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (b *Binance) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } openOrders, err := b.OpenOrders("") if err != nil { @@ -277,9 +470,11 @@ func (b *Binance) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cance } for i := range openOrders { - _, err = b.CancelExistingOrder(openOrders[i].Symbol, openOrders[i].OrderID, "") + _, err = b.CancelExistingOrder(openOrders[i].Symbol, + openOrders[i].OrderID, + "") if err != nil { - cancelAllOrdersResponse.OrderStatus[strconv.FormatInt(openOrders[i].OrderID, 10)] = err.Error() + cancelAllOrdersResponse.Status[strconv.FormatInt(openOrders[i].OrderID, 10)] = err.Error() } } @@ -287,8 +482,8 @@ func (b *Binance) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cance } // GetOrderInfo returns information on a current open order -func (b *Binance) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (b *Binance) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -299,7 +494,7 @@ func (b *Binance) GetDepositAddress(cryptocurrency currency.Code, _ string) (str // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *Binance) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Binance) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { amountStr := strconv.FormatFloat(withdrawRequest.Amount, 'f', -1, 64) return b.WithdrawCrypto(withdrawRequest.Currency.String(), withdrawRequest.Address, @@ -309,13 +504,13 @@ func (b *Binance) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.Withdraw // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (b *Binance) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Binance) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (b *Binance) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Binance) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -326,7 +521,7 @@ func (b *Binance) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (b *Binance) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (b.APIKey == "" || b.APISecret == "") && // Todo check connection status + if (!b.AllowAuthenticatedRequest() || b.SkipAuthCheck) && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -334,85 +529,87 @@ func (b *Binance) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) } // GetActiveOrders retrieves any orders that are active/open -func (b *Binance) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - if len(getOrdersRequest.Currencies) == 0 { +func (b *Binance) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { + if len(req.Currencies) == 0 { return nil, errors.New("at least one currency is required to fetch order history") } - var orders []exchange.OrderDetail - for _, c := range getOrdersRequest.Currencies { - resp, err := b.OpenOrders(exchange.FormatExchangeCurrency(b.Name, c).String()) + var orders []order.Detail + for x := range req.Currencies { + resp, err := b.OpenOrders(b.FormatExchangeCurrency(req.Currencies[x], + asset.Spot).String()) if err != nil { return nil, err } for i := range resp { - orderSide := exchange.OrderSide(strings.ToUpper(resp[i].Side)) - orderType := exchange.OrderType(strings.ToUpper(resp[i].Type)) + orderSide := order.Side(strings.ToUpper(resp[i].Side)) + orderType := order.Type(strings.ToUpper(resp[i].Type)) orderDate := time.Unix(0, int64(resp[i].Time)*int64(time.Millisecond)) - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ Amount: resp[i].OrigQty, OrderDate: orderDate, Exchange: b.Name, - ID: fmt.Sprintf("%v", resp[i].OrderID), + ID: strconv.FormatInt(resp[i].OrderID, 10), OrderSide: orderSide, OrderType: orderType, Price: resp[i].Price, - Status: resp[i].Status, + Status: order.Status(resp[i].Status), CurrencyPair: currency.NewPairFromString(resp[i].Symbol), }) } } - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (b *Binance) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - if len(getOrdersRequest.Currencies) == 0 { +func (b *Binance) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + if len(req.Currencies) == 0 { return nil, errors.New("at least one currency is required to fetch order history") } - var orders []exchange.OrderDetail - for _, c := range getOrdersRequest.Currencies { - resp, err := b.AllOrders(exchange.FormatExchangeCurrency(b.Name, c).String(), "", "1000") + var orders []order.Detail + for x := range req.Currencies { + resp, err := b.AllOrders(b.FormatExchangeCurrency(req.Currencies[x], + asset.Spot).String(), + "", + "1000") if err != nil { return nil, err } for i := range resp { - orderSide := exchange.OrderSide(strings.ToUpper(resp[i].Side)) - orderType := exchange.OrderType(strings.ToUpper(resp[i].Type)) + orderSide := order.Side(strings.ToUpper(resp[i].Side)) + orderType := order.Type(strings.ToUpper(resp[i].Type)) orderDate := time.Unix(0, int64(resp[i].Time)*int64(time.Millisecond)) // New orders are covered in GetOpenOrders if resp[i].Status == "NEW" { continue } - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ Amount: resp[i].OrigQty, OrderDate: orderDate, Exchange: b.Name, - ID: fmt.Sprintf("%v", resp[i].OrderID), + ID: strconv.FormatInt(resp[i].OrderID, 10), OrderSide: orderSide, OrderType: orderType, Price: resp[i].Price, CurrencyPair: currency.NewPairFromString(resp[i].Symbol), - Status: resp[i].Status, + Status: order.Status(resp[i].Status), }) } } - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) return orders, nil } diff --git a/exchanges/bitfinex/README.md b/exchanges/bitfinex/README.md index 921727a7..616cb503 100644 --- a/exchanges/bitfinex/README.md +++ b/exchanges/bitfinex/README.md @@ -48,22 +48,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bitfinex" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bitfinex" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } @@ -138,4 +138,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index c1a17d92..a79811f3 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -1,6 +1,7 @@ package bitfinex import ( + "encoding/json" "errors" "fmt" "net/http" @@ -9,11 +10,10 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -85,118 +85,15 @@ const ( // depending on some factors (e.g. servers load, endpoint, etc.). type Bitfinex struct { exchange.Base - WebsocketConn *wshandler.WebsocketConnection - WebsocketSubdChannels map[int]WebsocketChanInfo -} - -// SetDefaults sets the basic defaults for bitfinex -func (b *Bitfinex) SetDefaults() { - b.Name = "Bitfinex" - b.Enabled = false - b.Verbose = false - b.RESTPollingDelay = 10 - b.WebsocketSubdChannels = make(map[int]WebsocketChanInfo) - b.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithAPIPermission | - exchange.AutoWithdrawFiatWithAPIPermission - b.RequestCurrencyPairFormat.Delimiter = "" - b.RequestCurrencyPairFormat.Uppercase = true - b.ConfigCurrencyPairFormat.Delimiter = "" - b.ConfigCurrencyPairFormat.Uppercase = true - b.AssetTypes = []string{ticker.Spot} - b.SupportsAutoPairUpdating = true - b.SupportsRESTTickerBatching = true - b.Requester = request.New(b.Name, - request.NewRateLimit(time.Second*60, bitfinexAuthRate), - request.NewRateLimit(time.Second*60, bitfinexUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - b.APIUrlDefault = bitfinexAPIURLBase - b.APIUrl = b.APIUrlDefault - b.Websocket = wshandler.New() - b.Websocket.Functionality = wshandler.WebsocketTickerSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported - b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup takes in the supplied exchange configuration details and sets params -func (b *Bitfinex) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - b.SetEnabled(false) - } else { - b.Enabled = true - b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - b.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - b.SetHTTPClientTimeout(exch.HTTPTimeout) - b.SetHTTPClientUserAgent(exch.HTTPUserAgent) - b.RESTPollingDelay = exch.RESTPollingDelay - b.Verbose = exch.Verbose - b.HTTPDebugging = exch.HTTPDebugging - b.Websocket.SetWsStatusAndConnection(exch.Websocket) - b.BaseCurrencies = exch.BaseCurrencies - b.AvailablePairs = exch.AvailablePairs - b.EnabledPairs = exch.EnabledPairs - err := b.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = b.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = b.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = b.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = b.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = b.Websocket.Setup(b.WsConnect, - b.Subscribe, - b.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - bitfinexWebsocket, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - b.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: b.Name, - URL: b.Websocket.GetWebsocketURL(), - ProxyURL: b.Websocket.GetProxyAddress(), - Verbose: b.Verbose, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - b.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - false, - false, - false, - exch.Name) - } + WebsocketConn *wshandler.WebsocketConnection + AuthenticatedWebsocketConn *wshandler.WebsocketConnection + WebsocketSubdChannels map[int]WebsocketChanInfo } // GetPlatformStatus returns the Bifinex platform status func (b *Bitfinex) GetPlatformStatus() (int, error) { var response []interface{} - path := fmt.Sprintf("%s/v%s/%s", b.APIUrl, bitfinexAPIVersion2, + path := fmt.Sprintf("%s/v%s/%s", b.API.Endpoints.URL, bitfinexAPIVersion2, bitfinexPlatformStatus) err := b.SendHTTPRequest(path, &response, b.Verbose) @@ -225,7 +122,7 @@ func (b *Bitfinex) GetLatestSpotPrice(symbol string) (float64, error) { // GetTicker returns ticker information func (b *Bitfinex) GetTicker(symbol string) (Ticker, error) { response := Ticker{} - path := common.EncodeURLValues(b.APIUrl+bitfinexAPIVersion+bitfinexTicker+symbol, + path := common.EncodeURLValues(b.API.Endpoints.URL+bitfinexAPIVersion+bitfinexTicker+symbol, url.Values{}) if err := b.SendHTTPRequest(path, &response, b.Verbose); err != nil { @@ -245,7 +142,7 @@ func (b *Bitfinex) GetTickerV2(symb string) (Tickerv2, error) { var tick Tickerv2 path := fmt.Sprintf("%s/v%s/%s/%s", - b.APIUrl, + b.API.Endpoints.URL, bitfinexAPIVersion2, bitfinexTickerV2, symb) @@ -292,7 +189,7 @@ func (b *Bitfinex) GetTickersV2(symbols string) ([]Tickersv2, error) { v.Set("symbols", symbols) path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s", - b.APIUrl, + b.API.Endpoints.URL, bitfinexAPIVersion2, bitfinexTickersV2), v) @@ -340,7 +237,7 @@ func (b *Bitfinex) GetTickersV2(symbols string) ([]Tickersv2, error) { // GetStats returns various statistics about the requested pair func (b *Bitfinex) GetStats(symbol string) ([]Stat, error) { var response []Stat - path := fmt.Sprint(b.APIUrl + bitfinexAPIVersion + bitfinexStats + symbol) + path := fmt.Sprint(b.API.Endpoints.URL + bitfinexAPIVersion + bitfinexStats + symbol) return response, b.SendHTTPRequest(path, &response, b.Verbose) } @@ -350,7 +247,7 @@ func (b *Bitfinex) GetStats(symbol string) ([]Stat, error) { // symbol - example "USD" func (b *Bitfinex) GetFundingBook(symbol string) (FundingBook, error) { response := FundingBook{} - path := fmt.Sprint(b.APIUrl + bitfinexAPIVersion + bitfinexLendbook + symbol) + path := fmt.Sprint(b.API.Endpoints.URL + bitfinexAPIVersion + bitfinexLendbook + symbol) if err := b.SendHTTPRequest(path, &response, b.Verbose); err != nil { return response, err @@ -371,7 +268,7 @@ func (b *Bitfinex) GetFundingBook(symbol string) (FundingBook, error) { func (b *Bitfinex) GetOrderbook(currencyPair string, values url.Values) (Orderbook, error) { response := Orderbook{} path := common.EncodeURLValues( - b.APIUrl+bitfinexAPIVersion+bitfinexOrderbook+currencyPair, + b.API.Endpoints.URL+bitfinexAPIVersion+bitfinexOrderbook+currencyPair, values, ) return response, b.SendHTTPRequest(path, &response, b.Verbose) @@ -386,7 +283,7 @@ func (b *Bitfinex) GetOrderbook(currencyPair string, values url.Values) (Orderbo func (b *Bitfinex) GetOrderbookV2(symbol, precision string, values url.Values) (OrderbookV2, error) { var response [][]interface{} var book OrderbookV2 - path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s/%s", b.APIUrl, + path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s/%s", b.API.Endpoints.URL, bitfinexAPIVersion2, bitfinexOrderbookV2, symbol, precision), values) err := b.SendHTTPRequest(path, &response, b.Verbose) if err != nil { @@ -433,7 +330,7 @@ func (b *Bitfinex) GetOrderbookV2(symbol, precision string, values url.Values) ( func (b *Bitfinex) GetTrades(currencyPair string, values url.Values) ([]TradeStructure, error) { var response []TradeStructure path := common.EncodeURLValues( - b.APIUrl+bitfinexAPIVersion+bitfinexTrades+currencyPair, + b.API.Endpoints.URL+bitfinexAPIVersion+bitfinexTrades+currencyPair, values, ) return response, b.SendHTTPRequest(path, &response, b.Verbose) @@ -462,16 +359,16 @@ func (b *Bitfinex) GetTradesV2(currencyPair string, timestampStart, timestampEnd } var tempHistory TradeStructureV2 - for _, data := range resp { - tempHistory.TID = int64(data[0].(float64)) - tempHistory.Timestamp = int64(data[1].(float64)) - tempHistory.Amount = data[2].(float64) - tempHistory.Price = data[3].(float64) + for i := range resp { + tempHistory.TID = int64(resp[i][0].(float64)) + tempHistory.Timestamp = int64(resp[i][1].(float64)) + tempHistory.Amount = resp[i][2].(float64) + tempHistory.Price = resp[i][3].(float64) tempHistory.Exchange = b.Name - tempHistory.Type = "BUY" + tempHistory.Type = order.Buy.String() if tempHistory.Amount < 0 { - tempHistory.Type = "SELL" + tempHistory.Type = order.Sell.String() tempHistory.Amount *= -1 } @@ -498,9 +395,8 @@ func (b *Bitfinex) GetLendbook(symbol string, values url.Values) (Lendbook, erro if len(symbol) == 6 { symbol = symbol[:3] } - path := common.EncodeURLValues(b.APIUrl+bitfinexAPIVersion+bitfinexLendbook+symbol, + path := common.EncodeURLValues(b.API.Endpoints.URL+bitfinexAPIVersion+bitfinexLendbook+symbol, values) - return response, b.SendHTTPRequest(path, &response, b.Verbose) } @@ -510,16 +406,15 @@ func (b *Bitfinex) GetLendbook(symbol string, values url.Values) (Lendbook, erro // Symbol - example "USD" func (b *Bitfinex) GetLends(symbol string, values url.Values) ([]Lends, error) { var response []Lends - path := common.EncodeURLValues(b.APIUrl+bitfinexAPIVersion+bitfinexLends+symbol, + path := common.EncodeURLValues(b.API.Endpoints.URL+bitfinexAPIVersion+bitfinexLends+symbol, values) - return response, b.SendHTTPRequest(path, &response, b.Verbose) } // GetSymbols returns the available currency pairs on the exchange func (b *Bitfinex) GetSymbols() ([]string, error) { var products []string - path := fmt.Sprint(b.APIUrl + bitfinexAPIVersion + bitfinexSymbols) + path := fmt.Sprint(b.API.Endpoints.URL + bitfinexAPIVersion + bitfinexSymbols) return products, b.SendHTTPRequest(path, &products, b.Verbose) } @@ -527,7 +422,7 @@ func (b *Bitfinex) GetSymbols() ([]string, error) { // GetSymbolsDetails a list of valid symbol IDs and the pair details func (b *Bitfinex) GetSymbolsDetails() ([]SymbolDetails, error) { var response []SymbolDetails - path := fmt.Sprint(b.APIUrl + bitfinexAPIVersion + bitfinexSymbolsDetails) + path := fmt.Sprint(b.API.Endpoints.URL + bitfinexAPIVersion + bitfinexSymbolsDetails) return response, b.SendHTTPRequest(path, &response, b.Verbose) } @@ -646,7 +541,7 @@ func (b *Bitfinex) WithdrawCryptocurrency(withdrawType, wallet, address, payment } // WithdrawFIAT Sends an authenticated request to withdraw FIAT currency -func (b *Bitfinex) WithdrawFIAT(withdrawalType, walletType string, withdrawRequest *exchange.WithdrawRequest) ([]Withdrawal, error) { +func (b *Bitfinex) WithdrawFIAT(withdrawalType, walletType string, withdrawRequest *exchange.FiatWithdrawRequest) ([]Withdrawal, error) { var response []Withdrawal req := make(map[string]interface{}) @@ -654,7 +549,7 @@ func (b *Bitfinex) WithdrawFIAT(withdrawalType, walletType string, withdrawReque req["walletselected"] = walletType req["amount"] = strconv.FormatFloat(withdrawRequest.Amount, 'f', -1, 64) req["account_name"] = withdrawRequest.BankAccountName - req["account_number"] = strconv.FormatFloat(withdrawRequest.BankAccountNumber, 'f', -1, 64) + req["account_number"] = withdrawRequest.BankAccountNumber req["bank_name"] = withdrawRequest.BankName req["bank_address"] = withdrawRequest.BankAddress req["bank_city"] = withdrawRequest.BankCity @@ -690,9 +585,9 @@ func (b *Bitfinex) NewOrder(currencyPair string, amount, price float64, buy bool req["is_hidden"] = hidden if buy { - req["side"] = "buy" + req["side"] = order.Buy.Lower() } else { - req["side"] = "sell" + req["side"] = order.Sell.Lower() } return response, @@ -765,9 +660,9 @@ func (b *Bitfinex) ReplaceOrder(orderID int64, symbol string, amount, price floa req["is_hidden"] = hidden if buy { - req["side"] = "buy" + req["side"] = order.Buy.Lower() } else { - req["side"] = "sell" + req["side"] = order.Sell.Lower() } return response, @@ -1045,7 +940,7 @@ func (b *Bitfinex) SendHTTPRequest(path string, result interface{}, verbose bool // SendAuthenticatedHTTPRequest sends an autheticated http request and json // unmarshals result to a supplied variable func (b *Bitfinex) SendAuthenticatedHTTPRequest(method, path string, params map[string]interface{}, result interface{}) error { - if !b.AuthenticatedAPISupport { + if !b.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } @@ -1053,32 +948,32 @@ func (b *Bitfinex) SendAuthenticatedHTTPRequest(method, path string, params map[ n := b.Requester.GetNonce(true) req := make(map[string]interface{}) - req["request"] = fmt.Sprintf("%s%s", bitfinexAPIVersion, path) + req["request"] = bitfinexAPIVersion + path req["nonce"] = n.String() for key, value := range params { req[key] = value } - PayloadJSON, err := common.JSONEncode(req) + PayloadJSON, err := json.Marshal(req) if err != nil { return errors.New("sendAuthenticatedAPIRequest: unable to JSON request") } if b.Verbose { - log.Debugf("Request JSON: %s\n", PayloadJSON) + log.Debugf(log.ExchangeSys, "Request JSON: %s\n", PayloadJSON) } - PayloadBase64 := common.Base64Encode(PayloadJSON) - hmac := common.GetHMAC(common.HashSHA512_384, []byte(PayloadBase64), - []byte(b.APISecret)) + PayloadBase64 := crypto.Base64Encode(PayloadJSON) + hmac := crypto.GetHMAC(crypto.HashSHA512_384, []byte(PayloadBase64), + []byte(b.API.Credentials.Secret)) headers := make(map[string]string) - headers["X-BFX-APIKEY"] = b.APIKey + headers["X-BFX-APIKEY"] = b.API.Credentials.Key headers["X-BFX-PAYLOAD"] = PayloadBase64 - headers["X-BFX-SIGNATURE"] = common.HexEncodeToString(hmac) + headers["X-BFX-SIGNATURE"] = crypto.HexEncodeToString(hmac) return b.SendPayload(method, - b.APIUrl+bitfinexAPIVersion+path, + b.API.Endpoints.URL+bitfinexAPIVersion+path, headers, nil, result, diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index a4e7652e..ce29001d 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -1,8 +1,10 @@ package bitfinex import ( + "log" "net/http" "net/url" + "os" "reflect" "testing" "time" @@ -12,6 +14,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -24,33 +27,58 @@ const ( ) var b Bitfinex +var wsAuthExecuted bool -func TestSetup(t *testing.T) { +func TestMain(m *testing.M) { b.SetDefaults() cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Bitfinex load config error", err) + } bfxConfig, err := cfg.GetExchangeConfig("Bitfinex") if err != nil { - t.Error("Test Failed - Bitfinex Setup() init error") + log.Fatal("Bitfinex Setup() init error") } - b.Setup(&bfxConfig) - b.APIKey = apiKey - b.APISecret = apiSecret - if !b.Enabled || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) || - b.Verbose || b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 || - len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 { - t.Error("Test Failed - Bitfinex Setup values not set correctly") + err = b.Setup(bfxConfig) + if err != nil { + log.Fatal("Bitfinex setup error", err) } - b.AuthenticatedWebsocketAPISupport = true - b.AuthenticatedAPISupport = true + b.API.Credentials.Key = apiKey + b.API.Credentials.Secret = apiSecret + if !b.Enabled || b.API.AuthenticatedSupport || + b.Verbose || b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 { + log.Fatal("Bitfinex Setup values not set correctly") + } + + if areTestAPIKeysSet() { + b.API.AuthenticatedSupport = true + b.API.AuthenticatedWebsocketSupport = true + } + // custom rate limit for testing b.Requester.SetRateLimit(true, time.Millisecond*300, 1) b.Requester.SetRateLimit(false, time.Millisecond*300, 1) + os.Exit(m.Run()) +} + +func TestAppendOptionalDelimiter(t *testing.T) { + t.Parallel() + curr1 := currency.NewPairFromString("BTCUSD") + b.appendOptionalDelimiter(&curr1) + if curr1.Delimiter != "" { + t.Errorf("Expected no delimiter, received %v", curr1.Delimiter) + } + curr2 := currency.NewPairFromString("DUSK:USD") + curr2.Delimiter = "" + b.appendOptionalDelimiter(&curr2) + if curr2.Delimiter != ":" { + t.Errorf("Expected \"-\" as a delimiter, received %v", curr2.Delimiter) + } } func TestGetPlatformStatus(t *testing.T) { t.Parallel() - result, err := b.GetPlatformStatus() if err != nil { t.Errorf("TestGetPlatformStatus error: %s", err) @@ -78,7 +106,7 @@ func TestGetTicker(t *testing.T) { _, err = b.GetTicker("wigwham") if err == nil { - t.Error("Test Failed - GetTicker() error") + t.Error("GetTicker() Expected error") } } @@ -112,7 +140,7 @@ func TestGetStats(t *testing.T) { _, err = b.GetStats("wigwham") if err == nil { - t.Error("Test Failed - GetStats() error") + t.Error("GetStats() Expected error") } } @@ -124,7 +152,7 @@ func TestGetFundingBook(t *testing.T) { } _, err = b.GetFundingBook("wigwham") if err == nil { - t.Error("Testing Failed - GetFundingBook() error") + t.Error("Testing Failed - GetFundingBook() Expected error") } } @@ -250,108 +278,113 @@ func TestGetAccountInfo(t *testing.T) { _, err := b.GetAccountInfo() if err != nil { - t.Error("Test Failed - GetAccountInfo error", err) + t.Error("GetAccountInfo error", err) } } func TestGetAccountFees(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetAccountFees() if err == nil { - t.Error("Test Failed - GetAccountFees error") + t.Error("GetAccountFees Expected error") } } func TestGetAccountSummary(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetAccountSummary() if err == nil { - t.Error("Test Failed - GetAccountSummary() error:") + t.Error("GetAccountSummary() Expected error") } } func TestNewDeposit(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.NewDeposit("blabla", "testwallet", 1) if err == nil { - t.Error("Test Failed - NewDeposit() error:", err) + t.Error("NewDeposit() Expected error") } } func TestGetKeyPermissions(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetKeyPermissions() if err == nil { - t.Error("Test Failed - GetKeyPermissions() error:") + t.Error("GetKeyPermissions() Expected error") } } func TestGetMarginInfo(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetMarginInfo() if err == nil { - t.Error("Test Failed - GetMarginInfo() error") + t.Error("GetMarginInfo() Expected error") } } func TestGetAccountBalance(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetAccountBalance() if err == nil { - t.Error("Test Failed - GetAccountBalance() error") + t.Error("GetAccountBalance() Expected error") } } func TestWalletTransfer(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.WalletTransfer(0.01, "bla", "bla", "bla") if err == nil { - t.Error("Test Failed - WalletTransfer() error") + t.Error("WalletTransfer() Expected error") } } func TestNewOrder(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() - _, err := b.NewOrder("BTCUSD", 1, 2, true, "market", false) + _, err := b.NewOrder("BTCUSD", + 1, + 2, + true, + order.Limit.Lower(), + false) if err == nil { - t.Error("Test Failed - NewOrder() error") + t.Error("NewOrder() Expected error") } } func TestNewOrderMulti(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() @@ -362,254 +395,255 @@ func TestNewOrderMulti(t *testing.T) { Amount: 1, Price: 1, Exchange: "bitfinex", - Side: "buy", - Type: "market", + Side: order.Buy.Lower(), + Type: order.Limit.Lower(), }, } _, err := b.NewOrderMulti(newOrder) if err == nil { - t.Error("Test Failed - NewOrderMulti() error") + t.Error("NewOrderMulti() Expected error") } } -func TestCancelExistingOrder(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { +func TestCancelOrder(t *testing.T) { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.CancelExistingOrder(1337) if err == nil { - t.Error("Test Failed - CancelExistingOrder() error") + t.Error("CancelExistingOrder() Expected error") } } func TestCancelMultipleOrders(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.CancelMultipleOrders([]int64{1337, 1336}) if err == nil { - t.Error("Test Failed - CancelMultipleOrders() error") + t.Error("CancelMultipleOrders() Expected error") } } -func TestCancelAllExistingOrders(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { +func TestCancelAllOrders(t *testing.T) { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.CancelAllExistingOrders() if err == nil { - t.Error("Test Failed - CancelAllExistingOrders() error") + t.Error("CancelAllExistingOrders() Expected error") } } func TestReplaceOrder(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() - _, err := b.ReplaceOrder(1337, "BTCUSD", 1, 1, true, "market", false) + _, err := b.ReplaceOrder(1337, "BTCUSD", + 1, 1, true, order.Limit.Lower(), false) if err == nil { - t.Error("Test Failed - ReplaceOrder() error") + t.Error("ReplaceOrder() Expected error") } } func TestGetOrderStatus(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetOrderStatus(1337) if err == nil { - t.Error("Test Failed - GetOrderStatus() error") + t.Error("GetOrderStatus() Expected error") } } func TestGetOpenOrders(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetOpenOrders() if err == nil { - t.Error("Test Failed - GetOpenOrders() error") + t.Error("GetOpenOrders() Expectederror") } } func TestGetActivePositions(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetActivePositions() if err == nil { - t.Error("Test Failed - GetActivePositions() error") + t.Error("GetActivePositions() Expected error") } } func TestClaimPosition(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.ClaimPosition(1337) if err == nil { - t.Error("Test Failed - ClaimPosition() error") + t.Error("ClaimPosition() Expected error") } } func TestGetBalanceHistory(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetBalanceHistory("USD", time.Time{}, time.Time{}, 1, "deposit") if err == nil { - t.Error("Test Failed - GetBalanceHistory() error") + t.Error("GetBalanceHistory() Expected error") } } func TestGetMovementHistory(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetMovementHistory("USD", "bitcoin", time.Time{}, time.Time{}, 1) if err == nil { - t.Error("Test Failed - GetMovementHistory() error") + t.Error("GetMovementHistory() Expected error") } } func TestGetTradeHistory(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetTradeHistory("BTCUSD", time.Time{}, time.Time{}, 1, 0) if err == nil { - t.Error("Test Failed - GetTradeHistory() error") + t.Error("GetTradeHistory() Expected error") } } func TestNewOffer(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.NewOffer("BTC", 1, 1, 1, "loan") if err == nil { - t.Error("Test Failed - NewOffer() error") + t.Error("NewOffer() Expected error") } } func TestCancelOffer(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.CancelOffer(1337) if err == nil { - t.Error("Test Failed - CancelOffer() error") + t.Error("CancelOffer() Expected error") } } func TestGetOfferStatus(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetOfferStatus(1337) if err == nil { - t.Error("Test Failed - NewOffer() error") + t.Error("NewOffer() Expected error") } } func TestGetActiveCredits(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetActiveCredits() if err == nil { - t.Error("Test Failed - GetActiveCredits() error", err) + t.Error("GetActiveCredits() Expected error") } } func TestGetActiveOffers(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetActiveOffers() if err == nil { - t.Error("Test Failed - GetActiveOffers() error", err) + t.Error("GetActiveOffers() Expected error") } } func TestGetActiveMarginFunding(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetActiveMarginFunding() if err == nil { - t.Error("Test Failed - GetActiveMarginFunding() error", err) + t.Error("GetActiveMarginFunding() Expected error") } } func TestGetUnusedMarginFunds(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetUnusedMarginFunds() if err == nil { - t.Error("Test Failed - GetUnusedMarginFunds() error", err) + t.Error("GetUnusedMarginFunds() Expected error") } } func TestGetMarginTotalTakenFunds(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.GetMarginTotalTakenFunds() if err == nil { - t.Error("Test Failed - GetMarginTotalTakenFunds() error", err) + t.Error("GetMarginTotalTakenFunds() Expected error") } } func TestCloseMarginFunding(t *testing.T) { - if b.APIKey == "" || b.APISecret == "" { + if !b.ValidateAPICredentials() { t.SkipNow() } t.Parallel() _, err := b.CloseMarginFunding(1337) if err == nil { - t.Error("Test Failed - CloseMarginFunding() error") + t.Error("CloseMarginFunding() Expected error") } } @@ -626,7 +660,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() b.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -638,15 +672,14 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - b.SetDefaults() - TestSetup(t) var feeBuilder = setFeeBuilder() + t.Parallel() - if apiKey != "" || apiSecret != "" { + if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic if resp, err := b.GetFee(feeBuilder); resp != float64(0.002) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity @@ -654,7 +687,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := b.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) t.Error(err) } @@ -662,7 +695,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := b.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) t.Error(err) } @@ -670,7 +703,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -678,7 +711,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := b.GetFee(feeBuilder); resp != float64(0.0004) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0004), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0004), resp) t.Error(err) } } @@ -687,7 +720,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -696,7 +729,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) t.Error(err) } @@ -705,28 +738,24 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - b.SetDefaults() + t.Parallel() expectedResult := exchange.AutoWithdrawCryptoWithAPIPermissionText + " & " + exchange.AutoWithdrawFiatWithAPIPermissionText - withdrawPermissions := b.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + t.Parallel() + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -738,11 +767,9 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + t.Parallel() + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -756,44 +783,47 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if b.APIKey != "" && b.APIKey != "Key" && - b.APISecret != "" && b.APISecret != "Secret" { - return true - } - return false + return b.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.LTC, - Quote: currency.BTC, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: "_", + Base: currency.BTC, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := b.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 1, "clientId") - if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { - t.Errorf("Order failed to be placed: %v", err) - } else if !areTestAPIKeysSet() && err == nil { + response, err := b.SubmitOrder(orderSubmission) + + if areTestAPIKeysSet() && err != nil { + t.Errorf("Could not cancel orders: %v", err) + } + if areTestAPIKeysSet() && !response.IsOrderPlaced { + t.Error("Order not placed") + } + if !areTestAPIKeysSet() && err == nil { t.Error("Expecting an error when no keys are set") } } func TestCancelExchangeOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -810,16 +840,13 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrdera(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -835,30 +862,35 @@ func TestCancelAllExchangeOrdera(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestModifyOrder(t *testing.T) { - _, err := b.ModifyOrder(&exchange.ModifyOrder{}) + t.Parallel() + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := b.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - b.SetDefaults() - TestSetup(t) + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } _, err := b.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) @@ -871,19 +903,19 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Description: "WITHDRAW IT ALL", + var withdrawFiatRequest = exchange.FiatWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.USD, + Description: "WITHDRAW IT ALL", + }, BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, + BankAccountNumber: "12345", BankAddress: "123 Fake St", BankCity: "Tarry Town", BankCountry: "Hyrule", @@ -904,19 +936,19 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Description: "WITHDRAW IT ALL", + var withdrawFiatRequest = exchange.FiatWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, + BankAccountNumber: "12345", BankAddress: "123 Fake St", BankCity: "Tarry Town", BankCountry: "Hyrule", @@ -943,53 +975,182 @@ func TestWithdrawInternationalBank(t *testing.T) { } func TestGetDepositAddress(t *testing.T) { + t.Parallel() if areTestAPIKeysSet() { _, err := b.GetDepositAddress(currency.BTC, "deposit") if err != nil { - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) } } else { _, err := b.GetDepositAddress(currency.BTC, "deposit") if err == nil { - t.Error("Test Failed - GetDepositAddress() error cannot be nil") + t.Error("GetDepositAddress() error cannot be nil") } } } -// TestWsAuth dials websocket, sends login request. -func TestWsAuth(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if !b.Websocket.IsEnabled() && !b.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { - t.Skip(wshandler.WebsocketNotEnabled) - } - b.WebsocketConn = &wshandler.WebsocketConnection{ +func setupWs() { + b.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{ ExchangeName: b.Name, - URL: b.Websocket.GetWebsocketURL(), + URL: authenticatedBitfinexWebsocketEndpoint, Verbose: b.Verbose, ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } var dialer websocket.Dialer - err := b.WebsocketConn.Dial(&dialer, http.Header{}) + err := b.AuthenticatedWebsocketConn.Dial(&dialer, http.Header{}) if err != nil { - t.Fatal(err) + log.Fatal(err) } b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() + go b.WsReadData(b.AuthenticatedWebsocketConn) go b.WsDataHandler() - err = b.WsSendAuth() +} + +// TestWsAuth dials websocket, sends login request. +func TestWsAuth(t *testing.T) { + if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") + } + runAuth(t) +} + +func runAuth(t *testing.T) { + setupWs() + err := b.WsSendAuth() if err != nil { t.Error(err) } timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) select { case resp := <-b.Websocket.DataHandler: - if resp.(map[string]interface{})["event"] != "auth" && resp.(map[string]interface{})["status"] != "OK" { - t.Error("expected successful login") + if logResponse, ok := resp.(map[string]interface{}); ok { + if logResponse["event"] != "auth" && logResponse["status"] != "OK" { + t.Error("expected successful login") + } + } else { + t.Error("Unexpected response") } case <-timer.C: t.Error("Have not received a response") } timer.Stop() + wsAuthExecuted = true +} + +// TestWsPlaceOrder dials websocket, sends order request. +func TestWsPlaceOrder(t *testing.T) { + if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") + } + if !wsAuthExecuted { + runAuth(t) + } + _, err := b.WsNewOrder(&WsNewOrderRequest{ + CustomID: 1337, + Type: order.Buy.String(), + Symbol: "tBTCUSD", + Amount: 10, + Price: -10, + }) + if err != nil { + t.Error(err) + } +} + +// TestWsCancelOrder dials websocket, sends cancel request. +func TestWsCancelOrder(t *testing.T) { + if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") + } + if !wsAuthExecuted { + runAuth(t) + } + err := b.WsCancelOrder(1234) + if err != nil { + t.Error(err) + } +} + +// TestWsCancelOrder dials websocket, sends modify request. +func TestWsUpdateOrder(t *testing.T) { + if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") + } + if !wsAuthExecuted { + runAuth(t) + } + err := b.WsModifyOrder(&WsUpdateOrderRequest{ + OrderID: 1234, + Price: -111, + Amount: 111, + }) + if err != nil { + t.Error(err) + } +} + +// TestWsCancelAllOrders dials websocket, sends cancel all request. +func TestWsCancelAllOrders(t *testing.T) { + if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") + } + if !wsAuthExecuted { + runAuth(t) + } + err := b.WsCancelAllOrders() + if err != nil { + t.Error(err) + } +} + +// TestWsCancelAllOrders dials websocket, sends cancel all request. +func TestWsCancelMultiOrders(t *testing.T) { + if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") + } + if !wsAuthExecuted { + runAuth(t) + } + err := b.WsCancelMultiOrders([]int64{1, 2, 3, 4}) + if err != nil { + t.Error(err) + } +} + +// TestWsNewOffer dials websocket, sends new offer request. +func TestWsNewOffer(t *testing.T) { + if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") + } + if !wsAuthExecuted { + runAuth(t) + } + err := b.WsNewOffer(&WsNewOfferRequest{ + Type: order.Limit.String(), + Symbol: "fBTC", + Amount: -10, + Rate: 10, + Period: 30, + }) + if err != nil { + t.Error(err) + } + time.Sleep(time.Second) +} + +// TestWsCancelOffer dials websocket, sends cancel offer request. +func TestWsCancelOffer(t *testing.T) { + if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { + t.Skip("API keys not set, skipping") + } + if !wsAuthExecuted { + runAuth(t) + } + err := b.WsCancelOffer(1234) + if err != nil { + t.Error(err) + } + time.Sleep(time.Second) } diff --git a/exchanges/bitfinex/bitfinex_types.go b/exchanges/bitfinex/bitfinex_types.go index 38a5d585..7ef5ceb6 100644 --- a/exchanges/bitfinex/bitfinex_types.go +++ b/exchanges/bitfinex/bitfinex_types.go @@ -1,5 +1,7 @@ package bitfinex +import "time" + // Ticker holds basic ticker information from the exchange type Ticker struct { Mid float64 `json:"mid,string"` @@ -28,6 +30,7 @@ type Tickerv2 struct { Volume float64 High float64 Low float64 + Timestamp time.Time } // Tickersv2 holds the version 2 tickers information @@ -382,7 +385,7 @@ type WebsocketChanInfo struct { // WebsocketBook holds booking information type WebsocketBook struct { Price float64 - Count int + ID int Amount float64 } @@ -394,6 +397,16 @@ type WebsocketTrade struct { Amount float64 } +// WebsocketCandle candle data +type WebsocketCandle struct { + Timestamp int64 + Open float64 + Close float64 + High float64 + Low float64 + Volume float64 +} + // WebsocketTicker holds ticker information type WebsocketTicker struct { Bid float64 @@ -413,7 +426,11 @@ type WebsocketPosition struct { Amount float64 Price float64 MarginFunding float64 - MarginFundingType int + MarginFundingType int64 + ProfitLoss float64 + ProfitLossPercent float64 + LiquidationPrice float64 + Leverage float64 } // WebsocketWallet holds wallet information @@ -456,6 +473,9 @@ type WebsocketTradeData struct { OrderID int64 AmountExecuted float64 PriceExecuted float64 + OrderType string + OrderPrice float64 + Maker bool Fee float64 FeeCurrency string } @@ -465,21 +485,215 @@ type ErrorCapture struct { Message string `json:"message"` } -// TimeInterval represents interval enum. -type TimeInterval string +// WebsocketHandshake defines the communication between the websocket API for +// initial connection +type WebsocketHandshake struct { + Event string `json:"event"` + Code int64 `json:"code"` + Version float64 `json:"version"` +} -// TimeInvterval vars -var ( - TimeIntervalMinute = TimeInterval("1m") - TimeIntervalFiveMinutes = TimeInterval("5m") - TimeIntervalFifteenMinutes = TimeInterval("15m") - TimeIntervalThirtyMinutes = TimeInterval("30m") - TimeIntervalHour = TimeInterval("1h") - TimeIntervalThreeHours = TimeInterval("3h") - TimeIntervalSixHours = TimeInterval("6h") - TimeIntervalTwelveHours = TimeInterval("12h") - TimeIntervalDay = TimeInterval("1d") - TimeIntervalSevenDays = TimeInterval("7d") - TimeIntervalFourteenDays = TimeInterval("14d") - TimeIntervalMonth = TimeInterval("1M") +const ( + authenticatedBitfinexWebsocketEndpoint = "wss://api.bitfinex.com/ws/2" + publicBitfinexWebsocketEndpoint = "wss://api-pub.bitfinex.com/ws/2" + pong = "pong" + wsHeartbeat = "hb" + wsPositionSnapshot = "ps" + wsPositionNew = "pn" + wsPositionUpdate = "pu" + wsPositionClose = "pc" + wsWalletSnapshot = "ws" + wsWalletUpdate = "wu" + wsTradeExecutionUpdate = "tu" + wsTradeExecuted = "te" + wsFundingCreditSnapshot = "fcs" + wsFundingCreditNew = "fcn" + wsFundingCreditUpdate = "fcu" + wsFundingCreditCancel = "fcc" + wsFundingLoanSnapshot = "fls" + wsFundingLoanNew = "fln" + wsFundingLoanUpdate = "flu" + wsFundingLoanCancel = "flc" + wsFundingTradeExecuted = "fte" + wsFundingTradeUpdate = "ftu" + wsFundingInfoUpdate = "fiu" + wsBalanceUpdate = "bu" + wsMarginInfoUpdate = "miu" + wsNotification = "n" + wsOrderNew = "on" + wsOrderUpdate = "ou" + wsOrderCancel = "oc" + wsFundingOrderSnapshot = "fos" + wsFundingOrderNew = "fon" + wsFundingOrderUpdate = "fou" + wsFundingOrderCancel = "foc" + wsCancelMultipleOrders = "oc_multi" + wsBook = "book" + wsCandles = "candles" + wsTicker = "ticker" + wsTrades = "trades" + wsError = "error" ) + +// WsAuthRequest container for WS auth request +type WsAuthRequest struct { + Event string `json:"event"` + APIKey string `json:"apiKey"` + AuthPayload string `json:"authPayload"` + AuthSig string `json:"authSig"` + AuthNonce string `json:"authNonce"` + DeadManSwitch int64 `json:"dms,omitempty"` +} + +// WsFundingOffer funding offer received via websocket +type WsFundingOffer struct { + ID int64 + Symbol string + Created int64 + Updated int64 + Amount float64 + AmountOrig float64 + Type string + Flags interface{} + Status string + Rate float64 + Period int64 + Notify bool + Hidden bool + Insure bool + Renew bool + RateReal float64 +} + +// WsCredit credit details received via websocket +type WsCredit struct { + ID int64 + Symbol string + Side string + Created int64 + Updated int64 + Amount float64 + Flags interface{} + Status string + Rate float64 + Period int64 + Opened int64 + LastPayout int64 + Notify bool + Hidden bool + Insure bool + Renew bool + RateReal float64 + NoClose bool + PositionPair string +} + +// WsWallet wallet update details received via websocket +type WsWallet struct { + Type string + Currency string + Balance float64 + UnsettledInterest float64 + BalanceAvailable float64 +} + +// WsBalanceInfo the total and net assets in your account received via websocket +type WsBalanceInfo struct { + TotalAssetsUnderManagement float64 + NetAssetsUnderManagement float64 +} + +// WsFundingInfo account funding info received via websocket +type WsFundingInfo struct { + Symbol string + YieldLoan float64 + YieldLend float64 + DurationLoan float64 + DurationLend float64 +} + +// WsMarginInfoBase account margin info received via websocket +type WsMarginInfoBase struct { + UserProfitLoss float64 + UserSwaps float64 + MarginBalance float64 + MarginNet float64 +} + +// WsFundingTrade recent funding trades received via websocket +type WsFundingTrade struct { + ID int64 + Symbol string + MTSCreated int64 + OfferID int64 + Amount float64 + Rate float64 + Period int64 + Maker bool +} + +// WsNewOrderRequest new order request... +type WsNewOrderRequest struct { + GroupID int64 `json:"gid,omitempty"` + CustomID int64 `json:"cid,omitempty"` + Type string `json:"type"` + Symbol string `json:"symbol"` + Amount float64 `json:"amount,string"` + Price float64 `json:"price,string"` + Leverage int64 `json:"lev,omitempty"` + TrailingPrice float64 `json:"price_trailing,string,omitempty"` + AuxiliaryLimitPrice float64 `json:"price_aux_limit,string,omitempty"` + StopPrice float64 `json:"price_oco_stop,string,omitempty"` + Flags int64 `json:"flags,omitempty"` + TimeInForce string `json:"tif,omitempty"` +} + +// WsUpdateOrderRequest update order request... +type WsUpdateOrderRequest struct { + OrderID int64 `json:"id,omitempty"` + CustomID int64 `json:"cid,omitempty"` + CustomIDDate string `json:"cid_date,omitempty"` + GroupID int64 `json:"gid,omitempty"` + Price float64 `json:"price,string,omitempty"` + Amount float64 `json:"amount,string,omitempty"` + Leverage int64 `json:"lev,omitempty"` + Delta float64 `json:"delta,string,omitempty"` + AuxiliaryLimitPrice float64 `json:"price_aux_limit,string,omitempty"` + TrailingPrice float64 `json:"price_trailing,string,omitempty"` + Flags int64 `json:"flags,omitempty"` + TimeInForce string `json:"tif,omitempty"` +} + +// WsCancelOrderRequest cancel order request... +type WsCancelOrderRequest struct { + OrderID int64 `json:"id,omitempty"` + CustomID int64 `json:"cid,omitempty"` + CustomIDDate string `json:"cid_date,omitempty"` +} + +// WsCancelGroupOrdersRequest cancel orders request... +type WsCancelGroupOrdersRequest struct { + OrderID []int64 `json:"id,omitempty"` + CustomID [][]int64 `json:"cid,omitempty"` + GroupOrderID []int64 `json:"gid,omitempty"` +} + +// WsNewOfferRequest new offer request +type WsNewOfferRequest struct { + Type string `json:"type,omitempty"` + Symbol string `json:"symbol,omitempty"` + Amount float64 `json:"amount,string,omitempty"` + Rate float64 `json:"rate,string,omitempty"` + Period float64 `json:"period,omitempty"` + Flags int64 `json:"flags,omitempty"` +} + +// WsCancelOfferRequest cancel offer request +type WsCancelOfferRequest struct { + OrderID int64 `json:"id"` +} + +// WsCancelAllOrdersRequest cancel all orders request +type WsCancelAllOrdersRequest struct { + All int64 `json:"all"` +} diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index 5e1aca49..2b54be05 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -1,116 +1,28 @@ package bitfinex import ( + "encoding/json" "errors" "fmt" "net/http" "reflect" "strconv" + "strings" "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" log "github.com/thrasher-corp/gocryptotrader/logger" ) -const ( - bitfinexWebsocket = "wss://api.bitfinex.com/ws" - bitfinexWebsocketVersion = "1.1" - bitfinexWebsocketPositionSnapshot = "ps" - bitfinexWebsocketPositionNew = "pn" - bitfinexWebsocketPositionUpdate = "pu" - bitfinexWebsocketPositionClose = "pc" - bitfinexWebsocketWalletSnapshot = "ws" - bitfinexWebsocketWalletUpdate = "wu" - bitfinexWebsocketOrderSnapshot = "os" - bitfinexWebsocketOrderNew = "on" - bitfinexWebsocketOrderUpdate = "ou" - bitfinexWebsocketOrderCancel = "oc" - bitfinexWebsocketTradeExecuted = "te" - bitfinexWebsocketTradeExecutionUpdate = "tu" - bitfinexWebsocketTradeSnapshots = "ts" - bitfinexWebsocketHeartbeat = "hb" - bitfinexWebsocketAlertRestarting = "20051" - bitfinexWebsocketAlertRefreshing = "20060" - bitfinexWebsocketAlertResume = "20061" - bitfinexWebsocketUnknownEvent = "10000" - bitfinexWebsocketUnknownPair = "10001" - bitfinexWebsocketSubscriptionFailed = "10300" - bitfinexWebsocketAlreadySubscribed = "10301" - bitfinexWebsocketUnknownChannel = "10302" -) - -// WebsocketHandshake defines the communication between the websocket API for -// initial connection -type WebsocketHandshake struct { - Event string `json:"event"` - Code int64 `json:"code"` - Version float64 `json:"version"` -} - -var pongReceive chan struct{} - -// WsPingHandler sends a ping request to the websocket server -func (b *Bitfinex) WsPingHandler() error { - req := make(map[string]string) - req["event"] = "ping" - - return b.WebsocketConn.SendMessage(req) -} - -// WsSendAuth sends a autheticated event payload -func (b *Bitfinex) WsSendAuth() error { - if !b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", b.Name) - } - req := make(map[string]interface{}) - payload := "AUTH" + strconv.FormatInt(time.Now().UnixNano(), 10)[:13] - req["event"] = "auth" - req["apiKey"] = b.APIKey - - req["authSig"] = common.HexEncodeToString( - common.GetHMAC( - common.HashSHA512_384, - []byte(payload), - []byte(b.APISecret))) - - req["authPayload"] = payload - - err := b.WebsocketConn.SendMessage(req) - if err != nil { - b.Websocket.SetCanUseAuthenticatedEndpoints(false) - return err - } - return nil -} - -// WsSendUnauth sends an unauthenticated payload -func (b *Bitfinex) WsSendUnauth() error { - req := make(map[string]string) - req["event"] = "unauth" - - return b.WebsocketConn.SendMessage(req) -} - -// WsAddSubscriptionChannel adds a new subscription channel to the -// WebsocketSubdChannels map in bitfinex.go (Bitfinex struct) -func (b *Bitfinex) WsAddSubscriptionChannel(chanID int, channel, pair string) { - chanInfo := WebsocketChanInfo{Pair: pair, Channel: channel} - b.WebsocketSubdChannels[chanID] = chanInfo - - if b.Verbose { - log.Debugf("%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n", - b.GetName(), - channel, - pair, - chanID) - } -} +var comms = make(chan wshandler.WebsocketResponse) // WsConnect starts a new websocket connection func (b *Bitfinex) WsConnect() error { @@ -119,69 +31,64 @@ func (b *Bitfinex) WsConnect() error { } var dialer websocket.Dialer - b.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: b.Name, - URL: b.Websocket.GetWebsocketURL(), - ProxyURL: b.Websocket.GetProxyAddress(), - Verbose: b.Verbose, - } err := b.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { return fmt.Errorf("%v unable to connect to Websocket. Error: %s", b.Name, err) } + go b.WsReadData(b.WebsocketConn) - resp, err := b.WebsocketConn.ReadMessage() - if err != nil { - return fmt.Errorf("%v unable to read from Websocket. Error: %s", b.Name, err) - } - b.Websocket.TrafficAlert <- struct{}{} - var hs WebsocketHandshake - err = common.JSONDecode(resp.Raw, &hs) - if err != nil { - return err - } - - err = b.WsSendAuth() - if err != nil { - log.Errorf("%v - authentication failed: %v", b.Name, err) - } - - b.GenerateDefaultSubscriptions() - if hs.Event == "info" { - if b.Verbose { - log.Debugf("%s Connected to Websocket.\n", b.GetName()) + if b.Websocket.CanUseAuthenticatedEndpoints() { + err = b.AuthenticatedWebsocketConn.Dial(&dialer, http.Header{}) + if err != nil { + log.Errorf(log.ExchangeSys, "%v unable to connect to authenticated Websocket. Error: %s", b.Name, err) + b.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + go b.WsReadData(b.AuthenticatedWebsocketConn) + err = b.WsSendAuth() + if err != nil { + log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", b.Name, err) + b.Websocket.SetCanUseAuthenticatedEndpoints(false) } } - pongReceive = make(chan struct{}, 1) - + b.GenerateDefaultSubscriptions() go b.WsDataHandler() - return nil } +// WsReadData funnels both auth and public ws data into one manageable place +func (b *Bitfinex) WsReadData(ws *wshandler.WebsocketConnection) { + b.Websocket.Wg.Add(1) + defer b.Websocket.Wg.Done() + for { + select { + case <-b.Websocket.ShutdownC: + return + default: + resp, err := ws.ReadMessage() + if err != nil { + b.Websocket.DataHandler <- err + return + } + b.Websocket.TrafficAlert <- struct{}{} + comms <- resp + } + } +} + // WsDataHandler handles data from WsReadData func (b *Bitfinex) WsDataHandler() { b.Websocket.Wg.Add(1) - defer b.Websocket.Wg.Done() for { select { case <-b.Websocket.ShutdownC: return - - default: - stream, err := b.WebsocketConn.ReadMessage() - if err != nil { - b.Websocket.DataHandler <- err - return - } - b.Websocket.TrafficAlert <- struct{}{} - + case stream := <-comms: if stream.Type == websocket.TextMessage { var result interface{} - err = common.JSONDecode(stream.Raw, &result) + err := json.Unmarshal(stream.Raw, &result) if err != nil { b.Websocket.DataHandler <- err return @@ -192,10 +99,17 @@ func (b *Bitfinex) WsDataHandler() { event := eventData["event"] switch event { case "subscribed": - b.WsAddSubscriptionChannel(int(eventData["chanId"].(float64)), - eventData["channel"].(string), - eventData["pair"].(string)) - + if symbol, ok := eventData["pair"].(string); ok { + b.WsAddSubscriptionChannel(int(eventData["chanId"].(float64)), + eventData["channel"].(string), + symbol, + ) + } else if key, ok := eventData["key"].(string); ok { + b.WsAddSubscriptionChannel(int(eventData["chanId"].(float64)), + eventData["channel"].(string), + key, + ) + } case "auth": status := eventData["status"].(string) if status == "OK" { @@ -206,199 +120,104 @@ func (b *Bitfinex) WsDataHandler() { eventData["code"].(string)) } } - case "[]interface {}": chanData := result.([]interface{}) chanID := int(chanData[0].(float64)) - chanInfo, ok := b.WebsocketSubdChannels[chanID] - if !ok { + if !ok && chanID != 0 { b.Websocket.DataHandler <- fmt.Errorf("bitfinex.go error - Unable to locate chanID: %d", chanID) continue } - if len(chanData) == 2 { - if reflect.TypeOf(chanData[1]).String() == "string" { - if chanData[1].(string) == bitfinexWebsocketHeartbeat { - continue - } else if chanData[1].(string) == "pong" { - pongReceive <- struct{}{} - continue - } - } - } switch chanInfo.Channel { - case "book": + case wsBook: var newOrderbook []WebsocketBook curr := currency.NewPairFromString(chanInfo.Pair) - switch len(chanData) { - case 2: - data := chanData[1].([]interface{}) - for i := range data { - y := data[i].([]interface{}) + if obSnapBundle, ok := chanData[1].([]interface{}); ok { + switch snapshot := obSnapBundle[0].(type) { + case []interface{}: + for i := range snapshot { + obSnap := snapshot[i].([]interface{}) + newOrderbook = append(newOrderbook, WebsocketBook{ + ID: int(obSnap[0].(float64)), + Price: obSnap[1].(float64), + Amount: obSnap[2].(float64)}) + } + err := b.WsInsertSnapshot(curr, + asset.Spot, + newOrderbook) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s", + err) + } + case float64: newOrderbook = append(newOrderbook, WebsocketBook{ - Price: y[0].(float64), - Count: int(y[1].(float64)), - Amount: y[2].(float64)}) - } - - err := b.WsInsertSnapshot(curr, - orderbook.Spot, - newOrderbook) - - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s", - err) - } - case 4: - newOrderbook = append(newOrderbook, WebsocketBook{ - Price: chanData[1].(float64), - Count: int(chanData[2].(float64)), - Amount: chanData[3].(float64)}) - err := b.WsUpdateOrderbook(curr, - orderbook.Spot, - newOrderbook) - - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go updating orderbook error: %s", - err) + ID: int(snapshot), + Price: obSnapBundle[1].(float64), + Amount: obSnapBundle[2].(float64)}) + err := b.WsUpdateOrderbook(curr, + asset.Spot, + newOrderbook) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s", + err) + } } } - case "ticker": - b.Websocket.DataHandler <- wshandler.TickerData{ - Quantity: chanData[8].(float64), - ClosePrice: chanData[7].(float64), - HighPrice: chanData[9].(float64), - LowPrice: chanData[10].(float64), - Pair: currency.NewPairFromString(chanInfo.Pair), - Exchange: b.GetName(), - AssetType: orderbook.Spot, - } - - case "account": - switch chanData[1].(string) { - case bitfinexWebsocketPositionSnapshot: - var positionSnapshot []WebsocketPosition - data := chanData[2].([]interface{}) - for i := range data { - y := data[i].([]interface{}) - positionSnapshot = append(positionSnapshot, - WebsocketPosition{ - Pair: y[0].(string), - Status: y[1].(string), - Amount: y[2].(float64), - Price: y[3].(float64), - MarginFunding: y[4].(float64), - MarginFundingType: int(y[5].(float64))}) - } - - if len(positionSnapshot) == 0 { + continue + case wsCandles: + curr := currency.NewPairFromString(chanInfo.Pair) + if candleBundle, ok := chanData[1].([]interface{}); ok { + if len(candleBundle) == 0 { continue } - - b.Websocket.DataHandler <- positionSnapshot - - case bitfinexWebsocketPositionNew, bitfinexWebsocketPositionUpdate, bitfinexWebsocketPositionClose: - data := chanData[2].([]interface{}) - position := WebsocketPosition{ - Pair: data[0].(string), - Status: data[1].(string), - Amount: data[2].(float64), - Price: data[3].(float64), - MarginFunding: data[4].(float64), - MarginFundingType: int(data[5].(float64))} - - b.Websocket.DataHandler <- position - - case bitfinexWebsocketWalletSnapshot: - data := chanData[2].([]interface{}) - var walletSnapshot []WebsocketWallet - for i := range data { - y := data[i].([]interface{}) - walletSnapshot = append(walletSnapshot, - WebsocketWallet{ - Name: y[0].(string), - Currency: y[1].(string), - Balance: y[2].(float64), - UnsettledInterest: y[3].(float64)}) + switch candleBundle[0].(type) { + case []interface{}: + for i := range candleBundle { + candle := candleBundle[i].([]interface{}) + b.Websocket.DataHandler <- wshandler.KlineData{ + Timestamp: time.Unix(0, candle[0].(int64)), + Exchange: b.Name, + AssetType: asset.Spot, + Pair: curr, + OpenPrice: candle[1].(float64), + ClosePrice: candle[2].(float64), + HighPrice: candle[3].(float64), + LowPrice: candle[4].(float64), + Volume: candle[5].(float64), + } + } + case float64: + b.Websocket.DataHandler <- wshandler.KlineData{ + Timestamp: time.Unix(0, candleBundle[0].(int64)), + Exchange: b.Name, + AssetType: asset.Spot, + Pair: curr, + OpenPrice: candleBundle[1].(float64), + ClosePrice: candleBundle[2].(float64), + HighPrice: candleBundle[3].(float64), + LowPrice: candleBundle[4].(float64), + Volume: candleBundle[5].(float64), + } } - - b.Websocket.DataHandler <- walletSnapshot - - case bitfinexWebsocketWalletUpdate: - data := chanData[2].([]interface{}) - wallet := WebsocketWallet{ - Name: data[0].(string), - Currency: data[1].(string), - Balance: data[2].(float64), - UnsettledInterest: data[3].(float64)} - - b.Websocket.DataHandler <- wallet - - case bitfinexWebsocketOrderSnapshot: - var orderSnapshot []WebsocketOrder - data := chanData[2].([]interface{}) - for i := range data { - y := data[i].([]interface{}) - orderSnapshot = append(orderSnapshot, - WebsocketOrder{ - OrderID: int64(y[0].(float64)), - Pair: y[1].(string), - Amount: y[2].(float64), - OrigAmount: y[3].(float64), - OrderType: y[4].(string), - Status: y[5].(string), - Price: y[6].(float64), - PriceAvg: y[7].(float64), - Timestamp: y[8].(string)}) - } - - b.Websocket.DataHandler <- orderSnapshot - - case bitfinexWebsocketOrderNew, bitfinexWebsocketOrderUpdate, bitfinexWebsocketOrderCancel: - data := chanData[2].([]interface{}) - order := WebsocketOrder{ - OrderID: int64(data[0].(float64)), - Pair: data[1].(string), - Amount: data[2].(float64), - OrigAmount: data[3].(float64), - OrderType: data[4].(string), - Status: data[5].(string), - Price: data[6].(float64), - PriceAvg: data[7].(float64), - Timestamp: data[8].(string), - Notify: int(data[9].(float64))} - - b.Websocket.DataHandler <- order - - case bitfinexWebsocketTradeExecuted: - data := chanData[2].([]interface{}) - trade := WebsocketTradeExecuted{ - TradeID: int64(data[0].(float64)), - Pair: data[1].(string), - Timestamp: int64(data[2].(float64)), - OrderID: int64(data[3].(float64)), - AmountExecuted: data[4].(float64), - PriceExecuted: data[5].(float64)} - - b.Websocket.DataHandler <- trade - case bitfinexWebsocketTradeSnapshots, bitfinexWebsocketTradeExecutionUpdate: - data := chanData[2].([]interface{}) - trade := WebsocketTradeData{ - TradeID: int64(data[0].(float64)), - Pair: data[1].(string), - Timestamp: int64(data[2].(float64)), - OrderID: int64(data[3].(float64)), - AmountExecuted: data[4].(float64), - PriceExecuted: data[5].(float64), - Fee: data[6].(float64), - FeeCurrency: data[7].(string)} - - b.Websocket.DataHandler <- trade } - - case "trades": + continue + case wsTicker: + tickerData := chanData[1].([]interface{}) + b.Websocket.DataHandler <- wshandler.TickerData{ + Exchange: b.Name, + Bid: tickerData[0].(float64), + Ask: tickerData[2].(float64), + Last: tickerData[6].(float64), + Volume: tickerData[7].(float64), + High: tickerData[8].(float64), + Low: tickerData[9].(float64), + AssetType: asset.Spot, + Pair: currency.NewPairFromString(chanInfo.Pair), + } + continue + case wsTrades: var trades []WebsocketTrade switch len(chanData) { case 2: @@ -408,42 +227,370 @@ func (b *Bitfinex) WsDataHandler() { if _, ok := y[0].(string); ok { continue } - - id, _ := y[0].(float64) - trades = append(trades, WebsocketTrade{ - ID: int64(id), + ID: int64(y[0].(float64)), Timestamp: int64(y[1].(float64)), - Price: y[2].(float64), - Amount: y[3].(float64)}) + Price: y[3].(float64), + Amount: y[2].(float64)}) } - - case 7: - trade := WebsocketTrade{ - ID: int64(chanData[3].(float64)), - Timestamp: int64(chanData[4].(float64)), - Price: chanData[5].(float64), - Amount: chanData[6].(float64)} - trades = append(trades, trade) + case 3: + if chanData[1].(string) == wsTradeExecuted { + // the te update contains less data then the "tu" + continue + } + data := chanData[2].([]interface{}) + trades = append(trades, WebsocketTrade{ + ID: int64(data[0].(float64)), + Timestamp: int64(data[1].(float64)), + Price: data[3].(float64), + Amount: data[2].(float64)}) } - if len(trades) > 0 { - side := "BUY" - newAmount := trades[0].Amount - if newAmount < 0 { - side = "SELL" - newAmount *= -1 + for i := range trades { + side := order.Buy.String() + newAmount := trades[i].Amount + if newAmount < 0 { + side = order.Sell.String() + newAmount *= -1 + } + b.Websocket.DataHandler <- wshandler.TradeData{ + CurrencyPair: currency.NewPairFromString(chanInfo.Pair), + Timestamp: time.Unix(trades[i].Timestamp, 0), + Price: trades[i].Price, + Amount: newAmount, + Exchange: b.Name, + AssetType: asset.Spot, + Side: side, + } } - - b.Websocket.DataHandler <- wshandler.TradeData{ - CurrencyPair: currency.NewPairFromString(chanInfo.Pair), - Timestamp: time.Unix(trades[0].Timestamp, 0), - Price: trades[0].Price, - Amount: newAmount, - Exchange: b.GetName(), - AssetType: orderbook.Spot, - Side: side, + continue + } + } + if authResp, ok := chanData[1].(string); ok { + switch authResp { + case wsHeartbeat, pong: + continue + case wsNotification: + notification := chanData[2].([]interface{}) + if data, ok := notification[4].([]interface{}); ok { + channelName := notification[1].(string) + switch { + case strings.Contains(channelName, wsOrderUpdate), + strings.Contains(channelName, wsOrderCancel), + strings.Contains(channelName, wsFundingOrderCancel): + if data[0] != nil && data[0].(float64) > 0 { + b.AuthenticatedWebsocketConn.AddResponseWithID(int64(data[0].(float64)), stream.Raw) + continue + } + case strings.Contains(channelName, wsOrderNew): + if data[2] != nil && data[2].(float64) > 0 { + b.AuthenticatedWebsocketConn.AddResponseWithID(int64(data[2].(float64)), stream.Raw) + continue + } + } + b.Websocket.DataHandler <- fmt.Errorf("%s - Unexpected data returned %s", b.Name, stream.Raw) + continue + } + if notification[5] != nil && strings.EqualFold(notification[5].(string), wsError) { + b.Websocket.DataHandler <- fmt.Errorf("%s - Error %s", b.Name, notification[6].(string)) + } + case wsPositionSnapshot: + var snapshot []WebsocketPosition + if snapBundle, ok := chanData[2].([]interface{}); ok && len(snapBundle) > 0 { + if _, ok := snapBundle[0].([]interface{}); ok { + for i := range snapBundle { + positionData := snapBundle[i].([]interface{}) + position := WebsocketPosition{ + Pair: positionData[0].(string), + Status: positionData[1].(string), + Amount: positionData[2].(float64), + Price: positionData[3].(float64), + MarginFunding: positionData[4].(float64), + MarginFundingType: int64(positionData[5].(float64)), + ProfitLoss: positionData[6].(float64), + ProfitLossPercent: positionData[7].(float64), + LiquidationPrice: positionData[8].(float64), + Leverage: positionData[9].(float64), + } + snapshot = append(snapshot, position) + } + b.Websocket.DataHandler <- snapshot + } + } + case wsPositionNew, wsPositionUpdate, wsPositionClose: + if positionData, ok := chanData[2].([]interface{}); ok && len(positionData) > 0 { + position := WebsocketPosition{ + Pair: positionData[0].(string), + Status: positionData[1].(string), + Amount: positionData[2].(float64), + Price: positionData[3].(float64), + MarginFunding: positionData[4].(float64), + MarginFundingType: int64(positionData[5].(float64)), + ProfitLoss: positionData[6].(float64), + ProfitLossPercent: positionData[7].(float64), + LiquidationPrice: positionData[8].(float64), + Leverage: positionData[9].(float64), + } + b.Websocket.DataHandler <- position + } + case wsTradeExecutionUpdate: + if tradeData, ok := chanData[2].([]interface{}); ok && len(tradeData) > 4 { + b.Websocket.DataHandler <- WebsocketTradeData{ + TradeID: int64(tradeData[0].(float64)), + Pair: tradeData[1].(string), + Timestamp: int64(tradeData[2].(float64)), + OrderID: int64(tradeData[3].(float64)), + AmountExecuted: tradeData[4].(float64), + PriceExecuted: tradeData[5].(float64), + OrderType: tradeData[6].(string), + OrderPrice: tradeData[7].(float64), + Maker: tradeData[8].(float64) == 1, + Fee: tradeData[9].(float64), + FeeCurrency: tradeData[10].(string), + } + } + case wsFundingOrderSnapshot: + var snapshot []WsFundingOffer + if snapBundle, ok := chanData[2].([]interface{}); ok && len(snapBundle) > 0 { + if _, ok := snapBundle[0].([]interface{}); ok { + for i := range snapBundle { + data := snapBundle[i].([]interface{}) + offer := WsFundingOffer{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + Created: int64(data[2].(float64)), + Updated: int64(data[3].(float64)), + Amount: data[4].(float64), + AmountOrig: data[5].(float64), + Type: data[6].(string), + Flags: data[9].(float64), + Status: data[10].(string), + Rate: data[14].(float64), + Period: int64(data[15].(float64)), + Notify: data[16].(float64) == 1, + Hidden: data[17].(float64) == 1, + Insure: data[18].(float64) == 1, + Renew: data[19].(float64) == 1, + RateReal: data[20].(float64), + } + snapshot = append(snapshot, offer) + } + b.Websocket.DataHandler <- snapshot + } + } + case wsFundingOrderNew, wsFundingOrderUpdate, wsFundingOrderCancel: + if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { + b.Websocket.DataHandler <- WsFundingOffer{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + Created: int64(data[2].(float64)), + Updated: int64(data[3].(float64)), + Amount: data[4].(float64), + AmountOrig: data[5].(float64), + Type: data[6].(string), + Flags: data[9].(float64), + Status: data[10].(string), + Rate: data[14].(float64), + Period: int64(data[15].(float64)), + Notify: data[16].(float64) == 1, + Hidden: data[17].(float64) == 1, + Insure: data[18].(float64) == 1, + Renew: data[19].(float64) == 1, + RateReal: data[20].(float64), + } + } + case wsFundingCreditSnapshot: + var snapshot []WsCredit + if snapBundle, ok := chanData[2].([]interface{}); ok && len(snapBundle) > 0 { + if _, ok := snapBundle[0].([]interface{}); ok { + for i := range snapBundle { + data := snapBundle[i].([]interface{}) + credit := WsCredit{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + Side: data[2].(string), + Created: int64(data[3].(float64)), + Updated: int64(data[4].(float64)), + Amount: data[5].(float64), + Flags: data[6].(string), + Status: data[7].(string), + Rate: data[11].(float64), + Period: int64(data[12].(float64)), + Opened: int64(data[13].(float64)), + LastPayout: int64(data[14].(float64)), + Notify: data[15].(float64) == 1, + Hidden: data[16].(float64) == 1, + Insure: data[17].(float64) == 1, + Renew: data[18].(float64) == 1, + RateReal: data[19].(float64), + NoClose: data[20].(float64) == 1, + PositionPair: data[21].(string), + } + snapshot = append(snapshot, credit) + } + b.Websocket.DataHandler <- snapshot + } + } + case wsFundingCreditNew, wsFundingCreditUpdate, wsFundingCreditCancel: + if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { + b.Websocket.DataHandler <- WsCredit{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + Side: data[2].(string), + Created: int64(data[3].(float64)), + Updated: int64(data[4].(float64)), + Amount: data[5].(float64), + Flags: data[6].(string), + Status: data[7].(string), + Rate: data[11].(float64), + Period: int64(data[12].(float64)), + Opened: int64(data[13].(float64)), + LastPayout: int64(data[14].(float64)), + Notify: data[15].(float64) == 1, + Hidden: data[16].(float64) == 1, + Insure: data[17].(float64) == 1, + Renew: data[18].(float64) == 1, + RateReal: data[19].(float64), + NoClose: data[20].(float64) == 1, + PositionPair: data[21].(string), + } + } + case wsFundingLoanSnapshot: + var snapshot []WsCredit + if snapBundle, ok := chanData[2].([]interface{}); ok && len(snapBundle) > 0 { + if _, ok := snapBundle[0].([]interface{}); ok { + for i := range snapBundle { + data := snapBundle[i].([]interface{}) + credit := WsCredit{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + Side: data[2].(string), + Created: int64(data[3].(float64)), + Updated: int64(data[4].(float64)), + Amount: data[5].(float64), + Flags: data[6].(string), + Status: data[7].(string), + Rate: data[11].(float64), + Period: int64(data[12].(float64)), + Opened: int64(data[13].(float64)), + LastPayout: int64(data[14].(float64)), + Notify: data[15].(float64) == 1, + Hidden: data[16].(float64) == 1, + Insure: data[17].(float64) == 1, + Renew: data[18].(float64) == 1, + RateReal: data[19].(float64), + NoClose: data[20].(float64) == 1, + } + snapshot = append(snapshot, credit) + } + b.Websocket.DataHandler <- snapshot + } + } + case wsFundingLoanNew, wsFundingLoanUpdate, wsFundingLoanCancel: + if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { + b.Websocket.DataHandler <- WsCredit{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + Side: data[2].(string), + Created: int64(data[3].(float64)), + Updated: int64(data[4].(float64)), + Amount: data[5].(float64), + Flags: data[6].(string), + Status: data[7].(string), + Rate: data[11].(float64), + Period: int64(data[12].(float64)), + Opened: int64(data[13].(float64)), + LastPayout: int64(data[14].(float64)), + Notify: data[15].(float64) == 1, + Hidden: data[16].(float64) == 1, + Insure: data[17].(float64) == 1, + Renew: data[18].(float64) == 1, + RateReal: data[19].(float64), + NoClose: data[20].(float64) == 1, + } + } + case wsWalletSnapshot: + var snapshot []WsWallet + if snapBundle, ok := chanData[2].([]interface{}); ok && len(snapBundle) > 0 { + if _, ok := snapBundle[0].([]interface{}); ok { + for i := range snapBundle { + data := snapBundle[i].([]interface{}) + var balanceAvailable float64 + if _, ok := data[4].(float64); ok { + balanceAvailable = data[4].(float64) + } + wallet := WsWallet{ + Type: data[0].(string), + Currency: data[1].(string), + Balance: data[2].(float64), + UnsettledInterest: data[3].(float64), + BalanceAvailable: balanceAvailable, + } + snapshot = append(snapshot, wallet) + } + b.Websocket.DataHandler <- snapshot + } + } + case wsWalletUpdate: + if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { + var balanceAvailable float64 + if _, ok := data[4].(float64); ok { + balanceAvailable = data[4].(float64) + } + b.Websocket.DataHandler <- WsWallet{ + Type: data[0].(string), + Currency: data[1].(string), + Balance: data[2].(float64), + UnsettledInterest: data[3].(float64), + BalanceAvailable: balanceAvailable, + } + } + case wsBalanceUpdate: + if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { + b.Websocket.DataHandler <- WsBalanceInfo{ + TotalAssetsUnderManagement: data[0].(float64), + NetAssetsUnderManagement: data[1].(float64), + } + } + case wsMarginInfoUpdate: + if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { + if data[0].(string) == "base" { + if infoBase, ok := chanData[2].([]interface{}); ok && len(infoBase) > 0 { + baseData := data[1].([]interface{}) + b.Websocket.DataHandler <- WsMarginInfoBase{ + UserProfitLoss: baseData[0].(float64), + UserSwaps: baseData[1].(float64), + MarginBalance: baseData[2].(float64), + MarginNet: baseData[3].(float64), + } + } + } + } + case wsFundingInfoUpdate: + if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { + if data[0].(string) == "sym" { + symbolData := data[1].([]interface{}) + b.Websocket.DataHandler <- WsFundingInfo{ + YieldLoan: symbolData[0].(float64), + YieldLend: symbolData[1].(float64), + DurationLoan: symbolData[2].(float64), + DurationLend: symbolData[3].(float64), + } + } + } + case wsFundingTradeExecuted, wsFundingTradeUpdate: + if data, ok := chanData[2].([]interface{}); ok && len(data) > 0 { + b.Websocket.DataHandler <- WsFundingTrade{ + ID: int64(data[0].(float64)), + Symbol: data[1].(string), + MTSCreated: int64(data[2].(float64)), + OfferID: int64(data[3].(float64)), + Amount: data[4].(float64), + Rate: data[5].(float64), + Period: int64(data[6].(float64)), + Maker: data[7].(float64) == 1, + } } } } @@ -455,7 +602,7 @@ func (b *Bitfinex) WsDataHandler() { // WsInsertSnapshot add the initial orderbook snapshot when subscribed to a // channel -func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType string, books []WebsocketBook) error { +func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books []WebsocketBook) error { if len(books) == 0 { return errors.New("bitfinex.go error - no orderbooks submitted") } @@ -475,29 +622,31 @@ func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType string, books []W newOrderBook.AssetType = assetType newOrderBook.Bids = bid newOrderBook.Pair = p - err := b.Websocket.Orderbook.LoadSnapshot(&newOrderBook, false) + newOrderBook.ExchangeName = b.Name + + err := b.Websocket.Orderbook.LoadSnapshot(&newOrderBook) if err != nil { return fmt.Errorf("bitfinex.go error - %s", err) } b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: p, Asset: assetType, - Exchange: b.GetName()} + Exchange: b.Name} return nil } // WsUpdateOrderbook updates the orderbook list, removing and adding to the // orderbook sides -func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType string, book []WebsocketBook) error { +func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book []WebsocketBook) error { orderbookUpdate := wsorderbook.WebsocketOrderbookUpdate{ - Asks: []orderbook.Item{}, - Bids: []orderbook.Item{}, - AssetType: assetType, - CurrencyPair: p, + Asks: []orderbook.Item{}, + Bids: []orderbook.Item{}, + Asset: assetType, + Pair: p, } for i := 0; i < len(book); i++ { switch { - case book[i].Count > 0: + case book[i].Price > 0: if book[i].Amount > 0 { // update bid orderbookUpdate.Bids = append(orderbookUpdate.Bids, orderbook.Item{Amount: book[i].Amount, Price: book[i].Price}) @@ -505,7 +654,7 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType string, book []W // update ask orderbookUpdate.Asks = append(orderbookUpdate.Asks, orderbook.Item{Amount: book[i].Amount * -1, Price: book[i].Price}) } - case book[i].Count == 0: + case book[i].Price == 0: if book[i].Amount == 1 { // delete bid orderbookUpdate.Bids = append(orderbookUpdate.Bids, orderbook.Item{Amount: 0, Price: book[i].Price}) @@ -515,7 +664,6 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType string, book []W } } } - orderbookUpdate.UpdateTime = time.Now() err := b.Websocket.Orderbook.Update(&orderbookUpdate) if err != nil { return err @@ -523,24 +671,39 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType string, book []W b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: p, Asset: assetType, - Exchange: b.GetName()} + Exchange: b.Name} return nil } // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (b *Bitfinex) GenerateDefaultSubscriptions() { - var channels = []string{"book", "trades", "ticker"} + var channels = []string{ + wsBook, + wsTrades, + wsTicker, + wsCandles, + } var subscriptions []wshandler.WebsocketChannelSubscription for i := range channels { - for j := range b.EnabledPairs { - params := make(map[string]interface{}) - if channels[i] == "book" { - params["prec"] = "P0" + enabledPairs := b.GetEnabledPairs(asset.Spot) + for j := range enabledPairs { + if strings.HasPrefix(enabledPairs[j].Base.String(), "f") { + log.Warnf(log.WebsocketMgr, + "%v - Websocket does not support funding currency %v, skipping", + b.Name, enabledPairs[j]) + continue } + b.appendOptionalDelimiter(&enabledPairs[j]) + params := make(map[string]interface{}) + if channels[i] == wsBook { + params["prec"] = "R0" + params["len"] = "100" + } + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: channels[i], - Currency: b.EnabledPairs[j], + Currency: enabledPairs[j], Params: params, }) } @@ -554,7 +717,14 @@ func (b *Bitfinex) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscr req["event"] = "subscribe" req["channel"] = channelToSubscribe.Channel if channelToSubscribe.Currency.String() != "" { - req["pair"] = channelToSubscribe.Currency.String() + if channelToSubscribe.Channel == wsCandles { + // TODO: Add ability to select timescale + req["key"] = fmt.Sprintf("trade:1D:%v", + b.FormatExchangeCurrency(channelToSubscribe.Currency, asset.Spot).String()) + } else { + req["symbol"] = b.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String() + } } if len(channelToSubscribe.Params) > 0 { for k, v := range channelToSubscribe.Params { @@ -577,3 +747,196 @@ func (b *Bitfinex) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubs } return b.WebsocketConn.SendMessage(req) } + +// WsSendAuth sends a autheticated event payload +func (b *Bitfinex) WsSendAuth() error { + if !b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", b.Name) + } + nonce := strconv.FormatInt(time.Now().Unix(), 10) + payload := "AUTH" + nonce + request := WsAuthRequest{ + Event: "auth", + APIKey: b.API.Credentials.Key, + AuthPayload: payload, + AuthSig: crypto.HexEncodeToString( + crypto.GetHMAC( + crypto.HashSHA512_384, + []byte(payload), + []byte(b.API.Credentials.Secret))), + AuthNonce: nonce, + DeadManSwitch: 0, + } + err := b.AuthenticatedWebsocketConn.SendMessage(request) + if err != nil { + b.Websocket.SetCanUseAuthenticatedEndpoints(false) + return err + } + return nil +} + +// WsSendUnauth sends an unauthenticated payload +func (b *Bitfinex) WsSendUnauth() error { + req := make(map[string]string) + req["event"] = "unauth" + + return b.WebsocketConn.SendMessage(req) +} + +// WsAddSubscriptionChannel adds a new subscription channel to the +// WebsocketSubdChannels map in bitfinex.go (Bitfinex struct) +func (b *Bitfinex) WsAddSubscriptionChannel(chanID int, channel, pair string) { + chanInfo := WebsocketChanInfo{Pair: pair, Channel: channel} + b.WebsocketSubdChannels[chanID] = chanInfo + + if b.Verbose { + log.Debugf(log.ExchangeSys, "%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n", + b.Name, + channel, + pair, + chanID) + } +} + +// WsNewOrder authenticated new order request +func (b *Bitfinex) WsNewOrder(data *WsNewOrderRequest) (string, error) { + data.CustomID = b.AuthenticatedWebsocketConn.GenerateMessageID(false) + request := makeRequestInterface(wsOrderNew, data) + resp, err := b.AuthenticatedWebsocketConn.SendMessageReturnResponse(data.CustomID, request) + if err != nil { + return "", err + } + if resp == nil { + return "", errors.New(b.Name + " - Order message not returned") + } + var respData []interface{} + err = json.Unmarshal(resp, &respData) + if err != nil { + return "", err + } + responseDataDetail := respData[2].([]interface{}) + responseOrderDetail := responseDataDetail[4].([]interface{}) + var orderID string + if responseOrderDetail[0] != nil && responseOrderDetail[0].(float64) > 0 { + orderID = strconv.FormatFloat(responseOrderDetail[0].(float64), 'f', -1, 64) + } + errCode := responseDataDetail[6].(string) + errorMessage := responseDataDetail[7].(string) + + if strings.EqualFold(errCode, wsError) { + return orderID, errors.New(b.Name + " - " + errCode + ": " + errorMessage) + } + + return orderID, nil +} + +// WsModifyOrder authenticated modify order request +func (b *Bitfinex) WsModifyOrder(data *WsUpdateOrderRequest) error { + request := makeRequestInterface(wsOrderUpdate, data) + resp, err := b.AuthenticatedWebsocketConn.SendMessageReturnResponse(data.OrderID, request) + if err != nil { + return err + } + if resp == nil { + return errors.New(b.Name + " - Order message not returned") + } + + var responseData []interface{} + err = json.Unmarshal(resp, &responseData) + if err != nil { + return err + } + responseOrderData := responseData[2].([]interface{}) + errCode := responseOrderData[6].(string) + errorMessage := responseOrderData[7].(string) + if strings.EqualFold(errCode, wsError) { + return errors.New(b.Name + " - " + errCode + ": " + errorMessage) + } + + return nil +} + +// WsCancelMultiOrders authenticated cancel multi order request +func (b *Bitfinex) WsCancelMultiOrders(orderIDs []int64) error { + cancel := WsCancelGroupOrdersRequest{ + OrderID: orderIDs, + } + request := makeRequestInterface(wsCancelMultipleOrders, cancel) + return b.AuthenticatedWebsocketConn.SendMessage(request) +} + +// WsCancelOrder authenticated cancel order request +func (b *Bitfinex) WsCancelOrder(orderID int64) error { + cancel := WsCancelOrderRequest{ + OrderID: orderID, + } + request := makeRequestInterface(wsOrderCancel, cancel) + resp, err := b.AuthenticatedWebsocketConn.SendMessageReturnResponse(orderID, request) + if err != nil { + return err + } + if resp == nil { + return fmt.Errorf("%v - Order %v failed to cancel", b.Name, orderID) + } + var responseData []interface{} + err = json.Unmarshal(resp, &responseData) + if err != nil { + return err + } + responseOrderData := responseData[2].([]interface{}) + errCode := responseOrderData[6].(string) + errorMessage := responseOrderData[7].(string) + if strings.EqualFold(errCode, wsError) { + return errors.New(b.Name + " - " + errCode + ": " + errorMessage) + } + + return nil +} + +// WsCancelAllOrders authenticated cancel all orders request +func (b *Bitfinex) WsCancelAllOrders() error { + cancelAll := WsCancelAllOrdersRequest{All: 1} + request := makeRequestInterface(wsCancelMultipleOrders, cancelAll) + return b.AuthenticatedWebsocketConn.SendMessage(request) +} + +// WsNewOffer authenticated new offer request +func (b *Bitfinex) WsNewOffer(data *WsNewOfferRequest) error { + request := makeRequestInterface(wsFundingOrderNew, data) + return b.AuthenticatedWebsocketConn.SendMessage(request) +} + +// WsCancelOffer authenticated cancel offer request +func (b *Bitfinex) WsCancelOffer(orderID int64) error { + cancel := WsCancelOrderRequest{ + OrderID: orderID, + } + request := makeRequestInterface(wsFundingOrderCancel, cancel) + resp, err := b.AuthenticatedWebsocketConn.SendMessageReturnResponse(orderID, request) + if err != nil { + return err + } + if resp == nil { + return fmt.Errorf("%v - Order %v failed to cancel", b.Name, orderID) + } + var responseData []interface{} + err = json.Unmarshal(resp, &responseData) + if err != nil { + return err + } + responseOrderData := responseData[2].([]interface{}) + errCode := responseOrderData[6].(string) + var errorMessage string + if responseOrderData[7] != nil { + errorMessage = responseOrderData[7].(string) + } + if strings.EqualFold(errCode, wsError) { + return errors.New(b.Name + " - " + errCode + ": " + errorMessage) + } + + return nil +} + +func makeRequestInterface(channelName string, data interface{}) []interface{} { + return []interface{}{0, channelName, nil, data} +} diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index a5aadc1f..555dedf4 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -2,7 +2,6 @@ package bitfinex import ( "errors" - "fmt" "net/url" "strconv" "strings" @@ -10,14 +9,189 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (b *Bitfinex) GetDefaultConfig() (*config.ExchangeConfig, error) { + b.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = b.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = b.BaseCurrencies + + err := b.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if b.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = b.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for bitfinex +func (b *Bitfinex) SetDefaults() { + b.Name = "Bitfinex" + b.Enabled = true + b.Verbose = true + b.WebsocketSubdChannels = make(map[int]WebsocketChanInfo) + b.API.CredentialsValidator.RequiresKey = true + b.API.CredentialsValidator.RequiresSecret = true + + b.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + } + + b.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + FiatWithdraw: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + SubmitOrders: true, + DepositHistory: true, + WithdrawalHistory: true, + TradeFetching: true, + UserTradeHistory: true, + TradeFee: true, + FiatDepositFee: true, + FiatWithdrawalFee: true, + CryptoDepositFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + AccountBalance: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + ModifyOrder: true, + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AccountInfo: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + MessageCorrelation: true, + DeadMansSwitch: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | + exchange.AutoWithdrawFiatWithAPIPermission, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + b.Requester = request.New(b.Name, + request.NewRateLimit(time.Second*60, bitfinexAuthRate), + request.NewRateLimit(time.Second*60, bitfinexUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + b.API.Endpoints.URLDefault = bitfinexAPIURLBase + b.API.Endpoints.URL = b.API.Endpoints.URLDefault + b.API.Endpoints.WebsocketURL = publicBitfinexWebsocketEndpoint + b.Websocket = wshandler.New() + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup takes in the supplied exchange configuration details and sets params +func (b *Bitfinex) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + b.SetEnabled(false) + return nil + } + + err := b.SetupDefaults(exch) + if err != nil { + return err + } + + err = b.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: publicBitfinexWebsocketEndpoint, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: b.WsConnect, + Subscriber: b.Subscribe, + UnSubscriber: b.Unsubscribe, + Features: &b.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + b.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: authenticatedBitfinexWebsocketEndpoint, + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + b.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + true, + false, + false, + false, + exch.Name) + return nil +} + // Start starts the Bitfinex go routine func (b *Bitfinex) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -30,76 +204,88 @@ func (b *Bitfinex) Start(wg *sync.WaitGroup) { // Run implements the Bitfinex wrapper func (b *Bitfinex) Run() { if b.Verbose { - log.Debugf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled())) - log.Debugf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) + log.Debugf(log.ExchangeSys, + "%s Websocket: %s.", + b.Name, + common.IsEnabled(b.Websocket.IsEnabled())) + b.PrintEnabledPairs() } - exchangeProducts, err := b.GetSymbols() + if !b.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := b.UpdateTradablePairs(false) if err != nil { - log.Errorf("%s Failed to get available symbols.\n", b.GetName()) - } else { - var newExchangeProducts currency.Pairs - for _, p := range exchangeProducts { - newExchangeProducts = append(newExchangeProducts, - currency.NewPairFromString(p)) - } - - err = b.UpdateCurrencies(newExchangeProducts, false, false) - if err != nil { - log.Errorf("%s Failed to update available symbols.\n", b.GetName()) - } + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", b.Name, err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (b *Bitfinex) FetchTradablePairs(asset asset.Item) ([]string, error) { + return b.GetSymbols() +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (b *Bitfinex) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := b.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (b *Bitfinex) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (b *Bitfinex) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - enabledPairs := b.GetEnabledCurrencies() - + enabledPairs := b.GetEnabledPairs(assetType) var pairs []string for x := range enabledPairs { + b.appendOptionalDelimiter(&enabledPairs[x]) pairs = append(pairs, "t"+enabledPairs[x].String()) } - - tickerNew, err := b.GetTickersV2(common.JoinStrings(pairs, ",")) + tickerNew, err := b.GetTickersV2(strings.Join(pairs, ",")) if err != nil { return tickerPrice, err } - - for x := range tickerNew { - newP := currency.NewPairFromStrings(tickerNew[x].Symbol[1:4], - tickerNew[x].Symbol[4:]) - - var tick ticker.Price - tick.Pair = newP - tick.Ask = tickerNew[x].Ask - tick.Bid = tickerNew[x].Bid - tick.Low = tickerNew[x].Low - tick.Last = tickerNew[x].Last - tick.Volume = tickerNew[x].Volume - tick.High = tickerNew[x].High - + for i := range tickerNew { + newP := tickerNew[i].Symbol[1:] // Remove the "t" prefix + tick := ticker.Price{ + Last: tickerNew[i].Last, + High: tickerNew[i].High, + Low: tickerNew[i].Low, + Bid: tickerNew[i].Bid, + Ask: tickerNew[i].Ask, + Volume: tickerNew[i].Volume, + Pair: currency.NewPairFromString(newP), + LastUpdated: tickerNew[i].Timestamp, + } err = ticker.ProcessTicker(b.Name, &tick, assetType) if err != nil { - return tickerPrice, err + log.Error(log.Ticker, err) } } + return ticker.GetTicker(b.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (b *Bitfinex) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tick, err := ticker.GetTicker(b.GetName(), p, ticker.Spot) +// FetchTicker returns the ticker for a currency pair +func (b *Bitfinex) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + b.appendOptionalDelimiter(&p) + tick, err := ticker.GetTicker(b.Name, p, asset.Spot) if err != nil { return b.UpdateTicker(p, assetType) } return tick, nil } -// GetOrderbookEx returns the orderbook for a currency pair -func (b *Bitfinex) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(b.GetName(), p, assetType) +// FetchOrderbook returns the orderbook for a currency pair +func (b *Bitfinex) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + b.appendOptionalDelimiter(&p) + ob, err := orderbook.Get(b.Name, p, assetType) if err != nil { return b.UpdateOrderbook(p, assetType) } @@ -107,7 +293,8 @@ func (b *Bitfinex) GetOrderbookEx(p currency.Pair, assetType string) (orderbook. } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (b *Bitfinex) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (b *Bitfinex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + b.appendOptionalDelimiter(&p) var orderBook orderbook.Base urlVals := url.Values{} urlVals.Set("limit_bids", "100") @@ -130,7 +317,7 @@ func (b *Bitfinex) UpdateOrderbook(p currency.Pair, assetType string) (orderbook } orderBook.Pair = p - orderBook.ExchangeName = b.GetName() + orderBook.ExchangeName = b.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -145,7 +332,8 @@ func (b *Bitfinex) UpdateOrderbook(p currency.Pair, assetType string) (orderbook // Bitfinex exchange func (b *Bitfinex) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = b.GetName() + response.Exchange = b.Name + accountBalance, err := b.GetAccountBalance() if err != nil { return response, err @@ -157,14 +345,14 @@ func (b *Bitfinex) GetAccountInfo() (exchange.AccountInfo, error) { {ID: "trading"}, } - for _, bal := range accountBalance { + for x := range accountBalance { for i := range Accounts { - if Accounts[i].ID == bal.Type { + if Accounts[i].ID == accountBalance[x].Type { Accounts[i].Currencies = append(Accounts[i].Currencies, exchange.AccountCurrencyInfo{ - CurrencyName: currency.NewCode(bal.Currency), - TotalValue: bal.Amount, - Hold: bal.Amount - bal.Available, + CurrencyName: currency.NewCode(accountBalance[x].Currency), + TotalValue: accountBalance[x].Amount, + Hold: accountBalance[x].Amount - accountBalance[x].Available, }) } } @@ -177,72 +365,106 @@ func (b *Bitfinex) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (b *Bitfinex) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (b *Bitfinex) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (b *Bitfinex) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (b *Bitfinex) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - var isBuying bool - - if side == exchange.BuyOrderSide { - isBuying = true +func (b *Bitfinex) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + err := o.Validate() + if err != nil { + return submitOrderResponse, err } + if b.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + submitOrderResponse.OrderID, err = b.WsNewOrder(&WsNewOrderRequest{ + CustomID: b.AuthenticatedWebsocketConn.GenerateMessageID(false), + Type: o.OrderType.String(), + Symbol: b.FormatExchangeCurrency(o.Pair, asset.Spot).String(), + Amount: o.Amount, + Price: o.Price, + }) + if err != nil { + return submitOrderResponse, err + } + } else { + var response Order + isBuying := o.OrderSide == order.Buy + b.appendOptionalDelimiter(&o.Pair) + response, err = b.NewOrder(o.Pair.String(), + o.Amount, + o.Price, + isBuying, + o.OrderType.String(), + false) + if err != nil { + return submitOrderResponse, err + } + if response.OrderID > 0 { + submitOrderResponse.OrderID = strconv.FormatInt(response.OrderID, 10) + } + if response.RemainingAmount == 0 { + submitOrderResponse.FullyMatched = true + } - response, err := b.NewOrder(p.String(), - amount, - price, - isBuying, - orderType.ToString(), - false) - - if response.OrderID > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response.OrderID) - } - - if err == nil { submitOrderResponse.IsOrderPlaced = true } - return submitOrderResponse, err } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (b *Bitfinex) ModifyOrder(action *exchange.ModifyOrder) (string, error) { - return "", common.ErrFunctionNotSupported +func (b *Bitfinex) ModifyOrder(action *order.Modify) (string, error) { + orderIDInt, err := strconv.ParseInt(action.OrderID, 10, 64) + if err != nil { + return action.OrderID, err + } + if b.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + if action.Side == order.Sell && action.Amount > 0 { + action.Amount = -1 * action.Amount + } + err = b.WsModifyOrder(&WsUpdateOrderRequest{ + OrderID: orderIDInt, + Price: action.Price, + Amount: action.Amount, + }) + return action.OrderID, err + } + return "", common.ErrNotYetImplemented } // CancelOrder cancels an order by its corresponding ID number -func (b *Bitfinex) CancelOrder(order *exchange.OrderCancellation) error { +func (b *Bitfinex) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) - if err != nil { return err } - - _, err = b.CancelExistingOrder(orderIDInt) - + if b.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + err = b.WsCancelOrder(orderIDInt) + } else { + _, err = b.CancelExistingOrder(orderIDInt) + } return err } // CancelAllOrders cancels all orders associated with a currency pair -func (b *Bitfinex) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - _, err := b.CancelAllExistingOrders() - return exchange.CancelAllOrdersResponse{}, err +func (b *Bitfinex) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + var err error + if b.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + err = b.WsCancelAllOrders() + } else { + _, err = b.CancelAllExistingOrders() + } + return order.CancelAllResponse{}, err } // GetOrderInfo returns information on a current open order -func (b *Bitfinex) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (b *Bitfinex) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -253,7 +475,8 @@ func (b *Bitfinex) GetDepositAddress(cryptocurrency currency.Code, accountID str return "", err } - resp, err := b.NewDeposit(method, accountID, 0) + var resp DepositResponse + resp, err = b.NewDeposit(method, accountID, 0) if err != nil { return "", err } @@ -262,7 +485,7 @@ func (b *Bitfinex) GetDepositAddress(cryptocurrency currency.Code, accountID str } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is submitted -func (b *Bitfinex) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bitfinex) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { withdrawalType := b.ConvertSymbolToWithdrawalType(withdrawRequest.Currency) // Bitfinex has support for three types, exchange, margin and deposit // As this is for trading, I've made the wrapper default 'exchange' @@ -281,12 +504,12 @@ func (b *Bitfinex) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.Withdra return "", errors.New("no withdrawID returned. Check order status") } - return fmt.Sprintf("%v", resp[0].WithdrawalID), err + return strconv.FormatInt(resp[0].WithdrawalID, 10), err } // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted // Returns comma delimited withdrawal IDs -func (b *Bitfinex) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bitfinex) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { withdrawalType := "wire" // Bitfinex has support for three types, exchange, margin and deposit // As this is for trading, I've made the wrapper default 'exchange' @@ -300,26 +523,25 @@ func (b *Bitfinex) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) return "", errors.New("no withdrawID returned. Check order status") } - var withdrawalSuccesses string - var withdrawalErrors string - for _, withdrawal := range resp { - if withdrawal.Status == "error" { - withdrawalErrors += fmt.Sprintf("%v ", withdrawal.Message) + var withdrawalSuccesses, withdrawalErrors strings.Builder + for x := range resp { + if resp[x].Status == "error" { + withdrawalErrors.WriteString(resp[x].Message + " ") } - if withdrawal.Status == "success" { - withdrawalSuccesses += fmt.Sprintf("%v,", withdrawal.WithdrawalID) + if resp[x].Status == "success" { + withdrawalSuccesses.WriteString(strconv.FormatInt(resp[x].WithdrawalID, 10) + ",") } } - if len(withdrawalErrors) > 0 { - return withdrawalSuccesses, errors.New(withdrawalErrors) + if withdrawalErrors.Len() > 0 { + return withdrawalSuccesses.String(), errors.New(withdrawalErrors.String()) } - return withdrawalSuccesses, nil + return withdrawalSuccesses.String(), nil } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is submitted // Returns comma delimited withdrawal IDs -func (b *Bitfinex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bitfinex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return b.WithdrawFiatFunds(withdrawRequest) } @@ -330,7 +552,7 @@ func (b *Bitfinex) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (b *Bitfinex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (b.APIKey == "" || b.APISecret == "") && // Todo check connection status + if !b.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -338,26 +560,28 @@ func (b *Bitfinex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error } // GetActiveOrders retrieves any orders that are active/open -func (b *Bitfinex) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var orders []exchange.OrderDetail +func (b *Bitfinex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { + var orders []order.Detail resp, err := b.GetOpenOrders() if err != nil { return nil, err } for i := range resp { - orderSide := exchange.OrderSide(strings.ToUpper(resp[i].Side)) + orderSide := order.Side(strings.ToUpper(resp[i].Side)) timestamp, err := strconv.ParseInt(resp[i].Timestamp, 10, 64) if err != nil { - log.Warnf("Unable to convert timestamp '%v', leaving blank", resp[i].Timestamp) + log.Warnf(log.ExchangeSys, + "Unable to convert timestamp '%s', leaving blank", + resp[i].Timestamp) } orderDate := time.Unix(timestamp, 0) - orderDetail := exchange.OrderDetail{ + orderDetail := order.Detail{ Amount: resp[i].OriginalAmount, OrderDate: orderDate, Exchange: b.Name, - ID: fmt.Sprintf("%v", resp[i].OrderID), + ID: strconv.FormatInt(resp[i].OrderID, 10), OrderSide: orderSide, Price: resp[i].Price, RemainingAmount: resp[i].RemainingAmount, @@ -367,57 +591,56 @@ func (b *Bitfinex) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) switch { case resp[i].IsLive: - orderDetail.Status = string(exchange.ActiveOrderStatus) + orderDetail.Status = order.Active case resp[i].IsCancelled: - orderDetail.Status = string(exchange.CancelledOrderStatus) + orderDetail.Status = order.Cancelled case resp[i].IsHidden: - orderDetail.Status = string(exchange.HiddenOrderStatus) + orderDetail.Status = order.Hidden default: - orderDetail.Status = string(exchange.UnknownOrderStatus) + orderDetail.Status = order.UnknownStatus } - // API docs discrepency. Example contains prefixed "exchange " + // API docs discrepancy. Example contains prefixed "exchange " // Return type suggests “market” / “limit” / “stop” / “trailing-stop” orderType := strings.Replace(resp[i].Type, "exchange ", "", 1) if orderType == "trailing-stop" { - orderDetail.OrderType = exchange.TrailingStopOrderType + orderDetail.OrderType = order.TrailingStop } else { - orderDetail.OrderType = exchange.OrderType(strings.ToUpper(orderType)) + orderDetail.OrderType = order.Type(strings.ToUpper(orderType)) } orders = append(orders, orderDetail) } - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (b *Bitfinex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var orders []exchange.OrderDetail +func (b *Bitfinex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + var orders []order.Detail resp, err := b.GetInactiveOrders() if err != nil { return nil, err } for i := range resp { - orderSide := exchange.OrderSide(strings.ToUpper(resp[i].Side)) + orderSide := order.Side(strings.ToUpper(resp[i].Side)) timestamp, err := strconv.ParseInt(resp[i].Timestamp, 10, 64) if err != nil { - log.Warnf("Unable to convert timestamp '%v', leaving blank", resp[i].Timestamp) + log.Warnf(log.ExchangeSys, "Unable to convert timestamp '%v', leaving blank", resp[i].Timestamp) } orderDate := time.Unix(timestamp, 0) - orderDetail := exchange.OrderDetail{ + orderDetail := order.Detail{ Amount: resp[i].OriginalAmount, OrderDate: orderDate, Exchange: b.Name, - ID: fmt.Sprintf("%v", resp[i].OrderID), + ID: strconv.FormatInt(resp[i].OrderID, 10), OrderSide: orderSide, Price: resp[i].Price, RemainingAmount: resp[i].RemainingAmount, @@ -427,38 +650,43 @@ func (b *Bitfinex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) switch { case resp[i].IsLive: - orderDetail.Status = string(exchange.ActiveOrderStatus) + orderDetail.Status = order.Active case resp[i].IsCancelled: - orderDetail.Status = string(exchange.CancelledOrderStatus) + orderDetail.Status = order.Cancelled case resp[i].IsHidden: - orderDetail.Status = string(exchange.HiddenOrderStatus) + orderDetail.Status = order.Hidden default: - orderDetail.Status = string(exchange.UnknownOrderStatus) + orderDetail.Status = order.UnknownStatus } // API docs discrepency. Example contains prefixed "exchange " // Return type suggests “market” / “limit” / “stop” / “trailing-stop” orderType := strings.Replace(resp[i].Type, "exchange ", "", 1) if orderType == "trailing-stop" { - orderDetail.OrderType = exchange.TrailingStopOrderType + orderDetail.OrderType = order.TrailingStop } else { - orderDetail.OrderType = exchange.OrderType(strings.ToUpper(orderType)) + orderDetail.OrderType = order.Type(strings.ToUpper(orderType)) } orders = append(orders, orderDetail) } - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + for i := range req.Currencies { + b.appendOptionalDelimiter(&req.Currencies[i]) + } + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing func (b *Bitfinex) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + for i := range channels { + b.appendOptionalDelimiter(&channels[i].Currency) + } b.Websocket.SubscribeToChannels(channels) return nil } @@ -466,6 +694,9 @@ func (b *Bitfinex) SubscribeToWebsocketChannels(channels []wshandler.WebsocketCh // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing func (b *Bitfinex) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + for i := range channels { + b.appendOptionalDelimiter(&channels[i].Currency) + } b.Websocket.RemoveSubscribedChannels(channels) return nil } @@ -479,3 +710,11 @@ func (b *Bitfinex) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, func (b *Bitfinex) AuthenticateWebsocket() error { return b.WsSendAuth() } + +// appendOptionalDelimiter ensures that a delimiter is present for long character currencies +func (b *Bitfinex) appendOptionalDelimiter(p *currency.Pair) { + if len(p.Quote.String()) > 3 || + len(p.Base.String()) > 3 { + p.Delimiter = ":" + } +} diff --git a/exchanges/bitflyer/README.md b/exchanges/bitflyer/README.md index 9326228d..34a95fec 100644 --- a/exchanges/bitflyer/README.md +++ b/exchanges/bitflyer/README.md @@ -47,22 +47,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bitflyer" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bitflyer" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } @@ -137,4 +137,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/bitflyer/bitflyer.go b/exchanges/bitflyer/bitflyer.go index 5348dd62..9c9a520a 100644 --- a/exchanges/bitflyer/bitflyer.go +++ b/exchanges/bitflyer/bitflyer.go @@ -6,16 +6,9 @@ import ( "net/http" "net/url" "strconv" - "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( @@ -77,78 +70,11 @@ type Bitflyer struct { exchange.Base } -// SetDefaults sets the basic defaults for Bitflyer -func (b *Bitflyer) SetDefaults() { - b.Name = "Bitflyer" - b.Enabled = false - b.Verbose = false - b.RESTPollingDelay = 10 - b.APIWithdrawPermissions = exchange.WithdrawCryptoViaWebsiteOnly | - exchange.AutoWithdrawFiat - b.RequestCurrencyPairFormat.Delimiter = "_" - b.RequestCurrencyPairFormat.Uppercase = true - b.ConfigCurrencyPairFormat.Delimiter = "_" - b.ConfigCurrencyPairFormat.Uppercase = true - b.AssetTypes = []string{ticker.Spot} - b.SupportsAutoPairUpdating = false - b.SupportsRESTTickerBatching = false - b.Requester = request.New(b.Name, - request.NewRateLimit(time.Minute, bitflyerAuthRate), - request.NewRateLimit(time.Minute, bitflyerUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - b.APIUrlDefault = japanURL - b.APIUrl = b.APIUrlDefault - b.APIUrlSecondaryDefault = chainAnalysis - b.APIUrlSecondary = b.APIUrlSecondaryDefault - b.Websocket = wshandler.New() -} - -// Setup takes in the supplied exchange configuration details and sets params -func (b *Bitflyer) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - b.SetEnabled(false) - } else { - b.Enabled = true - b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - b.SetHTTPClientTimeout(exch.HTTPTimeout) - b.SetHTTPClientUserAgent(exch.HTTPUserAgent) - b.RESTPollingDelay = exch.RESTPollingDelay - b.Verbose = exch.Verbose - b.HTTPDebugging = exch.HTTPDebugging - b.Websocket.SetWsStatusAndConnection(exch.Websocket) - b.BaseCurrencies = exch.BaseCurrencies - b.AvailablePairs = exch.AvailablePairs - b.EnabledPairs = exch.EnabledPairs - err := b.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = b.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = b.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = b.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = b.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - } -} - // GetLatestBlockCA returns the latest block information from bitflyer chain // analysis system func (b *Bitflyer) GetLatestBlockCA() (ChainAnalysisBlock, error) { var resp ChainAnalysisBlock - path := fmt.Sprintf("%s%s", b.APIUrlSecondary, latestBlock) - + path := b.API.Endpoints.URLSecondary + latestBlock return resp, b.SendHTTPRequest(path, &resp) } @@ -156,8 +82,7 @@ func (b *Bitflyer) GetLatestBlockCA() (ChainAnalysisBlock, error) { // analysis system func (b *Bitflyer) GetBlockCA(blockhash string) (ChainAnalysisBlock, error) { var resp ChainAnalysisBlock - path := fmt.Sprintf("%s%s%s", b.APIUrlSecondary, blockByBlockHash, blockhash) - + path := b.API.Endpoints.URLSecondary + blockByBlockHash + blockhash return resp, b.SendHTTPRequest(path, &resp) } @@ -165,8 +90,9 @@ func (b *Bitflyer) GetBlockCA(blockhash string) (ChainAnalysisBlock, error) { // analysis system func (b *Bitflyer) GetBlockbyHeightCA(height int64) (ChainAnalysisBlock, error) { var resp ChainAnalysisBlock - path := fmt.Sprintf("%s%s%s", b.APIUrlSecondary, blockByBlockHeight, strconv.FormatInt(height, 10)) - + path := b.API.Endpoints.URLSecondary + + blockByBlockHeight + + strconv.FormatInt(height, 10) return resp, b.SendHTTPRequest(path, &resp) } @@ -174,8 +100,7 @@ func (b *Bitflyer) GetBlockbyHeightCA(height int64) (ChainAnalysisBlock, error) // bitflyer chain analysis system func (b *Bitflyer) GetTransactionByHashCA(txHash string) (ChainAnalysisTransaction, error) { var resp ChainAnalysisTransaction - path := fmt.Sprintf("%s%s%s", b.APIUrlSecondary, transaction, txHash) - + path := b.API.Endpoints.URLSecondary + transaction + txHash return resp, b.SendHTTPRequest(path, &resp) } @@ -183,7 +108,7 @@ func (b *Bitflyer) GetTransactionByHashCA(txHash string) (ChainAnalysisTransacti // from bitflyer chain analysis system func (b *Bitflyer) GetAddressInfoCA(addressln string) (ChainAnalysisAddress, error) { var resp ChainAnalysisAddress - path := fmt.Sprintf("%s%s%s", b.APIUrlSecondary, address, addressln) + path := b.API.Endpoints.URLSecondary + address + addressln return resp, b.SendHTTPRequest(path, &resp) } @@ -191,7 +116,7 @@ func (b *Bitflyer) GetAddressInfoCA(addressln string) (ChainAnalysisAddress, err // GetMarkets returns market information func (b *Bitflyer) GetMarkets() ([]MarketInfo, error) { var resp []MarketInfo - path := fmt.Sprintf("%s%s", b.APIUrl, pubGetMarkets) + path := b.API.Endpoints.URL + pubGetMarkets return resp, b.SendHTTPRequest(path, &resp) } @@ -201,7 +126,7 @@ func (b *Bitflyer) GetOrderBook(symbol string) (Orderbook, error) { var resp Orderbook v := url.Values{} v.Set("product_code", symbol) - path := fmt.Sprintf("%s%s?%s", b.APIUrl, pubGetBoard, v.Encode()) + path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, pubGetBoard, v.Encode()) return resp, b.SendHTTPRequest(path, &resp) } @@ -211,8 +136,7 @@ func (b *Bitflyer) GetTicker(symbol string) (Ticker, error) { var resp Ticker v := url.Values{} v.Set("product_code", symbol) - path := fmt.Sprintf("%s%s?%s", b.APIUrl, pubGetTicker, v.Encode()) - + path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, pubGetTicker, v.Encode()) return resp, b.SendHTTPRequest(path, &resp) } @@ -221,7 +145,7 @@ func (b *Bitflyer) GetExecutionHistory(symbol string) ([]ExecutedTrade, error) { var resp []ExecutedTrade v := url.Values{} v.Set("product_code", symbol) - path := fmt.Sprintf("%s%s?%s", b.APIUrl, pubGetExecutionHistory, v.Encode()) + path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, pubGetExecutionHistory, v.Encode()) return resp, b.SendHTTPRequest(path, &resp) } @@ -230,7 +154,7 @@ func (b *Bitflyer) GetExecutionHistory(symbol string) ([]ExecutedTrade, error) { func (b *Bitflyer) GetExchangeStatus() (string, error) { resp := make(map[string]string) - path := fmt.Sprintf("%s%s", b.APIUrl, pubGetHealth) + path := b.API.Endpoints.URL + pubGetHealth err := b.SendHTTPRequest(path, &resp) if err != nil { @@ -257,8 +181,7 @@ func (b *Bitflyer) GetChats(fromDate string) ([]ChatLog, error) { var resp []ChatLog v := url.Values{} v.Set("from_date", fromDate) - path := fmt.Sprintf("%s%s?%s", b.APIUrl, pubGetChats, v.Encode()) - + path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, pubGetChats, v.Encode()) return resp, b.SendHTTPRequest(path, &resp) } @@ -398,7 +321,7 @@ func (b *Bitflyer) SendHTTPRequest(path string, result interface{}) error { // TODO: Fill out this function once API access is obtained func (b *Bitflyer) SendAuthHTTPRequest() { // nolint: gocritic headers := make(map[string]string) - // headers["ACCESS-KEY"] = b.APIKey + // headers["ACCESS-KEY"] = b.API.Credentials.Key // headers["ACCESS-TIMESTAMP"] = strconv.FormatInt(time.Now().UnixNano(), 10) } diff --git a/exchanges/bitflyer/bitflyer_test.go b/exchanges/bitflyer/bitflyer_test.go index 66fec3ac..31a87a5c 100644 --- a/exchanges/bitflyer/bitflyer_test.go +++ b/exchanges/bitflyer/bitflyer_test.go @@ -1,13 +1,16 @@ package bitflyer import ( + "log" + "os" "testing" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // Please supply your own keys here for due diligence testing @@ -19,30 +22,35 @@ const ( var b Bitflyer -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { b.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Bitflyer load config error", err) + } bitflyerConfig, err := cfg.GetExchangeConfig("Bitflyer") if err != nil { - t.Error("Test Failed - bitflyer Setup() init error") + log.Fatal("bitflyer Setup() init error") } - bitflyerConfig.AuthenticatedAPISupport = true - bitflyerConfig.APIKey = apiKey - bitflyerConfig.APISecret = apiSecret + bitflyerConfig.API.AuthenticatedSupport = true + bitflyerConfig.API.Credentials.Key = apiKey + bitflyerConfig.API.Credentials.Secret = apiSecret - b.Setup(&bitflyerConfig) + err = b.Setup(bitflyerConfig) + if err != nil { + log.Fatal("Bitflyer setup error", err) + } + + os.Exit(m.Run()) } func TestGetLatestBlockCA(t *testing.T) { t.Parallel() _, err := b.GetLatestBlockCA() if err != nil { - t.Error("test failed - Bitflyer - GetLatestBlockCA() error:", err) + t.Error("Bitflyer - GetLatestBlockCA() error:", err) } } @@ -50,7 +58,7 @@ func TestGetBlockCA(t *testing.T) { t.Parallel() _, err := b.GetBlockCA("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") if err != nil { - t.Error("test failed - Bitflyer - GetBlockCA() error:", err) + t.Error("Bitflyer - GetBlockCA() error:", err) } } @@ -58,7 +66,7 @@ func TestGetBlockbyHeightCA(t *testing.T) { t.Parallel() _, err := b.GetBlockbyHeightCA(0) if err != nil { - t.Error("test failed - Bitflyer - GetBlockbyHeightCA() error:", err) + t.Error("Bitflyer - GetBlockbyHeightCA() error:", err) } } @@ -66,7 +74,7 @@ func TestGetTransactionByHashCA(t *testing.T) { t.Parallel() _, err := b.GetTransactionByHashCA("0562d1f063cd4127053d838b165630445af5e480ceb24e1fd9ecea52903cb772") if err != nil { - t.Error("test failed - Bitflyer - GetTransactionByHashCA() error:", err) + t.Error("Bitflyer - GetTransactionByHashCA() error:", err) } } @@ -74,10 +82,10 @@ func TestGetAddressInfoCA(t *testing.T) { t.Parallel() v, err := b.GetAddressInfoCA("1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB") if err != nil { - t.Error("test failed - Bitflyer - GetAddressInfoCA() error:", err) + t.Error("Bitflyer - GetAddressInfoCA() error:", err) } if v.UnconfirmedBalance == 0 || v.ConfirmedBalance == 0 { - log.Warn("Donation wallet is empty :( - please consider donating") + t.Log("Donation wallet is empty :( - please consider donating") } } @@ -85,7 +93,7 @@ func TestGetMarkets(t *testing.T) { t.Parallel() _, err := b.GetMarkets() if err != nil { - t.Error("test failed - Bitflyer - GetMarkets() error:", err) + t.Error("Bitflyer - GetMarkets() error:", err) } } @@ -93,7 +101,7 @@ func TestGetOrderBook(t *testing.T) { t.Parallel() _, err := b.GetOrderBook("BTC_JPY") if err != nil { - t.Error("test failed - Bitflyer - GetOrderBook() error:", err) + t.Error("Bitflyer - GetOrderBook() error:", err) } } @@ -101,7 +109,7 @@ func TestGetTicker(t *testing.T) { t.Parallel() _, err := b.GetTicker("BTC_JPY") if err != nil { - t.Error("test failed - Bitflyer - GetTicker() error:", err) + t.Error("Bitflyer - GetTicker() error:", err) } } @@ -109,7 +117,7 @@ func TestGetExecutionHistory(t *testing.T) { t.Parallel() _, err := b.GetExecutionHistory("BTC_JPY") if err != nil { - t.Error("test failed - Bitflyer - GetExecutionHistory() error:", err) + t.Error("Bitflyer - GetExecutionHistory() error:", err) } } @@ -117,7 +125,7 @@ func TestGetExchangeStatus(t *testing.T) { t.Parallel() _, err := b.GetExchangeStatus() if err != nil { - t.Error("test failed - Bitflyer - GetExchangeStatus() error:", err) + t.Error("Bitflyer - GetExchangeStatus() error:", err) } } @@ -126,15 +134,15 @@ func TestCheckFXString(t *testing.T) { p := currency.NewPairDelimiter("FXBTC_JPY", "_") p = b.CheckFXString(p) if p.Base.String() != "FX_BTC" { - t.Error("test failed - Bitflyer - CheckFXString() error") + t.Error("Bitflyer - CheckFXString() error") } } -func TestGetTickerPrice(t *testing.T) { +func TestFetchTicker(t *testing.T) { t.Parallel() var p currency.Pair - currencies := b.GetAvailableCurrencies() + currencies := b.GetAvailablePairs(asset.Spot) for _, pair := range currencies { if pair.String() == "FXBTC_JPY" { p = pair @@ -142,9 +150,9 @@ func TestGetTickerPrice(t *testing.T) { } } - _, err := b.GetTickerPrice(p, b.AssetTypes[0]) + _, err := b.FetchTicker(p, asset.Spot) if err != nil { - t.Error("test failed - Bitflyer - GetTickerPrice() error", err) + t.Error("Bitflyer - FetchTicker() error", err) } } @@ -163,7 +171,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() b.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -175,15 +183,14 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - b.SetDefaults() - TestSetup(t) + t.Parallel() var feeBuilder = setFeeBuilder() - if apiKey != "" || apiSecret != "" { + if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) } // CryptocurrencyTradeFee High quantity @@ -191,7 +198,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -199,7 +206,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := b.GetFee(feeBuilder); resp != float64(0.1) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.1), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.1), resp) t.Error(err) } @@ -207,7 +214,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -215,7 +222,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } @@ -224,7 +231,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -233,7 +240,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.JPY if resp, err := b.GetFee(feeBuilder); resp != float64(324) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(324), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(324), resp) t.Error(err) } @@ -242,28 +249,24 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.JPY if resp, err := b.GetFee(feeBuilder); resp != float64(540) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(540), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(540), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - b.SetDefaults() + t.Parallel() expectedResult := exchange.AutoWithdrawFiatText + " & " + exchange.WithdrawCryptoViaWebsiteOnlyText - withdrawPermissions := b.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + t.Parallel() + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -275,11 +278,9 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + t.Parallel() + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -291,42 +292,40 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if b.APIKey != "" && b.APIKey != "Key" && - b.APISecret != "" && b.APISecret != "Secret" { - return true - } - return false + return b.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.LTC, - Quote: currency.BTC, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.LTC, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - _, err := b.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 1, "clientId") + _, err := b.SubmitOrder(orderSubmission) if err != common.ErrNotYetImplemented { t.Errorf("Expected 'Not Yet Implemented', received %v", err) } } func TestCancelExchangeOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -341,15 +340,13 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -364,19 +361,20 @@ func TestCancelAllExchangeOrders(t *testing.T) { } func TestWithdraw(t *testing.T) { - b.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", - } - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", + } + _, err := b.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) if err != common.ErrNotYetImplemented { t.Errorf("Expected 'Not Yet Implemented', received %v", err) @@ -384,21 +382,23 @@ func TestWithdraw(t *testing.T) { } func TestModifyOrder(t *testing.T) { - _, err := b.ModifyOrder(&exchange.ModifyOrder{}) + t.Parallel() + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := b.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdrawFiat(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := b.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrNotYetImplemented { @@ -407,14 +407,12 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := b.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrNotYetImplemented { diff --git a/exchanges/bitflyer/bitflyer_wrapper.go b/exchanges/bitflyer/bitflyer_wrapper.go index 16b89cdf..7bbce079 100644 --- a/exchanges/bitflyer/bitflyer_wrapper.go +++ b/exchanges/bitflyer/bitflyer_wrapper.go @@ -1,17 +1,112 @@ package bitflyer import ( + "strings" "sync" + "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (b *Bitflyer) GetDefaultConfig() (*config.ExchangeConfig, error) { + b.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = b.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = b.BaseCurrencies + + err := b.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if b.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = b.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for Bitflyer +func (b *Bitflyer) SetDefaults() { + b.Name = "Bitflyer" + b.Enabled = true + b.Verbose = true + b.API.CredentialsValidator.RequiresKey = true + b.API.CredentialsValidator.RequiresSecret = true + + b.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + asset.Futures, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "_", + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "_", + Uppercase: true, + }, + } + + b.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: false, + RESTCapabilities: protocol.Features{ + TickerFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + TradeFee: true, + FiatDepositFee: true, + FiatWithdrawalFee: true, + }, + WithdrawPermissions: exchange.WithdrawCryptoViaWebsiteOnly | + exchange.AutoWithdrawFiat, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + b.Requester = request.New(b.Name, + request.NewRateLimit(time.Minute, bitflyerAuthRate), + request.NewRateLimit(time.Minute, bitflyerUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + b.API.Endpoints.URLDefault = japanURL + b.API.Endpoints.URL = b.API.Endpoints.URLDefault + b.API.Endpoints.URLSecondaryDefault = chainAnalysis + b.API.Endpoints.URLSecondary = b.API.Endpoints.URLSecondaryDefault +} + +// Setup takes in the supplied exchange configuration details and sets params +func (b *Bitflyer) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + b.SetEnabled(false) + return nil + } + + return b.SetupDefaults(exch) +} + // Start starts the Bitflyer go routine func (b *Bitflyer) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -24,35 +119,63 @@ func (b *Bitflyer) Start(wg *sync.WaitGroup) { // Run implements the Bitflyer wrapper func (b *Bitflyer) Run() { if b.Verbose { - log.Debugf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled())) - log.Debugf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) + b.PrintEnabledPairs() } - /* nolint: gocritic - This has been disabled in master but enabled in engine due to multiple - asset support + if !b.GetEnabledFeatures().AutoPairUpdates { + return + } - marketInfo, err := b.GetMarkets() + err := b.UpdateTradablePairs(false) if err != nil { - log.Printf("%s Failed to get available symbols.\n", b.GetName()) - } else { - var exchangeProducts []string + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", b.Name, err) + } +} - for _, info := range marketInfo { - exchangeProducts = append(exchangeProducts, info.ProductCode) - } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (b *Bitflyer) FetchTradablePairs(assetType asset.Item) ([]string, error) { + pairs, err := b.GetMarkets() + if err != nil { + return nil, err + } - err = b.UpdateAvailableCurrencies(exchangeProducts, false) - if err != nil { - log.Printf("%s Failed to get config.\n", b.GetName()) + var products []string + for i := range pairs { + if pairs[i].Alias != "" && assetType == asset.Futures { + products = append(products, pairs[i].Alias) + } else if pairs[i].Alias == "" && + assetType == asset.Spot && + strings.Contains(pairs[i].ProductCode, + b.GetPairFormat(assetType, false).Delimiter) { + products = append(products, pairs[i].ProductCode) } } - */ + return products, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (b *Bitflyer) UpdateTradablePairs(forceUpdate bool) error { + for x := range b.CurrencyPairs.AssetTypes { + a := b.CurrencyPairs.AssetTypes[x] + pairs, err := b.FetchTradablePairs(a) + if err != nil { + return err + } + + err = b.UpdatePairs(currency.NewPairsFromStrings(pairs), + a, + false, + forceUpdate) + if err != nil { + return err + } + } + return nil } // UpdateTicker updates and returns the ticker for a currency pair -func (b *Bitflyer) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (b *Bitflyer) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price p = b.CheckFXString(p) @@ -65,11 +188,9 @@ func (b *Bitflyer) UpdateTicker(p currency.Pair, assetType string) (ticker.Price tickerPrice.Pair = p tickerPrice.Ask = tickerNew.BestAsk tickerPrice.Bid = tickerNew.BestBid - // tickerPrice.Low tickerPrice.Last = tickerNew.Last tickerPrice.Volume = tickerNew.Volume - // tickerPrice.High - err = ticker.ProcessTicker(b.GetName(), &tickerPrice, assetType) + err = ticker.ProcessTicker(b.Name, &tickerPrice, assetType) if err != nil { return tickerPrice, err } @@ -77,9 +198,9 @@ func (b *Bitflyer) UpdateTicker(p currency.Pair, assetType string) (ticker.Price return ticker.GetTicker(b.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (b *Bitflyer) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tick, err := ticker.GetTicker(b.GetName(), p, ticker.Spot) +// FetchTicker returns the ticker for a currency pair +func (b *Bitflyer) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tick, err := ticker.GetTicker(b.Name, p, assetType) if err != nil { return b.UpdateTicker(p, assetType) } @@ -88,16 +209,16 @@ func (b *Bitflyer) GetTickerPrice(p currency.Pair, assetType string) (ticker.Pri // CheckFXString upgrades currency pair if needed func (b *Bitflyer) CheckFXString(p currency.Pair) currency.Pair { - if common.StringContains(p.Base.String(), "FX") { + if strings.Contains(p.Base.String(), "FX") { p.Base = currency.FX_BTC return p } return p } -// GetOrderbookEx returns the orderbook for a currency pair -func (b *Bitflyer) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(b.GetName(), p, assetType) +// FetchOrderbook returns the orderbook for a currency pair +func (b *Bitflyer) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(b.Name, p, assetType) if err != nil { return b.UpdateOrderbook(p, assetType) } @@ -105,7 +226,7 @@ func (b *Bitflyer) GetOrderbookEx(p currency.Pair, assetType string) (orderbook. } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (b *Bitflyer) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (b *Bitflyer) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base p = b.CheckFXString(p) @@ -124,7 +245,7 @@ func (b *Bitflyer) UpdateOrderbook(p currency.Pair, assetType string) (orderbook } orderBook.Pair = p - orderBook.ExchangeName = b.GetName() + orderBook.ExchangeName = b.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -148,40 +269,36 @@ func (b *Bitflyer) GetFundingHistory() ([]exchange.FundHistory, error) { } // GetExchangeHistory returns historic trade data since exchange opening. -func (b *Bitflyer) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (b *Bitflyer) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (b *Bitflyer) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - - return submitOrderResponse, common.ErrNotYetImplemented +func (b *Bitflyer) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + return order.SubmitResponse{}, common.ErrNotYetImplemented } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (b *Bitflyer) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (b *Bitflyer) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (b *Bitflyer) CancelOrder(order *exchange.OrderCancellation) error { +func (b *Bitflyer) CancelOrder(order *order.Cancel) error { return common.ErrNotYetImplemented } // CancelAllOrders cancels all orders associated with a currency pair -func (b *Bitflyer) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { +func (b *Bitflyer) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { // TODO, implement BitFlyer API b.CancelAllExistingOrders() - return exchange.CancelAllOrdersResponse{}, common.ErrNotYetImplemented + return order.CancelAllResponse{}, common.ErrNotYetImplemented } // GetOrderInfo returns information on a current open order -func (b *Bitflyer) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (b *Bitflyer) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -192,19 +309,19 @@ func (b *Bitflyer) GetDepositAddress(cryptocurrency currency.Code, accountID str // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *Bitflyer) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bitflyer) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { return "", common.ErrNotYetImplemented } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (b *Bitflyer) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bitflyer) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrNotYetImplemented } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (b *Bitflyer) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bitflyer) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrNotYetImplemented } @@ -214,19 +331,19 @@ func (b *Bitflyer) GetWebsocket() (*wshandler.Websocket, error) { } // GetActiveOrders retrieves any orders that are active/open -func (b *Bitflyer) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (b *Bitflyer) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { return nil, common.ErrNotYetImplemented } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (b *Bitflyer) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (b *Bitflyer) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { return nil, common.ErrNotYetImplemented } // GetFeeByType returns an estimate of fee based on the type of transaction func (b *Bitflyer) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (b.APIKey == "" || b.APISecret == "") && // Todo check connection status + if !b.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } diff --git a/exchanges/bithumb/README.md b/exchanges/bithumb/README.md index f38f220b..052e307e 100644 --- a/exchanges/bithumb/README.md +++ b/exchanges/bithumb/README.md @@ -47,22 +47,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bithumb" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bithumb" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +130,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/bithumb/bithumb.go b/exchanges/bithumb/bithumb.go index 2d9e0c7e..d4fc20fc 100644 --- a/exchanges/bithumb/bithumb.go +++ b/exchanges/bithumb/bithumb.go @@ -10,16 +10,10 @@ import ( "reflect" "strconv" "strings" - "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( @@ -61,71 +55,6 @@ type Bithumb struct { exchange.Base } -// SetDefaults sets the basic defaults for Bithumb -func (b *Bithumb) SetDefaults() { - b.Name = "Bithumb" - b.Enabled = false - b.Verbose = false - b.RESTPollingDelay = 10 - b.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | - exchange.AutoWithdrawFiat - b.RequestCurrencyPairFormat.Delimiter = "" - b.RequestCurrencyPairFormat.Uppercase = true - b.ConfigCurrencyPairFormat.Delimiter = "" - b.ConfigCurrencyPairFormat.Uppercase = true - b.ConfigCurrencyPairFormat.Index = "KRW" - b.AssetTypes = []string{ticker.Spot} - b.SupportsAutoPairUpdating = true - b.SupportsRESTTickerBatching = true - b.Requester = request.New(b.Name, - request.NewRateLimit(time.Second, bithumbAuthRate), - request.NewRateLimit(time.Second, bithumbUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - b.APIUrlDefault = apiURL - b.APIUrl = b.APIUrlDefault - b.Websocket = wshandler.New() -} - -// Setup takes in the supplied exchange configuration details and sets params -func (b *Bithumb) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - b.SetEnabled(false) - } else { - b.Enabled = true - b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - b.SetHTTPClientTimeout(exch.HTTPTimeout) - b.SetHTTPClientUserAgent(exch.HTTPUserAgent) - b.RESTPollingDelay = exch.RESTPollingDelay - b.Verbose = exch.Verbose - b.HTTPDebugging = exch.HTTPDebugging - b.Websocket.SetWsStatusAndConnection(exch.Websocket) - b.BaseCurrencies = exch.BaseCurrencies - b.AvailablePairs = exch.AvailablePairs - b.EnabledPairs = exch.EnabledPairs - err := b.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = b.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = b.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = b.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = b.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - } -} - // GetTradablePairs returns a list of tradable currencies func (b *Bithumb) GetTradablePairs() ([]string, error) { result, err := b.GetAllTickers() @@ -145,11 +74,9 @@ func (b *Bithumb) GetTradablePairs() ([]string, error) { // symbol e.g. "btc" func (b *Bithumb) GetTicker(symbol string) (Ticker, error) { var response TickerResponse - path := fmt.Sprintf("%s%s%s", - b.APIUrl, - publicTicker, - strings.ToUpper(symbol)) - + path := b.API.Endpoints.URL + + publicTicker + + strings.ToUpper(symbol) err := b.SendHTTPRequest(path, &response) if err != nil { return response.Data, err @@ -165,8 +92,7 @@ func (b *Bithumb) GetTicker(symbol string) (Ticker, error) { // GetAllTickers returns all ticker information func (b *Bithumb) GetAllTickers() (map[string]Ticker, error) { var response TickersResponse - path := fmt.Sprintf("%s%s%s", b.APIUrl, publicTicker, "all") - + path := b.API.Endpoints.URL + publicTicker + "all" err := b.SendHTTPRequest(path, &response) if err != nil { return nil, err @@ -182,7 +108,7 @@ func (b *Bithumb) GetAllTickers() (map[string]Ticker, error) { continue } var newTicker Ticker - err := common.JSONDecode(v, &newTicker) + err := json.Unmarshal(v, &newTicker) if err != nil { return nil, err } @@ -196,7 +122,7 @@ func (b *Bithumb) GetAllTickers() (map[string]Ticker, error) { // symbol e.g. "btc" func (b *Bithumb) GetOrderBook(symbol string) (Orderbook, error) { response := Orderbook{} - path := fmt.Sprintf("%s%s%s", b.APIUrl, publicOrderBook, common.StringToUpper(symbol)) + path := b.API.Endpoints.URL + publicOrderBook + strings.ToUpper(symbol) err := b.SendHTTPRequest(path, &response) if err != nil { @@ -215,7 +141,9 @@ func (b *Bithumb) GetOrderBook(symbol string) (Orderbook, error) { // symbol e.g. "btc" func (b *Bithumb) GetTransactionHistory(symbol string) (TransactionHistory, error) { response := TransactionHistory{} - path := fmt.Sprintf("%s%s%s", b.APIUrl, publicTransactionHistory, common.StringToUpper(symbol)) + path := b.API.Endpoints.URL + + publicTransactionHistory + + strings.ToUpper(symbol) err := b.SendHTTPRequest(path, &response) if err != nil { @@ -266,7 +194,7 @@ func (b *Bithumb) GetAccountBalance(c string) (FullBalance, error) { // Added due to increasing of the usuable currencies on exchange, usually // without notificatation, so we dont need to update structs later on for tag, datum := range response.Data { - splitTag := common.SplitStrings(tag, "_") + splitTag := strings.Split(tag, "_") c := splitTag[len(splitTag)-1] var val float64 if reflect.TypeOf(datum).String() != "float64" { @@ -309,7 +237,7 @@ func (b *Bithumb) GetAccountBalance(c string) (FullBalance, error) { func (b *Bithumb) GetWalletAddress(currency string) (WalletAddressRes, error) { response := WalletAddressRes{} params := url.Values{} - params.Set("currency", common.StringToUpper(currency)) + params.Set("currency", strings.ToUpper(currency)) err := b.SendAuthenticatedHTTPRequest(privateWalletAdd, params, &response) if err != nil { @@ -361,7 +289,7 @@ func (b *Bithumb) GetOrders(orderID, transactionType, count, after, currency str } if len(currency) > 0 { - params.Set("currency", common.StringToUpper(currency)) + params.Set("currency", strings.ToUpper(currency)) } return response, @@ -387,9 +315,9 @@ func (b *Bithumb) PlaceTrade(orderCurrency, transactionType string, units float6 response := OrderPlace{} params := url.Values{} - params.Set("order_currency", common.StringToUpper(orderCurrency)) + params.Set("order_currency", strings.ToUpper(orderCurrency)) params.Set("Payment_currency", "KRW") - params.Set("type", common.StringToUpper(transactionType)) + params.Set("type", strings.ToUpper(transactionType)) params.Set("units", strconv.FormatFloat(units, 'f', -1, 64)) params.Set("price", strconv.FormatInt(price, 10)) @@ -402,9 +330,9 @@ func (b *Bithumb) ModifyTrade(orderID, orderCurrency, transactionType string, un response := OrderPlace{} params := url.Values{} - params.Set("order_currency", common.StringToUpper(orderCurrency)) + params.Set("order_currency", strings.ToUpper(orderCurrency)) params.Set("Payment_currency", "KRW") - params.Set("type", common.StringToUpper(transactionType)) + params.Set("type", strings.ToUpper(transactionType)) params.Set("units", strconv.FormatFloat(units, 'f', -1, 64)) params.Set("price", strconv.FormatInt(price, 10)) params.Set("order_id", orderID) @@ -423,9 +351,9 @@ func (b *Bithumb) GetOrderDetails(orderID, transactionType, currency string) (Or response := OrderDetails{} params := url.Values{} - params.Set("order_id", common.StringToUpper(orderID)) + params.Set("order_id", strings.ToUpper(orderID)) params.Set("type", transactionType) - params.Set("currency", common.StringToUpper(currency)) + params.Set("currency", strings.ToUpper(currency)) return response, b.SendAuthenticatedHTTPRequest(privateOrderDetail, params, &response) @@ -440,9 +368,9 @@ func (b *Bithumb) CancelTrade(transactionType, orderID, currency string) (Action response := ActionStatus{} params := url.Values{} - params.Set("order_id", common.StringToUpper(orderID)) - params.Set("type", common.StringToUpper(transactionType)) - params.Set("currency", common.StringToUpper(currency)) + params.Set("order_id", strings.ToUpper(orderID)) + params.Set("type", strings.ToUpper(transactionType)) + params.Set("currency", strings.ToUpper(currency)) return response, b.SendAuthenticatedHTTPRequest(privateCancelTrade, nil, &response) @@ -464,7 +392,7 @@ func (b *Bithumb) WithdrawCrypto(address, destination, currency string, units fl if len(destination) > 0 { params.Set("destination", destination) } - params.Set("currency", common.StringToUpper(currency)) + params.Set("currency", strings.ToUpper(currency)) params.Set("units", strconv.FormatFloat(units, 'f', -1, 64)) return response, @@ -506,7 +434,7 @@ func (b *Bithumb) MarketBuyOrder(currency string, units float64) (MarketBuy, err response := MarketBuy{} params := url.Values{} - params.Set("currency", common.StringToUpper(currency)) + params.Set("currency", strings.ToUpper(currency)) params.Set("units", strconv.FormatFloat(units, 'f', -1, 64)) return response, @@ -522,7 +450,7 @@ func (b *Bithumb) MarketSellOrder(currency string, units float64) (MarketSell, e response := MarketSell{} params := url.Values{} - params.Set("currency", common.StringToUpper(currency)) + params.Set("currency", strings.ToUpper(currency)) params.Set("units", strconv.FormatFloat(units, 'f', -1, 64)) return response, @@ -545,7 +473,7 @@ func (b *Bithumb) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to bithumb func (b *Bithumb) SendAuthenticatedHTTPRequest(path string, params url.Values, result interface{}) error { - if !b.AuthenticatedAPISupport { + if !b.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } @@ -558,14 +486,14 @@ func (b *Bithumb) SendAuthenticatedHTTPRequest(path string, params url.Values, r params.Set("endpoint", path) payload := params.Encode() hmacPayload := path + string(0) + payload + string(0) + n - hmac := common.GetHMAC(common.HashSHA512, + hmac := crypto.GetHMAC(crypto.HashSHA512, []byte(hmacPayload), - []byte(b.APISecret)) - hmacStr := common.HexEncodeToString(hmac) + []byte(b.API.Credentials.Secret)) + hmacStr := crypto.HexEncodeToString(hmac) headers := make(map[string]string) - headers["Api-Key"] = b.APIKey - headers["Api-Sign"] = common.Base64Encode([]byte(hmacStr)) + headers["Api-Key"] = b.API.Credentials.Key + headers["Api-Sign"] = crypto.Base64Encode([]byte(hmacStr)) headers["Api-Nonce"] = n headers["Content-Type"] = "application/x-www-form-urlencoded" @@ -577,7 +505,7 @@ func (b *Bithumb) SendAuthenticatedHTTPRequest(path string, params url.Values, r }{} err := b.SendPayload(http.MethodPost, - b.APIUrl+path, + b.API.Endpoints.URL+path, headers, bytes.NewBufferString(payload), &intermediary, @@ -590,7 +518,7 @@ func (b *Bithumb) SendAuthenticatedHTTPRequest(path string, params url.Values, r return err } - err = common.JSONDecode(intermediary, &errCapture) + err = json.Unmarshal(intermediary, &errCapture) if err == nil { if errCapture.Status != "" && errCapture.Status != noError { return fmt.Errorf("sendAuthenticatedAPIRequest error code: %s message:%s", @@ -599,7 +527,7 @@ func (b *Bithumb) SendAuthenticatedHTTPRequest(path string, params url.Values, r } } - return common.JSONDecode(intermediary, result) + return json.Unmarshal(intermediary, result) } // GetFee returns an estimate of fee based on type of transaction diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index 678e5fd4..b2331ba2 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -1,12 +1,15 @@ package bithumb import ( + "log" + "os" "testing" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // Please supply your own keys here for due diligence testing @@ -14,42 +17,48 @@ const ( apiKey = "" apiSecret = "" canManipulateRealOrders = false + testCurrency = "btc" ) var b Bithumb -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { b.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Bithumb load config error", err) + } bitConfig, err := cfg.GetExchangeConfig("Bithumb") if err != nil { - t.Error("Test Failed - Bithumb Setup() init error") + log.Fatal("Bithumb Setup() init error") } - bitConfig.AuthenticatedAPISupport = true - bitConfig.APIKey = apiKey - bitConfig.APISecret = apiSecret + bitConfig.API.AuthenticatedSupport = true + bitConfig.API.Credentials.Key = apiKey + bitConfig.API.Credentials.Secret = apiSecret - b.Setup(&bitConfig) + err = b.Setup(bitConfig) + if err != nil { + log.Fatal("Bithumb setup error", err) + } + + os.Exit(m.Run()) } func TestGetTradablePairs(t *testing.T) { t.Parallel() _, err := b.GetTradablePairs() if err != nil { - t.Error("test failed - Bithumb GetTradablePairs() error", err) + t.Error("Bithumb GetTradablePairs() error", err) } } func TestGetTicker(t *testing.T) { t.Parallel() - _, err := b.GetTicker("btc") + _, err := b.GetTicker(testCurrency) if err != nil { - t.Error("test failed - Bithumb GetTicker() error", err) + t.Error("Bithumb GetTicker() error", err) } } @@ -57,47 +66,47 @@ func TestGetAllTickers(t *testing.T) { t.Parallel() _, err := b.GetAllTickers() if err != nil { - t.Error("test failed - Bithumb GetAllTickers() error", err) + t.Error("Bithumb GetAllTickers() error", err) } } func TestGetOrderBook(t *testing.T) { t.Parallel() - _, err := b.GetOrderBook("btc") + _, err := b.GetOrderBook(testCurrency) if err != nil { - t.Error("test failed - Bithumb GetOrderBook() error", err) + t.Error("Bithumb GetOrderBook() error", err) } } func TestGetTransactionHistory(t *testing.T) { t.Parallel() - _, err := b.GetTransactionHistory("btc") + _, err := b.GetTransactionHistory(testCurrency) if err != nil { - t.Error("test failed - Bithumb GetTransactionHistory() error", err) + t.Error("Bithumb GetTransactionHistory() error", err) } } func TestGetAccountBalance(t *testing.T) { t.Parallel() - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { t.Skip() } - _, err := b.GetAccountBalance("BTC") + _, err := b.GetAccountBalance(testCurrency) if err == nil { - t.Error("test failed - Bithumb GetAccountBalance() error", err) + t.Error("Bithumb GetAccountBalance() Expected error") } } func TestGetWalletAddress(t *testing.T) { - if apiKey == "" || apiSecret == "" { + t.Parallel() + if !areTestAPIKeysSet() { t.Skip() } - t.Parallel() _, err := b.GetWalletAddress("") if err == nil { - t.Error("test failed - Bithumb GetWalletAddress() error", err) + t.Error("Bithumb GetWalletAddress() Expected error") } } @@ -105,15 +114,15 @@ func TestGetLastTransaction(t *testing.T) { t.Parallel() _, err := b.GetLastTransaction() if err == nil { - t.Error("test failed - Bithumb GetLastTransaction() error", err) + t.Error("Bithumb GetLastTransaction() Expected error") } } func TestGetOrders(t *testing.T) { t.Parallel() - _, err := b.GetOrders("1337", "bid", "100", "", "BTC") + _, err := b.GetOrders("1337", order.Bid.Lower(), "100", "", testCurrency) if err == nil { - t.Error("test failed - Bithumb GetOrders() error", err) + t.Error("Bithumb GetOrders() Expected error") } } @@ -121,23 +130,23 @@ func TestGetUserTransactions(t *testing.T) { t.Parallel() _, err := b.GetUserTransactions() if err == nil { - t.Error("test failed - Bithumb GetUserTransactions() error", err) + t.Error("Bithumb GetUserTransactions() Expected error") } } func TestPlaceTrade(t *testing.T) { t.Parallel() - _, err := b.PlaceTrade("btc", "bid", 0, 0) + _, err := b.PlaceTrade(testCurrency, order.Bid.Lower(), 0, 0) if err == nil { - t.Error("test failed - Bithumb PlaceTrade() error", err) + t.Error("Bithumb PlaceTrade() Expected error") } } func TestGetOrderDetails(t *testing.T) { t.Parallel() - _, err := b.GetOrderDetails("1337", "bid", "btc") + _, err := b.GetOrderDetails("1337", order.Bid.Lower(), testCurrency) if err == nil { - t.Error("test failed - Bithumb GetOrderDetails() error", err) + t.Error("Bithumb GetOrderDetails() Expected error") } } @@ -145,7 +154,7 @@ func TestCancelTrade(t *testing.T) { t.Parallel() _, err := b.CancelTrade("", "", "") if err == nil { - t.Error("test failed - Bithumb CancelTrade() error", err) + t.Error("Bithumb CancelTrade() Expected error") } } @@ -153,18 +162,18 @@ func TestWithdrawCrypto(t *testing.T) { t.Parallel() _, err := b.WithdrawCrypto("LQxiDhKU7idKiWQhx4ALKYkBx8xKEQVxJR", "", "ltc", 0) if err == nil { - t.Error("test failed - Bithumb WithdrawCrypto() error", err) + t.Error("Bithumb WithdrawCrypto() Expected error") } } func TestRequestKRWDepositDetails(t *testing.T) { t.Parallel() - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { t.Skip() } _, err := b.RequestKRWDepositDetails() if err == nil { - t.Error("test failed - Bithumb RequestKRWDepositDetails() error", err) + t.Error("Bithumb RequestKRWDepositDetails() Expected error") } } @@ -172,23 +181,23 @@ func TestRequestKRWWithdraw(t *testing.T) { t.Parallel() _, err := b.RequestKRWWithdraw("102_bank", "1337", 1000) if err == nil { - t.Error("test failed - Bithumb RequestKRWWithdraw() error", err) + t.Error("Bithumb RequestKRWWithdraw() Expected error") } } func TestMarketBuyOrder(t *testing.T) { t.Parallel() - _, err := b.MarketBuyOrder("btc", 0) + _, err := b.MarketBuyOrder(testCurrency, 0) if err == nil { - t.Error("test failed - Bithumb MarketBuyOrder() error", err) + t.Error("Bithumb MarketBuyOrder() Expected error") } } func TestMarketSellOrder(t *testing.T) { t.Parallel() - _, err := b.MarketSellOrder("btc", 0) + _, err := b.MarketSellOrder(testCurrency, 0) if err == nil { - t.Error("test failed - Bithumb MarketSellOrder() error", err) + t.Error("Bithumb MarketSellOrder() Expected error") } } @@ -205,7 +214,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() b.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -217,14 +226,11 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - b.SetDefaults() - TestSetup(t) var feeBuilder = setFeeBuilder() - // CryptocurrencyTradeFee Basic if resp, err := b.GetFee(feeBuilder); resp != float64(0.0025) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0025), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0025), resp) } // CryptocurrencyTradeFee High quantity @@ -232,7 +238,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := b.GetFee(feeBuilder); resp != float64(2500) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2500), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2500), resp) t.Error(err) } @@ -240,7 +246,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := b.GetFee(feeBuilder); resp != float64(0.0025) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0025), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0025), resp) t.Error(err) } @@ -248,7 +254,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -256,7 +262,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := b.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) t.Error(err) } @@ -264,7 +270,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -273,7 +279,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -282,29 +288,25 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - b.SetDefaults() + t.Parallel() expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.AutoWithdrawFiatText - withdrawPermissions := b.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, - OrderSide: exchange.SellOrderSide, + t.Parallel() + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, + OrderSide: order.Sell, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -316,11 +318,9 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + t.Parallel() + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -334,27 +334,27 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if b.APIKey != "" && b.APIKey != "Key" && - b.APISecret != "" && b.APISecret != "Secret" { - return true - } - return false + return b.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.BTC, - Quote: currency.LTC, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.LTC, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := b.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 1, "clientId") + response, err := b.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -363,16 +363,13 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -389,16 +386,13 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -414,50 +408,53 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel order: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestGetAccountInfo(t *testing.T) { t.Parallel() - if apiKey != "" || apiSecret != "" { + if areTestAPIKeysSet() { _, err := b.GetAccountInfo() if err != nil { - t.Error("test failed - Bithumb GetAccountInfo() error", err) + t.Error("Bithumb GetAccountInfo() error", err) } } else { _, err := b.GetAccountInfo() if err == nil { - t.Error("test failed - Bithumb GetAccountInfo() error") + t.Error("Bithumb GetAccountInfo() Expected error") } } } func TestModifyOrder(t *testing.T) { + t.Parallel() curr := currency.NewPairFromString("BTCUSD") - _, err := b.ModifyOrder(&exchange.ModifyOrder{OrderID: "1337", + _, err := b.ModifyOrder(&order.Modify{ + OrderID: "1337", Price: 100, Amount: 1000, - OrderSide: exchange.SellOrderSide, + Side: order.Sell, CurrencyPair: curr}) if err == nil { - t.Error("Test Failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - b.SetDefaults() - TestSetup(t) + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } _, err := b.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) @@ -470,19 +467,19 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.KRW, - Description: "WITHDRAW IT ALL", + var withdrawFiatRequest = exchange.FiatWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.USD, + Description: "WITHDRAW IT ALL", + }, BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, + BankAccountNumber: "12345", BankCode: 123, BankAddress: "123 Fake St", BankCity: "Tarry Town", @@ -504,15 +501,12 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - b.SetDefaults() - TestSetup(t) - + t.Parallel() if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := b.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -520,15 +514,16 @@ func TestWithdrawInternationalBank(t *testing.T) { } func TestGetDepositAddress(t *testing.T) { - if apiKey != "" && apiSecret != "" { + t.Parallel() + if areTestAPIKeysSet() { _, err := b.GetDepositAddress(currency.BTC, "") if err != nil { - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) } } else { _, err := b.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error cannot be nil") + t.Error("GetDepositAddress() error cannot be nil") } } } diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index a1ffc127..73dc3c3f 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -9,15 +9,118 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) -// Start starts the OKEX go routine +// GetDefaultConfig returns a default exchange config +func (b *Bithumb) GetDefaultConfig() (*config.ExchangeConfig, error) { + b.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = b.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = b.BaseCurrencies + + err := b.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if b.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = b.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for Bithumb +func (b *Bithumb) SetDefaults() { + b.Name = "Bithumb" + b.Enabled = true + b.Verbose = true + b.API.CredentialsValidator.RequiresKey = true + b.API.CredentialsValidator.RequiresSecret = true + + b.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Index: "KRW", + }, + } + + b.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + CryptoWithdrawal: true, + FiatDeposit: true, + FiatWithdraw: true, + GetOrder: true, + CancelOrder: true, + SubmitOrder: true, + ModifyOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + TradeFee: true, + FiatWithdrawalFee: true, + CryptoDepositFee: true, + CryptoWithdrawalFee: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.AutoWithdrawFiat, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + b.Requester = request.New(b.Name, + request.NewRateLimit(time.Second, bithumbAuthRate), + request.NewRateLimit(time.Second, bithumbUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + b.API.Endpoints.URLDefault = apiURL + b.API.Endpoints.URL = b.API.Endpoints.URLDefault +} + +// Setup takes in the supplied exchange configuration details and sets params +func (b *Bithumb) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + b.SetEnabled(false) + return nil + } + + return b.SetupDefaults(exch) +} + +// Start starts the Bithumb go routine func (b *Bithumb) Start(wg *sync.WaitGroup) { wg.Add(1) go func() { @@ -26,33 +129,24 @@ func (b *Bithumb) Start(wg *sync.WaitGroup) { }() } -// Run implements the OKEX wrapper +// Run implements the Bithumb wrapper func (b *Bithumb) Run() { if b.Verbose { - log.Debugf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()), b.WebsocketURL) - log.Debugf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) + b.PrintEnabledPairs() } - exchangeProducts, err := b.GetTradingPairs() - if err != nil { - log.Errorf("%s Failed to get available symbols.\n", b.GetName()) - } else { - var newExchangeProducts currency.Pairs - for _, p := range exchangeProducts { - newExchangeProducts = append(newExchangeProducts, - currency.NewPairFromString(p)) - } + if !b.GetEnabledFeatures().AutoPairUpdates { + return + } - err = b.UpdateCurrencies(newExchangeProducts, false, false) - if err != nil { - log.Errorf("%s Failed to update available symbols.\n", b.GetName()) - } + err := b.UpdateTradablePairs(false) + if err != nil { + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", b.Name, err) } } -// GetTradingPairs gets the available trading currencies -func (b *Bithumb) GetTradingPairs() ([]string, error) { +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (b *Bithumb) FetchTradablePairs(asset asset.Item) ([]string, error) { currencies, err := b.GetTradablePairs() if err != nil { return nil, err @@ -65,70 +159,93 @@ func (b *Bithumb) GetTradingPairs() ([]string, error) { return currencies, nil } -// UpdateTicker updates and returns the ticker for a currency pair -func (b *Bithumb) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { - var tickerPrice ticker.Price +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (b *Bithumb) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := b.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + +// UpdateTicker updates and returns the ticker for a currency pair +func (b *Bithumb) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + var tickerPrice ticker.Price tickers, err := b.GetAllTickers() if err != nil { return tickerPrice, err } - - for _, x := range b.GetEnabledCurrencies() { - currency := x.Base.String() - var tp ticker.Price - tp.Pair = x - tp.Low = tickers[currency].MinPrice - tp.Last = tickers[currency].ClosingPrice - tp.Volume = tickers[currency].UnitsTraded24Hr - tp.High = tickers[currency].MaxPrice - + pairs := b.GetEnabledPairs(assetType) + for i := range pairs { + curr := pairs[i].Base.String() + t, ok := tickers[curr] + if !ok { + continue + } + tp := ticker.Price{ + High: t.MaxPrice, + Low: t.MinPrice, + Volume: t.UnitsTraded24Hr, + Open: t.OpeningPrice, + Close: t.ClosingPrice, + Pair: pairs[i], + } err = ticker.ProcessTicker(b.Name, &tp, assetType) if err != nil { - return tickerPrice, err + log.Error(log.Ticker, err) } } return ticker.GetTicker(b.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (b *Bithumb) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (b *Bithumb) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(b.Name, p, assetType) if err != nil { return b.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (b *Bithumb) GetOrderbookEx(currency currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(b.GetName(), currency, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (b *Bithumb) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(b.Name, p, assetType) if err != nil { - return b.UpdateOrderbook(currency, assetType) + return b.UpdateOrderbook(p, assetType) } return ob, nil } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (b *Bithumb) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (b *Bithumb) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - currency := p.Base.String() + curr := p.Base.String() - orderbookNew, err := b.GetOrderBook(currency) + orderbookNew, err := b.GetOrderBook(curr) if err != nil { return orderBook, err } - for _, bids := range orderbookNew.Data.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: bids.Quantity, Price: bids.Price}) + for i := range orderbookNew.Data.Bids { + orderBook.Bids = append(orderBook.Bids, + orderbook.Item{ + Amount: orderbookNew.Data.Bids[i].Quantity, + Price: orderbookNew.Data.Bids[i].Price, + }) } - for _, asks := range orderbookNew.Data.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: asks.Quantity, Price: asks.Price}) + for i := range orderbookNew.Data.Asks { + orderBook.Asks = append(orderBook.Asks, + orderbook.Item{ + Amount: orderbookNew.Data.Asks[i].Quantity, + Price: orderbookNew.Data.Asks[i].Price, + }) } orderBook.Pair = p - orderBook.ExchangeName = b.GetName() + orderBook.ExchangeName = b.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -167,57 +284,61 @@ func (b *Bithumb) GetAccountInfo() (exchange.AccountInfo, error) { Currencies: exchangeBalances, }) - info.Exchange = b.GetName() + info.Exchange = b.Name return info, nil } // GetFundingHistory returns funding history, deposits and // withdrawals func (b *Bithumb) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (b *Bithumb) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (b *Bithumb) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order // TODO: Fill this out to support limit orders -func (b *Bithumb) SubmitOrder(p currency.Pair, side exchange.OrderSide, _ exchange.OrderType, amount, _ float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - var err error +func (b *Bithumb) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } + var orderID string - if side == exchange.BuyOrderSide { + var err error + if s.OrderSide == order.Buy { var result MarketBuy - result, err = b.MarketBuyOrder(p.Base.String(), amount) + result, err = b.MarketBuyOrder(s.Pair.Base.String(), s.Amount) + if err != nil { + return submitOrderResponse, err + } orderID = result.OrderID - } else if side == exchange.SellOrderSide { + } else if s.OrderSide == order.Sell { var result MarketSell - result, err = b.MarketSellOrder(p.Base.String(), amount) + result, err = b.MarketSellOrder(s.Pair.Base.String(), s.Amount) + if err != nil { + return submitOrderResponse, err + } orderID = result.OrderID } - if orderID != "" { - submitOrderResponse.OrderID = fmt.Sprintf("%v", orderID) + submitOrderResponse.OrderID = orderID + submitOrderResponse.FullyMatched = true } + submitOrderResponse.IsOrderPlaced = true - if err == nil { - submitOrderResponse.IsOrderPlaced = true - } - - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (b *Bithumb) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (b *Bithumb) ModifyOrder(action *order.Modify) (string, error) { order, err := b.ModifyTrade(action.OrderID, action.CurrencyPair.Base.String(), - common.StringToLower(action.OrderSide.ToString()), + action.Side.Lower(), action.Amount, int64(action.Price)) @@ -229,26 +350,27 @@ func (b *Bithumb) ModifyOrder(action *exchange.ModifyOrder) (string, error) { } // CancelOrder cancels an order by its corresponding ID number -func (b *Bithumb) CancelOrder(order *exchange.OrderCancellation) error { - _, err := b.CancelTrade(order.Side.ToString(), +func (b *Bithumb) CancelOrder(order *order.Cancel) error { + _, err := b.CancelTrade(order.Side.String(), order.OrderID, order.CurrencyPair.Base.String()) return err } // CancelAllOrders cancels all orders associated with a currency pair -func (b *Bithumb) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (b *Bithumb) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } var allOrders []OrderData - for _, currency := range b.GetEnabledCurrencies() { + currs := b.GetEnabledPairs(asset.Spot) + for i := range currs { orders, err := b.GetOrders("", - orderCancellation.Side.ToString(), + orderCancellation.Side.String(), "100", "", - currency.Base.String()) + currs[i].Base.String()) if err != nil { return cancelAllOrdersResponse, err } @@ -256,11 +378,11 @@ func (b *Bithumb) CancelAllOrders(orderCancellation *exchange.OrderCancellation) } for i := range allOrders { - _, err := b.CancelTrade(orderCancellation.Side.ToString(), + _, err := b.CancelTrade(orderCancellation.Side.String(), allOrders[i].OrderID, orderCancellation.CurrencyPair.Base.String()) if err != nil { - cancelAllOrdersResponse.OrderStatus[allOrders[i].OrderID] = err.Error() + cancelAllOrdersResponse.Status[allOrders[i].OrderID] = err.Error() } } @@ -268,8 +390,8 @@ func (b *Bithumb) CancelAllOrders(orderCancellation *exchange.OrderCancellation) } // GetOrderInfo returns information on a current open order -func (b *Bithumb) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (b *Bithumb) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -285,24 +407,26 @@ func (b *Bithumb) GetDepositAddress(cryptocurrency currency.Code, _ string) (str // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *Bithumb) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { - _, err := b.WithdrawCrypto(withdrawRequest.Address, withdrawRequest.AddressTag, withdrawRequest.Currency.String(), withdrawRequest.Amount) +func (b *Bithumb) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { + _, err := b.WithdrawCrypto(withdrawRequest.Address, + withdrawRequest.AddressTag, + withdrawRequest.Currency.String(), + withdrawRequest.Amount) return "", err } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (b *Bithumb) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bithumb) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { if math.Mod(withdrawRequest.Amount, 1) != 0 { return "", errors.New("currency KRW does not support decimal places") } if withdrawRequest.Currency != currency.KRW { return "", errors.New("only KRW is supported") } - bankDetails := fmt.Sprintf("%v_%v", withdrawRequest.BankCode, withdrawRequest.BankName) - bankAccountNumber := strconv.FormatFloat(withdrawRequest.BankAccountNumber, 'f', -1, 64) - withdrawAmountInt := int64(withdrawRequest.Amount) - resp, err := b.RequestKRWWithdraw(bankDetails, bankAccountNumber, withdrawAmountInt) + bankDetails := strconv.FormatFloat(withdrawRequest.BankCode, 'f', -1, 64) + + "_" + withdrawRequest.BankName + resp, err := b.RequestKRWWithdraw(bankDetails, withdrawRequest.BankAccountNumber, int64(withdrawRequest.Amount)) if err != nil { return "", err } @@ -314,7 +438,7 @@ func (b *Bithumb) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) ( } // WithdrawFiatFundsToInternationalBank is not supported as Bithumb only withdraws KRW to South Korean banks -func (b *Bithumb) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bithumb) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -325,7 +449,7 @@ func (b *Bithumb) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (b *Bithumb) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (b.APIKey == "" || b.APISecret == "") && // Todo check connection status + if !b.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -333,8 +457,8 @@ func (b *Bithumb) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) } // GetActiveOrders retrieves any orders that are active/open -func (b *Bithumb) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var orders []exchange.OrderDetail +func (b *Bithumb) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { + var orders []order.Detail resp, err := b.GetOrders("", "", "1000", "", "") if err != nil { return nil, err @@ -346,40 +470,38 @@ func (b *Bithumb) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ( } orderDate := time.Unix(resp.Data[i].OrderDate, 0) - orderDetail := exchange.OrderDetail{ + orderDetail := order.Detail{ Amount: resp.Data[i].Units, Exchange: b.Name, ID: resp.Data[i].OrderID, OrderDate: orderDate, Price: resp.Data[i].Price, RemainingAmount: resp.Data[i].UnitsRemaining, - Status: string(exchange.ActiveOrderStatus), + Status: order.Active, CurrencyPair: currency.NewPairWithDelimiter(resp.Data[i].OrderCurrency, resp.Data[i].PaymentCurrency, - b.ConfigCurrencyPairFormat.Delimiter), + b.GetPairFormat(asset.Spot, false).Delimiter), } if resp.Data[i].Type == "bid" { - orderDetail.OrderSide = exchange.BuyOrderSide + orderDetail.OrderSide = order.Buy } else if resp.Data[i].Type == "ask" { - orderDetail.OrderSide = exchange.SellOrderSide + orderDetail.OrderSide = order.Sell } orders = append(orders, orderDetail) } - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (b *Bithumb) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var orders []exchange.OrderDetail +func (b *Bithumb) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + var orders []order.Detail resp, err := b.GetOrders("", "", "1000", "", "") if err != nil { return nil, err @@ -391,7 +513,7 @@ func (b *Bithumb) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( } orderDate := time.Unix(resp.Data[i].OrderDate, 0) - orderDetail := exchange.OrderDetail{ + orderDetail := order.Detail{ Amount: resp.Data[i].Units, Exchange: b.Name, ID: resp.Data[i].OrderID, @@ -400,23 +522,21 @@ func (b *Bithumb) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( RemainingAmount: resp.Data[i].UnitsRemaining, CurrencyPair: currency.NewPairWithDelimiter(resp.Data[i].OrderCurrency, resp.Data[i].PaymentCurrency, - b.ConfigCurrencyPairFormat.Delimiter), + b.GetPairFormat(asset.Spot, false).Delimiter), } if resp.Data[i].Type == "bid" { - orderDetail.OrderSide = exchange.BuyOrderSide + orderDetail.OrderSide = order.Buy } else if resp.Data[i].Type == "ask" { - orderDetail.OrderSide = exchange.SellOrderSide + orderDetail.OrderSide = order.Sell } orders = append(orders, orderDetail) } - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } diff --git a/exchanges/bitmex/README.md b/exchanges/bitmex/README.md index ac88462e..d77a9fed 100644 --- a/exchanges/bitmex/README.md +++ b/exchanges/bitmex/README.md @@ -47,22 +47,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bitmex" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bitmex" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +130,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/bitmex/bitmex.go b/exchanges/bitmex/bitmex.go index 80c71c6f..5a886b4d 100644 --- a/exchanges/bitmex/bitmex.go +++ b/exchanges/bitmex/bitmex.go @@ -9,14 +9,10 @@ import ( "strings" "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) // Bitmex is the overarching type across this package @@ -111,107 +107,6 @@ const ( ContractUpsideProfit ) -// SetDefaults sets the basic defaults for Bitmex -func (b *Bitmex) SetDefaults() { - b.Name = "Bitmex" - b.Enabled = false - b.Verbose = false - b.RESTPollingDelay = 10 - b.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithAPIPermission | - exchange.WithdrawCryptoWithEmail | - exchange.WithdrawCryptoWith2FA | - exchange.NoFiatWithdrawals - b.RequestCurrencyPairFormat.Delimiter = "" - b.RequestCurrencyPairFormat.Uppercase = true - b.ConfigCurrencyPairFormat.Delimiter = "" - b.ConfigCurrencyPairFormat.Uppercase = true - b.AssetTypes = []string{ticker.Spot} - b.Requester = request.New(b.Name, - request.NewRateLimit(time.Second, bitmexAuthRate), - request.NewRateLimit(time.Second, bitmexUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - b.APIUrlDefault = bitmexAPIURL - b.APIUrl = b.APIUrlDefault - b.SupportsAutoPairUpdating = true - b.Websocket = wshandler.New() - b.Websocket.Functionality = wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported | - wshandler.WebsocketAccountDataSupported | - wshandler.WebsocketDeadMansSwitchSupported - b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup takes in the supplied exchange configuration details and sets params -func (b *Bitmex) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - b.SetEnabled(false) - } else { - b.Enabled = true - b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - b.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - b.RESTPollingDelay = exch.RESTPollingDelay - b.Verbose = exch.Verbose - b.HTTPDebugging = exch.HTTPDebugging - b.Websocket.SetWsStatusAndConnection(exch.Websocket) - b.BaseCurrencies = exch.BaseCurrencies - b.AvailablePairs = exch.AvailablePairs - b.EnabledPairs = exch.EnabledPairs - err := b.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = b.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = b.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = b.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = b.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = b.Websocket.Setup(b.WsConnector, - b.Subscribe, - b.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - bitmexWSURL, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - b.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: b.Name, - URL: b.Websocket.GetWebsocketURL(), - ProxyURL: b.Websocket.GetProxyAddress(), - Verbose: b.Verbose, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - b.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - false, - false, - true, - exch.Name) - } -} - // GetAnnouncement returns the general announcements from Bitmex func (b *Bitmex) GetAnnouncement() ([]Announcement, error) { var announcement []Announcement @@ -432,9 +327,8 @@ func (b *Bitmex) GetCurrentNotifications() ([]Notification, error) { // GetOrders returns all the orders, open and closed func (b *Bitmex) GetOrders(params *OrdersRequest) ([]Order, error) { var orders []Order - return orders, b.SendAuthenticatedHTTPRequest(http.MethodGet, - fmt.Sprintf("%v%v", bitmexEndpointOrder, ""), + bitmexEndpointOrder, params, &orders) } @@ -872,7 +766,7 @@ func (b *Bitmex) GetWalletSummary(currency string) ([]TransactionInfo, error) { // SendHTTPRequest sends an unauthenticated HTTP request func (b *Bitmex) SendHTTPRequest(path string, params Parameter, result interface{}) error { var respCheck interface{} - path = b.APIUrl + path + path = b.API.Endpoints.URL + path if params != nil { if !params.IsNil() { encodedPath, err := params.ToURLVals(path) @@ -913,7 +807,7 @@ func (b *Bitmex) SendHTTPRequest(path string, params Parameter, result interface // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to bitmex func (b *Bitmex) SendAuthenticatedHTTPRequest(verb, path string, params Parameter, result interface{}) error { - if !b.AuthenticatedAPISupport { + if !b.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } @@ -925,7 +819,7 @@ func (b *Bitmex) SendAuthenticatedHTTPRequest(verb, path string, params Paramete headers := make(map[string]string) headers["Content-Type"] = "application/json" headers["api-expires"] = timestampNew - headers["api-key"] = b.APIKey + headers["api-key"] = b.API.Credentials.Key var payload string if params != nil { @@ -933,23 +827,23 @@ func (b *Bitmex) SendAuthenticatedHTTPRequest(verb, path string, params Paramete if err != nil { return err } - data, err := common.JSONEncode(params) + data, err := json.Marshal(params) if err != nil { return err } payload = string(data) } - hmac := common.GetHMAC(common.HashSHA256, + hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(verb+"/api/v1"+path+timestampNew+payload), - []byte(b.APISecret)) + []byte(b.API.Credentials.Secret)) - headers["api-signature"] = common.HexEncodeToString(hmac) + headers["api-signature"] = crypto.HexEncodeToString(hmac) var respCheck interface{} err := b.SendPayload(verb, - b.APIUrl+path, + b.API.Endpoints.URL+path, headers, bytes.NewBuffer([]byte(payload)), &respCheck, @@ -974,14 +868,14 @@ func (b *Bitmex) CaptureError(resp, reType interface{}) error { return err } - err = common.JSONDecode(marshalled, &Error) + err = json.Unmarshal(marshalled, &Error) if err == nil { return fmt.Errorf("bitmex error %s: %s", Error.Error.Name, Error.Error.Message) } - return common.JSONDecode(marshalled, reType) + return json.Unmarshal(marshalled, reType) } // GetFee returns an estimate of fee based on type of transaction diff --git a/exchanges/bitmex/bitmex_parameters.go b/exchanges/bitmex/bitmex_parameters.go index b5ecf4ed..d1beda15 100644 --- a/exchanges/bitmex/bitmex_parameters.go +++ b/exchanges/bitmex/bitmex_parameters.go @@ -5,6 +5,7 @@ import ( "net/url" "reflect" "strconv" + "strings" "github.com/thrasher-corp/gocryptotrader/common" ) @@ -35,7 +36,7 @@ func StructValsToURLVals(v interface{}) (url.Values, error) { if structType.Field(i).Tag != "" { jsonTag := structType.Field(i).Tag.Get("json") if jsonTag != "" { - split := common.SplitStrings(jsonTag, ",") + split := strings.Split(jsonTag, ",") outgoingTag = split[0] } } diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index cd519573..f92fdee0 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -1,7 +1,9 @@ package bitmex import ( + "log" "net/http" + "os" "sync" "testing" "time" @@ -11,6 +13,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -24,23 +27,28 @@ const ( var b Bitmex -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { b.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Bitmex load config error", err) + } bitmexConfig, err := cfg.GetExchangeConfig("Bitmex") if err != nil { - t.Error("Test Failed - Bitmex Setup() init error") + log.Fatal("Bitmex Setup() init error") } - bitmexConfig.AuthenticatedWebsocketAPISupport = true - bitmexConfig.AuthenticatedAPISupport = true - bitmexConfig.APIKey = apiKey - bitmexConfig.APISecret = apiSecret - b.Setup(&bitmexConfig) + bitmexConfig.API.AuthenticatedSupport = true + bitmexConfig.API.AuthenticatedWebsocketSupport = true + bitmexConfig.API.Credentials.Key = apiKey + bitmexConfig.API.Credentials.Secret = apiSecret + + err = b.Setup(bitmexConfig) + if err != nil { + log.Fatal("Bitmex setup error", err) + } + os.Exit(m.Run()) } func TestStart(t *testing.T) { @@ -52,42 +60,42 @@ func TestStart(t *testing.T) { func TestGetUrgentAnnouncement(t *testing.T) { _, err := b.GetUrgentAnnouncement() if err == nil { - t.Error("test failed - GetUrgentAnnouncement() error", err) + t.Error("GetUrgentAnnouncement() Expected error") } } func TestGetAPIKeys(t *testing.T) { _, err := b.GetAPIKeys() if err == nil { - t.Error("test failed - GetAPIKeys() error", err) + t.Error("GetAPIKeys() Expected error") } } func TestRemoveAPIKey(t *testing.T) { _, err := b.RemoveAPIKey(APIKeyParams{APIKeyID: "1337"}) if err == nil { - t.Error("test failed - RemoveAPIKey() error", err) + t.Error("RemoveAPIKey() Expected error") } } func TestDisableAPIKey(t *testing.T) { _, err := b.DisableAPIKey(APIKeyParams{APIKeyID: "1337"}) if err == nil { - t.Error("test failed - DisableAPIKey() error", err) + t.Error("DisableAPIKey() Expected error") } } func TestEnableAPIKey(t *testing.T) { _, err := b.EnableAPIKey(APIKeyParams{APIKeyID: "1337"}) if err == nil { - t.Error("test failed - EnableAPIKey() error", err) + t.Error("EnableAPIKey() Expected error") } } func TestGetTrollboxMessages(t *testing.T) { _, err := b.GetTrollboxMessages(ChatGetParams{Count: 5}) if err != nil { - t.Error("test failed - GetTrollboxMessages() error", err) + t.Error("GetTrollboxMessages() error", err) } } @@ -96,126 +104,126 @@ func TestSendTrollboxMessage(t *testing.T) { ChannelID: 1337, Message: "Hello,World!"}) if err == nil { - t.Error("test failed - SendTrollboxMessage() error", err) + t.Error("SendTrollboxMessage() Expected error") } } func TestGetTrollboxChannels(t *testing.T) { _, err := b.GetTrollboxChannels() if err != nil { - t.Error("test failed - GetTrollboxChannels() error", err) + t.Error("GetTrollboxChannels() error", err) } } func TestGetTrollboxConnectedUsers(t *testing.T) { _, err := b.GetTrollboxConnectedUsers() if err == nil { - t.Error("test failed - GetTrollboxConnectedUsers() error", err) + t.Error("GetTrollboxConnectedUsers() Expected error") } } func TestGetAccountExecutions(t *testing.T) { _, err := b.GetAccountExecutions(&GenericRequestParams{}) if err == nil { - t.Error("test failed - GetAccountExecutions() error", err) + t.Error("GetAccountExecutions() Expected error") } } func TestGetAccountExecutionTradeHistory(t *testing.T) { _, err := b.GetAccountExecutionTradeHistory(&GenericRequestParams{}) if err == nil { - t.Error("test failed - GetAccountExecutionTradeHistory() error", err) + t.Error("GetAccountExecutionTradeHistory() Expected error") } } func TestGetFundingHistory(t *testing.T) { _, err := b.GetFundingHistory() if err == nil { - t.Error("test failed - GetFundingHistory() error", err) + t.Error("GetFundingHistory() Expected error") } } func TestGetInstruments(t *testing.T) { _, err := b.GetInstruments(&GenericRequestParams{}) if err != nil { - t.Error("test failed - GetInstruments() error", err) + t.Error("GetInstruments() error", err) } } func TestGetActiveInstruments(t *testing.T) { _, err := b.GetActiveInstruments(&GenericRequestParams{}) if err != nil { - t.Error("test failed - GetActiveInstruments() error", err) + t.Error("GetActiveInstruments() error", err) } } func TestGetActiveAndIndexInstruments(t *testing.T) { _, err := b.GetActiveAndIndexInstruments() if err != nil { - t.Error("test failed - GetActiveAndIndexInstruments() error", err) + t.Error("GetActiveAndIndexInstruments() error", err) } } func TestGetActiveIntervals(t *testing.T) { _, err := b.GetActiveIntervals() if err == nil { - t.Error("test failed - GetActiveIntervals() error", err) + t.Error("GetActiveIntervals() Expected error") } } func TestGetCompositeIndex(t *testing.T) { _, err := b.GetCompositeIndex(&GenericRequestParams{}) if err == nil { - t.Error("test failed - GetCompositeIndex() error", err) + t.Error("GetCompositeIndex() Expected error") } } func TestGetIndices(t *testing.T) { _, err := b.GetIndices() if err != nil { - t.Error("test failed - GetIndices() error", err) + t.Error("GetIndices() error", err) } } func TestGetInsuranceFundHistory(t *testing.T) { _, err := b.GetInsuranceFundHistory(&GenericRequestParams{}) if err != nil { - t.Error("test failed - GetInsuranceFundHistory() error", err) + t.Error("GetInsuranceFundHistory() error", err) } } func TestGetLeaderboard(t *testing.T) { _, err := b.GetLeaderboard(LeaderboardGetParams{}) if err != nil { - t.Error("test failed - GetLeaderboard() error", err) + t.Error("GetLeaderboard() error", err) } } func TestGetAliasOnLeaderboard(t *testing.T) { _, err := b.GetAliasOnLeaderboard() if err == nil { - t.Error("test failed - GetAliasOnLeaderboard() error", err) + t.Error("GetAliasOnLeaderboard() Expected error") } } func TestGetLiquidationOrders(t *testing.T) { _, err := b.GetLiquidationOrders(&GenericRequestParams{}) if err != nil { - t.Error("test failed - GetLiquidationOrders() error", err) + t.Error("GetLiquidationOrders() error", err) } } func TestGetCurrentNotifications(t *testing.T) { _, err := b.GetCurrentNotifications() if err == nil { - t.Error("test failed - GetCurrentNotifications() error", err) + t.Error("GetCurrentNotifications() Expected error") } } func TestAmendOrder(t *testing.T) { _, err := b.AmendOrder(&OrderAmendParams{}) if err == nil { - t.Error("test failed - AmendOrder() error", err) + t.Error("AmendOrder() Expected error") } } @@ -225,126 +233,126 @@ func TestCreateOrder(t *testing.T) { ClOrdID: "mm_bitmex_1a/oemUeQ4CAJZgP3fjHsA", OrderQty: 98}) if err == nil { - t.Error("test failed - CreateOrder() error", err) + t.Error("CreateOrder() Expected error") } } func TestCancelOrders(t *testing.T) { _, err := b.CancelOrders(&OrderCancelParams{}) if err == nil { - t.Error("test failed - CancelOrders() error", err) + t.Error("CancelOrders() Expected error") } } func TestCancelAllOrders(t *testing.T) { _, err := b.CancelAllExistingOrders(OrderCancelAllParams{}) if err == nil { - t.Error("test failed - CancelAllOrders(orderCancellation *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error)", err) + t.Error("CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error)", err) } } func TestAmendBulkOrders(t *testing.T) { _, err := b.AmendBulkOrders(OrderAmendBulkParams{}) if err == nil { - t.Error("test failed - AmendBulkOrders() error", err) + t.Error("AmendBulkOrders() Expected error") } } func TestCreateBulkOrders(t *testing.T) { _, err := b.CreateBulkOrders(OrderNewBulkParams{}) if err == nil { - t.Error("test failed - CreateBulkOrders() error", err) + t.Error("CreateBulkOrders() Expected error") } } func TestCancelAllOrdersAfterTime(t *testing.T) { _, err := b.CancelAllOrdersAfterTime(OrderCancelAllAfterParams{}) if err == nil { - t.Error("test failed - CancelAllOrdersAfterTime() error", err) + t.Error("CancelAllOrdersAfterTime() Expected error") } } func TestClosePosition(t *testing.T) { _, err := b.ClosePosition(OrderClosePositionParams{}) if err == nil { - t.Error("test failed - ClosePosition() error", err) + t.Error("ClosePosition() Expected error") } } func TestGetOrderbook(t *testing.T) { _, err := b.GetOrderbook(OrderBookGetL2Params{Symbol: "XBT"}) if err != nil { - t.Error("test failed - GetOrderbook() error", err) + t.Error("GetOrderbook() error", err) } } func TestGetPositions(t *testing.T) { _, err := b.GetPositions(PositionGetParams{}) if err == nil { - t.Error("test failed - GetPositions() error", err) + t.Error("GetPositions() Expected error") } } func TestIsolatePosition(t *testing.T) { _, err := b.IsolatePosition(PositionIsolateMarginParams{Symbol: "XBT"}) if err == nil { - t.Error("test failed - IsolatePosition() error", err) + t.Error("IsolatePosition() Expected error") } } func TestLeveragePosition(t *testing.T) { _, err := b.LeveragePosition(PositionUpdateLeverageParams{}) if err == nil { - t.Error("test failed - LeveragePosition() error", err) + t.Error("LeveragePosition() Expected error") } } func TestUpdateRiskLimit(t *testing.T) { _, err := b.UpdateRiskLimit(PositionUpdateRiskLimitParams{}) if err == nil { - t.Error("test failed - UpdateRiskLimit() error", err) + t.Error("UpdateRiskLimit() Expected error") } } func TestTransferMargin(t *testing.T) { _, err := b.TransferMargin(PositionTransferIsolatedMarginParams{}) if err == nil { - t.Error("test failed - TransferMargin() error", err) + t.Error("TransferMargin() Expected error") } } func TestGetQuotesByBuckets(t *testing.T) { _, err := b.GetQuotesByBuckets(&QuoteGetBucketedParams{}) if err == nil { - t.Error("test failed - GetQuotesByBuckets() error", err) + t.Error("GetQuotesByBuckets() Expected error") } } func TestGetSettlementHistory(t *testing.T) { _, err := b.GetSettlementHistory(&GenericRequestParams{}) if err != nil { - t.Error("test failed - GetSettlementHistory() error", err) + t.Error("GetSettlementHistory() error", err) } } func TestGetStats(t *testing.T) { _, err := b.GetStats() if err != nil { - t.Error("test failed - GetStats() error", err) + t.Error("GetStats() error", err) } } func TestGetStatsHistorical(t *testing.T) { _, err := b.GetStatsHistorical() if err != nil { - t.Error("test failed - GetStatsHistorical() error", err) + t.Error("GetStatsHistorical() error", err) } } func TestGetStatSummary(t *testing.T) { _, err := b.GetStatSummary() if err != nil { - t.Error("test failed - GetStatSummary() error", err) + t.Error("GetStatSummary() error", err) } } @@ -354,14 +362,14 @@ func TestGetTrade(t *testing.T) { StartTime: time.Now().Format(time.RFC3339), Reverse: true}) if err != nil { - t.Error("test failed - GetTrade() error", err) + t.Error("GetTrade() error", err) } } func TestGetPreviousTrades(t *testing.T) { _, err := b.GetPreviousTrades(&TradeGetBucketedParams{}) if err == nil { - t.Error("test failed - GetPreviousTrades() error", err) + t.Error("GetPreviousTrades() Expected error") } } @@ -378,7 +386,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() b.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -390,14 +398,11 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - b.SetDefaults() - TestSetup(t) - var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic if resp, err := b.GetFee(feeBuilder); resp != float64(0.00075) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.00075), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.00075), resp) } // CryptocurrencyTradeFee High quantity @@ -405,7 +410,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := b.GetFee(feeBuilder); resp != float64(750) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(750), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(750), resp) t.Error(err) } @@ -413,7 +418,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := b.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) t.Error(err) } @@ -421,7 +426,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -429,7 +434,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -437,7 +442,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -446,7 +451,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -455,29 +460,23 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - b.SetDefaults() expectedResult := exchange.AutoWithdrawCryptoWithAPIPermissionText + " & " + exchange.WithdrawCryptoWith2FAText + " & " + exchange.WithdrawCryptoWithEmailText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := b.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -489,11 +488,8 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, } @@ -509,27 +505,26 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if b.APIKey != "" && b.APIKey != "Key" && - b.APISecret != "" && b.APISecret != "Secret" { - return true - } - return false + return b.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.XBT, - Quote: currency.USD, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.XBT, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := b.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 1, "clientId") + response, err := b.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -538,16 +533,12 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "123456789012345678901234567890123456", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -564,16 +555,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "123456789012345678901234567890123456", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -589,41 +576,44 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestGetAccountInfo(t *testing.T) { - if apiKey != "" || apiSecret != "" { + if areTestAPIKeysSet() { _, err := b.GetAccountInfo() if err != nil { - t.Error("Test Failed - GetAccountInfo() error", err) + t.Error("GetAccountInfo() error", err) } } else { _, err := b.GetAccountInfo() if err == nil { - t.Error("Test Failed - GetAccountInfo() error") + t.Error("GetAccountInfo() error") } } } func TestModifyOrder(t *testing.T) { - _, err := b.ModifyOrder(&exchange.ModifyOrder{OrderID: "1337"}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := b.ModifyOrder(&order.Modify{OrderID: "1337"}) if err == nil { - t.Error("Test Failed - ModifyOrder() error") + t.Error("ModifyOrder() error") } } func TestWithdraw(t *testing.T) { - b.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.XBT, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", - OneTimePassword: 000000, + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + OneTimePassword: 000000, + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -640,15 +630,11 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := b.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -656,15 +642,11 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := b.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -675,21 +657,19 @@ func TestGetDepositAddress(t *testing.T) { if areTestAPIKeysSet() { _, err := b.GetDepositAddress(currency.BTC, "") if err != nil { - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) } } else { _, err := b.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error cannot be nil") + t.Error("GetDepositAddress() error cannot be nil") } } } // TestWsAuth dials websocket, sends login request. func TestWsAuth(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if !b.Websocket.IsEnabled() && !b.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } b.WebsocketConn = &wshandler.WebsocketConnection{ diff --git a/exchanges/bitmex/bitmex_types.go b/exchanges/bitmex/bitmex_types.go index 3a84e8fa..9445045b 100644 --- a/exchanges/bitmex/bitmex_types.go +++ b/exchanges/bitmex/bitmex_types.go @@ -1,6 +1,11 @@ package bitmex -import exchange "github.com/thrasher-corp/gocryptotrader/exchanges" +import ( + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) // RequestError allows for a general error capture from requests type RequestError struct { @@ -117,107 +122,107 @@ type Funding struct { // Instrument Tradeable Contracts, Indices, and History type Instrument struct { - AskPrice float64 `json:"askPrice"` - BankruptLimitDownPrice float64 `json:"bankruptLimitDownPrice"` - BankruptLimitUpPrice float64 `json:"bankruptLimitUpPrice"` - BidPrice float64 `json:"bidPrice"` - BuyLeg string `json:"buyLeg"` - CalcInterval string `json:"calcInterval"` - Capped bool `json:"capped"` - ClosingTimestamp string `json:"closingTimestamp"` - Deleverage bool `json:"deleverage"` - Expiry string `json:"expiry"` - FairBasis float64 `json:"fairBasis"` - FairBasisRate float64 `json:"fairBasisRate"` - FairMethod string `json:"fairMethod"` - FairPrice float64 `json:"fairPrice"` - Front string `json:"front"` - FundingBaseSymbol string `json:"fundingBaseSymbol"` - FundingInterval string `json:"fundingInterval"` - FundingPremiumSymbol string `json:"fundingPremiumSymbol"` - FundingQuoteSymbol string `json:"fundingQuoteSymbol"` - FundingRate float64 `json:"fundingRate"` - FundingTimestamp string `json:"fundingTimestamp"` - HasLiquidity bool `json:"hasLiquidity"` - HighPrice float64 `json:"highPrice"` - ImpactAskPrice float64 `json:"impactAskPrice"` - ImpactBidPrice float64 `json:"impactBidPrice"` - ImpactMidPrice float64 `json:"impactMidPrice"` - IndicativeFundingRate float64 `json:"indicativeFundingRate"` - IndicativeSettlePrice float64 `json:"indicativeSettlePrice"` - IndicativeTaxRate float64 `json:"indicativeTaxRate"` - InitMargin float64 `json:"initMargin"` - InsuranceFee float64 `json:"insuranceFee"` - InverseLeg string `json:"inverseLeg"` - IsInverse bool `json:"isInverse"` - IsQuanto bool `json:"isQuanto"` - LastChangePcnt float64 `json:"lastChangePcnt"` - LastPrice float64 `json:"lastPrice"` - LastPriceProtected float64 `json:"lastPriceProtected"` - LastTickDirection string `json:"lastTickDirection"` - Limit float64 `json:"limit"` - LimitDownPrice float64 `json:"limitDownPrice"` - LimitUpPrice float64 `json:"limitUpPrice"` - Listing string `json:"listing"` - LotSize int64 `json:"lotSize"` - LowPrice float64 `json:"lowPrice"` - MaintMargin float64 `json:"maintMargin"` - MakerFee float64 `json:"makerFee"` - MarkMethod string `json:"markMethod"` - MarkPrice float64 `json:"markPrice"` - MaxOrderQty int64 `json:"maxOrderQty"` - MaxPrice float64 `json:"maxPrice"` - MidPrice float64 `json:"midPrice"` - Multiplier int64 `json:"multiplier"` - OpenInterest int64 `json:"openInterest"` - OpenValue int64 `json:"openValue"` - OpeningTimestamp string `json:"openingTimestamp"` - OptionMultiplier float64 `json:"optionMultiplier"` - OptionStrikePcnt float64 `json:"optionStrikePcnt"` - OptionStrikePrice float64 `json:"optionStrikePrice"` - OptionStrikeRound float64 `json:"optionStrikeRound"` - OptionUnderlyingPrice float64 `json:"optionUnderlyingPrice"` - PositionCurrency string `json:"positionCurrency"` - PrevClosePrice float64 `json:"prevClosePrice"` - PrevPrice24h float64 `json:"prevPrice24h"` - PrevTotalTurnover int64 `json:"prevTotalTurnover"` - PrevTotalVolume int64 `json:"prevTotalVolume"` - PublishInterval string `json:"publishInterval"` - PublishTime string `json:"publishTime"` - QuoteCurrency string `json:"quoteCurrency"` - QuoteToSettleMultiplier int64 `json:"quoteToSettleMultiplier"` - RebalanceInterval string `json:"rebalanceInterval"` - RebalanceTimestamp string `json:"rebalanceTimestamp"` - Reference string `json:"reference"` - ReferenceSymbol string `json:"referenceSymbol"` - RelistInterval string `json:"relistInterval"` - RiskLimit int64 `json:"riskLimit"` - RiskStep int64 `json:"riskStep"` - RootSymbol string `json:"rootSymbol"` - SellLeg string `json:"sellLeg"` - SessionInterval string `json:"sessionInterval"` - SettlCurrency string `json:"settlCurrency"` - Settle string `json:"settle"` - SettledPrice float64 `json:"settledPrice"` - SettlementFee float64 `json:"settlementFee"` - State string `json:"state"` - Symbol string `json:"symbol"` - TakerFee float64 `json:"takerFee"` - Taxed bool `json:"taxed"` - TickSize float64 `json:"tickSize"` - Timestamp string `json:"timestamp"` - TotalTurnover int64 `json:"totalTurnover"` - TotalVolume int64 `json:"totalVolume"` - Turnover int64 `json:"turnover"` - Turnover24h int64 `json:"turnover24h"` - Typ string `json:"typ"` - Underlying string `json:"underlying"` - UnderlyingSymbol string `json:"underlyingSymbol"` - UnderlyingToPositionMultiplier int64 `json:"underlyingToPositionMultiplier"` - UnderlyingToSettleMultiplier int64 `json:"underlyingToSettleMultiplier"` - Volume int64 `json:"volume"` - Volume24h int64 `json:"volume24h"` - Vwap float64 `json:"vwap"` + AskPrice float64 `json:"askPrice"` + BankruptLimitDownPrice float64 `json:"bankruptLimitDownPrice"` + BankruptLimitUpPrice float64 `json:"bankruptLimitUpPrice"` + BidPrice float64 `json:"bidPrice"` + BuyLeg string `json:"buyLeg"` + CalcInterval string `json:"calcInterval"` + Capped bool `json:"capped"` + ClosingTimestamp string `json:"closingTimestamp"` + Deleverage bool `json:"deleverage"` + Expiry string `json:"expiry"` + FairBasis float64 `json:"fairBasis"` + FairBasisRate float64 `json:"fairBasisRate"` + FairMethod string `json:"fairMethod"` + FairPrice float64 `json:"fairPrice"` + Front string `json:"front"` + FundingBaseSymbol string `json:"fundingBaseSymbol"` + FundingInterval string `json:"fundingInterval"` + FundingPremiumSymbol string `json:"fundingPremiumSymbol"` + FundingQuoteSymbol string `json:"fundingQuoteSymbol"` + FundingRate float64 `json:"fundingRate"` + FundingTimestamp string `json:"fundingTimestamp"` + HasLiquidity bool `json:"hasLiquidity"` + HighPrice float64 `json:"highPrice"` + ImpactAskPrice float64 `json:"impactAskPrice"` + ImpactBidPrice float64 `json:"impactBidPrice"` + ImpactMidPrice float64 `json:"impactMidPrice"` + IndicativeFundingRate float64 `json:"indicativeFundingRate"` + IndicativeSettlePrice float64 `json:"indicativeSettlePrice"` + IndicativeTaxRate float64 `json:"indicativeTaxRate"` + InitMargin float64 `json:"initMargin"` + InsuranceFee float64 `json:"insuranceFee"` + InverseLeg string `json:"inverseLeg"` + IsInverse bool `json:"isInverse"` + IsQuanto bool `json:"isQuanto"` + LastChangePcnt float64 `json:"lastChangePcnt"` + LastPrice float64 `json:"lastPrice"` + LastPriceProtected float64 `json:"lastPriceProtected"` + LastTickDirection string `json:"lastTickDirection"` + Limit float64 `json:"limit"` + LimitDownPrice float64 `json:"limitDownPrice"` + LimitUpPrice float64 `json:"limitUpPrice"` + Listing string `json:"listing"` + LotSize int64 `json:"lotSize"` + LowPrice float64 `json:"lowPrice"` + MaintMargin float64 `json:"maintMargin"` + MakerFee float64 `json:"makerFee"` + MarkMethod string `json:"markMethod"` + MarkPrice float64 `json:"markPrice"` + MaxOrderQty int64 `json:"maxOrderQty"` + MaxPrice float64 `json:"maxPrice"` + MidPrice float64 `json:"midPrice"` + Multiplier int64 `json:"multiplier"` + OpenInterest int64 `json:"openInterest"` + OpenValue int64 `json:"openValue"` + OpeningTimestamp string `json:"openingTimestamp"` + OptionMultiplier float64 `json:"optionMultiplier"` + OptionStrikePcnt float64 `json:"optionStrikePcnt"` + OptionStrikePrice float64 `json:"optionStrikePrice"` + OptionStrikeRound float64 `json:"optionStrikeRound"` + OptionUnderlyingPrice float64 `json:"optionUnderlyingPrice"` + PositionCurrency string `json:"positionCurrency"` + PrevClosePrice float64 `json:"prevClosePrice"` + PrevPrice24h float64 `json:"prevPrice24h"` + PrevTotalTurnover int64 `json:"prevTotalTurnover"` + PrevTotalVolume int64 `json:"prevTotalVolume"` + PublishInterval string `json:"publishInterval"` + PublishTime string `json:"publishTime"` + QuoteCurrency string `json:"quoteCurrency"` + QuoteToSettleMultiplier int64 `json:"quoteToSettleMultiplier"` + RebalanceInterval string `json:"rebalanceInterval"` + RebalanceTimestamp string `json:"rebalanceTimestamp"` + Reference string `json:"reference"` + ReferenceSymbol string `json:"referenceSymbol"` + RelistInterval string `json:"relistInterval"` + RiskLimit int64 `json:"riskLimit"` + RiskStep int64 `json:"riskStep"` + RootSymbol string `json:"rootSymbol"` + SellLeg string `json:"sellLeg"` + SessionInterval string `json:"sessionInterval"` + SettlCurrency string `json:"settlCurrency"` + Settle string `json:"settle"` + SettledPrice float64 `json:"settledPrice"` + SettlementFee float64 `json:"settlementFee"` + State string `json:"state"` + Symbol currency.Pair `json:"symbol"` + TakerFee float64 `json:"takerFee"` + Taxed bool `json:"taxed"` + TickSize float64 `json:"tickSize"` + Timestamp time.Time `json:"timestamp"` + TotalTurnover int64 `json:"totalTurnover"` + TotalVolume int64 `json:"totalVolume"` + Turnover int64 `json:"turnover"` + Turnover24h int64 `json:"turnover24h"` + Typ string `json:"typ"` + Underlying string `json:"underlying"` + UnderlyingSymbol string `json:"underlyingSymbol"` + UnderlyingToPositionMultiplier int64 `json:"underlyingToPositionMultiplier"` + UnderlyingToSettleMultiplier int64 `json:"underlyingToSettleMultiplier"` + Volume float64 `json:"volume"` + Volume24h float64 `json:"volume24h"` + Vwap float64 `json:"vwap"` } // InstrumentInterval instrument interval @@ -670,15 +675,15 @@ type WalletInfo struct { } // orderTypeMap holds order type info based on Bitmex data -var orderTypeMap = map[int64]exchange.OrderType{ - 1: exchange.MarketOrderType, - 2: exchange.LimitOrderType, - 3: exchange.StopOrderType, - 7: exchange.TrailingStopOrderType, +var orderTypeMap = map[int64]order.Type{ + 1: order.Market, + 2: order.Limit, + 3: order.Stop, + 7: order.TrailingStop, } // orderSideMap holds order type info based on Bitmex data -var orderSideMap = map[int64]exchange.OrderSide{ - 1: exchange.BuyOrderSide, - 2: exchange.SellOrderSide, +var orderSideMap = map[int64]order.Side{ + 1: order.Buy, + 2: order.Sell, } diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index 86f75d35..61db8928 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -1,6 +1,7 @@ package bitmex import ( + "encoding/json" "errors" "fmt" "net/http" @@ -9,9 +10,11 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" @@ -64,8 +67,8 @@ var ( pongChan = make(chan int, 1) ) -// WsConnector initiates a new websocket connection -func (b *Bitmex) WsConnector() error { +// WsConnect initiates a new websocket connection +func (b *Bitmex) WsConnect() error { if !b.Websocket.IsEnabled() || !b.IsEnabled() { return errors.New(wshandler.WebsocketNotEnabled) } @@ -77,17 +80,18 @@ func (b *Bitmex) WsConnector() error { p, err := b.WebsocketConn.ReadMessage() if err != nil { + b.Websocket.ReadMessageErrors <- err return err } b.Websocket.TrafficAlert <- struct{}{} var welcomeResp WebsocketWelcome - err = common.JSONDecode(p.Raw, &welcomeResp) + err = json.Unmarshal(p.Raw, &welcomeResp) if err != nil { return err } if b.Verbose { - log.Debugf("Successfully connected to Bitmex %s at time: %s Limit: %d", + log.Debugf(log.ExchangeSys, "Successfully connected to Bitmex %s at time: %s Limit: %d", welcomeResp.Info, welcomeResp.Timestamp, welcomeResp.Limit.Remaining) @@ -98,7 +102,7 @@ func (b *Bitmex) WsConnector() error { err = b.websocketSendAuth() if err != nil { - log.Errorf("%v - authentication failed: %v", b.Name, err) + log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", b.Name, err) } b.GenerateAuthenticatedSubscriptions() return nil @@ -125,12 +129,12 @@ func (b *Bitmex) wsHandleIncomingData() { } b.Websocket.TrafficAlert <- struct{}{} message := string(resp.Raw) - if common.StringContains(message, "pong") { + if strings.Contains(message, "pong") { pongChan <- 1 continue } - if common.StringContains(message, "ping") { + if strings.Contains(message, "ping") { err = b.WebsocketConn.SendMessage("pong") if err != nil { b.Websocket.DataHandler <- err @@ -139,7 +143,7 @@ func (b *Bitmex) wsHandleIncomingData() { } quickCapture := make(map[string]interface{}) - err = common.JSONDecode(resp.Raw, &quickCapture) + err = json.Unmarshal(resp.Raw, &quickCapture) if err != nil { b.Websocket.DataHandler <- err continue @@ -147,7 +151,7 @@ func (b *Bitmex) wsHandleIncomingData() { var respError WebsocketErrorResponse if _, ok := quickCapture["status"]; ok { - err = common.JSONDecode(resp.Raw, &respError) + err = json.Unmarshal(resp.Raw, &respError) if err != nil { b.Websocket.DataHandler <- err continue @@ -158,7 +162,7 @@ func (b *Bitmex) wsHandleIncomingData() { if _, ok := quickCapture["success"]; ok { var decodedResp WebsocketSubscribeResp - err := common.JSONDecode(resp.Raw, &decodedResp) + err := json.Unmarshal(resp.Raw, &decodedResp) if err != nil { b.Websocket.DataHandler <- err continue @@ -168,13 +172,13 @@ func (b *Bitmex) wsHandleIncomingData() { b.Websocket.DataHandler <- decodedResp if len(quickCapture) == 3 { if b.Verbose { - log.Debugf("%s websocket: Successfully subscribed to %s", + log.Debugf(log.ExchangeSys, "%s websocket: Successfully subscribed to %s", b.Name, decodedResp.Subscribe) } } else { b.Websocket.SetCanUseAuthenticatedEndpoints(true) if b.Verbose { - log.Debugf("%s websocket: Successfully authenticated websocket connection", + log.Debugf(log.ExchangeSys, "%s websocket: Successfully authenticated websocket connection", b.Name) } } @@ -185,7 +189,7 @@ func (b *Bitmex) wsHandleIncomingData() { b.Name, decodedResp.Subscribe) } else if _, ok := quickCapture["table"]; ok { var decodedResp WebsocketMainResponse - err := common.JSONDecode(resp.Raw, &decodedResp) + err := json.Unmarshal(resp.Raw, &decodedResp) if err != nil { b.Websocket.DataHandler <- err continue @@ -194,15 +198,24 @@ func (b *Bitmex) wsHandleIncomingData() { switch decodedResp.Table { case bitmexWSOrderbookL2: var orderbooks OrderBookData - err = common.JSONDecode(resp.Raw, &orderbooks) + err = json.Unmarshal(resp.Raw, &orderbooks) if err != nil { b.Websocket.DataHandler <- err continue } p := currency.NewPairFromString(orderbooks.Data[0].Symbol) - // TODO: update this to support multiple asset types - err = b.processOrderbook(orderbooks.Data, orderbooks.Action, p, "CONTRACT") + var a asset.Item + a, err = b.GetPairAssetType(p) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + err = b.processOrderbook(orderbooks.Data, + orderbooks.Action, + p, + a) if err != nil { b.Websocket.DataHandler <- err continue @@ -210,7 +223,7 @@ func (b *Bitmex) wsHandleIncomingData() { case bitmexWSTrade: var trades TradeData - err = common.JSONDecode(resp.Raw, &trades) + err = json.Unmarshal(resp.Raw, &trades) if err != nil { b.Websocket.DataHandler <- err continue @@ -233,7 +246,7 @@ func (b *Bitmex) wsHandleIncomingData() { Price: trades.Data[i].Price, Amount: float64(trades.Data[i].Size), CurrencyPair: currency.NewPairFromString(trades.Data[i].Symbol), - Exchange: b.GetName(), + Exchange: b.Name, AssetType: "CONTRACT", Side: trades.Data[i].Side, } @@ -241,7 +254,7 @@ func (b *Bitmex) wsHandleIncomingData() { case bitmexWSAnnouncement: var announcement AnnouncementData - err = common.JSONDecode(resp.Raw, &announcement) + err = json.Unmarshal(resp.Raw, &announcement) if err != nil { b.Websocket.DataHandler <- err continue @@ -254,7 +267,7 @@ func (b *Bitmex) wsHandleIncomingData() { b.Websocket.DataHandler <- announcement.Data case bitmexWSAffiliate: var response WsAffiliateResponse - err = common.JSONDecode(resp.Raw, &response) + err = json.Unmarshal(resp.Raw, &response) if err != nil { b.Websocket.DataHandler <- err continue @@ -262,7 +275,7 @@ func (b *Bitmex) wsHandleIncomingData() { b.Websocket.DataHandler <- response case bitmexWSExecution: var response WsExecutionResponse - err = common.JSONDecode(resp.Raw, &response) + err = json.Unmarshal(resp.Raw, &response) if err != nil { b.Websocket.DataHandler <- err continue @@ -270,7 +283,7 @@ func (b *Bitmex) wsHandleIncomingData() { b.Websocket.DataHandler <- response case bitmexWSOrder: var response WsOrderResponse - err = common.JSONDecode(resp.Raw, &response) + err = json.Unmarshal(resp.Raw, &response) if err != nil { b.Websocket.DataHandler <- err continue @@ -278,7 +291,7 @@ func (b *Bitmex) wsHandleIncomingData() { b.Websocket.DataHandler <- response case bitmexWSMargin: var response WsMarginResponse - err = common.JSONDecode(resp.Raw, &response) + err = json.Unmarshal(resp.Raw, &response) if err != nil { b.Websocket.DataHandler <- err continue @@ -286,7 +299,7 @@ func (b *Bitmex) wsHandleIncomingData() { b.Websocket.DataHandler <- response case bitmexWSPosition: var response WsPositionResponse - err = common.JSONDecode(resp.Raw, &response) + err = json.Unmarshal(resp.Raw, &response) if err != nil { b.Websocket.DataHandler <- err continue @@ -294,7 +307,7 @@ func (b *Bitmex) wsHandleIncomingData() { b.Websocket.DataHandler <- response case bitmexWSPrivateNotifications: var response WsPrivateNotificationsResponse - err = common.JSONDecode(resp.Raw, &response) + err = json.Unmarshal(resp.Raw, &response) if err != nil { b.Websocket.DataHandler <- err continue @@ -302,7 +315,7 @@ func (b *Bitmex) wsHandleIncomingData() { b.Websocket.DataHandler <- response case bitmexWSTransact: var response WsTransactResponse - err = common.JSONDecode(resp.Raw, &response) + err = json.Unmarshal(resp.Raw, &response) if err != nil { b.Websocket.DataHandler <- err continue @@ -310,7 +323,7 @@ func (b *Bitmex) wsHandleIncomingData() { b.Websocket.DataHandler <- response case bitmexWSWallet: var response WsWalletResponse - err = common.JSONDecode(resp.Raw, &response) + err = json.Unmarshal(resp.Raw, &response) if err != nil { b.Websocket.DataHandler <- err continue @@ -326,7 +339,7 @@ func (b *Bitmex) wsHandleIncomingData() { } // ProcessOrderbook processes orderbook updates -func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPair currency.Pair, assetType string) error { // nolint: unparam +func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPair currency.Pair, assetType asset.Item) error { // nolint: unparam if len(data) < 1 { return errors.New("bitmex_websocket.go error - no orderbook data") } @@ -334,30 +347,26 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai switch action { case bitmexActionInitialData: var newOrderBook orderbook.Base - var bids, asks []orderbook.Item for i := range data { - if strings.EqualFold(data[i].Side, exchange.SellOrderSide.ToString()) { - asks = append(asks, orderbook.Item{ + if strings.EqualFold(data[i].Side, order.Sell.String()) { + newOrderBook.Asks = append(newOrderBook.Asks, orderbook.Item{ Price: data[i].Price, Amount: float64(data[i].Size), + ID: data[i].ID, }) continue } - bids = append(bids, orderbook.Item{ + newOrderBook.Bids = append(newOrderBook.Bids, orderbook.Item{ Price: data[i].Price, Amount: float64(data[i].Size), + ID: data[i].ID, }) } - - if len(bids) == 0 || len(asks) == 0 { - return errors.New("bitmex_websocket.go error - snapshot not initialised correctly") - } - - newOrderBook.Asks = asks - newOrderBook.Bids = bids newOrderBook.AssetType = assetType newOrderBook.Pair = currencyPair - err := b.Websocket.Orderbook.LoadSnapshot(&newOrderBook, false) + newOrderBook.ExchangeName = b.Name + + err := b.Websocket.Orderbook.LoadSnapshot(&newOrderBook) if err != nil { return fmt.Errorf("bitmex_websocket.go process orderbook error - %s", err) @@ -365,31 +374,30 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: currencyPair, Asset: assetType, - Exchange: b.GetName(), + Exchange: b.Name, } default: var asks, bids []orderbook.Item for i := range data { if strings.EqualFold(data[i].Side, "Sell") { asks = append(asks, orderbook.Item{ - Price: data[i].Price, Amount: float64(data[i].Size), + ID: data[i].ID, }) continue } bids = append(bids, orderbook.Item{ - Price: data[i].Price, Amount: float64(data[i].Size), + ID: data[i].ID, }) } err := b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: currencyPair, - UpdateTime: time.Now(), - AssetType: assetType, - Action: action, + Bids: bids, + Asks: asks, + Pair: currencyPair, + Asset: assetType, + Action: action, }) if err != nil { return err @@ -398,7 +406,7 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: currencyPair, Asset: assetType, - Exchange: b.GetName(), + Exchange: b.Name, } } return nil @@ -406,7 +414,16 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (b *Bitmex) GenerateDefaultSubscriptions() { - contracts := b.GetEnabledCurrencies() + assets := b.GetAssetTypes() + var allPairs currency.Pairs + + for x := range assets { + contracts := b.GetEnabledPairs(assets[x]) + for y := range contracts { + allPairs = allPairs.Add(contracts[y]) + } + } + channels := []string{bitmexWSOrderbookL2, bitmexWSTrade} subscriptions := []wshandler.WebsocketChannelSubscription{ { @@ -415,10 +432,10 @@ func (b *Bitmex) GenerateDefaultSubscriptions() { } for i := range channels { - for j := range contracts { + for j := range allPairs { subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: fmt.Sprintf("%v:%v", channels[i], contracts[j].String()), - Currency: contracts[j], + Channel: channels[i] + ":" + allPairs[j].String(), + Currency: allPairs[j], }) } } @@ -430,7 +447,7 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() { if !b.Websocket.CanUseAuthenticatedEndpoints() { return } - contracts := b.GetEnabledCurrencies() + contracts := b.GetEnabledPairs(asset.PerpetualContract) channels := []string{bitmexWSExecution, bitmexWSPosition, } @@ -457,7 +474,7 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() { for i := range channels { for j := range contracts { subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: fmt.Sprintf("%v:%v", channels[i], contracts[j].String()), + Channel: channels[i] + ":" + contracts[j].String(), Currency: contracts[j], }) } @@ -491,13 +508,14 @@ func (b *Bitmex) websocketSendAuth() error { b.Websocket.SetCanUseAuthenticatedEndpoints(true) timestamp := time.Now().Add(time.Hour * 1).Unix() newTimestamp := strconv.FormatInt(timestamp, 10) - hmac := common.GetHMAC(common.HashSHA256, + hmac := crypto.GetHMAC(crypto.HashSHA256, []byte("GET/realtime"+newTimestamp), - []byte(b.APISecret)) - signature := common.HexEncodeToString(hmac) + []byte(b.API.Credentials.Secret)) + signature := crypto.HexEncodeToString(hmac) + var sendAuth WebsocketRequest sendAuth.Command = "authKeyExpires" - sendAuth.Arguments = append(sendAuth.Arguments, b.APIKey, timestamp, + sendAuth.Arguments = append(sendAuth.Arguments, b.API.Credentials.Key, timestamp, signature) err := b.WebsocketConn.SendMessage(sendAuth) if err != nil { diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 4a4ae9b0..4f65011d 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -2,20 +2,200 @@ package bitmex import ( "errors" - "fmt" "math" "strings" "sync" + "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (b *Bitmex) GetDefaultConfig() (*config.ExchangeConfig, error) { + b.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = b.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = b.BaseCurrencies + + err := b.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if b.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = b.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for Bitmex +func (b *Bitmex) SetDefaults() { + b.Name = "Bitmex" + b.Enabled = true + b.Verbose = true + b.API.CredentialsValidator.RequiresKey = true + b.API.CredentialsValidator.RequiresSecret = true + + b.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.PerpetualContract, + asset.Futures, + asset.DownsideProfitContract, + asset.UpsideProfitContract, + }, + UseGlobalFormat: false, + } + + // Same format used for perpetual contracts and futures + fmt1 := currency.PairStore{ + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + } + b.CurrencyPairs.Store(asset.PerpetualContract, fmt1) + b.CurrencyPairs.Store(asset.Futures, fmt1) + + // Upside and Downside profit contracts use the same format + fmt2 := currency.PairStore{ + RequestFormat: ¤cy.PairFormat{ + Delimiter: "_", + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "_", + Uppercase: true, + }, + } + b.CurrencyPairs.Store(asset.DownsideProfitContract, fmt2) + b.CurrencyPairs.Store(asset.UpsideProfitContract, fmt2) + + b.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + SubmitOrders: true, + ModifyOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TradeFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + AccountInfo: true, + DeadMansSwitch: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | + exchange.WithdrawCryptoWithEmail | + exchange.WithdrawCryptoWith2FA | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + b.Requester = request.New(b.Name, + request.NewRateLimit(time.Second, bitmexAuthRate), + request.NewRateLimit(time.Second, bitmexUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + b.API.Endpoints.URLDefault = bitmexAPIURL + b.API.Endpoints.URL = b.API.Endpoints.URLDefault + b.API.Endpoints.WebsocketURL = bitmexWSURL + b.Websocket = wshandler.New() + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup takes in the supplied exchange configuration details and sets params +func (b *Bitmex) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + b.SetEnabled(false) + return nil + } + + err := b.SetupDefaults(exch) + if err != nil { + return err + } + + err = b.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: bitmexWSURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: b.WsConnect, + Subscriber: b.Subscribe, + UnSubscriber: b.Unsubscribe, + Features: &b.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + b.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + false, + false, + false, + true, + exch.Name) + return nil +} + // Start starts the Bitmex go routine func (b *Bitmex) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -28,92 +208,150 @@ func (b *Bitmex) Start(wg *sync.WaitGroup) { // Run implements the Bitmex wrapper func (b *Bitmex) Run() { if b.Verbose { - log.Debugf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()), b.WebsocketURL) - log.Debugf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) + log.Debugf(log.ExchangeSys, "%s Websocket: %s. (url: %s).\n", b.Name, common.IsEnabled(b.Websocket.IsEnabled()), b.API.Endpoints.WebsocketURL) + b.PrintEnabledPairs() } + if !b.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := b.UpdateTradablePairs(false) + if err != nil { + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", b.Name, err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (b *Bitmex) FetchTradablePairs(asset asset.Item) ([]string, error) { marketInfo, err := b.GetActiveInstruments(&GenericRequestParams{}) if err != nil { - log.Errorf("%s Failed to get available symbols.\n", b.GetName()) - } else { - var exchangeProducts []string - for i := range marketInfo { - exchangeProducts = append(exchangeProducts, marketInfo[i].Symbol) - } - - var NewExchangeProducts currency.Pairs - for _, p := range exchangeProducts { - NewExchangeProducts = append(NewExchangeProducts, - currency.NewPairFromString(p)) - } - - err = b.UpdateCurrencies(NewExchangeProducts, false, false) - if err != nil { - log.Errorf("%s Failed to update available currencies.\n", b.GetName()) - } + return nil, err } + + var products []string + for x := range marketInfo { + products = append(products, marketInfo[x].Symbol.String()) + } + + return products, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (b *Bitmex) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := b.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + var assetPairs []string + for x := range b.CurrencyPairs.AssetTypes { + switch b.CurrencyPairs.AssetTypes[x] { + case asset.PerpetualContract: + for y := range pairs { + if strings.Contains(pairs[y], "USD") { + assetPairs = append(assetPairs, pairs[y]) + } + } + case asset.Futures: + for y := range pairs { + if strings.Contains(pairs[y], "19") { + assetPairs = append(assetPairs, pairs[y]) + } + } + case asset.DownsideProfitContract: + for y := range pairs { + if strings.Contains(pairs[y], "_D") { + assetPairs = append(assetPairs, pairs[y]) + } + } + case asset.UpsideProfitContract: + for y := range pairs { + if strings.Contains(pairs[y], "_U") { + assetPairs = append(assetPairs, pairs[y]) + } + } + } + + err = b.UpdatePairs(currency.NewPairsFromStrings(assetPairs), b.CurrencyPairs.AssetTypes[x], false, false) + if err != nil { + log.Warnf(log.ExchangeSys, "%s failed to update available pairs. Err: %v", b.Name, err) + } + assetPairs = nil + } + return nil } // UpdateTicker updates and returns the ticker for a currency pair -func (b *Bitmex) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (b *Bitmex) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - currency := exchange.FormatExchangeCurrency(b.Name, p) - - tick, err := b.GetTrade(&GenericRequestParams{ - Symbol: currency.String(), - Reverse: true, - Count: 1}) + tick, err := b.GetActiveInstruments(&GenericRequestParams{}) if err != nil { return tickerPrice, err } - - if len(tick) == 0 { - return tickerPrice, fmt.Errorf("%s REST error: no ticker return", b.Name) + pairs := b.GetEnabledPairs(assetType) + for i := range pairs { + for j := range tick { + if !pairs[i].Equal(tick[j].Symbol) { + continue + } + tickerPrice = ticker.Price{ + Last: tick[j].LastPrice, + High: tick[j].HighPrice, + Low: tick[j].LowPrice, + Bid: tick[j].BidPrice, + Ask: tick[j].AskPrice, + Volume: tick[j].Volume24h, + Close: tick[j].PrevClosePrice, + Pair: tick[j].Symbol, + LastUpdated: tick[j].Timestamp, + } + err = ticker.ProcessTicker(b.Name, &tickerPrice, assetType) + if err != nil { + log.Error(log.Ticker, err) + } + } } - - tickerPrice.Pair = p - tickerPrice.Last = tick[0].Price - tickerPrice.Volume = float64(tick[0].Size) - return tickerPrice, ticker.ProcessTicker(b.Name, &tickerPrice, assetType) + return ticker.GetTicker(b.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (b *Bitmex) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (b *Bitmex) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(b.Name, p, assetType) if err != nil { return b.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (b *Bitmex) GetOrderbookEx(currency currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(b.GetName(), currency, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (b *Bitmex) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(b.Name, p, assetType) if err != nil { - return b.UpdateOrderbook(currency, assetType) + return b.UpdateOrderbook(p, assetType) } return ob, nil } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (b *Bitmex) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (b *Bitmex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base orderbookNew, err := b.GetOrderbook(OrderBookGetL2Params{ - Symbol: exchange.FormatExchangeCurrency(b.Name, p).String(), + Symbol: b.FormatExchangeCurrency(p, assetType).String(), Depth: 500}) if err != nil { return orderBook, err } for _, ob := range orderbookNew { - if strings.EqualFold(ob.Side, exchange.SellOrderSide.ToString()) { + if strings.EqualFold(ob.Side, order.Sell.String()) { orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: float64(ob.Size), Price: ob.Price}) continue } - if strings.EqualFold(ob.Side, exchange.BuyOrderSide.ToString()) { + if strings.EqualFold(ob.Side, order.Buy.String()) { orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: float64(ob.Size), Price: ob.Price}) continue @@ -121,7 +359,7 @@ func (b *Bitmex) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.B } orderBook.Pair = p - orderBook.ExchangeName = b.GetName() + orderBook.ExchangeName = b.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -151,7 +389,7 @@ func (b *Bitmex) GetAccountInfo() (exchange.AccountInfo, error) { }) } - info.Exchange = b.GetName() + info.Exchange = b.Name info.Accounts = append(info.Accounts, exchange.Account{ Currencies: balances, }) @@ -166,47 +404,51 @@ func (b *Bitmex) GetFundingHistory() ([]exchange.FundHistory, error) { } // GetExchangeHistory returns historic trade data since exchange opening. -func (b *Bitmex) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (b *Bitmex) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (b *Bitmex) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse +func (b *Bitmex) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } - if math.Mod(amount, 1) != 0 { + if math.Mod(s.Amount, 1) != 0 { return submitOrderResponse, - errors.New("contract amount can not have decimals") + errors.New("order contract amount can not have decimals") } var orderNewParams = OrderNewParams{ - OrdType: side.ToString(), - Symbol: p.String(), - OrderQty: amount, - Side: side.ToString(), + OrdType: s.OrderSide.String(), + Symbol: s.Pair.String(), + OrderQty: s.Amount, + Side: s.OrderSide.String(), } - if orderType == exchange.LimitOrderType { - orderNewParams.Price = price + if s.OrderType == order.Limit { + orderNewParams.Price = s.Price } response, err := b.CreateOrder(&orderNewParams) + if err != nil { + return submitOrderResponse, err + } if response.OrderID != "" { submitOrderResponse.OrderID = response.OrderID } - - if err == nil { - submitOrderResponse.IsOrderPlaced = true + if s.OrderType == order.Market { + submitOrderResponse.FullyMatched = true } + submitOrderResponse.IsOrderPlaced = true - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (b *Bitmex) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (b *Bitmex) ModifyOrder(action *order.Modify) (string, error) { var params OrderAmendParams if math.Mod(action.Amount, 1) != 0 { @@ -226,19 +468,18 @@ func (b *Bitmex) ModifyOrder(action *exchange.ModifyOrder) (string, error) { } // CancelOrder cancels an order by its corresponding ID number -func (b *Bitmex) CancelOrder(order *exchange.OrderCancellation) error { +func (b *Bitmex) CancelOrder(order *order.Cancel) error { var params = OrderCancelParams{ OrderID: order.OrderID, } _, err := b.CancelOrders(¶ms) - return err } // CancelAllOrders cancels all orders associated with a currency pair -func (b *Bitmex) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (b *Bitmex) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } var emptyParams OrderCancelAllParams orders, err := b.CancelAllExistingOrders(emptyParams) @@ -248,7 +489,7 @@ func (b *Bitmex) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cancel for i := range orders { if orders[i].OrdRejReason != "" { - cancelAllOrdersResponse.OrderStatus[orders[i].OrderID] = orders[i].OrdRejReason + cancelAllOrdersResponse.Status[orders[i].OrderID] = orders[i].OrdRejReason } } @@ -256,8 +497,8 @@ func (b *Bitmex) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cancel } // GetOrderInfo returns information on a current open order -func (b *Bitmex) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (b *Bitmex) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -268,7 +509,7 @@ func (b *Bitmex) GetDepositAddress(cryptocurrency currency.Code, _ string) (stri // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *Bitmex) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bitmex) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { var request = UserRequestWithdrawalParams{ Address: withdrawRequest.Address, Amount: withdrawRequest.Amount, @@ -289,13 +530,13 @@ func (b *Bitmex) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawR // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *Bitmex) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bitmex) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted -func (b *Bitmex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bitmex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -306,7 +547,7 @@ func (b *Bitmex) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (b *Bitmex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (b.APIKey == "" || b.APISecret == "") && // Todo check connection status + if !b.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -315,8 +556,8 @@ func (b *Bitmex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) // GetActiveOrders retrieves any orders that are active/open // This function is not concurrency safe due to orderSide/orderType maps -func (b *Bitmex) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var orders []exchange.OrderDetail +func (b *Bitmex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { + var orders []order.Detail params := OrdersRequest{} params.Filter = "{\"open\":true}" @@ -329,39 +570,37 @@ func (b *Bitmex) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([ orderSide := orderSideMap[resp[i].Side] orderType := orderTypeMap[resp[i].OrdType] if orderType == "" { - orderType = exchange.UnknownOrderType + orderType = order.Unknown } - orderDetail := exchange.OrderDetail{ + orderDetail := order.Detail{ Price: resp[i].Price, Amount: float64(resp[i].OrderQty), Exchange: b.Name, ID: resp[i].OrderID, OrderSide: orderSide, OrderType: orderType, - Status: resp[i].OrdStatus, + Status: order.Status(resp[i].OrdStatus), CurrencyPair: currency.NewPairWithDelimiter(resp[i].Symbol, resp[i].SettlCurrency, - b.ConfigCurrencyPairFormat.Delimiter), + b.GetPairFormat(asset.PerpetualContract, false).Delimiter), } orders = append(orders, orderDetail) } - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status // This function is not concurrency safe due to orderSide/orderType maps -func (b *Bitmex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var orders []exchange.OrderDetail +func (b *Bitmex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + var orders []order.Detail params := OrdersRequest{} resp, err := b.GetOrders(¶ms) if err != nil { @@ -372,30 +611,29 @@ func (b *Bitmex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ orderSide := orderSideMap[resp[i].Side] orderType := orderTypeMap[resp[i].OrdType] if orderType == "" { - orderType = exchange.UnknownOrderType + orderType = order.Unknown } - orderDetail := exchange.OrderDetail{ + orderDetail := order.Detail{ Price: resp[i].Price, Amount: float64(resp[i].OrderQty), Exchange: b.Name, ID: resp[i].OrderID, OrderSide: orderSide, OrderType: orderType, - Status: resp[i].OrdStatus, + Status: order.Status(resp[i].OrdStatus), CurrencyPair: currency.NewPairWithDelimiter(resp[i].Symbol, resp[i].SettlCurrency, - b.ConfigCurrencyPairFormat.Delimiter), + b.GetPairFormat(asset.PerpetualContract, false).Delimiter), } orders = append(orders, orderDetail) } - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } diff --git a/exchanges/bitstamp/README.md b/exchanges/bitstamp/README.md index c25ab16e..0acefd1f 100644 --- a/exchanges/bitstamp/README.md +++ b/exchanges/bitstamp/README.md @@ -48,22 +48,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bitstamp" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bitstamp" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } @@ -138,4 +138,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/bitstamp/bitstamp.go b/exchanges/bitstamp/bitstamp.go index d1d198cf..ad254a60 100644 --- a/exchanges/bitstamp/bitstamp.go +++ b/exchanges/bitstamp/bitstamp.go @@ -7,17 +7,15 @@ import ( "fmt" "net/http" "net/url" - "reflect" "strconv" "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -36,14 +34,13 @@ const ( bitstampAPIOrderStatus = "order_status" bitstampAPICancelOrder = "cancel_order" bitstampAPICancelAllOrders = "cancel_all_orders" - bitstampAPIBuy = "buy" - bitstampAPISell = "sell" bitstampAPIMarket = "market" bitstampAPIWithdrawalRequests = "withdrawal_requests" bitstampAPIOpenWithdrawal = "withdrawal/open" bitstampAPIBitcoinWithdrawal = "bitcoin_withdrawal" bitstampAPILTCWithdrawal = "ltc_withdrawal" bitstampAPIETHWithdrawal = "eth_withdrawal" + bitstampAPIBCHWithdrawal = "bch_withdrawal" bitstampAPIBitcoinDeposit = "bitcoin_deposit_address" bitstampAPILitecoinDeposit = "ltc_address" bitstampAPIEthereumDeposit = "eth_address" @@ -56,8 +53,9 @@ const ( bitstampAPIReturnType = "string" bitstampAPITradingPairsInfo = "trading-pairs-info" - bitstampAuthRate = 600 - bitstampUnauthRate = 600 + bitstampAuthRate = 8000 + bitstampUnauthRate = 8000 + bitstampTimeLayout = "2006-1-2 15:04:05" ) // Bitstamp is the overarching type across the bitstamp package @@ -66,109 +64,6 @@ type Bitstamp struct { WebsocketConn *wshandler.WebsocketConnection } -// SetDefaults sets default for Bitstamp -func (b *Bitstamp) SetDefaults() { - b.Name = "Bitstamp" - b.Enabled = false - b.Verbose = false - b.RESTPollingDelay = 10 - b.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | - exchange.AutoWithdrawFiat - b.RequestCurrencyPairFormat.Delimiter = "" - b.RequestCurrencyPairFormat.Uppercase = true - b.ConfigCurrencyPairFormat.Delimiter = "" - b.ConfigCurrencyPairFormat.Uppercase = true - b.AssetTypes = []string{ticker.Spot} - b.SupportsAutoPairUpdating = true - b.SupportsRESTTickerBatching = false - b.Requester = request.New(b.Name, - request.NewRateLimit(time.Minute*10, bitstampAuthRate), - request.NewRateLimit(time.Minute*10, bitstampUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - b.APIUrlDefault = bitstampAPIURL - b.APIUrl = b.APIUrlDefault - b.Websocket = wshandler.New() - b.Websocket.Functionality = wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported - b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup sets configuration values to bitstamp -func (b *Bitstamp) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - b.SetEnabled(false) - } else { - b.Enabled = true - b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - b.SetAPIKeys(exch.APIKey, exch.APISecret, exch.ClientID, false) - b.SetHTTPClientTimeout(exch.HTTPTimeout) - b.SetHTTPClientUserAgent(exch.HTTPUserAgent) - b.RESTPollingDelay = exch.RESTPollingDelay - b.Verbose = exch.Verbose - b.HTTPDebugging = exch.HTTPDebugging - b.Websocket.SetWsStatusAndConnection(exch.Websocket) - b.BaseCurrencies = exch.BaseCurrencies - b.AvailablePairs = exch.AvailablePairs - b.EnabledPairs = exch.EnabledPairs - b.APIKey = exch.APIKey - b.APISecret = exch.APISecret - b.SetAPIKeys(exch.APIKey, exch.APISecret, b.ClientID, false) - b.AuthenticatedAPISupport = true - b.WebsocketURL = bitstampWSURL - err := b.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = b.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = b.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = b.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = b.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = b.Websocket.Setup(b.WsConnect, - b.Subscribe, - b.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - bitstampWSURL, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - b.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: b.Name, - URL: b.Websocket.GetWebsocketURL(), - ProxyURL: b.Websocket.GetProxyAddress(), - Verbose: b.Verbose, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - b.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - true, - true, - false, - exch.Name) - } -} - // GetFee returns an estimate of fee based on type of transaction func (b *Bitstamp) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { var fee float64 @@ -228,22 +123,17 @@ func getInternationalBankDepositFee(amount float64) float64 { } // CalculateTradingFee returns fee on a currency pair -func (b *Bitstamp) CalculateTradingFee(base, quote currency.Code, purchasePrice, amount float64, balances *Balances) float64 { +func (b *Bitstamp) CalculateTradingFee(base, quote currency.Code, purchasePrice, amount float64, balances Balances) float64 { var fee float64 - - switch base.String() + quote.String() { - case currency.BTC.String() + currency.USD.String(): - fee = balances.BTCUSDFee - case currency.BTC.String() + currency.EUR.String(): - fee = balances.BTCEURFee - case currency.XRP.String() + currency.EUR.String(): - fee = balances.XRPEURFee - case currency.XRP.String() + currency.USD.String(): - fee = balances.XRPUSDFee - case currency.EUR.String() + currency.USD.String(): - fee = balances.EURUSDFee - default: - fee = 0 + if v, ok := balances[base.String()]; ok { + switch quote { + case currency.BTC: + fee = v.BTCFee + case currency.USD: + fee = v.USDFee + case currency.EUR: + fee = v.EURFee + } } return fee * purchasePrice * amount } @@ -259,10 +149,10 @@ func (b *Bitstamp) GetTicker(currency string, hourly bool) (Ticker, error) { path := fmt.Sprintf( "%s/v%s/%s/%s/", - b.APIUrl, + b.API.Endpoints.URL, bitstampAPIVersion, tickerEndpoint, - common.StringToLower(currency), + strings.ToLower(currency), ) return response, b.SendHTTPRequest(path, &response) } @@ -280,10 +170,10 @@ func (b *Bitstamp) GetOrderbook(currency string) (Orderbook, error) { path := fmt.Sprintf( "%s/v%s/%s/%s/", - b.APIUrl, + b.API.Endpoints.URL, bitstampAPIVersion, bitstampAPIOrderbook, - common.StringToLower(currency), + strings.ToLower(currency), ) err := b.SendHTTPRequest(path, &resp) @@ -297,12 +187,12 @@ func (b *Bitstamp) GetOrderbook(currency string) (Orderbook, error) { for _, x := range resp.Bids { price, err := strconv.ParseFloat(x[0], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } amount, err := strconv.ParseFloat(x[1], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } orderbook.Bids = append(orderbook.Bids, OrderbookBase{price, amount}) @@ -311,12 +201,12 @@ func (b *Bitstamp) GetOrderbook(currency string) (Orderbook, error) { for _, x := range resp.Asks { price, err := strconv.ParseFloat(x[0], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } amount, err := strconv.ParseFloat(x[1], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } orderbook.Asks = append(orderbook.Asks, OrderbookBase{price, amount}) @@ -331,7 +221,7 @@ func (b *Bitstamp) GetTradingPairs() ([]TradingPair, error) { var result []TradingPair path := fmt.Sprintf("%s/v%s/%s", - b.APIUrl, + b.API.Endpoints.URL, bitstampAPIVersion, bitstampAPITradingPairsInfo) @@ -346,10 +236,10 @@ func (b *Bitstamp) GetTransactions(currencyPair string, values url.Values) ([]Tr path := common.EncodeURLValues( fmt.Sprintf( "%s/v%s/%s/%s/", - b.APIUrl, + b.API.Endpoints.URL, bitstampAPIVersion, bitstampAPITransactions, - common.StringToLower(currencyPair), + strings.ToLower(currencyPair), ), values, ) @@ -360,31 +250,68 @@ func (b *Bitstamp) GetTransactions(currencyPair string, values url.Values) ([]Tr // GetEURUSDConversionRate returns the conversion rate between Euro and USD func (b *Bitstamp) GetEURUSDConversionRate() (EURUSDConversionRate, error) { rate := EURUSDConversionRate{} - path := fmt.Sprintf("%s/%s", b.APIUrl, bitstampAPIEURUSD) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bitstampAPIEURUSD) return rate, b.SendHTTPRequest(path, &rate) } // GetBalance returns full balance of currency held on the exchange -func (b *Bitstamp) GetBalance() (*Balances, error) { - var balance Balances - return &balance, - b.SendAuthenticatedHTTPRequest(bitstampAPIBalance, true, nil, &balance) +func (b *Bitstamp) GetBalance() (Balances, error) { + var balance map[string]string + err := b.SendAuthenticatedHTTPRequest(bitstampAPIBalance, true, nil, &balance) + if err != nil { + return nil, err + } + + balances := make(map[string]Balance) + for k := range balance { + curr := k[0:3] + _, ok := balances[strings.ToUpper(curr)] + if !ok { + avail, _ := strconv.ParseFloat(balance[curr+"_available"], 64) + bal, _ := strconv.ParseFloat(balance[curr+"_balance"], 64) + reserved, _ := strconv.ParseFloat(balance[curr+"_reserved"], 64) + withdrawalFee, _ := strconv.ParseFloat(balance[curr+"_withdrawal_fee"], 64) + currBalance := Balance{ + Available: avail, + Balance: bal, + Reserved: reserved, + WithdrawalFee: withdrawalFee, + } + switch strings.ToUpper(curr) { + case currency.USD.String(): + eurFee, _ := strconv.ParseFloat(balance[curr+"eur_fee"], 64) + currBalance.EURFee = eurFee + case currency.EUR.String(): + usdFee, _ := strconv.ParseFloat(balance[curr+"usd_fee"], 64) + currBalance.USDFee = usdFee + default: + btcFee, _ := strconv.ParseFloat(balance[curr+"btc_fee"], 64) + currBalance.BTCFee = btcFee + eurFee, _ := strconv.ParseFloat(balance[curr+"eur_fee"], 64) + currBalance.EURFee = eurFee + usdFee, _ := strconv.ParseFloat(balance[curr+"usd_fee"], 64) + currBalance.USDFee = usdFee + } + balances[strings.ToUpper(curr)] = currBalance + } + } + return balances, nil } // GetUserTransactions returns an array of transactions func (b *Bitstamp) GetUserTransactions(currencyPair string) ([]UserTransactions, error) { type Response struct { - Date string `json:"datetime"` - TransID int64 `json:"id"` - Type int `json:"type,string"` - USD interface{} `json:"usd"` - EUR float64 `json:"eur"` - XRP float64 `json:"xrp"` - BTC interface{} `json:"btc"` - BTCUSD interface{} `json:"btc_usd"` - Fee float64 `json:"fee,string"` - OrderID int64 `json:"order_id"` + Date string `json:"datetime"` + TransactionID int64 `json:"id"` + Type int `json:"type,string"` + USD interface{} `json:"usd"` + EUR interface{} `json:"eur"` + XRP interface{} `json:"xrp"` + BTC interface{} `json:"btc"` + BTCUSD interface{} `json:"btc_usd"` + Fee float64 `json:"fee,string"` + OrderID int64 `json:"order_id"` } var response []Response @@ -404,41 +331,31 @@ func (b *Bitstamp) GetUserTransactions(currencyPair string) ([]UserTransactions, } } + processNumber := func(i interface{}) float64 { + switch t := i.(type) { + case float64: + return t + case string: + amt, _ := strconv.ParseFloat(t, 64) + return amt + default: + return 0 + } + } + var transactions []UserTransactions - - for _, y := range response { + for x := range response { tx := UserTransactions{} - tx.Date = y.Date - tx.TransID = y.TransID - tx.Type = y.Type - - /* Hack due to inconsistent JSON values... */ - varType := reflect.TypeOf(y.USD).String() - if varType == bitstampAPIReturnType { - tx.USD, _ = strconv.ParseFloat(y.USD.(string), 64) - } else { - tx.USD = y.USD.(float64) - } - - tx.EUR = y.EUR - tx.XRP = y.XRP - - varType = reflect.TypeOf(y.BTC).String() - if varType == bitstampAPIReturnType { - tx.BTC, _ = strconv.ParseFloat(y.BTC.(string), 64) - } else { - tx.BTC = y.BTC.(float64) - } - - varType = reflect.TypeOf(y.BTCUSD).String() - if varType == bitstampAPIReturnType { - tx.BTCUSD, _ = strconv.ParseFloat(y.BTCUSD.(string), 64) - } else { - tx.BTCUSD = y.BTCUSD.(float64) - } - - tx.Fee = y.Fee - tx.OrderID = y.OrderID + tx.Date = response[x].Date + tx.TransactionID = response[x].TransactionID + tx.Type = response[x].Type + tx.EUR = processNumber(response[x].EUR) + tx.XRP = processNumber(response[x].XRP) + tx.USD = processNumber(response[x].USD) + tx.BTC = processNumber(response[x].BTC) + tx.BTCUSD = processNumber(response[x].BTCUSD) + tx.Fee = response[x].Fee + tx.OrderID = response[x].OrderID transactions = append(transactions, tx) } @@ -449,7 +366,7 @@ func (b *Bitstamp) GetUserTransactions(currencyPair string) ([]UserTransactions, func (b *Bitstamp) GetOpenOrders(currencyPair string) ([]Order, error) { var resp []Order path := fmt.Sprintf( - "%s/%s", bitstampAPIOpenOrders, common.StringToLower(currencyPair), + "%s/%s", bitstampAPIOpenOrders, strings.ToLower(currencyPair), ) return resp, b.SendAuthenticatedHTTPRequest(path, true, nil, &resp) @@ -466,13 +383,17 @@ func (b *Bitstamp) GetOrderStatus(orderID int64) (OrderStatus, error) { } // CancelExistingOrder cancels order by ID -func (b *Bitstamp) CancelExistingOrder(orderID int64) (bool, error) { - result := false +func (b *Bitstamp) CancelExistingOrder(orderID int64) (CancelOrder, error) { var req = url.Values{} req.Add("id", strconv.FormatInt(orderID, 10)) - return result, - b.SendAuthenticatedHTTPRequest(bitstampAPICancelOrder, true, req, &result) + var result CancelOrder + err := b.SendAuthenticatedHTTPRequest(bitstampAPICancelOrder, true, req, &result) + if err != nil { + return result, err + } + + return result, nil } // CancelAllExistingOrders cancels all open orders on the exchange @@ -489,17 +410,17 @@ func (b *Bitstamp) PlaceOrder(currencyPair string, price, amount float64, buy, m req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) req.Add("price", strconv.FormatFloat(price, 'f', -1, 64)) response := Order{} - orderType := bitstampAPIBuy + orderType := order.Buy.Lower() if !buy { - orderType = bitstampAPISell + orderType = order.Sell.Lower() } var path string if market { - path = fmt.Sprintf("%s/%s/%s", orderType, bitstampAPIMarket, common.StringToLower(currencyPair)) + path = fmt.Sprintf("%s/%s/%s", orderType, bitstampAPIMarket, strings.ToLower(currencyPair)) } else { - path = fmt.Sprintf("%s/%s", orderType, common.StringToLower(currencyPair)) + path = fmt.Sprintf("%s/%s", orderType, strings.ToLower(currencyPair)) } return response, @@ -539,23 +460,25 @@ func (b *Bitstamp) CryptoWithdrawal(amount float64, address, symbol, destTag str resp := CryptoWithdrawalResponse{} var endpoint string - switch common.StringToLower(symbol) { - case "btc": + switch strings.ToLower(symbol) { + case currency.BTC.Lower().String(): if instant { req.Add("instant", "1") } else { req.Add("instant", "0") } endpoint = bitstampAPIBitcoinWithdrawal - case "ltc": + case currency.LTC.Lower().String(): endpoint = bitstampAPILTCWithdrawal - case "eth": + case currency.ETH.Lower().String(): endpoint = bitstampAPIETHWithdrawal - case "xrp": + case currency.XRP.Lower().String(): if destTag != "" { req.Add("destination_tag", destTag) } endpoint = bitstampAPIXrpWithdrawal + case currency.BCH.Lower().String(): + endpoint = bitstampAPIBCHWithdrawal default: return resp, errors.New("incorrect symbol") } @@ -616,6 +539,9 @@ func (b *Bitstamp) OpenInternationalBankWithdrawal(amount float64, currency, // crypto - example "btc", "ltc", "eth", "xrp" or "bch" func (b *Bitstamp) GetCryptoDepositAddress(crypto currency.Code) (string, error) { var resp string + v2Resp := struct { + Address string `json:"address"` + }{} switch crypto { case currency.BTC: @@ -623,20 +549,20 @@ func (b *Bitstamp) GetCryptoDepositAddress(crypto currency.Code) (string, error) b.SendAuthenticatedHTTPRequest(bitstampAPIBitcoinDeposit, false, nil, &resp) case currency.LTC: - return resp, - b.SendAuthenticatedHTTPRequest(bitstampAPILitecoinDeposit, true, nil, &resp) + return v2Resp.Address, + b.SendAuthenticatedHTTPRequest(bitstampAPILitecoinDeposit, true, nil, &v2Resp) case currency.ETH: - return resp, - b.SendAuthenticatedHTTPRequest(bitstampAPIEthereumDeposit, true, nil, &resp) + return v2Resp.Address, + b.SendAuthenticatedHTTPRequest(bitstampAPIEthereumDeposit, true, nil, &v2Resp) case currency.XRP: - return resp, - b.SendAuthenticatedHTTPRequest(bitstampAPIXrpDeposit, true, nil, &resp) + return v2Resp.Address, + b.SendAuthenticatedHTTPRequest(bitstampAPIXrpDeposit, true, nil, &v2Resp) case currency.BCH: - return resp, - b.SendAuthenticatedHTTPRequest(bitstampAPIBitcoinCashDeposit, true, nil, &resp) + return v2Resp.Address, + b.SendAuthenticatedHTTPRequest(bitstampAPIBitcoinCashDeposit, true, nil, &v2Resp) default: return resp, fmt.Errorf("unsupported cryptocurrency string %s", crypto) @@ -695,7 +621,7 @@ func (b *Bitstamp) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated request func (b *Bitstamp) SendAuthenticatedHTTPRequest(path string, v2 bool, values url.Values, result interface{}) error { - if !b.AuthenticatedAPISupport { + if !b.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } @@ -705,19 +631,21 @@ func (b *Bitstamp) SendAuthenticatedHTTPRequest(path string, v2 bool, values url values = url.Values{} } - values.Set("key", b.APIKey) + values.Set("key", b.API.Credentials.Key) values.Set("nonce", n) - hmac := common.GetHMAC(common.HashSHA256, []byte(n+b.ClientID+b.APIKey), []byte(b.APISecret)) - values.Set("signature", common.StringToUpper(common.HexEncodeToString(hmac))) + hmac := crypto.GetHMAC(crypto.HashSHA256, + []byte(n+b.API.Credentials.ClientID+b.API.Credentials.Key), + []byte(b.API.Credentials.Secret)) + values.Set("signature", strings.ToUpper(crypto.HexEncodeToString(hmac))) if v2 { - path = fmt.Sprintf("%s/v%s/%s/", b.APIUrl, bitstampAPIVersion, path) + path = fmt.Sprintf("%s/v%s/%s/", b.API.Endpoints.URL, bitstampAPIVersion, path) } else { - path = fmt.Sprintf("%s/%s/", b.APIUrl, path) + path = fmt.Sprintf("%s/%s/", b.API.Endpoints.URL, path) } if b.Verbose { - log.Debugf("Sending POST request to " + path) + log.Debugf(log.ExchangeSys, "Sending POST request to "+path) } headers := make(map[string]string) @@ -748,16 +676,16 @@ func (b *Bitstamp) SendAuthenticatedHTTPRequest(path string, v2 bool, values url return err } - if err := common.JSONDecode(interim, &errCap); err == nil { + if err := json.Unmarshal(interim, &errCap); err == nil { if errCap.Error != "" { return errors.New(errCap.Error) } if data, ok := errCap.Reason.(map[string][]string); ok { - var details string - for _, v := range data { - details += strings.Join(v, "") + var details strings.Builder + for x := range data { + details.WriteString(strings.Join(data[x], "")) } - return errors.New(details) + return errors.New(details.String()) } if data, ok := errCap.Reason.(string); ok { @@ -769,5 +697,9 @@ func (b *Bitstamp) SendAuthenticatedHTTPRequest(path string, v2 bool, values url } } - return common.JSONDecode(interim, result) + return json.Unmarshal(interim, result) +} + +func parseTime(dateTime string) (time.Time, error) { + return time.Parse(bitstampTimeLayout, dateTime) } diff --git a/exchanges/bitstamp/bitstamp_live_test.go b/exchanges/bitstamp/bitstamp_live_test.go index 5c6a6edc..66d0ef87 100644 --- a/exchanges/bitstamp/bitstamp_live_test.go +++ b/exchanges/bitstamp/bitstamp_live_test.go @@ -17,17 +17,23 @@ var mockTests = false func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Bitstamp load config error", err) + } bitstampConfig, err := cfg.GetExchangeConfig("Bitstamp") if err != nil { - log.Fatal("Test Failed - Bitstamp Setup() init error", err) + log.Fatal("Bitstamp Setup() init error", err) } - bitstampConfig.AuthenticatedAPISupport = true - bitstampConfig.APIKey = apiKey - bitstampConfig.APISecret = apiSecret - bitstampConfig.ClientID = customerID + bitstampConfig.API.AuthenticatedSupport = true + bitstampConfig.API.Credentials.Key = apiKey + bitstampConfig.API.Credentials.Secret = apiSecret + bitstampConfig.API.Credentials.ClientID = customerID b.SetDefaults() - b.Setup(&bitstampConfig) - log.Printf(sharedtestvalues.LiveTesting, b.GetName(), b.APIUrl) + err = b.Setup(bitstampConfig) + if err != nil { + log.Fatal("Bitstamp setup error", err) + } + log.Printf(sharedtestvalues.LiveTesting, b.Name, b.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/bitstamp/bitstamp_mock_test.go b/exchanges/bitstamp/bitstamp_mock_test.go index fa956c96..ffb3943f 100644 --- a/exchanges/bitstamp/bitstamp_mock_test.go +++ b/exchanges/bitstamp/bitstamp_mock_test.go @@ -20,26 +20,33 @@ var mockTests = true func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Bitstamp load config error", err) + } bitstampConfig, err := cfg.GetExchangeConfig("Bitstamp") if err != nil { - log.Fatal("Test Failed - Bitstamp Setup() init error", err) + log.Fatal("Bitstamp Setup() init error", err) } - bitstampConfig.AuthenticatedAPISupport = true - bitstampConfig.APIKey = apiKey - bitstampConfig.APISecret = apiSecret - bitstampConfig.ClientID = customerID + b.SkipAuthCheck = true + bitstampConfig.API.AuthenticatedSupport = true + bitstampConfig.API.Credentials.Key = apiKey + bitstampConfig.API.Credentials.Secret = apiSecret + bitstampConfig.API.Credentials.ClientID = customerID b.SetDefaults() - b.Setup(&bitstampConfig) + err = b.Setup(bitstampConfig) + if err != nil { + log.Fatal("Bitstamp setup error", err) + } serverDetails, newClient, err := mock.NewVCRServer(mockfile) if err != nil { - log.Fatalf("Test Failed - Mock server error %s", err) + log.Fatalf("Mock server error %s", err) } b.HTTPClient = newClient - b.APIUrl = serverDetails + "/api" + b.API.Endpoints.URL = serverDetails + "/api" - log.Printf(sharedtestvalues.MockTesting, b.GetName(), b.APIUrl) + log.Printf(sharedtestvalues.MockTesting, b.Name, b.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/bitstamp/bitstamp_test.go b/exchanges/bitstamp/bitstamp_test.go index d6364750..39d7dfd4 100644 --- a/exchanges/bitstamp/bitstamp_test.go +++ b/exchanges/bitstamp/bitstamp_test.go @@ -6,6 +6,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // Please add your private keys and customerID for better tests @@ -19,11 +20,7 @@ const ( var b Bitstamp func areTestAPIKeysSet() bool { - if b.APIKey != "" && b.APIKey != "Key" && - b.APISecret != "" && b.APISecret != "Secret" { - return true - } - return false + return b.ValidateAPICredentials() } func setFeeBuilder() *exchange.FeeBuilder { @@ -41,7 +38,7 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() b.GetFeeByType(feeBuilder) - if !areTestAPIKeysSet() || mockTests { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, @@ -64,7 +61,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee Basic if resp, err := b.GetFee(feeBuilder); resp != float64(0) || (areTestAPIKeysSet() && err != nil) { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) } @@ -74,7 +71,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := b.GetFee(feeBuilder); resp != float64(0) || (areTestAPIKeysSet() && err != nil) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) @@ -84,7 +81,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := b.GetFee(feeBuilder); resp != float64(0) || (areTestAPIKeysSet() && err != nil) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) @@ -94,7 +91,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := b.GetFee(feeBuilder); resp != float64(0) || (areTestAPIKeysSet() && err != nil) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) @@ -104,7 +101,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) @@ -114,7 +111,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) @@ -125,7 +122,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(7.5) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(7.5), resp) t.Error(err) @@ -136,7 +133,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(15) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(15), resp) t.Error(err) @@ -146,23 +143,25 @@ func TestGetFee(t *testing.T) { func TestCalculateTradingFee(t *testing.T) { t.Parallel() - var newBalance = new(Balances) - newBalance.BTCUSDFee = 1 - newBalance.BTCEURFee = 0 + newBalance := make(Balances) + newBalance["BTC"] = Balance{ + USDFee: 1, + EURFee: 0, + } if resp := b.CalculateTradingFee(currency.BTC, currency.USD, 0, 0, newBalance); resp != 0 { - t.Error("Test Failed - GetFee() error") + t.Error("GetFee() error") } if resp := b.CalculateTradingFee(currency.BTC, currency.USD, 2, 2, newBalance); resp != float64(4) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(4), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(4), resp) } if resp := b.CalculateTradingFee(currency.BTC, currency.EUR, 2, 2, newBalance); resp != float64(0) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) } dummy1, dummy2 := currency.NewCode(""), currency.NewCode("") if resp := b.CalculateTradingFee(dummy1, dummy2, 0, 0, newBalance); resp != 0 { - t.Error("Test Failed - GetFee() error") + t.Error("GetFee() error") } } @@ -171,7 +170,7 @@ func TestGetTicker(t *testing.T) { _, err := b.GetTicker(currency.BTC.String()+currency.USD.String(), false) if err != nil { - t.Error("Test Failed - GetTicker() error", err) + t.Error("GetTicker() error", err) } } @@ -180,7 +179,7 @@ func TestGetOrderbook(t *testing.T) { _, err := b.GetOrderbook(currency.BTC.String() + currency.USD.String()) if err != nil { - t.Error("Test Failed - GetOrderbook() error", err) + t.Error("GetOrderbook() error", err) } } @@ -189,7 +188,7 @@ func TestGetTradingPairs(t *testing.T) { _, err := b.GetTradingPairs() if err != nil { - t.Error("Test Failed - GetTradingPairs() error", err) + t.Error("GetTradingPairs() error", err) } } @@ -201,7 +200,7 @@ func TestGetTransactions(t *testing.T) { _, err := b.GetTransactions(currency.BTC.String()+currency.USD.String(), value) if err != nil { - t.Error("Test Failed - GetTransactions() error", err) + t.Error("GetTransactions() error", err) } } @@ -210,7 +209,7 @@ func TestGetEURUSDConversionRate(t *testing.T) { _, err := b.GetEURUSDConversionRate() if err != nil { - t.Error("Test Failed - GetEURUSDConversionRate() error", err) + t.Error("GetEURUSDConversionRate() error", err) } } @@ -220,11 +219,11 @@ func TestGetBalance(t *testing.T) { _, err := b.GetBalance() switch { case areTestAPIKeysSet() && err != nil && !mockTests: - t.Error("Test Failed - GetBalance() error", err) + t.Error("GetBalance() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: t.Error("Expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - GetBalance() error", err) + t.Error("GetBalance() error", err) } } @@ -234,11 +233,11 @@ func TestGetUserTransactions(t *testing.T) { _, err := b.GetUserTransactions("btcusd") switch { case areTestAPIKeysSet() && err != nil && !mockTests: - t.Error("Test Failed - GetUserTransactions() error", err) + t.Error("GetUserTransactions() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: t.Error("Expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - GetUserTransactions() error", err) + t.Error("GetUserTransactions() error", err) } } @@ -248,11 +247,11 @@ func TestGetOpenOrders(t *testing.T) { _, err := b.GetOpenOrders("btcusd") switch { case areTestAPIKeysSet() && err != nil && !mockTests: - t.Error("Test Failed - GetOpenOrders() error", err) + t.Error("GetOpenOrders() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: t.Error("Expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - GetOpenOrders() error", err) + t.Error("GetOpenOrders() error", err) } } @@ -262,7 +261,7 @@ func TestGetOrderStatus(t *testing.T) { _, err := b.GetOrderStatus(1337) switch { case areTestAPIKeysSet() && err != nil && !mockTests: - t.Error("Test Failed - GetOrderStatus() error", err) + t.Error("GetOrderStatus() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: t.Error("Expecting an error when no keys are set") case mockTests && err == nil: @@ -276,11 +275,11 @@ func TestGetWithdrawalRequests(t *testing.T) { _, err := b.GetWithdrawalRequests(0) switch { case areTestAPIKeysSet() && err != nil && !mockTests: - t.Error("Test Failed - GetWithdrawalRequests() error", err) + t.Error("GetWithdrawalRequests() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: t.Error("Expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - GetWithdrawalRequests() error", err) + t.Error("GetWithdrawalRequests() error", err) } } @@ -290,11 +289,11 @@ func TestGetUnconfirmedBitcoinDeposits(t *testing.T) { _, err := b.GetUnconfirmedBitcoinDeposits() switch { case areTestAPIKeysSet() && err != nil && !mockTests: - t.Error("Test Failed - GetUnconfirmedBitcoinDeposits() error", err) + t.Error("GetUnconfirmedBitcoinDeposits() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: t.Error("Expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - GetUnconfirmedBitcoinDeposits() error", err) + t.Error("GetUnconfirmedBitcoinDeposits() error", err) } } @@ -307,7 +306,7 @@ func TestTransferAccountBalance(t *testing.T) { err := b.TransferAccountBalance(0.01, "btc", "testAccount", true) if !mockTests && err != nil { - t.Error("Test Failed - TransferAccountBalance() error", err) + t.Error("TransferAccountBalance() error", err) } if mockTests && err == nil { t.Error("Expecting an error until a QA pass can be completed") @@ -320,7 +319,6 @@ func TestFormatWithdrawPermissions(t *testing.T) { expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.AutoWithdrawFiatText - withdrawPermissions := b.FormatWithdrawPermissions() if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", @@ -332,8 +330,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -350,8 +348,8 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -375,17 +373,18 @@ func TestSubmitOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.BTC, - Quote: currency.USD, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := b.SubmitOrder(p, - exchange.BuyOrderSide, - exchange.MarketOrderType, - 1, - 1, - "clientId") + response, err := b.SubmitOrder(orderSubmission) switch { case areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) && !mockTests: t.Errorf("Order failed to be placed: %v", err) @@ -403,15 +402,9 @@ func TestCancelExchangeOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ - OrderID: "1", - WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - AccountID: "1", - CurrencyPair: currencyPair, + orderCancellation := &order.Cancel{ + OrderID: "1234", } - err := b.CancelOrder(orderCancellation) switch { case !areTestAPIKeysSet() && err == nil && !mockTests: @@ -430,16 +423,7 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ - OrderID: "1", - WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - AccountID: "1", - CurrencyPair: currencyPair, - } - - resp, err := b.CancelAllOrders(orderCancellation) + resp, err := b.CancelAllOrders(&order.Cancel{}) switch { case !areTestAPIKeysSet() && err == nil && !mockTests: t.Error("Expecting an error when no keys are set") @@ -449,17 +433,17 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestModifyOrder(t *testing.T) { t.Parallel() - _, err := b.ModifyOrder(&exchange.ModifyOrder{}) + _, err := b.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } @@ -470,11 +454,13 @@ func TestWithdraw(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } _, err := b.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) @@ -495,12 +481,14 @@ func TestWithdrawFiat(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.USD, - Description: "WITHDRAW IT ALL", + var withdrawFiatRequest = exchange.FiatWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.USD, + Description: "WITHDRAW IT ALL", + }, BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, + BankAccountNumber: "12345", BankAddress: "123 Fake St", BankCity: "Tarry Town", BankCountry: "AU", @@ -531,12 +519,14 @@ func TestWithdrawInternationalBank(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.USD, - Description: "WITHDRAW IT ALL", + var withdrawFiatRequest = exchange.FiatWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.USD, + Description: "WITHDRAW IT ALL", + }, BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, + BankAccountNumber: "12345", BankAddress: "123 Fake St", BankCity: "Tarry Town", BankCountry: "AU", @@ -572,10 +562,28 @@ func TestGetDepositAddress(t *testing.T) { _, err := b.GetDepositAddress(currency.BTC, "") switch { case areTestAPIKeysSet() && customerID != "" && err != nil && !mockTests: - t.Error("Test Failed - GetDepositAddress error", err) + t.Error("GetDepositAddress error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - GetDepositAddress error cannot be nil") + t.Error("GetDepositAddress error cannot be nil") case mockTests && err != nil: - t.Error("Test Failed - GetDepositAddress error", err) + t.Error("GetDepositAddress error", err) + } +} + +func TestParseTime(t *testing.T) { + t.Parallel() + + tm, err := parseTime("2019-10-18 01:55:14") + if err != nil { + t.Error(err) + } + + if tm.Year() != 2019 || + tm.Month() != 10 || + tm.Day() != 18 || + tm.Hour() != 1 || + tm.Minute() != 55 || + tm.Second() != 14 { + t.Error("invalid time values") } } diff --git a/exchanges/bitstamp/bitstamp_types.go b/exchanges/bitstamp/bitstamp_types.go index e8d8a676..8483211b 100644 --- a/exchanges/bitstamp/bitstamp_types.go +++ b/exchanges/bitstamp/bitstamp_types.go @@ -1,5 +1,19 @@ package bitstamp +// Transaction types +const ( + Deposit = iota + Withdrawal + MarketTrade + SubAccountTransfer = 14 +) + +// Order side type +const ( + BuyOrder = iota + SellOrder +) + // Ticker holds ticker information type Ticker struct { Last float64 `json:"last,string"` @@ -52,55 +66,51 @@ type EURUSDConversionRate struct { Sell float64 `json:"sell,string"` } -// Balances holds full balance information with the supplied APIKEYS -type Balances struct { - USDBalance float64 `json:"usd_balance,string"` - BTCBalance float64 `json:"btc_balance,string"` - EURBalance float64 `json:"eur_balance,string"` - XRPBalance float64 `json:"xrp_balance,string"` - USDReserved float64 `json:"usd_reserved,string"` - BTCReserved float64 `json:"btc_reserved,string"` - EURReserved float64 `json:"eur_reserved,string"` - XRPReserved float64 `json:"xrp_reserved,string"` - USDAvailable float64 `json:"usd_available,string"` - BTCAvailable float64 `json:"btc_available,string"` - EURAvailable float64 `json:"eur_available,string"` - XRPAvailable float64 `json:"xrp_available,string"` - BTCUSDFee float64 `json:"btcusd_fee,string"` - BTCEURFee float64 `json:"btceur_fee,string"` - EURUSDFee float64 `json:"eurusd_fee,string"` - XRPUSDFee float64 `json:"xrpusd_fee,string"` - XRPEURFee float64 `json:"xrpeur_fee,string"` - XRPBTCFee float64 `json:"xrpbtc_fee,string"` - Fee float64 `json:"fee,string"` +// Balance stores the balance info +type Balance struct { + Available float64 + Balance float64 + Reserved float64 + WithdrawalFee float64 + BTCFee float64 // for cryptocurrency pairs + USDFee float64 + EURFee float64 } +// Balances holds full balance information with the supplied APIKEYS +type Balances map[string]Balance + // UserTransactions holds user transaction information type UserTransactions struct { - Date string `json:"datetime"` - TransID int64 `json:"id"` - Type int `json:"type,string"` - USD float64 `json:"usd"` - EUR float64 `json:"eur"` - BTC float64 `json:"btc"` - XRP float64 `json:"xrp"` - BTCUSD float64 `json:"btc_usd"` - Fee float64 `json:"fee,string"` - OrderID int64 `json:"order_id"` + Date string `json:"datetime"` + TransactionID int64 `json:"id"` + Type int `json:"type,string"` + USD float64 `json:"usd"` + EUR float64 `json:"eur"` + BTC float64 `json:"btc"` + XRP float64 `json:"xrp"` + BTCUSD float64 `json:"btc_usd"` + Fee float64 `json:"fee,string"` + OrderID int64 `json:"order_id"` } // Order holds current open order data type Order struct { - ID int64 `json:"id"` - Date int64 `json:"datetime"` - Type int `json:"type"` - Price float64 `json:"price"` - Amount float64 `json:"amount"` + ID int64 `json:"id,string"` + DateTime string `json:"datetime"` + Type int `json:"type,string"` + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` Currency string `json:"currency_pair"` } // OrderStatus holds order status information type OrderStatus struct { + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + Type int `json:"type"` + ID int64 `json:"id,string"` + DateTime string `json:"datetime"` Status string Transactions []struct { TradeID int64 `json:"tid"` @@ -111,6 +121,14 @@ type OrderStatus struct { } } +// CancelOrder holds the order cancellation info +type CancelOrder struct { + Price float64 `json:"price"` + Amount float64 `json:"amount"` + Type int `json:"type"` + ID int64 `json:"id"` +} + // WithdrawalRequests holds request information on withdrawals type WithdrawalRequests struct { OrderID int64 `json:"id"` diff --git a/exchanges/bitstamp/bitstamp_websocket.go b/exchanges/bitstamp/bitstamp_websocket.go index f9df0ce9..65be546a 100644 --- a/exchanges/bitstamp/bitstamp_websocket.go +++ b/exchanges/bitstamp/bitstamp_websocket.go @@ -1,18 +1,18 @@ package bitstamp import ( + "encoding/json" "errors" - "fmt" "net/http" "strconv" + "strings" + "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -31,7 +31,7 @@ func (b *Bitstamp) WsConnect() error { return err } if b.Verbose { - log.Debugf("%s Connected to Websocket.\n", b.GetName()) + log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", b.Name) } err = b.seedOrderBook() @@ -61,12 +61,12 @@ func (b *Bitstamp) WsHandleData() { default: resp, err := b.WebsocketConn.ReadMessage() if err != nil { - b.Websocket.DataHandler <- err + b.Websocket.ReadMessageErrors <- err return } b.Websocket.TrafficAlert <- struct{}{} wsResponse := websocketResponse{} - err = common.JSONDecode(resp.Raw, &wsResponse) + err = json.Unmarshal(resp.Raw, &wsResponse) if err != nil { b.Websocket.DataHandler <- err continue @@ -75,22 +75,22 @@ func (b *Bitstamp) WsHandleData() { switch wsResponse.Event { case "bts:request_reconnect": if b.Verbose { - log.Debugf("%v - Websocket reconnection request received", b.GetName()) + log.Debugf(log.ExchangeSys, "%v - Websocket reconnection request received", b.Name) } - go b.Websocket.WebsocketReset() + go b.Websocket.Shutdown() // Connection monitor will reconnect case "data": wsOrderBookTemp := websocketOrderBookResponse{} - err := common.JSONDecode(resp.Raw, &wsOrderBookTemp) + err := json.Unmarshal(resp.Raw, &wsOrderBookTemp) if err != nil { b.Websocket.DataHandler <- err continue } - currencyPair := common.SplitStrings(wsResponse.Channel, "_") - p := currency.NewPairFromString(common.StringToUpper(currencyPair[3])) + currencyPair := strings.Split(wsResponse.Channel, "_") + p := currency.NewPairFromString(strings.ToUpper(currencyPair[2])) - err = b.wsUpdateOrderbook(wsOrderBookTemp.Data, p, ticker.Spot) + err = b.wsUpdateOrderbook(wsOrderBookTemp.Data, p, asset.Spot) if err != nil { b.Websocket.DataHandler <- err continue @@ -99,21 +99,21 @@ func (b *Bitstamp) WsHandleData() { case "trade": wsTradeTemp := websocketTradeResponse{} - err := common.JSONDecode(resp.Raw, &wsTradeTemp) + err := json.Unmarshal(resp.Raw, &wsTradeTemp) if err != nil { b.Websocket.DataHandler <- err continue } - currencyPair := common.SplitStrings(wsResponse.Channel, "_") - p := currency.NewPairFromString(common.StringToUpper(currencyPair[2])) + currencyPair := strings.Split(wsResponse.Channel, "_") + p := currency.NewPairFromString(strings.ToUpper(currencyPair[2])) b.Websocket.DataHandler <- wshandler.TradeData{ Price: wsTradeTemp.Data.Price, Amount: wsTradeTemp.Data.Amount, CurrencyPair: p, - Exchange: b.GetName(), - AssetType: ticker.Spot, + Exchange: b.Name, + AssetType: asset.Spot, } } } @@ -121,13 +121,13 @@ func (b *Bitstamp) WsHandleData() { } func (b *Bitstamp) generateDefaultSubscriptions() { - var channels = []string{"live_trades_", "diff_order_book_"} - enabledCurrencies := b.GetEnabledCurrencies() + var channels = []string{"live_trades_", "order_book_"} + enabledCurrencies := b.GetEnabledPairs(asset.Spot) var subscriptions []wshandler.WebsocketChannelSubscription for i := range channels { for j := range enabledCurrencies { subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: fmt.Sprintf("%v%v", channels[i], enabledCurrencies[j].Lower().String()), + Channel: channels[i] + enabledCurrencies[j].Lower().String(), }) } } @@ -156,53 +156,51 @@ func (b *Bitstamp) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubs return b.WebsocketConn.SendMessage(req) } -func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair, assetType string) error { +func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair, assetType asset.Item) error { if len(update.Asks) == 0 && len(update.Bids) == 0 { return errors.New("bitstamp_websocket.go error - no orderbook data") } var asks, bids []orderbook.Item - if len(update.Asks) > 0 { - for i := range update.Asks { - target, err := strconv.ParseFloat(update.Asks[i][0], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - amount, err := strconv.ParseFloat(update.Asks[i][1], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - asks = append(asks, orderbook.Item{Price: target, Amount: amount}) + for i := range update.Asks { + target, err := strconv.ParseFloat(update.Asks[i][0], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue } + + amount, err := strconv.ParseFloat(update.Asks[i][1], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + asks = append(asks, orderbook.Item{Price: target, Amount: amount}) } - if len(update.Bids) > 0 { - for i := range update.Bids { - target, err := strconv.ParseFloat(update.Bids[i][0], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - amount, err := strconv.ParseFloat(update.Bids[i][1], 64) - if err != nil { - b.Websocket.DataHandler <- err - continue - } - - bids = append(bids, orderbook.Item{Price: target, Amount: amount}) + for i := range update.Bids { + target, err := strconv.ParseFloat(update.Bids[i][0], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue } + + amount, err := strconv.ParseFloat(update.Bids[i][1], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + bids = append(bids, orderbook.Item{Price: target, Amount: amount}) } - err := b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ + + err := b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ Bids: bids, Asks: asks, - CurrencyPair: p, - UpdateID: update.Timestamp, - AssetType: orderbook.Spot, + Pair: p, + LastUpdated: time.Unix(update.Timestamp, 0), + AssetType: asset.Spot, + ExchangeName: b.Name, }) if err != nil { return err @@ -211,14 +209,14 @@ func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair, b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: p, Asset: assetType, - Exchange: b.GetName(), + Exchange: b.Name, } return nil } func (b *Bitstamp) seedOrderBook() error { - p := b.GetEnabledCurrencies() + p := b.GetEnabledPairs(asset.Spot) for x := range p { orderbookSeed, err := b.GetOrderbook(p[x].String()) if err != nil { @@ -226,36 +224,31 @@ func (b *Bitstamp) seedOrderBook() error { } var newOrderBook orderbook.Base - var asks, bids []orderbook.Item - for i := range orderbookSeed.Asks { - var item orderbook.Item - item.Amount = orderbookSeed.Asks[i].Amount - item.Price = orderbookSeed.Asks[i].Price - asks = append(asks, item) + newOrderBook.Asks = append(newOrderBook.Asks, orderbook.Item{ + Price: orderbookSeed.Asks[i].Price, + Amount: orderbookSeed.Asks[i].Amount, + }) } - for i := range orderbookSeed.Bids { - var item orderbook.Item - item.Amount = orderbookSeed.Bids[i].Amount - item.Price = orderbookSeed.Bids[i].Price - bids = append(bids, item) + newOrderBook.Bids = append(newOrderBook.Bids, orderbook.Item{ + Price: orderbookSeed.Bids[i].Price, + Amount: orderbookSeed.Bids[i].Amount, + }) } - - newOrderBook.Asks = asks - newOrderBook.Bids = bids newOrderBook.Pair = p[x] - newOrderBook.AssetType = ticker.Spot + newOrderBook.AssetType = asset.Spot + newOrderBook.ExchangeName = b.Name - err = b.Websocket.Orderbook.LoadSnapshot(&newOrderBook, false) + err = b.Websocket.Orderbook.LoadSnapshot(&newOrderBook) if err != nil { return err } b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: p[x], - Asset: ticker.Spot, - Exchange: b.GetName(), + Asset: asset.Spot, + Exchange: b.Name, } } return nil diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 68522421..fc6fa247 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -2,21 +2,166 @@ package bitstamp import ( "errors" - "fmt" "strconv" "strings" "sync" "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (b *Bitstamp) GetDefaultConfig() (*config.ExchangeConfig, error) { + b.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = b.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = b.BaseCurrencies + + err := b.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if b.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = b.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets default for Bitstamp +func (b *Bitstamp) SetDefaults() { + b.Name = "Bitstamp" + b.Enabled = true + b.Verbose = true + b.API.CredentialsValidator.RequiresKey = true + b.API.CredentialsValidator.RequiresSecret = true + b.API.CredentialsValidator.RequiresClientID = true + + b.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + } + + b.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + FiatDeposit: true, + FiatWithdraw: true, + TradeFee: true, + FiatDepositFee: true, + FiatWithdrawalFee: true, + CryptoDepositFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TradeFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.AutoWithdrawFiat, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + b.Requester = request.New(b.Name, + request.NewRateLimit(time.Minute*10, bitstampAuthRate), + request.NewRateLimit(time.Minute*10, bitstampUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + b.API.Endpoints.URLDefault = bitstampAPIURL + b.API.Endpoints.URL = b.API.Endpoints.URLDefault + b.API.Endpoints.WebsocketURL = bitstampWSURL + b.Websocket = wshandler.New() + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup sets configuration values to bitstamp +func (b *Bitstamp) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + b.SetEnabled(false) + return nil + } + + err := b.SetupDefaults(exch) + if err != nil { + return err + } + + err = b.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: bitstampWSURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: b.WsConnect, + Subscriber: b.Subscribe, + UnSubscriber: b.Unsubscribe, + Features: &b.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + return nil +} + // Start starts the Bitstamp go routine func (b *Bitstamp) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -29,53 +174,78 @@ func (b *Bitstamp) Start(wg *sync.WaitGroup) { // Run implements the Bitstamp wrapper func (b *Bitstamp) Run() { if b.Verbose { - log.Debugf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled())) - log.Debugf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) + log.Debugf(log.ExchangeSys, + "%s Websocket: %s.", + b.Name, + common.IsEnabled(b.Websocket.IsEnabled())) + b.PrintEnabledPairs() } - pairs, err := b.GetTradingPairs() + if !b.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := b.UpdateTradablePairs(false) if err != nil { - log.Errorf("%s failed to get trading pairs. Err: %s", b.Name, err) - } else { - var currencies []string - for x := range pairs { - if pairs[x].Trading != "Enabled" { - continue - } - p := strings.Split(pairs[x].Name, "/") - currencies = append(currencies, p[0]+p[1]) - } - - var newCurrencies currency.Pairs - for _, p := range currencies { - newCurrencies = append(newCurrencies, - currency.NewPairFromString(p)) - } - - err = b.UpdateCurrencies(newCurrencies, false, false) - if err != nil { - log.Errorf("%s Failed to update available currencies.\n", b.Name) - } + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + b.Name, + err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (b *Bitstamp) FetchTradablePairs(asset asset.Item) ([]string, error) { + pairs, err := b.GetTradingPairs() + if err != nil { + return nil, err + } + + var products []string + for x := range pairs { + if pairs[x].Trading != "Enabled" { + continue + } + + pair := strings.Split(pairs[x].Name, "/") + products = append(products, pair[0]+pair[1]) + } + + return products, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (b *Bitstamp) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := b.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (b *Bitstamp) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (b *Bitstamp) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price tick, err := b.GetTicker(p.String(), false) if err != nil { return tickerPrice, err } - tickerPrice.Pair = p - tickerPrice.Ask = tick.Ask - tickerPrice.Bid = tick.Bid - tickerPrice.Low = tick.Low - tickerPrice.Last = tick.Last - tickerPrice.Volume = tick.Volume - tickerPrice.High = tick.High - err = ticker.ProcessTicker(b.GetName(), &tickerPrice, assetType) + tickerPrice = ticker.Price{ + Last: tick.Last, + High: tick.High, + Low: tick.Low, + Bid: tick.Bid, + Ask: tick.Ask, + Volume: tick.Volume, + Open: tick.Open, + Pair: p, + LastUpdated: time.Unix(tick.Timestamp, 0), + } + + err = ticker.ProcessTicker(b.Name, &tickerPrice, assetType) if err != nil { return tickerPrice, err } @@ -83,9 +253,9 @@ func (b *Bitstamp) UpdateTicker(p currency.Pair, assetType string) (ticker.Price return ticker.GetTicker(b.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (b *Bitstamp) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tick, err := ticker.GetTicker(b.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (b *Bitstamp) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tick, err := ticker.GetTicker(b.Name, p, assetType) if err != nil { return b.UpdateTicker(p, assetType) } @@ -94,16 +264,16 @@ func (b *Bitstamp) GetTickerPrice(p currency.Pair, assetType string) (ticker.Pri // GetFeeByType returns an estimate of fee based on type of transaction func (b *Bitstamp) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (b.APIKey == "" || b.APISecret == "") && // Todo check connection status + if (!b.AllowAuthenticatedRequest() || b.SkipAuthCheck) && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } return b.GetFee(feeBuilder) } -// GetOrderbookEx returns the orderbook for a currency pair -func (b *Bitstamp) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(b.GetName(), p, assetType) +// FetchOrderbook returns the orderbook for a currency pair +func (b *Bitstamp) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(b.Name, p, assetType) if err != nil { return b.UpdateOrderbook(p, assetType) } @@ -111,7 +281,7 @@ func (b *Bitstamp) GetOrderbookEx(p currency.Pair, assetType string) (orderbook. } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (b *Bitstamp) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (b *Bitstamp) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base orderbookNew, err := b.GetOrderbook(p.String()) if err != nil { @@ -119,17 +289,21 @@ func (b *Bitstamp) UpdateOrderbook(p currency.Pair, assetType string) (orderbook } for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data.Amount, Price: data.Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[x].Amount, + Price: orderbookNew.Bids[x].Price, + }) } for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data.Amount, Price: data.Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[x].Amount, + Price: orderbookNew.Asks[x].Price, + }) } orderBook.Pair = p - orderBook.ExchangeName = b.GetName() + orderBook.ExchangeName = b.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -144,33 +318,19 @@ func (b *Bitstamp) UpdateOrderbook(p currency.Pair, assetType string) (orderbook // Bitstamp exchange func (b *Bitstamp) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = b.GetName() + response.Exchange = b.Name accountBalance, err := b.GetBalance() if err != nil { return response, err } - var currencies = []exchange.AccountCurrencyInfo{ - { - CurrencyName: currency.BTC, - TotalValue: accountBalance.BTCAvailable, - Hold: accountBalance.BTCReserved, - }, - { - CurrencyName: currency.XRP, - TotalValue: accountBalance.XRPAvailable, - Hold: accountBalance.XRPReserved, - }, - { - CurrencyName: currency.USD, - TotalValue: accountBalance.USDAvailable, - Hold: accountBalance.USDReserved, - }, - { - CurrencyName: currency.EUR, - TotalValue: accountBalance.EURAvailable, - Hold: accountBalance.EURReserved, - }, + var currencies []exchange.AccountCurrencyInfo + for k, v := range accountBalance { + currencies = append(currencies, exchange.AccountCurrencyInfo{ + CurrencyName: currency.NewCode(k), + TotalValue: v.Available, + Hold: v.Reserved, + }) } response.Accounts = append(response.Accounts, exchange.Account{ Currencies: currencies, @@ -182,69 +342,74 @@ func (b *Bitstamp) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (b *Bitstamp) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (b *Bitstamp) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (b *Bitstamp) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (b *Bitstamp) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - buy := side == exchange.BuyOrderSide - market := orderType == exchange.MarketOrderType - response, err := b.PlaceOrder(p.String(), price, amount, buy, market) +func (b *Bitstamp) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } + buy := s.OrderSide == order.Buy + market := s.OrderType == order.Market + response, err := b.PlaceOrder(s.Pair.String(), + s.Price, + s.Amount, + buy, + market) + if err != nil { + return submitOrderResponse, err + } if response.ID > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response.ID) + submitOrderResponse.OrderID = strconv.FormatInt(response.ID, 10) } - if err == nil { - submitOrderResponse.IsOrderPlaced = true + submitOrderResponse.IsOrderPlaced = true + if s.OrderType == order.Market { + submitOrderResponse.FullyMatched = true } - - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (b *Bitstamp) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (b *Bitstamp) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (b *Bitstamp) CancelOrder(order *exchange.OrderCancellation) error { +func (b *Bitstamp) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) - if err != nil { return err } _, err = b.CancelExistingOrder(orderIDInt) - return err } // CancelAllOrders cancels all orders associated with a currency pair -func (b *Bitstamp) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { +func (b *Bitstamp) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { success, err := b.CancelAllExistingOrders() if err != nil { - return exchange.CancelAllOrdersResponse{}, err + return order.CancelAllResponse{}, err } if !success { err = errors.New("cancel all orders failed. Bitstamp provides no further information. Check order status to verify") } - return exchange.CancelAllOrdersResponse{}, err + return order.CancelAllResponse{}, err } // GetOrderInfo returns information on a current open order -func (b *Bitstamp) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (b *Bitstamp) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -255,17 +420,21 @@ func (b *Bitstamp) GetDepositAddress(cryptocurrency currency.Code, _ string) (st // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *Bitstamp) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { - resp, err := b.CryptoWithdrawal(withdrawRequest.Amount, withdrawRequest.Address, withdrawRequest.Currency.String(), withdrawRequest.AddressTag, true) +func (b *Bitstamp) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { + resp, err := b.CryptoWithdrawal(withdrawRequest.Amount, + withdrawRequest.Address, + withdrawRequest.Currency.String(), + withdrawRequest.AddressTag, + true) if err != nil { return "", err } if len(resp.Error) != 0 { - var details string - for _, v := range resp.Error { - details += strings.Join(v, "") + var details strings.Builder + for x := range resp.Error { + details.WriteString(strings.Join(resp.Error[x], "")) } - return "", errors.New(details) + return "", errors.New(details.String()) } return resp.ID, nil @@ -273,20 +442,27 @@ func (b *Bitstamp) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.Withdra // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (b *Bitstamp) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { - resp, err := b.OpenBankWithdrawal(withdrawRequest.Amount, withdrawRequest.Currency.String(), - withdrawRequest.BankAccountName, withdrawRequest.IBAN, withdrawRequest.SwiftCode, withdrawRequest.BankAddress, - withdrawRequest.BankPostalCode, withdrawRequest.BankCity, withdrawRequest.BankCountry, - withdrawRequest.Description, sepaWithdrawal) +func (b *Bitstamp) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { + resp, err := b.OpenBankWithdrawal(withdrawRequest.Amount, + withdrawRequest.Currency.String(), + withdrawRequest.BankAccountName, + withdrawRequest.IBAN, + withdrawRequest.SwiftCode, + withdrawRequest.BankAddress, + withdrawRequest.BankPostalCode, + withdrawRequest.BankCity, + withdrawRequest.BankCountry, + withdrawRequest.Description, + sepaWithdrawal) if err != nil { return "", err } if resp.Status == errStr { - var details string - for _, v := range resp.Reason { - details += strings.Join(v, "") + var details strings.Builder + for x := range resp.Reason { + details.WriteString(strings.Join(resp.Reason[x], "")) } - return "", errors.New(details) + return "", errors.New(details.String()) } return resp.ID, nil @@ -294,22 +470,33 @@ func (b *Bitstamp) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (b *Bitstamp) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { - resp, err := b.OpenInternationalBankWithdrawal(withdrawRequest.Amount, withdrawRequest.Currency.String(), - withdrawRequest.BankAccountName, withdrawRequest.IBAN, withdrawRequest.SwiftCode, withdrawRequest.BankAddress, - withdrawRequest.BankPostalCode, withdrawRequest.BankCity, withdrawRequest.BankCountry, - withdrawRequest.IntermediaryBankName, withdrawRequest.IntermediaryBankAddress, withdrawRequest.IntermediaryBankPostalCode, - withdrawRequest.IntermediaryBankCity, withdrawRequest.IntermediaryBankCountry, withdrawRequest.WireCurrency, - withdrawRequest.Description, internationalWithdrawal) +func (b *Bitstamp) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { + resp, err := b.OpenInternationalBankWithdrawal(withdrawRequest.Amount, + withdrawRequest.Currency.String(), + withdrawRequest.BankAccountName, + withdrawRequest.IBAN, + withdrawRequest.SwiftCode, + withdrawRequest.BankAddress, + withdrawRequest.BankPostalCode, + withdrawRequest.BankCity, + withdrawRequest.BankCountry, + withdrawRequest.IntermediaryBankName, + withdrawRequest.IntermediaryBankAddress, + withdrawRequest.IntermediaryBankPostalCode, + withdrawRequest.IntermediaryBankCity, + withdrawRequest.IntermediaryBankCountry, + withdrawRequest.WireCurrency, + withdrawRequest.Description, + internationalWithdrawal) if err != nil { return "", err } if resp.Status == errStr { - var details string - for _, v := range resp.Reason { - details += strings.Join(v, "") + var details strings.Builder + for x := range resp.Reason { + details.WriteString(strings.Join(resp.Reason[x], "")) } - return "", errors.New(details) + return "", errors.New(details.String()) } return resp.ID, nil @@ -321,13 +508,12 @@ func (b *Bitstamp) GetWebsocket() (*wshandler.Websocket, error) { } // GetActiveOrders retrieves any orders that are active/open -func (b *Bitstamp) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var orders []exchange.OrderDetail +func (b *Bitstamp) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { var currPair string - if len(getOrdersRequest.Currencies) != 1 { + if len(req.Currencies) != 1 { currPair = "all" } else { - currPair = getOrdersRequest.Currencies[0].String() + currPair = req.Currencies[0].String() } resp, err := b.GetOpenOrders(currPair) @@ -335,86 +521,102 @@ func (b *Bitstamp) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) return nil, err } - for _, order := range resp { - orderDate := time.Unix(order.Date, 0) - orders = append(orders, exchange.OrderDetail{ - Amount: order.Amount, - ID: fmt.Sprintf("%v", order.ID), - Price: order.Price, - OrderDate: orderDate, - CurrencyPair: currency.NewPairFromStrings(order.Currency[0:3], - order.Currency[len(order.Currency)-3:]), - Exchange: b.Name, + var orders []order.Detail + for i := range resp { + orderSide := order.Buy + if resp[i].Type == SellOrder { + orderSide = order.Sell + } + + tm, err := parseTime(resp[i].DateTime) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s GetActiveOrders unable to parse time: %s\n", b.Name, err) + } + + orders = append(orders, order.Detail{ + Amount: resp[i].Amount, + ID: strconv.FormatInt(resp[i].ID, 10), + Price: resp[i].Price, + OrderType: order.Limit, + OrderSide: orderSide, + OrderDate: tm, + CurrencyPair: currency.NewPairFromString(resp[i].Currency), + Exchange: b.Name, }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (b *Bitstamp) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (b *Bitstamp) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { var currPair string - if len(getOrdersRequest.Currencies) == 1 { - currPair = getOrdersRequest.Currencies[0].String() + if len(req.Currencies) == 1 { + currPair = req.Currencies[0].String() } resp, err := b.GetUserTransactions(currPair) if err != nil { return nil, err } - var orders []exchange.OrderDetail - for _, order := range resp { - if order.Type != 2 { + var orders []order.Detail + for i := range resp { + if resp[i].Type != MarketTrade { continue } var quoteCurrency, baseCurrency currency.Code switch { - case order.BTC > 0: + case resp[i].BTC > 0: baseCurrency = currency.BTC - case order.XRP > 0: + case resp[i].XRP > 0: baseCurrency = currency.XRP default: - log.Warnf("no base currency found for OrderID '%v'", order.OrderID) + log.Warnf(log.ExchangeSys, + "%s No base currency found for OrderID '%d'\n", + b.Name, + resp[i].OrderID) } switch { - case order.USD > 0: + case resp[i].USD > 0: quoteCurrency = currency.USD - case order.EUR > 0: + case resp[i].EUR > 0: quoteCurrency = currency.EUR default: - log.Warnf("no quote currency found for orderID '%v'", order.OrderID) + log.Warnf(log.ExchangeSys, + "%s No quote currency found for orderID '%d'\n", + b.Name, + resp[i].OrderID) } var currPair currency.Pair if quoteCurrency.String() != "" && baseCurrency.String() != "" { currPair = currency.NewPairWithDelimiter(baseCurrency.String(), quoteCurrency.String(), - b.ConfigCurrencyPairFormat.Delimiter) + b.GetPairFormat(asset.Spot, false).Delimiter) } - orderDate, err := time.Parse("2006-01-02 15:04:05", order.Date) + tm, err := parseTime(resp[i].Date) if err != nil { - return nil, err + log.Errorf(log.ExchangeSys, + "%s GetOrderHistory unable to parse time: %s\n", b.Name, err) } - orders = append(orders, exchange.OrderDetail{ - ID: fmt.Sprintf("%v", order.OrderID), - OrderDate: orderDate, + orders = append(orders, order.Detail{ + ID: strconv.FormatInt(resp[i].OrderID, 10), + OrderDate: tm, Exchange: b.Name, CurrencyPair: currPair, }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } diff --git a/exchanges/bittrex/README.md b/exchanges/bittrex/README.md index af749330..039bc2e9 100644 --- a/exchanges/bittrex/README.md +++ b/exchanges/bittrex/README.md @@ -47,22 +47,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Bittrex" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Bittrex" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +130,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/bittrex/bittrex.go b/exchanges/bittrex/bittrex.go index 10345f9c..a564dd16 100644 --- a/exchanges/bittrex/bittrex.go +++ b/exchanges/bittrex/bittrex.go @@ -6,16 +6,12 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( @@ -58,6 +54,7 @@ const ( bittrexAuthRate = 0 bittrexUnauthRate = 0 + bittrexTimeLayout = "2006-01-02T15:04:05" ) // Bittrex is the overaching type across the bittrex methods @@ -65,74 +62,11 @@ type Bittrex struct { exchange.Base } -// SetDefaults method assignes the default values for Bittrex -func (b *Bittrex) SetDefaults() { - b.Name = "Bittrex" - b.Enabled = false - b.Verbose = false - b.RESTPollingDelay = 10 - b.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithAPIPermission | - exchange.NoFiatWithdrawals - b.RequestCurrencyPairFormat.Delimiter = "-" - b.RequestCurrencyPairFormat.Uppercase = true - b.ConfigCurrencyPairFormat.Delimiter = "-" - b.ConfigCurrencyPairFormat.Uppercase = true - b.AssetTypes = []string{ticker.Spot} - b.SupportsAutoPairUpdating = true - b.SupportsRESTTickerBatching = true - b.Requester = request.New(b.Name, - request.NewRateLimit(time.Second, bittrexAuthRate), - request.NewRateLimit(time.Second, bittrexUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - b.APIUrlDefault = bittrexAPIURL - b.APIUrl = b.APIUrlDefault - b.Websocket = wshandler.New() -} - -// Setup method sets current configuration details if enabled -func (b *Bittrex) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - b.SetEnabled(false) - } else { - b.Enabled = true - b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - b.SetAPIKeys(exch.APIKey, exch.APISecret, exch.ClientID, false) - b.SetHTTPClientTimeout(exch.HTTPTimeout) - b.SetHTTPClientUserAgent(exch.HTTPUserAgent) - b.RESTPollingDelay = exch.RESTPollingDelay - b.Verbose = exch.Verbose - b.HTTPDebugging = exch.HTTPDebugging - b.BaseCurrencies = exch.BaseCurrencies - b.AvailablePairs = exch.AvailablePairs - b.EnabledPairs = exch.EnabledPairs - err := b.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = b.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = b.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = b.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = b.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - } -} - // GetMarkets is used to get the open and available trading markets at Bittrex // along with other meta data. func (b *Bittrex) GetMarkets() (Market, error) { var markets Market - path := fmt.Sprintf("%s/%s/", b.APIUrl, bittrexAPIGetMarkets) + path := fmt.Sprintf("%s/%s/", b.API.Endpoints.URL, bittrexAPIGetMarkets) if err := b.SendHTTPRequest(path, &markets); err != nil { return markets, err @@ -147,7 +81,7 @@ func (b *Bittrex) GetMarkets() (Market, error) { // GetCurrencies is used to get all supported currencies at Bittrex func (b *Bittrex) GetCurrencies() (Currency, error) { var currencies Currency - path := fmt.Sprintf("%s/%s/", b.APIUrl, bittrexAPIGetCurrencies) + path := fmt.Sprintf("%s/%s/", b.API.Endpoints.URL, bittrexAPIGetCurrencies) if err := b.SendHTTPRequest(path, ¤cies); err != nil { return currencies, err @@ -163,8 +97,8 @@ func (b *Bittrex) GetCurrencies() (Currency, error) { // on the supplied currency. Example currency input param "btc-ltc". func (b *Bittrex) GetTicker(currencyPair string) (Ticker, error) { tick := Ticker{} - path := fmt.Sprintf("%s/%s?market=%s", b.APIUrl, bittrexAPIGetTicker, - common.StringToUpper(currencyPair), + path := fmt.Sprintf("%s/%s?market=%s", b.API.Endpoints.URL, bittrexAPIGetTicker, + strings.ToUpper(currencyPair), ) if err := b.SendHTTPRequest(path, &tick); err != nil { @@ -181,7 +115,7 @@ func (b *Bittrex) GetTicker(currencyPair string) (Ticker, error) { // exchanges func (b *Bittrex) GetMarketSummaries() (MarketSummary, error) { var summaries MarketSummary - path := fmt.Sprintf("%s/%s/", b.APIUrl, bittrexAPIGetMarketSummaries) + path := fmt.Sprintf("%s/%s/", b.API.Endpoints.URL, bittrexAPIGetMarketSummaries) if err := b.SendHTTPRequest(path, &summaries); err != nil { return summaries, err @@ -197,8 +131,8 @@ func (b *Bittrex) GetMarketSummaries() (MarketSummary, error) { // exchanges by currency pair (btc-ltc). func (b *Bittrex) GetMarketSummary(currencyPair string) (MarketSummary, error) { var summary MarketSummary - path := fmt.Sprintf("%s/%s?market=%s", b.APIUrl, - bittrexAPIGetMarketSummary, common.StringToLower(currencyPair), + path := fmt.Sprintf("%s/%s?market=%s", b.API.Endpoints.URL, + bittrexAPIGetMarketSummary, strings.ToLower(currencyPair), ) if err := b.SendHTTPRequest(path, &summary); err != nil { @@ -220,8 +154,8 @@ func (b *Bittrex) GetMarketSummary(currencyPair string) (MarketSummary, error) { // it returns full depth. So depth default is 50. func (b *Bittrex) GetOrderbook(currencyPair string) (OrderBooks, error) { var orderbooks OrderBooks - path := fmt.Sprintf("%s/%s?market=%s&type=both&depth=50", b.APIUrl, - bittrexAPIGetOrderbook, common.StringToUpper(currencyPair), + path := fmt.Sprintf("%s/%s?market=%s&type=both&depth=50", b.API.Endpoints.URL, + bittrexAPIGetOrderbook, strings.ToUpper(currencyPair), ) if err := b.SendHTTPRequest(path, &orderbooks); err != nil { @@ -238,8 +172,8 @@ func (b *Bittrex) GetOrderbook(currencyPair string) (OrderBooks, error) { // market func (b *Bittrex) GetMarketHistory(currencyPair string) (MarketHistory, error) { var marketHistoriae MarketHistory - path := fmt.Sprintf("%s/%s?market=%s", b.APIUrl, - bittrexAPIGetMarketHistory, common.StringToUpper(currencyPair), + path := fmt.Sprintf("%s/%s?market=%s", b.API.Endpoints.URL, + bittrexAPIGetMarketHistory, strings.ToUpper(currencyPair), ) if err := b.SendHTTPRequest(path, &marketHistoriae); err != nil { @@ -264,7 +198,7 @@ func (b *Bittrex) PlaceBuyLimit(currencyPair string, quantity, rate float64) (UU values.Set("market", currencyPair) values.Set("quantity", strconv.FormatFloat(quantity, 'E', -1, 64)) values.Set("rate", strconv.FormatFloat(rate, 'E', -1, 64)) - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPIBuyLimit) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPIBuyLimit) if err := b.SendAuthenticatedHTTPRequest(path, values, &id); err != nil { return id, err @@ -288,7 +222,7 @@ func (b *Bittrex) PlaceSellLimit(currencyPair string, quantity, rate float64) (U values.Set("market", currencyPair) values.Set("quantity", strconv.FormatFloat(quantity, 'E', -1, 64)) values.Set("rate", strconv.FormatFloat(rate, 'E', -1, 64)) - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPISellLimit) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPISellLimit) if err := b.SendAuthenticatedHTTPRequest(path, values, &id); err != nil { return id, err @@ -308,7 +242,7 @@ func (b *Bittrex) GetOpenOrders(currencyPair string) (Order, error) { if !(currencyPair == "" || currencyPair == " ") { values.Set("market", currencyPair) } - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPIGetOpenOrders) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPIGetOpenOrders) if err := b.SendAuthenticatedHTTPRequest(path, values, &orders); err != nil { return orders, err @@ -325,7 +259,7 @@ func (b *Bittrex) CancelExistingOrder(uuid string) (Balances, error) { var balances Balances values := url.Values{} values.Set("uuid", uuid) - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPICancel) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPICancel) if err := b.SendAuthenticatedHTTPRequest(path, values, &balances); err != nil { return balances, err @@ -340,7 +274,7 @@ func (b *Bittrex) CancelExistingOrder(uuid string) (Balances, error) { // GetAccountBalances is used to retrieve all balances from your account func (b *Bittrex) GetAccountBalances() (Balances, error) { var balances Balances - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPIGetBalances) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPIGetBalances) if err := b.SendAuthenticatedHTTPRequest(path, url.Values{}, &balances); err != nil { return balances, err @@ -358,7 +292,7 @@ func (b *Bittrex) GetAccountBalanceByCurrency(currency string) (Balance, error) var balance Balance values := url.Values{} values.Set("currency", currency) - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPIGetBalance) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPIGetBalance) if err := b.SendAuthenticatedHTTPRequest(path, values, &balance); err != nil { return balance, err @@ -377,7 +311,7 @@ func (b *Bittrex) GetCryptoDepositAddress(currency string) (DepositAddress, erro var address DepositAddress values := url.Values{} values.Set("currency", currency) - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPIGetDepositAddress) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPIGetDepositAddress) if err := b.SendAuthenticatedHTTPRequest(path, values, &address); err != nil { return address, err @@ -395,13 +329,13 @@ func (b *Bittrex) Withdraw(currency, paymentID, address string, quantity float64 var id UUID values := url.Values{} values.Set("currency", currency) - values.Set("quantity", fmt.Sprintf("%v", quantity)) + values.Set("quantity", strconv.FormatFloat(quantity, 'f', -1, 64)) values.Set("address", address) if len(paymentID) > 0 { values.Set("paymentid", paymentID) } - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPIWithdraw) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPIWithdraw) if err := b.SendAuthenticatedHTTPRequest(path, values, &id); err != nil { return id, err @@ -418,7 +352,7 @@ func (b *Bittrex) GetOrder(uuid string) (Order, error) { var order Order values := url.Values{} values.Set("uuid", uuid) - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPIGetOrder) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPIGetOrder) if err := b.SendAuthenticatedHTTPRequest(path, values, &order); err != nil { return order, err @@ -439,7 +373,7 @@ func (b *Bittrex) GetOrderHistoryForCurrency(currencyPair string) (Order, error) if !(currencyPair == "" || currencyPair == " ") { values.Set("market", currencyPair) } - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPIGetOrderHistory) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPIGetOrderHistory) if err := b.SendAuthenticatedHTTPRequest(path, values, &orders); err != nil { return orders, err @@ -460,7 +394,7 @@ func (b *Bittrex) GetWithdrawalHistory(currency string) (WithdrawalHistory, erro if !(currency == "" || currency == " ") { values.Set("currency", currency) } - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPIGetWithdrawalHistory) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPIGetWithdrawalHistory) if err := b.SendAuthenticatedHTTPRequest(path, values, &history); err != nil { return history, err @@ -481,7 +415,7 @@ func (b *Bittrex) GetDepositHistory(currency string) (WithdrawalHistory, error) if !(currency == "" || currency == " ") { values.Set("currency", currency) } - path := fmt.Sprintf("%s/%s", b.APIUrl, bittrexAPIGetDepositHistory) + path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bittrexAPIGetDepositHistory) if err := b.SendAuthenticatedHTTPRequest(path, values, &history); err != nil { return history, err @@ -510,20 +444,20 @@ func (b *Bittrex) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated http request to a desired // path func (b *Bittrex) SendAuthenticatedHTTPRequest(path string, values url.Values, result interface{}) (err error) { - if !b.AuthenticatedAPISupport { + if !b.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } n := b.Requester.GetNonce(true).String() - values.Set("apikey", b.APIKey) + values.Set("apikey", b.API.Credentials.Key) values.Set("nonce", n) rawQuery := path + "?" + values.Encode() - hmac := common.GetHMAC( - common.HashSHA512, []byte(rawQuery), []byte(b.APISecret), + hmac := crypto.GetHMAC( + crypto.HashSHA512, []byte(rawQuery), []byte(b.API.Credentials.Secret), ) headers := make(map[string]string) - headers["apisign"] = common.HexEncodeToString(hmac) + headers["apisign"] = crypto.HexEncodeToString(hmac) return b.SendPayload(http.MethodGet, rawQuery, @@ -576,3 +510,7 @@ func (b *Bittrex) GetWithdrawalFee(c currency.Code) (float64, error) { func calculateTradingFee(price, amount float64) float64 { return 0.0025 * price * amount } + +func parseTime(t string) (time.Time, error) { + return time.Parse(bittrexTimeLayout, t) +} diff --git a/exchanges/bittrex/bittrex_test.go b/exchanges/bittrex/bittrex_test.go index 6a1367e3..5cc15712 100644 --- a/exchanges/bittrex/bittrex_test.go +++ b/exchanges/bittrex/bittrex_test.go @@ -1,13 +1,15 @@ package bittrex import ( + "log" + "os" "testing" - "time" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // Please supply you own test keys here to run better tests. @@ -19,39 +21,39 @@ const ( var b Bittrex -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { b.SetDefaults() - if b.GetName() != "Bittrex" { - t.Error("Test Failed - Bittrex - SetDefaults() error") - } -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Bittrex load config error", err) + } bConfig, err := cfg.GetExchangeConfig("Bittrex") if err != nil { - t.Error("Test Failed - Bittrex Setup() init error") + log.Fatal("Bittrex Setup() init error") } - bConfig.APIKey = apiKey - bConfig.APISecret = apiSecret - bConfig.AuthenticatedAPISupport = true + bConfig.API.Credentials.Key = apiKey + bConfig.API.Credentials.Secret = apiSecret + bConfig.API.AuthenticatedSupport = true - b.Setup(&bConfig) - - if !b.IsEnabled() || - b.RESTPollingDelay != time.Duration(10) || b.Verbose || - b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 || - len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 { - t.Error("Test Failed - Bittrex Setup values not set correctly") + err = b.Setup(bConfig) + if err != nil { + log.Fatal("Bittrex setup error", err) } + + if !b.IsEnabled() || !b.API.AuthenticatedSupport || + b.Verbose || len(b.BaseCurrencies) < 1 { + log.Fatal("Bittrex Setup values not set correctly") + } + + os.Exit(m.Run()) } func TestGetMarkets(t *testing.T) { t.Parallel() _, err := b.GetMarkets() if err != nil { - t.Errorf("Test Failed - Bittrex - GetMarkets() error: %s", err) + t.Errorf("Bittrex - GetMarkets() error: %s", err) } } @@ -59,7 +61,7 @@ func TestGetCurrencies(t *testing.T) { t.Parallel() _, err := b.GetCurrencies() if err != nil { - t.Errorf("Test Failed - Bittrex - GetCurrencies() error: %s", err) + t.Errorf("Bittrex - GetCurrencies() error: %s", err) } } @@ -69,7 +71,7 @@ func TestGetTicker(t *testing.T) { _, err := b.GetTicker(btc) if err != nil { - t.Errorf("Test Failed - Bittrex - GetTicker() error: %s", err) + t.Errorf("Bittrex - GetTicker() error: %s", err) } } @@ -77,7 +79,7 @@ func TestGetMarketSummaries(t *testing.T) { t.Parallel() _, err := b.GetMarketSummaries() if err != nil { - t.Errorf("Test Failed - Bittrex - GetMarketSummaries() error: %s", err) + t.Errorf("Bittrex - GetMarketSummaries() error: %s", err) } } @@ -87,7 +89,7 @@ func TestGetMarketSummary(t *testing.T) { _, err := b.GetMarketSummary(pairOne) if err != nil { - t.Errorf("Test Failed - Bittrex - GetMarketSummary() error: %s", err) + t.Errorf("Bittrex - GetMarketSummary() error: %s", err) } } @@ -96,7 +98,7 @@ func TestGetOrderbook(t *testing.T) { _, err := b.GetOrderbook("btc-ltc") if err != nil { - t.Errorf("Test Failed - Bittrex - GetOrderbook() error: %s", err) + t.Errorf("Bittrex - GetOrderbook() error: %s", err) } } @@ -105,7 +107,7 @@ func TestGetMarketHistory(t *testing.T) { _, err := b.GetMarketHistory("btc-ltc") if err != nil { - t.Errorf("Test Failed - Bittrex - GetMarketHistory() error: %s", err) + t.Errorf("Bittrex - GetMarketHistory() error: %s", err) } } @@ -114,7 +116,7 @@ func TestPlaceBuyLimit(t *testing.T) { _, err := b.PlaceBuyLimit("btc-ltc", 1, 1) if err == nil { - t.Error("Test Failed - Bittrex - PlaceBuyLimit() error") + t.Error("Bittrex - PlaceBuyLimit() Expected error") } } @@ -123,7 +125,7 @@ func TestPlaceSellLimit(t *testing.T) { _, err := b.PlaceSellLimit("btc-ltc", 1, 1) if err == nil { - t.Error("Test Failed - Bittrex - PlaceSellLimit() error") + t.Error("Bittrex - PlaceSellLimit() Expected error") } } @@ -132,11 +134,11 @@ func TestGetOpenOrders(t *testing.T) { _, err := b.GetOpenOrders("") if err == nil { - t.Error("Test Failed - Bittrex - GetOrder() error") + t.Error("Bittrex - GetOrder() Expected error") } _, err = b.GetOpenOrders("btc-ltc") if err == nil { - t.Error("Test Failed - Bittrex - GetOrder() error") + t.Error("Bittrex - GetOrder() Expected error") } } @@ -145,7 +147,7 @@ func TestCancelExistingOrder(t *testing.T) { _, err := b.CancelExistingOrder("blaaaaaaa") if err == nil { - t.Error("Test Failed - Bittrex - CancelExistingOrder() error") + t.Error("Bittrex - CancelExistingOrder() Expected error") } } @@ -154,7 +156,7 @@ func TestGetAccountBalances(t *testing.T) { _, err := b.GetAccountBalances() if err == nil { - t.Error("Test Failed - Bittrex - GetAccountBalances() error") + t.Error("Bittrex - GetAccountBalances() Expected error") } } @@ -163,7 +165,7 @@ func TestGetAccountBalanceByCurrency(t *testing.T) { _, err := b.GetAccountBalanceByCurrency("btc") if err == nil { - t.Error("Test Failed - Bittrex - GetAccountBalanceByCurrency() error") + t.Error("Bittrex - GetAccountBalanceByCurrency() Expected error") } } @@ -172,11 +174,11 @@ func TestGetOrder(t *testing.T) { _, err := b.GetOrder("0cb4c4e4-bdc7-4e13-8c13-430e587d2cc1") if err == nil { - t.Error("Test Failed - Bittrex - GetOrder() error") + t.Error("Bittrex - GetOrder() Expected error") } _, err = b.GetOrder("") if err == nil { - t.Error("Test Failed - Bittrex - GetOrder() error") + t.Error("Bittrex - GetOrder() Expected error") } } @@ -185,11 +187,11 @@ func TestGetOrderHistoryForCurrency(t *testing.T) { _, err := b.GetOrderHistoryForCurrency("") if err == nil { - t.Error("Test Failed - Bittrex - GetOrderHistory() error") + t.Error("Bittrex - GetOrderHistory() Expected error") } _, err = b.GetOrderHistoryForCurrency("btc-ltc") if err == nil { - t.Error("Test Failed - Bittrex - GetOrderHistory() error") + t.Error("Bittrex - GetOrderHistory() Expected error") } } @@ -198,11 +200,11 @@ func TestGetwithdrawalHistory(t *testing.T) { _, err := b.GetWithdrawalHistory("") if err == nil { - t.Error("Test Failed - Bittrex - GetWithdrawalHistory() error") + t.Error("Bittrex - GetWithdrawalHistory() Expected error") } _, err = b.GetWithdrawalHistory("btc-ltc") if err == nil { - t.Error("Test Failed - Bittrex - GetWithdrawalHistory() error") + t.Error("Bittrex - GetWithdrawalHistory() Expected error") } } @@ -211,11 +213,11 @@ func TestGetDepositHistory(t *testing.T) { _, err := b.GetDepositHistory("") if err == nil { - t.Error("Test Failed - Bittrex - GetDepositHistory() error") + t.Error("Bittrex - GetDepositHistory() Expected error") } _, err = b.GetDepositHistory("btc-ltc") if err == nil { - t.Error("Test Failed - Bittrex - GetDepositHistory() error") + t.Error("Bittrex - GetDepositHistory() Expected error") } } @@ -232,7 +234,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() b.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -244,15 +246,11 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - b.SetDefaults() - TestSetup(t) - var feeBuilder = setFeeBuilder() - // CryptocurrencyTradeFee Basic if resp, err := b.GetFee(feeBuilder); resp != float64(0.0025) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0025), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0025), resp) } // CryptocurrencyTradeFee High quantity @@ -260,7 +258,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := b.GetFee(feeBuilder); resp != float64(2500) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2500), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2500), resp) t.Error(err) } @@ -268,7 +266,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := b.GetFee(feeBuilder); resp != float64(0.0025) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0025), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0025), resp) t.Error(err) } @@ -276,7 +274,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -284,7 +282,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := b.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) t.Error(err) } @@ -292,7 +290,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -301,7 +299,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -310,28 +308,22 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.HKD if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - b.SetDefaults() expectedResult := exchange.AutoWithdrawCryptoWithAPIPermissionText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := b.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, } @@ -347,11 +339,8 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) @@ -365,27 +354,27 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if b.APIKey != "" && b.APIKey != "Key" && - b.APISecret != "" && b.APISecret != "Secret" { - return true - } - return false + return b.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "-", - Base: currency.BTC, - Quote: currency.LTC, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: "-", + Base: currency.BTC, + Quote: currency.LTC, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := b.SubmitOrder(p, exchange.BuyOrderSide, exchange.LimitOrderType, 1, 1, "clientId") + response, err := b.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -394,16 +383,12 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -420,16 +405,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -445,26 +426,29 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestModifyOrder(t *testing.T) { - _, err := b.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := b.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - b.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.LTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -481,14 +465,11 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := b.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { @@ -497,14 +478,11 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - b.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := b.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { @@ -516,12 +494,30 @@ func TestGetDepositAddress(t *testing.T) { if areTestAPIKeysSet() { _, err := b.GetDepositAddress(currency.BTC, "") if err != nil { - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) } } else { _, err := b.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error cannot be nil") + t.Error("GetDepositAddress() error cannot be nil") } } } + +func TestParseTime(t *testing.T) { + t.Parallel() + + tm, err := parseTime("2019-11-21T02:08:34.87") + if err != nil { + t.Fatal(err) + } + + if tm.Year() != 2019 || + tm.Month() != 11 || + tm.Day() != 21 || + tm.Hour() != 2 || + tm.Minute() != 8 || + tm.Second() != 34 { + t.Error("invalid time values") + } +} diff --git a/exchanges/bittrex/bittrex_types.go b/exchanges/bittrex/bittrex_types.go index 854281a5..be400fd5 100644 --- a/exchanges/bittrex/bittrex_types.go +++ b/exchanges/bittrex/bittrex_types.go @@ -1,6 +1,8 @@ package bittrex -import "encoding/json" +import ( + "encoding/json" +) // Response is the generalised response type for Bittrex type Response struct { diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index a692ae1f..0af7bb87 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -2,20 +2,119 @@ package bittrex import ( "errors" - "fmt" "strings" "sync" "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (b *Bittrex) GetDefaultConfig() (*config.ExchangeConfig, error) { + b.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = b.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = b.BaseCurrencies + + err := b.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if b.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = b.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults method assignes the default values for Bittrex +func (b *Bittrex) SetDefaults() { + b.Name = "Bittrex" + b.Enabled = true + b.Verbose = true + b.API.CredentialsValidator.RequiresKey = true + b.API.CredentialsValidator.RequiresSecret = true + + b.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + } + + b.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: false, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + GetOrders: true, + CancelOrder: true, + SubmitOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + CryptoWithdrawalFee: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + b.Requester = request.New(b.Name, + request.NewRateLimit(time.Second, bittrexAuthRate), + request.NewRateLimit(time.Second, bittrexUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + b.API.Endpoints.URLDefault = bittrexAPIURL + b.API.Endpoints.URL = b.API.Endpoints.URLDefault +} + +// Setup method sets current configuration details if enabled +func (b *Bittrex) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + b.SetEnabled(false) + return nil + } + + return b.SetupDefaults(exch) +} + // Start starts the Bittrex go routine func (b *Bittrex) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -28,58 +127,72 @@ func (b *Bittrex) Start(wg *sync.WaitGroup) { // Run implements the Bittrex wrapper func (b *Bittrex) Run() { if b.Verbose { - log.Debugf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) + b.PrintEnabledPairs() } - exchangeProducts, err := b.GetMarkets() - if err != nil { - log.Errorf("%s Failed to get available symbols.\n", b.GetName()) - } else { - forceUpgrade := false - if !common.StringDataContains(b.EnabledPairs.Strings(), "-") || - !common.StringDataContains(b.AvailablePairs.Strings(), "-") { - forceUpgrade = true - } - var currencies []string - for x := range exchangeProducts.Result { - if !exchangeProducts.Result[x].IsActive || - exchangeProducts.Result[x].MarketName == "" { - continue - } - currencies = append(currencies, exchangeProducts.Result[x].MarketName) - } + forceUpdate := false + if !common.StringDataContains(b.GetEnabledPairs(asset.Spot).Strings(), "-") || + !common.StringDataContains(b.GetAvailablePairs(asset.Spot).Strings(), "-") { + forceUpdate = true + enabledPairs := []string{"USDT-BTC"} + log.Warn(log.ExchangeSys, "Available pairs for Bittrex reset due to config upgrade, please enable the ones you would like again") - if forceUpgrade { - enabledPairs := currency.Pairs{currency.Pair{Base: currency.USDT, - Quote: currency.BTC, Delimiter: "-"}} - - log.Warn("Available pairs for Bittrex reset due to config upgrade, please enable the ones you would like again") - - err = b.UpdateCurrencies(enabledPairs, true, true) - if err != nil { - log.Errorf("%s Failed to get config.", b.GetName()) - } - } - - var newCurrencies currency.Pairs - for _, p := range currencies { - newCurrencies = append(newCurrencies, - currency.NewPairFromString(p)) - } - - err = b.UpdateCurrencies(newCurrencies, false, forceUpgrade) + err := b.UpdatePairs(currency.NewPairsFromStrings(enabledPairs), asset.Spot, true, true) if err != nil { - log.Errorf("%s Failed to get config.", b.GetName()) + log.Errorf(log.ExchangeSys, + "%s failed to update currencies. Err: %s\n", + b.Name, + err) } } + + if !b.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { + return + } + + err := b.UpdateTradablePairs(forceUpdate) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + b.Name, + err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (b *Bittrex) FetchTradablePairs(asset asset.Item) ([]string, error) { + markets, err := b.GetMarkets() + if err != nil { + return nil, err + } + + var pairs []string + for x := range markets.Result { + if !markets.Result[x].IsActive || markets.Result[x].MarketName == "" { + continue + } + pairs = append(pairs, markets.Result[x].MarketName) + } + + return pairs, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (b *Bittrex) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := b.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) } // GetAccountInfo Retrieves balances for all enabled currencies for the // Bittrex exchange func (b *Bittrex) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = b.GetName() + response.Exchange = b.Name accountBalance, err := b.GetAccountBalances() if err != nil { return response, err @@ -102,44 +215,57 @@ func (b *Bittrex) GetAccountInfo() (exchange.AccountInfo, error) { } // UpdateTicker updates and returns the ticker for a currency pair -func (b *Bittrex) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (b *Bittrex) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - tick, err := b.GetMarketSummaries() + ticks, err := b.GetMarketSummaries() if err != nil { return tickerPrice, err } - - for _, x := range b.GetEnabledCurrencies() { - curr := exchange.FormatExchangeCurrency(b.Name, x) - for y := range tick.Result { - if tick.Result[y].MarketName != curr.String() { + pairs := b.GetEnabledPairs(assetType) + for i := range pairs { + for j := range ticks.Result { + if !strings.EqualFold(ticks.Result[j].MarketName, pairs[i].String()) { continue } - tickerPrice.Pair = x - tickerPrice.High = tick.Result[y].High - tickerPrice.Low = tick.Result[y].Low - tickerPrice.Ask = tick.Result[y].Ask - tickerPrice.Bid = tick.Result[y].Bid - tickerPrice.Last = tick.Result[y].Last - tickerPrice.Volume = tick.Result[y].Volume - ticker.ProcessTicker(b.GetName(), &tickerPrice, assetType) + tickerTime, err := parseTime(ticks.Result[j].TimeStamp) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s UpdateTicker unable to parse time: %s\n", b.Name, err) + } + tickerPrice = ticker.Price{ + Last: ticks.Result[j].Last, + High: ticks.Result[j].High, + Low: ticks.Result[j].Low, + Bid: ticks.Result[j].Bid, + Ask: ticks.Result[j].Ask, + Volume: ticks.Result[j].BaseVolume, + QuoteVolume: ticks.Result[j].Volume, + Close: ticks.Result[j].PrevDay, + Pair: pairs[i], + LastUpdated: tickerTime, + } + err = ticker.ProcessTicker(b.Name, &tickerPrice, assetType) + if err != nil { + log.Error(log.Ticker, err) + } } } + return ticker.GetTicker(b.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (b *Bittrex) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tick, err := ticker.GetTicker(b.GetName(), p, ticker.Spot) +// FetchTicker returns the ticker for a currency pair +func (b *Bittrex) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tick, err := ticker.GetTicker(b.Name, p, assetType) if err != nil { return b.UpdateTicker(p, assetType) } return tick, nil } -// GetOrderbookEx returns the orderbook for a currency pair -func (b *Bittrex) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(b.GetName(), p, assetType) +// FetchOrderbook returns the orderbook for a currency pair +func (b *Bittrex) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(b.Name, p, assetType) if err != nil { return b.UpdateOrderbook(p, assetType) } @@ -147,9 +273,9 @@ func (b *Bittrex) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.B } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (b *Bittrex) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (b *Bittrex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := b.GetOrderbook(exchange.FormatExchangeCurrency(b.GetName(), p).String()) + orderbookNew, err := b.GetOrderbook(b.FormatExchangeCurrency(p, assetType).String()) if err != nil { return orderBook, err } @@ -173,7 +299,7 @@ func (b *Bittrex) UpdateOrderbook(p currency.Pair, assetType string) (orderbook. } orderBook.Pair = p - orderBook.ExchangeName = b.GetName() + orderBook.ExchangeName = b.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -187,62 +313,67 @@ func (b *Bittrex) UpdateOrderbook(p currency.Pair, assetType string) (orderbook. // GetFundingHistory returns funding history, deposits and // withdrawals func (b *Bittrex) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (b *Bittrex) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (b *Bittrex) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (b *Bittrex) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - buy := side == exchange.BuyOrderSide +func (b *Bittrex) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } + + buy := s.OrderSide == order.Buy + if s.OrderType != order.Limit { + return submitOrderResponse, + errors.New("limit orders only supported on exchange") + } + var response UUID var err error - - if orderType != exchange.LimitOrderType { - return submitOrderResponse, errors.New("not supported on exchange") - } - if buy { - response, err = b.PlaceBuyLimit(p.String(), amount, price) + response, err = b.PlaceBuyLimit(s.Pair.String(), + s.Amount, + s.Price) } else { - response, err = b.PlaceSellLimit(p.String(), amount, price) + response, err = b.PlaceSellLimit(s.Pair.String(), + s.Amount, + s.Price) + } + if err != nil { + return submitOrderResponse, err } - if response.Result.ID != "" { submitOrderResponse.OrderID = response.Result.ID } - if err == nil { - submitOrderResponse.IsOrderPlaced = true - } + submitOrderResponse.IsOrderPlaced = true - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (b *Bittrex) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (b *Bittrex) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (b *Bittrex) CancelOrder(order *exchange.OrderCancellation) error { +func (b *Bittrex) CancelOrder(order *order.Cancel) error { _, err := b.CancelExistingOrder(order.OrderID) return err } // CancelAllOrders cancels all orders associated with a currency pair -func (b *Bittrex) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (b *Bittrex) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } openOrders, err := b.GetOpenOrders("") if err != nil { @@ -252,7 +383,7 @@ func (b *Bittrex) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cance for i := range openOrders.Result { _, err := b.CancelExistingOrder(openOrders.Result[i].OrderUUID) if err != nil { - cancelAllOrdersResponse.OrderStatus[openOrders.Result[i].OrderUUID] = err.Error() + cancelAllOrdersResponse.Status[openOrders.Result[i].OrderUUID] = err.Error() } } @@ -260,8 +391,8 @@ func (b *Bittrex) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cance } // GetOrderInfo returns information on a current open order -func (b *Bittrex) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (b *Bittrex) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -277,20 +408,20 @@ func (b *Bittrex) GetDepositAddress(cryptocurrency currency.Code, _ string) (str // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *Bittrex) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bittrex) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { uuid, err := b.Withdraw(withdrawRequest.Currency.String(), withdrawRequest.AddressTag, withdrawRequest.Address, withdrawRequest.Amount) - return fmt.Sprintf("%v", uuid), err + return uuid.Result.ID, err } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (b *Bittrex) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bittrex) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (b *Bittrex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *Bittrex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -301,7 +432,7 @@ func (b *Bittrex) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (b *Bittrex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (b.APIKey == "" || b.APISecret == "") && // Todo check connection status + if !b.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -309,10 +440,10 @@ func (b *Bittrex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) } // GetActiveOrders retrieves any orders that are active/open -func (b *Bittrex) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (b *Bittrex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { var currPair string - if len(getOrdersRequest.Currencies) == 1 { - currPair = getOrdersRequest.Currencies[0].String() + if len(req.Currencies) == 1 { + currPair = req.Currencies[0].String() } resp, err := b.GetOpenOrders(currPair) @@ -320,19 +451,23 @@ func (b *Bittrex) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ( return nil, err } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range resp.Result { - orderDate, err := time.Parse(time.RFC3339, resp.Result[i].Opened) + orderDate, err := parseTime(resp.Result[i].Opened) if err != nil { - log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v", - b.Name, "GetActiveOrders", resp.Result[i].OrderUUID, resp.Result[i].Opened) + log.Errorf(log.ExchangeSys, + "Exchange %v Func %v Order %v Could not parse date to unix with value of %v", + b.Name, + "GetActiveOrders", + resp.Result[i].OrderUUID, + resp.Result[i].Opened) } pair := currency.NewPairDelimiter(resp.Result[i].Exchange, - b.ConfigCurrencyPairFormat.Delimiter) - orderType := exchange.OrderType(strings.ToUpper(resp.Result[i].Type)) + b.GetPairFormat(asset.Spot, false).Delimiter) + orderType := order.Type(strings.ToUpper(resp.Result[i].Type)) - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ Amount: resp.Result[i].Quantity, RemainingAmount: resp.Result[i].QuantityRemaining, Price: resp.Result[i].Price, @@ -344,20 +479,18 @@ func (b *Bittrex) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ( }) } - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (b *Bittrex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (b *Bittrex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { var currPair string - if len(getOrdersRequest.Currencies) == 1 { - currPair = getOrdersRequest.Currencies[0].String() + if len(req.Currencies) == 1 { + currPair = req.Currencies[0].String() } resp, err := b.GetOrderHistoryForCurrency(currPair) @@ -365,19 +498,23 @@ func (b *Bittrex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( return nil, err } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range resp.Result { - orderDate, err := time.Parse(time.RFC3339, resp.Result[i].TimeStamp) + orderDate, err := parseTime(resp.Result[i].TimeStamp) if err != nil { - log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v", - b.Name, "GetActiveOrders", resp.Result[i].OrderUUID, resp.Result[i].Opened) + log.Errorf(log.ExchangeSys, + "Exchange %v Func %v Order %v Could not parse date to unix with value of %v", + b.Name, + "GetActiveOrders", + resp.Result[i].OrderUUID, + resp.Result[i].Opened) } pair := currency.NewPairDelimiter(resp.Result[i].Exchange, - b.ConfigCurrencyPairFormat.Delimiter) - orderType := exchange.OrderType(strings.ToUpper(resp.Result[i].Type)) + b.GetPairFormat(asset.Spot, false).Delimiter) + orderType := order.Type(strings.ToUpper(resp.Result[i].Type)) - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ Amount: resp.Result[i].Quantity, RemainingAmount: resp.Result[i].QuantityRemaining, Price: resp.Result[i].Price, @@ -390,11 +527,9 @@ func (b *Bittrex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( }) } - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } diff --git a/exchanges/btcmarkets/README.md b/exchanges/btcmarkets/README.md index fc2d7507..06a67665 100644 --- a/exchanges/btcmarkets/README.md +++ b/exchanges/btcmarkets/README.md @@ -23,6 +23,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader ### Current Features + REST Support ++ Websocket Support ### How to enable @@ -47,22 +48,22 @@ main.go ```go var b exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "BTCMarkets" { - b = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "BTCMarkets" { + b = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := b.GetTickerPrice() +tick, err := b.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := b.GetOrderbookEx() +ob, err := b.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +131,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index 695ab31e..a477f483 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -2,433 +2,661 @@ package btcmarkets import ( "bytes" + "encoding/json" "errors" "fmt" + "io" "net/http" "net/url" + "strconv" + "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( - btcMarketsAPIURL = "https://api.btcmarkets.net" - btcMarketsAPIVersion = "0" - btcMarketsAccountBalance = "/account/balance" - btcMarketsTradingFee = "/account/%s/%s/tradingfee" - btcMarketsOrderCreate = "/order/create" - btcMarketsOrderCancel = "/order/cancel" - btcMarketsOrderHistory = "/order/history" - btcMarketsOrderOpen = "/order/open" - btcMarketsOrderTradeHistory = "/order/trade/history" - btcMarketsOrderDetail = "/order/detail" - btcMarketsWithdrawCrypto = "/fundtransfer/withdrawCrypto" - btcMarketsWithdrawAud = "/fundtransfer/withdrawEFT" + btcMarketsAPIURL = "https://api.btcmarkets.net" + btcMarketsAPIVersion = "/v3" - // Status Values - orderStatusNew = "New" - orderStatusPlaced = "Placed" - orderStatusFailed = "Failed" - orderStatusError = "Error" - orderStatusCancelled = "Cancelled" - orderStatusPartiallyCancelled = "Partially Cancelled" - orderStatusFullyMatched = "Fully Matched" - orderStatusPartiallyMatched = "Partially Matched" + // UnAuthenticated EPs + btcMarketsAllMarkets = "/markets/" + btcMarketsGetTicker = "/ticker/" + btcMarketsGetTrades = "/trades?" + btcMarketOrderBooks = "/orderbook?" + btcMarketsCandles = "/candles?" + btcMarketsTickers = "/tickers?" + btcMarketsMultipleOrderbooks = "/orderbooks?" + btcMarketsGetTime = "/time" + btcMarketsWithdrawalFees = "/withdrawal-fees" + btcMarketsUnauthPath = btcMarketsAPIURL + btcMarketsAPIVersion + btcMarketsAllMarkets - btcmarketsAuthLimit = 10 - btcmarketsUnauthLimit = 25 + // Authenticated EPs + btcMarketsAccountBalance = "/accounts/me/balances" + btcMarketsTradingFees = "/accounts/me/trading-fees" + btcMarketsTransactions = "/accounts/me/transactions" + btcMarketsOrders = "/orders" + btcMarketsTradeHistory = "/trades" + btcMarketsWithdrawals = "/withdrawals" + btcMarketsDeposits = "/deposits" + btcMarketsTransfers = "/transfers" + btcMarketsAddresses = "/addresses" + btcMarketsAssets = "/assets" + btcMarketsReports = "/reports" + btcMarketsBatchOrders = "/batchorders" + + btcmarketsAuthLimit = 3 + btcmarketsUnauthLimit = 50 + + orderFailed = "Failed" + orderPartiallyCancelled = "Partially Cancelled" + orderCancelled = "Cancelled" + orderFullyMatched = "FullyMatched" + orderPartiallyMatched = "Partially Matched" + orderPlaced = "Placed" + orderAccepted = "Accepted" + + ask = "ask" + limit = "Limit" + market = "Market" + stopLimit = "Stop Limit" + stop = "Stop" + takeProfit = "Take Profit" + + subscribe = "subscribe" + fundChange = "fundChange" + orderChange = "orderChange" + heartbeat = "heartbeat" + tick = "tick" + wsOB = "orderbook" + trade = "trade" ) // BTCMarkets is the overarching type across the BTCMarkets package type BTCMarkets struct { exchange.Base - Ticker map[string]Ticker -} - -// SetDefaults sets basic defaults -func (b *BTCMarkets) SetDefaults() { - b.Name = "BTC Markets" - b.Enabled = false - b.Fee = 0.85 - b.Verbose = false - b.RESTPollingDelay = 10 - b.Ticker = make(map[string]Ticker) - b.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | - exchange.AutoWithdrawFiat - b.RequestCurrencyPairFormat.Delimiter = "" - b.RequestCurrencyPairFormat.Uppercase = true - b.ConfigCurrencyPairFormat.Delimiter = "-" - b.ConfigCurrencyPairFormat.Uppercase = true - b.AssetTypes = []string{ticker.Spot} - b.SupportsAutoPairUpdating = true - b.SupportsRESTTickerBatching = false - b.Requester = request.New(b.Name, - request.NewRateLimit(time.Second*10, btcmarketsAuthLimit), - request.NewRateLimit(time.Second*10, btcmarketsUnauthLimit), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - b.APIUrlDefault = btcMarketsAPIURL - b.APIUrl = b.APIUrlDefault - b.Websocket = wshandler.New() -} - -// Setup takes in an exchange configuration and sets all parameters -func (b *BTCMarkets) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - b.SetEnabled(false) - } else { - b.Enabled = true - b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - b.SetAPIKeys(exch.APIKey, exch.APISecret, "", true) - b.SetHTTPClientTimeout(exch.HTTPTimeout) - b.SetHTTPClientUserAgent(exch.HTTPUserAgent) - b.RESTPollingDelay = exch.RESTPollingDelay - b.Verbose = exch.Verbose - b.HTTPDebugging = exch.HTTPDebugging - b.BaseCurrencies = exch.BaseCurrencies - b.AvailablePairs = exch.AvailablePairs - b.EnabledPairs = exch.EnabledPairs - err := b.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = b.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = b.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = b.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = b.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - } + WebsocketConn *wshandler.WebsocketConnection } // GetMarkets returns the BTCMarkets instruments func (b *BTCMarkets) GetMarkets() ([]Market, error) { - type marketsResp struct { - Response - Markets []Market `json:"markets"` - } - - var resp marketsResp - path := fmt.Sprintf("%s/v2/market/active", b.APIUrl) - - err := b.SendHTTPRequest(path, &resp) - if err != nil { - return nil, err - } - - if !resp.Success { - return nil, fmt.Errorf("%s unable to get markets: %s", b.Name, resp.ErrorMessage) - } - - return resp.Markets, nil + var resp []Market + return resp, b.SendHTTPRequest(btcMarketsUnauthPath, &resp) } // GetTicker returns a ticker // symbol - example "btc" or "ltc" -func (b *BTCMarkets) GetTicker(firstPair, secondPair string) (Ticker, error) { - tick := Ticker{} - path := fmt.Sprintf("%s/market/%s/%s/tick", - b.APIUrl, - common.StringToUpper(firstPair), - common.StringToUpper(secondPair)) - - return tick, b.SendHTTPRequest(path, &tick) -} - -// GetOrderbook returns current orderbook -// symbol - example "btc" or "ltc" -func (b *BTCMarkets) GetOrderbook(firstPair, secondPair string) (Orderbook, error) { - orderbook := Orderbook{} - path := fmt.Sprintf("%s/market/%s/%s/orderbook", - b.APIUrl, - common.StringToUpper(firstPair), - common.StringToUpper(secondPair)) - - return orderbook, b.SendHTTPRequest(path, &orderbook) +func (b *BTCMarkets) GetTicker(marketID string) (Ticker, error) { + var tick Ticker + return tick, b.SendHTTPRequest(btcMarketsUnauthPath+marketID+btcMarketsGetTicker, &tick) } // GetTrades returns executed trades on the exchange -// symbol - example "btc" or "ltc" -// values - optional paramater "since" example values.Set(since, "59868345231") -func (b *BTCMarkets) GetTrades(firstPair, secondPair string, values url.Values) ([]Trade, error) { +func (b *BTCMarkets) GetTrades(marketID string, before, after, limit int64) ([]Trade, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } var trades []Trade - path := common.EncodeURLValues(fmt.Sprintf("%s/market/%s/%s/trades", - b.APIUrl, common.StringToUpper(firstPair), - common.StringToUpper(secondPair)), values) - - return trades, b.SendHTTPRequest(path, &trades) + params := url.Values{} + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return trades, b.SendHTTPRequest(btcMarketsUnauthPath+marketID+btcMarketsGetTrades+params.Encode(), + &trades) } -// NewOrder requests a new order and returns an ID -// currency - example "AUD" -// instrument - example "BTC" -// price - example 13000000000 (i.e x 100000000) -// amount - example 100000000 (i.e x 100000000) -// orderside - example "Bid" or "Ask" -// orderType - example "limit" -// clientReq - example "abc-cdf-1000" -func (b *BTCMarkets) NewOrder(currency, instrument string, price, amount float64, orderSide, orderType, clientReq string) (int64, error) { - newPrice := int64(price * float64(common.SatoshisPerBTC)) - newVolume := int64(amount * float64(common.SatoshisPerBTC)) - - order := OrderToGo{ - Currency: common.StringToUpper(currency), - Instrument: common.StringToUpper(instrument), - Price: newPrice, - Volume: newVolume, - OrderSide: orderSide, - OrderType: orderType, - ClientRequestID: clientReq, +// GetOrderbook returns current orderbook +func (b *BTCMarkets) GetOrderbook(marketID string, level int64) (Orderbook, error) { + var orderbook Orderbook + var temp tempOrderbook + params := url.Values{} + if level != 0 { + params.Set("level", strconv.FormatInt(level, 10)) } - - resp := Response{} - - err := b.SendAuthenticatedRequest(http.MethodPost, btcMarketsOrderCreate, order, &resp) + err := b.SendHTTPRequest(btcMarketsUnauthPath+marketID+btcMarketOrderBooks+params.Encode(), + &temp) if err != nil { - return 0, err + return orderbook, err } - if !resp.Success { - return 0, fmt.Errorf("%s Unable to place order. Error message: %s", b.GetName(), resp.ErrorMessage) - } - return int64(resp.ID), nil -} - -// CancelExistingOrder cancels an order by its ID -// orderID - id for order example "1337" -func (b *BTCMarkets) CancelExistingOrder(orderID []int64) ([]ResponseDetails, error) { - resp := Response{} - type CancelOrder struct { - OrderIDs []int64 `json:"orderIds"` - } - orders := CancelOrder{} - orders.OrderIDs = append(orders.OrderIDs, orderID...) - - err := b.SendAuthenticatedRequest(http.MethodPost, btcMarketsOrderCancel, orders, &resp) - if err != nil { - return resp.Responses, err - } - - if !resp.Success { - return resp.Responses, fmt.Errorf("%s Unable to cancel order. Error message: %s", b.GetName(), resp.ErrorMessage) - } - - return resp.Responses, nil -} - -// GetOrders returns current order information on the exchange -// currency - example "AUD" -// instrument - example "BTC" -// limit - example "10" -// since - since a time example "33434568724" -// historic - if false just normal Orders open -func (b *BTCMarkets) GetOrders(currency, instrument string, limit, since int64, historic bool) ([]Order, error) { - req := make(map[string]interface{}) - - if currency != "" { - req["currency"] = common.StringToUpper(currency) - } - if instrument != "" { - req["instrument"] = common.StringToUpper(instrument) - } - if limit != 0 { - req["limit"] = limit - } - if since != 0 { - req["since"] = since - } - - path := btcMarketsOrderOpen - if historic { - path = btcMarketsOrderHistory - } - - resp := Response{} - - err := b.SendAuthenticatedRequest(http.MethodPost, path, req, &resp) - if err != nil { - return nil, err - } - - if !resp.Success { - return nil, errors.New(resp.ErrorMessage) - } - - for i := range resp.Orders { - resp.Orders[i].Price /= common.SatoshisPerBTC - resp.Orders[i].OpenVolume /= common.SatoshisPerBTC - resp.Orders[i].Volume /= common.SatoshisPerBTC - - for j := range resp.Orders[i].Trades { - resp.Orders[i].Trades[j].Fee /= common.SatoshisPerBTC - resp.Orders[i].Trades[j].Price /= common.SatoshisPerBTC - resp.Orders[i].Trades[j].Volume /= common.SatoshisPerBTC + orderbook.MarketID = temp.MarketID + orderbook.SnapshotID = temp.SnapshotID + for x := range temp.Asks { + price, err := strconv.ParseFloat(temp.Asks[x][0], 64) + if err != nil { + return orderbook, err } + amount, err := strconv.ParseFloat(temp.Asks[x][1], 64) + if err != nil { + return orderbook, err + } + orderbook.Asks = append(orderbook.Asks, OBData{ + Price: price, + Volume: amount, + }) } - - return resp.Orders, nil + for a := range temp.Bids { + price, err := strconv.ParseFloat(temp.Bids[a][0], 64) + if err != nil { + return orderbook, err + } + amount, err := strconv.ParseFloat(temp.Bids[a][1], 64) + if err != nil { + return orderbook, err + } + orderbook.Bids = append(orderbook.Bids, OBData{ + Price: price, + Volume: amount, + }) + } + return orderbook, nil } -// GetOpenOrders returns all open orders -func (b *BTCMarkets) GetOpenOrders() ([]Order, error) { - type marketsResp struct { - Response +// GetMarketCandles gets candles for specified currency pair +func (b *BTCMarkets) GetMarketCandles(marketID, timeWindow, from, to string, before, after, limit int64) ([]MarketCandle, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") } - req := make(map[string]interface{}) - var resp marketsResp - path := fmt.Sprintf("/v2/order/open") - - err := b.SendAuthenticatedRequest(http.MethodGet, path, req, &resp) + var marketCandles []MarketCandle + var temp [][]string + params := url.Values{} + if timeWindow != "" { + params.Set("timeWindow", timeWindow) + } + if from != "" { + params.Set("from", from) + } + if to != "" { + params.Set("to", to) + } + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + err := b.SendHTTPRequest(btcMarketsUnauthPath+marketID+btcMarketsCandles+params.Encode(), + &temp) if err != nil { - return nil, err + return marketCandles, err } - - if !resp.Success { - return nil, errors.New(resp.ErrorMessage) - } - - for i := range resp.Orders { - resp.Orders[i].Price /= common.SatoshisPerBTC - resp.Orders[i].OpenVolume /= common.SatoshisPerBTC - resp.Orders[i].Volume /= common.SatoshisPerBTC - - for j := range resp.Orders[i].Trades { - resp.Orders[i].Trades[j].Fee /= common.SatoshisPerBTC - resp.Orders[i].Trades[j].Price /= common.SatoshisPerBTC - resp.Orders[i].Trades[j].Volume /= common.SatoshisPerBTC + var tempData MarketCandle + var tempTime time.Time + for x := range temp { + tempTime, err = time.Parse(time.RFC3339, temp[x][0]) + if err != nil { + return marketCandles, err } + tempData.Time = tempTime + tempData.Open, err = strconv.ParseFloat(temp[x][1], 64) + if err != nil { + return marketCandles, err + } + tempData.High, err = strconv.ParseFloat(temp[x][2], 64) + if err != nil { + return marketCandles, err + } + tempData.Low, err = strconv.ParseFloat(temp[x][3], 64) + if err != nil { + return marketCandles, err + } + tempData.Close, err = strconv.ParseFloat(temp[x][4], 64) + if err != nil { + return marketCandles, err + } + tempData.Volume, err = strconv.ParseFloat(temp[x][5], 64) + if err != nil { + return marketCandles, err + } + marketCandles = append(marketCandles, tempData) } - - return resp.Orders, nil + return marketCandles, nil } -// GetOrderDetail returns order information an a specific order -// orderID - example "1337" -func (b *BTCMarkets) GetOrderDetail(orderID []int64) ([]Order, error) { - type OrderDetail struct { - OrderIDs []int64 `json:"orderIds"` +// GetTickers gets multiple tickers +func (b *BTCMarkets) GetTickers(marketIDs []string) ([]Ticker, error) { + var tickers []Ticker + params := url.Values{} + for x := range marketIDs { + params.Add("marketId", marketIDs[x]) } - orders := OrderDetail{} - orders.OrderIDs = append(orders.OrderIDs, orderID...) + return tickers, b.SendHTTPRequest(btcMarketsUnauthPath+btcMarketsTickers+params.Encode(), + &tickers) +} - resp := Response{} - - err := b.SendAuthenticatedRequest(http.MethodPost, btcMarketsOrderDetail, orders, &resp) +// GetMultipleOrderbooks gets orderbooks +func (b *BTCMarkets) GetMultipleOrderbooks(marketIDs []string) ([]Orderbook, error) { + var orderbooks []Orderbook + var temp []tempOrderbook + var tempOB Orderbook + params := url.Values{} + for x := range marketIDs { + params.Add("marketId", marketIDs[x]) + } + err := b.SendHTTPRequest(btcMarketsUnauthPath+btcMarketsMultipleOrderbooks+params.Encode(), + &temp) if err != nil { - return nil, err + return orderbooks, err } - - if !resp.Success { - return nil, errors.New(resp.ErrorMessage) - } - - for i := range resp.Orders { - resp.Orders[i].Price /= common.SatoshisPerBTC - resp.Orders[i].OpenVolume /= common.SatoshisPerBTC - resp.Orders[i].Volume /= common.SatoshisPerBTC - - for x := range resp.Orders[i].Trades { - resp.Orders[i].Trades[x].Fee /= common.SatoshisPerBTC - resp.Orders[i].Trades[x].Price /= common.SatoshisPerBTC - resp.Orders[i].Trades[x].Volume /= common.SatoshisPerBTC + for i := range temp { + var price, volume float64 + tempOB.MarketID = temp[i].MarketID + tempOB.SnapshotID = temp[i].SnapshotID + for a := range temp[i].Asks { + volume, err = strconv.ParseFloat(temp[i].Asks[a][1], 64) + if err != nil { + return orderbooks, err + } + price, err = strconv.ParseFloat(temp[i].Asks[a][0], 64) + if err != nil { + return orderbooks, err + } + tempOB.Asks = append(tempOB.Asks, OBData{Price: price, Volume: volume}) } + for y := range temp[i].Bids { + volume, err = strconv.ParseFloat(temp[i].Bids[y][1], 64) + if err != nil { + return orderbooks, err + } + price, err = strconv.ParseFloat(temp[i].Bids[y][0], 64) + if err != nil { + return orderbooks, err + } + tempOB.Bids = append(tempOB.Bids, OBData{Price: price, Volume: volume}) + } + orderbooks = append(orderbooks, tempOB) } - return resp.Orders, nil + return orderbooks, nil +} + +// GetServerTime gets time from btcmarkets +func (b *BTCMarkets) GetServerTime() (time.Time, error) { + var resp TimeResp + return resp.Time, b.SendHTTPRequest(btcMarketsAPIURL+btcMarketsAPIVersion+btcMarketsGetTime, + &resp) } // GetAccountBalance returns the full account balance -func (b *BTCMarkets) GetAccountBalance() ([]AccountBalance, error) { - var balance []AccountBalance - - err := b.SendAuthenticatedRequest(http.MethodGet, btcMarketsAccountBalance, nil, &balance) - if err != nil { - return nil, err - } - - // All values are returned in Satoshis, even for fiat currencies. - for i := range balance { - balance[i].Balance /= common.SatoshisPerBTC - balance[i].PendingFunds /= common.SatoshisPerBTC - } - return balance, nil +func (b *BTCMarkets) GetAccountBalance() ([]AccountData, error) { + var resp []AccountData + return resp, + b.SendAuthenticatedRequest(http.MethodGet, + btcMarketsAccountBalance, + nil, + &resp) } -// GetTradingFee returns the account's trading fee for a currency pair -func (b *BTCMarkets) GetTradingFee(base, quote currency.Code) (TradingFee, error) { - var tradingFee TradingFee - path := fmt.Sprintf(btcMarketsTradingFee, base, quote) - return tradingFee, b.SendAuthenticatedRequest(http.MethodGet, path, nil, &tradingFee) -} - -// WithdrawCrypto withdraws cryptocurrency into a designated address -func (b *BTCMarkets) WithdrawCrypto(amount float64, currency, address string) (string, error) { - newAmount := int64(amount * float64(common.SatoshisPerBTC)) - - req := WithdrawRequestCrypto{ - Amount: newAmount, - Currency: common.StringToUpper(currency), - Address: address, - } - - resp := Response{} - err := b.SendAuthenticatedRequest(http.MethodPost, - btcMarketsWithdrawCrypto, - req, +// GetTradingFees returns trading fees for all pairs based on trading activity +func (b *BTCMarkets) GetTradingFees() (TradingFeeResponse, error) { + var resp TradingFeeResponse + return resp, b.SendAuthenticatedRequest(http.MethodGet, + btcMarketsTradingFees, + nil, &resp) - if err != nil { - return "", err - } - - if !resp.Success { - return "", errors.New(resp.ErrorMessage) - } - - return resp.Status, nil } -// WithdrawAUD withdraws AUD into a designated bank address -// Does not return a TxID! -func (b *BTCMarkets) WithdrawAUD(accountName, accountNumber, bankName, bsbNumber string, amount float64) (string, error) { - newAmount := int64(amount * float64(common.SatoshisPerBTC)) - - req := WithdrawRequestAUD{ - AccountName: accountName, - AccountNumber: accountNumber, - BankName: bankName, - BSBNumber: bsbNumber, - Amount: newAmount, - Currency: "AUD", +// GetTradeHistory returns trade history +func (b *BTCMarkets) GetTradeHistory(marketID, orderID string, before, after, limit int64) ([]TradeHistoryData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") } - - resp := Response{} - err := b.SendAuthenticatedRequest(http.MethodPost, btcMarketsWithdrawAud, - req, + var resp []TradeHistoryData + params := url.Values{} + if marketID != "" { + params.Set("marketId", marketID) + } + if orderID != "" { + params.Set("orderId", orderID) + } + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsTradeHistory, params), + nil, &resp) - if err != nil { - return "", err - } +} - if !resp.Success { - return "", errors.New(resp.ErrorMessage) - } +// GetTradeByID returns the singular trade of the ID given +func (b *BTCMarkets) GetTradeByID(id string) (TradeHistoryData, error) { + var resp TradeHistoryData + return resp, b.SendAuthenticatedRequest(http.MethodGet, + btcMarketsTradeHistory+"/"+id, + nil, + &resp) +} - return resp.Status, nil +// NewOrder requests a new order and returns an ID +func (b *BTCMarkets) NewOrder(marketID string, price, amount float64, orderType, side string, triggerPrice, + targetAmount float64, timeInForce string, postOnly bool, selfTrade, clientOrderID string) (OrderData, error) { + var resp OrderData + req := make(map[string]interface{}) + req["marketId"] = marketID + req["price"] = strconv.FormatFloat(price, 'f', -1, 64) + req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) + req["type"] = orderType + req["side"] = side + if orderType == stopLimit || orderType == takeProfit || orderType == stop { + req["triggerPrice"] = strconv.FormatFloat(triggerPrice, 'f', -1, 64) + } + if targetAmount > 0 { + req["targetAmount"] = strconv.FormatFloat(targetAmount, 'f', -1, 64) + } + if timeInForce != "" { + req["timeInForce"] = timeInForce + } + req["postOnly"] = postOnly + if selfTrade != "" { + req["selfTrade"] = selfTrade + } + if clientOrderID != "" { + req["clientOrderID"] = clientOrderID + } + return resp, b.SendAuthenticatedRequest(http.MethodPost, btcMarketsOrders, req, &resp) +} + +// GetOrders returns current order information on the exchange +func (b *BTCMarkets) GetOrders(marketID string, before, after, limit int64, status string) ([]OrderData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } + var resp []OrderData + params := url.Values{} + if marketID != "" { + params.Set("marketId", marketID) + } + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + if status != "" { + params.Set("status", status) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsOrders, params), nil, &resp) +} + +// CancelAllOpenOrdersByPairs cancels all open orders unless pairs are specified +func (b *BTCMarkets) CancelAllOpenOrdersByPairs(marketIDs []string) ([]CancelOrderResp, error) { + var resp []CancelOrderResp + req := make(map[string]interface{}) + if len(marketIDs) > 0 { + var strTemp strings.Builder + for x := range marketIDs { + strTemp.WriteString("marketId=" + marketIDs[x] + "&") + } + req["marketId"] = strTemp.String()[:strTemp.Len()-1] + } + return resp, b.SendAuthenticatedRequest(http.MethodDelete, btcMarketsOrders, req, &resp) +} + +// FetchOrder finds order based on the provided id +func (b *BTCMarkets) FetchOrder(id string) (OrderData, error) { + var resp OrderData + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsOrders+"/"+id, + nil, &resp) +} + +// RemoveOrder removes a given order +func (b *BTCMarkets) RemoveOrder(id string) (CancelOrderResp, error) { + var resp CancelOrderResp + return resp, b.SendAuthenticatedRequest(http.MethodDelete, btcMarketsOrders+"/"+id, + nil, &resp) +} + +// ListWithdrawals lists the withdrawal history +func (b *BTCMarkets) ListWithdrawals(before, after, limit int64) ([]TransferData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } + var resp []TransferData + params := url.Values{} + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsWithdrawals, params), + nil, + &resp) +} + +// GetWithdrawal gets withdrawawl info for a given id +func (b *BTCMarkets) GetWithdrawal(id string) (TransferData, error) { + var resp TransferData + if id == "" { + return resp, errors.New("id cannot be an empty string") + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsWithdrawals+"/"+id, + nil, &resp) +} + +// ListDeposits lists the deposit history +func (b *BTCMarkets) ListDeposits(before, after, limit int64) ([]TransferData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } + var resp []TransferData + params := url.Values{} + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsDeposits, params), + nil, + &resp) +} + +// GetDeposit gets deposit info for a given ID +func (b *BTCMarkets) GetDeposit(id string) (TransferData, error) { + var resp TransferData + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsDeposits+"/"+id, + nil, &resp) +} + +// ListTransfers lists the past asset transfers +func (b *BTCMarkets) ListTransfers(before, after, limit int64) ([]TransferData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } + var resp []TransferData + params := url.Values{} + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsTransfers, params), + nil, + &resp) +} + +// GetTransfer gets asset transfer info for a given ID +func (b *BTCMarkets) GetTransfer(id string) (TransferData, error) { + var resp TransferData + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsTransfers+"/"+id, + nil, &resp) +} + +// FetchDepositAddress gets deposit address for the given asset +func (b *BTCMarkets) FetchDepositAddress(assetName string, before, after, limit int64) (DepositAddress, error) { + var resp DepositAddress + if (before > 0) && (after >= 0) { + return resp, errors.New("BTCMarkets only supports either before or after, not both") + } + params := url.Values{} + params.Set("assetName", assetName) + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsAddresses, params), + nil, + &resp) +} + +// GetWithdrawalFees gets withdrawal fees for all assets +func (b *BTCMarkets) GetWithdrawalFees() ([]WithdrawalFeeData, error) { + var resp []WithdrawalFeeData + return resp, b.SendHTTPRequest(btcMarketsAPIURL+btcMarketsAPIVersion+btcMarketsWithdrawalFees, + &resp) +} + +// ListAssets lists all available assets +func (b *BTCMarkets) ListAssets() ([]AssetData, error) { + var resp []AssetData + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsAssets, nil, &resp) +} + +// GetTransactions gets trading fees +func (b *BTCMarkets) GetTransactions(assetName string, before, after, limit int64) ([]TransactionData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } + var resp []TransactionData + params := url.Values{} + if assetName != "" { + params.Set("assetName", assetName) + } + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsTransactions, params), + nil, + &resp) +} + +// CreateNewReport creates a new report +func (b *BTCMarkets) CreateNewReport(reportType, format string) (CreateReportResp, error) { + var resp CreateReportResp + req := make(map[string]interface{}) + req["type"] = reportType + req["format"] = format + return resp, b.SendAuthenticatedRequest(http.MethodPost, btcMarketsReports, req, &resp) +} + +// GetReport finds details bout a past report +func (b *BTCMarkets) GetReport(reportID string) (ReportData, error) { + var resp ReportData + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsReports+"/"+reportID, + nil, &resp) +} + +// RequestWithdraw requests withdrawals +func (b *BTCMarkets) RequestWithdraw(assetName string, amount float64, + toAddress, accountName, accountNumber, bsbNumber, bankName string) (TransferData, error) { + var resp TransferData + req := make(map[string]interface{}) + req["assetName"] = assetName + req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) + if assetName != "AUD" { + req["toAddress"] = toAddress + } else { + if accountName != "" { + req["accountName"] = accountName + } + if accountNumber != "" { + req["accountNumber"] = accountNumber + } + if bsbNumber != "" { + req["bsbNumber"] = bsbNumber + } + if bankName != "" { + req["bankName"] = bankName + } + } + return resp, b.SendAuthenticatedRequest(http.MethodPost, btcMarketsWithdrawals, req, &resp) +} + +// BatchPlaceCancelOrders places and cancels batch orders +func (b *BTCMarkets) BatchPlaceCancelOrders(cancelOrders []CancelBatch, placeOrders []PlaceBatch) (BatchPlaceCancelResponse, error) { + var resp BatchPlaceCancelResponse + var orderRequests []interface{} + if len(cancelOrders)+len(placeOrders) > 4 { + return resp, errors.New("BTCMarkets can only handle 4 orders at a time") + } + for x := range cancelOrders { + orderRequests = append(orderRequests, CancelOrderMethod{CancelOrder: cancelOrders[x]}) + } + for y := range placeOrders { + if placeOrders[y].ClientOrderID == "" { + return resp, errors.New("placeorders must have clientorderids filled") + } + orderRequests = append(orderRequests, PlaceOrderMethod{PlaceOrder: placeOrders[y]}) + } + return resp, b.SendAuthenticatedRequest(http.MethodPost, btcMarketsBatchOrders, orderRequests, &resp) +} + +// GetBatchTrades gets batch trades +func (b *BTCMarkets) GetBatchTrades(ids []string) (BatchTradeResponse, error) { + var resp BatchTradeResponse + if len(ids) > 50 { + return resp, errors.New("batchtrades can only handle 50 ids at a time") + } + marketIDs := strings.Join(ids, ",") + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsBatchOrders+"/"+marketIDs, + nil, &resp) +} + +// CancelBatchOrders cancels given ids +func (b *BTCMarkets) CancelBatchOrders(ids []string) (BatchCancelResponse, error) { + var resp BatchCancelResponse + marketIDs := strings.Join(ids, ",") + return resp, b.SendAuthenticatedRequest(http.MethodDelete, btcMarketsBatchOrders+"/"+marketIDs, + nil, &resp) } // SendHTTPRequest sends an unauthenticated HTTP request @@ -446,52 +674,47 @@ func (b *BTCMarkets) SendHTTPRequest(path string, result interface{}) error { } // SendAuthenticatedRequest sends an authenticated HTTP request -func (b *BTCMarkets) SendAuthenticatedRequest(reqType, path string, data, result interface{}) (err error) { - if !b.AuthenticatedAPISupport { +func (b *BTCMarkets) SendAuthenticatedRequest(method, path string, data, result interface{}) (err error) { + if !b.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } - n := b.Requester.GetNonce(true).String()[0:13] + strTime := strconv.FormatInt(time.Now().UTC().UnixNano()/1000000, 10) - var req string - payload := []byte("") - - if data != nil { - payload, err = common.JSONEncode(data) + var body io.Reader + var payload, hmac []byte + switch data.(type) { + case map[string]interface{}, []interface{}: + payload, err = json.Marshal(data) if err != nil { return err } - req = path + "\n" + n + "\n" + string(payload) - } else { - req = path + "\n" + n + "\n" - } - - hmac := common.GetHMAC(common.HashSHA512, - []byte(req), []byte(b.APISecret)) - - if b.Verbose { - log.Debugf("Sending %s request to URL %s with params %s\n", - reqType, - b.APIUrl+path, - req) + body = bytes.NewBuffer(payload) + strMsg := method + btcMarketsAPIVersion + path + strTime + string(payload) + hmac = crypto.GetHMAC(crypto.HashSHA512, + []byte(strMsg), []byte(b.API.Credentials.Secret)) + default: + strArray := strings.Split(path, "?") + hmac = crypto.GetHMAC(crypto.HashSHA512, + []byte(method+btcMarketsAPIVersion+strArray[0]+strTime), + []byte(b.API.Credentials.Secret)) } headers := make(map[string]string) headers["Accept"] = "application/json" headers["Accept-Charset"] = "UTF-8" headers["Content-Type"] = "application/json" - headers["apikey"] = b.APIKey - headers["timestamp"] = n - headers["signature"] = common.Base64Encode(hmac) - - return b.SendPayload(reqType, - b.APIUrl+path, + headers["BM-AUTH-APIKEY"] = b.API.Credentials.Key + headers["BM-AUTH-TIMESTAMP"] = strTime + headers["BM-AUTH-SIGNATURE"] = crypto.Base64Encode(hmac) + return b.SendPayload(method, + btcMarketsAPIURL+btcMarketsAPIVersion+path, headers, - bytes.NewBuffer(payload), + body, result, true, - true, + false, b.Verbose, b.HTTPDebugging, b.HTTPRecording) @@ -503,22 +726,33 @@ func (b *BTCMarkets) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { switch feeBuilder.FeeType { case exchange.CryptocurrencyTradeFee: - tradingFee, err := b.GetTradingFee(feeBuilder.Pair.Base, - feeBuilder.Pair.Quote) + temp, err := b.GetTradingFees() if err != nil { - return 0, err + return fee, err + } + for x := range temp.FeeByMarkets { + if currency.NewPairFromString(temp.FeeByMarkets[x].MarketID) == feeBuilder.Pair { + fee = temp.FeeByMarkets[x].MakerFeeRate + if !feeBuilder.IsMaker { + fee = temp.FeeByMarkets[x].TakerFeeRate + } + } } - - fee = calculateTradingFee(tradingFee, - feeBuilder.PurchasePrice, - feeBuilder.Amount) - case exchange.CryptocurrencyWithdrawalFee: - fee = getCryptocurrencyWithdrawalFee(feeBuilder.Pair.Base) + temp, err := b.GetWithdrawalFees() + if err != nil { + return fee, err + } + for x := range temp { + if currency.NewCode(temp[x].AssetName) == feeBuilder.Pair.Base { + fee = temp[x].Fee * feeBuilder.PurchasePrice * feeBuilder.Amount + } + } case exchange.InternationalBankWithdrawalFee: - fee = getInternationalBankWithdrawalFee(feeBuilder.FiatCurrency) + return 0, errors.New("international bank withdrawals are not supported") + case exchange.OfflineTradeFee: - fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount) + fee = getOfflineTradeFee(feeBuilder) } if fee < 0 { fee = 0 @@ -527,24 +761,11 @@ func (b *BTCMarkets) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { } // getOfflineTradeFee calculates the worst case-scenario trading fee -func getOfflineTradeFee(price, amount float64) float64 { - return 0.0085 * price * amount -} - -func calculateTradingFee(tradingFee TradingFee, purchasePrice, amount float64) (fee float64) { - fee = tradingFee.TradingFeeRate / 100000000 - return fee * amount * purchasePrice -} - -func getCryptocurrencyWithdrawalFee(c currency.Code) float64 { - return WithdrawalFees[c] -} - -func getInternationalBankWithdrawalFee(c currency.Code) float64 { - var fee float64 - - if c == currency.AUD { - fee = 0 +func getOfflineTradeFee(feeBuilder *exchange.FeeBuilder) float64 { + switch { + case feeBuilder.Pair.IsCryptoPair(): + return 0.002 * feeBuilder.PurchasePrice * feeBuilder.Amount + default: + return 0.0085 * feeBuilder.PurchasePrice * feeBuilder.Amount } - return fee } diff --git a/exchanges/btcmarkets/btcmarkets_test.go b/exchanges/btcmarkets/btcmarkets_test.go index 29d38127..f7e669cb 100644 --- a/exchanges/btcmarkets/btcmarkets_test.go +++ b/exchanges/btcmarkets/btcmarkets_test.go @@ -1,13 +1,14 @@ package btcmarkets import ( - "net/url" + "log" + "os" "testing" - "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) var b BTCMarkets @@ -17,476 +18,464 @@ const ( apiKey = "" apiSecret = "" canManipulateRealOrders = false + BTCAUD = "BTC-AUD" + LTCAUD = "LTC-AUD" + ETHAUD = "ETH-AUD" + fakePair = "Fake-USDT" + bid = "bid" ) -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { b.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal(err) + } bConfig, err := cfg.GetExchangeConfig("BTC Markets") if err != nil { - t.Error("Test Failed - BTC Markets Setup() init error") + log.Fatal(err) } - bConfig.APIKey = apiKey - bConfig.APISecret = apiSecret - bConfig.AuthenticatedAPISupport = true + bConfig.API.Credentials.Key = apiKey + bConfig.API.Credentials.Secret = apiSecret + bConfig.API.AuthenticatedSupport = true - b.Setup(&bConfig) + err = b.Setup(bConfig) + if err != nil { + log.Fatal(err) + } + + os.Exit(m.Run()) +} + +func areTestAPIKeysSet() bool { + return b.AllowAuthenticatedRequest() } func TestGetMarkets(t *testing.T) { t.Parallel() _, err := b.GetMarkets() if err != nil { - t.Error("Test failed - GetMarkets() error", err) + t.Error("GetTicker() error", err) } } func TestGetTicker(t *testing.T) { t.Parallel() - _, err := b.GetTicker("BTC", "AUD") + _, err := b.GetTicker(BTCAUD) if err != nil { - t.Error("Test failed - GetTicker() error", err) - } -} - -func TestGetOrderbook(t *testing.T) { - t.Parallel() - _, err := b.GetOrderbook("BTC", "AUD") - if err != nil { - t.Error("Test failed - GetOrderbook() error", err) + t.Error("GetOrderbook() error", err) } } func TestGetTrades(t *testing.T) { t.Parallel() - _, err := b.GetTrades("BTC", "AUD", nil) + _, err := b.GetTrades(BTCAUD, 0, 0, 5) if err != nil { - t.Error("Test failed - GetTrades() error", err) + t.Error("GetTrades() error", err) } +} - val := url.Values{} - val.Set("since", "0") - _, err = b.GetTrades("BTC", "AUD", val) +func TestGetOrderbook(t *testing.T) { + t.Parallel() + _, err := b.GetOrderbook(BTCAUD, 2) if err != nil { - t.Error("Test failed - GetTrades() error", err) + t.Error("GetTrades() error", err) } } -func TestNewOrder(t *testing.T) { +func TestGetMarketCandles(t *testing.T) { t.Parallel() - _, err := b.NewOrder("AUD", "BTC", 0, 0, "Bid", "limit", "testTest") - if err == nil { - t.Error("Test failed - NewOrder() error", err) + _, err := b.GetMarketCandles(BTCAUD, "", "", "", 0, 0, 5) + if err != nil { + t.Error(err) } } -func TestCancelExistingOrder(t *testing.T) { +func TestGetTickers(t *testing.T) { t.Parallel() - _, err := b.CancelExistingOrder([]int64{1337}) - if err == nil { - t.Error("Test failed - CancelExistingOrder() error", err) + temp := []string{BTCAUD, LTCAUD, ETHAUD} + _, err := b.GetTickers(temp) + if err != nil { + t.Error(err) } } -func TestGetOrders(t *testing.T) { +func TestGetMultipleOrderbooks(t *testing.T) { t.Parallel() - _, err := b.GetOrders("AUD", "BTC", 10, 0, false) - if err == nil { - t.Error("Test failed - GetOrders() error", err) - } - _, err = b.GetOrders("AUD", "BTC", 10, 0, true) - if err == nil { - t.Error("Test failed - GetOrders() error", err) + temp := []string{BTCAUD, LTCAUD, ETHAUD} + _, err := b.GetMultipleOrderbooks(temp) + if err != nil { + t.Error(err) } } -func TestGetOrderDetail(t *testing.T) { +func TestGetServerTime(t *testing.T) { t.Parallel() - _, err := b.GetOrderDetail([]int64{1337}) - if err == nil { - t.Error("Test failed - GetOrderDetail() error", err) + _, err := b.GetServerTime() + if err != nil { + t.Error(err) } } func TestGetAccountBalance(t *testing.T) { t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } _, err := b.GetAccountBalance() - if err == nil { - t.Error("Test failed - GetAccountBalance() error", err) + if err != nil { + t.Error(err) } } -func TestWithdrawCrypto(t *testing.T) { +func TestGetTradingFees(t *testing.T) { t.Parallel() - _, err := b.WithdrawCrypto(0, "BTC", "LOLOLOL") - if err == nil { - t.Error("Test failed - WithdrawCrypto() error", err) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetTradingFees() + if err != nil { + t.Error(err) } } -func TestWithdrawAUD(t *testing.T) { +func TestGetTradeHistory(t *testing.T) { t.Parallel() - _, err := b.WithdrawAUD("BLA", "1337", "blawest", "1336", 10000000) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetTradeHistory(ETHAUD, "", -1, -1, -1) + if err != nil { + t.Error(err) + } + _, err = b.GetTradeHistory(BTCAUD, "", -1, -1, 1) + if err != nil { + t.Error(err) + } + _, err = b.GetTradeHistory(fakePair, "", -1, -1, -1) if err == nil { - t.Error("Test failed - WithdrawAUD() error", err) + t.Error("expected an error due to invalid trading pair") + } +} + +func TestGetTradeByID(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetTradeByID("4712043732") + if err != nil { + t.Error(err) + } +} + +func TestNewOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + _, err := b.NewOrder(BTCAUD, 100, 1, limit, bid, 0, 0, "", true, "", "") + if err != nil { + t.Error(err) + } + _, err = b.NewOrder(BTCAUD, 100, 1, "invalid", bid, 0, 0, "", true, "", "") + if err == nil { + t.Error("expected an error due to invalid ordertype") + } + _, err = b.NewOrder(BTCAUD, 100, 1, limit, "invalid", 0, 0, "", true, "", "") + if err == nil { + t.Error("expected an error due to invalid orderside") + } +} + +func TestGetOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetOrders("", -1, -1, 2, "") + if err != nil { + t.Error(err) + } + _, err = b.GetOrders(LTCAUD, -1, -1, -1, "open") + if err != nil { + t.Error(err) + } +} + +func TestCancelOpenOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + temp := []string{BTCAUD, LTCAUD} + _, err := b.CancelAllOpenOrdersByPairs(temp) + if err != nil { + t.Error(err) + } + temp = []string{BTCAUD, fakePair} + _, err = b.CancelAllOpenOrdersByPairs(temp) + if err == nil { + t.Error("expected an error due to invalid marketID") + } +} + +func TestFetchOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.FetchOrder("4477045999") + if err != nil { + t.Error(err) + } + _, err = b.FetchOrder("696969") + if err == nil { + t.Error(err) + } +} + +func TestRemoveOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + _, err := b.RemoveOrder("") + if err != nil { + t.Error(err) + } +} + +func TestListWithdrawals(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.ListWithdrawals(-1, -1, -1) + if err != nil { + t.Error(err) + } +} + +func TestGetWithdrawal(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetWithdrawal("4477381751") + if err != nil { + t.Error(err) + } +} + +func TestListDeposits(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.ListDeposits(-1, -1, -1) + if err != nil { + t.Error(err) + } +} + +func TestGetDeposit(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetDeposit("4476769607") + if err != nil { + t.Error(err) + } +} + +func TestListTransfers(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.ListTransfers(-1, -1, -1) + if err != nil { + t.Error(err) + } +} + +func TestGetTransfer(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetTransfer("4476769607") + if err != nil { + t.Error(err) + } + _, err = b.GetTransfer("6969696") + if err == nil { + t.Error("expected an error due to invalid transferID") + } +} + +func TestFetchDepositAddress(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.FetchDepositAddress("LTC", -1, -1, -1) + if err != nil { + t.Error(err) + } + _, err = b.FetchDepositAddress(fakePair, -1, -1, -1) + if err != nil { + t.Error("expected an error due to invalid assetID") + } +} + +func TestGetWithdrawalFees(t *testing.T) { + t.Parallel() + _, err := b.GetWithdrawalFees() + if err != nil { + t.Error(err) + } +} + +func TestListAssets(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.ListAssets() + if err != nil { + t.Error(err) + } +} + +func TestGetTransactions(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetTransactions("", -1, -1, -1) + if err != nil { + t.Error(err) + } +} + +func TestCreateNewReport(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.CreateNewReport("TransactionReport", "json") + if err != nil { + t.Error(err) + } +} + +func TestGetReport(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetReport("1kv38epne5v7lek9f18m60idg6") + if err != nil { + t.Error(err) + } +} + +func TestRequestWithdaw(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + _, err := b.RequestWithdraw("BTC", 1, "sdjflajdslfjld", "", "", "", "") + if err == nil { + t.Error("expected an error due to invalid toAddress") + } +} + +func TestBatchPlaceCancelOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + var temp []PlaceBatch + o := PlaceBatch{ + MarketID: BTCAUD, + Amount: 11000, + Price: 1, + OrderType: order.Limit.String(), + Side: bid, + } + _, err := b.BatchPlaceCancelOrders(nil, append(temp, o)) + if err != nil { + t.Error(err) + } +} + +func TestGetBatchTrades(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + temp := []string{"4477045999", "4477381751", "4476769607"} + _, err := b.GetBatchTrades(temp) + if err != nil { + t.Error(err) + } +} + +func TestCancelBatchOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + temp := []string{"4477045999", "4477381751", "4477381751"} + _, err := b.CancelBatchOrders(temp) + if err != nil { + t.Error(err) } } func TestGetAccountInfo(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } _, err := b.GetAccountInfo() - if err == nil { - t.Error("Test failed - GetAccountInfo() error", err) - } -} - -func TestGetFundingHistory(t *testing.T) { - _, err := b.GetFundingHistory() - if err == nil { - t.Error("Test failed - GetAccountInfo() error", err) - } -} - -func TestCancelOrder(t *testing.T) { - _, err := b.CancelExistingOrder([]int64{1337}) - - if err == nil { - t.Error("Test failed - CancelgOrder() error", err) - } -} - -func TestGetOrderInfo(t *testing.T) { - _, err := b.GetOrderInfo("1337") - if err == nil { - t.Error("Test failed - GetOrderInfo() error", err) - } -} - -func setFeeBuilder() *exchange.FeeBuilder { - return &exchange.FeeBuilder{ - Amount: 1, - FeeType: exchange.CryptocurrencyTradeFee, - Pair: currency.NewPair(currency.BTC, currency.LTC), - PurchasePrice: 1, - } -} - -// TestGetFeeByTypeOfflineTradeFee logic test -func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { - var feeBuilder = setFeeBuilder() - b.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { - if feeBuilder.FeeType != exchange.OfflineTradeFee { - t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) - } - } else { - if feeBuilder.FeeType != exchange.CryptocurrencyTradeFee { - t.Errorf("Expected %v, received %v", exchange.CryptocurrencyTradeFee, feeBuilder.FeeType) - } - } -} - -func TestGetFee(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var feeBuilder = setFeeBuilder() - - if apiKey != "" || apiSecret != "" { - // CryptocurrencyTradeFee Fiat - feeBuilder = setFeeBuilder() - feeBuilder.Pair.Quote = currency.USD - if resp, err := b.GetFee(feeBuilder); resp != float64(0.00849999) || err != nil { - t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.00849999), resp) - } - - // CryptocurrencyTradeFee Basic - feeBuilder = setFeeBuilder() - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0022) || err != nil { - t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0022), resp) - } - - // CryptocurrencyTradeFee High quantity - feeBuilder = setFeeBuilder() - feeBuilder.Amount = 1000 - feeBuilder.PurchasePrice = 1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(2200) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(22000), resp) - t.Error(err) - } - - // CryptocurrencyTradeFee IsMaker - feeBuilder = setFeeBuilder() - feeBuilder.IsMaker = true - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0022) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0022), resp) - t.Error(err) - } - - // CryptocurrencyTradeFee Negative purchase price - feeBuilder = setFeeBuilder() - feeBuilder.PurchasePrice = -1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) - t.Error(err) - } - } - - // CryptocurrencyWithdrawalFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + if err != nil { t.Error(err) } - - // CyptocurrencyDepositFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) - t.Error(err) - } - - // InternationalBankDepositFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.InternationalBankDepositFee - feeBuilder.FiatCurrency = currency.AUD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) - t.Error(err) - } - - // InternationalBankWithdrawalFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee - feeBuilder.FiatCurrency = currency.AUD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) - t.Error(err) - } -} - -func TestFormatWithdrawPermissions(t *testing.T) { - b.SetDefaults() - expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.AutoWithdrawFiatText - - withdrawPermissions := b.FormatWithdrawPermissions() - - if withdrawPermissions != expectedResult { - t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) - } -} - -func TestGetActiveOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, - } - - _, err := b.GetActiveOrders(&getOrdersRequest) - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not get open orders: %s", err) - } else if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } } func TestGetOrderHistory(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, - Currencies: []currency.Pair{currency.NewPair(currency.LTC, - currency.BTC)}, + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") } - - _, err := b.GetOrderHistory(&getOrdersRequest) - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not get order history: %s", err) - } else if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") + var input order.GetOrdersRequest + input.OrderSide = order.Buy + _, err := b.GetOrderHistory(&input) + if err != nil { + t.Error(err) } } -// Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them -// ---------------------------------------------------------------------------------------------------------------------------- -func areTestAPIKeysSet() bool { - if b.APIKey != "" && b.APIKey != "Key" && - b.APISecret != "" && b.APISecret != "Secret" { - return true - } - return false -} - -func TestSubmitOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - var p = currency.Pair{ - Delimiter: "-", - Base: currency.BTC, - Quote: currency.LTC, - } - response, err := b.SubmitOrder(p, exchange.BuyOrderSide, exchange.LimitOrderType, 1, 1, "clientId") - if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { - t.Errorf("Order failed to be placed: %v", err) - } else if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") +func TestUpdateOrderbook(t *testing.T) { + t.Parallel() + cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.AUD.String(), "-") + _, err := b.UpdateOrderbook(cp, asset.Spot) + if err != nil { + t.Error(err) } } -func TestCancelExchangeOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ - OrderID: "1", - WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - AccountID: "1", - CurrencyPair: currencyPair, - } - - err := b.CancelOrder(orderCancellation) - if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not cancel orders: %v", err) - } -} - -func TestCancelAllExchangeOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ - OrderID: "1", - WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - AccountID: "1", - CurrencyPair: currencyPair, - } - - resp, err := b.CancelAllOrders(orderCancellation) - - if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not cancel orders: %v", err) - } - - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) - } -} - -func TestModifyOrder(t *testing.T) { - _, err := b.ModifyOrder(&exchange.ModifyOrder{}) - if err == nil { - t.Error("Test failed - ModifyOrder() error") - } -} - -func TestWithdraw(t *testing.T) { - b.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.LTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", - } - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - _, err := b.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) - if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } - if areTestAPIKeysSet() && err != nil { - t.Errorf("Withdraw failed to be placed: %v", err) - } -} - -func TestWithdrawFiat(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - var withdrawFiatRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.AUD, - Description: "WITHDRAW IT ALL", - BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, - BankAddress: "123 Fake St", - BankCity: "Tarry Town", - BankCountry: "Hyrule", - BankName: "Commonwealth Bank of Australia", - WireCurrency: currency.AUD.String(), - SwiftCode: "Taylor", - RequiresIntermediaryBank: false, - IsExpressWire: false, - } - - _, err := b.WithdrawFiatFunds(&withdrawFiatRequest) - if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } - if areTestAPIKeysSet() && err != nil { - t.Errorf("Withdraw failed to be placed: %v", err) - } -} - -func TestWithdrawInternationalBank(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - var withdrawFiatRequest = exchange.WithdrawRequest{} - - _, err := b.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) - if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) - } -} - -func TestGetDepositAddress(t *testing.T) { - _, err := b.GetDepositAddress(currency.BTC, "") - if err == nil { - t.Error("Test Failed - GetDepositAddress() error cannot be nil") +func TestUpdateTicker(t *testing.T) { + t.Parallel() + cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.AUD.String(), "-") + _, err := b.UpdateTicker(cp, asset.Spot) + if err != nil { + t.Error(err) } } diff --git a/exchanges/btcmarkets/btcmarkets_types.go b/exchanges/btcmarkets/btcmarkets_types.go index 896cf221..64730e9a 100644 --- a/exchanges/btcmarkets/btcmarkets_types.go +++ b/exchanges/btcmarkets/btcmarkets_types.go @@ -1,59 +1,74 @@ package btcmarkets -import "github.com/thrasher-corp/gocryptotrader/currency" - -// Response is the genralized response type -type Response struct { - Success bool `json:"success"` - ErrorCode int `json:"errorCode"` - ErrorMessage string `json:"errorMessage"` - ID int `json:"id"` - Responses []ResponseDetails `json:"responses"` - ClientRequestID string `json:"clientRequestId"` - Orders []Order `json:"orders"` - Status string `json:"status"` -} - -// ResponseDetails holds order status details -type ResponseDetails struct { - Success bool `json:"success"` - ErrorCode int `json:"errorCode"` - ErrorMessage string `json:"errorMessage"` - ID int64 `json:"id"` -} +import "time" // Market holds a tradable market instrument type Market struct { - Instrument string `json:"instrument"` - Currency string `json:"currency"` + MarketID string `json:"marketId"` + BaseAsset string `json:"baseAsset"` + QuoteAsset string `json:"quoteAsset"` + MinOrderAmount float64 `json:"minOrderAmount,string"` + MaxOrderAmount float64 `json:"maxOrderAmount,string"` + AmountDecimals int64 `json:"amountDecimals,string"` + PriceDecimals int64 `json:"priceDecimals,string"` } // Ticker holds ticker information type Ticker struct { - BestBID float64 `json:"bestBid"` - BestAsk float64 `json:"bestAsk"` - LastPrice float64 `json:"lastPrice"` - Currency string `json:"currency"` - Instrument string `json:"instrument"` - Timestamp int64 `json:"timestamp"` - Volume float64 `json:"volume24h"` -} - -// Orderbook holds current orderbook information returned from the exchange -type Orderbook struct { - Currency string `json:"currency"` - Instrument string `json:"instrument"` - Timestamp int64 `json:"timestamp"` - Asks [][]float64 `json:"asks"` - Bids [][]float64 `json:"bids"` + MarketID string `json:"marketId"` + BestBID float64 `json:"bestBid,string"` + BestAsk float64 `json:"bestAsk,string"` + LastPrice float64 `json:"lastPrice,string"` + Volume float64 `json:"volume24h,string"` + Change24h float64 `json:"price24h,string"` + Low24h float64 `json:"low24h,string"` + High24h float64 `json:"high24h,string"` + Timestamp time.Time `json:"timestamp"` } // Trade holds trade information type Trade struct { - TradeID int64 `json:"tid"` - Amount float64 `json:"amount"` - Price float64 `json:"price"` - Date int64 `json:"date"` + TradeID string `json:"id"` + Amount float64 `json:"amount,string"` + Price float64 `json:"price,string"` + Timestamp time.Time `json:"timestamp"` +} + +// tempOrderbook stores orderbook data +type tempOrderbook struct { + MarketID string `json:"marketId"` + SnapshotID int64 `json:"snapshotId"` + Asks [][2]string `json:"asks"` + Bids [][2]string `json:"bids"` +} + +// OBData stores orderbook data +type OBData struct { + Price float64 + Volume float64 +} + +// Orderbook holds current orderbook information returned from the exchange +type Orderbook struct { + MarketID string + SnapshotID int64 + Asks []OBData + Bids []OBData +} + +// MarketCandle stores candle data for a given pair +type MarketCandle struct { + Time time.Time + Open float64 + Close float64 + Low float64 + High float64 + Volume float64 +} + +// TimeResp stores server time +type TimeResp struct { + Time time.Time `json:"timestamp"` } // TradingFee 30 day trade volume @@ -83,7 +98,7 @@ type Order struct { Instrument string `json:"instrument"` OrderSide string `json:"orderSide"` OrderType string `json:"ordertype"` - CreationTime float64 `json:"creationTime"` + CreationTime time.Time `json:"creationTime"` Status string `json:"status"` ErrorMessage string `json:"errorMessage"` Price float64 `json:"price"` @@ -95,19 +110,164 @@ type Order struct { // TradeResponse holds trade information type TradeResponse struct { - ID int64 `json:"id"` - CreationTime float64 `json:"creationTime"` - Description string `json:"description"` - Price float64 `json:"price"` - Volume float64 `json:"volume"` - Fee float64 `json:"fee"` + ID int64 `json:"id"` + CreationTime time.Time `json:"creationTime"` + Description string `json:"description"` + Price float64 `json:"price"` + Volume float64 `json:"volume"` + Fee float64 `json:"fee"` } -// AccountBalance holds account balance details -type AccountBalance struct { - Balance float64 `json:"balance"` - PendingFunds float64 `json:"pendingFunds"` - Currency string `json:"currency"` +// AccountData stores account data +type AccountData struct { + AssetName string `json:"assetName"` + Balance float64 `json:"balance,string"` + Available float64 `json:"available,string"` + Locked float64 `json:"locked,string"` +} + +// TradeHistoryData stores data of past trades +type TradeHistoryData struct { + ID string `json:"id"` + MarketID string `json:"marketId"` + Timestamp time.Time `json:"timestamp"` + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + Side string `json:"side"` + Fee float64 `json:"fee,string"` + OrderID string `json:"orderId"` + LiquidityType string `json:"liquidityType"` +} + +// OrderData stores data for new order created +type OrderData struct { + OrderID string `json:"orderId"` + MarketID string `json:"marketId"` + Side string `json:"side"` + Type string `json:"type"` + CreationTime time.Time `json:"creationTime"` + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + OpenAmount float64 `json:"openAmount,string"` + Status string `json:"status"` +} + +// CancelOrderResp stores data for cancelled orders +type CancelOrderResp struct { + OrderID string `json:"orderId"` + ClientOrderID string `json:"clientOrderId"` +} + +// PaymentDetails stores payment address +type PaymentDetails struct { + Address string `json:"address"` +} + +// TransferData stores data from asset transfers +type TransferData struct { + ID string `json:"id"` + AssetName string `json:"assetName"` + Amount float64 `json:"amount,string"` + RequestType string `json:"type"` + CreationTime time.Time `json:"creationTime"` + Status string `json:"status"` + Description string `json:"description"` + Fee float64 `json:"fee,string"` + LastUpdate string `json:"lastUpdate"` + PaymentDetails PaymentDetails `json:"paymentDetail"` +} + +// DepositAddress stores deposit address data +type DepositAddress struct { + Address string `json:"address"` + AssetName string `json:"assetName"` +} + +// WithdrawalFeeData stores data for fees +type WithdrawalFeeData struct { + AssetName string `json:"assetName"` + Fee float64 `json:"fee,string"` +} + +// AssetData stores data for given asset +type AssetData struct { + AssetName string `json:"assetName"` + MinDepositAmount float64 `json:"minDepositAmount,string"` + MaxDepositAmount float64 `json:"maxDepositAmount,string"` + DepositDecimals float64 `json:"depositDecimals,string"` + MinWithdrawalAmount float64 `json:"minWithdrawalAmount,string"` + MaxWithdrawalAmount float64 `json:"maxWithdrawalAmount,string"` + WithdrawalDecimals float64 `json:"withdrawalDecimals,string"` + WithdrawalFee float64 `json:"withdrawalFee,string"` + DepositFee float64 `json:"depositFee,string"` +} + +// TransactionData stores data from past transactions +type TransactionData struct { + ID string `json:"id"` + CreationTime time.Time `json:"creationTime"` + Description string `json:"description"` + AssetName string `json:"assetName"` + Amount float64 `json:"amount,string"` + Balance float64 `json:"balance,string"` + FeeType string `json:"type"` + RecordType string `json:"recordType"` + ReferrenceID string `json:"referrenceId"` +} + +// CreateReportResp stores data for created report +type CreateReportResp struct { + ReportID string `json:"reportId"` +} + +// ReportData gets data for a created report +type ReportData struct { + ID string `json:"id"` + ContentURL string `json:"contentUrl"` + CreationTime time.Time `json:"creationTime"` + ReportType string `json:"reportType"` + Status string `json:"status"` + Format string `json:"format"` +} + +// BatchPlaceData stores data for placed batch orders +type BatchPlaceData struct { + OrderID string `json:"orderId"` + MarketID string `json:"marketId"` + Side string `json:"side"` + Type string `json:"type"` + CreationTime time.Time `json:"creationTime"` + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + OpenAmount float64 `json:"openAmount,string"` + Status string `json:"status"` + ClientOrderID string `json:"clientOrderId"` +} + +// UnprocessedBatchResp stores data for unprocessed response +type UnprocessedBatchResp struct { + Code string `json:"code"` + Message string `json:"message"` + RequestID string `json:"requestId"` +} + +// BatchPlaceCancelResponse stores place and cancel batch data +type BatchPlaceCancelResponse struct { + PlacedOrders []BatchPlaceData `json:"placeOrders"` + CancelledOrders []CancelOrderResp `json:"cancelOrders"` + UnprocessedOrders []UnprocessedBatchResp `json:"unprocessedRequests"` +} + +// BatchTradeResponse stores the trades from batchtrades +type BatchTradeResponse struct { + Orders []BatchPlaceData `json:"orders"` + UnprocessedRequests []UnprocessedBatchResp `json:"unprocessedRequests"` +} + +// BatchCancelResponse stores the cancellation details from batch cancels +type BatchCancelResponse struct { + CancelOrders []CancelOrderResp `json:"cancelOrders"` + UnprocessedRequests []UnprocessedBatchResp `json:"unprocessedRequests"` } // WithdrawRequestCrypto is a generalized withdraw request type @@ -127,16 +287,143 @@ type WithdrawRequestAUD struct { BSBNumber string `json:"bsbNumber"` } -// WithdrawalFees the large list of predefined withdrawal fees -// Prone to change -var WithdrawalFees = map[currency.Code]float64{ - currency.AUD: 0, - currency.BTC: 0.001, - currency.ETH: 0.001, - currency.ETC: 0.001, - currency.LTC: 0.0001, - currency.XRP: 0.15, - currency.BCH: 0.0001, - currency.OMG: 0.15, - currency.POWR: 5, +// CancelBatch stores data for batch cancel request +type CancelBatch struct { + OrderID string `json:"orderId,omitempty"` + ClientOrderID string `json:"clientOrderId,omitempty"` +} + +// PlaceBatch stores data for place batch request +type PlaceBatch struct { + MarketID string `json:"marketId"` + Price float64 `json:"price"` + Amount float64 `json:"amount"` + OrderType string `json:"type"` + Side string `json:"side"` + TriggerPrice float64 `json:"triggerPrice,omitempty"` + TriggerAmount float64 `json:"triggerAmount,omitempty"` + TimeInForce string `json:"timeInForce,omitempty"` + PostOnly bool `json:"postOnly,omitempty"` + SelfTrade string `json:"selfTrade,omitempty"` + ClientOrderID string `json:"clientOrderId,omitempty"` +} + +// PlaceOrderMethod stores data for place request +type PlaceOrderMethod struct { + PlaceOrder PlaceBatch `json:"placeOrder,omitempty"` +} + +// CancelOrderMethod stores data for Cancel request +type CancelOrderMethod struct { + CancelOrder CancelBatch `json:"cancelOrder,omitempty"` +} + +// TradingFeeData stores trading fee data +type TradingFeeData struct { + MakerFeeRate float64 `json:"makerFeeRate,string"` + TakerFeeRate float64 `json:"takerFeeRate,string"` + MarketID string `json:"marketId"` +} + +// TradingFeeResponse stores trading fee data +type TradingFeeResponse struct { + MonthlyVolume float64 `json:"volume30Day,string"` + FeeByMarkets []TradingFeeData `json:"FeeByMarkets"` +} + +// WsSubscribe message sent via ws to subscribe +type WsSubscribe struct { + MarketIDs []string `json:"marketIds,omitempty"` + Channels []string `json:"channels"` + MessageType string `json:"messageType"` +} + +// WsAuthSubscribe message sent via login to subscribe +type WsAuthSubscribe struct { + MarketIDs []string `json:"marketIds,omitempty"` + Channels []string `json:"channels"` + Key string `json:"key"` + Signature string `json:"signature"` + Timestamp string `json:"timestamp"` + MessageType string `json:"messageType"` +} + +// WsMessageType message sent via ws to determine type +type WsMessageType struct { + MessageType string `json:"messageType"` +} + +// WsTick message received for ticker data +type WsTick struct { + Currency string `json:"marketId"` + Timestamp time.Time `json:"timestamp"` + Bid float64 `json:"bestBid,string"` + Ask float64 `json:"bestAsk,string"` + Last float64 `json:"lastPrice,string"` + Volume float64 `json:"volume24h,string"` + Price24h float64 `json:"price24h,string"` + Low24h float64 `json:"low24h,string"` + High24 float64 `json:"high24h,string"` + MessageType string `json:"messageType"` +} + +// WsTrade message received for trade data +type WsTrade struct { + Currency string `json:"marketId"` + Timestamp time.Time `json:"timestamp"` + TradeID int64 `json:"tradeId"` + Price float64 `json:"price,string"` + Volume float64 `json:"volume,string"` + MessageType string `json:"messageType"` +} + +// WsOrderbook message received for orderbook data +type WsOrderbook struct { + Currency string `json:"marketId"` + Timestamp time.Time `json:"timestamp"` + Bids [][]string `json:"bids"` + Asks [][]string `json:"asks"` + MessageType string `json:"messageType"` +} + +// WsFundTransfer stores fund transfer data for websocket +type WsFundTransfer struct { + FundTransferID int64 `json:"fundtransferId"` + TransferType string `json:"type"` + Status string `json:"status"` + Timestamp time.Time `json:"timestamp"` + Amount float64 `json:"amount,string"` + Currency string `json:"currency"` + Fee float64 `json:"fee,string"` + MessageType string `json:"messageType"` +} + +// WsTradeData stores trade data for websocket +type WsTradeData struct { + TradeID int64 `json:"tradeId"` + Price float64 `json:"price,string"` + Volume float64 `json:"volume,string"` + Fee float64 `json:"fee,string"` + LiquidityType string `json:"liquidityType"` +} + +// WsOrderChange stores order data +type WsOrderChange struct { + OrderID int64 `json:"orderId"` + MarketID string `json:"marketId"` + Side string `json:"side"` + OrderType string `json:"type"` + OpenVolume float64 `json:"openVolume,string"` + Status string `json:"status"` + TriggerStatus string `json:"triggerStatus"` + Trades []WsTradeData `json:"trades"` + Timestamp time.Time `json:"timestamp"` + MessageType string `json:"messageType"` +} + +// WsError stores websocket error data +type WsError struct { + MessageType string `json:"messageType"` + Code int64 `json:"code"` + Message string `json:"message"` } diff --git a/exchanges/btcmarkets/btcmarkets_websocket.go b/exchanges/btcmarkets/btcmarkets_websocket.go new file mode 100644 index 00000000..79cf815a --- /dev/null +++ b/exchanges/btcmarkets/btcmarkets_websocket.go @@ -0,0 +1,290 @@ +package btcmarkets + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +const ( + btcMarketsWSURL = "wss://socket.btcmarkets.net/v2" +) + +// WsConnect connects to a websocket feed +func (b *BTCMarkets) WsConnect() error { + if !b.Websocket.IsEnabled() || !b.IsEnabled() { + return errors.New(wshandler.WebsocketNotEnabled) + } + var dialer websocket.Dialer + err := b.WebsocketConn.Dial(&dialer, http.Header{}) + if err != nil { + return err + } + if b.Verbose { + log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", b.Name) + } + go b.WsHandleData() + if b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + b.createChannels() + if err != nil { + b.Websocket.DataHandler <- err + b.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + } + b.generateDefaultSubscriptions() + return nil +} + +// WsHandleData handles websocket data from WsReadData +func (b *BTCMarkets) WsHandleData() { + b.Websocket.Wg.Add(1) + defer func() { + b.Websocket.Wg.Done() + }() + + for { + select { + case <-b.Websocket.ShutdownC: + return + default: + resp, err := b.WebsocketConn.ReadMessage() + if err != nil { + b.Websocket.ReadMessageErrors <- err + return + } + b.Websocket.TrafficAlert <- struct{}{} + var wsResponse WsMessageType + err = json.Unmarshal(resp.Raw, &wsResponse) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + switch wsResponse.MessageType { + case heartbeat: + if b.Verbose { + log.Debugf(log.ExchangeSys, "%v - Websocket heartbeat received %s", b.Name, resp.Raw) + } + case wsOB: + var ob WsOrderbook + err := json.Unmarshal(resp.Raw, &ob) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + p := currency.NewPairFromString(ob.Currency) + var bids, asks []orderbook.Item + for x := range ob.Bids { + var price, amount float64 + price, err = strconv.ParseFloat(ob.Bids[x][0], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + amount, err = strconv.ParseFloat(ob.Bids[x][1], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + bids = append(bids, orderbook.Item{ + Amount: amount, + Price: price, + }) + } + for x := range ob.Asks { + var price, amount float64 + price, err = strconv.ParseFloat(ob.Asks[x][0], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + amount, err = strconv.ParseFloat(ob.Asks[x][1], 64) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + asks = append(asks, orderbook.Item{ + Amount: amount, + Price: price, + }) + } + err = b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ + Pair: p, + Bids: bids, + Asks: asks, + LastUpdated: ob.Timestamp, + AssetType: asset.Spot, + ExchangeName: b.Name, + }) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Pair: p, + Asset: asset.Spot, + Exchange: b.Name, + } + case trade: + var trade WsTrade + err := json.Unmarshal(resp.Raw, &trade) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + p := currency.NewPairFromString(trade.Currency) + b.Websocket.DataHandler <- wshandler.TradeData{ + Timestamp: trade.Timestamp, + CurrencyPair: p, + AssetType: asset.Spot, + Exchange: b.Name, + Price: trade.Price, + Amount: trade.Volume, + } + case tick: + var tick WsTick + err := json.Unmarshal(resp.Raw, &tick) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + p := currency.NewPairFromString(tick.Currency) + b.Websocket.DataHandler <- wshandler.TickerData{ + Exchange: b.Name, + Volume: tick.Volume, + High: tick.High24, + Low: tick.Low24h, + Bid: tick.Bid, + Ask: tick.Ask, + Last: tick.Last, + Timestamp: tick.Timestamp, + AssetType: asset.Spot, + Pair: p, + } + case fundChange: + var transferData WsFundTransfer + err := json.Unmarshal(resp.Raw, &transferData) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + b.Websocket.DataHandler <- transferData + case orderChange: + var orderData WsOrderChange + err := json.Unmarshal(resp.Raw, &orderData) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + b.Websocket.DataHandler <- orderData + case "error": + var wsErr WsError + err := json.Unmarshal(resp.Raw, &wsErr) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + b.Websocket.DataHandler <- fmt.Errorf("%v websocket error. Code: %v Message: %v", b.Name, wsErr.Code, wsErr.Message) + default: + b.Websocket.DataHandler <- fmt.Errorf("%v Unhandled websocket message %s", b.Name, resp.Raw) + } + } + } +} + +func (b *BTCMarkets) generateDefaultSubscriptions() { + var channels = []string{tick, trade, wsOB} + enabledCurrencies := b.GetEnabledPairs(asset.Spot) + var subscriptions []wshandler.WebsocketChannelSubscription + for i := range channels { + for j := range enabledCurrencies { + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ + Channel: channels[i], + Currency: enabledCurrencies[j], + }) + } + } + b.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (b *BTCMarkets) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { + unauthChannels := []string{tick, trade, wsOB} + authChannels := []string{fundChange, heartbeat, orderChange} + switch { + case common.StringDataCompare(unauthChannels, channelToSubscribe.Channel): + req := WsSubscribe{ + MarketIDs: []string{b.FormatExchangeCurrency(channelToSubscribe.Currency, asset.Spot).String()}, + Channels: []string{channelToSubscribe.Channel}, + MessageType: subscribe, + } + err := b.WebsocketConn.SendMessage(req) + if err != nil { + return err + } + case common.StringDataCompare(authChannels, channelToSubscribe.Channel): + message, ok := channelToSubscribe.Params["AuthSub"].(WsAuthSubscribe) + if !ok { + return errors.New("invalid params data") + } + tempAuthData := b.generateAuthSubscriptions() + message.Channels = append(message.Channels, channelToSubscribe.Channel, heartbeat) + message.Key = tempAuthData.Key + message.Signature = tempAuthData.Signature + message.Timestamp = tempAuthData.Timestamp + err := b.WebsocketConn.SendMessage(message) + if err != nil { + return err + } + } + return nil +} + +// Login logs in allowing private ws events +func (b *BTCMarkets) generateAuthSubscriptions() WsAuthSubscribe { + var authSubInfo WsAuthSubscribe + signTime := strconv.FormatInt(time.Now().UTC().UnixNano()/1000000, 10) + strToSign := "/users/self/subscribe" + "\n" + signTime + tempSign := crypto.GetHMAC(crypto.HashSHA512, + []byte(strToSign), + []byte(b.API.Credentials.Secret)) + sign := crypto.Base64Encode(tempSign) + authSubInfo.Key = b.API.Credentials.Key + authSubInfo.Signature = sign + authSubInfo.Timestamp = signTime + return authSubInfo +} + +// createChannels creates channels that need to be +func (b *BTCMarkets) createChannels() { + tempChannels := []string{orderChange, fundChange} + var channels []wshandler.WebsocketChannelSubscription + pairArray := b.GetEnabledPairs(asset.Spot) + for y := range tempChannels { + for x := range pairArray { + var authSub WsAuthSubscribe + var channel wshandler.WebsocketChannelSubscription + channel.Params = make(map[string]interface{}) + channel.Channel = tempChannels[y] + authSub.MarketIDs = append(authSub.MarketIDs, b.FormatExchangeCurrency(pairArray[x], asset.Spot).String()) + authSub.MessageType = subscribe + channel.Params["AuthSub"] = authSub + channels = append(channels, channel) + } + } + b.Websocket.SubscribeToChannels(channels) +} diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index 4bfec6f6..7c59d882 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -3,20 +3,163 @@ package btcmarkets import ( "errors" "fmt" - "strconv" "strings" "sync" "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (b *BTCMarkets) GetDefaultConfig() (*config.ExchangeConfig, error) { + b.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = b.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = b.BaseCurrencies + + err := b.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if b.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = b.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets basic defaults +func (b *BTCMarkets) SetDefaults() { + b.Name = "BTC Markets" + b.Enabled = true + b.Verbose = true + b.API.CredentialsValidator.RequiresKey = true + b.API.CredentialsValidator.RequiresSecret = true + b.API.CredentialsValidator.RequiresBase64DecodeSecret = true + b.API.Endpoints.URLDefault = btcMarketsAPIURL + b.API.Endpoints.URL = b.API.Endpoints.URLDefault + + b.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + } + + b.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + SubmitOrder: true, + UserTradeHistory: true, + CryptoWithdrawal: true, + FiatWithdraw: true, + TradeFee: true, + FiatWithdrawalFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AccountInfo: true, + Subscribe: true, + AuthenticatedEndpoints: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.AutoWithdrawFiat, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + b.Requester = request.New(b.Name, + request.NewRateLimit(time.Second*10, btcmarketsAuthLimit), + request.NewRateLimit(time.Second*10, btcmarketsUnauthLimit), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + b.API.Endpoints.WebsocketURL = btcMarketsWSURL + b.Websocket = wshandler.New() + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup takes in an exchange configuration and sets all parameters +func (b *BTCMarkets) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + b.SetEnabled(false) + return nil + } + + err := b.SetupDefaults(exch) + if err != nil { + return err + } + + err = b.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: btcMarketsWSURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: b.WsConnect, + Subscriber: b.Subscribe, + Features: &b.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + return nil +} + // Start starts the BTC Markets go routine func (b *BTCMarkets) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -29,77 +172,112 @@ func (b *BTCMarkets) Start(wg *sync.WaitGroup) { // Run implements the BTC Markets wrapper func (b *BTCMarkets) Run() { if b.Verbose { - log.Debugf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) + log.Debugf(log.ExchangeSys, + "%s Websocket: %s (url: %s).\n", + b.Name, + common.IsEnabled(b.Websocket.IsEnabled()), + btcMarketsWSURL) + b.PrintEnabledPairs() + } + forceUpdate := false + if !common.StringDataContains(b.GetEnabledPairs(asset.Spot).Strings(), "-") || + !common.StringDataContains(b.GetAvailablePairs(asset.Spot).Strings(), "-") { + log.Warnln(log.ExchangeSys, "Available pairs for BTC Markets reset due to config upgrade, please enable the pairs you would like again.") + forceUpdate = true + } + if forceUpdate { + enabledPairs := currency.Pairs{currency.Pair{ + Base: currency.BTC.Lower(), + Quote: currency.AUD.Lower(), + Delimiter: "-", + }, + } + err := b.UpdatePairs(enabledPairs, asset.Spot, true, true) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s Failed to update enabled currencies.\n", + b.Name) + } } + if !b.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { + return + } + + err := b.UpdateTradablePairs(forceUpdate) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + b.Name, + err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (b *BTCMarkets) FetchTradablePairs(a asset.Item) ([]string, error) { + if a != asset.Spot { + return nil, fmt.Errorf("asset type of %s is not supported by %s", a, b.Name) + } markets, err := b.GetMarkets() if err != nil { - log.Errorf("%s failed to get active market. Err: %s", b.Name, err) - } else { - forceUpgrade := false - if !common.StringDataContains(b.EnabledPairs.Strings(), "-") || - !common.StringDataContains(b.AvailablePairs.Strings(), "-") { - forceUpgrade = true - } - - var currencies currency.Pairs - for x := range markets { - currencies = append(currencies, - currency.NewPairWithDelimiter(markets[x].Instrument, - markets[x].Currency, "-")) - } - - if forceUpgrade { - enabledPairs := currency.Pairs{currency.Pair{Base: currency.BTC, - Quote: currency.AUD, Delimiter: "-"}} - - log.Warn("Available pairs for BTC Makrets reset due to config upgrade, please enable the pairs you would like again.") - - err = b.UpdateCurrencies(enabledPairs, true, true) - if err != nil { - log.Errorf("%s failed to update currencies. Err: %s", b.Name, err) - } - } - err = b.UpdateCurrencies(currencies, false, forceUpgrade) - if err != nil { - log.Errorf("%s failed to update currencies. Err: %s", b.Name, err) - } + return nil, err } + + var pairs []string + for x := range markets { + pairs = append(pairs, markets[x].MarketID) + } + return pairs, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (b *BTCMarkets) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := b.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) } // UpdateTicker updates and returns the ticker for a currency pair -func (b *BTCMarkets) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { - var tickerPrice ticker.Price - tick, err := b.GetTicker(p.Base.String(), p.Quote.String()) - if err != nil { - return tickerPrice, err +func (b *BTCMarkets) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + var resp ticker.Price + allPairs := b.GetEnabledPairs(assetType) + for x := range allPairs { + tick, err := b.GetTicker(b.FormatExchangeCurrency(allPairs[x], assetType).String()) + if err != nil { + return resp, err + } + resp.Pair = allPairs[x] + resp.Last = tick.LastPrice + resp.High = tick.High24h + resp.Low = tick.Low24h + resp.Bid = tick.BestBID + resp.Ask = tick.BestAsk + resp.Volume = tick.Volume + resp.LastUpdated = time.Now() + err = ticker.ProcessTicker(b.Name, &resp, assetType) + if err != nil { + return resp, err + } } - tickerPrice.Pair = p - tickerPrice.Ask = tick.BestAsk - tickerPrice.Bid = tick.BestBID - tickerPrice.Last = tick.LastPrice - - err = ticker.ProcessTicker(b.GetName(), &tickerPrice, assetType) - if err != nil { - return tickerPrice, err - } - return ticker.GetTicker(b.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (b *BTCMarkets) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (b *BTCMarkets) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(b.Name, p, assetType) if err != nil { return b.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (b *BTCMarkets) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(b.GetName(), p, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (b *BTCMarkets) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(b.Name, p, assetType) if err != nil { return b.UpdateOrderbook(p, assetType) } @@ -107,229 +285,245 @@ func (b *BTCMarkets) GetOrderbookEx(p currency.Pair, assetType string) (orderboo } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (b *BTCMarkets) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (b *BTCMarkets) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := b.GetOrderbook(p.Base.String(), - p.Quote.String()) + tempResp, err := b.GetOrderbook(b.FormatExchangeCurrency(p, assetType).String(), 2) if err != nil { return orderBook, err } - - for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data[1], Price: data[0]}) + for x := range tempResp.Bids { + orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + Amount: tempResp.Bids[x].Volume, + Price: tempResp.Bids[x].Price}) } - - for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data[1], Price: data[0]}) + for y := range tempResp.Asks { + orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + Amount: tempResp.Asks[y].Volume, + Price: tempResp.Asks[y].Price}) } - orderBook.Pair = p - orderBook.ExchangeName = b.GetName() + orderBook.ExchangeName = b.Name orderBook.AssetType = assetType - err = orderBook.Process() if err != nil { return orderBook, err } - return orderbook.Get(b.Name, p, assetType) } -// GetAccountInfo retrieves balances for all enabled currencies for the -// BTCMarkets exchange +// GetAccountInfo retrieves balances for all enabled currencies func (b *BTCMarkets) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - response.Exchange = b.GetName() - - accountBalance, err := b.GetAccountBalance() + var resp exchange.AccountInfo + data, err := b.GetAccountBalance() if err != nil { - return response, err + return resp, err } - - var currencies []exchange.AccountCurrencyInfo - for i := 0; i < len(accountBalance); i++ { - var exchangeCurrency exchange.AccountCurrencyInfo - exchangeCurrency.CurrencyName = currency.NewCode(accountBalance[i].Currency) - exchangeCurrency.TotalValue = accountBalance[i].Balance - exchangeCurrency.Hold = accountBalance[i].PendingFunds - - currencies = append(currencies, exchangeCurrency) + var account exchange.Account + for key := range data { + c := currency.NewCode(data[key].AssetName) + hold := data[key].Locked + total := data[key].Balance + account.Currencies = append(account.Currencies, + exchange.AccountCurrencyInfo{CurrencyName: c, + TotalValue: total, + Hold: hold}) } - - response.Accounts = append(response.Accounts, exchange.Account{ - Currencies: currencies, - }) - - return response, nil + resp.Accounts = append(resp.Accounts, account) + resp.Exchange = b.Name + return resp, nil } // GetFundingHistory returns funding history, deposits and // withdrawals func (b *BTCMarkets) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (b *BTCMarkets) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (b *BTCMarkets) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (b *BTCMarkets) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - response, err := b.NewOrder(p.Base.Upper().String(), - p.Quote.Upper().String(), - price, - amount, - side.ToString(), - orderType.ToString(), - clientID) - - if response > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response) +func (b *BTCMarkets) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var resp order.SubmitResponse + if err := s.Validate(); err != nil { + return resp, err } - if err == nil { - submitOrderResponse.IsOrderPlaced = true + if s.OrderSide == order.Sell { + s.OrderSide = order.Ask + } + if s.OrderSide == order.Buy { + s.OrderSide = order.Bid } - return submitOrderResponse, err + tempResp, err := b.NewOrder(b.FormatExchangeCurrency(s.Pair, asset.Spot).String(), + s.Price, + s.Amount, + s.OrderSide.String(), + s.OrderType.String(), + s.TriggerPrice, + s.TargetAmount, + "", + false, + "", + s.ClientID) + if err != nil { + return resp, err + } + resp.IsOrderPlaced = true + resp.OrderID = tempResp.OrderID + return resp, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (b *BTCMarkets) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (b *BTCMarkets) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (b *BTCMarkets) CancelOrder(order *exchange.OrderCancellation) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) - if err != nil { - return err - } - - _, err = b.CancelExistingOrder([]int64{orderIDInt}) +func (b *BTCMarkets) CancelOrder(o *order.Cancel) error { + _, err := b.RemoveOrder(o.OrderID) return err } // CancelAllOrders cancels all orders associated with a currency pair -func (b *BTCMarkets) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), - } - openOrders, err := b.GetOpenOrders() +func (b *BTCMarkets) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + var resp order.CancelAllResponse + tempMap := make(map[string]string) + var orderIDs []string + orders, err := b.GetOrders("", -1, -1, -1, "open") if err != nil { - return cancelAllOrdersResponse, err + return resp, err } - - var orderList []int64 - for i := range openOrders { - orderList = append(orderList, openOrders[i].ID) + for x := range orders { + orderIDs = append(orderIDs, orders[x].OrderID) } - - if len(orderList) > 0 { - orders, err := b.CancelExistingOrder(orderList) - if err != nil { - return cancelAllOrdersResponse, err - } - - for i := range orders { - if !orders[i].Success { - cancelAllOrdersResponse.OrderStatus[strconv.FormatInt(orders[i].ID, 10)] = orders[i].ErrorMessage - } - } + tempResp, err := b.CancelBatchOrders(orderIDs) + if err != nil { + return resp, err } - return cancelAllOrdersResponse, nil + for y := range tempResp.CancelOrders { + tempMap[tempResp.CancelOrders[y].OrderID] = "Success" + } + for z := range tempResp.UnprocessedRequests { + tempMap[tempResp.UnprocessedRequests[z].RequestID] = "Cancellation Failed" + } + return resp, nil } // GetOrderInfo returns information on a current open order -func (b *BTCMarkets) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var OrderDetail exchange.OrderDetail - - o, err := strconv.ParseInt(orderID, 10, 64) +func (b *BTCMarkets) GetOrderInfo(orderID string) (order.Detail, error) { + var resp order.Detail + o, err := b.FetchOrder(orderID) if err != nil { - return OrderDetail, err + return resp, err } - - orders, err := b.GetOrderDetail([]int64{o}) - if err != nil { - return OrderDetail, err + resp.Exchange = b.Name + resp.ID = orderID + resp.CurrencyPair = currency.NewPairFromString(o.MarketID) + resp.Price = o.Price + resp.OrderDate = o.CreationTime + resp.ExecutedAmount = o.Amount - o.OpenAmount + resp.OrderSide = order.Bid + if o.Side == ask { + resp.OrderSide = order.Ask } - - if len(orders) > 1 { - return OrderDetail, errors.New("too many orders returned") + switch o.Type { + case limit: + resp.OrderType = order.Limit + case market: + resp.OrderType = order.Market + case stopLimit: + resp.OrderType = order.Stop + case stop: + resp.OrderType = order.Stop + case takeProfit: + resp.OrderType = order.ImmediateOrCancel + default: + resp.OrderType = order.Unknown } - - if len(orders) == 0 { - return OrderDetail, errors.New("no orders found") + resp.RemainingAmount = o.OpenAmount + switch o.Status { + case orderAccepted: + resp.Status = order.Active + case orderPlaced: + resp.Status = order.Active + case orderPartiallyMatched: + resp.Status = order.PartiallyFilled + case orderFullyMatched: + resp.Status = order.Filled + case orderCancelled: + resp.Status = order.Cancelled + case orderPartiallyCancelled: + resp.Status = order.PartiallyCancelled + case orderFailed: + resp.Status = order.Rejected + default: + resp.Status = order.UnknownStatus } - - for i := range orders { - var side exchange.OrderSide - if strings.EqualFold(orders[i].OrderSide, exchange.AskOrderSide.ToString()) { - side = exchange.SellOrderSide - } else if strings.EqualFold(orders[i].OrderSide, exchange.BidOrderSide.ToString()) { - side = exchange.BuyOrderSide - } - orderDate := time.Unix(int64(orders[i].CreationTime), 0) - orderType := exchange.OrderType(strings.ToUpper(orders[i].OrderType)) - - OrderDetail.Amount = orders[i].Volume - OrderDetail.OrderDate = orderDate - OrderDetail.Exchange = b.GetName() - OrderDetail.ID = strconv.FormatInt(orders[i].ID, 10) - OrderDetail.RemainingAmount = orders[i].OpenVolume - OrderDetail.OrderSide = side - OrderDetail.OrderType = orderType - OrderDetail.Price = orders[i].Price - OrderDetail.Status = orders[i].Status - OrderDetail.CurrencyPair = currency.NewPairWithDelimiter(orders[i].Instrument, - orders[i].Currency, - b.ConfigCurrencyPairFormat.Delimiter) - } - - return OrderDetail, nil + return resp, nil } // GetDepositAddress returns a deposit address for a specified currency func (b *BTCMarkets) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) { - return "", common.ErrFunctionNotSupported + temp, err := b.FetchDepositAddress(strings.ToUpper(cryptocurrency.String()), -1, -1, -1) + if err != nil { + return "", err + } + return temp.Address, nil } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is submitted -func (b *BTCMarkets) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { - return b.WithdrawCrypto(withdrawRequest.Amount, withdrawRequest.Currency.String(), withdrawRequest.Address) +func (b *BTCMarkets) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { + a, err := b.RequestWithdraw(withdrawRequest.Currency.String(), + withdrawRequest.Amount, + withdrawRequest.Address, + "", + "", + "", + "") + if err != nil { + return "", err + } + return a.Status, nil } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (b *BTCMarkets) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *BTCMarkets) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { if withdrawRequest.Currency != currency.AUD { - return "", errors.New("only AUD is supported for withdrawals") + return "", errors.New("only aud is supported for withdrawals") } - return b.WithdrawAUD(withdrawRequest.BankAccountName, fmt.Sprintf("%v", withdrawRequest.BankAccountNumber), withdrawRequest.BankName, fmt.Sprintf("%v", withdrawRequest.BankCode), withdrawRequest.Amount) + a, err := b.RequestWithdraw(withdrawRequest.GenericWithdrawRequestInfo.Currency.String(), + withdrawRequest.GenericWithdrawRequestInfo.Amount, + "", + withdrawRequest.BankAccountName, + withdrawRequest.BankAccountNumber, + withdrawRequest.BSB, + withdrawRequest.BankName) + if err != nil { + return "", err + } + return a.Status, nil } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (b *BTCMarkets) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *BTCMarkets) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // GetWebsocket returns a pointer to the exchange websocket func (b *BTCMarkets) GetWebsocket() (*wshandler.Websocket, error) { - return nil, common.ErrNotYetImplemented + return b.Websocket, nil } // GetFeeByType returns an estimate of fee based on type of transaction func (b *BTCMarkets) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (b.APIKey == "" || b.APISecret == "") && // Todo check connection status + if !b.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -337,129 +531,117 @@ func (b *BTCMarkets) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, err } // GetActiveOrders retrieves any orders that are active/open -func (b *BTCMarkets) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - resp, err := b.GetOpenOrders() - if err != nil { - return nil, err +func (b *BTCMarkets) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { + var resp []order.Detail + var tempResp order.Detail + var tempData []OrderData + if len(req.Currencies) == 0 { + allPairs := b.GetEnabledPairs(asset.Spot) + for a := range allPairs { + req.Currencies = append(req.Currencies, + allPairs[a]) + } } - - var orders []exchange.OrderDetail - for i := range resp { - var side exchange.OrderSide - if strings.EqualFold(resp[i].OrderSide, exchange.AskOrderSide.ToString()) { - side = exchange.SellOrderSide - } else if strings.EqualFold(resp[i].OrderSide, exchange.BidOrderSide.ToString()) { - side = exchange.BuyOrderSide + var err error + for x := range req.Currencies { + tempData, err = b.GetOrders(b.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String(), -1, -1, -1, "") + if err != nil { + return resp, err } - orderDate := time.Unix(int64(resp[i].CreationTime), 0) - orderType := exchange.OrderType(strings.ToUpper(resp[i].OrderType)) - - openOrder := exchange.OrderDetail{ - ID: strconv.FormatInt(resp[i].ID, 10), - Amount: resp[i].Volume, - Exchange: b.Name, - RemainingAmount: resp[i].OpenVolume, - OrderDate: orderDate, - OrderSide: side, - OrderType: orderType, - Price: resp[i].Price, - Status: resp[i].Status, - CurrencyPair: currency.NewPairWithDelimiter(resp[i].Instrument, - resp[i].Currency, - b.ConfigCurrencyPairFormat.Delimiter), + for y := range tempData { + tempResp.Exchange = b.Name + tempResp.CurrencyPair = req.Currencies[x] + tempResp.OrderSide = order.Bid + if tempData[y].Side == ask { + tempResp.OrderSide = order.Ask + } + tempResp.OrderDate = tempData[y].CreationTime + switch tempData[y].Status { + case orderAccepted: + tempResp.Status = order.Active + case orderPlaced: + tempResp.Status = order.Active + case orderPartiallyMatched: + tempResp.Status = order.PartiallyFilled + case orderFullyMatched: + tempResp.Status = order.Filled + case orderCancelled: + tempResp.Status = order.Cancelled + case orderPartiallyCancelled: + tempResp.Status = order.PartiallyCancelled + case orderFailed: + tempResp.Status = order.Rejected + } + tempResp.Price = tempData[y].Price + tempResp.Amount = tempData[y].Amount + tempResp.ExecutedAmount = tempData[y].Amount - tempData[y].OpenAmount + tempResp.RemainingAmount = tempData[y].OpenAmount + resp = append(resp, tempResp) } - - for j := range resp[i].Trades { - tradeDate := time.Unix(int64(resp[i].Trades[j].CreationTime), 0) - openOrder.Trades = append(openOrder.Trades, exchange.TradeHistory{ - Amount: resp[i].Trades[j].Volume, - Exchange: b.Name, - Price: resp[i].Trades[j].Price, - TID: resp[i].Trades[j].ID, - Timestamp: tradeDate, - Fee: resp[i].Trades[j].Fee, - Description: resp[i].Trades[j].Description, - }) - } - - orders = append(orders, openOrder) } - - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - - return orders, nil + order.FilterOrdersByType(&resp, req.OrderType) + order.FilterOrdersByTickRange(&resp, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&resp, req.OrderSide) + return resp, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (b *BTCMarkets) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - if len(getOrdersRequest.Currencies) == 0 { - return nil, errors.New("requires at least one currency pair to retrieve history") - } - - var respOrders []Order - for _, currency := range getOrdersRequest.Currencies { - resp, err := b.GetOrders(currency.Base.String(), - currency.Quote.String(), - 200, - 0, - true) +func (b *BTCMarkets) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + var resp []order.Detail + var tempResp order.Detail + var tempArray []string + if len(req.Currencies) == 0 { + orders, err := b.GetOrders("", -1, -1, -1, "") if err != nil { - return nil, err + return resp, err + } + for x := range orders { + tempArray = append(tempArray, orders[x].OrderID) } - respOrders = append(respOrders, resp...) } - - var orders []exchange.OrderDetail - for i := range respOrders { - var side exchange.OrderSide - if strings.EqualFold(respOrders[i].OrderSide, exchange.AskOrderSide.ToString()) { - side = exchange.SellOrderSide - } else if strings.EqualFold(respOrders[i].OrderSide, exchange.BidOrderSide.ToString()) { - side = exchange.BuyOrderSide + for y := range req.Currencies { + orders, err := b.GetOrders(b.FormatExchangeCurrency(req.Currencies[y], asset.Spot).String(), -1, -1, -1, "") + if err != nil { + return resp, err } - orderDate := time.Unix(int64(respOrders[i].CreationTime), 0) - orderType := exchange.OrderType(strings.ToUpper(respOrders[i].OrderType)) - - openOrder := exchange.OrderDetail{ - ID: strconv.FormatInt(respOrders[i].ID, 10), - Amount: respOrders[i].Volume, - Exchange: b.Name, - RemainingAmount: respOrders[i].OpenVolume, - OrderDate: orderDate, - OrderSide: side, - OrderType: orderType, - Price: respOrders[i].Price, - Status: respOrders[i].Status, - CurrencyPair: currency.NewPairWithDelimiter(respOrders[i].Instrument, - respOrders[i].Currency, - b.ConfigCurrencyPairFormat.Delimiter), + for z := range orders { + tempArray = append(tempArray, orders[z].OrderID) } - - for j := range respOrders[i].Trades { - tradeDate := time.Unix(int64(respOrders[i].Trades[j].CreationTime), 0) - openOrder.Trades = append(openOrder.Trades, exchange.TradeHistory{ - Amount: respOrders[i].Trades[j].Volume, - Exchange: b.Name, - Price: respOrders[i].Trades[j].Price, - TID: respOrders[i].Trades[j].ID, - Timestamp: tradeDate, - Fee: respOrders[i].Trades[j].Fee, - Description: respOrders[i].Trades[j].Description, - }) - } - - orders = append(orders, openOrder) } - - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - - return orders, nil + tempData, err := b.GetBatchTrades(tempArray) + if err != nil { + return resp, err + } + for c := range tempData.Orders { + switch tempData.Orders[c].Status { + case orderFailed: + tempResp.Status = order.Rejected + case orderPartiallyCancelled: + tempResp.Status = order.PartiallyCancelled + case orderCancelled: + tempResp.Status = order.Cancelled + case orderFullyMatched: + tempResp.Status = order.Filled + case orderPartiallyMatched: + continue + case orderPlaced: + continue + case orderAccepted: + continue + } + tempResp.Exchange = b.Name + tempResp.CurrencyPair = currency.NewPairFromString(tempData.Orders[c].MarketID) + tempResp.OrderSide = order.Bid + if tempData.Orders[c].Side == ask { + tempResp.OrderSide = order.Ask + } + tempResp.OrderDate = tempData.Orders[c].CreationTime + tempResp.Price = tempData.Orders[c].Price + tempResp.ExecutedAmount = tempData.Orders[c].Amount + resp = append(resp, tempResp) + } + return resp, nil } // SubscribeToWebsocketChannels appends to ChannelsToSubscribe diff --git a/exchanges/btse/README.md b/exchanges/btse/README.md index 5c5688e8..cdb5fd7e 100644 --- a/exchanges/btse/README.md +++ b/exchanges/btse/README.md @@ -1,30 +1,133 @@ - -# GoCryptoTrader Btse Exchange Wrapper +# GoCryptoTrader package Btse -An exchange interface wrapper for the GoCryptoTrader application. + +[![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/exchanges/btse) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This btse package is part of the GoCryptoTrader codebase. ## This is still in active development - You can track ideas, planned features and what's in progresss on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). +You can track ideas, planned features and what's in progresss on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). -## Current Btse Exchange Features +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) -+ REST Support -+ Websocket Support +## BTCMarkets Exchange -+ Can be used as a package +### Current Features -## Notes ++ REST Support ++ Websocket Support -+ Please add notes here with any production issues -+ Please provide link to exchange website and API documentation +### How to enable -## Contributors ++ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example) -+ Please add your information ++ Individual package example below: -|User|Github|Contribution| -|--|--|--| -|AliasGoesHere|https://github.com/AliasGoesHere |WHAT-YOU-DID| +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### How to do REST public/private calls + ++ If enabled via "configuration".json file the exchange will be added to the +IBotExchange array in the ```go var bot Bot``` and you will only be able to use +the wrapper interface functions for accessing exchange data. View routines.go +for an example of integration usage with GoCryptoTrader. Rudimentary example +below: + +main.go +```go +var b exchange.IBotExchange + +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "BTSE" { + b = bot.Exchanges[i] + } +} + +// Public calls - wrapper functions + +// Fetches current ticker information +tick, err := b.FetchTicker() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := b.FetchOrderbook() +if err != nil { + // Handle error +} + +// Private calls - wrapper functions - make sure your APIKEY and APISECRET are +// set and AuthenticatedAPISupport is set to true + +// Fetches current account information +accountInfo, err := b.GetAccountInfo() +if err != nil { + // Handle error +} +``` + ++ If enabled via individually importing package, rudimentary example below: + +```go +// Public calls + +// Fetches current ticker information +ticker, err := b.GetTicker() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := b.GetOrderBook() +if err != nil { + // Handle error +} + +// Private calls - make sure your APIKEY and APISECRET are set and +// AuthenticatedAPISupport is set to true + +// GetUserInfo returns account info +accountInfo, err := b.GetUserInfo(...) +if err != nil { + // Handle error +} + +// Submits an order and the exchange and returns its tradeID +tradeID, err := b.Trade(...) +if err != nil { + // Handle error +} +``` + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** diff --git a/exchanges/btse/btse.go b/exchanges/btse/btse.go index adc862ed..99faa3b7 100644 --- a/exchanges/btse/btse.go +++ b/exchanges/btse/btse.go @@ -2,6 +2,7 @@ package btse import ( "bytes" + "encoding/json" "errors" "fmt" "io" @@ -9,12 +10,9 @@ import ( "strconv" "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -44,104 +42,9 @@ const ( btsePendingOrders = "pending" btseDeleteOrder = "deleteOrder" btseFills = "fills" + btseTimeLayout = "2006-01-02 15:04:04" ) -// SetDefaults sets the basic defaults for BTSE -func (b *BTSE) SetDefaults() { - b.Name = "BTSE" - b.Enabled = false - b.Verbose = false - b.RESTPollingDelay = 10 - b.APIWithdrawPermissions = exchange.NoAPIWithdrawalMethods - b.RequestCurrencyPairFormat.Delimiter = "-" - b.RequestCurrencyPairFormat.Uppercase = true - b.ConfigCurrencyPairFormat.Delimiter = "-" - b.ConfigCurrencyPairFormat.Uppercase = true - b.AssetTypes = []string{ticker.Spot} - b.Requester = request.New(b.Name, - request.NewRateLimit(time.Second, 0), - request.NewRateLimit(time.Second, 0), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - b.APIUrlDefault = btseAPIURL - b.APIUrl = b.APIUrlDefault - b.SupportsAutoPairUpdating = true - b.SupportsRESTTickerBatching = false - b.Websocket = wshandler.New() - b.Websocket.Functionality = wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketTickerSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported - b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup takes in the supplied exchange configuration details and sets params -func (b *BTSE) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - b.SetEnabled(false) - } else { - b.Enabled = true - b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - b.SetHTTPClientTimeout(exch.HTTPTimeout) - b.SetHTTPClientUserAgent(exch.HTTPUserAgent) - b.RESTPollingDelay = exch.RESTPollingDelay - b.Verbose = exch.Verbose - b.Websocket.SetWsStatusAndConnection(exch.Websocket) - b.BaseCurrencies = exch.BaseCurrencies - b.AvailablePairs = exch.AvailablePairs - b.EnabledPairs = exch.EnabledPairs - err := b.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = b.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = b.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = b.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = b.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = b.Websocket.Setup(b.WsConnect, - b.Subscribe, - b.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - btseWebsocket, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - b.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: b.Name, - URL: b.Websocket.GetWebsocketURL(), - ProxyURL: b.Websocket.GetProxyAddress(), - Verbose: b.Verbose, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - b.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - false, - false, - false, - false, - exch.Name) - } -} - // GetMarketsSummary stores market summary data func (b *BTSE) GetMarketsSummary() (*HighLevelMarketData, error) { var m HighLevelMarketData @@ -213,7 +116,7 @@ func (b *BTSE) CreateOrder(amount, price float64, side, orderType, symbol, timeI req["symbol"] = symbol } if timeInForce != "" { - req["timeInForce"] = timeInForce + req["time_in_force"] = timeInForce } if tag != "" { req["tag"] = tag @@ -285,7 +188,7 @@ func (b *BTSE) GetFills(orderID, symbol, before, after, limit, username string) // SendHTTPRequest sends an HTTP request to the desired endpoint func (b *BTSE) SendHTTPRequest(method, endpoint string, result interface{}) error { return b.SendPayload(method, - btseAPIURL+btseAPIPath+endpoint, + b.API.Endpoints.URL+btseAPIPath+endpoint, nil, nil, &result, @@ -298,12 +201,13 @@ func (b *BTSE) SendHTTPRequest(method, endpoint string, result interface{}) erro // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to the desired endpoint func (b *BTSE) SendAuthenticatedHTTPRequest(method, endpoint string, req map[string]interface{}, result interface{}) error { - if !b.AuthenticatedAPISupport { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) + if !b.AllowAuthenticatedRequest() { + return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, + b.Name) } path := btseAPIPath + endpoint headers := make(map[string]string) - headers["btse-api"] = b.APIKey + headers["btse-api"] = b.API.Credentials.Key nonce := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) headers["btse-nonce"] = nonce var body io.Reader @@ -311,29 +215,31 @@ func (b *BTSE) SendAuthenticatedHTTPRequest(method, endpoint string, req map[str var payload []byte if len(req) != 0 { var err error - payload, err = common.JSONEncode(req) + payload, err = json.Marshal(req) if err != nil { return err } body = bytes.NewBuffer(payload) - hmac = common.GetHMAC( - common.HashSHA512_384, + hmac = crypto.GetHMAC( + crypto.HashSHA512_384, []byte((path + nonce + string(payload))), - []byte(b.APISecret), + []byte(b.API.Credentials.Secret), ) } else { - hmac = common.GetHMAC( - common.HashSHA512_384, + hmac = crypto.GetHMAC( + crypto.HashSHA512_384, []byte((path + nonce)), - []byte(b.APISecret), + []byte(b.API.Credentials.Secret), ) } - headers["btse-sign"] = common.HexEncodeToString(hmac) + headers["btse-sign"] = crypto.HexEncodeToString(hmac) if b.Verbose { - log.Debugf("Sending %s request to URL %s with params %s\n", method, path, string(payload)) + log.Debugf(log.ExchangeSys, + "%s Sending %s request to URL %s with params %s\n", + b.Name, method, path, string(payload)) } return b.SendPayload(method, - btseAPIURL+path, + b.API.Endpoints.URL+path, headers, body, &result, @@ -415,7 +321,6 @@ func calculateTradingFee(isMaker bool) float64 { return fee } -func parseOrderTime(timeStr string) time.Time { - t, _ := time.Parse("2006-01-02 15:04:04", timeStr) - return t +func parseOrderTime(timeStr string) (time.Time, error) { + return time.Parse(btseTimeLayout, timeStr) } diff --git a/exchanges/btse/btse_test.go b/exchanges/btse/btse_test.go index 8e243faa..d0cd92fa 100644 --- a/exchanges/btse/btse_test.go +++ b/exchanges/btse/btse_test.go @@ -1,13 +1,15 @@ package btse import ( + "log" "os" + "strings" "testing" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // Please supply your own keys here to do better tests @@ -15,6 +17,7 @@ const ( apiKey = "" apiSecret = "" canManipulateRealOrders = false + testPair = "BTC-USD" ) var b BTSE @@ -22,26 +25,29 @@ var b BTSE func TestMain(m *testing.M) { b.SetDefaults() cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal(err) + } btseConfig, err := cfg.GetExchangeConfig("BTSE") if err != nil { - log.Fatal("BTSE Setup() init error", err) + log.Fatal(err) } - btseConfig.AuthenticatedAPISupport = true - btseConfig.APIKey = apiKey - btseConfig.APISecret = apiSecret + btseConfig.API.AuthenticatedSupport = true + btseConfig.API.Credentials.Key = apiKey + btseConfig.API.Credentials.Secret = apiSecret + + err = b.Setup(btseConfig) + if err != nil { + log.Fatal(err) + } - b.Setup(&btseConfig) os.Exit(m.Run()) } func areTestAPIKeysSet() bool { - if b.APIKey != "" && b.APIKey != "Key" && - b.APISecret != "" && b.APISecret != "Secret" { - return true - } - return false + return b.ValidateAPICredentials() } func TestGetMarketsSummary(t *testing.T) { @@ -62,7 +68,7 @@ func TestGetMarkets(t *testing.T) { func TestFetchOrderBook(t *testing.T) { t.Parallel() - _, err := b.FetchOrderBook("BTC-USD") + _, err := b.FetchOrderBook(testPair) if err != nil { t.Error(err) } @@ -70,7 +76,7 @@ func TestFetchOrderBook(t *testing.T) { func TestGetTrades(t *testing.T) { t.Parallel() - _, err := b.GetTrades("BTC-USD") + _, err := b.GetTrades(testPair) if err != nil { t.Error(err) } @@ -78,7 +84,7 @@ func TestGetTrades(t *testing.T) { func TestGetTicker(t *testing.T) { t.Parallel() - _, err := b.GetTicker("BTC-USD") + _, err := b.GetTicker(testPair) if err != nil { t.Error(err) } @@ -86,7 +92,7 @@ func TestGetTicker(t *testing.T) { func TestGetMarketStatistics(t *testing.T) { t.Parallel() - _, err := b.GetMarketStatistics("BTC-USD") + _, err := b.GetMarketStatistics(testPair) if err != nil { t.Error(err) } @@ -116,7 +122,7 @@ func TestGetFills(t *testing.T) { if !areTestAPIKeysSet() { t.Skip("API keys not set, skipping test") } - _, err := b.GetFills("", "BTC-USD", "", "", "", "") + _, err := b.GetFills("", testPair, "", "", "", "") if err != nil { t.Error(err) } @@ -127,7 +133,13 @@ func TestCreateOrder(t *testing.T) { if !areTestAPIKeysSet() || !canManipulateRealOrders { t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") } - _, err := b.CreateOrder(4.5, 3.4, "buy", "limit", "BTC-USD", "", "") + _, err := b.CreateOrder(0.1, + 10000, + order.Sell.String(), + order.Limit.String(), + testPair, + "", + "") if err != nil { t.Error(err) } @@ -149,8 +161,8 @@ func TestGetActiveOrders(t *testing.T) { if !areTestAPIKeysSet() { t.Skip("API keys not set, skipping test") } - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetActiveOrders(&getOrdersRequest) @@ -164,8 +176,8 @@ func TestGetOrderHistory(t *testing.T) { if !areTestAPIKeysSet() { t.Skip("API keys not set, skipping test") } - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := b.GetOrderHistory(&getOrdersRequest) if err != nil { @@ -193,7 +205,7 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } b.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -265,9 +277,12 @@ func TestGetFee(t *testing.T) { func TestParseOrderTime(t *testing.T) { expected := int64(1534794360) - actual := parseOrderTime("2018-08-20 19:20:46").Unix() - if expected != actual { - t.Errorf("TestParseOrderTime expected: %d, got %d", expected, actual) + actual, err := parseOrderTime("2018-08-20 19:20:46") + if err != nil { + t.Fatal(err) + } + if expected != actual.Unix() { + t.Errorf("TestParseOrderTime expected: %d, got %d", expected, actual.Unix()) } } @@ -278,12 +293,18 @@ func TestSubmitOrder(t *testing.T) { if !areTestAPIKeysSet() || !canManipulateRealOrders { t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.BTC, - Quote: currency.USD, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 100000, + Amount: 0.1, + ClientID: "meowOrder", } - response, err := b.SubmitOrder(p, exchange.SellOrderSide, exchange.LimitOrderType, 0.01, 1000000, "clientId") + response, err := b.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -299,9 +320,8 @@ func TestCancelExchangeOrder(t *testing.T) { currencyPair := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-") - - var orderCancellation = &exchange.OrderCancellation{ - OrderID: "0b66ccaf-dfd4-4b9f-a30b-2380b9c7b66d", + var orderCancellation = &order.Cancel{ + OrderID: "b334ecef-2b42-4998-b8a4-b6b14f6d2671", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", CurrencyPair: currencyPair, @@ -320,8 +340,7 @@ func TestCancelAllExchangeOrders(t *testing.T) { currencyPair := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-") - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -332,7 +351,9 @@ func TestCancelAllExchangeOrders(t *testing.T) { if err != nil { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + for k, v := range resp.Status { + if strings.Contains(v, "Failed") { + t.Errorf("order id: %s failed to cancel: %v", k, v) + } } } diff --git a/exchanges/btse/btse_types.go b/exchanges/btse/btse_types.go index 0914e6f4..75672552 100644 --- a/exchanges/btse/btse_types.go +++ b/exchanges/btse/btse_types.go @@ -2,6 +2,11 @@ package btse import "time" +const ( + // Default order type is good till cancel (or filled) + goodTillCancel = "gtc" +) + // OverviewData stores market overview data type OverviewData struct { High24Hr float64 `json:"high24hr,string"` @@ -66,12 +71,12 @@ type Ticker struct { // MarketStatistics stores market statistics for a particular product type MarketStatistics struct { - Open float64 `json:"open,string"` - Low float64 `json:"low,string"` - High float64 `json:"high,string"` - Close float64 `json:"close,string"` - Volume float64 `json:"volume,string"` - Time string `json:"time"` + Open float64 `json:"open,string"` + Low float64 `json:"low,string"` + High float64 `json:"high,string"` + Close float64 `json:"close,string"` + Volume float64 `json:"volume,string"` + Time time.Time `json:"time"` } // ServerTime stores the server time data diff --git a/exchanges/btse/btse_websocket.go b/exchanges/btse/btse_websocket.go index 0e8f3528..b5bc3997 100644 --- a/exchanges/btse/btse_websocket.go +++ b/exchanges/btse/btse_websocket.go @@ -1,6 +1,7 @@ package btse import ( + "encoding/json" "errors" "fmt" "net/http" @@ -9,9 +10,9 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" @@ -55,36 +56,40 @@ func (b *BTSE) WsHandleData() { default: resp, err := b.WebsocketConn.ReadMessage() if err != nil { - b.Websocket.DataHandler <- err + b.Websocket.ReadMessageErrors <- err return } b.Websocket.TrafficAlert <- struct{}{} type Result map[string]interface{} result := Result{} - err = common.JSONDecode(resp.Raw, &result) + err = json.Unmarshal(resp.Raw, &result) if err != nil { b.Websocket.DataHandler <- err continue } switch { case strings.Contains(result["topic"].(string), "tradeHistory"): - log.Warnf("%s: Buy/Sell side functionality is broken for this exchange currently! 'gain' has no correlation with buy side or sell side", b.Name) + log.Warnf(log.ExchangeSys, + "%s: Buy/Sell side functionality is broken for this exchange "+ + "currently! 'gain' has no correlation with buy side or "+ + "sell side", + b.Name) var tradeHistory wsTradeHistory - err = common.JSONDecode(resp.Raw, &tradeHistory) + err = json.Unmarshal(resp.Raw, &tradeHistory) if err != nil { b.Websocket.DataHandler <- err continue } for x := range tradeHistory.Data { - side := exchange.BuyOrderSide.ToString() + side := order.Buy.String() if tradeHistory.Data[x].Gain == -1 { - side = exchange.SellOrderSide.ToString() + side = order.Sell.String() } b.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Unix(tradeHistory.Data[x].TransactionTime, 0), CurrencyPair: currency.NewPairFromString(strings.Replace(tradeHistory.Topic, "tradeHistory", "", 1)), - AssetType: orderbook.Spot, + AssetType: asset.Spot, Exchange: b.Name, Price: tradeHistory.Data[x].Price, Amount: tradeHistory.Data[x].Amount, @@ -93,13 +98,13 @@ func (b *BTSE) WsHandleData() { } case strings.Contains(result["topic"].(string), "orderBookApi"): var t wsOrderBook - err = common.JSONDecode(resp.Raw, &t) + err = json.Unmarshal(resp.Raw, &t) if err != nil { b.Websocket.DataHandler <- err continue } + var newOB orderbook.Base var price, amount float64 - var asks, bids []orderbook.Item for i := range t.Data.SellQuote { p := strings.Replace(t.Data.SellQuote[i].Price, ",", "", -1) price, err = strconv.ParseFloat(p, 64) @@ -113,7 +118,10 @@ func (b *BTSE) WsHandleData() { b.Websocket.DataHandler <- err continue } - asks = append(asks, orderbook.Item{Price: price, Amount: amount}) + newOB.Asks = append(newOB.Asks, orderbook.Item{ + Price: price, + Amount: amount, + }) } for j := range t.Data.BuyQuote { p := strings.Replace(t.Data.BuyQuote[j].Price, ",", "", -1) @@ -128,24 +136,25 @@ func (b *BTSE) WsHandleData() { b.Websocket.DataHandler <- err continue } - bids = append(bids, orderbook.Item{Price: price, Amount: amount}) + newOB.Bids = append(newOB.Bids, orderbook.Item{ + Price: price, + Amount: amount, + }) } - var newOB orderbook.Base - newOB.Asks = asks - newOB.Bids = bids - newOB.AssetType = orderbook.Spot + newOB.AssetType = asset.Spot newOB.Pair = currency.NewPairFromString(t.Topic[strings.Index(t.Topic, ":")+1 : strings.Index(t.Topic, "_")]) newOB.ExchangeName = b.Name - err = b.Websocket.Orderbook.LoadSnapshot(&newOB, true) + err = b.Websocket.Orderbook.LoadSnapshot(&newOB) if err != nil { b.Websocket.DataHandler <- err continue } b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, - Asset: orderbook.Spot, + Asset: asset.Spot, Exchange: b.Name} default: - log.Warnf("%s: unhandled websocket response: %s", b.Name, resp.Raw) + log.Warnf(log.ExchangeSys, + "%s: unhandled websocket response: %s", b.Name, resp.Raw) } } } @@ -154,12 +163,13 @@ func (b *BTSE) WsHandleData() { // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (b *BTSE) GenerateDefaultSubscriptions() { var channels = []string{"orderBookApi:%s_0", "tradeHistory:%s"} + pairs := b.GetEnabledPairs(asset.Spot) var subscriptions []wshandler.WebsocketChannelSubscription for i := range channels { - for j := range b.EnabledPairs { + for j := range pairs { subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: fmt.Sprintf(channels[i], b.EnabledPairs[j]), - Currency: b.EnabledPairs[j], + Channel: fmt.Sprintf(channels[i], pairs[j]), + Currency: pairs[j], }) } } diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index 2e3ff51e..02444533 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -3,19 +3,169 @@ package btse import ( "errors" "fmt" + "strconv" "strings" "sync" "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (b *BTSE) GetDefaultConfig() (*config.ExchangeConfig, error) { + b.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = b.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = b.BaseCurrencies + + err := b.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if b.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = b.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for BTSE +func (b *BTSE) SetDefaults() { + b.Name = "BTSE" + b.Enabled = true + b.Verbose = true + b.API.CredentialsValidator.RequiresKey = true + b.API.CredentialsValidator.RequiresSecret = true + + b.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "-", + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "-", + }, + } + + b.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + TradeFee: true, + FiatDepositFee: true, + FiatWithdrawalFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + // TradeHistory is supported but it is currently broken on BTSE's + // API so it has been left as unsupported + }, + WithdrawPermissions: exchange.NoAPIWithdrawalMethods, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + b.Requester = request.New(b.Name, + request.NewRateLimit(time.Second, 0), + request.NewRateLimit(time.Second, 0), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + b.API.Endpoints.URLDefault = btseAPIURL + b.API.Endpoints.URL = b.API.Endpoints.URLDefault + b.Websocket = wshandler.New() + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup takes in the supplied exchange configuration details and sets params +func (b *BTSE) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + b.SetEnabled(false) + return nil + } + + err := b.SetupDefaults(exch) + if err != nil { + return err + } + + err = b.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: btseWebsocket, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: b.WsConnect, + Subscriber: b.Subscribe, + UnSubscriber: b.Unsubscribe, + Features: &b.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + b.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + false, + false, + false, + false, + exch.Name) + return nil +} + // Start starts the BTSE go routine func (b *BTSE) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -28,42 +178,60 @@ func (b *BTSE) Start(wg *sync.WaitGroup) { // Run implements the BTSE wrapper func (b *BTSE) Run() { if b.Verbose { - log.Debugf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()), b.Websocket.GetWebsocketURL()) - log.Debugf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) + b.PrintEnabledPairs() } - m, err := b.GetMarkets() + if !b.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := b.UpdateTradablePairs(false) if err != nil { - log.Errorf("%s failed to get trading pairs. Err: %s", b.Name, err) - } else { - var currencies []string - for x := range m { - if m[x].Status != "active" { - continue - } - currencies = append(currencies, m[x].Symbol) - } - err = b.UpdateCurrencies(currency.NewPairsFromStrings(currencies), - false, - false) - if err != nil { - log.Errorf("%s Failed to update available currencies. Error: %s\n", - b.Name, err) - } + log.Errorf(log.ExchangeSys, + "%s Failed to update tradable pairs. Error: %s", b.Name, err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (b *BTSE) FetchTradablePairs(asset asset.Item) ([]string, error) { + m, err := b.GetMarkets() + if err != nil { + return nil, err + } + + var currencies []string + for x := range m { + if m[x].Status != "active" { + continue + } + currencies = append(currencies, m[x].Symbol) + } + return currencies, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (b *BTSE) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := b.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (b *BTSE) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (b *BTSE) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - t, err := b.GetTicker(exchange.FormatExchangeCurrency(b.Name, p).String()) + t, err := b.GetTicker(b.FormatExchangeCurrency(p, + assetType).String()) if err != nil { return tickerPrice, err } - s, err := b.GetMarketStatistics(exchange.FormatExchangeCurrency(b.Name, p).String()) + s, err := b.GetMarketStatistics(b.FormatExchangeCurrency(p, + assetType).String()) if err != nil { return tickerPrice, err } @@ -75,26 +243,27 @@ func (b *BTSE) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, er tickerPrice.Last = t.Price tickerPrice.Volume = s.Volume tickerPrice.High = s.High + tickerPrice.LastUpdated = s.Time - err = ticker.ProcessTicker(b.GetName(), &tickerPrice, assetType) + err = ticker.ProcessTicker(b.Name, &tickerPrice, assetType) if err != nil { return tickerPrice, err } return ticker.GetTicker(b.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (b *BTSE) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (b *BTSE) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(b.Name, p, assetType) if err != nil { return b.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (b *BTSE) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(b.GetName(), p, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (b *BTSE) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(b.Name, p, assetType) if err != nil { return b.UpdateOrderbook(p, assetType) } @@ -102,9 +271,9 @@ func (b *BTSE) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (b *BTSE) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (b *BTSE) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var resp orderbook.Base - a, err := b.FetchOrderBook(exchange.FormatExchangeCurrency(b.Name, p).String()) + a, err := b.FetchOrderBook(b.FormatExchangeCurrency(p, assetType).String()) if err != nil { return resp, err } @@ -163,15 +332,24 @@ func (b *BTSE) GetFundingHistory() ([]exchange.FundHistory, error) { } // GetExchangeHistory returns historic trade data since exchange opening. -func (b *BTSE) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { +func (b *BTSE) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (b *BTSE) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (exchange.SubmitOrderResponse, error) { - var resp exchange.SubmitOrderResponse - r, err := b.CreateOrder(amount, price, side.ToString(), - orderType.ToString(), exchange.FormatExchangeCurrency(b.Name, p).String(), "", clientID) +func (b *BTSE) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var resp order.SubmitResponse + if err := s.Validate(); err != nil { + return resp, err + } + + r, err := b.CreateOrder(s.Amount, + s.Price, + s.OrderSide.String(), + s.OrderType.String(), + b.FormatExchangeCurrency(s.Pair, asset.Spot).String(), + goodTillCancel, + s.ClientID) if err != nil { return resp, err } @@ -180,20 +358,23 @@ func (b *BTSE) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType e resp.IsOrderPlaced = true resp.OrderID = *r } - + if s.OrderType == order.Market { + resp.FullyMatched = true + } return resp, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (b *BTSE) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (b *BTSE) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (b *BTSE) CancelOrder(order *exchange.OrderCancellation) error { +func (b *BTSE) CancelOrder(order *order.Cancel) error { r, err := b.CancelExistingOrder(order.OrderID, - exchange.FormatExchangeCurrency(b.Name, order.CurrencyPair).String()) + b.FormatExchangeCurrency(order.CurrencyPair, + asset.Spot).String()) if err != nil { return err } @@ -211,15 +392,20 @@ func (b *BTSE) CancelOrder(order *exchange.OrderCancellation) error { // CancelAllOrders cancels all orders associated with a currency pair // If product ID is sent, all orders of that specified market will be cancelled // If not specified, all orders of all markets will be cancelled -func (b *BTSE) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - var resp exchange.CancelAllOrdersResponse - a, err := b.GetMarkets() +func (b *BTSE) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) { + var resp order.CancelAllResponse + markets, err := b.GetMarkets() if err != nil { return resp, err } - for x := range a { - strPair := exchange.FormatExchangeCurrency(b.Name, orderCancellation.CurrencyPair).String() - checkPair := currency.NewPairWithDelimiter(a[x].BaseCurrency, a[x].QuoteCurrency, b.RequestCurrencyPairFormat.Delimiter).String() + + resp.Status = make(map[string]string) + for x := range markets { + strPair := b.FormatExchangeCurrency(orderCancellation.CurrencyPair, + orderCancellation.AssetType).String() + checkPair := currency.NewPairWithDelimiter(markets[x].BaseCurrency, + markets[x].QuoteCurrency, + b.GetPairFormat(asset.Spot, false).Delimiter).String() if strPair != "" && strPair != checkPair { continue } else { @@ -233,7 +419,7 @@ func (b *BTSE) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (e if err != nil { success = "Order Cancellation Failed" } - resp.OrderStatus[orders[y].Order.ID] = success + resp.Status[orders[y].Order.ID] = success } } } @@ -241,13 +427,13 @@ func (b *BTSE) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (e } // GetOrderInfo returns information on a current open order -func (b *BTSE) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { +func (b *BTSE) GetOrderInfo(orderID string) (order.Detail, error) { o, err := b.GetOrders("") if err != nil { - return exchange.OrderDetail{}, err + return order.Detail{}, err } - var od exchange.OrderDetail + var od order.Detail if len(o) == 0 { return od, errors.New("no orders found") } @@ -257,36 +443,46 @@ func (b *BTSE) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { continue } - var side = exchange.BuyOrderSide - if strings.EqualFold(o[i].Side, exchange.AskOrderSide.ToString()) { - side = exchange.SellOrderSide + var side = order.Buy + if strings.EqualFold(o[i].Side, order.Ask.String()) { + side = order.Sell } od.CurrencyPair = currency.NewPairDelimiter(o[i].Symbol, - b.ConfigCurrencyPairFormat.Delimiter) + b.GetPairFormat(asset.Spot, false).Delimiter) od.Exchange = b.Name od.Amount = o[i].Amount od.ID = o[i].ID - od.OrderDate = parseOrderTime(o[i].CreatedAt) + od.OrderDate, err = parseOrderTime(o[i].CreatedAt) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s GetOrderInfo unable to parse time: %s\n", b.Name, err) + } od.OrderSide = side - od.OrderType = exchange.OrderType(strings.ToUpper(o[i].Type)) + od.OrderType = order.Type(strings.ToUpper(o[i].Type)) od.Price = o[i].Price - od.Status = o[i].Status + od.Status = order.Status(o[i].Status) fills, err := b.GetFills(orderID, "", "", "", "", "") if err != nil { - return od, fmt.Errorf("unable to get order fills for orderID %s", orderID) + return od, + fmt.Errorf("unable to get order fills for orderID %s", + orderID) } for i := range fills { - createdAt, _ := time.Parse(time.RFC3339, fills[i].CreatedAt) - od.Trades = append(od.Trades, exchange.TradeHistory{ + createdAt, err := parseOrderTime(fills[i].CreatedAt) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s GetOrderInfo unable to parse time: %s\n", b.Name, err) + } + od.Trades = append(od.Trades, order.TradeHistory{ Timestamp: createdAt, - TID: fills[i].ID, + TID: strconv.FormatInt(fills[i].ID, 10), Price: fills[i].Price, Amount: fills[i].Amount, Exchange: b.Name, - Type: fills[i].Side, + Side: order.Side(fills[i].Side), Fee: fills[i].Fee, }) } @@ -301,19 +497,19 @@ func (b *BTSE) GetDepositAddress(cryptocurrency currency.Code, accountID string) // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *BTSE) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *BTSE) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is // submitted -func (b *BTSE) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *BTSE) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted -func (b *BTSE) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (b *BTSE) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -323,68 +519,85 @@ func (b *BTSE) GetWebsocket() (*wshandler.Websocket, error) { } // GetActiveOrders retrieves any orders that are active/open -func (b *BTSE) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (b *BTSE) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := b.GetOrders("") if err != nil { return nil, err } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range resp { - var side = exchange.BuyOrderSide - if strings.EqualFold(resp[i].Side, exchange.AskOrderSide.ToString()) { - side = exchange.SellOrderSide + var side = order.Buy + if strings.EqualFold(resp[i].Side, order.Ask.String()) { + side = order.Sell } - openOrder := exchange.OrderDetail{ + tm, err := parseOrderTime(resp[i].CreatedAt) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s GetActiveOrders unable to parse time: %s\n", + b.Name, + err) + } + + openOrder := order.Detail{ CurrencyPair: currency.NewPairDelimiter(resp[i].Symbol, - b.ConfigCurrencyPairFormat.Delimiter), + b.GetPairFormat(asset.Spot, false).Delimiter), Exchange: b.Name, Amount: resp[i].Amount, ID: resp[i].ID, - OrderDate: parseOrderTime(resp[i].CreatedAt), + OrderDate: tm, OrderSide: side, - OrderType: exchange.OrderType(strings.ToUpper(resp[i].Type)), + OrderType: order.Type(strings.ToUpper(resp[i].Type)), Price: resp[i].Price, - Status: resp[i].Status, + Status: order.Status(resp[i].Status), } fills, err := b.GetFills(resp[i].ID, "", "", "", "", "") if err != nil { - log.Errorf("%s: unable to get order fills for orderID %s", b.Name, resp[i].ID) + log.Errorf(log.ExchangeSys, + "%s: Unable to get order fills for orderID %s", + b.Name, + resp[i].ID) continue } for i := range fills { - createdAt, _ := time.Parse(time.RFC3339, fills[i].CreatedAt) - openOrder.Trades = append(openOrder.Trades, exchange.TradeHistory{ + createdAt, err := parseOrderTime(fills[i].CreatedAt) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s GetActiveOrders unable to parse time: %s\n", + b.Name, + err) + } + openOrder.Trades = append(openOrder.Trades, order.TradeHistory{ Timestamp: createdAt, - TID: fills[i].ID, + TID: strconv.FormatInt(fills[i].ID, 10), Price: fills[i].Price, Amount: fills[i].Amount, Exchange: b.Name, - Type: fills[i].Side, + Side: order.Side(fills[i].Side), Fee: fills[i].Fee, }) } orders = append(orders, openOrder) } - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (b *BTSE) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (b *BTSE) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { return nil, common.ErrFunctionNotSupported } // GetFeeByType returns an estimate of fee based on type of transaction func (b *BTSE) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (b.APIKey == "" || b.APISecret == "") && // Todo check connection status + if !b.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } diff --git a/exchanges/coinbasepro/README.md b/exchanges/coinbasepro/README.md index 5e4c426b..b734dd6b 100644 --- a/exchanges/coinbasepro/README.md +++ b/exchanges/coinbasepro/README.md @@ -48,22 +48,22 @@ main.go ```go var c exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "CoinbasePro" { - c = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "CoinbasePro" { + c = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := c.GetTickerPrice() +tick, err := c.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := c.GetOrderbookEx() +ob, err := c.FetchOrderbook() if err != nil { // Handle error } @@ -138,4 +138,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/coinbasepro/coinbasepro.go b/exchanges/coinbasepro/coinbasepro.go index b2ffe8d4..8ebcab1a 100644 --- a/exchanges/coinbasepro/coinbasepro.go +++ b/exchanges/coinbasepro/coinbasepro.go @@ -2,20 +2,19 @@ package coinbasepro import ( "bytes" + "encoding/json" "errors" "fmt" "net/http" "net/url" "strconv" "strings" - "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -63,128 +62,22 @@ type CoinbasePro struct { WebsocketConn *wshandler.WebsocketConnection } -// SetDefaults sets default values for the exchange -func (c *CoinbasePro) SetDefaults() { - c.Name = "CoinbasePro" - c.Enabled = false - c.Verbose = false - c.TakerFee = 0.25 - c.MakerFee = 0 - c.RESTPollingDelay = 10 - c.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithAPIPermission | - exchange.AutoWithdrawFiatWithAPIPermission - c.RequestCurrencyPairFormat.Delimiter = "-" - c.RequestCurrencyPairFormat.Uppercase = true - c.ConfigCurrencyPairFormat.Delimiter = "" - c.ConfigCurrencyPairFormat.Uppercase = true - c.AssetTypes = []string{ticker.Spot} - c.SupportsAutoPairUpdating = true - c.SupportsRESTTickerBatching = false - c.Requester = request.New(c.Name, - request.NewRateLimit(time.Second, coinbaseproAuthRate), - request.NewRateLimit(time.Second, coinbaseproUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - c.APIUrlDefault = coinbaseproAPIURL - c.APIUrl = c.APIUrlDefault - c.Websocket = wshandler.New() - c.Websocket.Functionality = wshandler.WebsocketTickerSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported | - wshandler.WebsocketSequenceNumberSupported - c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - c.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup initialises the exchange parameters with the current configuration -func (c *CoinbasePro) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - c.SetEnabled(false) - } else { - c.Enabled = true - c.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - c.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - c.SetAPIKeys(exch.APIKey, exch.APISecret, exch.ClientID, true) - c.SetHTTPClientTimeout(exch.HTTPTimeout) - c.SetHTTPClientUserAgent(exch.HTTPUserAgent) - c.RESTPollingDelay = exch.RESTPollingDelay - c.Verbose = exch.Verbose - c.HTTPDebugging = exch.HTTPDebugging - c.Websocket.SetWsStatusAndConnection(exch.Websocket) - c.BaseCurrencies = exch.BaseCurrencies - c.AvailablePairs = exch.AvailablePairs - c.EnabledPairs = exch.EnabledPairs - if exch.UseSandbox { - c.APIUrl = coinbaseproSandboxAPIURL - } - err := c.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = c.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = c.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = c.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = c.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = c.Websocket.Setup(c.WsConnect, - c.Subscribe, - c.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - coinbaseproWebsocketURL, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - c.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: c.Name, - URL: c.Websocket.GetWebsocketURL(), - ProxyURL: c.Websocket.GetProxyAddress(), - Verbose: c.Verbose, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - c.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - true, - false, - false, - exch.Name) - } -} - // GetProducts returns supported currency pairs on the exchange with specific // information about the pair func (c *CoinbasePro) GetProducts() ([]Product, error) { var products []Product - return products, c.SendHTTPRequest(c.APIUrl+coinbaseproProducts, &products) + return products, c.SendHTTPRequest(c.API.Endpoints.URL+coinbaseproProducts, &products) } // GetOrderbook returns orderbook by currency pair and level func (c *CoinbasePro) GetOrderbook(symbol string, level int) (interface{}, error) { orderbook := OrderbookResponse{} - path := fmt.Sprintf("%s/%s/%s", c.APIUrl+coinbaseproProducts, symbol, coinbaseproOrderbook) + path := fmt.Sprintf("%s/%s/%s", c.API.Endpoints.URL+coinbaseproProducts, symbol, coinbaseproOrderbook) if level > 0 { levelStr := strconv.Itoa(level) - path = fmt.Sprintf("%s/%s/%s?level=%s", c.APIUrl+coinbaseproProducts, symbol, coinbaseproOrderbook, levelStr) + path = fmt.Sprintf("%s/%s/%s?level=%s", c.API.Endpoints.URL+coinbaseproProducts, symbol, coinbaseproOrderbook, levelStr) } if err := c.SendHTTPRequest(path, &orderbook); err != nil { @@ -254,7 +147,7 @@ func (c *CoinbasePro) GetOrderbook(symbol string, level int) (interface{}, error func (c *CoinbasePro) GetTicker(currencyPair string) (Ticker, error) { tick := Ticker{} path := fmt.Sprintf( - "%s/%s/%s", c.APIUrl+coinbaseproProducts, currencyPair, coinbaseproTicker) + "%s/%s/%s", c.API.Endpoints.URL+coinbaseproProducts, currencyPair, coinbaseproTicker) return tick, c.SendHTTPRequest(path, &tick) } @@ -263,8 +156,7 @@ func (c *CoinbasePro) GetTicker(currencyPair string) (Ticker, error) { func (c *CoinbasePro) GetTrades(currencyPair string) ([]Trade, error) { var trades []Trade path := fmt.Sprintf( - "%s/%s/%s", c.APIUrl+coinbaseproProducts, currencyPair, coinbaseproTrades) - + "%s/%s/%s", c.API.Endpoints.URL+coinbaseproProducts, currencyPair, coinbaseproTrades) return trades, c.SendHTTPRequest(path, &trades) } @@ -288,7 +180,7 @@ func (c *CoinbasePro) GetHistoricRates(currencyPair string, start, end, granular } path := common.EncodeURLValues( - fmt.Sprintf("%s/%s/%s", c.APIUrl+coinbaseproProducts, currencyPair, coinbaseproHistory), + fmt.Sprintf("%s/%s/%s", c.API.Endpoints.URL+coinbaseproProducts, currencyPair, coinbaseproHistory), values) if err := c.SendHTTPRequest(path, &resp); err != nil { @@ -320,7 +212,7 @@ func (c *CoinbasePro) GetHistoricRates(currencyPair string, start, end, granular func (c *CoinbasePro) GetStats(currencyPair string) (Stats, error) { stats := Stats{} path := fmt.Sprintf( - "%s/%s/%s", c.APIUrl+coinbaseproProducts, currencyPair, coinbaseproStats) + "%s/%s/%s", c.API.Endpoints.URL+coinbaseproProducts, currencyPair, coinbaseproStats) return stats, c.SendHTTPRequest(path, &stats) } @@ -330,14 +222,14 @@ func (c *CoinbasePro) GetStats(currencyPair string) (Stats, error) { func (c *CoinbasePro) GetCurrencies() ([]Currency, error) { var currencies []Currency - return currencies, c.SendHTTPRequest(c.APIUrl+coinbaseproCurrencies, ¤cies) + return currencies, c.SendHTTPRequest(c.API.Endpoints.URL+coinbaseproCurrencies, ¤cies) } // GetServerTime returns the API server time func (c *CoinbasePro) GetServerTime() (ServerTime, error) { serverTime := ServerTime{} - return serverTime, c.SendHTTPRequest(c.APIUrl+coinbaseproTime, &serverTime) + return serverTime, c.SendHTTPRequest(c.API.Endpoints.URL+coinbaseproTime, &serverTime) } // GetAccounts returns a list of trading accounts associated with the APIKEYS @@ -398,7 +290,7 @@ func (c *CoinbasePro) GetHolds(accountID string) ([]AccountHolds, error) { func (c *CoinbasePro) PlaceLimitOrder(clientRef string, price, amount float64, side, timeInforce, cancelAfter, productID, stp string, postOnly bool) (string, error) { resp := GeneralizedOrderResponse{} req := make(map[string]interface{}) - req["type"] = "limit" + req["type"] = order.Limit.Lower() req["price"] = strconv.FormatFloat(price, 'f', -1, 64) req["size"] = strconv.FormatFloat(amount, 'f', -1, 64) req["side"] = side @@ -449,7 +341,7 @@ func (c *CoinbasePro) PlaceMarketOrder(clientRef string, size, funds float64, si req := make(map[string]interface{}) req["side"] = side req["product_id"] = productID - req["type"] = "market" + req["type"] = order.Market.Lower() if size != 0 { req["size"] = strconv.FormatFloat(size, 'f', -1, 64) @@ -552,7 +444,7 @@ func (c *CoinbasePro) GetOrders(status []string, currencyPair string) ([]General params.Set("product_id", currencyPair) } - path := common.EncodeURLValues(c.APIUrl+coinbaseproOrders, params) + path := common.EncodeURLValues(c.API.Endpoints.URL+coinbaseproOrders, params) path = common.GetURIPath(path) return resp, @@ -582,7 +474,7 @@ func (c *CoinbasePro) GetFills(orderID, currencyPair string) ([]FillResponse, er return resp, errors.New("no parameters set") } - path := common.EncodeURLValues(c.APIUrl+coinbaseproFills, params) + path := common.EncodeURLValues(c.API.Endpoints.URL+coinbaseproFills, params) uri := common.GetURIPath(path) return resp, @@ -598,7 +490,7 @@ func (c *CoinbasePro) GetFundingRecords(status string) ([]Funding, error) { params := url.Values{} params.Set("status", status) - path := common.EncodeURLValues(c.APIUrl+coinbaseproFunding, params) + path := common.EncodeURLValues(c.API.Endpoints.URL+coinbaseproFunding, params) uri := common.GetURIPath(path) return resp, @@ -834,7 +726,7 @@ func (c *CoinbasePro) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated HTTP reque func (c *CoinbasePro) SendAuthenticatedHTTPRequest(method, path string, params map[string]interface{}, result interface{}) (err error) { - if !c.AuthenticatedAPISupport { + if !c.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, c.Name) } @@ -842,28 +734,28 @@ func (c *CoinbasePro) SendAuthenticatedHTTPRequest(method, path string, params m payload := []byte("") if params != nil { - payload, err = common.JSONEncode(params) + payload, err = json.Marshal(params) if err != nil { return errors.New("sendAuthenticatedHTTPRequest: Unable to JSON request") } if c.Verbose { - log.Debugf("Request JSON: %s\n", payload) + log.Debugf(log.ExchangeSys, "Request JSON: %s\n", payload) } } n := c.Requester.GetNonce(false).String() message := n + method + "/" + path + string(payload) - hmac := common.GetHMAC(common.HashSHA256, []byte(message), []byte(c.APISecret)) + hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(message), []byte(c.API.Credentials.Secret)) headers := make(map[string]string) - headers["CB-ACCESS-SIGN"] = common.Base64Encode(hmac) + headers["CB-ACCESS-SIGN"] = crypto.Base64Encode(hmac) headers["CB-ACCESS-TIMESTAMP"] = n - headers["CB-ACCESS-KEY"] = c.APIKey - headers["CB-ACCESS-PASSPHRASE"] = c.ClientID + headers["CB-ACCESS-KEY"] = c.API.Credentials.Key + headers["CB-ACCESS-PASSPHRASE"] = c.API.Credentials.ClientID headers["Content-Type"] = "application/json" return c.SendPayload(method, - c.APIUrl+path, + c.API.Endpoints.URL+path, headers, bytes.NewBuffer(payload), result, diff --git a/exchanges/coinbasepro/coinbasepro_test.go b/exchanges/coinbasepro/coinbasepro_test.go index 6f8f1c97..b594603c 100644 --- a/exchanges/coinbasepro/coinbasepro_test.go +++ b/exchanges/coinbasepro/coinbasepro_test.go @@ -1,7 +1,9 @@ package coinbasepro import ( + "log" "net/http" + "os" "testing" "time" @@ -9,6 +11,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -21,74 +24,81 @@ const ( apiSecret = "" clientID = "" // passphrase you made at API CREATION canManipulateRealOrders = false + testPair = "BTC-USD" ) -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { c.SetDefaults() c.Requester.SetRateLimit(false, time.Second, 1) -} -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("coinbasepro load config error", err) + } gdxConfig, err := cfg.GetExchangeConfig("CoinbasePro") if err != nil { - t.Error("Test Failed - coinbasepro Setup() init error") + log.Fatal("coinbasepro Setup() init error") } - gdxConfig.APIKey = apiKey - gdxConfig.APISecret = apiSecret - gdxConfig.ClientID = clientID - gdxConfig.AuthenticatedAPISupport = true - gdxConfig.AuthenticatedWebsocketAPISupport = true - c.Setup(&gdxConfig) + gdxConfig.API.Credentials.Key = apiKey + gdxConfig.API.Credentials.Secret = apiSecret + gdxConfig.API.Credentials.ClientID = clientID + gdxConfig.API.AuthenticatedSupport = true + gdxConfig.API.AuthenticatedWebsocketSupport = true + err = c.Setup(gdxConfig) + if err != nil { + log.Fatal("CoinbasePro setup error", err) + } + + os.Exit(m.Run()) } func TestGetProducts(t *testing.T) { _, err := c.GetProducts() if err != nil { - t.Errorf("Test failed - Coinbase, GetProducts() Error: %s", err) + t.Errorf("Coinbase, GetProducts() Error: %s", err) } } func TestGetTicker(t *testing.T) { - _, err := c.GetTicker("BTC-USD") + _, err := c.GetTicker(testPair) if err != nil { - t.Error("Test failed - GetTicker() error", err) + t.Error("GetTicker() error", err) } } func TestGetTrades(t *testing.T) { - _, err := c.GetTrades("BTC-USD") + _, err := c.GetTrades(testPair) if err != nil { - t.Error("Test failed - GetTrades() error", err) + t.Error("GetTrades() error", err) } } func TestGetHistoricRates(t *testing.T) { - _, err := c.GetHistoricRates("BTC-USD", 0, 0, 0) + _, err := c.GetHistoricRates(testPair, 0, 0, 0) if err != nil { - t.Error("Test failed - GetHistoricRates() error", err) + t.Error("GetHistoricRates() error", err) } } func TestGetStats(t *testing.T) { - _, err := c.GetStats("BTC-USD") + _, err := c.GetStats(testPair) if err != nil { - t.Error("Test failed - GetStats() error", err) + t.Error("GetStats() error", err) } } func TestGetCurrencies(t *testing.T) { _, err := c.GetCurrencies() if err != nil { - t.Error("Test failed - GetCurrencies() error", err) + t.Error("GetCurrencies() error", err) } } func TestGetServerTime(t *testing.T) { _, err := c.GetServerTime() if err != nil { - t.Error("Test failed - GetServerTime() error", err) + t.Error("GetServerTime() error", err) } } @@ -98,7 +108,7 @@ func TestAuthRequests(t *testing.T) { } _, err := c.GetAccounts() if err != nil { - t.Error("Test failed - GetAccounts() error", err) + t.Error("GetAccounts() error", err) } accountResponse, err := c.GetAccount("13371337-1337-1337-1337-133713371337") if accountResponse.ID != "" { @@ -121,21 +131,23 @@ func TestAuthRequests(t *testing.T) { if err == nil { t.Error("Expecting error") } - orderResponse, err := c.PlaceLimitOrder("", 0.001, 0.001, "buy", "", "", "BTC-USD", "", false) + orderResponse, err := c.PlaceLimitOrder("", 0.001, 0.001, + order.Buy.Lower(), "", "", testPair, "", false) if orderResponse != "" { t.Error("Expecting no data returned") } if err == nil { t.Error("Expecting error") } - marketOrderResponse, err := c.PlaceMarketOrder("", 1, 0, "buy", "BTC-USD", "") + marketOrderResponse, err := c.PlaceMarketOrder("", 1, 0, + order.Buy.Lower(), testPair, "") if marketOrderResponse != "" { t.Error("Expecting no data returned") } if err == nil { t.Error("Expecting error") } - fillsResponse, err := c.GetFills("1337", "BTC-USD") + fillsResponse, err := c.GetFills("1337", testPair) if len(fillsResponse) > 0 { t.Error("Expecting no data returned") } @@ -167,11 +179,11 @@ func TestAuthRequests(t *testing.T) { } _, err = c.GetPayMethods() if err != nil { - t.Error("Test failed - GetPayMethods() error", err) + t.Error("GetPayMethods() error", err) } _, err = c.GetCoinbaseAccounts() if err != nil { - t.Error("Test failed - GetCoinbaseAccounts() error", err) + t.Error("GetCoinbaseAccounts() error", err) } } @@ -188,7 +200,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() c.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -200,16 +212,13 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - c.SetDefaults() - TestSetup(t) - var feeBuilder = setFeeBuilder() - if apiKey != "" || apiSecret != "" { + if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic if resp, err := c.GetFee(feeBuilder); resp != float64(0.003) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) } // CryptocurrencyTradeFee High quantity @@ -217,7 +226,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := c.GetFee(feeBuilder); resp != float64(3000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(3000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(3000), resp) t.Error(err) } @@ -225,7 +234,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.01), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.01), resp) t.Error(err) } @@ -233,7 +242,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } @@ -242,7 +251,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -250,7 +259,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -259,7 +268,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.EUR if resp, err := c.GetFee(feeBuilder); resp != float64(0.15) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -268,7 +277,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := c.GetFee(feeBuilder); resp != float64(25) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } @@ -284,7 +293,7 @@ func TestCalculateTradingFee(t *testing.T) { } if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0.003) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) } // lowercase @@ -296,7 +305,7 @@ func TestCalculateTradingFee(t *testing.T) { } if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0.003) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) } // mixedCase @@ -308,7 +317,7 @@ func TestCalculateTradingFee(t *testing.T) { } if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0.003) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.003), resp) } // medium volume @@ -320,7 +329,7 @@ func TestCalculateTradingFee(t *testing.T) { } if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0.002) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // high volume @@ -332,7 +341,7 @@ func TestCalculateTradingFee(t *testing.T) { } if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0.001) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) } // no match @@ -344,7 +353,7 @@ func TestCalculateTradingFee(t *testing.T) { } if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, false); resp != float64(0) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) } // taker @@ -356,27 +365,21 @@ func TestCalculateTradingFee(t *testing.T) { } if resp := c.calculateTradingFee(volume, currency.BTC, currency.USD, "_", 1, 1, true); resp != float64(0) { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) } } func TestFormatWithdrawPermissions(t *testing.T) { - c.SetDefaults() expectedResult := exchange.AutoWithdrawCryptoWithAPIPermissionText + " & " + exchange.AutoWithdrawFiatWithAPIPermissionText - withdrawPermissions := c.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - c.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.BTC, currency.LTC)}, } @@ -390,11 +393,8 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - c.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.BTC, currency.LTC)}, } @@ -410,27 +410,27 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if c.APIKey != "" && c.APIKey != "Key" && - c.APISecret != "" && c.APISecret != "Secret" { - return true - } - return false + return c.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - c.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "-", - Base: currency.BTC, - Quote: currency.LTC, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: "-", + Base: currency.BTC, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := c.SubmitOrder(p, exchange.BuyOrderSide, exchange.LimitOrderType, 1, 1, "clientId") + response, err := c.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -439,16 +439,12 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - c.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -465,16 +461,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - c.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -490,26 +482,29 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestModifyOrder(t *testing.T) { - _, err := c.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := c.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - c.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.LTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -526,16 +521,15 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - c.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.USD, + var withdrawFiatRequest = exchange.FiatWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: 100, + Currency: currency.USD, + }, BankName: "Federal Reserve Bank", } @@ -549,16 +543,15 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - c.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.USD, + var withdrawFiatRequest = exchange.FiatWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: 100, + Currency: currency.USD, + }, BankName: "Federal Reserve Bank", } @@ -574,15 +567,13 @@ func TestWithdrawInternationalBank(t *testing.T) { func TestGetDepositAddress(t *testing.T) { _, err := c.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) } } // TestWsAuth dials websocket, sends login request. func TestWsAuth(t *testing.T) { - c.SetDefaults() - TestSetup(t) - if !c.Websocket.IsEnabled() && !c.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + if !c.Websocket.IsEnabled() && !c.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } c.WebsocketConn = &wshandler.WebsocketConnection{ @@ -602,7 +593,7 @@ func TestWsAuth(t *testing.T) { go c.WsHandleData() err = c.Subscribe(wshandler.WebsocketChannelSubscription{ Channel: "user", - Currency: currency.NewPairFromString("BTC-USD"), + Currency: currency.NewPairFromString(testPair), }) if err != nil { t.Error(err) diff --git a/exchanges/coinbasepro/coinbasepro_types.go b/exchanges/coinbasepro/coinbasepro_types.go index 77afd7a0..05ea954b 100644 --- a/exchanges/coinbasepro/coinbasepro_types.go +++ b/exchanges/coinbasepro/coinbasepro_types.go @@ -1,6 +1,10 @@ package coinbasepro -import "time" +import ( + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" +) // Product holds product information type Product struct { @@ -15,10 +19,13 @@ type Product struct { // Ticker holds basic ticker information type Ticker struct { - TradeID int64 `json:"trade_id"` - Price float64 `json:"price,string"` - Size float64 `json:"size,string"` - Time string `json:"time"` + TradeID int64 `json:"trade_id"` + Ask float64 `json:"ask,string"` + Bid float64 `json:"bid,string"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + Volume float64 `json:"volume,string"` + Time time.Time `json:"time"` } // Trade holds executed trade information @@ -435,21 +442,21 @@ type WebsocketHeartBeat struct { // WebsocketTicker defines ticker websocket response type WebsocketTicker struct { - Type string `json:"type"` - Sequence int64 `json:"sequence"` - ProductID string `json:"product_id"` - Price float64 `json:"price,string"` - Open24H float64 `json:"open_24h,string"` - Volume24H float64 `json:"volumen_24h,string"` - Low24H float64 `json:"low_24h,string"` - High24H float64 `json:"high_24h,string"` - Volume30D float64 `json:"volume_30d,string"` - BestBid float64 `json:"best_bid,string"` - BestAsk float64 `json:"best_ask,string"` - Side string `json:"side"` - Time time.Time `json:"time"` - TradeID int64 `json:"trade_id"` - LastSize float64 `json:"last_size,string"` + Type string `json:"type"` + Sequence int64 `json:"sequence"` + ProductID currency.Pair `json:"product_id"` + Price float64 `json:"price,string"` + Open24H float64 `json:"open_24h,string"` + Volume24H float64 `json:"volume_24h,string"` + Low24H float64 `json:"low_24h,string"` + High24H float64 `json:"high_24h,string"` + Volume30D float64 `json:"volume_30d,string"` + BestBid float64 `json:"best_bid,string"` + BestAsk float64 `json:"best_ask,string"` + Side string `json:"side"` + Time time.Time `json:"time"` + TradeID int64 `json:"trade_id"` + LastSize float64 `json:"last_size,string"` } // WebsocketOrderbookSnapshot defines a snapshot response diff --git a/exchanges/coinbasepro/coinbasepro_websocket.go b/exchanges/coinbasepro/coinbasepro_websocket.go index f411e424..6994211b 100644 --- a/exchanges/coinbasepro/coinbasepro_websocket.go +++ b/exchanges/coinbasepro/coinbasepro_websocket.go @@ -1,16 +1,18 @@ package coinbasepro import ( + "encoding/json" "errors" - "fmt" "net/http" "strconv" "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" @@ -52,7 +54,7 @@ func (c *CoinbasePro) WsHandleData() { default: resp, err := c.WebsocketConn.ReadMessage() if err != nil { - c.Websocket.DataHandler <- err + c.Websocket.ReadMessageErrors <- err return } c.Websocket.TrafficAlert <- struct{}{} @@ -64,7 +66,7 @@ func (c *CoinbasePro) WsHandleData() { } msgType := MsgType{} - err = common.JSONDecode(resp.Raw, &msgType) + err = json.Unmarshal(resp.Raw, &msgType) if err != nil { c.Websocket.DataHandler <- err continue @@ -80,27 +82,29 @@ func (c *CoinbasePro) WsHandleData() { case "ticker": ticker := WebsocketTicker{} - err := common.JSONDecode(resp.Raw, &ticker) + err := json.Unmarshal(resp.Raw, &ticker) if err != nil { c.Websocket.DataHandler <- err continue } c.Websocket.DataHandler <- wshandler.TickerData{ - Timestamp: ticker.Time, - Pair: currency.NewPairFromString(ticker.ProductID), - AssetType: orderbook.Spot, - Exchange: c.GetName(), - OpenPrice: ticker.Open24H, - HighPrice: ticker.High24H, - LowPrice: ticker.Low24H, - ClosePrice: ticker.Price, - Quantity: ticker.Volume24H, + Timestamp: ticker.Time, + Pair: ticker.ProductID, + AssetType: asset.Spot, + Exchange: c.Name, + Open: ticker.Open24H, + High: ticker.High24H, + Low: ticker.Low24H, + Last: ticker.Price, + Volume: ticker.Volume24H, + Bid: ticker.BestBid, + Ask: ticker.BestAsk, } case "snapshot": snapshot := WebsocketOrderbookSnapshot{} - err := common.JSONDecode(resp.Raw, &snapshot) + err := json.Unmarshal(resp.Raw, &snapshot) if err != nil { c.Websocket.DataHandler <- err continue @@ -114,7 +118,7 @@ func (c *CoinbasePro) WsHandleData() { case "l2update": update := WebsocketL2Update{} - err := common.JSONDecode(resp.Raw, &update) + err := json.Unmarshal(resp.Raw, &update) if err != nil { c.Websocket.DataHandler <- err continue @@ -128,7 +132,7 @@ func (c *CoinbasePro) WsHandleData() { case "received": // We currently use l2update to calculate orderbook changes received := WebsocketReceived{} - err := common.JSONDecode(resp.Raw, &received) + err := json.Unmarshal(resp.Raw, &received) if err != nil { c.Websocket.DataHandler <- err continue @@ -137,7 +141,7 @@ func (c *CoinbasePro) WsHandleData() { case "open": // We currently use l2update to calculate orderbook changes open := WebsocketOpen{} - err := common.JSONDecode(resp.Raw, &open) + err := json.Unmarshal(resp.Raw, &open) if err != nil { c.Websocket.DataHandler <- err continue @@ -146,7 +150,7 @@ func (c *CoinbasePro) WsHandleData() { case "done": // We currently use l2update to calculate orderbook changes done := WebsocketDone{} - err := common.JSONDecode(resp.Raw, &done) + err := json.Unmarshal(resp.Raw, &done) if err != nil { c.Websocket.DataHandler <- err continue @@ -155,7 +159,7 @@ func (c *CoinbasePro) WsHandleData() { case "change": // We currently use l2update to calculate orderbook changes change := WebsocketChange{} - err := common.JSONDecode(resp.Raw, &change) + err := json.Unmarshal(resp.Raw, &change) if err != nil { c.Websocket.DataHandler <- err continue @@ -164,7 +168,7 @@ func (c *CoinbasePro) WsHandleData() { case "activate": // We currently use l2update to calculate orderbook changes activate := WebsocketActivate{} - err := common.JSONDecode(resp.Raw, &activate) + err := json.Unmarshal(resp.Raw, &activate) if err != nil { c.Websocket.DataHandler <- err continue @@ -209,18 +213,19 @@ func (c *CoinbasePro) ProcessSnapshot(snapshot *WebsocketOrderbookSnapshot) erro } pair := currency.NewPairFromString(snapshot.ProductID) - base.AssetType = orderbook.Spot + base.AssetType = asset.Spot base.Pair = pair + base.ExchangeName = c.Name - err := c.Websocket.Orderbook.LoadSnapshot(&base, false) + err := c.Websocket.Orderbook.LoadSnapshot(&base) if err != nil { return err } c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: pair, - Asset: orderbook.Spot, - Exchange: c.GetName(), + Asset: asset.Spot, + Exchange: c.Name, } return nil @@ -234,7 +239,7 @@ func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error { price, _ := strconv.ParseFloat(update.Changes[i][1].(string), 64) volume, _ := strconv.ParseFloat(update.Changes[i][2].(string), 64) - if update.Changes[i][0].(string) == "buy" { + if update.Changes[i][0].(string) == order.Buy.Lower() { bids = append(bids, orderbook.Item{Price: price, Amount: volume}) } else { asks = append(asks, orderbook.Item{Price: price, Amount: volume}) @@ -251,11 +256,11 @@ func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error { return err } err = c.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: p, - UpdateTime: timestamp, - AssetType: orderbook.Spot, + Bids: bids, + Asks: asks, + Pair: p, + UpdateTime: timestamp, + Asset: asset.Spot, }) if err != nil { return err @@ -263,8 +268,8 @@ func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error { c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: p, - Asset: orderbook.Spot, - Exchange: c.GetName(), + Asset: asset.Spot, + Exchange: c.Name, } return nil @@ -273,17 +278,17 @@ func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error { // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (c *CoinbasePro) GenerateDefaultSubscriptions() { var channels = []string{"heartbeat", "level2", "ticker", "user"} - enabledCurrencies := c.GetEnabledCurrencies() + enabledCurrencies := c.GetEnabledPairs(asset.Spot) var subscriptions []wshandler.WebsocketChannelSubscription for i := range channels { if (channels[i] == "user" || channels[i] == "full") && !c.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { continue } for j := range enabledCurrencies { - enabledCurrencies[j].Delimiter = "-" subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: channels[i], - Currency: enabledCurrencies[j], + Channel: channels[i], + Currency: c.FormatExchangeCurrency(enabledCurrencies[j], + asset.Spot), }) } } @@ -298,18 +303,20 @@ func (c *CoinbasePro) Subscribe(channelToSubscribe wshandler.WebsocketChannelSub { Name: channelToSubscribe.Channel, ProductIDs: []string{ - channelToSubscribe.Currency.String(), + c.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String(), }, }, }, } if channelToSubscribe.Channel == "user" || channelToSubscribe.Channel == "full" { - n := fmt.Sprintf("%v", time.Now().Unix()) + n := strconv.FormatInt(time.Now().Unix(), 10) message := n + "GET" + "/users/self/verify" - hmac := common.GetHMAC(common.HashSHA256, []byte(message), []byte(c.APISecret)) - subscribe.Signature = common.Base64Encode(hmac) - subscribe.Key = c.APIKey - subscribe.Passphrase = c.ClientID + hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(message), + []byte(c.API.Credentials.Secret)) + subscribe.Signature = crypto.Base64Encode(hmac) + subscribe.Key = c.API.Credentials.Key + subscribe.Passphrase = c.API.Credentials.ClientID subscribe.Timestamp = n } return c.WebsocketConn.SendMessage(subscribe) @@ -323,7 +330,8 @@ func (c *CoinbasePro) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelS { Name: channelToSubscribe.Channel, ProductIDs: []string{ - channelToSubscribe.Currency.String(), + c.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String(), }, }, }, diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index 5c0f02f2..db4848e8 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -8,14 +8,173 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (c *CoinbasePro) GetDefaultConfig() (*config.ExchangeConfig, error) { + c.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = c.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = c.BaseCurrencies + + err := c.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if c.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = c.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets default values for the exchange +func (c *CoinbasePro) SetDefaults() { + c.Name = "CoinbasePro" + c.Enabled = true + c.Verbose = true + c.API.CredentialsValidator.RequiresKey = true + c.API.CredentialsValidator.RequiresSecret = true + c.API.CredentialsValidator.RequiresClientID = true + c.API.CredentialsValidator.RequiresBase64DecodeSecret = true + + c.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + } + + c.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + FiatDeposit: true, + FiatWithdraw: true, + TradeFee: true, + FiatDepositFee: true, + FiatWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + MessageSequenceNumbers: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | + exchange.AutoWithdrawFiatWithAPIPermission, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + c.Requester = request.New(c.Name, + request.NewRateLimit(time.Second, coinbaseproAuthRate), + request.NewRateLimit(time.Second, coinbaseproUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + c.API.Endpoints.URLDefault = coinbaseproAPIURL + c.API.Endpoints.URL = c.API.Endpoints.URLDefault + c.API.Endpoints.WebsocketURL = coinbaseproWebsocketURL + c.Websocket = wshandler.New() + c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + c.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup initialises the exchange parameters with the current configuration +func (c *CoinbasePro) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + c.SetEnabled(false) + return nil + } + + err := c.SetupDefaults(exch) + if err != nil { + return err + } + + err = c.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: coinbaseproWebsocketURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: c.WsConnect, + Subscriber: c.Subscribe, + UnSubscriber: c.Unsubscribe, + Features: &c.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + c.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: c.Name, + URL: c.Websocket.GetWebsocketURL(), + ProxyURL: c.Websocket.GetProxyAddress(), + Verbose: c.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + c.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + true, + true, + false, + false, + exch.Name) + return nil +} + // Start starts the coinbasepro go routine func (c *CoinbasePro) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -28,40 +187,76 @@ func (c *CoinbasePro) Start(wg *sync.WaitGroup) { // Run implements the coinbasepro wrapper func (c *CoinbasePro) Run() { if c.Verbose { - log.Debugf("%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket.IsEnabled()), coinbaseproWebsocketURL) - log.Debugf("%s polling delay: %ds.\n", c.GetName(), c.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", c.GetName(), len(c.EnabledPairs), c.EnabledPairs) + log.Debugf(log.ExchangeSys, + "%s Websocket: %s. (url: %s).\n", + c.Name, + common.IsEnabled(c.Websocket.IsEnabled()), + coinbaseproWebsocketURL) + c.PrintEnabledPairs() } - exchangeProducts, err := c.GetProducts() - if err != nil { - log.Errorf("%s Failed to get available products.\n", c.GetName()) - } else { - var currencies []string - for _, x := range exchangeProducts { - if x.ID != "BTC" && x.ID != "USD" && x.ID != "GBP" { - currencies = append(currencies, x.ID[0:3]+x.ID[4:]) - } - } + forceUpdate := false + delim := c.GetPairFormat(asset.Spot, false).Delimiter + if !common.StringDataContains(c.CurrencyPairs.GetPairs(asset.Spot, + true).Strings(), delim) || + !common.StringDataContains(c.CurrencyPairs.GetPairs(asset.Spot, + false).Strings(), delim) { + enabledPairs := currency.NewPairsFromStrings( + []string{fmt.Sprintf("BTC%sUSD", delim)}, + ) + log.Warn(log.ExchangeSys, + "Enabled pairs for CoinbasePro reset due to config upgrade, please enable the ones you would like to use again") + forceUpdate = true - var newCurrencies currency.Pairs - for _, p := range currencies { - newCurrencies = append(newCurrencies, - currency.NewPairFromString(p)) - } - - err = c.UpdateCurrencies(newCurrencies, false, false) + err := c.UpdatePairs(enabledPairs, asset.Spot, true, true) if err != nil { - log.Errorf("%s Failed to update available currencies.\n", c.GetName()) + log.Errorf(log.ExchangeSys, "%s failed to update currencies. Err: %s\n", c.Name, err) } } + + if !c.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { + return + } + + err := c.UpdateTradablePairs(forceUpdate) + if err != nil { + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", c.Name, err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (c *CoinbasePro) FetchTradablePairs(asset asset.Item) ([]string, error) { + pairs, err := c.GetProducts() + if err != nil { + return nil, err + } + + var products []string + for x := range pairs { + products = append(products, pairs[x].BaseCurrency+ + c.GetPairFormat(asset, false).Delimiter+ + pairs[x].QuoteCurrency) + } + + return products, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (c *CoinbasePro) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := c.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return c.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) } // GetAccountInfo retrieves balances for all enabled currencies for the // coinbasepro exchange func (c *CoinbasePro) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = c.GetName() + response.Exchange = c.Name accountBalance, err := c.GetAccounts() if err != nil { return response, err @@ -85,26 +280,30 @@ func (c *CoinbasePro) GetAccountInfo() (exchange.AccountInfo, error) { } // UpdateTicker updates and returns the ticker for a currency pair -func (c *CoinbasePro) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (c *CoinbasePro) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - tick, err := c.GetTicker(exchange.FormatExchangeCurrency(c.Name, p).String()) + tick, err := c.GetTicker(c.FormatExchangeCurrency(p, assetType).String()) + if err != nil { + return ticker.Price{}, err + } + stats, err := c.GetStats(c.FormatExchangeCurrency(p, assetType).String()) if err != nil { return ticker.Price{}, err } - stats, err := c.GetStats(exchange.FormatExchangeCurrency(c.Name, p).String()) - - if err != nil { - return ticker.Price{}, err + tickerPrice = ticker.Price{ + Last: tick.Size, + High: stats.High, + Low: stats.Low, + Bid: tick.Bid, + Ask: tick.Ask, + Volume: tick.Volume, + Open: stats.Open, + Pair: p, + LastUpdated: tick.Time, } - tickerPrice.Pair = p - tickerPrice.Volume = stats.Volume - tickerPrice.Last = tick.Price - tickerPrice.High = stats.High - tickerPrice.Low = stats.Low - - err = ticker.ProcessTicker(c.GetName(), &tickerPrice, assetType) + err = ticker.ProcessTicker(c.Name, &tickerPrice, assetType) if err != nil { return tickerPrice, err } @@ -112,18 +311,18 @@ func (c *CoinbasePro) UpdateTicker(p currency.Pair, assetType string) (ticker.Pr return ticker.GetTicker(c.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (c *CoinbasePro) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(c.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (c *CoinbasePro) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(c.Name, p, assetType) if err != nil { return c.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (c *CoinbasePro) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(c.GetName(), p, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (c *CoinbasePro) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(c.Name, p, assetType) if err != nil { return c.UpdateOrderbook(p, assetType) } @@ -131,9 +330,10 @@ func (c *CoinbasePro) GetOrderbookEx(p currency.Pair, assetType string) (orderbo } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (c *CoinbasePro) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (c *CoinbasePro) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := c.GetOrderbook(exchange.FormatExchangeCurrency(c.Name, p).String(), 2) + orderbookNew, err := c.GetOrderbook(c.FormatExchangeCurrency(p, + assetType).String(), 2) if err != nil { return orderBook, err } @@ -149,7 +349,7 @@ func (c *CoinbasePro) UpdateOrderbook(p currency.Pair, assetType string) (orderb } orderBook.Pair = p - orderBook.ExchangeName = c.GetName() + orderBook.ExchangeName = c.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -163,77 +363,80 @@ func (c *CoinbasePro) UpdateOrderbook(p currency.Pair, assetType string) (orderb // GetFundingHistory returns funding history, deposits and // withdrawals func (c *CoinbasePro) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (c *CoinbasePro) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (c *CoinbasePro) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (c *CoinbasePro) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse +func (c *CoinbasePro) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } + var response string var err error - - switch orderType { - case exchange.MarketOrderType: - response, err = c.PlaceMarginOrder("", - amount, - amount, - side.ToString(), - p.String(), + switch s.OrderType { + case order.Market: + response, err = c.PlaceMarketOrder("", + s.Amount, + s.Amount, + s.OrderSide.String(), + c.FormatExchangeCurrency(s.Pair, asset.Spot).String(), "") - case exchange.LimitOrderType: + case order.Limit: response, err = c.PlaceLimitOrder("", - price, - amount, - side.ToString(), + s.Price, + s.Amount, + s.OrderSide.String(), "", "", - p.String(), + c.FormatExchangeCurrency(s.Pair, asset.Spot).String(), "", false) default: err = errors.New("order type not supported") } - + if err != nil { + return submitOrderResponse, err + } + if s.OrderType == order.Market { + submitOrderResponse.FullyMatched = true + } if response != "" { submitOrderResponse.OrderID = response } - if err == nil { - submitOrderResponse.IsOrderPlaced = true - } + submitOrderResponse.IsOrderPlaced = true - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (c *CoinbasePro) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (c *CoinbasePro) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (c *CoinbasePro) CancelOrder(order *exchange.OrderCancellation) error { +func (c *CoinbasePro) CancelOrder(order *order.Cancel) error { return c.CancelExistingOrder(order.OrderID) } // CancelAllOrders cancels all orders associated with a currency pair -func (c *CoinbasePro) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { +func (c *CoinbasePro) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { // CancellAllExisting orders returns a list of successful cancellations, we're only interested in failures _, err := c.CancelAllExistingOrders("") - return exchange.CancelAllOrdersResponse{}, err + return order.CancelAllResponse{}, err } // GetOrderInfo returns information on a current open order -func (c *CoinbasePro) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (c *CoinbasePro) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -244,14 +447,14 @@ func (c *CoinbasePro) GetDepositAddress(cryptocurrency currency.Code, accountID // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (c *CoinbasePro) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (c *CoinbasePro) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { resp, err := c.WithdrawCrypto(withdrawRequest.Amount, withdrawRequest.Currency.String(), withdrawRequest.Address) return resp.ID, err } // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is // submitted -func (c *CoinbasePro) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (c *CoinbasePro) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { paymentMethods, err := c.GetPayMethods() if err != nil { return "", err @@ -278,7 +481,7 @@ func (c *CoinbasePro) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawReques // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (c *CoinbasePro) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (c *CoinbasePro) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return c.WithdrawFiatFunds(withdrawRequest) } @@ -289,7 +492,7 @@ func (c *CoinbasePro) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (c *CoinbasePro) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (c.APIKey == "" || c.APISecret == "") && // Todo check connection status + if !c.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -297,90 +500,95 @@ func (c *CoinbasePro) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, er } // GetActiveOrders retrieves any orders that are active/open -func (c *CoinbasePro) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (c *CoinbasePro) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { var respOrders []GeneralizedOrderResponse - for i := range getOrdersRequest.Currencies { + for i := range req.Currencies { resp, err := c.GetOrders([]string{"open", "pending", "active"}, - exchange.FormatExchangeCurrency(c.Name, getOrdersRequest.Currencies[i]).String()) + c.FormatExchangeCurrency(req.Currencies[i], asset.Spot).String()) if err != nil { return nil, err } respOrders = append(respOrders, resp...) } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range respOrders { - currency := currency.NewPairDelimiter(respOrders[i].ProductID, - c.ConfigCurrencyPairFormat.Delimiter) - orderSide := exchange.OrderSide(strings.ToUpper(respOrders[i].Side)) - orderType := exchange.OrderType(strings.ToUpper(respOrders[i].Type)) + curr := currency.NewPairDelimiter(respOrders[i].ProductID, + c.GetPairFormat(asset.Spot, false).Delimiter) + orderSide := order.Side(strings.ToUpper(respOrders[i].Side)) + orderType := order.Type(strings.ToUpper(respOrders[i].Type)) orderDate, err := time.Parse(time.RFC3339, respOrders[i].CreatedAt) if err != nil { - log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v", - c.Name, "GetActiveOrders", respOrders[i].ID, respOrders[i].CreatedAt) + log.Errorf(log.ExchangeSys, + "Exchange %v Func %v Order %v Could not parse date to unix with value of %v", + c.Name, + "GetActiveOrders", + respOrders[i].ID, + respOrders[i].CreatedAt) } - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ ID: respOrders[i].ID, Amount: respOrders[i].Size, ExecutedAmount: respOrders[i].FilledSize, OrderType: orderType, OrderDate: orderDate, OrderSide: orderSide, - CurrencyPair: currency, + CurrencyPair: curr, Exchange: c.Name, }) } - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (c *CoinbasePro) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (c *CoinbasePro) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { var respOrders []GeneralizedOrderResponse - for _, currency := range getOrdersRequest.Currencies { + for i := range req.Currencies { resp, err := c.GetOrders([]string{"done", "settled"}, - exchange.FormatExchangeCurrency(c.Name, currency).String()) + c.FormatExchangeCurrency(req.Currencies[i], asset.Spot).String()) if err != nil { return nil, err } respOrders = append(respOrders, resp...) } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range respOrders { - currency := currency.NewPairDelimiter(respOrders[i].ProductID, - c.ConfigCurrencyPairFormat.Delimiter) - orderSide := exchange.OrderSide(strings.ToUpper(respOrders[i].Side)) - orderType := exchange.OrderType(strings.ToUpper(respOrders[i].Type)) + curr := currency.NewPairDelimiter(respOrders[i].ProductID, + c.GetPairFormat(asset.Spot, false).Delimiter) + orderSide := order.Side(strings.ToUpper(respOrders[i].Side)) + orderType := order.Type(strings.ToUpper(respOrders[i].Type)) orderDate, err := time.Parse(time.RFC3339, respOrders[i].CreatedAt) if err != nil { - log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v", - c.Name, "GetActiveOrders", respOrders[i].ID, respOrders[i].CreatedAt) + log.Errorf(log.ExchangeSys, + "Exchange %v Func %v Order %v Could not parse date to unix with value of %v", + c.Name, + "GetActiveOrders", + respOrders[i].ID, + respOrders[i].CreatedAt) } - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ ID: respOrders[i].ID, Amount: respOrders[i].Size, ExecutedAmount: respOrders[i].FilledSize, OrderType: orderType, OrderDate: orderDate, OrderSide: orderSide, - CurrencyPair: currency, + CurrencyPair: curr, Exchange: c.Name, }) } - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } diff --git a/exchanges/coinbene/README.md b/exchanges/coinbene/README.md index 44f6ef31..c4fe862f 100644 --- a/exchanges/coinbene/README.md +++ b/exchanges/coinbene/README.md @@ -48,22 +48,22 @@ main.go ```go var c exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Coinbene" { - c = bot.exchanges[i] +for i := range Bot.Exchanges { + if Bot.Exchanges[i].GetName() == "Coinbene" { + c = Bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := c.GetTickerPrice() +tick, err := c.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := c.GetOrderbookEx() +ob, err := c.FetchOrderbook() if err != nil { // Handle error } @@ -90,7 +90,7 @@ if err != nil { } // Fetches current orderbook information -ob, err := c.GetOrderBook() +ob, err := c.GetOrderbook() if err != nil { // Handle error } @@ -105,7 +105,7 @@ if err != nil { } // Submits an order and the exchange and returns its tradeID -tradeID, err := c.Trade(...) +resp, err := c.SubmitOrder(...) if err != nil { // Handle error } @@ -138,4 +138,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/coinbene/coinbene.go b/exchanges/coinbene/coinbene.go index b1909785..482edab7 100644 --- a/exchanges/coinbene/coinbene.go +++ b/exchanges/coinbene/coinbene.go @@ -12,12 +12,10 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) // Coinbene is the overarching type across this package @@ -30,8 +28,6 @@ const ( coinbeneAPIURL = "https://openapi-exchange.coinbene.com/api/exchange/" coinbeneAuthPath = "/api/exchange/v2" coinbeneAPIVersion = "v2" - buy = "buy" - sell = "sell" // Public endpoints coinbeneFetchTicker = "/market/ticker/one" @@ -52,124 +48,22 @@ const ( unauthRateLimit = 10 ) -// SetDefaults sets the basic defaults for Coinbene -func (c *Coinbene) SetDefaults() { - c.Name = "Coinbene" - c.Enabled = false - c.Verbose = false - c.RESTPollingDelay = 10 - c.RequestCurrencyPairFormat.Delimiter = "/" - c.RequestCurrencyPairFormat.Uppercase = true - c.ConfigCurrencyPairFormat.Delimiter = "/" - c.ConfigCurrencyPairFormat.Uppercase = true - c.AssetTypes = []string{ticker.Spot} - c.SupportsAutoPairUpdating = true - c.SupportsRESTTickerBatching = false - c.Requester = request.New(c.Name, - request.NewRateLimit(time.Minute, authRateLimit), - request.NewRateLimit(time.Second, unauthRateLimit), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - c.APIUrlDefault = coinbeneAPIURL - c.APIUrl = c.APIUrlDefault - c.Websocket = wshandler.New() - c.WebsocketURL = coinbeneWsURL - c.Websocket.Functionality = wshandler.WebsocketTickerSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketKlineSupported | - wshandler.WebsocketAccountDataSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported - c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - c.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup takes in the supplied exchange configuration details and sets params -func (c *Coinbene) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - c.SetEnabled(false) - } else { - c.Enabled = true - c.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - c.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - c.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - c.SetHTTPClientTimeout(exch.HTTPTimeout) - c.SetHTTPClientUserAgent(exch.HTTPUserAgent) - c.RESTPollingDelay = exch.RESTPollingDelay - c.Verbose = exch.Verbose - c.Websocket.SetWsStatusAndConnection(exch.Websocket) - c.BaseCurrencies = exch.BaseCurrencies - c.AvailablePairs = exch.AvailablePairs - c.EnabledPairs = exch.EnabledPairs - err := c.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = c.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = c.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = c.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = c.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - - err = c.Websocket.Setup(c.WsConnect, - c.Subscribe, - c.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - coinbeneWsURL, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - c.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: c.Name, - URL: c.Websocket.GetWebsocketURL(), - ProxyURL: c.Websocket.GetProxyAddress(), - Verbose: c.Verbose, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - c.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - true, - false, - false, - exch.Name) - } -} - -// FetchTicker gets and stores ticker data for a currency pair -func (c *Coinbene) FetchTicker(symbol string) (TickerResponse, error) { +// GetTicker gets and stores ticker data for a currency pair +func (c *Coinbene) GetTicker(symbol string) (TickerResponse, error) { var t TickerResponse params := url.Values{} params.Set("symbol", symbol) - path := common.EncodeURLValues(c.APIUrl+coinbeneAPIVersion+coinbeneFetchTicker, params) + path := common.EncodeURLValues(c.API.Endpoints.URL+coinbeneAPIVersion+coinbeneFetchTicker, params) return t, c.SendHTTPRequest(path, &t) } -// FetchOrderbooks gets and stores orderbook data for given pair -func (c *Coinbene) FetchOrderbooks(symbol string, size int64) (OrderbookResponse, error) { +// GetOrderbook gets and stores orderbook data for given pair +func (c *Coinbene) GetOrderbook(symbol string, size int64) (OrderbookResponse, error) { var o OrderbookResponse params := url.Values{} params.Set("symbol", symbol) params.Set("depth", strconv.FormatInt(size, 10)) - path := common.EncodeURLValues(c.APIUrl+coinbeneAPIVersion+coinbeneFetchOrderBook, params) + path := common.EncodeURLValues(c.API.Endpoints.URL+coinbeneAPIVersion+coinbeneFetchOrderBook, params) return o, c.SendHTTPRequest(path, &o) } @@ -178,7 +72,7 @@ func (c *Coinbene) GetTrades(symbol string) (TradeResponse, error) { var t TradeResponse params := url.Values{} params.Set("symbol", symbol) - path := common.EncodeURLValues(c.APIUrl+coinbeneAPIVersion+coinbeneGetTrades, params) + path := common.EncodeURLValues(c.API.Endpoints.URL+coinbeneAPIVersion+coinbeneGetTrades, params) return t, c.SendHTTPRequest(path, &t) } @@ -187,21 +81,21 @@ func (c *Coinbene) GetPairInfo(symbol string) (PairResponse, error) { var resp PairResponse params := url.Values{} params.Set("symbol", symbol) - path := common.EncodeURLValues(c.APIUrl+coinbeneAPIVersion+coinbenePairInfo, params) + path := common.EncodeURLValues(c.API.Endpoints.URL+coinbeneAPIVersion+coinbenePairInfo, params) return resp, c.SendHTTPRequest(path, &resp) } // GetAllPairs gets all pairs on the exchange func (c *Coinbene) GetAllPairs() (AllPairResponse, error) { var a AllPairResponse - path := c.APIUrl + coinbeneAPIVersion + coinbeneGetAllPairs + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneGetAllPairs return a, c.SendHTTPRequest(path, &a) } // GetUserBalance gets user balanace info func (c *Coinbene) GetUserBalance() (UserBalanceResponse, error) { var resp UserBalanceResponse - path := c.APIUrl + coinbeneAPIVersion + coinbeneGetUserBalance + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneGetUserBalance err := c.SendAuthHTTPRequest(http.MethodGet, path, coinbeneGetUserBalance, nil, &resp) if err != nil { return resp, err @@ -215,13 +109,13 @@ func (c *Coinbene) GetUserBalance() (UserBalanceResponse, error) { // PlaceOrder creates an order func (c *Coinbene) PlaceOrder(price, quantity float64, symbol, direction, clientID string) (PlaceOrderResponse, error) { var resp PlaceOrderResponse - path := c.APIUrl + coinbeneAPIVersion + coinbenePlaceOrder + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbenePlaceOrder params := url.Values{} params.Set("symbol", symbol) switch direction { - case sell: + case order.Buy.Lower(): params.Set("direction", "2") - case buy: + case order.Sell.Lower(): params.Set("direction", "1") default: return resp, @@ -251,7 +145,7 @@ func (c *Coinbene) FetchOrderInfo(orderID string) (OrderInfoResponse, error) { var resp OrderInfoResponse params := url.Values{} params.Set("orderId", orderID) - path := c.APIUrl + coinbeneAPIVersion + coinbeneOrderInfo + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneOrderInfo err := c.SendAuthHTTPRequest(http.MethodGet, path, coinbeneOrderInfo, params, &resp) if err != nil { return resp, err @@ -271,7 +165,7 @@ func (c *Coinbene) RemoveOrder(orderID string) (RemoveOrderResponse, error) { var resp RemoveOrderResponse params := url.Values{} params.Set("orderId", orderID) - path := c.APIUrl + coinbeneAPIVersion + coinbeneRemoveOrder + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneRemoveOrder err := c.SendAuthHTTPRequest(http.MethodPost, path, coinbeneRemoveOrder, params, &resp) if err != nil { return resp, err @@ -287,7 +181,7 @@ func (c *Coinbene) FetchOpenOrders(symbol string) (OpenOrderResponse, error) { var resp OpenOrderResponse params := url.Values{} params.Set("symbol", symbol) - path := c.APIUrl + coinbeneAPIVersion + coinbeneOpenOrders + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneOpenOrders for i := int64(1); ; i++ { var temp OpenOrderResponse params.Set("pageNum", strconv.FormatInt(i, 10)) @@ -315,7 +209,7 @@ func (c *Coinbene) FetchClosedOrders(symbol, latestID string) (ClosedOrderRespon params := url.Values{} params.Set("symbol", symbol) params.Set("latestOrderId", latestID) - path := c.APIUrl + coinbeneAPIVersion + coinbeneClosedOrders + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneClosedOrders for i := int64(1); ; i++ { var temp ClosedOrderResponse params.Set("pageNum", strconv.FormatInt(i, 10)) @@ -352,9 +246,14 @@ func (c *Coinbene) SendHTTPRequest(path string, result interface{}) error { // SendAuthHTTPRequest sends an authenticated HTTP request func (c *Coinbene) SendAuthHTTPRequest(method, path, epPath string, params url.Values, result interface{}) error { + if !c.AllowAuthenticatedRequest() { + return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, c.Name) + } + if params == nil { params = url.Values{} } + timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.999Z") var finalBody io.Reader var preSign string @@ -376,11 +275,13 @@ func (c *Coinbene) SendAuthHTTPRequest(method, path, epPath string, params url.V case len(params) == 0: preSign = timestamp + method + coinbeneAuthPath + epPath } - tempSign := common.GetHMAC(common.HashSHA256, []byte(preSign), []byte(c.APISecret)) + tempSign := crypto.GetHMAC(crypto.HashSHA256, + []byte(preSign), + []byte(c.API.Credentials.Secret)) headers := make(map[string]string) headers["Content-Type"] = "application/json" - headers["ACCESS-KEY"] = c.APIKey - headers["ACCESS-SIGN"] = common.HexEncodeToString(tempSign) + headers["ACCESS-KEY"] = c.API.Credentials.Key + headers["ACCESS-SIGN"] = crypto.HexEncodeToString(tempSign) headers["ACCESS-TIMESTAMP"] = timestamp return c.SendPayload(method, path, diff --git a/exchanges/coinbene/coinbene_test.go b/exchanges/coinbene/coinbene_test.go index 34e38a98..5c77b5f1 100644 --- a/exchanges/coinbene/coinbene_test.go +++ b/exchanges/coinbene/coinbene_test.go @@ -7,6 +7,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // Please supply your own keys here for due diligence testing @@ -14,8 +15,7 @@ const ( testAPIKey = "" testAPISecret = "" canManipulateRealOrders = false - - btcusdt = "BTC/USDT" + btcusdt = "BTC/USDT" ) var c Coinbene @@ -23,42 +23,42 @@ var c Coinbene func TestMain(m *testing.M) { c.SetDefaults() cfg := config.GetConfig() - err := cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) if err != nil { - log.Fatalf("Test Failed - Coinbene Setup() init error:, %v", err) + log.Fatal(err) } coinbeneConfig, err := cfg.GetExchangeConfig("Coinbene") if err != nil { - log.Fatalf("Test Failed - Coinbene Setup() init error: %v", err) + log.Fatal(err) + } + coinbeneConfig.API.AuthenticatedWebsocketSupport = true + coinbeneConfig.API.AuthenticatedSupport = true + coinbeneConfig.API.Credentials.Secret = testAPISecret + coinbeneConfig.API.Credentials.Key = testAPIKey + + err = c.Setup(coinbeneConfig) + if err != nil { + log.Fatal(err) } - coinbeneConfig.Websocket = true - coinbeneConfig.AuthenticatedAPISupport = true - coinbeneConfig.APISecret = testAPISecret - coinbeneConfig.APIKey = testAPIKey - c.Setup(&coinbeneConfig) os.Exit(m.Run()) } func areTestAPIKeysSet() bool { - if c.APIKey != "" && c.APIKey != "Key" && - c.APISecret != "" && c.APISecret != "Secret" { - return true - } - return false + return c.AllowAuthenticatedRequest() } -func TestFetchTicker(t *testing.T) { +func TestGetTicker(t *testing.T) { t.Parallel() - _, err := c.FetchTicker(btcusdt) + _, err := c.GetTicker(btcusdt) if err != nil { t.Error(err) } } -func TestFetchOrderbooks(t *testing.T) { +func TestGetOrderbook(t *testing.T) { t.Parallel() - _, err := c.FetchOrderbooks(btcusdt, 100) + _, err := c.GetOrderbook(btcusdt, 100) if err != nil { t.Error(err) } @@ -104,7 +104,7 @@ func TestPlaceOrder(t *testing.T) { if !areTestAPIKeysSet() || !canManipulateRealOrders { t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") } - _, err := c.PlaceOrder(140, 1, btcusdt, "buy", "") + _, err := c.PlaceOrder(1, 1, btcusdt, order.Buy.Lower(), "") if err != nil { t.Error(err) } diff --git a/exchanges/coinbene/coinbene_websocket.go b/exchanges/coinbene/coinbene_websocket.go index 7f83df30..c1245a7c 100644 --- a/exchanges/coinbene/coinbene_websocket.go +++ b/exchanges/coinbene/coinbene_websocket.go @@ -1,6 +1,7 @@ package coinbene import ( + "encoding/json" "errors" "fmt" "net/http" @@ -9,9 +10,10 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" @@ -50,12 +52,13 @@ func (c *Coinbene) WsConnect() error { func (c *Coinbene) GenerateDefaultSubscriptions() { var channels = []string{"orderBook.%s.100", "tradeList.%s", "ticker.%s", "kline.%s"} var subscriptions []wshandler.WebsocketChannelSubscription + pairs := c.GetEnabledPairs(asset.PerpetualSwap) for x := range channels { - for y := range c.EnabledPairs { - c.EnabledPairs[y].Delimiter = "" + for y := range pairs { + pairs[y].Delimiter = "" subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: fmt.Sprintf(channels[x], c.EnabledPairs[y]), - Currency: c.EnabledPairs[y], + Channel: fmt.Sprintf(channels[x], pairs[y]), + Currency: pairs[y], }) } } @@ -99,7 +102,7 @@ func (c *Coinbene) WsDataHandler() { continue } var result map[string]interface{} - err = common.JSONDecode(stream.Raw, &result) + err = json.Unmarshal(stream.Raw, &result) if err != nil { c.Websocket.DataHandler <- err } @@ -124,25 +127,27 @@ func (c *Coinbene) WsDataHandler() { switch { case strings.Contains(result[topic].(string), "ticker"): var ticker WsTicker - err = common.JSONDecode(stream.Raw, &ticker) + err = json.Unmarshal(stream.Raw, &ticker) if err != nil { c.Websocket.DataHandler <- err continue } for x := range ticker.Data { c.Websocket.DataHandler <- wshandler.TickerData{ - Quantity: ticker.Data[x].Volume24h, - ClosePrice: ticker.Data[x].LastPrice, - HighPrice: ticker.Data[x].High24h, - LowPrice: ticker.Data[x].Low24h, - Pair: currency.NewPairFromString(ticker.Data[x].Symbol), - Exchange: c.Name, - AssetType: orderbook.Swap, + Volume: ticker.Data[x].Volume24h, + Last: ticker.Data[x].LastPrice, + High: ticker.Data[x].High24h, + Low: ticker.Data[x].Low24h, + Pair: currency.NewPairFromFormattedPairs(ticker.Data[x].Symbol, + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)), + Exchange: c.Name, + AssetType: asset.PerpetualSwap, } } case strings.Contains(result[topic].(string), "tradeList"): var tradeList WsTradeList - err = common.JSONDecode(stream.Raw, &tradeList) + err = json.Unmarshal(stream.Raw, &tradeList) if err != nil { c.Websocket.DataHandler <- err continue @@ -164,22 +169,29 @@ func (c *Coinbene) WsDataHandler() { c.Websocket.DataHandler <- err continue } + p := strings.Replace(tradeList.Topic, "tradeList.", "", 1) c.Websocket.DataHandler <- wshandler.TradeData{ - CurrencyPair: currency.NewPairFromString(strings.Replace(tradeList.Topic, "tradeList.", "", 1)), - Timestamp: t, - Price: price, - Amount: amount, - Exchange: c.Name, - AssetType: orderbook.Swap, - Side: tradeList.Data[0][1], + CurrencyPair: currency.NewPairFromFormattedPairs(p, + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)), + Timestamp: t, + Price: price, + Amount: amount, + Exchange: c.Name, + AssetType: asset.PerpetualSwap, + Side: tradeList.Data[0][1], } case strings.Contains(result[topic].(string), "orderBook"): var orderBook WsOrderbook - err = common.JSONDecode(stream.Raw, &orderBook) + err = json.Unmarshal(stream.Raw, &orderBook) if err != nil { c.Websocket.DataHandler <- err continue } + p := strings.Replace(orderBook.Topic, "tradeList.", "", 1) + cp := currency.NewPairFromFormattedPairs(p, + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)) var amount, price float64 var asks, bids []orderbook.Item for i := range orderBook.Data[0].Asks { @@ -218,33 +230,33 @@ func (c *Coinbene) WsDataHandler() { var newOB orderbook.Base newOB.Asks = asks newOB.Bids = bids - newOB.AssetType = orderbook.Swap - newOB.Pair = currency.NewPairFromString(strings.Replace(orderBook.Topic, "tradeList.", "", 1)) + newOB.AssetType = asset.PerpetualSwap + newOB.Pair = cp newOB.ExchangeName = c.Name - err = c.Websocket.Orderbook.LoadSnapshot(&newOB, true) + err = c.Websocket.Orderbook.LoadSnapshot(&newOB) if err != nil { c.Websocket.DataHandler <- err continue } c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, - Asset: orderbook.Swap, + Asset: asset.PerpetualSwap, Exchange: c.Name, } } else if orderBook.Action == "update" { newOB := wsorderbook.WebsocketOrderbookUpdate{ - Asks: asks, - Bids: bids, - AssetType: orderbook.Swap, - CurrencyPair: currency.NewPairFromString(strings.Replace(orderBook.Topic, "tradeList.", "", 1)), - UpdateID: orderBook.Version, + Asks: asks, + Bids: bids, + Asset: asset.PerpetualSwap, + Pair: cp, + UpdateID: orderBook.Version, } err = c.Websocket.Orderbook.Update(&newOB) if err != nil { c.Websocket.DataHandler <- err continue } - c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.CurrencyPair, - Asset: orderbook.Swap, + c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, + Asset: asset.PerpetualSwap, Exchange: c.Name, } } @@ -252,7 +264,7 @@ func (c *Coinbene) WsDataHandler() { var kline WsKline var tempFloat float64 var tempKline []float64 - err = common.JSONDecode(stream.Raw, &kline) + err = json.Unmarshal(stream.Raw, &kline) if err != nil { c.Websocket.DataHandler <- err continue @@ -265,10 +277,13 @@ func (c *Coinbene) WsDataHandler() { } tempKline = append(tempKline, tempFloat) } + p := currency.NewPairFromFormattedPairs(kline.Data[0][0].(string), + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)) c.Websocket.DataHandler <- wshandler.KlineData{ Timestamp: time.Unix(int64(kline.Data[0][1].(float64)), 0), - Pair: currency.NewPairFromString(kline.Data[0][0].(string)), - AssetType: orderbook.Swap, + Pair: p, + AssetType: asset.PerpetualSwap, Exchange: c.Name, OpenPrice: tempKline[0], ClosePrice: tempKline[1], @@ -278,7 +293,7 @@ func (c *Coinbene) WsDataHandler() { } case strings.Contains(result[topic].(string), "user.account"): var userinfo WsUserInfo - err = common.JSONDecode(stream.Raw, &userinfo) + err = json.Unmarshal(stream.Raw, &userinfo) if err != nil { c.Websocket.DataHandler <- err continue @@ -286,7 +301,7 @@ func (c *Coinbene) WsDataHandler() { c.Websocket.DataHandler <- userinfo case strings.Contains(result[topic].(string), "user.position"): var position WsPosition - err = common.JSONDecode(stream.Raw, &position) + err = json.Unmarshal(stream.Raw, &position) if err != nil { c.Websocket.DataHandler <- err continue @@ -294,7 +309,7 @@ func (c *Coinbene) WsDataHandler() { c.Websocket.DataHandler <- position case strings.Contains(result[topic].(string), "user.order"): var orders WsUserOrders - err = common.JSONDecode(stream.Raw, &orders) + err = json.Unmarshal(stream.Raw, &orders) if err != nil { c.Websocket.DataHandler <- err continue @@ -328,9 +343,11 @@ func (c *Coinbene) Login() error { var sub WsSub expTime := time.Now().Add(time.Minute * 10).Format("2006-01-02T15:04:05Z") signMsg := expTime + http.MethodGet + "/login" - tempSign := common.GetHMAC(common.HashSHA256, []byte(signMsg), []byte(c.APISecret)) - sign := common.HexEncodeToString(tempSign) + tempSign := crypto.GetHMAC(crypto.HashSHA256, + []byte(signMsg), + []byte(c.API.Credentials.Secret)) + sign := crypto.HexEncodeToString(tempSign) sub.Operation = "login" - sub.Arguments = []string{c.APIKey, expTime, sign} + sub.Arguments = []string{c.API.Credentials.Key, expTime, sign} return c.WebsocketConn.SendMessage(sub) } diff --git a/exchanges/coinbene/coinbene_wrapper.go b/exchanges/coinbene/coinbene_wrapper.go index 90fd1c92..373222f1 100644 --- a/exchanges/coinbene/coinbene_wrapper.go +++ b/exchanges/coinbene/coinbene_wrapper.go @@ -3,18 +3,177 @@ package coinbene import ( "fmt" "strconv" + "strings" "sync" "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (c *Coinbene) GetDefaultConfig() (*config.ExchangeConfig, error) { + c.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = c.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = c.BaseCurrencies + + err := c.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if c.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = c.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for Coinbene +func (c *Coinbene) SetDefaults() { + c.Name = "Coinbene" + c.Enabled = true + c.Verbose = true + c.API.CredentialsValidator.RequiresKey = true + c.API.CredentialsValidator.RequiresSecret = true + + c.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "/", + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "/", + }, + } + + c.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: false, // Purposely disabled until SWAP is supported + RESTCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AccountBalance: true, + AutoPairUpdates: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + CancelOrders: true, + SubmitOrder: true, + TradeFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + AccountBalance: true, + AccountInfo: true, + OrderbookFetching: true, + TradeFetching: true, + KlineFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + }, + WithdrawPermissions: exchange.NoFiatWithdrawals | + exchange.WithdrawCryptoViaWebsiteOnly, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + c.Requester = request.New(c.Name, + request.NewRateLimit(time.Minute, authRateLimit), + request.NewRateLimit(time.Second, unauthRateLimit), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + c.API.Endpoints.URLDefault = coinbeneAPIURL + c.API.Endpoints.URL = c.API.Endpoints.URLDefault + c.API.Endpoints.WebsocketURL = coinbeneWsURL + c.Websocket = wshandler.New() + c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + c.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup takes in the supplied exchange configuration details and sets params +func (c *Coinbene) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + c.SetEnabled(false) + return nil + } + + err := c.SetupDefaults(exch) + if err != nil { + return err + } + + // TO-DO: Remove this once SWAP is supported + if exch.Features.Enabled.Websocket { + log.Warnf(log.ExchangeSys, + "%s websocket only supports SWAP which GoCryptoTrader currently "+ + "does not. Disabling.\n", + c.Name) + exch.Features.Enabled.Websocket = false + } + + err = c.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: coinbeneWsURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: c.WsConnect, + Subscriber: c.Subscribe, + UnSubscriber: c.Unsubscribe, + }) + if err != nil { + return err + } + + c.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: c.Name, + URL: c.Websocket.GetWebsocketURL(), + ProxyURL: c.Websocket.GetProxyAddress(), + Verbose: c.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + c.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + true, + true, + false, + false, + exch.Name) + + return nil +} + // Start starts the Coinbene go routine func (c *Coinbene) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -27,35 +186,63 @@ func (c *Coinbene) Start(wg *sync.WaitGroup) { // Run implements the Coinbene wrapper func (c *Coinbene) Run() { if c.Verbose { - log.Debugf("%s Websocket: %s. (url: %s).\n", c.Name, common.IsEnabled(c.Websocket.IsEnabled()), c.Websocket.GetWebsocketURL()) - log.Debugf("%s polling delay: %ds.\n", c.Name, c.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", c.Name, len(c.EnabledPairs), c.EnabledPairs) + log.Debugf(log.ExchangeSys, + "%s Websocket: %s. (url: %s).\n", + c.Name, + common.IsEnabled(c.Websocket.IsEnabled()), + c.Websocket.GetWebsocketURL(), + ) + c.PrintEnabledPairs() } - exchangeCurrencies, err := c.GetAllPairs() + + if !c.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := c.UpdateTradablePairs(false) if err != nil { - log.Errorf("%s Failed to get available symbols.\n", c.Name) - } else { - var newExchangeCurrencies currency.Pairs - for p := range exchangeCurrencies.Data { - newExchangeCurrencies = append(newExchangeCurrencies, - currency.NewPairFromString(exchangeCurrencies.Data[p].Symbol)) - } - err = c.UpdateCurrencies(newExchangeCurrencies, false, false) - if err != nil { - log.Errorf("%s Failed to update available currencies %s.\n", - c.Name, - err) - } + log.Errorf(log.ExchangeSys, + "%s Failed to update tradable pairs. Error: %s", + c.Name, + err) } } +// FetchTradablePairs returns a list of exchange tradable pairs +func (c *Coinbene) FetchTradablePairs(a asset.Item) ([]string, error) { + pairs, err := c.GetAllPairs() + if err != nil { + return nil, err + } + + var currencies []string + for x := range pairs.Data { + currencies = append(currencies, pairs.Data[x].Symbol) + } + return currencies, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them +func (c *Coinbene) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := c.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return c.UpdatePairs(currency.NewPairsFromStrings(pairs), + asset.Spot, + false, + forceUpdate) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (c *Coinbene) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (c *Coinbene) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var resp ticker.Price - allPairs := c.GetEnabledCurrencies() + allPairs := c.GetEnabledPairs(assetType) for x := range allPairs { - tempResp, err := c.FetchTicker(exchange.FormatExchangeCurrency(c.Name, - allPairs[x]).String()) + tempResp, err := c.GetTicker(c.FormatExchangeCurrency(allPairs[x], + assetType).String()) if err != nil { return resp, err } @@ -75,8 +262,8 @@ func (c *Coinbene) UpdateTicker(p currency.Pair, assetType string) (ticker.Price return ticker.GetTicker(c.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (c *Coinbene) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { +// FetchTicker returns the ticker for a currency pair +func (c *Coinbene) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { tickerNew, err := ticker.GetTicker(c.Name, p, assetType) if err != nil { return c.UpdateTicker(p, assetType) @@ -84,8 +271,8 @@ func (c *Coinbene) GetTickerPrice(p currency.Pair, assetType string) (ticker.Pri return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (c *Coinbene) GetOrderbookEx(currency currency.Pair, assetType string) (orderbook.Base, error) { +// FetchOrderbook returns orderbook base on the currency pair +func (c *Coinbene) FetchOrderbook(currency currency.Pair, assetType asset.Item) (orderbook.Base, error) { ob, err := orderbook.Get(c.Name, currency, assetType) if err != nil { return c.UpdateOrderbook(currency, assetType) @@ -94,10 +281,12 @@ func (c *Coinbene) GetOrderbookEx(currency currency.Pair, assetType string) (ord } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (c *Coinbene) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (c *Coinbene) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var resp orderbook.Base - strPair := exchange.FormatExchangeCurrency(c.Name, p).String() - tempResp, err := c.FetchOrderbooks(strPair, 100) + tempResp, err := c.GetOrderbook( + c.FormatExchangeCurrency(p, assetType).String(), + 100, + ) if err != nil { return resp, err } @@ -168,22 +357,31 @@ func (c *Coinbene) GetFundingHistory() ([]exchange.FundHistory, error) { } // GetExchangeHistory returns historic trade data since exchange opening. -func (c *Coinbene) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { +func (c *Coinbene) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { return nil, common.ErrFunctionNotSupported } // SubmitOrder submits a new order -func (c *Coinbene) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (exchange.SubmitOrderResponse, error) { - var resp exchange.SubmitOrderResponse - if side != exchange.BuyOrderSide && side != exchange.SellOrderSide { - return resp, - fmt.Errorf("%s orderside is not supported by this exchange", side) +func (c *Coinbene) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var resp order.SubmitResponse + if err := s.Validate(); err != nil { + return resp, err } - tempResp, err := c.PlaceOrder(price, - amount, - exchange.FormatExchangeCurrency(c.Name, p).String(), - orderType.ToString(), - clientID) + + if s.OrderSide != order.Buy && s.OrderSide != order.Sell { + return resp, + fmt.Errorf("%s orderside is not supported by this exchange", + s.OrderSide) + } + + if s.OrderType != order.Limit { + return resp, fmt.Errorf("only limit order is supported by this exchange") + } + tempResp, err := c.PlaceOrder(s.Price, + s.Amount, + c.FormatExchangeCurrency(s.Pair, asset.Spot).String(), + s.OrderType.String(), + s.ClientID) if err != nil { return resp, err } @@ -194,22 +392,24 @@ func (c *Coinbene) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderTy // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (c *Coinbene) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (c *Coinbene) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (c *Coinbene) CancelOrder(order *exchange.OrderCancellation) error { +func (c *Coinbene) CancelOrder(order *order.Cancel) error { _, err := c.RemoveOrder(order.OrderID) return err } // CancelAllOrders cancels all orders associated with a currency pair -func (c *Coinbene) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - var resp exchange.CancelAllOrdersResponse +func (c *Coinbene) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) { + var resp order.CancelAllResponse tempMap := make(map[string]string) - orders, err := c.FetchOpenOrders(exchange.FormatExchangeCurrency(c.Name, - orderCancellation.CurrencyPair).String()) + orders, err := c.FetchOpenOrders( + c.FormatExchangeCurrency(orderCancellation.CurrencyPair, + asset.Spot).String(), + ) if err != nil { return resp, err } @@ -221,13 +421,13 @@ func (c *Coinbene) CancelAllOrders(orderCancellation *exchange.OrderCancellation tempMap[orders.OpenOrders[x].OrderID] = "Success" } } - resp.OrderStatus = tempMap + resp.Status = tempMap return resp, nil } // GetOrderInfo returns information on a current open order -func (c *Coinbene) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var resp exchange.OrderDetail +func (c *Coinbene) GetOrderInfo(orderID string) (order.Detail, error) { + var resp order.Detail tempResp, err := c.FetchOrderInfo(orderID) if err != nil { return resp, err @@ -256,19 +456,19 @@ func (c *Coinbene) GetDepositAddress(cryptocurrency currency.Code, accountID str // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (c *Coinbene) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (c *Coinbene) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is // submitted -func (c *Coinbene) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (c *Coinbene) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted -func (c *Coinbene) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (c *Coinbene) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -278,9 +478,9 @@ func (c *Coinbene) GetWebsocket() (*wshandler.Websocket, error) { } // GetActiveOrders retrieves any orders that are active/open -func (c *Coinbene) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var resp []exchange.OrderDetail - var tempResp exchange.OrderDetail +func (c *Coinbene) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { + var resp []order.Detail + var tempResp order.Detail var tempData OpenOrderResponse if len(getOrdersRequest.Currencies) == 0 { allPairs, err := c.GetAllPairs() @@ -293,7 +493,11 @@ func (c *Coinbene) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) } var err error for x := range getOrdersRequest.Currencies { - tempData, err = c.FetchOpenOrders(exchange.FormatExchangeCurrency(c.Name, getOrdersRequest.Currencies[x]).String()) + tempData, err = c.FetchOpenOrders( + c.FormatExchangeCurrency( + getOrdersRequest.Currencies[x], + asset.Spot).String(), + ) if err != nil { return resp, err } @@ -301,16 +505,16 @@ func (c *Coinbene) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) for y := range tempData.OpenOrders { tempResp.Exchange = c.Name tempResp.CurrencyPair = getOrdersRequest.Currencies[x] - tempResp.OrderSide = buy - if tempData.OpenOrders[y].OrderType == sell { - tempResp.OrderSide = sell + tempResp.OrderSide = order.Buy + if strings.EqualFold(tempData.OpenOrders[y].OrderType, order.Sell.String()) { + tempResp.OrderSide = order.Sell } t, err = time.Parse(time.RFC3339, tempData.OpenOrders[y].OrderTime) if err != nil { return resp, err } tempResp.OrderDate = t - tempResp.Status = tempData.OpenOrders[y].OrderStatus + tempResp.Status = order.Status(tempData.OpenOrders[y].OrderStatus) tempResp.Price = tempData.OpenOrders[y].OrderPrice tempResp.Amount = tempData.OpenOrders[y].Amount tempResp.ExecutedAmount = tempData.OpenOrders[y].FilledAmount @@ -324,9 +528,9 @@ func (c *Coinbene) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (c *Coinbene) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var resp []exchange.OrderDetail - var tempResp exchange.OrderDetail +func (c *Coinbene) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { + var resp []order.Detail + var tempResp order.Detail var tempData ClosedOrderResponse if len(getOrdersRequest.Currencies) == 0 { allPairs, err := c.GetAllPairs() @@ -339,7 +543,12 @@ func (c *Coinbene) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) } var err error for x := range getOrdersRequest.Currencies { - tempData, err = c.FetchClosedOrders(exchange.FormatExchangeCurrency(c.Name, getOrdersRequest.Currencies[x]).String(), "") + tempData, err = c.FetchClosedOrders( + c.FormatExchangeCurrency( + getOrdersRequest.Currencies[x], + asset.Spot).String(), + "", + ) if err != nil { return resp, err } @@ -347,16 +556,16 @@ func (c *Coinbene) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) for y := range tempData.Data { tempResp.Exchange = c.Name tempResp.CurrencyPair = getOrdersRequest.Currencies[x] - tempResp.OrderSide = exchange.BuyOrderSide - if tempData.Data[y].OrderType == sell { - tempResp.OrderSide = exchange.SellOrderSide + tempResp.OrderSide = order.Buy + if strings.EqualFold(tempData.Data[y].OrderType, order.Sell.String()) { + tempResp.OrderSide = order.Sell } t, err = time.Parse(time.RFC3339, tempData.Data[y].OrderTime) if err != nil { return resp, err } tempResp.OrderDate = t - tempResp.Status = tempData.Data[y].OrderStatus + tempResp.Status = order.Status(tempData.Data[y].OrderStatus) tempResp.Price = tempData.Data[y].OrderPrice tempResp.Amount = tempData.Data[y].Amount tempResp.ExecutedAmount = tempData.Data[y].FilledAmount @@ -371,7 +580,10 @@ func (c *Coinbene) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) // GetFeeByType returns an estimate of fee based on the type of transaction func (c *Coinbene) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { var fee float64 - tempData, err := c.GetPairInfo(exchange.FormatExchangeCurrency(c.Name, feeBuilder.Pair).String()) + tempData, err := c.GetPairInfo( + c.FormatExchangeCurrency( + feeBuilder.Pair, asset.Spot).String(), + ) if err != nil { return fee, err } diff --git a/exchanges/coinut/README.md b/exchanges/coinut/README.md index 28977f0e..dc2cc3ce 100644 --- a/exchanges/coinut/README.md +++ b/exchanges/coinut/README.md @@ -48,22 +48,22 @@ main.go ```go var c exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Coinut" { - c = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Coinut" { + c = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := c.GetTickerPrice() +tick, err := c.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := c.GetOrderbookEx() +ob, err := c.FetchOrderbook() if err != nil { // Handle error } @@ -138,4 +138,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/coinut/coinut.go b/exchanges/coinut/coinut.go index 270ae1ce..7cb7157e 100644 --- a/exchanges/coinut/coinut.go +++ b/exchanges/coinut/coinut.go @@ -6,15 +6,14 @@ import ( "errors" "fmt" "net/http" - "time" + "strconv" + "strings" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -44,141 +43,49 @@ const ( coinutStatusOK = "OK" ) +var ( + errLookupInstrumentID = errors.New("unable to lookup instrument ID") + errLookupInstrumentCurrency = errors.New("unable to lookup instrument") +) + // COINUT is the overarching type across the coinut package type COINUT struct { exchange.Base WebsocketConn *wshandler.WebsocketConnection - InstrumentMap map[string]int + instrumentMap instrumentMap } -// SetDefaults sets current default values -func (c *COINUT) SetDefaults() { - c.Name = "COINUT" - c.Enabled = false - c.Verbose = false - c.TakerFee = 0.1 // spot - c.MakerFee = 0 - c.Verbose = false - c.RESTPollingDelay = 10 - c.APIWithdrawPermissions = exchange.WithdrawCryptoViaWebsiteOnly | - exchange.WithdrawFiatViaWebsiteOnly - c.RequestCurrencyPairFormat.Delimiter = "" - c.RequestCurrencyPairFormat.Uppercase = true - c.ConfigCurrencyPairFormat.Delimiter = "" - c.ConfigCurrencyPairFormat.Uppercase = true - c.AssetTypes = []string{ticker.Spot} - c.SupportsAutoPairUpdating = true - c.SupportsRESTTickerBatching = false - c.Requester = request.New(c.Name, - request.NewRateLimit(time.Second, coinutAuthRate), - request.NewRateLimit(time.Second, coinutUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - c.APIUrlDefault = coinutAPIURL - c.APIUrl = c.APIUrlDefault - c.Websocket = wshandler.New() - c.Websocket.Functionality = wshandler.WebsocketTickerSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported | - wshandler.WebsocketSubmitOrderSupported | - wshandler.WebsocketCancelOrderSupported | - wshandler.WebsocketMessageCorrelationSupported - c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - c.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup sets the current exchange configuration -func (c *COINUT) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - c.SetEnabled(false) - } else { - c.Enabled = true - c.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - c.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - c.SetAPIKeys(exch.APIKey, exch.APISecret, exch.ClientID, false) - c.SetHTTPClientTimeout(exch.HTTPTimeout) - c.SetHTTPClientUserAgent(exch.HTTPUserAgent) - c.RESTPollingDelay = exch.RESTPollingDelay - c.Verbose = exch.Verbose - c.HTTPDebugging = exch.HTTPDebugging - c.Websocket.SetWsStatusAndConnection(exch.Websocket) - c.BaseCurrencies = exch.BaseCurrencies - c.AvailablePairs = exch.AvailablePairs - c.EnabledPairs = exch.EnabledPairs - err := c.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = c.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = c.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = c.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = c.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = c.Websocket.Setup(c.WsConnect, - c.Subscribe, - c.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - coinutWebsocketURL, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - c.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: c.Name, - URL: c.Websocket.GetWebsocketURL(), - ProxyURL: c.Websocket.GetProxyAddress(), - Verbose: c.Verbose, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - RateLimit: coinutWebsocketRateLimit, - } - c.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - true, - true, - false, - exch.Name) +// SeedInstruments seeds the instrument map +func (c *COINUT) SeedInstruments() error { + i, err := c.GetInstruments() + if err != nil { + return err } + + for _, y := range i.Instruments { + c.instrumentMap.Seed(y[0].Base+y[0].Quote, y[0].InstID) + } + return nil } // GetInstruments returns instruments func (c *COINUT) GetInstruments() (Instruments, error) { var result Instruments params := make(map[string]interface{}) - params["sec_type"] = orderbook.Spot - + params["sec_type"] = strings.ToUpper(asset.Spot.String()) return result, c.SendHTTPRequest(coinutInstruments, params, false, &result) } // GetInstrumentTicker returns a ticker for a specific instrument -func (c *COINUT) GetInstrumentTicker(instrumentID int) (Ticker, error) { +func (c *COINUT) GetInstrumentTicker(instrumentID int64) (Ticker, error) { var result Ticker params := make(map[string]interface{}) params["inst_id"] = instrumentID - return result, c.SendHTTPRequest(coinutTicker, params, false, &result) } // GetInstrumentOrderbook returns the orderbooks for a specific instrument -func (c *COINUT) GetInstrumentOrderbook(instrumentID, limit int) (Orderbook, error) { +func (c *COINUT) GetInstrumentOrderbook(instrumentID, limit int64) (Orderbook, error) { var result Orderbook params := make(map[string]interface{}) params["inst_id"] = instrumentID @@ -199,38 +106,27 @@ func (c *COINUT) GetTrades(instrumentID int) (Trades, error) { } // GetUserBalance returns the full user balance -func (c *COINUT) GetUserBalance() (UserBalance, error) { - result := UserBalance{} - +func (c *COINUT) GetUserBalance() (*UserBalance, error) { + var result *UserBalance return result, c.SendHTTPRequest(coinutBalance, nil, true, &result) } // NewOrder places a new order on the exchange -func (c *COINUT) NewOrder(instrumentID int, quantity, price float64, buy bool, orderID uint32) (interface{}, error) { +func (c *COINUT) NewOrder(instrumentID int64, quantity, price float64, buy bool, orderID uint32) (interface{}, error) { var result interface{} params := make(map[string]interface{}) params["inst_id"] = instrumentID if price > 0 { - params["price"] = fmt.Sprintf("%v", price) + params["price"] = strconv.FormatFloat(price, 'f', -1, 64) } - params["qty"] = fmt.Sprintf("%v", quantity) - params["side"] = "BUY" + params["qty"] = strconv.FormatFloat(quantity, 'f', -1, 64) + params["side"] = order.Buy.String() if !buy { - params["side"] = "SELL" + params["side"] = order.Sell.String() } params["client_ord_id"] = orderID - err := c.SendHTTPRequest(coinutOrder, params, true, &result) - if _, ok := result.(OrderRejectResponse); ok { - return result.(OrderRejectResponse), err - } - if _, ok := result.(OrderFilledResponse); ok { - return result.(OrderFilledResponse), err - } - if _, ok := result.(OrdersBase); ok { - return result.(OrdersBase), err - } - return result, err + return result, c.SendHTTPRequest(coinutOrder, params, true, &result) } // NewOrders places multiple orders on the exchange @@ -243,21 +139,20 @@ func (c *COINUT) NewOrders(orders []Order) ([]OrdersBase, error) { } // GetOpenOrders returns a list of open order and relevant information -func (c *COINUT) GetOpenOrders(instrumentID int) (GetOpenOrdersResponse, error) { +func (c *COINUT) GetOpenOrders(instrumentID int64) (GetOpenOrdersResponse, error) { var result GetOpenOrdersResponse params := make(map[string]interface{}) params["inst_id"] = instrumentID - return result, c.SendHTTPRequest(coinutOrdersOpen, params, true, &result) } // CancelExistingOrder cancels a specific order and returns if it was actioned -func (c *COINUT) CancelExistingOrder(instrumentID, orderID int) (bool, error) { +func (c *COINUT) CancelExistingOrder(instrumentID, orderID int64) (bool, error) { var result GenericResponse params := make(map[string]interface{}) type Request struct { - InstrumentID int `json:"inst_id"` - OrderID int `json:"order_id"` + InstrumentID int64 `json:"inst_id"` + OrderID int64 `json:"order_id"` } var entry = Request{ @@ -292,7 +187,7 @@ func (c *COINUT) CancelOrders(orders []CancelOrders) (CancelOrdersResponse, erro } // GetTradeHistory returns trade history for a specific instrument. -func (c *COINUT) GetTradeHistory(instrumentID, start, limit int) (TradeHistory, error) { +func (c *COINUT) GetTradeHistory(instrumentID, start, limit int64) (TradeHistory, error) { var result TradeHistory params := make(map[string]interface{}) params["inst_id"] = instrumentID @@ -366,7 +261,7 @@ func (c *COINUT) GetOpenPositions(instrumentID int) ([]OpenPosition, error) { // SendHTTPRequest sends either an authenticated or unauthenticated HTTP request func (c *COINUT) SendHTTPRequest(apiRequest string, params map[string]interface{}, authenticated bool, result interface{}) (err error) { - if !c.AuthenticatedAPISupport && authenticated { + if !c.API.AuthenticatedSupport && authenticated { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, c.Name) } @@ -378,26 +273,26 @@ func (c *COINUT) SendHTTPRequest(apiRequest string, params map[string]interface{ params["nonce"] = n params["request"] = apiRequest - payload, err := common.JSONEncode(params) + payload, err := json.Marshal(params) if err != nil { return errors.New("sendHTTPRequest: Unable to JSON request") } if c.Verbose { - log.Debugf("Request JSON: %s", payload) + log.Debugf(log.ExchangeSys, "Request JSON: %s", payload) } headers := make(map[string]string) if authenticated { - headers["X-USER"] = c.ClientID - hmac := common.GetHMAC(common.HashSHA256, payload, []byte(c.APIKey)) - headers["X-SIGNATURE"] = common.HexEncodeToString(hmac) + headers["X-USER"] = c.API.Credentials.ClientID + hmac := crypto.GetHMAC(crypto.HashSHA256, payload, []byte(c.API.Credentials.Key)) + headers["X-SIGNATURE"] = crypto.HexEncodeToString(hmac) } headers["Content-Type"] = "application/json" var rawMsg json.RawMessage err = c.SendPayload(http.MethodPost, - c.APIUrl, + c.API.Endpoints.URL, headers, bytes.NewBuffer(payload), &rawMsg, @@ -411,7 +306,7 @@ func (c *COINUT) SendHTTPRequest(apiRequest string, params map[string]interface{ } var genResp GenericResponse - err = common.JSONDecode(rawMsg, &genResp) + err = json.Unmarshal(rawMsg, &genResp) if err != nil { return err } @@ -421,7 +316,7 @@ func (c *COINUT) SendHTTPRequest(apiRequest string, params map[string]interface{ genResp.Status[0]) } - return common.JSONDecode(rawMsg, result) + return json.Unmarshal(rawMsg, result) } // GetFee returns an estimate of fee based on type of transaction @@ -520,3 +415,77 @@ func getInternationalBankDepositFee(c currency.Code, amount float64) float64 { return fee } + +// IsLoaded returns whether or not the instrument map has been seeded +func (i *instrumentMap) IsLoaded() bool { + i.m.Lock() + defer i.m.Unlock() + return i.Loaded +} + +// Seed seeds the instrument map +func (i *instrumentMap) Seed(curr string, id int64) { + i.m.Lock() + defer i.m.Unlock() + + if !i.Loaded { + i.Instruments = make(map[string]int64) + } + + // check to see if the instrument already exists + _, ok := i.Instruments[curr] + if ok { + return + } + + i.Instruments[curr] = id + i.Loaded = true +} + +// LookupInstrument looks up an instrument based on an id +func (i *instrumentMap) LookupInstrument(id int64) string { + i.m.Lock() + defer i.m.Unlock() + + if !i.Loaded { + return "" + } + + for k, v := range i.Instruments { + if v == id { + return k + } + } + return "" +} + +// LookupID looks up an ID based on a string +func (i *instrumentMap) LookupID(curr string) int64 { + i.m.Lock() + defer i.m.Unlock() + + if !i.Loaded { + return 0 + } + + if ic, ok := i.Instruments[curr]; ok { + return ic + } + return 0 +} + +// GetInstrumentIDs returns a list of IDs +func (i *instrumentMap) GetInstrumentIDs() []int64 { + i.m.Lock() + defer i.m.Unlock() + + if !i.Loaded { + return nil + } + + var instruments []int64 + for _, x := range i.Instruments { + instruments = append(instruments, x) + } + return instruments +} diff --git a/exchanges/coinut/coinut_test.go b/exchanges/coinut/coinut_test.go index 7d1d8506..c3616708 100644 --- a/exchanges/coinut/coinut_test.go +++ b/exchanges/coinut/coinut_test.go @@ -1,15 +1,17 @@ package coinut import ( + "log" "net/http" + "os" "testing" - "time" "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -24,40 +26,42 @@ const ( canManipulateRealOrders = false ) -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { c.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Coinut load config error", err) + } bConfig, err := cfg.GetExchangeConfig("COINUT") if err != nil { - t.Error("Test Failed - Coinut Setup() init error") + log.Fatal("Coinut Setup() init error") + } + bConfig.API.AuthenticatedSupport = true + bConfig.API.AuthenticatedWebsocketSupport = true + bConfig.API.Credentials.Key = apiKey + bConfig.API.Credentials.ClientID = clientID + err = c.Setup(bConfig) + if err != nil { + log.Fatal("Coinut setup error", err) } - bConfig.AuthenticatedAPISupport = true - bConfig.AuthenticatedWebsocketAPISupport = true - bConfig.APIKey = apiKey - c.Setup(&bConfig) - c.ClientID = clientID - if !c.IsEnabled() || - c.RESTPollingDelay != time.Duration(10) || - c.Websocket.IsEnabled() || len(c.BaseCurrencies) < 1 || - len(c.AvailablePairs) < 1 || len(c.EnabledPairs) < 1 { - t.Error("Test Failed - Coinut Setup values not set correctly") - } + c.SeedInstruments() + + os.Exit(m.Run()) } func setupWSTestAuth(t *testing.T) { if wsSetupRan { return } - c.SetDefaults() - TestSetup(t) - if !c.Websocket.IsEnabled() && !c.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + + if !c.Websocket.IsEnabled() && !c.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } + if areTestAPIKeysSet() { + c.Websocket.SetCanUseAuthenticatedEndpoints(true) + } c.WebsocketConn = &wshandler.WebsocketConnection{ ExchangeName: c.Name, URL: coinutWebsocketURL, @@ -66,6 +70,7 @@ func setupWSTestAuth(t *testing.T) { ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } + var dialer websocket.Dialer err := c.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { @@ -78,16 +83,29 @@ func setupWSTestAuth(t *testing.T) { if err != nil { t.Error(err) } - - instrumentListByString = make(map[string]int64) - instrumentListByString[currency.NewPair(currency.LTC, currency.BTC).String()] = 1 wsSetupRan = true + _, err = c.WsGetInstruments() + if err != nil { + t.Error(err) + } } func TestGetInstruments(t *testing.T) { _, err := c.GetInstruments() if err != nil { - t.Error("Test failed - GetInstruments() error", err) + t.Error("GetInstruments() error", err) + } +} + +func TestSeedInstruments(t *testing.T) { + err := c.SeedInstruments() + if err != nil { + // No point checking the next condition + t.Fatal(err) + } + + if len(c.instrumentMap.GetInstrumentIDs()) == 0 { + t.Error("instrument map hasn't been seeded") } } @@ -116,16 +134,12 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - c.SetDefaults() - TestSetup(t) t.Parallel() - var feeBuilder = setFeeBuilder() - // CryptocurrencyTradeFee Basic if resp, err := c.GetFee(feeBuilder); resp != float64(0.001) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0010), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0010), resp) } // CryptocurrencyTradeFee High quantity @@ -133,7 +147,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := c.GetFee(feeBuilder); resp != float64(1000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(1000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(1000), resp) t.Error(err) } @@ -141,7 +155,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -149,7 +163,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -157,7 +171,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -165,7 +179,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -174,7 +188,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.EUR if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -183,7 +197,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.USD if resp, err := c.GetFee(feeBuilder); resp != float64(10) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(10), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(10), resp) t.Error(err) } @@ -192,7 +206,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.SGD if resp, err := c.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -201,7 +215,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := c.GetFee(feeBuilder); resp != float64(10) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(10), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(10), resp) t.Error(err) } @@ -210,7 +224,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.CAD if resp, err := c.GetFee(feeBuilder); resp != float64(2) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2), resp) t.Error(err) } @@ -219,7 +233,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.SGD if resp, err := c.GetFee(feeBuilder); resp != float64(10) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(10), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(10), resp) t.Error(err) } @@ -228,44 +242,35 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.CAD if resp, err := c.GetFee(feeBuilder); resp != float64(2) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - c.SetDefaults() expectedResult := exchange.WithdrawCryptoViaWebsiteOnlyText + " & " + exchange.WithdrawFiatViaWebsiteOnlyText - withdrawPermissions := c.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - c.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } - _, err := c.GetActiveOrders(&getOrdersRequest) if areTestAPIKeysSet() && err != nil { t.Errorf("Could not get open orders: %s", err) } } -func TestGetOrderHistory(t *testing.T) { - c.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, +func TestGetOrderHistoryWrapper(t *testing.T) { + setupWSTestAuth(t) + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.BTC, - currency.LTC)}, + currency.USD)}, } _, err := c.GetOrderHistory(&getOrdersRequest) @@ -277,26 +282,26 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if c.APIKey != "" && c.APIKey != "Key" { - return true - } - return false + return c.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - c.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.BTC, - Quote: currency.USD, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "123", } - response, err := c.SubmitOrder(p, exchange.BuyOrderSide, exchange.LimitOrderType, 1, 10, "1234234") + response, err := c.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -305,16 +310,11 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - c.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + currencyPair := currency.NewPair(currency.BTC, currency.USD) + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -331,16 +331,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - c.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -356,8 +352,8 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } @@ -365,31 +361,34 @@ func TestGetAccountInfo(t *testing.T) { if apiKey != "" || clientID != "" { _, err := c.GetAccountInfo() if err != nil { - t.Error("Test Failed - GetAccountInfo() error", err) + t.Error("GetAccountInfo() error", err) } } else { _, err := c.GetAccountInfo() if err == nil { - t.Error("Test Failed - GetAccountInfo() error") + t.Error("GetAccountInfo() Expected error") } } } func TestModifyOrder(t *testing.T) { - _, err := c.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := c.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - c.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.LTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -403,15 +402,11 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - c.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := c.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -419,15 +414,11 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - c.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := c.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -437,7 +428,7 @@ func TestWithdrawInternationalBank(t *testing.T) { func TestGetDepositAddress(t *testing.T) { _, err := c.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() function unsupported cannot be nil") + t.Error("GetDepositAddress() function unsupported cannot be nil") } } @@ -456,14 +447,14 @@ func TestWsAuthSubmitOrder(t *testing.T) { if !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - order := WsSubmitOrderParameters{ + ord := WsSubmitOrderParameters{ Amount: 1, Currency: currency.NewPair(currency.LTC, currency.BTC), OrderID: 1, Price: 1, - Side: exchange.BuyOrderSide, + Side: order.Buy, } - _, err := c.wsSubmitOrder(&order) + _, err := c.wsSubmitOrder(&ord) if err != nil { t.Error(err) } @@ -480,14 +471,14 @@ func TestWsAuthSubmitOrders(t *testing.T) { Currency: currency.NewPair(currency.LTC, currency.BTC), OrderID: 1, Price: 1, - Side: exchange.BuyOrderSide, + Side: order.Buy, } order2 := WsSubmitOrderParameters{ Amount: 3, Currency: currency.NewPair(currency.LTC, currency.BTC), OrderID: 2, Price: 2, - Side: exchange.BuyOrderSide, + Side: order.Buy, } _, err := c.wsSubmitOrders([]WsSubmitOrderParameters{order1, order2}) if err != nil { @@ -496,12 +487,13 @@ func TestWsAuthSubmitOrders(t *testing.T) { } // TestWsAuthCancelOrders dials websocket, cancels orders +// doesn't care about if the order cancellations fail func TestWsAuthCancelOrders(t *testing.T) { setupWSTestAuth(t) if !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - order := WsCancelOrderParameters{ + ord := WsCancelOrderParameters{ Currency: currency.NewPair(currency.LTC, currency.BTC), OrderID: 1, } @@ -509,9 +501,28 @@ func TestWsAuthCancelOrders(t *testing.T) { Currency: currency.NewPair(currency.LTC, currency.BTC), OrderID: 2, } - _, errs := c.wsCancelOrders([]WsCancelOrderParameters{order, order2}) - if len(errs) > 0 { - t.Error(errs) + resp, err := c.wsCancelOrders([]WsCancelOrderParameters{ord, order2}) + if err != nil { + t.Error(err) + } + if resp.Status[0] != "OK" { + t.Error("Order failed to cancel") + } +} + +// TestWsAuthCancelOrders dials websocket, cancels orders +// Checks that the wrapper oversight works +func TestWsAuthCancelOrdersWrapper(t *testing.T) { + setupWSTestAuth(t) + if !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + orderDetails := order.Cancel{ + CurrencyPair: currency.NewPair(currency.LTC, currency.BTC), + } + _, err := c.CancelAllOrders(&orderDetails) + if err != nil { + t.Error(err) } } @@ -521,21 +532,109 @@ func TestWsAuthCancelOrder(t *testing.T) { if !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - order := WsCancelOrderParameters{ + ord := &WsCancelOrderParameters{ Currency: currency.NewPair(currency.LTC, currency.BTC), OrderID: 1, } - err := c.wsCancelOrder(order) + resp, err := c.wsCancelOrder(ord) if err != nil { t.Error(err) } + if len(resp.Status) >= 1 && resp.Status[0] != "OK" { + t.Errorf("Failed to cancel order") + } } // TestWsAuthGetOpenOrders dials websocket, retrieves open orders func TestWsAuthGetOpenOrders(t *testing.T) { setupWSTestAuth(t) - err := c.wsGetOpenOrders(currency.NewPair(currency.LTC, currency.BTC)) + _, err := c.wsGetOpenOrders(currency.NewPair(currency.LTC, currency.BTC).String()) if err != nil { t.Error(err) } } + +func TestCurrencyMapIsLoaded(t *testing.T) { + t.Parallel() + var i instrumentMap + if l := i.IsLoaded(); l { + t.Error("unexpected result") + } + + i.Seed("BTCUSD", 1337) + if l := i.IsLoaded(); !l { + t.Error("unexpected result") + } +} + +func TestCurrencyMapSeed(t *testing.T) { + t.Parallel() + var i instrumentMap + // Test non-seeded lookups + if id := i.LookupInstrument(1234); id != "" { + t.Error("unexpected result") + } + if id := i.LookupID("BLAH"); id != 0 { + t.Error("unexpected result") + } + + // Test seeded lookups + i.Seed("BTCUSD", 1337) + if id := i.LookupID("BTCUSD"); id != 1337 { + t.Error("unexpected result") + } + if id := i.LookupInstrument(1337); id != "BTCUSD" { + t.Error("unexpected result") + } + + // Test invalid lookups + if id := i.LookupInstrument(1234); id != "" { + t.Error("unexpected result") + } + if id := i.LookupID("BLAH"); id != 0 { + t.Error("unexpected result") + } + + // Test seeding existing item + i.Seed("BTCUSD", 1234) + if id := i.LookupID("BTCUSD"); id != 1337 { + t.Error("unexpected result") + } + if id := i.LookupInstrument(1337); id != "BTCUSD" { + t.Error("unexpected result") + } +} + +func TestCurrencyMapInstrumentIDs(t *testing.T) { + t.Parallel() + + var i instrumentMap + if r := i.GetInstrumentIDs(); len(r) > 0 { + t.Error("non initialised instrument map shouldn't return any ids") + } + + // Seed the instrument map + i.Seed("BTCUSD", 1234) + i.Seed("LTCUSD", 1337) + + f := func(ids []int64, target int64) bool { + for x := range ids { + if ids[x] == target { + return true + } + } + return false + } + + // Test 2 valid instruments and one invalid + ids := i.GetInstrumentIDs() + if r := f(ids, 1234); !r { + t.Error("unexpected result") + } + if r := f(ids, 1337); !r { + t.Error("unexpected result") + } + if r := f(ids, 4321); r { + t.Error("unexpected result") + } +} diff --git a/exchanges/coinut/coinut_types.go b/exchanges/coinut/coinut_types.go index b4bbff32..a647afdf 100644 --- a/exchanges/coinut/coinut_types.go +++ b/exchanges/coinut/coinut_types.go @@ -1,8 +1,10 @@ package coinut import ( + "sync" + "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // GenericResponse is the generic response you will get from coinut @@ -17,7 +19,7 @@ type GenericResponse struct { type InstrumentBase struct { Base string `json:"base"` DecimalPlaces int `json:"decimal_places"` - InstID int `json:"inst_id"` + InstID int64 `json:"inst_id"` Quote string `json:"quote"` } @@ -28,15 +30,22 @@ type Instruments struct { // Ticker holds ticker information type Ticker struct { - HighestBuy float64 `json:"highest_buy,string"` - InstrumentID int `json:"inst_id"` - Last float64 `json:"last,string"` - LowestSell float64 `json:"lowest_sell,string"` - OpenInterest float64 `json:"open_interest,string"` - Timestamp float64 `json:"timestamp"` - TransID int64 `json:"trans_id"` - Volume float64 `json:"volume,string"` - Volume24 float64 `json:"volume24,string"` + High24 float64 `json:"high24,string"` + HighestBuy float64 `json:"highest_buy,string"` + InstrumentID int `json:"inst_id"` + Last float64 `json:"last,string"` + Low24 float64 `json:"low24,string"` + LowestSell float64 `json:"lowest_sell,string"` + PrevTransID int64 `json:"prev_trans_id"` + PriceChange24 float64 `json:"price_change_24,string"` + Reply string `json:"reply"` + OpenInterest float64 `json:"open_interest,string"` + Timestamp int64 `json:"timestamp"` + TransID int64 `json:"trans_id"` + Volume float64 `json:"volume,string"` + Volume24 float64 `json:"volume24,string"` + Volume24Quote float64 `json:"volume24_quote,string"` + VolumeQuote float64 `json:"volume_quote,string"` } // OrderbookBase is a sub-type holding price and quantity @@ -167,7 +176,7 @@ type CancelOrdersResponse struct { Results []struct { OrderID int64 `json:"order_id"` Status string `json:"status"` - InstrumentID int `json:"inst_id"` + InstrumentID int64 `json:"inst_id"` } `json:"results"` } @@ -280,16 +289,23 @@ type wsHeartbeatResp struct { // WsTicker defines the resp for ticker updates from the websocket connection type WsTicker struct { - HighestBuy float64 `json:"highest_buy,string"` - InstID int64 `json:"inst_id"` - Last float64 `json:"last,string"` - LowestSell float64 `json:"lowest_sell,string"` - OpenInterest float64 `json:"open_interest,string"` - Reply string `json:"reply"` - Timestamp int64 `json:"timestamp"` - TransID int64 `json:"trans_id"` - Volume float64 `json:"volume,string"` - Volume24H float64 `json:"volume24,string"` + High24 float64 `json:"high24,string"` + HighestBuy float64 `json:"highest_buy,string"` + InstID int64 `json:"inst_id"` + Last float64 `json:"last,string"` + Low24 float64 `json:"low24,string"` + LowestSell float64 `json:"lowest_sell,string"` + Nonce int64 `json:"nonce"` + PrevTransID int64 `json:"prev_trans_id"` + PriceChange24 float64 `json:"price_change_24,string"` + Reply string `json:"reply"` + Status []string `json:"status"` + Timestamp int64 `json:"timestamp"` + TransID int64 `json:"trans_id"` + Volume float64 `json:"volume,string"` + Volume24 float64 `json:"volume24,string"` + Volume24Quote float64 `json:"volume24_quote,string"` + VolumeQuote float64 `json:"volume_quote,string"` } // WsOrderbookSnapshot defines the resp for orderbook snapshot updates from @@ -355,10 +371,10 @@ type WsTradeUpdate struct { // WsInstrumentList defines instrument list type WsInstrumentList struct { - Spot map[string][]WsSupportedCurrency `json:"SPOT"` - Nonce int64 `json:"nonce"` - Reply string `json:"inst_list"` - Status []interface{} `json:"status"` + Spot map[string][]InstrumentBase `json:"SPOT"` + Nonce int64 `json:"nonce,omitempty"` + Reply string `json:"inst_list,omitempty"` + Status []interface{} `json:"status,omitempty"` } // WsSupportedCurrency defines supported currency on the exchange @@ -467,7 +483,7 @@ type WsSubmitOrderRequest struct { // WsSubmitOrderParameters ws request parameters type WsSubmitOrderParameters struct { Currency currency.Pair - Side exchange.OrderSide + Side order.Side Amount, Price float64 OrderID int64 } @@ -519,14 +535,15 @@ type WsOrderFilledResponse struct { // WsOrderData ws response data type WsOrderData struct { - ClientOrdID int64 `json:"client_ord_id"` - InstID int64 `json:"inst_id"` - OpenQty float64 `json:"open_qty,string"` - OrderID int64 `json:"order_id"` - Price float64 `json:"price,string"` - Qty float64 `json:"qty,string"` - Side string `json:"side"` - Timestamp int64 `json:"timestamp"` + ClientOrdID int64 `json:"client_ord_id"` + InstID int64 `json:"inst_id"` + OpenQty float64 `json:"open_qty,string"` + OrderID int64 `json:"order_id"` + Price float64 `json:"price,string"` + Qty float64 `json:"qty,string"` + Side string `json:"side"` + Timestamp int64 `json:"timestamp"` + Status []string `json:"status"` } // WsOrderFilledCommissionData ws response data @@ -639,22 +656,28 @@ type WsNewOrderResponse struct { // WsGetAccountBalanceResponse contains values of each currency type WsGetAccountBalanceResponse struct { - BCH string `json:"BCH"` - BTC string `json:"BTC"` - BTG string `json:"BTG"` - CAD string `json:"CAD"` - ETC string `json:"ETC"` - ETH string `json:"ETH"` - LCH string `json:"LCH"` - LTC string `json:"LTC"` - MYR string `json:"MYR"` - SGD string `json:"SGD"` - USD string `json:"USD"` - USDT string `json:"USDT"` - XMR string `json:"XMR"` - ZEC string `json:"ZEC"` + BCH float64 `json:"BCH,string"` + BTC float64 `json:"BTC,string"` + BTG float64 `json:"BTG,string"` + CAD float64 `json:"CAD,string"` + ETC float64 `json:"ETC,string"` + ETH float64 `json:"ETH,string"` + LCH float64 `json:"LCH,string"` + LTC float64 `json:"LTC,string"` + MYR float64 `json:"MYR,string"` + SGD float64 `json:"SGD,string"` + USD float64 `json:"USD,string"` + USDT float64 `json:"USDT,string"` + XMR float64 `json:"XMR,string"` + ZEC float64 `json:"ZEC,string"` Nonce int64 `json:"nonce"` Reply string `json:"reply"` Status []string `json:"status"` TransID int64 `json:"trans_id"` } + +type instrumentMap struct { + Instruments map[string]int64 + Loaded bool + m sync.Mutex +} diff --git a/exchanges/coinut/coinut_websocket.go b/exchanges/coinut/coinut_websocket.go index a0a840be..a60a2c96 100644 --- a/exchanges/coinut/coinut_websocket.go +++ b/exchanges/coinut/coinut_websocket.go @@ -1,29 +1,34 @@ package coinut import ( + "encoding/json" "errors" "fmt" "net/http" + "strconv" "strings" "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" + log "github.com/thrasher-corp/gocryptotrader/logger" ) -const coinutWebsocketURL = "wss://wsapi.coinut.com" -const coinutWebsocketRateLimit = 30 +const ( + coinutWebsocketURL = "wss://wsapi.coinut.com" + coinutWebsocketRateLimit = 30 +) -var nNonce map[int64]string -var channels map[string]chan []byte -var instrumentListByString map[string]int64 -var instrumentListByCode map[int64]string -var populatedList bool +var ( + channels map[string]chan []byte +) // NOTE for speed considerations // wss://wsapi-as.coinut.com @@ -42,16 +47,17 @@ func (c *COINUT) WsConnect() error { } go c.WsHandleData() - if !populatedList { - instrumentListByString = make(map[string]int64) - instrumentListByCode = make(map[int64]string) - err = c.WsSetInstrumentList() + if !c.instrumentMap.IsLoaded() { + _, err = c.WsGetInstruments() if err != nil { return err } - populatedList = true } - c.wsAuthenticate() + err = c.wsAuthenticate() + if err != nil { + c.Websocket.SetCanUseAuthenticatedEndpoints(false) + log.Error(log.WebsocketMgr, err) + } c.GenerateDefaultSubscriptions() // define bi-directional communication @@ -77,14 +83,14 @@ func (c *COINUT) WsHandleData() { default: resp, err := c.WebsocketConn.ReadMessage() if err != nil { - c.Websocket.DataHandler <- err + c.Websocket.ReadMessageErrors <- err return } c.Websocket.TrafficAlert <- struct{}{} if strings.HasPrefix(string(resp.Raw), "[") { var incoming []wsResponse - err = common.JSONDecode(resp.Raw, &incoming) + err = json.Unmarshal(resp.Raw, &incoming) if err != nil { c.Websocket.DataHandler <- err continue @@ -95,7 +101,7 @@ func (c *COINUT) WsHandleData() { break } var individualJSON []byte - individualJSON, err = common.JSONEncode(incoming[i]) + individualJSON, err = json.Marshal(incoming[i]) if err != nil { c.Websocket.DataHandler <- err continue @@ -104,7 +110,7 @@ func (c *COINUT) WsHandleData() { } } else { var incoming wsResponse - err = common.JSONDecode(resp.Raw, &incoming) + err = json.Unmarshal(resp.Raw, &incoming) if err != nil { c.Websocket.DataHandler <- err continue @@ -118,7 +124,7 @@ func (c *COINUT) WsHandleData() { func (c *COINUT) wsProcessResponse(resp []byte) { var incoming wsResponse - err := common.JSONDecode(resp, &incoming) + err := json.Unmarshal(resp, &incoming) if err != nil { c.Websocket.DataHandler <- err return @@ -128,24 +134,32 @@ func (c *COINUT) wsProcessResponse(resp []byte) { channels["hb"] <- resp case "inst_tick": var ticker WsTicker - err := common.JSONDecode(resp, &ticker) + err := json.Unmarshal(resp, &ticker) if err != nil { c.Websocket.DataHandler <- err return } + + currencyPair := c.instrumentMap.LookupInstrument(ticker.InstID) c.Websocket.DataHandler <- wshandler.TickerData{ - Timestamp: time.Unix(0, ticker.Timestamp), - Exchange: c.GetName(), - AssetType: orderbook.Spot, - HighPrice: ticker.HighestBuy, - LowPrice: ticker.LowestSell, - ClosePrice: ticker.Last, - Quantity: ticker.Volume, + Exchange: c.Name, + Volume: ticker.Volume24, + QuoteVolume: ticker.Volume24Quote, + Bid: ticker.HighestBuy, + Ask: ticker.LowestSell, + High: ticker.High24, + Low: ticker.Low24, + Last: ticker.Last, + Timestamp: time.Unix(0, ticker.Timestamp), + AssetType: asset.Spot, + Pair: currency.NewPairFromFormattedPairs(currencyPair, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)), } case "inst_order_book": var orderbooksnapshot WsOrderbookSnapshot - err := common.JSONDecode(resp, &orderbooksnapshot) + err := json.Unmarshal(resp, &orderbooksnapshot) if err != nil { c.Websocket.DataHandler <- err return @@ -155,15 +169,17 @@ func (c *COINUT) wsProcessResponse(resp []byte) { c.Websocket.DataHandler <- err return } - currencyPair := instrumentListByCode[orderbooksnapshot.InstID] + currencyPair := c.instrumentMap.LookupInstrument(orderbooksnapshot.InstID) c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Exchange: c.GetName(), - Asset: orderbook.Spot, - Pair: currency.NewPairFromString(currencyPair), + Exchange: c.Name, + Asset: asset.Spot, + Pair: currency.NewPairFromFormattedPairs(currencyPair, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)), } case "inst_order_book_update": var orderbookUpdate WsOrderbookUpdate - err := common.JSONDecode(resp, &orderbookUpdate) + err := json.Unmarshal(resp, &orderbookUpdate) if err != nil { c.Websocket.DataHandler <- err return @@ -173,15 +189,17 @@ func (c *COINUT) wsProcessResponse(resp []byte) { c.Websocket.DataHandler <- err return } - currencyPair := instrumentListByCode[orderbookUpdate.InstID] + currencyPair := c.instrumentMap.LookupInstrument(orderbookUpdate.InstID) c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Exchange: c.GetName(), - Asset: orderbook.Spot, - Pair: currency.NewPairFromString(currencyPair), + Exchange: c.Name, + Asset: asset.Spot, + Pair: currency.NewPairFromFormattedPairs(currencyPair, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)), } case "inst_trade": var tradeSnap WsTradeSnapshot - err := common.JSONDecode(resp, &tradeSnap) + err := json.Unmarshal(resp, &tradeSnap) if err != nil { c.Websocket.DataHandler <- err return @@ -189,19 +207,21 @@ func (c *COINUT) wsProcessResponse(resp []byte) { case "inst_trade_update": var tradeUpdate WsTradeUpdate - err := common.JSONDecode(resp, &tradeUpdate) + err := json.Unmarshal(resp, &tradeUpdate) if err != nil { c.Websocket.DataHandler <- err return } - currencyPair := instrumentListByCode[tradeUpdate.InstID] + currencyPair := c.instrumentMap.LookupInstrument(tradeUpdate.InstID) c.Websocket.DataHandler <- wshandler.TradeData{ - Timestamp: time.Unix(tradeUpdate.Timestamp, 0), - CurrencyPair: currency.NewPairFromString(currencyPair), - AssetType: orderbook.Spot, - Exchange: c.GetName(), - Price: tradeUpdate.Price, - Side: tradeUpdate.Side, + Timestamp: time.Unix(tradeUpdate.Timestamp, 0), + CurrencyPair: currency.NewPairFromFormattedPairs(currencyPair, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)), + AssetType: asset.Spot, + Exchange: c.Name, + Price: tradeUpdate.Price, + Side: tradeUpdate.Side, } default: if incoming.Nonce > 0 { @@ -223,30 +243,29 @@ func (c *COINUT) GetNonce() int64 { return int64(c.Nonce.Get()) } -// WsSetInstrumentList fetches instrument list and propagates a local cache -func (c *COINUT) WsSetInstrumentList() error { +// WsGetInstruments fetches instrument list and propagates a local cache +func (c *COINUT) WsGetInstruments() (Instruments, error) { + var list Instruments request := wsRequest{ Request: "inst_list", - SecType: orderbook.Spot, + SecType: strings.ToUpper(asset.Spot.String()), Nonce: c.WebsocketConn.GenerateMessageID(false), } resp, err := c.WebsocketConn.SendMessageReturnResponse(request.Nonce, request) if err != nil { - return err + return list, err } - var list WsInstrumentList - err = common.JSONDecode(resp, &list) + err = json.Unmarshal(resp, &list) if err != nil { - return err + return list, err } - for currency, data := range list.Spot { - instrumentListByString[currency] = data[0].InstID - instrumentListByCode[data[0].InstID] = currency + for curr, data := range list.Instruments { + c.instrumentMap.Seed(curr, data[0].InstID) } - if len(instrumentListByString) == 0 || len(instrumentListByCode) == 0 { - return errors.New("instrument lists failed to populate") + if len(c.instrumentMap.GetInstrumentIDs()) == 0 { + return list, errors.New("instrument list failed to populate") } - return nil + return list, nil } // WsProcessOrderbookSnapshot processes the orderbook snapshot @@ -270,21 +289,30 @@ func (c *COINUT) WsProcessOrderbookSnapshot(ob *WsOrderbookSnapshot) error { var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids - newOrderBook.Pair = currency.NewPairFromString(instrumentListByCode[ob.InstID]) - newOrderBook.AssetType = orderbook.Spot + newOrderBook.Pair = currency.NewPairFromFormattedPairs( + c.instrumentMap.LookupInstrument(ob.InstID), + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true), + ) + newOrderBook.AssetType = asset.Spot + newOrderBook.ExchangeName = c.Name - return c.Websocket.Orderbook.LoadSnapshot(&newOrderBook, false) + return c.Websocket.Orderbook.LoadSnapshot(&newOrderBook) } // WsProcessOrderbookUpdate process an orderbook update func (c *COINUT) WsProcessOrderbookUpdate(update *WsOrderbookUpdate) error { - p := currency.NewPairFromString(instrumentListByCode[update.InstID]) + p := currency.NewPairFromFormattedPairs( + c.instrumentMap.LookupInstrument(update.InstID), + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true), + ) bufferUpdate := &wsorderbook.WebsocketOrderbookUpdate{ - CurrencyPair: p, - UpdateID: update.TransID, - AssetType: orderbook.Spot, + Pair: p, + UpdateID: update.TransID, + Asset: asset.Spot, } - if strings.EqualFold(update.Side, "buy") { + if strings.EqualFold(update.Side, order.Buy.Lower()) { bufferUpdate.Bids = []orderbook.Item{{Price: update.Price, Amount: update.Volume}} } else { bufferUpdate.Asks = []orderbook.Item{{Price: update.Price, Amount: update.Volume}} @@ -296,7 +324,7 @@ func (c *COINUT) WsProcessOrderbookUpdate(update *WsOrderbookUpdate) error { func (c *COINUT) GenerateDefaultSubscriptions() { var channels = []string{"inst_tick", "inst_order_book"} var subscriptions []wshandler.WebsocketChannelSubscription - enabledCurrencies := c.GetEnabledCurrencies() + enabledCurrencies := c.GetEnabledPairs(asset.Spot) for i := range channels { for j := range enabledCurrencies { subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ @@ -311,8 +339,9 @@ func (c *COINUT) GenerateDefaultSubscriptions() { // Subscribe sends a websocket message to receive data from the channel func (c *COINUT) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := wsRequest{ - Request: channelToSubscribe.Channel, - InstID: instrumentListByString[channelToSubscribe.Currency.String()], + Request: channelToSubscribe.Channel, + InstID: c.instrumentMap.LookupID(c.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String()), Subscribe: true, Nonce: c.WebsocketConn.GenerateMessageID(false), } @@ -322,8 +351,9 @@ func (c *COINUT) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscrip // Unsubscribe sends a websocket message to stop receiving data from the channel func (c *COINUT) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := wsRequest{ - Request: channelToSubscribe.Channel, - InstID: instrumentListByString[channelToSubscribe.Currency.String()], + Request: channelToSubscribe.Channel, + InstID: c.instrumentMap.LookupID(c.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String()), Subscribe: false, Nonce: c.WebsocketConn.GenerateMessageID(false), } @@ -332,7 +362,7 @@ func (c *COINUT) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscr return err } var response map[string]interface{} - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return err } @@ -348,8 +378,10 @@ func (c *COINUT) wsAuthenticate() error { } timestamp := time.Now().Unix() nonce := c.WebsocketConn.GenerateMessageID(false) - payload := fmt.Sprintf("%v|%v|%v", c.ClientID, timestamp, nonce) - hmac := common.GetHMAC(common.HashSHA256, []byte(payload), []byte(c.APIKey)) + payload := c.API.Credentials.ClientID + "|" + + strconv.FormatInt(timestamp, 10) + "|" + + strconv.FormatInt(nonce, 10) + hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(payload), []byte(c.API.Credentials.Key)) loginRequest := struct { Request string `json:"request"` Username string `json:"username"` @@ -358,9 +390,9 @@ func (c *COINUT) wsAuthenticate() error { Timestamp int64 `json:"timestamp"` }{ Request: "login", - Username: c.ClientID, + Username: c.API.Credentials.ClientID, Nonce: nonce, - Hmac: common.HexEncodeToString(hmac), + Hmac: crypto.HexEncodeToString(hmac), Timestamp: timestamp, } @@ -369,7 +401,7 @@ func (c *COINUT) wsAuthenticate() error { return err } var response map[string]interface{} - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return err } @@ -381,7 +413,7 @@ func (c *COINUT) wsAuthenticate() error { return nil } -func (c *COINUT) wsGetAccountBalance() (*WsGetAccountBalanceResponse, error) { +func (c *COINUT) wsGetAccountBalance() (*UserBalance, error) { if !c.Websocket.CanUseAuthenticatedEndpoints() { return nil, fmt.Errorf("%v not authorised to submit order", c.Name) } @@ -393,8 +425,8 @@ func (c *COINUT) wsGetAccountBalance() (*WsGetAccountBalanceResponse, error) { if err != nil { return nil, err } - var response WsGetAccountBalanceResponse - err = common.JSONDecode(resp, &response) + var response UserBalance + err = json.Unmarshal(resp, &response) if err != nil { return nil, err } @@ -404,21 +436,21 @@ func (c *COINUT) wsGetAccountBalance() (*WsGetAccountBalanceResponse, error) { return &response, nil } -func (c *COINUT) wsSubmitOrder(order *WsSubmitOrderParameters) (*WsStandardOrderResponse, error) { +func (c *COINUT) wsSubmitOrder(o *WsSubmitOrderParameters) (*WsStandardOrderResponse, error) { if !c.Websocket.CanUseAuthenticatedEndpoints() { return nil, fmt.Errorf("%v not authorised to submit order", c.Name) } - currency := exchange.FormatExchangeCurrency(c.Name, order.Currency).String() + curr := c.FormatExchangeCurrency(o.Currency, asset.Spot).String() var orderSubmissionRequest WsSubmitOrderRequest orderSubmissionRequest.Request = "new_order" orderSubmissionRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) - orderSubmissionRequest.InstID = instrumentListByString[currency] - orderSubmissionRequest.Qty = order.Amount - orderSubmissionRequest.Price = order.Price - orderSubmissionRequest.Side = string(order.Side) + orderSubmissionRequest.InstID = c.instrumentMap.LookupID(curr) + orderSubmissionRequest.Qty = o.Amount + orderSubmissionRequest.Price = o.Price + orderSubmissionRequest.Side = string(o.Side) - if order.OrderID > 0 { - orderSubmissionRequest.OrderID = order.OrderID + if o.OrderID > 0 { + orderSubmissionRequest.OrderID = o.OrderID } resp, err := c.WebsocketConn.SendMessageReturnResponse(orderSubmissionRequest.Nonce, orderSubmissionRequest) if err != nil { @@ -441,14 +473,14 @@ func (c *COINUT) wsSubmitOrder(order *WsSubmitOrderParameters) (*WsStandardOrder func (c *COINUT) wsStandardiseOrderResponse(resp []byte) (WsStandardOrderResponse, error) { var response WsStandardOrderResponse var incoming wsResponse - err := common.JSONDecode(resp, &incoming) + err := json.Unmarshal(resp, &incoming) if err != nil { return response, err } switch incoming.Reply { case "order_accepted": var orderAccepted WsOrderAcceptedResponse - err := common.JSONDecode(resp, &orderAccepted) + err := json.Unmarshal(resp, &orderAccepted) if err != nil { return response, err } @@ -467,7 +499,7 @@ func (c *COINUT) wsStandardiseOrderResponse(resp []byte) (WsStandardOrderRespons } case "order_filled": var orderFilled WsOrderFilledResponse - err := common.JSONDecode(resp, &orderFilled) + err := json.Unmarshal(resp, &orderFilled) if err != nil { return response, err } @@ -486,7 +518,7 @@ func (c *COINUT) wsStandardiseOrderResponse(resp []byte) (WsStandardOrderRespons } case "order_rejected": var orderRejected WsOrderRejectedResponse - err := common.JSONDecode(resp, &orderRejected) + err := json.Unmarshal(resp, &orderRejected) if err != nil { return response, err } @@ -517,13 +549,13 @@ func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) ([]WsStandardO } orderRequest := WsSubmitOrdersRequest{} for i := range orders { - currency := exchange.FormatExchangeCurrency(c.Name, orders[i].Currency).String() + curr := c.FormatExchangeCurrency(orders[i].Currency, asset.Spot).String() orderRequest.Orders = append(orderRequest.Orders, WsSubmitOrdersRequestData{ Qty: orders[i].Amount, Price: orders[i].Price, Side: string(orders[i].Side), - InstID: instrumentListByString[currency], + InstID: c.instrumentMap.LookupID(curr), ClientOrdID: i + 1, }) } @@ -536,14 +568,14 @@ func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) ([]WsStandardO return nil, errors } var incoming []interface{} - err = common.JSONDecode(resp, &incoming) + err = json.Unmarshal(resp, &incoming) if err != nil { errors = append(errors, err) return nil, errors } for i := range incoming { var individualJSON []byte - individualJSON, err = common.JSONEncode(incoming[i]) + individualJSON, err = json.Marshal(incoming[i]) if err != nil { errors = append(errors, err) continue @@ -560,7 +592,7 @@ func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) ([]WsStandardO if len(standardOrder.Reasons) > 0 && standardOrder.Reasons[0] != "" { errors = append(errors, fmt.Errorf("%v order submission failed for currency %v and orderID %v, message %v ", c.Name, - instrumentListByCode[standardOrder.InstID], + c.instrumentMap.LookupInstrument(standardOrder.InstID), standardOrder.OrderID, standardOrder.Reasons[0])) @@ -572,73 +604,73 @@ func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) ([]WsStandardO return ordersResponse, errors } -func (c *COINUT) wsGetOpenOrders(p currency.Pair) error { +func (c *COINUT) wsGetOpenOrders(curr string) (*WsUserOpenOrdersResponse, error) { + var response *WsUserOpenOrdersResponse if !c.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authorised to get open orders", c.Name) + return response, fmt.Errorf("%v not authorised to get open orders", c.Name) } - currency := exchange.FormatExchangeCurrency(c.Name, p).String() var openOrdersRequest WsGetOpenOrdersRequest openOrdersRequest.Request = "user_open_orders" openOrdersRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) - openOrdersRequest.InstID = instrumentListByString[currency] + openOrdersRequest.InstID = c.instrumentMap.LookupID(curr) resp, err := c.WebsocketConn.SendMessageReturnResponse(openOrdersRequest.Nonce, openOrdersRequest) if err != nil { - return err + return response, err } - var response map[string]interface{} - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { - return err + return response, err } - if response["status"].([]interface{})[0] != "OK" { - return fmt.Errorf("%v get open orders failed for currency %v", + if response.Status[0] != "OK" { + return response, fmt.Errorf("%v get open orders failed for currency %v", c.Name, - p) + curr) } - return nil + return response, nil } -func (c *COINUT) wsCancelOrder(cancellation WsCancelOrderParameters) error { +func (c *COINUT) wsCancelOrder(cancellation *WsCancelOrderParameters) (*CancelOrdersResponse, error) { + var response *CancelOrdersResponse if !c.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authorised to cancel order", c.Name) + return response, fmt.Errorf("%v not authorised to cancel order", c.Name) } - currency := exchange.FormatExchangeCurrency(c.Name, cancellation.Currency).String() + curr := c.FormatExchangeCurrency(cancellation.Currency, asset.Spot).String() var cancellationRequest WsCancelOrderRequest cancellationRequest.Request = "cancel_order" - cancellationRequest.InstID = instrumentListByString[currency] + cancellationRequest.InstID = c.instrumentMap.LookupID(curr) cancellationRequest.OrderID = cancellation.OrderID cancellationRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) resp, err := c.WebsocketConn.SendMessageReturnResponse(cancellationRequest.Nonce, cancellationRequest) if err != nil { - return err + return response, err } - var response map[string]interface{} - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { - return err + return response, err } - if response["status"].([]interface{})[0] != "OK" { - return fmt.Errorf("%v order cancellation failed for currency %v and orderID %v, message %v", + if response.Status[0] != "OK" { + return response, fmt.Errorf("%v order cancellation failed for currency %v and orderID %v, message %v", c.Name, cancellation.Currency, cancellation.OrderID, - response["status"]) + response.Status[0]) } - return nil + return response, nil } -func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) (*WsCancelOrdersResponse, []error) { - var errors []error +func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) (*CancelOrdersResponse, error) { + var err error + var response *CancelOrdersResponse if !c.Websocket.CanUseAuthenticatedEndpoints() { - return nil, errors + return nil, err } - cancelOrderRequest := WsCancelOrdersRequest{} + var cancelOrderRequest WsCancelOrdersRequest for i := range cancellations { - currency := exchange.FormatExchangeCurrency(c.Name, cancellations[i].Currency).String() + curr := c.FormatExchangeCurrency(cancellations[i].Currency, asset.Spot).String() cancelOrderRequest.Entries = append(cancelOrderRequest.Entries, WsCancelOrdersRequestEntry{ - InstID: instrumentListByString[currency], + InstID: c.instrumentMap.LookupID(curr), OrderID: cancellations[i].OrderID, }) } @@ -647,53 +679,40 @@ func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) (*WsCan cancelOrderRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) resp, err := c.WebsocketConn.SendMessageReturnResponse(cancelOrderRequest.Nonce, cancelOrderRequest) if err != nil { - return nil, []error{err} + return response, err } - var response WsCancelOrdersResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { - return nil, []error{err} + return response, err } - if response.Status[0] != "OK" { - return &response, []error{err} - } - for i := range response.Results { - if response.Results[i].Status != "OK" { - errors = append(errors, fmt.Errorf("%v order cancellation failed for currency %v and orderID %v, message %v", - c.Name, - instrumentListByCode[response.Results[i].InstID], - response.Results[i].OrderID, - response.Results[i].Status)) - } - } - return &response, errors + return response, err } -func (c *COINUT) wsGetTradeHistory(p currency.Pair, start, limit int64) error { +func (c *COINUT) wsGetTradeHistory(p currency.Pair, start, limit int64) (*WsTradeHistoryResponse, error) { + var response *WsTradeHistoryResponse if !c.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authorised to get trade history", c.Name) + return response, fmt.Errorf("%v not authorised to get trade history", c.Name) } - currency := exchange.FormatExchangeCurrency(c.Name, p).String() + curr := c.FormatExchangeCurrency(p, asset.Spot).String() var request WsTradeHistoryRequest request.Request = "trade_history" - request.InstID = instrumentListByString[currency] + request.InstID = c.instrumentMap.LookupID(curr) request.Nonce = c.WebsocketConn.GenerateMessageID(false) request.Start = start request.Limit = limit resp, err := c.WebsocketConn.SendMessageReturnResponse(request.Nonce, request) if err != nil { - return err + return response, err } - var response map[string]interface{} - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { - return err + return response, err } - if response["status"].([]interface{})[0] != "OK" { - return fmt.Errorf("%v get trade history failed for %v", + if response.Status[0] != "OK" { + return response, fmt.Errorf("%v get trade history failed for %v", c.Name, request) } - return nil + return response, nil } diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 2bc9bba5..aaa0b0e3 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -9,14 +9,172 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (c *COINUT) GetDefaultConfig() (*config.ExchangeConfig, error) { + c.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = c.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = c.BaseCurrencies + + err := c.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if c.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = c.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets current default values +func (c *COINUT) SetDefaults() { + c.Name = "COINUT" + c.Enabled = true + c.Verbose = true + c.API.CredentialsValidator.RequiresKey = true + c.API.CredentialsValidator.RequiresClientID = true + + c.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "-", + }, + } + + c.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + SubmitOrders: true, + UserTradeHistory: true, + TradeFee: true, + FiatDepositFee: true, + FiatWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + AccountBalance: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + SubmitOrders: true, + UserTradeHistory: true, + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AccountInfo: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + MessageCorrelation: true, + }, + WithdrawPermissions: exchange.WithdrawCryptoViaWebsiteOnly | + exchange.WithdrawFiatViaWebsiteOnly, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + c.Requester = request.New(c.Name, + request.NewRateLimit(time.Second, coinutAuthRate), + request.NewRateLimit(time.Second, coinutUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + c.API.Endpoints.URLDefault = coinutAPIURL + c.API.Endpoints.URL = c.API.Endpoints.URLDefault + c.API.Endpoints.WebsocketURL = coinutWebsocketURL + c.Websocket = wshandler.New() + c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + c.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup sets the current exchange configuration +func (c *COINUT) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + c.SetEnabled(false) + return nil + } + + err := c.SetupDefaults(exch) + if err != nil { + return err + } + + err = c.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: coinutWebsocketURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: c.WsConnect, + Subscriber: c.Subscribe, + UnSubscriber: c.Unsubscribe, + Features: &c.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + c.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: c.Name, + URL: c.Websocket.GetWebsocketURL(), + ProxyURL: c.Websocket.GetProxyAddress(), + Verbose: c.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + c.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + true, + true, + true, + false, + exch.Name) + return nil +} + // Start starts the COINUT go routine func (c *COINUT) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -29,43 +187,96 @@ func (c *COINUT) Start(wg *sync.WaitGroup) { // Run implements the COINUT wrapper func (c *COINUT) Run() { if c.Verbose { - log.Debugf("%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket.IsEnabled()), coinutWebsocketURL) - log.Debugf("%s polling delay: %ds.\n", c.GetName(), c.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", c.GetName(), len(c.EnabledPairs), c.EnabledPairs) + log.Debugf(log.ExchangeSys, "%s Websocket: %s. (url: %s).\n", c.Name, common.IsEnabled(c.Websocket.IsEnabled()), coinutWebsocketURL) + c.PrintEnabledPairs() } - exchangeProducts, err := c.GetInstruments() - if err != nil { - log.Debugf("%s Failed to get available products.\n", c.GetName()) + forceUpdate := false + delim := c.GetPairFormat(asset.Spot, false).Delimiter + if !common.StringDataContains(c.CurrencyPairs.GetPairs(asset.Spot, + true).Strings(), delim) || + !common.StringDataContains(c.CurrencyPairs.GetPairs(asset.Spot, + false).Strings(), delim) { + enabledPairs := currency.NewPairsFromStrings( + []string{fmt.Sprintf("LTC%sUSDT", delim)}, + ) + log.Warn(log.ExchangeSys, + "Enabled pairs for Coinut reset due to config upgrade, please enable the ones you would like to use again") + forceUpdate = true + + err := c.UpdatePairs(enabledPairs, asset.Spot, true, true) + if err != nil { + log.Errorf(log.ExchangeSys, "%s failed to update currencies. Err: %s\n", c.Name, err) + } + } + + if !c.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { return } - var currencies []string - c.InstrumentMap = make(map[string]int) - for x, y := range exchangeProducts.Instruments { - c.InstrumentMap[x] = y[0].InstID - currencies = append(currencies, x) - } - - var newCurrencies currency.Pairs - for _, p := range currencies { - newCurrencies = append(newCurrencies, - currency.NewPairFromString(p)) - } - - err = c.UpdateCurrencies(newCurrencies, false, false) + err := c.UpdateTradablePairs(forceUpdate) if err != nil { - log.Errorf("%s Failed to update available currencies.\n", c.GetName()) + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", c.Name, err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (c *COINUT) FetchTradablePairs(asset asset.Item) ([]string, error) { + var instruments map[string][]InstrumentBase + var resp Instruments + var err error + if c.Websocket.IsConnected() { + resp, err = c.WsGetInstruments() + if err != nil { + return nil, err + } + } else { + resp, err = c.GetInstruments() + if err != nil { + return nil, err + } + } + instruments = resp.Instruments + var pairs []string + for i := range instruments { + c.instrumentMap.Seed(instruments[i][0].Base+instruments[i][0].Quote, instruments[i][0].InstID) + p := instruments[i][0].Base + c.GetPairFormat(asset, false).Delimiter + instruments[i][0].Quote + pairs = append(pairs, p) + } + + return pairs, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (c *COINUT) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := c.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return c.UpdatePairs(currency.NewPairsFromStrings(pairs), + asset.Spot, false, forceUpdate) +} + // GetAccountInfo retrieves balances for all enabled currencies for the // COINUT exchange func (c *COINUT) GetAccountInfo() (exchange.AccountInfo, error) { var info exchange.AccountInfo - bal, err := c.GetUserBalance() - if err != nil { - return info, err + var bal *UserBalance + var err error + if c.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + var resp *UserBalance + resp, err = c.wsGetAccountBalance() + if err != nil { + return info, err + } + bal = resp + } else { + bal, err = c.GetUserBalance() + if err != nil { + return info, err + } } var balances = []exchange.AccountCurrencyInfo{ @@ -126,7 +337,7 @@ func (c *COINUT) GetAccountInfo() (exchange.AccountInfo, error) { TotalValue: bal.ZEC, }, } - info.Exchange = c.GetName() + info.Exchange = c.Name info.Accounts = append(info.Accounts, exchange.Account{ Currencies: balances, }) @@ -135,20 +346,34 @@ func (c *COINUT) GetAccountInfo() (exchange.AccountInfo, error) { } // UpdateTicker updates and returns the ticker for a currency pair -func (c *COINUT) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (c *COINUT) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - tick, err := c.GetInstrumentTicker(c.InstrumentMap[p.String()]) + err := c.loadInstrumentsIfNotLoaded() if err != nil { - return ticker.Price{}, err + return tickerPrice, err } - tickerPrice.Pair = p - tickerPrice.Volume = tick.Volume - tickerPrice.Last = tick.Last - tickerPrice.High = tick.HighestBuy - tickerPrice.Low = tick.LowestSell - - err = ticker.ProcessTicker(c.GetName(), &tickerPrice, assetType) + instID := c.instrumentMap.LookupID(c.FormatExchangeCurrency(p, + assetType).String()) + if instID == 0 { + return tickerPrice, errors.New("unable to lookup instrument ID") + } + var tick Ticker + tick, err = c.GetInstrumentTicker(instID) + if err != nil { + return tickerPrice, err + } + tickerPrice = ticker.Price{ + Last: tick.Last, + High: tick.High24, + Low: tick.Low24, + Bid: tick.HighestBuy, + Ask: tick.LowestSell, + Volume: tick.Volume24, + Pair: p, + LastUpdated: time.Unix(0, tick.Timestamp), + } + err = ticker.ProcessTicker(c.Name, &tickerPrice, assetType) if err != nil { return tickerPrice, err } @@ -156,18 +381,18 @@ func (c *COINUT) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, return ticker.GetTicker(c.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (c *COINUT) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(c.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (c *COINUT) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(c.Name, p, assetType) if err != nil { return c.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (c *COINUT) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(c.GetName(), p, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (c *COINUT) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(c.Name, p, assetType) if err != nil { return c.UpdateOrderbook(p, assetType) } @@ -175,9 +400,20 @@ func (c *COINUT) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Ba } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (c *COINUT) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (c *COINUT) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := c.GetInstrumentOrderbook(c.InstrumentMap[p.String()], 200) + err := c.loadInstrumentsIfNotLoaded() + if err != nil { + return orderBook, err + } + + instID := c.instrumentMap.LookupID(c.FormatExchangeCurrency(p, + assetType).String()) + if instID == 0 { + return orderBook, errLookupInstrumentID + } + + orderbookNew, err := c.GetInstrumentOrderbook(instID, 200) if err != nil { return orderBook, err } @@ -191,7 +427,7 @@ func (c *COINUT) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.B } orderBook.Pair = p - orderBook.ExchangeName = c.GetName() + orderBook.ExchangeName = c.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -205,139 +441,194 @@ func (c *COINUT) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.B // GetFundingHistory returns funding history, deposits and // withdrawals func (c *COINUT) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (c *COINUT) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (c *COINUT) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (c *COINUT) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse +func (c *COINUT) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse var err error - var APIresponse interface{} - isBuyOrder := side == exchange.BuyOrderSide - clientIDInt, err := strconv.ParseUint(clientID, 0, 32) - clientIDUint := uint32(clientIDInt) + if _, err = strconv.Atoi(o.ClientID); err != nil { + return submitOrderResponse, fmt.Errorf("%s - ClientID must be a number, received: %s", c.Name, o.ClientID) + } + err = o.Validate() - if err != nil { - return submitOrderResponse, err - } - // Need to get the ID of the currency sent - instruments, err := c.GetInstruments() if err != nil { return submitOrderResponse, err } - currencyArray := instruments.Instruments[p.String()] - currencyID := currencyArray[0].InstID - - switch orderType { - case exchange.LimitOrderType: - APIresponse, err = c.NewOrder(currencyID, amount, price, isBuyOrder, clientIDUint) - case exchange.MarketOrderType: - APIresponse, err = c.NewOrder(currencyID, amount, 0, isBuyOrder, clientIDUint) - default: - return submitOrderResponse, errors.New("unsupported order type") - } - - switch apiResp := APIresponse.(type) { - case OrdersBase: - orderResult := apiResp - submitOrderResponse.OrderID = fmt.Sprintf("%v", orderResult.OrderID) - case OrderFilledResponse: - orderResult := apiResp - submitOrderResponse.OrderID = fmt.Sprintf("%v", orderResult.Order.OrderID) - case OrderRejectResponse: - orderResult := apiResp - submitOrderResponse.OrderID = fmt.Sprintf("%v", orderResult.OrderID) - err = fmt.Errorf("orderID: %v was rejected: %v", orderResult.OrderID, orderResult.Reasons) - } - - if err == nil { + if c.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + var response *WsStandardOrderResponse + response, err = c.wsSubmitOrder(&WsSubmitOrderParameters{ + Currency: o.Pair, + Side: o.OrderSide, + Amount: o.Amount, + Price: o.Price, + }) + if err != nil { + return submitOrderResponse, err + } + submitOrderResponse.OrderID = strconv.FormatInt(response.OrderID, 10) submitOrderResponse.IsOrderPlaced = true - } + } else { + err = c.loadInstrumentsIfNotLoaded() + if err != nil { + return submitOrderResponse, err + } - return submitOrderResponse, err + currencyID := c.instrumentMap.LookupID(c.FormatExchangeCurrency(o.Pair, + asset.Spot).String()) + if currencyID == 0 { + return submitOrderResponse, errLookupInstrumentID + } + + var APIResponse interface{} + var clientIDInt uint64 + isBuyOrder := o.OrderSide == order.Buy + clientIDInt, err = strconv.ParseUint(o.ClientID, 0, 32) + if err != nil { + return submitOrderResponse, err + } + clientIDUint := uint32(clientIDInt) + APIResponse, err = c.NewOrder(currencyID, o.Amount, o.Price, + isBuyOrder, clientIDUint) + if err != nil { + return submitOrderResponse, err + } + responseMap := APIResponse.(map[string]interface{}) + switch responseMap["reply"].(string) { + case "order_rejected": + return submitOrderResponse, fmt.Errorf("clientOrderID: %v was rejected: %v", o.ClientID, responseMap["reasons"]) + case "order_filled": + orderID := responseMap["order_id"].(float64) + submitOrderResponse.OrderID = strconv.FormatFloat(orderID, 'f', -1, 64) + submitOrderResponse.IsOrderPlaced = true + submitOrderResponse.FullyMatched = true + return submitOrderResponse, nil + case "order_accepted": + orderID := responseMap["order_id"].(float64) + submitOrderResponse.OrderID = strconv.FormatFloat(orderID, 'f', -1, 64) + submitOrderResponse.IsOrderPlaced = true + return submitOrderResponse, nil + } + } + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (c *COINUT) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (c *COINUT) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (c *COINUT) CancelOrder(order *exchange.OrderCancellation) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) - +func (c *COINUT) CancelOrder(o *order.Cancel) error { + err := c.loadInstrumentsIfNotLoaded() + if err != nil { + return err + } + orderIDInt, err := strconv.ParseInt(o.OrderID, 10, 64) if err != nil { return err } - // Need to get the ID of the currency sent - instruments, err := c.GetInstruments() - - if err != nil { - return err + currencyID := c.instrumentMap.LookupID(c.FormatExchangeCurrency( + o.CurrencyPair, + asset.Spot).String(), + ) + if c.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + var resp *CancelOrdersResponse + resp, err = c.wsCancelOrder(&WsCancelOrderParameters{ + Currency: o.CurrencyPair, + OrderID: orderIDInt, + }) + if err != nil { + return err + } + if len(resp.Status) >= 1 && resp.Status[0] != "OK" { + return errors.New(c.Name + " - Failed to cancel order " + o.OrderID) + } + } else { + if currencyID == 0 { + return errLookupInstrumentID + } + _, err = c.CancelExistingOrder(currencyID, orderIDInt) + if err != nil { + return err + } } - currencyArray := instruments.Instruments[exchange.FormatExchangeCurrency(c.Name, order.CurrencyPair).String()] - currencyID := currencyArray[0].InstID - _, err = c.CancelExistingOrder(currencyID, int(orderIDInt)) - - return err + return nil } // CancelAllOrders cancels all orders associated with a currency pair -func (c *COINUT) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - // TODO, this is a terrible implementation. Requires DB to improve - // Coinut provides no way of retrieving orders without a currency - // So we need to retrieve all currencies, then retrieve orders for each currency - // Then cancel. Advisable to never use this until DB due to performance - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), - } - instruments, err := c.GetInstruments() +func (c *COINUT) CancelAllOrders(details *order.Cancel) (order.CancelAllResponse, error) { + var cancelAllOrdersResponse order.CancelAllResponse + err := c.loadInstrumentsIfNotLoaded() if err != nil { return cancelAllOrdersResponse, err } - - var allTheOrders []OrderResponse - for _, allInstrumentData := range instruments.Instruments { - for _, instrumentData := range allInstrumentData { - openOrders, err := c.GetOpenOrders(instrumentData.InstID) - if err != nil { - return cancelAllOrdersResponse, err - } - allTheOrders = append(allTheOrders, openOrders.Orders...) - } - } - - var allTheOrdersToCancel []CancelOrders - for _, orderToCancel := range allTheOrders { - cancelOrder := CancelOrders{ - InstrumentID: orderToCancel.InstrumentID, - OrderID: orderToCancel.OrderID, - } - allTheOrdersToCancel = append(allTheOrdersToCancel, cancelOrder) - } - - if len(allTheOrdersToCancel) > 0 { - resp, err := c.CancelOrders(allTheOrdersToCancel) + cancelAllOrdersResponse.Status = make(map[string]string) + if c.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + openOrders, err := c.wsGetOpenOrders(details.CurrencyPair.String()) if err != nil { return cancelAllOrdersResponse, err } + var ordersToCancel []WsCancelOrderParameters + for i := range openOrders.Orders { + if openOrders.Orders[i].InstID == c.instrumentMap.LookupID(c.FormatExchangeCurrency(details.CurrencyPair, asset.Spot).String()) { + ordersToCancel = append(ordersToCancel, WsCancelOrderParameters{ + Currency: details.CurrencyPair, + OrderID: openOrders.Orders[i].OrderID, + }) + } + } + resp, err := c.wsCancelOrders(ordersToCancel) + if err != nil { + return cancelAllOrdersResponse, err + } + for i := range resp.Results { + if openOrders.Orders[i].Status[0] != "OK" { + cancelAllOrdersResponse.Status[strconv.FormatInt(openOrders.Orders[i].OrderID, 10)] = strings.Join(openOrders.Orders[i].Status, ",") + } + } + } else { + var allTheOrders []OrderResponse + ids := c.instrumentMap.GetInstrumentIDs() + for x := range ids { + if ids[x] == c.instrumentMap.LookupID(c.FormatExchangeCurrency(details.CurrencyPair, asset.Spot).String()) { + openOrders, err := c.GetOpenOrders(ids[x]) + if err != nil { + return cancelAllOrdersResponse, err + } + allTheOrders = append(allTheOrders, openOrders.Orders...) + } + } - for _, order := range resp.Results { - if order.Status != "OK" { - cancelAllOrdersResponse.OrderStatus[strconv.FormatInt(order.OrderID, 10)] = order.Status + var allTheOrdersToCancel []CancelOrders + for i := range allTheOrders { + cancelOrder := CancelOrders{ + InstrumentID: allTheOrders[i].InstrumentID, + OrderID: allTheOrders[i].OrderID, + } + allTheOrdersToCancel = append(allTheOrdersToCancel, cancelOrder) + } + + if len(allTheOrdersToCancel) > 0 { + resp, err := c.CancelOrders(allTheOrdersToCancel) + if err != nil { + return cancelAllOrdersResponse, err + } + + for i := range resp.Results { + if resp.Results[i].Status != "OK" { + cancelAllOrdersResponse.Status[strconv.FormatInt(resp.Results[i].OrderID, 10)] = resp.Results[i].Status + } } } } @@ -346,9 +637,8 @@ func (c *COINUT) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cancel } // GetOrderInfo returns information on a current open order -func (c *COINUT) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail - return orderDetail, common.ErrNotYetImplemented +func (c *COINUT) GetOrderInfo(orderID string) (order.Detail, error) { + return order.Detail{}, common.ErrNotYetImplemented } // GetDepositAddress returns a deposit address for a specified currency @@ -358,19 +648,19 @@ func (c *COINUT) GetDepositAddress(cryptocurrency currency.Code, accountID strin // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (c *COINUT) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (c *COINUT) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (c *COINUT) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (c *COINUT) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (c *COINUT) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (c *COINUT) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -381,7 +671,7 @@ func (c *COINUT) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (c *COINUT) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if c.APIKey == "" && // Todo check connection status + if !c.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -389,117 +679,171 @@ func (c *COINUT) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) } // GetActiveOrders retrieves any orders that are active/open -func (c *COINUT) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - instruments, err := c.GetInstruments() +func (c *COINUT) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { + err := c.loadInstrumentsIfNotLoaded() if err != nil { return nil, err } + var orders []order.Detail + var currenciesToCheck []string + if len(req.Currencies) == 0 { + for i := range req.Currencies { + currenciesToCheck = append(currenciesToCheck, c.FormatExchangeCurrency(req.Currencies[i], asset.Spot).String()) + } + } else { + for k := range c.instrumentMap.Instruments { + currenciesToCheck = append(currenciesToCheck, k) + } + } + if c.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + for x := range currenciesToCheck { + openOrders, err := c.wsGetOpenOrders(currenciesToCheck[x]) + if err != nil { + return nil, err + } + for i := range openOrders.Orders { + orders = append(orders, order.Detail{ + Exchange: c.Name, + ID: strconv.FormatInt(openOrders.Orders[i].OrderID, 10), + CurrencyPair: c.FormatExchangeCurrency(currency.NewPairFromString(currenciesToCheck[x]), asset.Spot), + OrderSide: order.Side(openOrders.Orders[i].Side), + OrderDate: time.Unix(0, openOrders.Orders[i].Timestamp), + Status: order.Active, + Price: openOrders.Orders[i].Price, + Amount: openOrders.Orders[i].Qty, + ExecutedAmount: openOrders.Orders[i].Qty - openOrders.Orders[i].OpenQty, + RemainingAmount: openOrders.Orders[i].OpenQty, + }) + } + } + } else { + var instrumentsToUse []int64 + if len(req.Currencies) > 0 { + for x := range req.Currencies { + curr := c.FormatExchangeCurrency(req.Currencies[x], + asset.Spot).String() + instrumentsToUse = append(instrumentsToUse, + c.instrumentMap.LookupID(curr)) + } + } else { + instrumentsToUse = c.instrumentMap.GetInstrumentIDs() + } - var allTheOrders []OrderResponse - for instrument, allInstrumentData := range instruments.Instruments { - for _, instrumentData := range allInstrumentData { - for _, currency := range getOrdersRequest.Currencies { - currStr := fmt.Sprintf("%v%v%v", - currency.Base.String(), - c.ConfigCurrencyPairFormat.Delimiter, - currency.Quote.String()) - if strings.EqualFold(currStr, instrument) { - openOrders, err := c.GetOpenOrders(instrumentData.InstID) - if err != nil { - return nil, err - } - allTheOrders = append(allTheOrders, openOrders.Orders...) + if len(instrumentsToUse) == 0 { + return nil, errors.New("no instrument IDs to use") + } - continue - } + for x := range instrumentsToUse { + openOrders, err := c.GetOpenOrders(instrumentsToUse[x]) + if err != nil { + return nil, err + } + for y := range openOrders.Orders { + curr := c.instrumentMap.LookupInstrument(instrumentsToUse[x]) + p := currency.NewPairFromFormattedPairs(curr, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)) + orderSide := order.Side(strings.ToUpper(openOrders.Orders[y].Side)) + orderDate := time.Unix(openOrders.Orders[y].Timestamp, 0) + orders = append(orders, order.Detail{ + ID: strconv.FormatInt(openOrders.Orders[y].OrderID, 10), + Amount: openOrders.Orders[y].Quantity, + Price: openOrders.Orders[y].Price, + Exchange: c.Name, + OrderSide: orderSide, + OrderDate: orderDate, + CurrencyPair: p, + }) } } } - var orders []exchange.OrderDetail - for _, order := range allTheOrders { - for instrument, allInstrumentData := range instruments.Instruments { - for _, instrumentData := range allInstrumentData { - if instrumentData.InstID == int(order.InstrumentID) { - currPair := currency.NewPairDelimiter(instrument, "") - orderSide := exchange.OrderSide(strings.ToUpper(order.Side)) - orderDate := time.Unix(order.Timestamp, 0) - orders = append(orders, exchange.OrderDetail{ - ID: strconv.FormatInt(order.OrderID, 10), - Amount: order.Quantity, - Price: order.Price, - Exchange: c.Name, - OrderSide: orderSide, - OrderDate: orderDate, - CurrencyPair: currPair, - }) - } - } - } - } - - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (c *COINUT) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - instruments, err := c.GetInstruments() +func (c *COINUT) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + err := c.loadInstrumentsIfNotLoaded() if err != nil { return nil, err } - - var allTheOrders []OrderFilledResponse - for instrument, allInstrumentData := range instruments.Instruments { - for _, instrumentData := range allInstrumentData { - for _, currency := range getOrdersRequest.Currencies { - currStr := fmt.Sprintf("%v%v%v", - currency.Base.String(), - c.ConfigCurrencyPairFormat.Delimiter, - currency.Quote.String()) - if strings.EqualFold(currStr, instrument) { - orders, err := c.GetTradeHistory(instrumentData.InstID, -1, -1) - if err != nil { - return nil, err - } - allTheOrders = append(allTheOrders, orders.Trades...) - - continue + var allOrders []order.Detail + if c.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + for i := range req.Currencies { + for j := int64(0); ; j += 100 { + trades, err := c.wsGetTradeHistory(req.Currencies[i], j, 100) + if err != nil { + return allOrders, err } - } - } - } - - var orders []exchange.OrderDetail - for i := range allTheOrders { - for instrument, allInstrumentData := range instruments.Instruments { - for j := range allInstrumentData { - if allInstrumentData[j].InstID == int(allTheOrders[i].Order.InstrumentID) { - currPair := currency.NewPairDelimiter(instrument, "") - orderSide := exchange.OrderSide(strings.ToUpper(allTheOrders[i].Order.Side)) - orderDate := time.Unix(allTheOrders[i].Order.Timestamp, 0) - orders = append(orders, exchange.OrderDetail{ - ID: strconv.FormatInt(allTheOrders[i].Order.OrderID, 10), - Amount: allTheOrders[i].Order.Quantity, - Price: allTheOrders[i].Order.Price, - Exchange: c.Name, - OrderSide: orderSide, - OrderDate: orderDate, - CurrencyPair: currPair, + for x := range trades.Trades { + curr := c.instrumentMap.LookupInstrument(trades.Trades[x].InstID) + allOrders = append(allOrders, order.Detail{ + Exchange: c.Name, + ID: strconv.FormatInt(trades.Trades[x].OrderID, 10), + CurrencyPair: currency.NewPairFromString(curr), + OrderSide: order.Side(trades.Trades[x].Side), + OrderDate: time.Unix(0, trades.Trades[x].Timestamp), + Status: order.Filled, + Price: trades.Trades[x].Price, + Amount: trades.Trades[x].Qty, + ExecutedAmount: trades.Trades[x].Qty, + RemainingAmount: trades.Trades[x].OpenQty, }) } + if len(trades.Trades) < 100 { + break + } + } + } + } else { + var instrumentsToUse []int64 + if len(req.Currencies) > 0 { + for x := range req.Currencies { + curr := c.FormatExchangeCurrency(req.Currencies[x], + asset.Spot).String() + instrumentID := c.instrumentMap.LookupID(curr) + if instrumentID > 0 { + instrumentsToUse = append(instrumentsToUse, instrumentID) + } + } + } else { + instrumentsToUse = c.instrumentMap.GetInstrumentIDs() + } + + if len(instrumentsToUse) == 0 { + return nil, errors.New("no instrument IDs to use") + } + for x := range instrumentsToUse { + orders, err := c.GetTradeHistory(instrumentsToUse[x], -1, -1) + if err != nil { + return nil, err + } + for y := range orders.Trades { + curr := c.instrumentMap.LookupInstrument(instrumentsToUse[x]) + p := currency.NewPairFromFormattedPairs(curr, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)) + orderSide := order.Side(strings.ToUpper(orders.Trades[y].Order.Side)) + orderDate := time.Unix(orders.Trades[y].Order.Timestamp, 0) + allOrders = append(allOrders, order.Detail{ + ID: strconv.FormatInt(orders.Trades[y].Order.OrderID, 10), + Amount: orders.Trades[y].Order.Quantity, + Price: orders.Trades[y].Order.Price, + Exchange: c.Name, + OrderSide: orderSide, + OrderDate: orderDate, + CurrencyPair: p, + }) } } } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - - return orders, nil + order.FilterOrdersByTickRange(&allOrders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&allOrders, req.OrderSide) + return allOrders, nil } // SubscribeToWebsocketChannels appends to ChannelsToSubscribe @@ -525,3 +869,20 @@ func (c *COINUT) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, e func (c *COINUT) AuthenticateWebsocket() error { return c.wsAuthenticate() } + +func (c *COINUT) loadInstrumentsIfNotLoaded() error { + if !c.instrumentMap.IsLoaded() { + if c.Websocket.IsConnected() { + _, err := c.WsGetInstruments() + if err != nil { + return err + } + } else { + err := c.SeedInstruments() + if err != nil { + return err + } + } + } + return nil +} diff --git a/exchanges/exchange.go b/exchanges/exchange.go index e6225344..2c71fafb 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -5,18 +5,15 @@ import ( "fmt" "net/http" "net/url" - "sort" "strings" - "sync" "time" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -24,8 +21,6 @@ const ( warningBase64DecryptSecretKeyFailed = "exchange %s unable to base64 decode secret key.. Disabling Authenticated API support" // nolint:gosec // WarningAuthenticatedRequestWithoutCredentialsSet error message for authenticated request without credentials set WarningAuthenticatedRequestWithoutCredentialsSet = "exchange %s authenticated HTTP request called but not supported due to unset/default API keys" - // ErrExchangeNotFound is a stand for an error message - ErrExchangeNotFound = "exchange not found in dataset" // DefaultHTTPTimeout is the default HTTP/HTTPS Timeout for exchange requests DefaultHTTPTimeout = time.Second * 15 // DefaultWebsocketResponseCheckTimeout is the default delay in checking for an expected websocket response @@ -36,362 +31,37 @@ const ( DefaultWebsocketOrderbookBufferLimit = 5 ) -// FeeType custom type for calculating fees based on method -type FeeType uint8 - -// Const declarations for fee types -const ( - BankFee FeeType = iota - InternationalBankDepositFee - InternationalBankWithdrawalFee - CryptocurrencyTradeFee - CyptocurrencyDepositFee - CryptocurrencyWithdrawalFee - OfflineTradeFee -) - -// InternationalBankTransactionType custom type for calculating fees based on fiat transaction types -type InternationalBankTransactionType uint8 - -// Const declarations for international transaction types -const ( - WireTransfer InternationalBankTransactionType = iota - PerfectMoney - Neteller - AdvCash - Payeer - Skrill - Simplex - SEPA - Swift - RapidTransfer - MisterTangoSEPA - Qiwi - VisaMastercard - WebMoney - Capitalist - WesternUnion - MoneyGram - Contact -) - -// SubmitOrderResponse is what is returned after submitting an order to an -// exchange -type SubmitOrderResponse struct { - IsOrderPlaced bool - OrderID string -} - -// FeeBuilder is the type which holds all parameters required to calculate a fee -// for an exchange -type FeeBuilder struct { - IsMaker bool - PurchasePrice float64 - Amount float64 - FeeType FeeType - FiatCurrency currency.Code - BankTransactionType InternationalBankTransactionType - Pair currency.Pair -} - -// OrderCancellation type required when requesting to cancel an order -type OrderCancellation struct { - AccountID string - OrderID string - WalletAddress string - Side OrderSide - CurrencyPair currency.Pair -} - -// WithdrawRequest used for wrapper crypto and FIAT withdraw methods -type WithdrawRequest struct { - // General withdraw information - Description string - OneTimePassword int64 - AccountID string - PIN int64 - TradePassword string - Amount float64 - Currency currency.Code - // Crypto related information - Address string - AddressTag string - FeeAmount float64 - // FIAT related information - BankAccountName string - BankAccountNumber float64 - BankName string - BankAddress string - BankCity string - BankCountry string - BankPostalCode string - SwiftCode string - IBAN string - BankCode float64 - IsExpressWire bool - // Intermediary bank information - RequiresIntermediaryBank bool - IntermediaryBankAccountNumber float64 - IntermediaryBankName string - IntermediaryBankAddress string - IntermediaryBankCity string - IntermediaryBankCountry string - IntermediaryBankPostalCode string - IntermediarySwiftCode string - IntermediaryBankCode float64 - IntermediaryIBAN string - WireCurrency string -} - -// Definitions for each type of withdrawal method for a given exchange -const ( - // No withdraw - NoAPIWithdrawalMethods uint32 = 0 - NoAPIWithdrawalMethodsText string = "NONE, WEBSITE ONLY" - AutoWithdrawCrypto uint32 = (1 << 0) - AutoWithdrawCryptoWithAPIPermission uint32 = (1 << 1) - AutoWithdrawCryptoWithSetup uint32 = (1 << 2) - AutoWithdrawCryptoText string = "AUTO WITHDRAW CRYPTO" - AutoWithdrawCryptoWithAPIPermissionText string = "AUTO WITHDRAW CRYPTO WITH API PERMISSION" - AutoWithdrawCryptoWithSetupText string = "AUTO WITHDRAW CRYPTO WITH SETUP" - WithdrawCryptoWith2FA uint32 = (1 << 3) - WithdrawCryptoWithSMS uint32 = (1 << 4) - WithdrawCryptoWithEmail uint32 = (1 << 5) - WithdrawCryptoWithWebsiteApproval uint32 = (1 << 6) - WithdrawCryptoWithAPIPermission uint32 = (1 << 7) - WithdrawCryptoWith2FAText string = "WITHDRAW CRYPTO WITH 2FA" - WithdrawCryptoWithSMSText string = "WITHDRAW CRYPTO WITH SMS" - WithdrawCryptoWithEmailText string = "WITHDRAW CRYPTO WITH EMAIL" - WithdrawCryptoWithWebsiteApprovalText string = "WITHDRAW CRYPTO WITH WEBSITE APPROVAL" - WithdrawCryptoWithAPIPermissionText string = "WITHDRAW CRYPTO WITH API PERMISSION" - AutoWithdrawFiat uint32 = (1 << 8) - AutoWithdrawFiatWithAPIPermission uint32 = (1 << 9) - AutoWithdrawFiatWithSetup uint32 = (1 << 10) - AutoWithdrawFiatText string = "AUTO WITHDRAW FIAT" - AutoWithdrawFiatWithAPIPermissionText string = "AUTO WITHDRAW FIAT WITH API PERMISSION" - AutoWithdrawFiatWithSetupText string = "AUTO WITHDRAW FIAT WITH SETUP" - WithdrawFiatWith2FA uint32 = (1 << 11) - WithdrawFiatWithSMS uint32 = (1 << 12) - WithdrawFiatWithEmail uint32 = (1 << 13) - WithdrawFiatWithWebsiteApproval uint32 = (1 << 14) - WithdrawFiatWithAPIPermission uint32 = (1 << 15) - WithdrawFiatWith2FAText string = "WITHDRAW FIAT WITH 2FA" - WithdrawFiatWithSMSText string = "WITHDRAW FIAT WITH SMS" - WithdrawFiatWithEmailText string = "WITHDRAW FIAT WITH EMAIL" - WithdrawFiatWithWebsiteApprovalText string = "WITHDRAW FIAT WITH WEBSITE APPROVAL" - WithdrawFiatWithAPIPermissionText string = "WITHDRAW FIAT WITH API PERMISSION" - WithdrawCryptoViaWebsiteOnly uint32 = (1 << 16) - WithdrawFiatViaWebsiteOnly uint32 = (1 << 17) - WithdrawCryptoViaWebsiteOnlyText string = "WITHDRAW CRYPTO VIA WEBSITE ONLY" - WithdrawFiatViaWebsiteOnlyText string = "WITHDRAW FIAT VIA WEBSITE ONLY" - NoFiatWithdrawals uint32 = (1 << 18) - NoFiatWithdrawalsText string = "NO FIAT WITHDRAWAL" - - UnknownWithdrawalTypeText string = "UNKNOWN" - - RestAuthentication uint8 = 0 - WebsocketAuthentication uint8 = 1 -) - -// AccountInfo is a Generic type to hold each exchange's holdings in -// all enabled currencies -type AccountInfo struct { - Exchange string - Accounts []Account -} - -// Account defines a singular account type with asocciated currencies -type Account struct { - ID string - Currencies []AccountCurrencyInfo -} - -// AccountCurrencyInfo is a sub type to store currency name and value -type AccountCurrencyInfo struct { - CurrencyName currency.Code - TotalValue float64 - Hold float64 -} - -// TradeHistory holds exchange history data -type TradeHistory struct { - Timestamp time.Time - TID int64 - Price float64 - Amount float64 - Exchange string - Type string - Fee float64 - Description string -} - -// OrderDetail holds order detail data -type OrderDetail struct { - Exchange string - AccountID string - ID string - CurrencyPair currency.Pair - OrderSide OrderSide - OrderType OrderType - OrderDate time.Time - Status string - Price float64 - Amount float64 - ExecutedAmount float64 - RemainingAmount float64 - Fee float64 - Trades []TradeHistory -} - -// FundHistory holds exchange funding history data -type FundHistory struct { - ExchangeName string - Status string - TransferID string - Description string - Timestamp time.Time - Currency string - Amount float64 - Fee float64 - TransferType string - CryptoToAddress string - CryptoFromAddress string - CryptoTxID string - BankTo string - BankFrom string -} - -// Base stores the individual exchange information -type Base struct { - Name string - Enabled bool - Verbose bool - RESTPollingDelay time.Duration - WebsocketResponseCheckTimeout time.Duration - WebsocketResponseMaxLimit time.Duration - WebsocketOrderbookBufferLimit int64 - AuthenticatedAPISupport bool - AuthenticatedWebsocketAPISupport bool - APIWithdrawPermissions uint32 - APIAuthPEMKeySupport bool - APISecret, APIKey, APIAuthPEMKey, ClientID string - TakerFee, MakerFee, Fee float64 - BaseCurrencies currency.Currencies - AvailablePairs currency.Pairs - EnabledPairs currency.Pairs - AssetTypes []string - PairsLastUpdated int64 - SupportsAutoPairUpdating bool - SupportsRESTTickerBatching bool - HTTPTimeout time.Duration - HTTPUserAgent string - HTTPDebugging bool - HTTPRecording bool - WebsocketURL string - APIUrl string - APIUrlDefault string - APIUrlSecondary string - APIUrlSecondaryDefault string - RequestCurrencyPairFormat config.CurrencyPairFormatConfig - ConfigCurrencyPairFormat config.CurrencyPairFormatConfig - Websocket *wshandler.Websocket - *request.Requester -} - -// IBotExchange enforces standard functions for all exchanges supported in -// GoCryptoTrader -type IBotExchange interface { - Setup(exch *config.ExchangeConfig) - Start(wg *sync.WaitGroup) - SetDefaults() - GetName() string - IsEnabled() bool - SetEnabled(bool) - GetTickerPrice(currency currency.Pair, assetType string) (ticker.Price, error) - UpdateTicker(currency currency.Pair, assetType string) (ticker.Price, error) - GetOrderbookEx(currency currency.Pair, assetType string) (orderbook.Base, error) - UpdateOrderbook(currency currency.Pair, assetType string) (orderbook.Base, error) - GetEnabledCurrencies() currency.Pairs - GetAvailableCurrencies() currency.Pairs - GetAssetTypes() []string - GetAccountInfo() (AccountInfo, error) - GetAuthenticatedAPISupport(endpoint uint8) bool - SetCurrencies(pairs []currency.Pair, enabledPairs bool) error - GetExchangeHistory(p currency.Pair, assetType string) ([]TradeHistory, error) - SupportsAutoPairUpdates() bool - GetLastPairsUpdateTime() int64 - SupportsRESTTickerBatchUpdates() bool - GetFeeByType(feeBuilder *FeeBuilder) (float64, error) - GetWithdrawPermissions() uint32 - FormatWithdrawPermissions() string - SupportsWithdrawPermissions(permissions uint32) bool - GetFundingHistory() ([]FundHistory, error) - SubmitOrder(p currency.Pair, side OrderSide, orderType OrderType, amount, price float64, clientID string) (SubmitOrderResponse, error) - ModifyOrder(action *ModifyOrder) (string, error) - CancelOrder(order *OrderCancellation) error - CancelAllOrders(orders *OrderCancellation) (CancelAllOrdersResponse, error) - GetOrderInfo(orderID string) (OrderDetail, error) - GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) - GetOrderHistory(getOrdersRequest *GetOrdersRequest) ([]OrderDetail, error) - GetActiveOrders(getOrdersRequest *GetOrdersRequest) ([]OrderDetail, error) - WithdrawCryptocurrencyFunds(withdrawRequest *WithdrawRequest) (string, error) - WithdrawFiatFunds(withdrawRequest *WithdrawRequest) (string, error) - WithdrawFiatFundsToInternationalBank(withdrawRequest *WithdrawRequest) (string, error) - GetWebsocket() (*wshandler.Websocket, error) - SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error - UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error - AuthenticateWebsocket() error - GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) -} - -// SupportsRESTTickerBatchUpdates returns whether or not the -// exhange supports REST batch ticker fetching -func (e *Base) SupportsRESTTickerBatchUpdates() bool { - return e.SupportsRESTTickerBatching +func (e *Base) checkAndInitRequester() { + if e.Requester == nil { + e.Requester = request.New(e.Name, + request.NewRateLimit(time.Second, 0), + request.NewRateLimit(time.Second, 0), + new(http.Client)) + } } // SetHTTPClientTimeout sets the timeout value for the exchanges // HTTP Client func (e *Base) SetHTTPClientTimeout(t time.Duration) { - if e.Requester == nil { - e.Requester = request.New(e.Name, - request.NewRateLimit(time.Second, 0), - request.NewRateLimit(time.Second, 0), - new(http.Client)) - } + e.checkAndInitRequester() e.Requester.HTTPClient.Timeout = t } // SetHTTPClient sets exchanges HTTP client func (e *Base) SetHTTPClient(h *http.Client) { - if e.Requester == nil { - e.Requester = request.New(e.Name, - request.NewRateLimit(time.Second, 0), - request.NewRateLimit(time.Second, 0), - new(http.Client)) - } + e.checkAndInitRequester() e.Requester.HTTPClient = h } // GetHTTPClient gets the exchanges HTTP client func (e *Base) GetHTTPClient() *http.Client { - if e.Requester == nil { - e.Requester = request.New(e.Name, - request.NewRateLimit(time.Second, 0), - request.NewRateLimit(time.Second, 0), - new(http.Client)) - } + e.checkAndInitRequester() return e.Requester.HTTPClient } // SetHTTPClientUserAgent sets the exchanges HTTP user agent func (e *Base) SetHTTPClientUserAgent(ua string) { - if e.Requester == nil { - e.Requester = request.New(e.Name, - request.NewRateLimit(time.Second, 0), - request.NewRateLimit(time.Second, 0), - new(http.Client)) - } + e.checkAndInitRequester() e.Requester.UserAgent = ua e.HTTPUserAgent = ua } @@ -410,11 +80,9 @@ func (e *Base) SetClientProxyAddress(addr string) error { err) } - err = e.Requester.SetProxy(proxy) - if err != nil { - return fmt.Errorf("exchange.go - setting proxy address error %s", - err) - } + // No needs to check err here as the only err condition is an empty + // string which is already checked above + _ = e.Requester.SetProxy(proxy) if e.Websocket != nil { err = e.Websocket.SetProxyAddress(addr) @@ -426,88 +94,150 @@ func (e *Base) SetClientProxyAddress(addr string) error { return nil } -// SetAutoPairDefaults sets the default values for whether or not the exchange -// supports auto pair updating or not -func (e *Base) SetAutoPairDefaults() error { - cfg := config.GetConfig() - exch, err := cfg.GetExchangeConfig(e.Name) - if err != nil { - return err - } - - update := false - if e.SupportsAutoPairUpdating { - if !exch.SupportsAutoPairUpdates { - exch.SupportsAutoPairUpdates = true - exch.PairsLastUpdated = 0 - update = true +// SetFeatureDefaults sets the exchanges default feature +// support set +func (e *Base) SetFeatureDefaults() { + if e.Config.Features == nil { + s := &config.FeaturesConfig{ + Supports: config.FeaturesSupportedConfig{ + Websocket: e.Features.Supports.Websocket, + REST: e.Features.Supports.REST, + RESTCapabilities: protocol.Features{ + AutoPairUpdates: e.Features.Supports.RESTCapabilities.AutoPairUpdates, + }, + }, } + + if e.Config.SupportsAutoPairUpdates != nil { + s.Supports.RESTCapabilities.AutoPairUpdates = *e.Config.SupportsAutoPairUpdates + s.Enabled.AutoPairUpdates = *e.Config.SupportsAutoPairUpdates + } else { + s.Supports.RESTCapabilities.AutoPairUpdates = e.Features.Supports.RESTCapabilities.AutoPairUpdates + s.Enabled.AutoPairUpdates = e.Features.Supports.RESTCapabilities.AutoPairUpdates + if !s.Supports.RESTCapabilities.AutoPairUpdates { + e.Config.CurrencyPairs.LastUpdated = time.Now().Unix() + e.CurrencyPairs.LastUpdated = e.Config.CurrencyPairs.LastUpdated + } + } + e.Config.Features = s + e.Config.SupportsAutoPairUpdates = nil } else { - if exch.PairsLastUpdated == 0 { - exch.PairsLastUpdated = time.Now().Unix() - e.PairsLastUpdated = exch.PairsLastUpdated - update = true + if e.Features.Supports.RESTCapabilities.AutoPairUpdates != e.Config.Features.Supports.RESTCapabilities.AutoPairUpdates { + e.Config.Features.Supports.RESTCapabilities.AutoPairUpdates = e.Features.Supports.RESTCapabilities.AutoPairUpdates + + if !e.Config.Features.Supports.RESTCapabilities.AutoPairUpdates { + e.Config.CurrencyPairs.LastUpdated = time.Now().Unix() + } } + + if e.Features.Supports.REST != e.Config.Features.Supports.REST { + e.Config.Features.Supports.REST = e.Features.Supports.REST + } + + if e.Features.Supports.RESTCapabilities.TickerBatching != e.Config.Features.Supports.RESTCapabilities.TickerBatching { + e.Config.Features.Supports.RESTCapabilities.TickerBatching = e.Features.Supports.RESTCapabilities.TickerBatching + } + + if e.Features.Supports.Websocket != e.Config.Features.Supports.Websocket { + e.Config.Features.Supports.Websocket = e.Features.Supports.Websocket + } + + e.Features.Enabled.AutoPairUpdates = e.Config.Features.Enabled.AutoPairUpdates + } +} + +// SetAPICredentialDefaults sets the API Credential validator defaults +func (e *Base) SetAPICredentialDefaults() { + // Exchange hardcoded settings take precedence and overwrite the config settings + if e.Config.API.CredentialsValidator == nil { + e.Config.API.CredentialsValidator = new(config.APICredentialsValidatorConfig) + } + if e.Config.API.CredentialsValidator.RequiresKey != e.API.CredentialsValidator.RequiresKey { + e.Config.API.CredentialsValidator.RequiresKey = e.API.CredentialsValidator.RequiresKey } - if update { - return cfg.UpdateExchangeConfig(&exch) + if e.Config.API.CredentialsValidator.RequiresSecret != e.API.CredentialsValidator.RequiresSecret { + e.Config.API.CredentialsValidator.RequiresSecret = e.API.CredentialsValidator.RequiresSecret } - return nil + + if e.Config.API.CredentialsValidator.RequiresBase64DecodeSecret != e.API.CredentialsValidator.RequiresBase64DecodeSecret { + e.Config.API.CredentialsValidator.RequiresBase64DecodeSecret = e.API.CredentialsValidator.RequiresBase64DecodeSecret + } + + if e.Config.API.CredentialsValidator.RequiresClientID != e.API.CredentialsValidator.RequiresClientID { + e.Config.API.CredentialsValidator.RequiresClientID = e.API.CredentialsValidator.RequiresClientID + } + + if e.Config.API.CredentialsValidator.RequiresPEM != e.API.CredentialsValidator.RequiresPEM { + e.Config.API.CredentialsValidator.RequiresPEM = e.API.CredentialsValidator.RequiresPEM + } +} + +// SetHTTPRateLimiter sets the exchanges default HTTP rate limiter and updates the exchange's config +// to default settings if it doesn't exist +func (e *Base) SetHTTPRateLimiter() { + e.checkAndInitRequester() + + if e.RequiresRateLimiter() { + if e.Config.HTTPRateLimiter == nil { + e.Config.HTTPRateLimiter = new(config.HTTPRateLimitConfig) + e.Config.HTTPRateLimiter.Authenticated.Duration = e.GetRateLimit(true).Duration + e.Config.HTTPRateLimiter.Authenticated.Rate = e.GetRateLimit(true).Rate + e.Config.HTTPRateLimiter.Unauthenticated.Duration = e.GetRateLimit(false).Duration + e.Config.HTTPRateLimiter.Unauthenticated.Rate = e.GetRateLimit(false).Rate + } else { + e.SetRateLimit(true, e.Config.HTTPRateLimiter.Authenticated.Duration, + e.Config.HTTPRateLimiter.Authenticated.Rate) + e.SetRateLimit(false, e.Config.HTTPRateLimiter.Unauthenticated.Duration, + e.Config.HTTPRateLimiter.Unauthenticated.Rate) + } + } +} + +// SupportsRESTTickerBatchUpdates returns whether or not the +// exhange supports REST batch ticker fetching +func (e *Base) SupportsRESTTickerBatchUpdates() bool { + return e.Features.Supports.RESTCapabilities.TickerBatching } // SupportsAutoPairUpdates returns whether or not the exchange supports // auto currency pair updating func (e *Base) SupportsAutoPairUpdates() bool { - return e.SupportsAutoPairUpdating + if e.Features.Supports.RESTCapabilities.AutoPairUpdates || e.Features.Supports.WebsocketCapabilities.AutoPairUpdates { + return true + } + return false } // GetLastPairsUpdateTime returns the unix timestamp of when the exchanges // currency pairs were last updated func (e *Base) GetLastPairsUpdateTime() int64 { - return e.PairsLastUpdated + return e.CurrencyPairs.LastUpdated } // SetAssetTypes checks the exchange asset types (whether it supports SPOT, // Binary or Futures) and sets it to a default setting if it doesn't exist -func (e *Base) SetAssetTypes() error { - cfg := config.GetConfig() - exch, err := cfg.GetExchangeConfig(e.Name) - if err != nil { - return err +func (e *Base) SetAssetTypes() { + if e.Config.CurrencyPairs.AssetTypes.JoinToString(",") == "" { + e.Config.CurrencyPairs.AssetTypes = e.CurrencyPairs.AssetTypes + } else if e.Config.CurrencyPairs.AssetTypes.JoinToString(",") != e.CurrencyPairs.AssetTypes.JoinToString(",") { + e.Config.CurrencyPairs.AssetTypes = e.CurrencyPairs.AssetTypes } - - var update bool - if exch.AssetTypes == "" { - exch.AssetTypes = common.JoinStrings(e.AssetTypes, ",") - update = true - } else { - e.AssetTypes = common.SplitStrings(exch.AssetTypes, ",") - update = true - } - - if update { - return cfg.UpdateExchangeConfig(&exch) - } - - return nil } // GetAssetTypes returns the available asset types for an individual exchange -func (e *Base) GetAssetTypes() []string { - return e.AssetTypes +func (e *Base) GetAssetTypes() asset.Items { + return e.CurrencyPairs.AssetTypes } -// GetExchangeAssetTypes returns the asset types the exchange supports (SPOT, -// binary, futures) -func GetExchangeAssetTypes(exchName string) ([]string, error) { - cfg := config.GetConfig() - exch, err := cfg.GetExchangeConfig(exchName) - if err != nil { - return nil, err +// GetPairAssetType returns the associated asset type for the currency pair +func (e *Base) GetPairAssetType(c currency.Pair) (asset.Item, error) { + for i := range e.GetAssetTypes() { + if e.GetEnabledPairs(e.GetAssetTypes()[i]).Contains(c, true) { + return e.GetAssetTypes()[i], nil + } } - - return common.SplitStrings(exch.AssetTypes, ","), nil + return "", errors.New("asset type not associated with currency pair") } // GetClientBankAccounts returns banking details associated with @@ -524,68 +254,41 @@ func (e *Base) GetExchangeBankAccounts(exchangeName, depositCurrency string) (co return cfg.GetExchangeBankAccounts(exchangeName, depositCurrency) } -// CompareCurrencyPairFormats checks and returns whether or not the two supplied -// config currency pairs match -func CompareCurrencyPairFormats(pair1 config.CurrencyPairFormatConfig, pair2 *config.CurrencyPairFormatConfig) bool { - if pair1.Delimiter != pair2.Delimiter || - pair1.Uppercase != pair2.Uppercase || - pair1.Separator != pair2.Separator || - pair1.Index != pair2.Index { - return false - } - return true -} - // SetCurrencyPairFormat checks the exchange request and config currency pair // formats and sets it to a default setting if it doesn't exist -func (e *Base) SetCurrencyPairFormat() error { - cfg := config.GetConfig() - exch, err := cfg.GetExchangeConfig(e.Name) - if err != nil { - return err +func (e *Base) SetCurrencyPairFormat() { + if e.Config.CurrencyPairs == nil { + e.Config.CurrencyPairs = new(currency.PairsManager) } - update := false - if exch.RequestCurrencyPairFormat == nil { - exch.RequestCurrencyPairFormat = &config.CurrencyPairFormatConfig{ - Delimiter: e.RequestCurrencyPairFormat.Delimiter, - Uppercase: e.RequestCurrencyPairFormat.Uppercase, - Separator: e.RequestCurrencyPairFormat.Separator, - Index: e.RequestCurrencyPairFormat.Index, + e.Config.CurrencyPairs.UseGlobalFormat = e.CurrencyPairs.UseGlobalFormat + if e.Config.CurrencyPairs.UseGlobalFormat { + if e.Config.CurrencyPairs.ConfigFormat == nil { + e.Config.CurrencyPairs.ConfigFormat = e.CurrencyPairs.ConfigFormat } - update = true - } else { - if CompareCurrencyPairFormats(e.RequestCurrencyPairFormat, - exch.RequestCurrencyPairFormat) { - e.RequestCurrencyPairFormat = *exch.RequestCurrencyPairFormat - } else { - *exch.RequestCurrencyPairFormat = e.RequestCurrencyPairFormat - update = true + if e.Config.CurrencyPairs.RequestFormat == nil { + e.Config.CurrencyPairs.RequestFormat = e.CurrencyPairs.RequestFormat } + return } - if exch.ConfigCurrencyPairFormat == nil { - exch.ConfigCurrencyPairFormat = &config.CurrencyPairFormatConfig{ - Delimiter: e.ConfigCurrencyPairFormat.Delimiter, - Uppercase: e.ConfigCurrencyPairFormat.Uppercase, - Separator: e.ConfigCurrencyPairFormat.Separator, - Index: e.ConfigCurrencyPairFormat.Index, - } - update = true - } else { - if CompareCurrencyPairFormats(e.ConfigCurrencyPairFormat, - exch.ConfigCurrencyPairFormat) { - e.ConfigCurrencyPairFormat = *exch.ConfigCurrencyPairFormat - } else { - *exch.ConfigCurrencyPairFormat = e.ConfigCurrencyPairFormat - update = true - } + if e.Config.CurrencyPairs.ConfigFormat != nil { + e.Config.CurrencyPairs.ConfigFormat = nil + } + if e.Config.CurrencyPairs.RequestFormat != nil { + e.Config.CurrencyPairs.RequestFormat = nil } - if update { - return cfg.UpdateExchangeConfig(&exch) + assetTypes := e.GetAssetTypes() + for x := range assetTypes { + if e.Config.CurrencyPairs.Get(assetTypes[x]) == nil { + r := e.CurrencyPairs.Get(assetTypes[x]) + if r == nil { + continue + } + e.Config.CurrencyPairs.Store(assetTypes[x], *e.CurrencyPairs.Get(assetTypes[x])) + } } - return nil } // GetAuthenticatedAPISupport returns whether the exchange supports @@ -593,9 +296,9 @@ func (e *Base) SetCurrencyPairFormat() error { func (e *Base) GetAuthenticatedAPISupport(endpoint uint8) bool { switch endpoint { case RestAuthentication: - return e.AuthenticatedAPISupport + return e.API.AuthenticatedSupport case WebsocketAuthentication: - return e.AuthenticatedWebsocketAPISupport + return e.API.AuthenticatedWebsocketSupport } return false } @@ -605,82 +308,82 @@ func (e *Base) GetName() string { return e.Name } -// GetEnabledCurrencies is a method that returns the enabled currency pairs of -// the exchange base -func (e *Base) GetEnabledCurrencies() currency.Pairs { - return e.EnabledPairs.Format(e.ConfigCurrencyPairFormat.Delimiter, - e.ConfigCurrencyPairFormat.Index, - e.ConfigCurrencyPairFormat.Uppercase) +// GetEnabledFeatures returns the exchanges enabled features +func (e *Base) GetEnabledFeatures() FeaturesEnabled { + return e.Features.Enabled } -// GetAvailableCurrencies is a method that returns the available currency pairs -// of the exchange base -func (e *Base) GetAvailableCurrencies() currency.Pairs { - return e.AvailablePairs.Format(e.ConfigCurrencyPairFormat.Delimiter, - e.ConfigCurrencyPairFormat.Index, - e.ConfigCurrencyPairFormat.Uppercase) +// GetSupportedFeatures returns the exchanges supported features +func (e *Base) GetSupportedFeatures() FeaturesSupported { + return e.Features.Supports } -// SupportsCurrency returns true or not whether a currency pair exists in the +// GetPairFormat returns the pair format based on the exchange and +// asset type +func (e *Base) GetPairFormat(assetType asset.Item, requestFormat bool) currency.PairFormat { + if e.CurrencyPairs.UseGlobalFormat { + if requestFormat { + return *e.CurrencyPairs.RequestFormat + } + return *e.CurrencyPairs.ConfigFormat + } + + if requestFormat { + return *e.CurrencyPairs.Get(assetType).RequestFormat + } + return *e.CurrencyPairs.Get(assetType).ConfigFormat +} + +// GetEnabledPairs is a method that returns the enabled currency pairs of +// the exchange by asset type +func (e *Base) GetEnabledPairs(assetType asset.Item) currency.Pairs { + format := e.GetPairFormat(assetType, false) + pairs := e.CurrencyPairs.GetPairs(assetType, true) + return pairs.Format(format.Delimiter, format.Index, format.Uppercase) +} + +// GetAvailablePairs is a method that returns the available currency pairs +// of the exchange by asset type +func (e *Base) GetAvailablePairs(assetType asset.Item) currency.Pairs { + format := e.GetPairFormat(assetType, false) + pairs := e.CurrencyPairs.GetPairs(assetType, false) + return pairs.Format(format.Delimiter, format.Index, format.Uppercase) +} + +// SupportsPair returns true or not whether a currency pair exists in the // exchange available currencies or not -func (e *Base) SupportsCurrency(p currency.Pair, enabledPairs bool) bool { +func (e *Base) SupportsPair(p currency.Pair, enabledPairs bool, assetType asset.Item) bool { if enabledPairs { - return e.GetEnabledCurrencies().Contains(p, false) + return e.GetEnabledPairs(assetType).Contains(p, false) } - return e.GetAvailableCurrencies().Contains(p, false) + return e.GetAvailablePairs(assetType).Contains(p, false) } -// GetExchangeFormatCurrencySeperator returns whether or not a specific -// exchange contains a separator used for API requests -func GetExchangeFormatCurrencySeperator(exchName string) bool { - cfg := config.GetConfig() - exch, err := cfg.GetExchangeConfig(exchName) - if err != nil { - return false - } - - if exch.RequestCurrencyPairFormat.Separator != "" { - return true - } - return false -} - -// GetAndFormatExchangeCurrencies returns a pair.CurrencyItem string containing +// FormatExchangeCurrencies returns a string containing // the exchanges formatted currency pairs -func GetAndFormatExchangeCurrencies(exchName string, pairs []currency.Pair) (string, error) { - var currencyItems string - cfg := config.GetConfig() - exch, err := cfg.GetExchangeConfig(exchName) - if err != nil { - return currencyItems, err - } +func (e *Base) FormatExchangeCurrencies(pairs []currency.Pair, assetType asset.Item) (string, error) { + var currencyItems strings.Builder + pairFmt := e.GetPairFormat(assetType, true) for x := range pairs { - currencyItems += FormatExchangeCurrency(exchName, pairs[x]).String() + currencyItems.WriteString(e.FormatExchangeCurrency(pairs[x], assetType).String()) if x == len(pairs)-1 { continue } - currencyItems += exch.RequestCurrencyPairFormat.Separator + currencyItems.WriteString(pairFmt.Separator) } - return currencyItems, nil + + if currencyItems.Len() == 0 { + return "", errors.New("returned empty string") + } + return currencyItems.String(), nil } // FormatExchangeCurrency is a method that formats and returns a currency pair -// based on the exchange currency format -func FormatExchangeCurrency(exchName string, p currency.Pair) currency.Pair { - cfg := config.GetConfig() - exch, _ := cfg.GetExchangeConfig(exchName) - - return p.Format(exch.RequestCurrencyPairFormat.Delimiter, - exch.RequestCurrencyPairFormat.Uppercase) -} - -// FormatCurrency is a method that formats and returns a currency pair // based on the user currency display preferences -func FormatCurrency(p currency.Pair) currency.Pair { - cfg := config.GetConfig() - return p.Format(cfg.Currency.CurrencyPairFormat.Delimiter, - cfg.Currency.CurrencyPairFormat.Uppercase) +func (e *Base) FormatExchangeCurrency(p currency.Pair, assetType asset.Item) currency.Pair { + pairFmt := e.GetPairFormat(assetType, true) + return p.Format(pairFmt.Delimiter, pairFmt.Uppercase) } // SetEnabled is a method that sets if the exchange is enabled @@ -694,64 +397,184 @@ func (e *Base) IsEnabled() bool { } // SetAPIKeys is a method that sets the current API keys for the exchange -func (e *Base) SetAPIKeys(apiKey, apiSecret, clientID string, b64Decode bool) { - if !e.AuthenticatedAPISupport && !e.AuthenticatedWebsocketAPISupport { - return - } - e.APIKey = apiKey - e.ClientID = clientID - if b64Decode { - result, err := common.Base64Decode(apiSecret) +func (e *Base) SetAPIKeys(apiKey, apiSecret, clientID string) { + e.API.Credentials.Key = apiKey + e.API.Credentials.ClientID = clientID + + if e.API.CredentialsValidator.RequiresBase64DecodeSecret { + result, err := crypto.Base64Decode(apiSecret) if err != nil { - e.AuthenticatedAPISupport = false - e.AuthenticatedWebsocketAPISupport = false - log.Warnf(warningBase64DecryptSecretKeyFailed, e.Name) + e.API.AuthenticatedSupport = false + e.API.AuthenticatedWebsocketSupport = false + log.Warnf(log.ExchangeSys, warningBase64DecryptSecretKeyFailed, e.Name) + return } - e.APISecret = string(result) + e.API.Credentials.Secret = string(result) } else { - e.APISecret = apiSecret + e.API.Credentials.Secret = apiSecret } } -// SetCurrencies sets the exchange currency pairs for either enabledPairs or +// SetupDefaults sets the exchange settings based on the supplied config +func (e *Base) SetupDefaults(exch *config.ExchangeConfig) error { + e.Enabled = true + e.LoadedByConfig = true + e.Config = exch + e.Verbose = exch.Verbose + + e.API.AuthenticatedSupport = exch.API.AuthenticatedSupport + e.API.AuthenticatedWebsocketSupport = exch.API.AuthenticatedWebsocketSupport + if e.API.AuthenticatedSupport || e.API.AuthenticatedWebsocketSupport { + e.SetAPIKeys(exch.API.Credentials.Key, exch.API.Credentials.Secret, exch.API.Credentials.ClientID) + } + + if exch.HTTPTimeout <= time.Duration(0) { + exch.HTTPTimeout = DefaultHTTPTimeout + } else { + e.SetHTTPClientTimeout(exch.HTTPTimeout) + } + + if exch.CurrencyPairs == nil { + exch.CurrencyPairs = new(currency.PairsManager) + } + + e.HTTPDebugging = exch.HTTPDebugging + e.SetHTTPClientUserAgent(exch.HTTPUserAgent) + e.SetHTTPRateLimiter() + e.SetAssetTypes() + e.SetCurrencyPairFormat() + e.SetFeatureDefaults() + e.SetAPIURL() + e.SetAPICredentialDefaults() + e.SetClientProxyAddress(exch.ProxyAddress) + e.SetHTTPRateLimiter() + + e.BaseCurrencies = exch.BaseCurrencies + + assetTypes := e.GetAssetTypes() + for x := range assetTypes { + p := exch.CurrencyPairs.Get(assetTypes[x]) + if p != nil { + e.CurrencyPairs.Store(assetTypes[x], *p) + } + } + + if e.Features.Supports.Websocket { + return e.Websocket.Initialise() + } + return nil +} + +// AllowAuthenticatedRequest checks to see if the required fields have been set before sending an authenticated +// API request +func (e *Base) AllowAuthenticatedRequest() bool { + // Skip auth check + if e.SkipAuthCheck { + return true + } + + // Individual package usage, allow request if API credentials are valid a + // and without needing to set AuthenticatedSupport to true + if !e.LoadedByConfig && !e.ValidateAPICredentials() { + return false + } + + // Bot usage, AuthenticatedSupport can be disabled by user if desired, so don't + // allow authenticated requests. + if (!e.API.AuthenticatedSupport && !e.API.AuthenticatedWebsocketSupport) && e.LoadedByConfig { + return false + } + + // Check to see if the user has enabled AuthenticatedSupport, but has invalid + // API credentials set and loaded by config + if (e.API.AuthenticatedSupport || e.API.AuthenticatedWebsocketSupport) && e.LoadedByConfig && !e.ValidateAPICredentials() { + return false + } + + return true +} + +// ValidateAPICredentials validates the exchanges API credentials +func (e *Base) ValidateAPICredentials() bool { + if e.API.CredentialsValidator.RequiresKey { + if e.API.Credentials.Key == "" || + e.API.Credentials.Key == config.DefaultAPIKey { + log.Warnf(log.ExchangeSys, + "exchange %s requires API key but default/empty one set", + e.Name) + return false + } + } + + if e.API.CredentialsValidator.RequiresSecret { + if e.API.Credentials.Secret == "" || + e.API.Credentials.Secret == config.DefaultAPISecret { + log.Warnf(log.ExchangeSys, + "exchange %s requires API secret but default/empty one set", + e.Name) + return false + } + } + + if e.API.CredentialsValidator.RequiresPEM { + if e.API.Credentials.PEMKey == "" || + strings.Contains(e.API.Credentials.PEMKey, "JUSTADUMMY") { + log.Warnf(log.ExchangeSys, + "exchange %s requires API PEM key but default/empty one set", + e.Name) + return false + } + } + + if e.API.CredentialsValidator.RequiresClientID { + if e.API.Credentials.ClientID == "" || + e.API.Credentials.ClientID == config.DefaultAPIClientID { + log.Warnf(log.ExchangeSys, + "exchange %s requires API ClientID but default/empty one set", + e.Name) + return false + } + } + + if e.API.CredentialsValidator.RequiresBase64DecodeSecret && !e.LoadedByConfig { + _, err := crypto.Base64Decode(e.API.Credentials.Secret) + if err != nil { + log.Warnf(log.ExchangeSys, + "exchange %s API secret base64 decode failed: %s", + e.Name, err) + return false + } + } + return true +} + +// SetPairs sets the exchange currency pairs for either enabledPairs or // availablePairs -func (e *Base) SetCurrencies(pairs []currency.Pair, enabledPairs bool) error { +func (e *Base) SetPairs(pairs currency.Pairs, assetType asset.Item, enabled bool) error { if len(pairs) == 0 { - return fmt.Errorf("%s SetCurrencies error - pairs is empty", e.Name) - } - - cfg := config.GetConfig() - exchCfg, err := cfg.GetExchangeConfig(e.Name) - if err != nil { - return err + return fmt.Errorf("%s SetPairs error - pairs is empty", e.Name) } + pairFmt := e.GetPairFormat(assetType, false) var newPairs currency.Pairs for x := range pairs { - newPairs = append(newPairs, pairs[x].Format(exchCfg.ConfigCurrencyPairFormat.Delimiter, - exchCfg.ConfigCurrencyPairFormat.Uppercase)) + newPairs = append(newPairs, pairs[x].Format(pairFmt.Delimiter, + pairFmt.Uppercase)) } - if enabledPairs { - exchCfg.EnabledPairs = newPairs - e.EnabledPairs = newPairs - } else { - exchCfg.AvailablePairs = newPairs - e.AvailablePairs = newPairs - } - - return cfg.UpdateExchangeConfig(&exchCfg) + e.CurrencyPairs.StorePairs(assetType, newPairs, enabled) + e.Config.CurrencyPairs.StorePairs(assetType, newPairs, enabled) + return nil } -// UpdateCurrencies updates the exchange currency pairs for either enabledPairs or +// UpdatePairs updates the exchange currency pairs for either enabledPairs or // availablePairs -func (e *Base) UpdateCurrencies(exchangeProducts currency.Pairs, enabled, force bool) error { +func (e *Base) UpdatePairs(exchangeProducts currency.Pairs, assetType asset.Item, enabled, force bool) error { if len(exchangeProducts) == 0 { - return fmt.Errorf("%s UpdateCurrencies error - exchangeProducts is empty", e.Name) + return fmt.Errorf("%s UpdatePairs error - exchangeProducts is empty", e.Name) } exchangeProducts = exchangeProducts.Upper() - var products currency.Pairs for x := range exchangeProducts { if exchangeProducts[x].String() == "" { @@ -762,157 +585,111 @@ func (e *Base) UpdateCurrencies(exchangeProducts currency.Pairs, enabled, force var newPairs, removedPairs currency.Pairs var updateType string + targetPairs := e.CurrencyPairs.GetPairs(assetType, enabled) if enabled { - newPairs, removedPairs = e.EnabledPairs.FindDifferences(products) + newPairs, removedPairs = targetPairs.FindDifferences(products) updateType = "enabled" } else { - newPairs, removedPairs = e.AvailablePairs.FindDifferences(products) + newPairs, removedPairs = targetPairs.FindDifferences(products) updateType = "available" } if force || len(newPairs) > 0 || len(removedPairs) > 0 { - cfg := config.GetConfig() - exch, err := cfg.GetExchangeConfig(e.Name) - if err != nil { - return err - } - if force { - log.Debugf("%s forced update of %s pairs.", e.Name, updateType) + log.Debugf(log.ExchangeSys, + "%s forced update of %s [%v] pairs.", e.Name, updateType, + strings.ToUpper(assetType.String())) } else { if len(newPairs) > 0 { - log.Debugf("%s Updating pairs - New: %s.\n", e.Name, newPairs) + log.Debugf(log.ExchangeSys, + "%s Updating pairs [%v] - New: %s.\n", e.Name, + strings.ToUpper(assetType.String()), newPairs) } if len(removedPairs) > 0 { - log.Debugf("%s Updating pairs - Removed: %s.\n", e.Name, removedPairs) + log.Debugf(log.ExchangeSys, + "%s Updating pairs [%v] - Removed: %s.\n", e.Name, + strings.ToUpper(assetType.String()), removedPairs) } } - if enabled { - exch.EnabledPairs = products - e.EnabledPairs = products - } else { - exch.AvailablePairs = products - e.AvailablePairs = products - } - return cfg.UpdateExchangeConfig(&exch) + e.Config.CurrencyPairs.StorePairs(assetType, products, enabled) + e.CurrencyPairs.StorePairs(assetType, products, enabled) } return nil } -// ModifyOrder is a an order modifyer -type ModifyOrder struct { - OrderID string - OrderType - OrderSide - Price float64 - Amount float64 - LimitPriceUpper float64 - LimitPriceLower float64 - CurrencyPair currency.Pair - - ImmediateOrCancel bool - HiddenOrder bool - FillOrKill bool - PostOnly bool -} - -// ModifyOrderResponse is an order modifying return type -type ModifyOrderResponse struct { - OrderID string -} - -// Format holds exchange formatting -type Format struct { - ExchangeName string - OrderType map[string]string - OrderSide map[string]string -} - -// CancelAllOrdersResponse returns the status from attempting to cancel all orders on an exchange -type CancelAllOrdersResponse struct { - OrderStatus map[string]string -} - -// Formatting contain a range of exchanges formatting -type Formatting []Format - -// OrderType enforces a standard for Ordertypes across the code base -type OrderType string - -// OrderType ...types -const ( - AnyOrderType OrderType = "ANY" - LimitOrderType OrderType = "LIMIT" - MarketOrderType OrderType = "MARKET" - ImmediateOrCancelOrderType OrderType = "IMMEDIATE_OR_CANCEL" - StopOrderType OrderType = "STOP" - TrailingStopOrderType OrderType = "TRAILINGSTOP" - UnknownOrderType OrderType = "UNKNOWN" -) - -// ToString changes the ordertype to the exchange standard and returns a string -func (o OrderType) ToString() string { - return string(o) -} - -// OrderSide enforces a standard for OrderSides across the code base -type OrderSide string - -// OrderSide types -const ( - AnyOrderSide OrderSide = "ANY" - BuyOrderSide OrderSide = "BUY" - SellOrderSide OrderSide = "SELL" - BidOrderSide OrderSide = "BID" - AskOrderSide OrderSide = "ASK" -) - -// ToString changes the ordertype to the exchange standard and returns a string -func (o OrderSide) ToString() string { - return string(o) -} - // SetAPIURL sets configuration API URL for an exchange -func (e *Base) SetAPIURL(ec *config.ExchangeConfig) error { - if ec.APIURL == "" || ec.APIURLSecondary == "" { - return errors.New("empty config API URLs") +func (e *Base) SetAPIURL() error { + if e.Config.API.Endpoints.URL == "" || e.Config.API.Endpoints.URLSecondary == "" { + return fmt.Errorf("exchange %s: SetAPIURL error. URL vals are empty", e.Name) } - if ec.APIURL != config.APIURLNonDefaultMessage { - e.APIUrl = ec.APIURL + + checkInsecureEndpoint := func(endpoint string) { + if !strings.Contains(endpoint, "https") { + return + } + log.Warnf(log.ExchangeSys, + "%s is using HTTP instead of HTTPS [%s] for API functionality, an"+ + " attacker could eavesdrop on this connection. Use at your"+ + " own risk.", + e.Name, endpoint) } - if !strings.Contains(e.APIUrl, "https") { - log.Warnf("%s is using HTTP instead of HTTPS for API functionality, an attacker could eavesdrop on this connection. Use at your own risk", e.Name) + + if e.Config.API.Endpoints.URL != config.APIURLNonDefaultMessage { + e.API.Endpoints.URL = e.Config.API.Endpoints.URL + checkInsecureEndpoint(e.API.Endpoints.URL) } - if ec.APIURLSecondary != config.APIURLNonDefaultMessage { - e.APIUrlSecondary = ec.APIURLSecondary + if e.Config.API.Endpoints.URLSecondary != config.APIURLNonDefaultMessage { + e.API.Endpoints.URLSecondary = e.Config.API.Endpoints.URLSecondary + checkInsecureEndpoint(e.API.Endpoints.URLSecondary) } return nil } // GetAPIURL returns the set API URL func (e *Base) GetAPIURL() string { - return e.APIUrl + return e.API.Endpoints.URL } // GetSecondaryAPIURL returns the set Secondary API URL func (e *Base) GetSecondaryAPIURL() string { - return e.APIUrlSecondary + return e.API.Endpoints.URLSecondary } // GetAPIURLDefault returns exchange default URL func (e *Base) GetAPIURLDefault() string { - return e.APIUrlDefault + return e.API.Endpoints.URLDefault } // GetAPIURLSecondaryDefault returns exchange default secondary URL func (e *Base) GetAPIURLSecondaryDefault() string { - return e.APIUrlSecondaryDefault + return e.API.Endpoints.URLSecondaryDefault +} + +// SupportsWebsocket returns whether or not the exchange supports +// websocket +func (e *Base) SupportsWebsocket() bool { + return e.Features.Supports.Websocket +} + +// SupportsREST returns whether or not the exchange supports +// REST +func (e *Base) SupportsREST() bool { + return e.Features.Supports.REST +} + +// IsWebsocketEnabled returns whether or not the exchange has its +// websocket client enabled +func (e *Base) IsWebsocketEnabled() bool { + if e.Websocket != nil { + return e.Websocket.IsEnabled() + } + return false } // GetWithdrawPermissions passes through the exchange's withdraw permissions func (e *Base) GetWithdrawPermissions() uint32 { - return e.APIWithdrawPermissions + return e.Features.Supports.WithdrawPermissions } // SupportsWithdrawPermissions compares the supplied permissions with the exchange's to verify they're supported @@ -978,223 +755,19 @@ func (e *Base) FormatWithdrawPermissions() string { return NoAPIWithdrawalMethodsText } -// GetOrdersRequest used for GetOrderHistory and GetOpenOrders wrapper functions -type GetOrdersRequest struct { - OrderType OrderType - OrderSide OrderSide - StartTicks time.Time - EndTicks time.Time - // Currencies Empty array = all currencies. Some endpoints only support singular currency enquiries - Currencies []currency.Pair +// SupportsAsset whether or not the supplied asset is supported +// by the exchange +func (e *Base) SupportsAsset(a asset.Item) bool { + return e.CurrencyPairs.AssetTypes.Contains(a) } -// OrderStatus defines order status types -type OrderStatus string - -// All OrderStatus types -const ( - AnyOrderStatus OrderStatus = "ANY" - NewOrderStatus OrderStatus = "NEW" - ActiveOrderStatus OrderStatus = "ACTIVE" - PartiallyFilledOrderStatus OrderStatus = "PARTIALLY_FILLED" - FilledOrderStatus OrderStatus = "FILLED" - CancelledOrderStatus OrderStatus = "CANCELED" - PendingCancelOrderStatus OrderStatus = "PENDING_CANCEL" - RejectedOrderStatus OrderStatus = "REJECTED" - ExpiredOrderStatus OrderStatus = "EXPIRED" - HiddenOrderStatus OrderStatus = "HIDDEN" - UnknownOrderStatus OrderStatus = "UNKNOWN" -) - -// FilterOrdersBySide removes any OrderDetails that don't match the orderStatus provided -func FilterOrdersBySide(orders *[]OrderDetail, orderSide OrderSide) { - if orderSide == "" || orderSide == AnyOrderSide { - return - } - - var filteredOrders []OrderDetail - for i := range *orders { - if strings.EqualFold(string((*orders)[i].OrderSide), string(orderSide)) { - filteredOrders = append(filteredOrders, (*orders)[i]) - } - } - - *orders = filteredOrders -} - -// FilterOrdersByType removes any OrderDetails that don't match the orderType provided -func FilterOrdersByType(orders *[]OrderDetail, orderType OrderType) { - if orderType == "" || orderType == AnyOrderType { - return - } - - var filteredOrders []OrderDetail - for i := range *orders { - if strings.EqualFold(string((*orders)[i].OrderType), string(orderType)) { - filteredOrders = append(filteredOrders, (*orders)[i]) - } - } - - *orders = filteredOrders -} - -// FilterOrdersByTickRange removes any OrderDetails outside of the tick range -func FilterOrdersByTickRange(orders *[]OrderDetail, startTicks, endTicks time.Time) { - if startTicks.IsZero() || endTicks.IsZero() || - startTicks.Unix() == 0 || endTicks.Unix() == 0 || endTicks.Before(startTicks) { - return - } - - var filteredOrders []OrderDetail - for i := range *orders { - if (*orders)[i].OrderDate.Unix() >= startTicks.Unix() && (*orders)[i].OrderDate.Unix() <= endTicks.Unix() { - filteredOrders = append(filteredOrders, (*orders)[i]) - } - } - - *orders = filteredOrders -} - -// FilterOrdersByCurrencies removes any OrderDetails that do not match the provided currency list -// It is forgiving in that the provided currencies can match quote or base currencies -func FilterOrdersByCurrencies(orders *[]OrderDetail, currencies []currency.Pair) { - if len(currencies) == 0 { - return - } - - var filteredOrders []OrderDetail - for i := range *orders { - matchFound := false - for _, c := range currencies { - if !matchFound && (*orders)[i].CurrencyPair.EqualIncludeReciprocal(c) { - matchFound = true - } - } - - if matchFound { - filteredOrders = append(filteredOrders, (*orders)[i]) - } - } - - *orders = filteredOrders -} - -// ByPrice used for sorting orders by price -type ByPrice []OrderDetail - -func (b ByPrice) Len() int { - return len(b) -} - -func (b ByPrice) Less(i, j int) bool { - return b[i].Price < b[j].Price -} - -func (b ByPrice) Swap(i, j int) { - b[i], b[j] = b[j], b[i] -} - -// SortOrdersByPrice the caller function to sort orders -func SortOrdersByPrice(orders *[]OrderDetail, reverse bool) { - if reverse { - sort.Sort(sort.Reverse(ByPrice(*orders))) - } else { - sort.Sort(ByPrice(*orders)) +// PrintEnabledPairs prints the exchanges enabled asset pairs +func (e *Base) PrintEnabledPairs() { + for k, v := range e.CurrencyPairs.Pairs { + log.Infof(log.ExchangeSys, "%s Asset type %v:\n\t Enabled pairs: %v", + e.Name, strings.ToUpper(k.String()), v.Enabled) } } -// ByOrderType used for sorting orders by order type -type ByOrderType []OrderDetail - -func (b ByOrderType) Len() int { - return len(b) -} - -func (b ByOrderType) Less(i, j int) bool { - return b[i].OrderType.ToString() < b[j].OrderType.ToString() -} - -func (b ByOrderType) Swap(i, j int) { - b[i], b[j] = b[j], b[i] -} - -// SortOrdersByType the caller function to sort orders -func SortOrdersByType(orders *[]OrderDetail, reverse bool) { - if reverse { - sort.Sort(sort.Reverse(ByOrderType(*orders))) - } else { - sort.Sort(ByOrderType(*orders)) - } -} - -// ByCurrency used for sorting orders by order currency -type ByCurrency []OrderDetail - -func (b ByCurrency) Len() int { - return len(b) -} - -func (b ByCurrency) Less(i, j int) bool { - return b[i].CurrencyPair.String() < b[j].CurrencyPair.String() -} - -func (b ByCurrency) Swap(i, j int) { - b[i], b[j] = b[j], b[i] -} - -// SortOrdersByCurrency the caller function to sort orders -func SortOrdersByCurrency(orders *[]OrderDetail, reverse bool) { - if reverse { - sort.Sort(sort.Reverse(ByCurrency(*orders))) - } else { - sort.Sort(ByCurrency(*orders)) - } -} - -// ByDate used for sorting orders by order date -type ByDate []OrderDetail - -func (b ByDate) Len() int { - return len(b) -} - -func (b ByDate) Less(i, j int) bool { - return b[i].OrderDate.Unix() < b[j].OrderDate.Unix() -} - -func (b ByDate) Swap(i, j int) { - b[i], b[j] = b[j], b[i] -} - -// SortOrdersByDate the caller function to sort orders -func SortOrdersByDate(orders *[]OrderDetail, reverse bool) { - if reverse { - sort.Sort(sort.Reverse(ByDate(*orders))) - } else { - sort.Sort(ByDate(*orders)) - } -} - -// ByOrderSide used for sorting orders by order side (buy sell) -type ByOrderSide []OrderDetail - -func (b ByOrderSide) Len() int { - return len(b) -} - -func (b ByOrderSide) Less(i, j int) bool { - return b[i].OrderSide.ToString() < b[j].OrderSide.ToString() -} - -func (b ByOrderSide) Swap(i, j int) { - b[i], b[j] = b[j], b[i] -} - -// SortOrdersBySide the caller function to sort orders -func SortOrdersBySide(orders *[]OrderDetail, reverse bool) { - if reverse { - sort.Sort(sort.Reverse(ByOrderSide(*orders))) - } else { - sort.Sort(ByOrderSide(*orders)) - } -} +// GetBase returns the exchange base +func (e *Base) GetBase() *Base { return e } diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index 1aec6d20..b070e27b 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -9,9 +9,9 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -21,22 +21,33 @@ const ( ) func TestSupportsRESTTickerBatchUpdates(t *testing.T) { + t.Parallel() + b := Base{ - Name: "RAWR", - SupportsRESTTickerBatching: true, + Name: "RAWR", + Features: Features{ + Supports: FeaturesSupported{ + REST: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + }, + }, + }, } if !b.SupportsRESTTickerBatchUpdates() { - t.Fatal("Test failed. TestSupportsRESTTickerBatchUpdates returned false") + t.Fatal("TestSupportsRESTTickerBatchUpdates returned false") } } func TestHTTPClient(t *testing.T) { + t.Parallel() + r := Base{Name: "asdf"} r.SetHTTPClientTimeout(time.Second * 5) if r.GetHTTPClient().Timeout != time.Second*5 { - t.Fatalf("Test failed. TestHTTPClient unexpected value") + t.Fatalf("TestHTTPClient unexpected value") } r.Requester = nil @@ -45,12 +56,12 @@ func TestHTTPClient(t *testing.T) { r.SetHTTPClient(newClient) if r.GetHTTPClient().Timeout != time.Second*10 { - t.Fatalf("Test failed. TestHTTPClient unexpected value") + t.Fatalf("TestHTTPClient unexpected value") } r.Requester = nil if r.GetHTTPClient() == nil { - t.Fatalf("Test failed. TestHTTPClient unexpected value") + t.Fatalf("TestHTTPClient unexpected value") } b := Base{Name: "RAWR"} @@ -61,7 +72,7 @@ func TestHTTPClient(t *testing.T) { b.SetHTTPClientTimeout(time.Second * 5) if b.GetHTTPClient().Timeout != time.Second*5 { - t.Fatalf("Test failed. TestHTTPClient unexpected value") + t.Fatalf("TestHTTPClient unexpected value") } newClient = new(http.Client) @@ -69,540 +80,674 @@ func TestHTTPClient(t *testing.T) { b.SetHTTPClient(newClient) if b.GetHTTPClient().Timeout != time.Second*10 { - t.Fatalf("Test failed. TestHTTPClient unexpected value") + t.Fatalf("TestHTTPClient unexpected value") + } + + b.SetHTTPClientUserAgent("epicUserAgent") + if !strings.Contains(b.GetHTTPClientUserAgent(), "epicUserAgent") { + t.Error("user agent not set properly") } } func TestSetClientProxyAddress(t *testing.T) { - requester := request.New("testicles", + t.Parallel() + + requester := request.New("rawr", &request.RateLimit{}, &request.RateLimit{}, &http.Client{}) - newBase := Base{Name: "Testicles", Requester: requester} + newBase := Base{ + Name: "rawr", + Requester: requester} newBase.Websocket = wshandler.New() - err := newBase.SetClientProxyAddress(":invalid") if err == nil { - t.Error("Test failed. SetClientProxyAddress parsed invalid URL") + t.Error("SetClientProxyAddress parsed invalid URL") } if newBase.Websocket.GetProxyAddress() != "" { - t.Error("Test failed. SetClientProxyAddress error", err) + t.Error("SetClientProxyAddress error", err) } err = newBase.SetClientProxyAddress("www.valid.com") if err != nil { - t.Error("Test failed. SetClientProxyAddress error", err) + t.Error("SetClientProxyAddress error", err) + } + + // calling this again will cause the ws check to fail + err = newBase.SetClientProxyAddress("www.valid.com") + if err == nil { + t.Error("trying to set the same proxy addr should thrown an err for ws") } if newBase.Websocket.GetProxyAddress() != "www.valid.com" { - t.Error("Test failed. SetClientProxyAddress error", err) + t.Error("SetClientProxyAddress error", err) + } +} + +func TestSetFeatureDefaults(t *testing.T) { + t.Parallel() + + // Test nil features with basic support capabilities + b := Base{ + Config: &config.ExchangeConfig{ + CurrencyPairs: ¤cy.PairsManager{}, + }, + Features: Features{ + Supports: FeaturesSupported{ + REST: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + }, + Websocket: true, + }, + }, + } + b.SetFeatureDefaults() + if !b.Config.Features.Supports.REST && b.Config.CurrencyPairs.LastUpdated == 0 { + t.Error("incorrect values") + } + + // Test upgrade when SupportsAutoPairUpdates is enabled + bptr := func(a bool) *bool { return &a } + b.Config.Features = nil + b.Config.SupportsAutoPairUpdates = bptr(true) + b.SetFeatureDefaults() + if !b.Config.Features.Supports.RESTCapabilities.AutoPairUpdates && + !b.Features.Enabled.AutoPairUpdates { + t.Error("incorrect values") + } + + // Test non migrated features config + b.Config.Features.Supports.REST = false + b.Config.Features.Supports.RESTCapabilities.TickerBatching = false + b.Config.Features.Supports.Websocket = false + b.SetFeatureDefaults() + + if !b.Features.Supports.REST || + !b.Features.Supports.RESTCapabilities.TickerBatching || + !b.Features.Supports.Websocket { + t.Error("incorrect values") + } +} + +func TestSetAPICredentialDefaults(t *testing.T) { + t.Parallel() + + b := Base{ + Config: &config.ExchangeConfig{}, + } + b.API.CredentialsValidator.RequiresKey = true + b.API.CredentialsValidator.RequiresSecret = true + b.API.CredentialsValidator.RequiresBase64DecodeSecret = true + b.API.CredentialsValidator.RequiresClientID = true + b.API.CredentialsValidator.RequiresPEM = true + b.SetAPICredentialDefaults() + + if !b.Config.API.CredentialsValidator.RequiresKey || + !b.Config.API.CredentialsValidator.RequiresSecret || + !b.Config.API.CredentialsValidator.RequiresBase64DecodeSecret || + !b.Config.API.CredentialsValidator.RequiresClientID || + !b.Config.API.CredentialsValidator.RequiresPEM { + t.Error("incorrect values") + } +} + +func TestSetHTTPRateLimiter(t *testing.T) { + t.Parallel() + + b := Base{ + Config: &config.ExchangeConfig{}, + Requester: request.New("asdf", + request.NewRateLimit(time.Second*5, 10), + request.NewRateLimit(time.Second*10, 15), + common.NewHTTPClientWithTimeout(DefaultHTTPTimeout)), + } + b.SetHTTPRateLimiter() + if b.Requester.GetRateLimit(true).Duration.String() != "5s" && + b.Requester.GetRateLimit(true).Rate != 10 && + b.Requester.GetRateLimit(false).Duration.String() != "10s" && + b.Requester.GetRateLimit(false).Rate != 15 { + t.Error("rate limiter not set properly") + } + + b.Config.HTTPRateLimiter = &config.HTTPRateLimitConfig{ + Unauthenticated: config.HTTPRateConfig{ + Duration: time.Second * 100, + Rate: 100, + }, + Authenticated: config.HTTPRateConfig{ + Duration: time.Second * 110, + Rate: 150, + }, + } + b.SetHTTPRateLimiter() + if b.Requester.GetRateLimit(true).Duration.String() != "1m50s" && + b.Requester.GetRateLimit(true).Rate != 150 && + b.Requester.GetRateLimit(false).Duration.String() != "1m40s" && + b.Requester.GetRateLimit(false).Rate != 100 { + t.Error("rate limiter not set properly") } } func TestSetAutoPairDefaults(t *testing.T) { cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) + err := cfg.LoadConfig(config.TestFile, true) if err != nil { - t.Fatalf("Test failed. TestSetAutoPairDefaults failed to load config file. Error: %s", err) + t.Fatalf("TestSetAutoPairDefaults failed to load config file. Error: %s", err) } - b := Base{ - Name: "TESTNAME", - SupportsAutoPairUpdating: true, - } - - err = b.SetAutoPairDefaults() - if err == nil { - t.Fatal("Test failed. TestSetAutoPairDefaults returned nil error for a non-existent exchange") - } - - b.Name = "Bitstamp" - err = b.SetAutoPairDefaults() + exch, err := cfg.GetExchangeConfig("Bitstamp") if err != nil { - t.Fatalf("Test failed. TestSetAutoPairDefaults. Error %s", err) + t.Fatalf("TestSetAutoPairDefaults load config failed. Error %s", err) } - exch, err := cfg.GetExchangeConfig(b.Name) + if !exch.Features.Supports.RESTCapabilities.AutoPairUpdates { + t.Fatalf("TestSetAutoPairDefaults Incorrect value") + } + + if exch.CurrencyPairs.LastUpdated != 0 { + t.Fatalf("TestSetAutoPairDefaults Incorrect value") + } + + exch.Features.Supports.RESTCapabilities.AutoPairUpdates = false + cfg.UpdateExchangeConfig(exch) + + exch, err = cfg.GetExchangeConfig("Bitstamp") if err != nil { - t.Fatalf("Test failed. TestSetAutoPairDefaults load config failed. Error %s", err) + t.Fatalf("TestSetAutoPairDefaults load config failed. Error %s", err) } - if !exch.SupportsAutoPairUpdates { - t.Fatalf("Test failed. TestSetAutoPairDefaults Incorrect value") - } - - if exch.PairsLastUpdated != 0 { - t.Fatalf("Test failed. TestSetAutoPairDefaults Incorrect value") - } - - exch.SupportsAutoPairUpdates = false - err = cfg.UpdateExchangeConfig(&exch) - if err != nil { - t.Fatalf("Test failed. TestSetAutoPairDefaults update config failed. Error %s", err) - } - - exch, err = cfg.GetExchangeConfig(b.Name) - if err != nil { - t.Fatalf("Test failed. TestSetAutoPairDefaults load config failed. Error %s", err) - } - - if exch.SupportsAutoPairUpdates { - t.Fatal("Test failed. TestSetAutoPairDefaults Incorrect value") - } - - err = b.SetAutoPairDefaults() - if err != nil { - t.Fatalf("Test failed. TestSetAutoPairDefaults. Error %s", err) - } - - exch, err = cfg.GetExchangeConfig(b.Name) - if err != nil { - t.Fatalf("Test failed. TestSetAutoPairDefaults load config failed. Error %s", err) - } - - if !exch.SupportsAutoPairUpdates { - t.Fatal("Test failed. TestSetAutoPairDefaults Incorrect value") - } - - b.SupportsAutoPairUpdating = false - err = b.SetAutoPairDefaults() - if err != nil { - t.Fatalf("Test failed. TestSetAutoPairDefaults. Error %s", err) - } - - if b.PairsLastUpdated == 0 { - t.Fatal("Test failed. TestSetAutoPairDefaults Incorrect value") + if exch.Features.Supports.RESTCapabilities.AutoPairUpdates { + t.Fatal("TestSetAutoPairDefaults Incorrect value") } } func TestSupportsAutoPairUpdates(t *testing.T) { + t.Parallel() + b := Base{ - Name: "TESTNAME", - SupportsAutoPairUpdating: false, + Name: "TESTNAME", } if b.SupportsAutoPairUpdates() { - t.Fatal("Test failed. TestSupportsAutoPairUpdates Incorrect value") + t.Error("exchange shouldn't support auto pair updates") + } + + b.Features.Supports.RESTCapabilities.AutoPairUpdates = true + if !b.SupportsAutoPairUpdates() { + t.Error("exchange should support auto pair updates") } } func TestGetLastPairsUpdateTime(t *testing.T) { + t.Parallel() + testTime := time.Now().Unix() - b := Base{ - Name: "TESTNAME", - PairsLastUpdated: testTime, - } + var b Base + b.CurrencyPairs.LastUpdated = testTime if b.GetLastPairsUpdateTime() != testTime { - t.Fatal("Test failed. TestGetLastPairsUpdateTim Incorrect value") + t.Fatal("TestGetLastPairsUpdateTim Incorrect value") } } func TestSetAssetTypes(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Fatalf("Test failed. TestSetAssetTypes failed to load config file. Error: %s", err) - } + t.Parallel() b := Base{ - Name: "TESTNAME", + Config: &config.ExchangeConfig{ + CurrencyPairs: ¤cy.PairsManager{}, + }, + CurrencyPairs: currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + asset.Binary, + asset.Futures, + }, + }, + } + b.SetAssetTypes() + if len(b.GetAssetTypes()) != 3 { + t.Error("incorrect assets len") } - err = b.SetAssetTypes() - if err == nil { - t.Fatal("Test failed. TestSetAssetTypes returned nil error for a non-existent exchange") + b.CurrencyPairs.AssetTypes = append(b.CurrencyPairs.AssetTypes, + asset.PerpetualSwap) + b.Config.CurrencyPairs.AssetTypes = asset.Items{ + asset.Index, } - - b.Name = defaultTestExchange - b.AssetTypes = []string{orderbook.Spot} - err = b.SetAssetTypes() - if err != nil { - t.Fatalf("Test failed. TestSetAssetTypes. Error %s", err) - } - - exch, err := cfg.GetExchangeConfig(b.Name) - if err != nil { - t.Fatalf("Test failed. TestSetAssetTypes load config failed. Error %s", err) - } - - exch.AssetTypes = "" - err = cfg.UpdateExchangeConfig(&exch) - if err != nil { - t.Fatalf("Test failed. TestSetAssetTypes update config failed. Error %s", err) - } - - exch, err = cfg.GetExchangeConfig(b.Name) - if err != nil { - t.Fatalf("Test failed. TestSetAssetTypes load config failed. Error %s", err) - } - - if exch.AssetTypes != "" { - t.Fatal("Test failed. TestSetAssetTypes assetTypes != ''") - } - - err = b.SetAssetTypes() - if err != nil { - t.Fatalf("Test failed. TestSetAssetTypes. Error %s", err) - } - - if !common.StringDataCompare(b.AssetTypes, ticker.Spot) { - t.Fatal("Test failed. TestSetAssetTypes assetTypes is not set") + b.SetAssetTypes() + if len(b.GetAssetTypes()) != 4 { + t.Error("incorrect assets len") } } func TestGetAssetTypes(t *testing.T) { + t.Parallel() + testExchange := Base{ - AssetTypes: []string{orderbook.Spot, "Binary", "Futures"}, + CurrencyPairs: currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + asset.Binary, + asset.Futures, + }, + }, } aT := testExchange.GetAssetTypes() if len(aT) != 3 { - t.Error("Test failed. TestGetAssetTypes failed") + t.Error("TestGetAssetTypes failed") } } -func TestGetExchangeAssetTypes(t *testing.T) { +func TestGetClientBankAccounts(t *testing.T) { cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) + err := cfg.LoadConfig(config.TestFile, true) if err != nil { - t.Fatalf("Failed to load config file. Error: %s", err) + t.Fatal(err) } - result, err := GetExchangeAssetTypes("Bitfinex") + var b Base + var r config.BankAccount + r, err = b.GetClientBankAccounts("Kraken", "USD") if err != nil { - t.Fatal("Test failed. Unable to obtain Bitfinex asset types") + t.Error(err) } - if !common.StringDataCompare(result, ticker.Spot) { - t.Fatal("Test failed. Bitfinex does not contain default asset type 'SPOT'") + if r.BankName != "test" { + t.Error("incorrect bank name") } - _, err = GetExchangeAssetTypes("non-existent-exchange") + r, err = b.GetClientBankAccounts("MEOW", "USD") if err == nil { - t.Fatal("Test failed. Got asset types for non-existent exchange") + t.Error("an error should have been thrown for a non-existent exchange") } } -func TestCompareCurrencyPairFormats(t *testing.T) { - cfgOne := config.CurrencyPairFormatConfig{ - Delimiter: "-", - Uppercase: true, - Index: "", - Separator: ",", +func TestGetExchangeBankAccounts(t *testing.T) { + cfg := config.GetConfig() + err := cfg.LoadConfig(config.TestFile, true) + if err != nil { + t.Fatal(err) } - cfgTwo := cfgOne - if !CompareCurrencyPairFormats(cfgOne, &cfgTwo) { - t.Fatal("Test failed. CompareCurrencyPairFormats should be true") + var b Base + var r config.BankAccount + r, err = b.GetExchangeBankAccounts("Bitfinex", "USD") + if err != nil { + t.Error(err) } - cfgTwo.Delimiter = "~" - if CompareCurrencyPairFormats(cfgOne, &cfgTwo) { - t.Fatal("Test failed. CompareCurrencyPairFormats should not be true") + if r.BankName != "Deutsche Bank Privat Und Geschaeftskunden AG" { + t.Error("incorrect bank name") + } + + _, err = b.GetExchangeBankAccounts("MEOW", "USD") + if err == nil { + t.Error("an error should have been thrown for a non-existent exchange") } } func TestSetCurrencyPairFormat(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Fatalf("Test failed. TestSetCurrencyPairFormat failed to load config file. Error: %s", err) - } + t.Parallel() b := Base{ - Name: "TESTNAME", + Config: &config.ExchangeConfig{}, + } + b.SetCurrencyPairFormat() + if b.Config.CurrencyPairs == nil { + t.Error("CurrencyPairs shouldn't be nil") } - err = b.SetCurrencyPairFormat() - if err == nil { - t.Fatal("Test failed. TestSetCurrencyPairFormat returned nil error for a non-existent exchange") + // Test global format logic + b.Config.CurrencyPairs.UseGlobalFormat = true + b.CurrencyPairs.UseGlobalFormat = true + pFmt := ¤cy.PairFormat{ + Delimiter: "#", + } + b.CurrencyPairs.RequestFormat = pFmt + b.CurrencyPairs.ConfigFormat = pFmt + b.SetCurrencyPairFormat() + if b.GetPairFormat(asset.Spot, true).Delimiter != "#" { + t.Error("incorrect pair format delimiter") } - b.Name = defaultTestExchange - err = b.SetCurrencyPairFormat() - if err != nil { - t.Fatalf("Test failed. TestSetCurrencyPairFormat. Error %s", err) + // Test individual asset type formatting logic + b.CurrencyPairs.UseGlobalFormat = false + // This will generate a nil pair store + b.CurrencyPairs.AssetTypes = asset.Items{asset.Index} + // Store non-nil pair stores + b.CurrencyPairs.Store(asset.Spot, currency.PairStore{ + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "~", + }, + }) + b.CurrencyPairs.Store(asset.Futures, currency.PairStore{ + ConfigFormat: ¤cy.PairFormat{ + Delimiter: ":)", + }, + }) + b.SetCurrencyPairFormat() + if b.GetPairFormat(asset.Spot, false).Delimiter != "~" { + t.Error("incorrect pair format delimiter") } - - exch, err := cfg.GetExchangeConfig(b.Name) - if err != nil { - t.Fatalf("Test failed. TestSetCurrencyPairFormat load config failed. Error %s", err) - } - - exch.ConfigCurrencyPairFormat = nil - exch.RequestCurrencyPairFormat = nil - err = cfg.UpdateExchangeConfig(&exch) - if err != nil { - t.Fatalf("Test failed. TestSetCurrencyPairFormat update config failed. Error %s", err) - } - - exch, err = cfg.GetExchangeConfig(b.Name) - if err != nil { - t.Fatalf("Test failed. TestSetCurrencyPairFormat load config failed. Error %s", err) - } - - if exch.ConfigCurrencyPairFormat != nil && exch.RequestCurrencyPairFormat != nil { - t.Fatal("Test failed. TestSetCurrencyPairFormat exch values are not nil") - } - - err = b.SetCurrencyPairFormat() - if err != nil { - t.Fatalf("Test failed. TestSetCurrencyPairFormat. Error %s", err) - } - - if b.ConfigCurrencyPairFormat.Delimiter != "" && - b.ConfigCurrencyPairFormat.Index != currency.BTC.String() && - b.ConfigCurrencyPairFormat.Uppercase { - t.Fatal("Test failed. TestSetCurrencyPairFormat ConfigCurrencyPairFormat values are incorrect") - } - - if b.RequestCurrencyPairFormat.Delimiter != "" && - b.RequestCurrencyPairFormat.Index != currency.BTC.String() && - b.RequestCurrencyPairFormat.Uppercase { - t.Fatal("Test failed. TestSetCurrencyPairFormat RequestCurrencyPairFormat values are incorrect") - } - - // if currency pairs are the same as the config, should load from config - err = b.SetCurrencyPairFormat() - if err != nil { - t.Fatalf("Test failed. TestSetCurrencyPairFormat. Error %s", err) + if b.GetPairFormat(asset.Futures, false).Delimiter != ":)" { + t.Error("incorrect pair format delimiter") } } // TestGetAuthenticatedAPISupport logic test func TestGetAuthenticatedAPISupport(t *testing.T) { + t.Parallel() + base := Base{ - AuthenticatedAPISupport: true, - AuthenticatedWebsocketAPISupport: false, + API: API{ + AuthenticatedSupport: true, + AuthenticatedWebsocketSupport: false, + }, } if !base.GetAuthenticatedAPISupport(RestAuthentication) { - t.Fatal("Test failed. Expected RestAuthentication to return true") + t.Fatal("Expected RestAuthentication to return true") } if base.GetAuthenticatedAPISupport(WebsocketAuthentication) { - t.Fatal("Test failed. Expected WebsocketAuthentication to return false") + t.Fatal("Expected WebsocketAuthentication to return false") } - base.AuthenticatedWebsocketAPISupport = true + base.API.AuthenticatedWebsocketSupport = true if !base.GetAuthenticatedAPISupport(WebsocketAuthentication) { - t.Fatal("Test failed. Expected WebsocketAuthentication to return true") + t.Fatal("Expected WebsocketAuthentication to return true") } if base.GetAuthenticatedAPISupport(2) { - t.Fatal("Test failed. Expected default case of 'false' to be returned") + t.Fatal("Expected default case of 'false' to be returned") } } func TestGetName(t *testing.T) { - GetName := Base{ + t.Parallel() + + b := Base{ Name: "TESTNAME", } - name := GetName.GetName() + name := b.GetName() if name != "TESTNAME" { - t.Error("Test Failed - Exchange getName() returned incorrect name") + t.Error("Exchange GetName() returned incorrect name") } } -func TestGetEnabledCurrencies(t *testing.T) { +func TestGetFeatures(t *testing.T) { + t.Parallel() + + // Test GetEnabledFeatures + var b Base + if b.GetEnabledFeatures().AutoPairUpdates { + t.Error("auto pair updates should be disabled") + } + b.Features.Enabled.AutoPairUpdates = true + if !b.GetEnabledFeatures().AutoPairUpdates { + t.Error("auto pair updates should be enabled") + } + + // Test GetSupportedFeatures + b.Features.Supports.RESTCapabilities.AutoPairUpdates = true + if !b.GetSupportedFeatures().RESTCapabilities.AutoPairUpdates { + t.Error("auto pair updates should be supported") + } + if b.GetSupportedFeatures().RESTCapabilities.TickerBatching { + t.Error("ticker batching shouldn't be supported") + } +} + +func TestGetPairFormat(t *testing.T) { + t.Parallel() + + // Test global formatting + var b Base + b.CurrencyPairs.UseGlobalFormat = true + b.CurrencyPairs.ConfigFormat = ¤cy.PairFormat{ + Uppercase: true, + } + b.CurrencyPairs.RequestFormat = ¤cy.PairFormat{ + Delimiter: "~", + } + pFmt := b.GetPairFormat(asset.Spot, true) + if pFmt.Delimiter != "~" && !pFmt.Uppercase { + t.Error("incorrect pair format values") + } + pFmt = b.GetPairFormat(asset.Spot, false) + if pFmt.Delimiter != "" && pFmt.Uppercase { + t.Error("incorrect pair format values") + } + + // Test individual asset pair store formatting + b.CurrencyPairs.UseGlobalFormat = false + b.CurrencyPairs.Store(asset.Spot, currency.PairStore{ + ConfigFormat: &pFmt, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "/", + Uppercase: true, + }, + }) + pFmt = b.GetPairFormat(asset.Spot, false) + if pFmt.Delimiter != "" && pFmt.Uppercase { + t.Error("incorrect pair format values") + } + pFmt = b.GetPairFormat(asset.Spot, true) + if pFmt.Delimiter != "~" && !pFmt.Uppercase { + t.Error("incorrect pair format values") + } +} + +func TestGetEnabledPairs(t *testing.T) { + t.Parallel() + b := Base{ Name: "TESTNAME", } - b.EnabledPairs = currency.NewPairsFromStrings([]string{"BTC-USD"}) - format := config.CurrencyPairFormatConfig{ + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{defaultTestCurrencyPair}), true) + format := currency.PairFormat{ Delimiter: "-", Index: "", Uppercase: true, } - b.RequestCurrencyPairFormat = format - b.ConfigCurrencyPairFormat = format - c := b.GetEnabledCurrencies() + assetType := asset.Spot + b.CurrencyPairs.UseGlobalFormat = true + b.CurrencyPairs.RequestFormat = &format + b.CurrencyPairs.ConfigFormat = &format + + c := b.GetEnabledPairs(assetType) if c[0].String() != defaultTestCurrencyPair { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + t.Error("Exchange GetAvailablePairs() incorrect string") } format.Delimiter = "~" - b.RequestCurrencyPairFormat = format - c = b.GetEnabledCurrencies() - if c[0].String() != defaultTestCurrencyPair { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + b.CurrencyPairs.RequestFormat = &format + c = b.GetEnabledPairs(assetType) + if c[0].String() != "BTC~USD" { + t.Error("Exchange GetAvailablePairs() incorrect string") } format.Delimiter = "" - b.ConfigCurrencyPairFormat = format - c = b.GetEnabledCurrencies() + b.CurrencyPairs.ConfigFormat = &format + c = b.GetEnabledPairs(assetType) if c[0].String() != "BTCUSD" { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + t.Error("Exchange GetAvailablePairs() incorrect string") } - b.EnabledPairs = currency.NewPairsFromStrings([]string{"BTCDOGE"}) - format.Index = "BTC" - b.ConfigCurrencyPairFormat = format - c = b.GetEnabledCurrencies() - if c[0].Base.String() != "BTC" && c[0].Quote.String() != "DOGE" { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") - } - - b.EnabledPairs = currency.NewPairsFromStrings([]string{"BTC_USD"}) - b.RequestCurrencyPairFormat.Delimiter = "" - b.ConfigCurrencyPairFormat.Delimiter = "_" - c = b.GetEnabledCurrencies() - if c[0].Base != currency.BTC && c[0].Quote != currency.USD { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") - } - - b.EnabledPairs = currency.NewPairsFromStrings([]string{"BTCDOGE"}) - b.RequestCurrencyPairFormat.Delimiter = "" - b.ConfigCurrencyPairFormat.Delimiter = "" - b.ConfigCurrencyPairFormat.Index = currency.BTC.String() - c = b.GetEnabledCurrencies() - if c[0].Base != currency.BTC && c[0].Quote != currency.DOGE { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") - } - - b.EnabledPairs = currency.NewPairsFromStrings([]string{"BTCUSD"}) - b.ConfigCurrencyPairFormat.Index = "" - c = b.GetEnabledCurrencies() - if c[0].Base != currency.BTC && c[0].Quote != currency.USD { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") - } -} - -func TestGetAvailableCurrencies(t *testing.T) { - b := Base{ - Name: "TESTNAME", - } - - b.AvailablePairs = currency.NewPairsFromStrings([]string{"BTC-USD"}) - format := config.CurrencyPairFormatConfig{ - Delimiter: "-", - Index: "", - Uppercase: true, - } - - b.RequestCurrencyPairFormat = format - b.ConfigCurrencyPairFormat = format - c := b.GetAvailableCurrencies() - if c[0].String() != defaultTestCurrencyPair { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") - } - - format.Delimiter = "~" - b.RequestCurrencyPairFormat = format - c = b.GetAvailableCurrencies() - if c[0].String() != defaultTestCurrencyPair { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") - } - - format.Delimiter = "" - b.ConfigCurrencyPairFormat = format - c = b.GetAvailableCurrencies() - if c[0].String() != "BTCUSD" { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string", c[0]) - } - - b.AvailablePairs = currency.NewPairsFromStrings([]string{"BTCDOGE"}) + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{"BTCDOGE"}), true) format.Index = currency.BTC.String() - b.ConfigCurrencyPairFormat = format - c = b.GetAvailableCurrencies() + b.CurrencyPairs.ConfigFormat = &format + c = b.GetEnabledPairs(assetType) if c[0].Base != currency.BTC && c[0].Quote != currency.DOGE { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + t.Error("Exchange GetAvailablePairs() incorrect string") } - b.AvailablePairs = currency.NewPairsFromStrings([]string{"BTC_USD"}) - b.RequestCurrencyPairFormat.Delimiter = "" - b.ConfigCurrencyPairFormat.Delimiter = "_" - c = b.GetAvailableCurrencies() + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{"BTC_USD"}), true) + b.CurrencyPairs.RequestFormat.Delimiter = "" + b.CurrencyPairs.ConfigFormat.Delimiter = "_" + c = b.GetEnabledPairs(assetType) if c[0].Base != currency.BTC && c[0].Quote != currency.USD { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + t.Error("Exchange GetAvailablePairs() incorrect string") } - b.AvailablePairs = currency.NewPairsFromStrings([]string{"BTCDOGE"}) - b.RequestCurrencyPairFormat.Delimiter = "" - b.ConfigCurrencyPairFormat.Delimiter = "" - b.ConfigCurrencyPairFormat.Index = currency.BTC.String() - c = b.GetAvailableCurrencies() + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{"BTCDOGE"}), true) + b.CurrencyPairs.RequestFormat.Delimiter = "" + b.CurrencyPairs.ConfigFormat.Delimiter = "" + b.CurrencyPairs.ConfigFormat.Index = currency.BTC.String() + c = b.GetEnabledPairs(assetType) if c[0].Base != currency.BTC && c[0].Quote != currency.DOGE { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + t.Error("Exchange GetAvailablePairs() incorrect string") } - b.AvailablePairs = currency.NewPairsFromStrings([]string{"BTCUSD"}) - b.ConfigCurrencyPairFormat.Index = "" - c = b.GetAvailableCurrencies() + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{"BTCUSD"}), true) + b.CurrencyPairs.ConfigFormat.Index = "" + c = b.GetEnabledPairs(assetType) if c[0].Base != currency.BTC && c[0].Quote != currency.USD { - t.Error("Test Failed - Exchange GetAvailableCurrencies() incorrect string") + t.Error("Exchange GetAvailablePairs() incorrect string") } } -func TestSupportsCurrency(t *testing.T) { +func TestGetAvailablePairs(t *testing.T) { + t.Parallel() + b := Base{ Name: "TESTNAME", } - b.AvailablePairs = currency.NewPairsFromStrings([]string{defaultTestCurrencyPair, "ETH-USD"}) - b.EnabledPairs = currency.NewPairsFromStrings([]string{defaultTestCurrencyPair}) + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{defaultTestCurrencyPair}), false) + format := currency.PairFormat{ + Delimiter: "-", + Index: "", + Uppercase: true, + } - format := config.CurrencyPairFormatConfig{ + assetType := asset.Spot + b.CurrencyPairs.UseGlobalFormat = true + b.CurrencyPairs.RequestFormat = &format + b.CurrencyPairs.ConfigFormat = &format + + c := b.GetAvailablePairs(assetType) + if c[0].String() != defaultTestCurrencyPair { + t.Error("Exchange GetAvailablePairs() incorrect string") + } + + format.Delimiter = "~" + b.CurrencyPairs.RequestFormat = &format + c = b.GetAvailablePairs(assetType) + if c[0].String() != "BTC~USD" { + t.Error("Exchange GetAvailablePairs() incorrect string") + } + + format.Delimiter = "" + b.CurrencyPairs.ConfigFormat = &format + c = b.GetAvailablePairs(assetType) + if c[0].String() != "BTCUSD" { + t.Error("Exchange GetAvailablePairs() incorrect string") + } + + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{"BTCDOGE"}), false) + format.Index = currency.BTC.String() + b.CurrencyPairs.ConfigFormat = &format + c = b.GetAvailablePairs(assetType) + if c[0].Base != currency.BTC && c[0].Quote != currency.DOGE { + t.Error("Exchange GetAvailablePairs() incorrect string") + } + + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{"BTC_USD"}), false) + b.CurrencyPairs.RequestFormat.Delimiter = "" + b.CurrencyPairs.ConfigFormat.Delimiter = "_" + c = b.GetAvailablePairs(assetType) + if c[0].Base != currency.BTC && c[0].Quote != currency.USD { + t.Error("Exchange GetAvailablePairs() incorrect string") + } + + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{"BTCDOGE"}), false) + b.CurrencyPairs.RequestFormat.Delimiter = "" + b.CurrencyPairs.ConfigFormat.Delimiter = "_" + b.CurrencyPairs.ConfigFormat.Index = currency.BTC.String() + c = b.GetAvailablePairs(assetType) + if c[0].Base != currency.BTC && c[0].Quote != currency.DOGE { + t.Error("Exchange GetAvailablePairs() incorrect string") + } + + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{"BTCUSD"}), false) + b.CurrencyPairs.ConfigFormat.Index = "" + c = b.GetAvailablePairs(assetType) + if c[0].Base != currency.BTC && c[0].Quote != currency.USD { + t.Error("Exchange GetAvailablePairs() incorrect string") + } +} + +func TestSupportsPair(t *testing.T) { + t.Parallel() + + b := Base{ + Name: "TESTNAME", + } + + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{ + defaultTestCurrencyPair, "ETH-USD"}), false) + b.CurrencyPairs.StorePairs(asset.Spot, + currency.NewPairsFromStrings([]string{defaultTestCurrencyPair}), true) + + format := ¤cy.PairFormat{ Delimiter: "-", Index: "", } - b.RequestCurrencyPairFormat = format - b.ConfigCurrencyPairFormat = format + b.CurrencyPairs.UseGlobalFormat = true + b.CurrencyPairs.RequestFormat = format + b.CurrencyPairs.ConfigFormat = format + assetType := asset.Spot - if !b.SupportsCurrency(currency.NewPairFromStrings("BTC", "USD"), true) { - t.Error("Test Failed - Exchange SupportsCurrency() incorrect value") + if !b.SupportsPair(currency.NewPair(currency.BTC, currency.USD), true, assetType) { + t.Error("Exchange SupportsPair() incorrect value") } - if !b.SupportsCurrency(currency.NewPairFromStrings("ETH", "USD"), false) { - t.Error("Test Failed - Exchange SupportsCurrency() incorrect value") + if !b.SupportsPair(currency.NewPair(currency.ETH, currency.USD), false, assetType) { + t.Error("Exchange SupportsPair() incorrect value") } - if b.SupportsCurrency(currency.NewPairFromStrings("ASD", "ASDF"), true) { - t.Error("Test Failed - Exchange SupportsCurrency() incorrect value") - } -} -func TestGetExchangeFormatCurrencySeperator(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Fatalf("Failed to load config file. Error: %s", err) - } - - expected := true - actual := GetExchangeFormatCurrencySeperator("Yobit") - - if expected != actual { - t.Errorf("Test failed - TestGetExchangeFormatCurrencySeperator expected %v != actual %v", - expected, actual) - } - - expected = false - actual = GetExchangeFormatCurrencySeperator("LocalBitcoins") - - if expected != actual { - t.Errorf("Test failed - TestGetExchangeFormatCurrencySeperator expected %v != actual %v", - expected, actual) - } - - expected = false - actual = GetExchangeFormatCurrencySeperator("blah") - - if expected != actual { - t.Errorf("Test failed - TestGetExchangeFormatCurrencySeperator expected %v != actual %v", - expected, actual) + if b.SupportsPair(currency.NewPairFromStrings("ASD", "ASDF"), true, assetType) { + t.Error("Exchange SupportsPair() incorrect value") } } -func TestGetAndFormatExchangeCurrencies(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Fatalf("Failed to load config file. Error: %s", err) +func TestFormatExchangeCurrencies(t *testing.T) { + t.Parallel() + + e := Base{ + CurrencyPairs: currency.PairsManager{ + UseGlobalFormat: true, + + RequestFormat: ¤cy.PairFormat{ + Uppercase: false, + Delimiter: "~", + Separator: "^", + }, + + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "_", + }, + }, } var pairs = []currency.Pair{ @@ -610,57 +755,45 @@ func TestGetAndFormatExchangeCurrencies(t *testing.T) { currency.NewPairDelimiter("LTC_BTC", "_"), } - actual, err := GetAndFormatExchangeCurrencies("Yobit", pairs) + actual, err := e.FormatExchangeCurrencies(pairs, asset.Spot) if err != nil { - t.Errorf("Test failed - Exchange TestGetAndFormatExchangeCurrencies error %s", err) + t.Errorf("Exchange TestFormatExchangeCurrencies error %s", err) } - expected := "btc_usd-ltc_btc" - + expected := "btc~usd^ltc~btc" if actual != expected { - t.Errorf("Test failed - Exchange TestGetAndFormatExchangeCurrencies %s != %s", + t.Errorf("Exchange TestFormatExchangeCurrencies %s != %s", actual, expected) } - _, err = GetAndFormatExchangeCurrencies("non-existent", pairs) + _, err = e.FormatExchangeCurrencies(nil, asset.Spot) if err == nil { - t.Errorf("Test failed - Exchange TestGetAndFormatExchangeCurrencies returned nil error on non-existent exchange") + t.Error("nil pairs should return an error") } } func TestFormatExchangeCurrency(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Fatalf("Failed to load config file. Error: %s", err) + t.Parallel() + + var b Base + b.CurrencyPairs.UseGlobalFormat = true + b.CurrencyPairs.RequestFormat = ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "-", } p := currency.NewPair(currency.BTC, currency.USD) expected := defaultTestCurrencyPair - actual := FormatExchangeCurrency("CoinbasePro", p) + actual := b.FormatExchangeCurrency(p, asset.Spot) if actual.String() != expected { - t.Errorf("Test failed - Exchange TestFormatExchangeCurrency %s != %s", - actual, expected) - } -} - -func TestFormatCurrency(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Fatalf("Failed to load config file. Error: %s", err) - } - - p := currency.NewPair(currency.BTC, currency.USD) - expected := defaultTestCurrencyPair - actual := FormatCurrency(p).String() - if actual != expected { - t.Errorf("Test failed - Exchange TestFormatCurrency %s != %s", + t.Errorf("Exchange TestFormatExchangeCurrency %s != %s", actual, expected) } } func TestSetEnabled(t *testing.T) { + t.Parallel() + SetEnabled := Base{ Name: "TESTNAME", Enabled: false, @@ -668,221 +801,387 @@ func TestSetEnabled(t *testing.T) { SetEnabled.SetEnabled(true) if !SetEnabled.Enabled { - t.Error("Test Failed - Exchange SetEnabled(true) did not set boolean") + t.Error("Exchange SetEnabled(true) did not set boolean") } } func TestIsEnabled(t *testing.T) { + t.Parallel() + IsEnabled := Base{ Name: "TESTNAME", Enabled: false, } if IsEnabled.IsEnabled() { - t.Error("Test Failed - Exchange IsEnabled() did not return correct boolean") + t.Error("Exchange IsEnabled() did not return correct boolean") } } // TestSetAPIKeys logic test func TestSetAPIKeys(t *testing.T) { - SetAPIKeys := Base{ - Name: "TESTNAME", - Enabled: false, - AuthenticatedAPISupport: false, - AuthenticatedWebsocketAPISupport: false, + t.Parallel() + + b := Base{ + Name: "TESTNAME", + Enabled: false, + API: API{ + AuthenticatedSupport: false, + AuthenticatedWebsocketSupport: false, + }, } - SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", false) - if SetAPIKeys.APIKey != "" && SetAPIKeys.APISecret != "" && SetAPIKeys.ClientID != "" { - t.Error("Test Failed - SetAPIKeys() set values without authenticated API support enabled") + b.SetAPIKeys("RocketMan", "Digereedoo", "007") + if b.API.Credentials.Key != "RocketMan" && b.API.Credentials.Secret != "Digereedoo" && b.API.Credentials.ClientID != "007" { + t.Error("invalid API credentials") } - SetAPIKeys.AuthenticatedAPISupport = true - SetAPIKeys.AuthenticatedWebsocketAPISupport = true - SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", false) - if SetAPIKeys.APIKey != "RocketMan" && SetAPIKeys.APISecret != "Digereedoo" && SetAPIKeys.ClientID != "007" { - t.Error("Test Failed - Exchange SetAPIKeys() did not set correct values") + // Invalid secret + b.API.CredentialsValidator.RequiresBase64DecodeSecret = true + b.API.AuthenticatedSupport = true + b.SetAPIKeys("RocketMan", "%%", "007") + if b.API.AuthenticatedSupport || b.API.AuthenticatedWebsocketSupport { + t.Error("invalid secret should disable authenticated API support") } - SetAPIKeys.AuthenticatedAPISupport = false - SetAPIKeys.AuthenticatedWebsocketAPISupport = true - SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", false) - if SetAPIKeys.APIKey != "RocketMan" && SetAPIKeys.APISecret != "Digereedoo" && SetAPIKeys.ClientID != "007" { - t.Error("Test Failed - Exchange SetAPIKeys() did not set correct values") + // valid secret + b.API.CredentialsValidator.RequiresBase64DecodeSecret = true + b.API.AuthenticatedSupport = true + b.SetAPIKeys("RocketMan", "aGVsbG8gd29ybGQ=", "007") + if !b.API.AuthenticatedSupport && b.API.Credentials.Secret != "hello world" { + t.Error("invalid secret should disable authenticated API support") } - - SetAPIKeys.AuthenticatedAPISupport = true - SetAPIKeys.AuthenticatedWebsocketAPISupport = false - SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", false) - if SetAPIKeys.APIKey != "RocketMan" && SetAPIKeys.APISecret != "Digereedoo" && SetAPIKeys.ClientID != "007" { - t.Error("Test Failed - Exchange SetAPIKeys() did not set correct values") - } - - SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", true) } -func TestSetCurrencies(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Fatal("Test failed. TestSetCurrencies failed to load config") +func TestSetupDefaults(t *testing.T) { + t.Parallel() + + var b Base + cfg := config.ExchangeConfig{ + HTTPTimeout: time.Duration(-1), + API: config.APIConfig{ + AuthenticatedSupport: true, + }, + } + if err := b.SetupDefaults(&cfg); err != nil { + t.Error(err) + } + if cfg.HTTPTimeout.String() != "15s" { + t.Error("HTTP timeout should be set to 15s") } - UAC := Base{Name: "ASDF"} - UAC.AvailablePairs = currency.NewPairsFromStrings([]string{"ETHLTC", "LTCBTC"}) - UAC.EnabledPairs = currency.NewPairsFromStrings([]string{"ETHLTC"}) - newPair := currency.NewPairDelimiter("ETH_USDT", "_") + // Test custom HTTP timeout is set + cfg.HTTPTimeout = time.Second * 30 + if err := b.SetupDefaults(&cfg); err != nil { + t.Error(err) + } + if cfg.HTTPTimeout.String() != "30s" { + t.Error("HTTP timeout should be set to 30s") + } - err = UAC.SetCurrencies([]currency.Pair{newPair}, true) - if err == nil { - t.Fatal("Test failed. TestSetCurrencies returned nil error on non-existent exchange") + // Test asset types + p := currency.NewPairDelimiter(defaultTestCurrencyPair, "-") + b.CurrencyPairs.Store(asset.Spot, + currency.PairStore{ + Enabled: currency.Pairs{ + p, + }, + }, + ) + if err := b.SetupDefaults(&cfg); err != nil { + t.Error(err) + } + ps := cfg.CurrencyPairs.Get(asset.Spot) + if !ps.Enabled.Contains(p, true) { + t.Error("default pair should be stored in the configs pair store") + } + + // Test websocket support + b.Websocket = wshandler.New() + b.Features.Supports.Websocket = true + if err := b.SetupDefaults(&cfg); err != nil { + t.Error(err) + } + b.Websocket.Setup(&wshandler.WebsocketSetup{ + Enabled: true, + }) + if !b.IsWebsocketEnabled() { + t.Error("websocket should be enabled") + } +} + +func TestAllowAuthenticatedRequest(t *testing.T) { + t.Parallel() + + b := Base{ + SkipAuthCheck: true, + } + + // Test SkipAuthCheck + if r := b.AllowAuthenticatedRequest(); !r { + t.Error("skip auth check should allow authenticated requests") + } + + // Test credentials failure + b.SkipAuthCheck = false + b.API.CredentialsValidator.RequiresKey = true + if r := b.AllowAuthenticatedRequest(); r { + t.Error("should fail with an empty key") + } + + // Test bot usage with authenticated API support disabled, but with + // valid credentials + b.LoadedByConfig = true + b.API.Credentials.Key = "k3y" + if r := b.AllowAuthenticatedRequest(); r { + t.Error("should fail when authenticated support is disabled") + } + + // Test enabled authenticated API support and loaded by config + // but invalid credentials + b.API.AuthenticatedSupport = true + b.API.Credentials.Key = "" + if r := b.AllowAuthenticatedRequest(); r { + t.Error("should fail with invalid credentials") + } + + // Finally a valid one + b.API.Credentials.Key = "k3y" + if r := b.AllowAuthenticatedRequest(); !r { + t.Error("show allow an authenticated request") + } +} + +func TestValidateAPICredentials(t *testing.T) { + t.Parallel() + + var b Base + type tester struct { + Key string + Secret string + ClientID string + PEMKey string + RequiresPEM bool + RequiresKey bool + RequiresSecret bool + RequiresClientID bool + RequiresBase64DecodeSecret bool + Expected bool + Result bool + } + + tests := []tester{ + // test key + {RequiresKey: true}, + {RequiresKey: true, Key: "k3y", Expected: true}, + // test secret + {RequiresSecret: true}, + {RequiresSecret: true, Secret: "s3cr3t", Expected: true}, + // test pem + {RequiresPEM: true}, + {RequiresPEM: true, PEMKey: "p3mK3y", Expected: true}, + // test clientID + {RequiresClientID: true}, + {RequiresClientID: true, ClientID: "cli3nt1D", Expected: true}, + // test requires base64 decode secret + {RequiresBase64DecodeSecret: true, RequiresSecret: true}, + {RequiresBase64DecodeSecret: true, Secret: "%%", Expected: false}, + {RequiresBase64DecodeSecret: true, Secret: "aGVsbG8gd29ybGQ=", Expected: true}, + } + + for x := range tests { + setupBase := func(b *Base, tData tester) { + b.API.Credentials.Key = tData.Key + b.API.Credentials.Secret = tData.Secret + b.API.Credentials.ClientID = tData.ClientID + b.API.Credentials.PEMKey = tData.PEMKey + b.API.CredentialsValidator.RequiresKey = tData.RequiresKey + b.API.CredentialsValidator.RequiresSecret = tData.RequiresSecret + b.API.CredentialsValidator.RequiresPEM = tData.RequiresPEM + b.API.CredentialsValidator.RequiresClientID = tData.RequiresClientID + b.API.CredentialsValidator.RequiresBase64DecodeSecret = tData.RequiresBase64DecodeSecret + } + + setupBase(&b, tests[x]) + if r := b.ValidateAPICredentials(); r != tests[x].Expected { + t.Errorf("Test %d: expected: %v: got %v", x, tests[x].Expected, r) + } + } +} + +func TestSetPairs(t *testing.T) { + t.Parallel() + + b := Base{ + CurrencyPairs: currency.PairsManager{ + UseGlobalFormat: true, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + }, + Config: &config.ExchangeConfig{ + CurrencyPairs: ¤cy.PairsManager{ + UseGlobalFormat: true, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + Pairs: map[asset.Item]*currency.PairStore{}, + }, + }, + } + + if err := b.SetPairs(nil, asset.Spot, true); err == nil { + t.Error("nil pairs should throw an error") + } + + pairs := currency.Pairs{ + currency.NewPair(currency.BTC, currency.USD), + } + err := b.SetPairs(pairs, asset.Spot, true) + if err != nil { + t.Error(err) + } + + if p := b.GetEnabledPairs(asset.Spot); len(p) != 1 { + t.Error("pairs shouldn't be nil") + } +} + +func TestUpdatePairs(t *testing.T) { + cfg := config.GetConfig() + err := cfg.LoadConfig(config.TestFile, true) + if err != nil { + t.Fatal("TestUpdatePairs failed to load config") } anxCfg, err := cfg.GetExchangeConfig(defaultTestExchange) if err != nil { - t.Fatal("Test failed. TestSetCurrencies failed to load config") + t.Fatal("TestUpdatePairs failed to load config") } - UAC.Name = defaultTestExchange - UAC.ConfigCurrencyPairFormat.Delimiter = anxCfg.ConfigCurrencyPairFormat.Delimiter - UAC.SetCurrencies(currency.Pairs{newPair}, true) - if !UAC.GetEnabledCurrencies().Contains(newPair, true) { - t.Fatal("Test failed. TestSetCurrencies failed to set currencies") - } - - UAC.SetCurrencies(currency.Pairs{newPair}, false) - if !UAC.GetAvailableCurrencies().Contains(newPair, true) { - t.Fatal("Test failed. TestSetCurrencies failed to set currencies") - } - - err = UAC.SetCurrencies(nil, false) - if err == nil { - t.Fatal("Test failed. TestSetCurrencies should return an error when attempting to set an empty pairs array") - } -} - -func TestUpdateCurrencies(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Fatal("Test failed. TestUpdateEnabledCurrencies failed to load config") - } - - UAC := Base{Name: "ANX"} + UAC := Base{Name: defaultTestExchange} + UAC.Config = anxCfg exchangeProducts := currency.NewPairsFromStrings([]string{"ltc", "btc", "usd", "aud", ""}) - - // Test updating exchange products for an exchange which doesn't exist - UAC.Name = "Blah" - err = UAC.UpdateCurrencies(exchangeProducts, true, false) - if err == nil { - t.Errorf("Test Failed - Exchange TestUpdateCurrencies succeeded on an exchange which doesn't exist") - } - - // Test updating exchange products - UAC.Name = defaultTestExchange - err = UAC.UpdateCurrencies(exchangeProducts, true, false) + err = UAC.UpdatePairs(exchangeProducts, asset.Spot, true, false) if err != nil { - t.Errorf("Test Failed - Exchange TestUpdateCurrencies error: %s", err) + t.Errorf("TestUpdatePairs error: %s", err) } // Test updating the same new products, diff should be 0 - UAC.Name = defaultTestExchange - err = UAC.UpdateCurrencies(exchangeProducts, true, false) + err = UAC.UpdatePairs(exchangeProducts, asset.Spot, true, false) if err != nil { - t.Errorf("Test Failed - Exchange TestUpdateCurrencies error: %s", err) + t.Errorf("TestUpdatePairs error: %s", err) } // Test force updating to only one product exchangeProducts = currency.NewPairsFromStrings([]string{"btc"}) - err = UAC.UpdateCurrencies(exchangeProducts, true, true) + err = UAC.UpdatePairs(exchangeProducts, asset.Spot, true, true) if err != nil { - t.Errorf("Test Failed - Forced Exchange TestUpdateCurrencies error: %s", err) + t.Errorf("TestUpdatePairs error: %s", err) } + // Test updating exchange products exchangeProducts = currency.NewPairsFromStrings([]string{"ltc", "btc", "usd", "aud"}) - // Test updating exchange products for an exchange which doesn't exist - UAC.Name = "Blah" - err = UAC.UpdateCurrencies(exchangeProducts, false, false) - if err == nil { - t.Errorf("Test Failed - Exchange UpdateCurrencies() succeeded on an exchange which doesn't exist") - } - - // Test updating exchange products UAC.Name = defaultTestExchange - err = UAC.UpdateCurrencies(exchangeProducts, false, false) + err = UAC.UpdatePairs(exchangeProducts, asset.Spot, false, false) if err != nil { - t.Errorf("Test Failed - Exchange UpdateCurrencies() error: %s", err) + t.Errorf("Exchange UpdatePairs() error: %s", err) } // Test updating the same new products, diff should be 0 - UAC.Name = defaultTestExchange - err = UAC.UpdateCurrencies(exchangeProducts, false, false) + err = UAC.UpdatePairs(exchangeProducts, asset.Spot, false, false) if err != nil { - t.Errorf("Test Failed - Exchange UpdateCurrencies() error: %s", err) + t.Errorf("Exchange UpdatePairs() error: %s", err) } // Test force updating to only one product exchangeProducts = currency.NewPairsFromStrings([]string{"btc"}) - err = UAC.UpdateCurrencies(exchangeProducts, false, true) + err = UAC.UpdatePairs(exchangeProducts, asset.Spot, false, true) if err != nil { - t.Errorf("Test Failed - Forced Exchange UpdateCurrencies() error: %s", err) + t.Errorf("Forced Exchange UpdatePairs() error: %s", err) } // Test update currency pairs with btc excluded exchangeProducts = currency.NewPairsFromStrings([]string{"ltc", "eth"}) - err = UAC.UpdateCurrencies(exchangeProducts, false, false) + err = UAC.UpdatePairs(exchangeProducts, asset.Spot, false, false) if err != nil { - t.Errorf("Test Failed - Forced Exchange UpdateCurrencies() error: %s", err) + t.Errorf("Forced Exchange UpdatePairs() error: %s", err) } // Test that empty exchange products should return an error exchangeProducts = nil - err = UAC.UpdateCurrencies(exchangeProducts, false, false) + err = UAC.UpdatePairs(exchangeProducts, asset.Spot, false, false) if err == nil { - t.Errorf("Test failed - empty available pairs should return an error") + t.Errorf("empty available pairs should return an error") + } + + // Test empty pair + p := currency.NewPairDelimiter(defaultTestCurrencyPair, "-") + pairs := currency.Pairs{ + currency.Pair{}, + p, + } + err = UAC.UpdatePairs(pairs, asset.Spot, true, true) + if err != nil { + t.Errorf("Forced Exchange UpdatePairs() error: %s", err) + } + UAC.CurrencyPairs.UseGlobalFormat = true + UAC.CurrencyPairs.ConfigFormat = ¤cy.PairFormat{ + Delimiter: "-", + } + if !UAC.GetEnabledPairs(asset.Spot).Contains(p, true) { + t.Fatal("expected currency pair not found") } } func TestSetAPIURL(t *testing.T) { + t.Parallel() + testURL := "https://api.something.com" testURLSecondary := "https://api.somethingelse.com" testURLDefault := "https://api.defaultsomething.com" testURLSecondaryDefault := "https://api.defaultsomethingelse.com" tester := Base{Name: "test"} + tester.Config = new(config.ExchangeConfig) - test := config.ExchangeConfig{} - - err := tester.SetAPIURL(&test) + err := tester.SetAPIURL() if err == nil { - t.Error("test failed - setting zero value config") + t.Error("setting zero value config") } - test.APIURL = testURL - test.APIURLSecondary = testURLSecondary + tester.Config.API.Endpoints.URL = testURL + tester.Config.API.Endpoints.URLSecondary = testURLSecondary - tester.APIUrlDefault = testURLDefault - tester.APIUrlSecondaryDefault = testURLSecondaryDefault + tester.API.Endpoints.URLDefault = testURLDefault + tester.API.Endpoints.URLSecondaryDefault = testURLSecondaryDefault - err = tester.SetAPIURL(&test) + err = tester.SetAPIURL() if err != nil { - t.Error("test failed", err) + t.Error(err) } if tester.GetAPIURL() != testURL { - t.Error("test failed - incorrect return URL") + t.Error("incorrect return URL") } if tester.GetSecondaryAPIURL() != testURLSecondary { - t.Error("test failed - incorrect return URL") + t.Error("incorrect return URL") } if tester.GetAPIURLDefault() != testURLDefault { - t.Error("test failed - incorrect return URL") + t.Error("incorrect return URL") } if tester.GetAPIURLSecondaryDefault() != testURLSecondaryDefault { - t.Error("test failed - incorrect return URL") + t.Error("incorrect return URL") + } + + tester.Config.API.Endpoints.URL = "http://insecureino.com" + tester.Config.API.Endpoints.URLSecondary = tester.Config.API.Endpoints.URL + err = tester.SetAPIURL() + if err != nil { + t.Error(err) } } @@ -891,23 +1190,73 @@ func BenchmarkSetAPIURL(b *testing.B) { test := config.ExchangeConfig{} - test.APIURL = "https://api.something.com" - test.APIURLSecondary = "https://api.somethingelse.com" + test.API.Endpoints.URL = "https://api.something.com" + test.API.Endpoints.URLSecondary = "https://api.somethingelse.com" - tester.APIUrlDefault = "https://api.defaultsomething.com" - tester.APIUrlSecondaryDefault = "https://api.defaultsomethingelse.com" + tester.API.Endpoints.URLDefault = "https://api.defaultsomething.com" + tester.API.Endpoints.URLDefault = "https://api.defaultsomethingelse.com" + + tester.Config = &test for i := 0; i < b.N; i++ { - err := tester.SetAPIURL(&test) + err := tester.SetAPIURL() if err != nil { b.Errorf("Benchmark failed %v", err) } } } +func TestSupportsWebsocket(t *testing.T) { + t.Parallel() + + var b Base + if b.SupportsWebsocket() { + t.Error("exchange doesn't support websocket") + } + + b.Features.Supports.Websocket = true + if !b.SupportsWebsocket() { + t.Error("exchange supports websocket") + } +} + +func TestSupportsREST(t *testing.T) { + t.Parallel() + + var b Base + if b.SupportsREST() { + t.Error("exchange doesn't support REST") + } + + b.Features.Supports.REST = true + if !b.SupportsREST() { + t.Error("exchange supports REST") + } +} + +func TestIsWebsocketEnabled(t *testing.T) { + t.Parallel() + + var b Base + if b.IsWebsocketEnabled() { + t.Error("exchange doesn't support websocket") + } + + b.Websocket = wshandler.New() + err := b.Websocket.Setup(&wshandler.WebsocketSetup{Enabled: true}) + if err != nil { + t.Error(err) + } + if !b.IsWebsocketEnabled() { + t.Error("websocket should be enabled") + } +} + func TestSupportsWithdrawPermissions(t *testing.T) { + t.Parallel() + UAC := Base{Name: defaultTestExchange} - UAC.APIWithdrawPermissions = AutoWithdrawCrypto | AutoWithdrawCryptoWithAPIPermission + UAC.Features.Supports.WithdrawPermissions = AutoWithdrawCrypto | AutoWithdrawCryptoWithAPIPermission withdrawPermissions := UAC.SupportsWithdrawPermissions(AutoWithdrawCrypto) if !withdrawPermissions { @@ -936,14 +1285,10 @@ func TestSupportsWithdrawPermissions(t *testing.T) { } func TestFormatWithdrawPermissions(t *testing.T) { - cfg := config.GetConfig() - err := cfg.LoadConfig(config.ConfigTestFile) - if err != nil { - t.Fatal("Test failed. TestUpdateEnabledCurrencies failed to load config") - } + t.Parallel() UAC := Base{Name: defaultTestExchange} - UAC.APIWithdrawPermissions = AutoWithdrawCrypto | + UAC.Features.Supports.WithdrawPermissions = AutoWithdrawCrypto | AutoWithdrawCryptoWithAPIPermission | AutoWithdrawCryptoWithSetup | WithdrawCryptoWith2FA | @@ -968,7 +1313,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { t.Errorf("Expected: %s, Received: %s", AutoWithdrawCryptoText+" & "+AutoWithdrawCryptoWithAPIPermissionText, withdrawPermissions) } - UAC.APIWithdrawPermissions = NoAPIWithdrawalMethods + UAC.Features.Supports.WithdrawPermissions = NoAPIWithdrawalMethods withdrawPermissions = UAC.FormatWithdrawPermissions() if withdrawPermissions != NoAPIWithdrawalMethodsText { @@ -976,284 +1321,70 @@ func TestFormatWithdrawPermissions(t *testing.T) { } } -func TestOrderTypes(t *testing.T) { - var ot OrderType = "Mo'Money" - - if ot.ToString() != "Mo'Money" { - t.Errorf("test failed - unexpected string %s", ot.ToString()) +func TestSupportsAsset(t *testing.T) { + t.Parallel() + var b Base + b.CurrencyPairs.AssetTypes = asset.Items{ + asset.Spot, } - - var os OrderSide = "BUY" - - if os.ToString() != "BUY" { - t.Errorf("test failed - unexpected string %s", os.ToString()) + if !b.SupportsAsset(asset.Spot) { + t.Error("spot should be supported") + } + if b.SupportsAsset(asset.Index) { + t.Error("index shouldn't be supported") } } -func TestFilterOrdersByType(t *testing.T) { - var orders = []OrderDetail{ - { - OrderType: ImmediateOrCancelOrderType, - }, - { - OrderType: LimitOrderType, +func TestPrintEnabledPairs(t *testing.T) { + t.Parallel() + + var b Base + b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) + b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ + Enabled: currency.Pairs{ + currency.NewPair(currency.BTC, currency.USD), }, } - FilterOrdersByType(&orders, AnyOrderType) - if len(orders) != 2 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders)) + b.PrintEnabledPairs() +} +func TestGetBase(t *testing.T) { + t.Parallel() + + b := Base{ + Name: "MEOW", } - FilterOrdersByType(&orders, LimitOrderType) - if len(orders) != 1 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) - } + p := b.GetBase() + p.Name = "rawr" - FilterOrdersByType(&orders, StopOrderType) - if len(orders) != 0 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders)) + if b.Name != "rawr" { + t.Error("name should be rawr") } } -func TestFilterOrdersBySide(t *testing.T) { - var orders = []OrderDetail{ - { - OrderSide: BuyOrderSide, +func TestGetAssetType(t *testing.T) { + var b Base + p := currency.NewPair(currency.BTC, currency.USD) + _, err := b.GetPairAssetType(p) + if err == nil { + t.Fatal("error cannot be nil") + } + b.CurrencyPairs.AssetTypes = asset.Items{asset.Spot} + b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) + b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ + Enabled: currency.Pairs{ + currency.NewPair(currency.BTC, currency.USD), }, - { - OrderSide: SellOrderSide, - }, - {}, + ConfigFormat: ¤cy.PairFormat{Delimiter: "-"}, } - FilterOrdersBySide(&orders, AnyOrderSide) - if len(orders) != 3 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) + a, err := b.GetPairAssetType(p) + if err != nil { + t.Fatal(err) } - FilterOrdersBySide(&orders, BuyOrderSide) - if len(orders) != 1 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) - } - - FilterOrdersBySide(&orders, SellOrderSide) - if len(orders) != 0 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders)) - } -} - -func TestFilterOrdersByTickRange(t *testing.T) { - var orders = []OrderDetail{ - { - OrderDate: time.Unix(100, 0), - }, - { - OrderDate: time.Unix(110, 0), - }, - { - OrderDate: time.Unix(111, 0), - }, - } - - FilterOrdersByTickRange(&orders, time.Unix(0, 0), time.Unix(0, 0)) - if len(orders) != 3 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) - } - - FilterOrdersByTickRange(&orders, time.Unix(100, 0), time.Unix(111, 0)) - if len(orders) != 3 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) - } - - FilterOrdersByTickRange(&orders, time.Unix(101, 0), time.Unix(111, 0)) - if len(orders) != 2 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders)) - } - - FilterOrdersByTickRange(&orders, time.Unix(200, 0), time.Unix(300, 0)) - if len(orders) != 0 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders)) - } -} - -func TestFilterOrdersByCurrencies(t *testing.T) { - var orders = []OrderDetail{ - { - CurrencyPair: currency.NewPair(currency.BTC, currency.USD), - }, - { - CurrencyPair: currency.NewPair(currency.LTC, currency.EUR), - }, - { - CurrencyPair: currency.NewPair(currency.DOGE, currency.RUB), - }, - } - - currencies := []currency.Pair{currency.NewPair(currency.BTC, currency.USD), - currency.NewPair(currency.LTC, currency.EUR), - currency.NewPair(currency.DOGE, currency.RUB)} - FilterOrdersByCurrencies(&orders, currencies) - if len(orders) != 3 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) - } - - currencies = []currency.Pair{currency.NewPair(currency.BTC, currency.USD), - currency.NewPair(currency.LTC, currency.EUR)} - FilterOrdersByCurrencies(&orders, currencies) - if len(orders) != 2 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders)) - } - - currencies = []currency.Pair{currency.NewPair(currency.BTC, currency.USD)} - FilterOrdersByCurrencies(&orders, currencies) - if len(orders) != 1 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) - } - - currencies = []currency.Pair{} - FilterOrdersByCurrencies(&orders, currencies) - if len(orders) != 1 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) - } -} - -func TestSortOrdersByPrice(t *testing.T) { - orders := []OrderDetail{ - { - Price: 100, - }, { - Price: 0, - }, { - Price: 50, - }, - } - - SortOrdersByPrice(&orders, false) - if orders[0].Price != 0 { - t.Errorf("Test failed. Expected: '%v', received: '%v'", 0, orders[0].Price) - } - - SortOrdersByPrice(&orders, true) - if orders[0].Price != 100 { - t.Errorf("Test failed. Expected: '%v', received: '%v'", 100, orders[0].Price) - } -} - -func TestSortOrdersByDate(t *testing.T) { - orders := []OrderDetail{ - { - OrderDate: time.Unix(0, 0), - }, { - OrderDate: time.Unix(1, 0), - }, { - OrderDate: time.Unix(2, 0), - }, - } - - SortOrdersByDate(&orders, false) - if orders[0].OrderDate.Unix() != time.Unix(0, 0).Unix() { - t.Errorf("Test failed. Expected: '%v', received: '%v'", - time.Unix(0, 0).Unix(), - orders[0].OrderDate.Unix()) - } - - SortOrdersByDate(&orders, true) - if orders[0].OrderDate.Unix() != time.Unix(2, 0).Unix() { - t.Errorf("Test failed. Expected: '%v', received: '%v'", - time.Unix(2, 0).Unix(), - orders[0].OrderDate.Unix()) - } -} - -func TestSortOrdersByCurrency(t *testing.T) { - orders := []OrderDetail{ - { - CurrencyPair: currency.NewPairWithDelimiter(currency.BTC.String(), - currency.USD.String(), - "-"), - }, { - CurrencyPair: currency.NewPairWithDelimiter(currency.DOGE.String(), - currency.USD.String(), - "-"), - }, { - CurrencyPair: currency.NewPairWithDelimiter(currency.BTC.String(), - currency.RUB.String(), - "-"), - }, { - CurrencyPair: currency.NewPairWithDelimiter(currency.LTC.String(), - currency.EUR.String(), - "-"), - }, { - CurrencyPair: currency.NewPairWithDelimiter(currency.LTC.String(), - currency.AUD.String(), - "-"), - }, - } - - SortOrdersByCurrency(&orders, false) - if orders[0].CurrencyPair.String() != currency.BTC.String()+"-"+currency.RUB.String() { - t.Errorf("Test failed. Expected: '%v', received: '%v'", - currency.BTC.String()+"-"+currency.RUB.String(), - orders[0].CurrencyPair.String()) - } - - SortOrdersByCurrency(&orders, true) - if orders[0].CurrencyPair.String() != currency.LTC.String()+"-"+currency.EUR.String() { - t.Errorf("Test failed. Expected: '%v', received: '%v'", - currency.LTC.String()+"-"+currency.EUR.String(), - orders[0].CurrencyPair.String()) - } -} - -func TestSortOrdersByOrderSide(t *testing.T) { - orders := []OrderDetail{ - { - OrderSide: BuyOrderSide, - }, { - OrderSide: SellOrderSide, - }, { - OrderSide: SellOrderSide, - }, { - OrderSide: BuyOrderSide, - }, - } - - SortOrdersBySide(&orders, false) - if !strings.EqualFold(orders[0].OrderSide.ToString(), BuyOrderSide.ToString()) { - t.Errorf("Test failed. Expected: '%v', received: '%v'", - BuyOrderSide, - orders[0].OrderSide) - } - - SortOrdersBySide(&orders, true) - if !strings.EqualFold(orders[0].OrderSide.ToString(), SellOrderSide.ToString()) { - t.Errorf("Test failed. Expected: '%v', received: '%v'", - SellOrderSide, - orders[0].OrderSide) - } -} - -func TestSortOrdersByOrderType(t *testing.T) { - orders := []OrderDetail{ - { - OrderType: MarketOrderType, - }, { - OrderType: LimitOrderType, - }, { - OrderType: ImmediateOrCancelOrderType, - }, { - OrderType: TrailingStopOrderType, - }, - } - - SortOrdersByType(&orders, false) - if !strings.EqualFold(orders[0].OrderType.ToString(), ImmediateOrCancelOrderType.ToString()) { - t.Errorf("Test failed. Expected: '%v', received: '%v'", ImmediateOrCancelOrderType, orders[0].OrderType) - } - - SortOrdersByType(&orders, true) - if !strings.EqualFold(orders[0].OrderType.ToString(), TrailingStopOrderType.ToString()) { - t.Errorf("Test failed. Expected: '%v', received: '%v'", TrailingStopOrderType, orders[0].OrderType) + if a != asset.Spot { + t.Error("should be spot but is", a) } } diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go new file mode 100644 index 00000000..804f7921 --- /dev/null +++ b/exchanges/exchange_types.go @@ -0,0 +1,284 @@ +package exchange + +import ( + "time" + + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" +) + +// Endpoint authentication types +const ( + RestAuthentication uint8 = 0 + WebsocketAuthentication uint8 = 1 + // Repeated exchange strings + // FeeType custom type for calculating fees based on method + WireTransfer InternationalBankTransactionType = iota + PerfectMoney + Neteller + AdvCash + Payeer + Skrill + Simplex + SEPA + Swift + RapidTransfer + MisterTangoSEPA + Qiwi + VisaMastercard + WebMoney + Capitalist + WesternUnion + MoneyGram + Contact + // Const declarations for fee types + BankFee FeeType = iota + InternationalBankDepositFee + InternationalBankWithdrawalFee + CryptocurrencyTradeFee + CyptocurrencyDepositFee + CryptocurrencyWithdrawalFee + OfflineTradeFee + // Definitions for each type of withdrawal method for a given exchange + NoAPIWithdrawalMethods uint32 = 0 + NoAPIWithdrawalMethodsText string = "NONE, WEBSITE ONLY" + AutoWithdrawCrypto uint32 = (1 << 0) + AutoWithdrawCryptoWithAPIPermission uint32 = (1 << 1) + AutoWithdrawCryptoWithSetup uint32 = (1 << 2) + AutoWithdrawCryptoText string = "AUTO WITHDRAW CRYPTO" + AutoWithdrawCryptoWithAPIPermissionText string = "AUTO WITHDRAW CRYPTO WITH API PERMISSION" + AutoWithdrawCryptoWithSetupText string = "AUTO WITHDRAW CRYPTO WITH SETUP" + WithdrawCryptoWith2FA uint32 = (1 << 3) + WithdrawCryptoWithSMS uint32 = (1 << 4) + WithdrawCryptoWithEmail uint32 = (1 << 5) + WithdrawCryptoWithWebsiteApproval uint32 = (1 << 6) + WithdrawCryptoWithAPIPermission uint32 = (1 << 7) + WithdrawCryptoWith2FAText string = "WITHDRAW CRYPTO WITH 2FA" + WithdrawCryptoWithSMSText string = "WITHDRAW CRYPTO WITH SMS" + WithdrawCryptoWithEmailText string = "WITHDRAW CRYPTO WITH EMAIL" + WithdrawCryptoWithWebsiteApprovalText string = "WITHDRAW CRYPTO WITH WEBSITE APPROVAL" + WithdrawCryptoWithAPIPermissionText string = "WITHDRAW CRYPTO WITH API PERMISSION" + AutoWithdrawFiat uint32 = (1 << 8) + AutoWithdrawFiatWithAPIPermission uint32 = (1 << 9) + AutoWithdrawFiatWithSetup uint32 = (1 << 10) + AutoWithdrawFiatText string = "AUTO WITHDRAW FIAT" + AutoWithdrawFiatWithAPIPermissionText string = "AUTO WITHDRAW FIAT WITH API PERMISSION" + AutoWithdrawFiatWithSetupText string = "AUTO WITHDRAW FIAT WITH SETUP" + WithdrawFiatWith2FA uint32 = (1 << 11) + WithdrawFiatWithSMS uint32 = (1 << 12) + WithdrawFiatWithEmail uint32 = (1 << 13) + WithdrawFiatWithWebsiteApproval uint32 = (1 << 14) + WithdrawFiatWithAPIPermission uint32 = (1 << 15) + WithdrawFiatWith2FAText string = "WITHDRAW FIAT WITH 2FA" + WithdrawFiatWithSMSText string = "WITHDRAW FIAT WITH SMS" + WithdrawFiatWithEmailText string = "WITHDRAW FIAT WITH EMAIL" + WithdrawFiatWithWebsiteApprovalText string = "WITHDRAW FIAT WITH WEBSITE APPROVAL" + WithdrawFiatWithAPIPermissionText string = "WITHDRAW FIAT WITH API PERMISSION" + WithdrawCryptoViaWebsiteOnly uint32 = (1 << 16) + WithdrawFiatViaWebsiteOnly uint32 = (1 << 17) + WithdrawCryptoViaWebsiteOnlyText string = "WITHDRAW CRYPTO VIA WEBSITE ONLY" + WithdrawFiatViaWebsiteOnlyText string = "WITHDRAW FIAT VIA WEBSITE ONLY" + NoFiatWithdrawals uint32 = (1 << 18) + NoFiatWithdrawalsText string = "NO FIAT WITHDRAWAL" + UnknownWithdrawalTypeText string = "UNKNOWN" +) + +type FeeType uint8 + +// InternationalBankTransactionType custom type for calculating fees based on fiat transaction types +type InternationalBankTransactionType uint8 + +// FeeBuilder is the type which holds all parameters required to calculate a fee +// for an exchange +type FeeBuilder struct { + FeeType FeeType + // Used for calculating crypto trading fees, deposits & withdrawals + Pair currency.Pair + IsMaker bool + // Fiat currency used for bank deposits & withdrawals + FiatCurrency currency.Code + BankTransactionType InternationalBankTransactionType + // Used to multiply for fee calculations + PurchasePrice float64 + Amount float64 +} + +// AccountInfo is a Generic type to hold each exchange's holdings in +// all enabled currencies +type AccountInfo struct { + Exchange string + Accounts []Account +} + +// Account defines a singular account type with asocciated currencies +type Account struct { + ID string + Currencies []AccountCurrencyInfo +} + +// AccountCurrencyInfo is a sub type to store currency name and value +type AccountCurrencyInfo struct { + CurrencyName currency.Code + TotalValue float64 + Hold float64 +} + +// TradeHistory holds exchange history data +type TradeHistory struct { + Timestamp time.Time + TID int64 + Price float64 + Amount float64 + Exchange string + Type string + Fee float64 + Description string +} + +// FundHistory holds exchange funding history data +type FundHistory struct { + ExchangeName string + Status string + TransferID string + Description string + Timestamp time.Time + Currency string + Amount float64 + Fee float64 + TransferType string + CryptoToAddress string + CryptoFromAddress string + CryptoTxID string + BankTo string + BankFrom string +} + +// GenericWithdrawRequestInfo stores genric withdraw request info +type GenericWithdrawRequestInfo struct { + // General withdraw information + Currency currency.Code + Description string + OneTimePassword int64 + AccountID string + PIN int64 + TradePassword string + Amount float64 +} + +// CryptoWithdrawRequest stores the info required for a crypto withdrawal request +type CryptoWithdrawRequest struct { + GenericWithdrawRequestInfo + // Crypto related information + Address string + AddressTag string + FeeAmount float64 +} + +// FiatWithdrawRequest used for fiat withdrawal requests +type FiatWithdrawRequest struct { + GenericWithdrawRequestInfo + // FIAT related information + BankAccountName string + BankAccountNumber string + BankName string + BankAddress string + BankCity string + BankCountry string + BankPostalCode string + BSB string + SwiftCode string + IBAN string + BankCode float64 + IsExpressWire bool + // Intermediary bank information + RequiresIntermediaryBank bool + IntermediaryBankAccountNumber float64 + IntermediaryBankName string + IntermediaryBankAddress string + IntermediaryBankCity string + IntermediaryBankCountry string + IntermediaryBankPostalCode string + IntermediarySwiftCode string + IntermediaryBankCode float64 + IntermediaryIBAN string + WireCurrency string +} + +// Features stores the supported and enabled features +// for the exchange +type Features struct { + Supports FeaturesSupported + Enabled FeaturesEnabled +} + +// FeaturesEnabled stores the exchange enabled features +type FeaturesEnabled struct { + AutoPairUpdates bool +} + +// FeaturesSupported stores the exchanges supported features +type FeaturesSupported struct { + REST bool + RESTCapabilities protocol.Features + Websocket bool + WebsocketCapabilities protocol.Features + WithdrawPermissions uint32 +} + +// API stores the exchange API settings +type API struct { + AuthenticatedSupport bool + AuthenticatedWebsocketSupport bool + PEMKeySupport bool + + Endpoints struct { + URL string + URLDefault string + URLSecondary string + URLSecondaryDefault string + WebsocketURL string + } + + Credentials struct { + Key string + Secret string + ClientID string + PEMKey string + } + + CredentialsValidator struct { + // For Huobi (optional) + RequiresPEM bool + + RequiresKey bool + RequiresSecret bool + RequiresClientID bool + RequiresBase64DecodeSecret bool + } +} + +// Base stores the individual exchange information +type Base struct { + Name string + Enabled bool + Verbose bool + LoadedByConfig bool + SkipAuthCheck bool + API API + BaseCurrencies currency.Currencies + CurrencyPairs currency.PairsManager + Features Features + HTTPTimeout time.Duration + HTTPUserAgent string + HTTPRecording bool + HTTPDebugging bool + WebsocketResponseCheckTimeout time.Duration + WebsocketResponseMaxLimit time.Duration + WebsocketOrderbookBufferLimit int64 + Websocket *wshandler.Websocket + *request.Requester + Config *config.ExchangeConfig +} diff --git a/exchanges/exmo/README.md b/exchanges/exmo/README.md index 5e02cc8a..64e212d8 100644 --- a/exchanges/exmo/README.md +++ b/exchanges/exmo/README.md @@ -47,22 +47,22 @@ main.go ```go var e exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Exmo" { - e = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Exmo" { + e = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := e.GetTickerPrice() +tick, err := e.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := e.GetOrderbookEx() +ob, err := e.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +130,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/exmo/exmo.go b/exchanges/exmo/exmo.go index 8c257a47..0e2dd899 100644 --- a/exchanges/exmo/exmo.go +++ b/exchanges/exmo/exmo.go @@ -7,15 +7,11 @@ import ( "net/url" "strconv" "strings" - "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -53,76 +49,12 @@ type EXMO struct { exchange.Base } -// SetDefaults sets the basic defaults for exmo -func (e *EXMO) SetDefaults() { - e.Name = "EXMO" - e.Enabled = false - e.Verbose = false - e.RESTPollingDelay = 10 - e.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithSetup | exchange.NoFiatWithdrawals - e.RequestCurrencyPairFormat.Delimiter = "_" - e.RequestCurrencyPairFormat.Uppercase = true - e.RequestCurrencyPairFormat.Separator = "," - e.ConfigCurrencyPairFormat.Delimiter = "_" - e.ConfigCurrencyPairFormat.Uppercase = true - e.AssetTypes = []string{ticker.Spot} - e.SupportsAutoPairUpdating = true - e.SupportsRESTTickerBatching = true - e.Requester = request.New(e.Name, - request.NewRateLimit(time.Minute, exmoAuthRate), - request.NewRateLimit(time.Minute, exmoUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - e.APIUrlDefault = exmoAPIURL - e.APIUrl = e.APIUrlDefault - e.Websocket = wshandler.New() -} - -// Setup takes in the supplied exchange configuration details and sets params -func (e *EXMO) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - e.SetEnabled(false) - } else { - e.Enabled = true - e.HTTPDebugging = exch.HTTPDebugging - e.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - e.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - e.SetHTTPClientTimeout(exch.HTTPTimeout) - e.SetHTTPClientUserAgent(exch.HTTPUserAgent) - e.RESTPollingDelay = exch.RESTPollingDelay - e.Verbose = exch.Verbose - e.BaseCurrencies = exch.BaseCurrencies - e.AvailablePairs = exch.AvailablePairs - e.EnabledPairs = exch.EnabledPairs - err := e.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = e.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = e.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = e.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = e.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - } -} - // GetTrades returns the trades for a symbol or symbols func (e *EXMO) GetTrades(symbol string) (map[string][]Trades, error) { v := url.Values{} v.Set("pair", symbol) result := make(map[string][]Trades) - urlPath := fmt.Sprintf("%s/v%s/%s", e.APIUrl, exmoAPIVersion, exmoTrades) - + urlPath := fmt.Sprintf("%s/v%s/%s", e.API.Endpoints.URL, exmoAPIVersion, exmoTrades) return result, e.SendHTTPRequest(common.EncodeURLValues(urlPath, v), &result) } @@ -131,33 +63,29 @@ func (e *EXMO) GetOrderbook(symbol string) (map[string]Orderbook, error) { v := url.Values{} v.Set("pair", symbol) result := make(map[string]Orderbook) - urlPath := fmt.Sprintf("%s/v%s/%s", e.APIUrl, exmoAPIVersion, exmoOrderbook) - + urlPath := fmt.Sprintf("%s/v%s/%s", e.API.Endpoints.URL, exmoAPIVersion, exmoOrderbook) return result, e.SendHTTPRequest(common.EncodeURLValues(urlPath, v), &result) } // GetTicker returns the ticker for a symbol or symbols -func (e *EXMO) GetTicker(symbol string) (map[string]Ticker, error) { +func (e *EXMO) GetTicker() (map[string]Ticker, error) { v := url.Values{} - v.Set("pair", symbol) result := make(map[string]Ticker) - urlPath := fmt.Sprintf("%s/v%s/%s", e.APIUrl, exmoAPIVersion, exmoTicker) - + urlPath := fmt.Sprintf("%s/v%s/%s", e.API.Endpoints.URL, exmoAPIVersion, exmoTicker) return result, e.SendHTTPRequest(common.EncodeURLValues(urlPath, v), &result) } // GetPairSettings returns the pair settings for a symbol or symbols func (e *EXMO) GetPairSettings() (map[string]PairSettings, error) { result := make(map[string]PairSettings) - urlPath := fmt.Sprintf("%s/v%s/%s", e.APIUrl, exmoAPIVersion, exmoPairSettings) - + urlPath := fmt.Sprintf("%s/v%s/%s", e.API.Endpoints.URL, exmoAPIVersion, exmoPairSettings) return result, e.SendHTTPRequest(urlPath, &result) } // GetCurrency returns a list of currencies func (e *EXMO) GetCurrency() ([]string, error) { var result []string - urlPath := fmt.Sprintf("%s/v%s/%s", e.APIUrl, exmoAPIVersion, exmoCurrency) + urlPath := fmt.Sprintf("%s/v%s/%s", e.API.Endpoints.URL, exmoAPIVersion, exmoCurrency) return result, e.SendHTTPRequest(urlPath, &result) } @@ -282,12 +210,9 @@ func (e *EXMO) GetCryptoDepositAddress() (map[string]string, error) { switch r := result.(type) { case map[string]interface{}: mapString := make(map[string]string) - for key, value := range r { - strValue := fmt.Sprintf("%v", value) - mapString[key] = strValue + mapString[key] = value.(string) } - return mapString, nil default: @@ -309,7 +234,7 @@ func (e *EXMO) WithdrawCryptocurrency(currency, address, invoice string, amount v.Set("currency", currency) v.Set("address", address) - if common.StringToUpper(currency) == "XRP" { + if strings.EqualFold(currency, "XRP") { v.Set(invoice, invoice) } @@ -387,7 +312,7 @@ func (e *EXMO) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated HTTP request func (e *EXMO) SendAuthenticatedHTTPRequest(method, endpoint string, vals url.Values, result interface{}) error { - if !e.AuthenticatedAPISupport { + if !e.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, e.Name) } @@ -396,23 +321,23 @@ func (e *EXMO) SendAuthenticatedHTTPRequest(method, endpoint string, vals url.Va vals.Set("nonce", n) payload := vals.Encode() - hash := common.GetHMAC(common.HashSHA512, + hash := crypto.GetHMAC(crypto.HashSHA512, []byte(payload), - []byte(e.APISecret)) + []byte(e.API.Credentials.Secret)) if e.Verbose { - log.Debugf("Sending %s request to %s with params %s\n", + log.Debugf(log.ExchangeSys, "Sending %s request to %s with params %s\n", method, endpoint, payload) } headers := make(map[string]string) - headers["Key"] = e.APIKey - headers["Sign"] = common.HexEncodeToString(hash) + headers["Key"] = e.API.Credentials.Key + headers["Sign"] = crypto.HexEncodeToString(hash) headers["Content-Type"] = "application/x-www-form-urlencoded" - path := fmt.Sprintf("%s/v%s/%s", e.APIUrl, exmoAPIVersion, endpoint) + path := fmt.Sprintf("%s/v%s/%s", e.API.Endpoints.URL, exmoAPIVersion, endpoint) return e.SendPayload(method, path, diff --git a/exchanges/exmo/exmo_test.go b/exchanges/exmo/exmo_test.go index 02af8411..2a5fa1d9 100644 --- a/exchanges/exmo/exmo_test.go +++ b/exchanges/exmo/exmo_test.go @@ -1,12 +1,15 @@ package exmo import ( + "log" + "os" "testing" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) const ( @@ -19,33 +22,35 @@ var ( e EXMO ) -func TestDefault(t *testing.T) { +func TestMain(m *testing.M) { e.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Exmo load config error", err) + } exmoConf, err := cfg.GetExchangeConfig("EXMO") if err != nil { - t.Error("Test Failed - OKCoin Setup() init error") + log.Fatal("Exmo Setup() init error") } - exmoConf.AuthenticatedAPISupport = true - exmoConf.APIKey = APIKey - exmoConf.APISecret = APISecret - e.Setup(&exmoConf) + err = e.Setup(exmoConf) + if err != nil { + log.Fatal("Exmo setup error", err) + } - e.AuthenticatedAPISupport = true - e.APIKey = APIKey - e.APISecret = APISecret + e.API.AuthenticatedSupport = true + e.API.Credentials.Key = APIKey + e.API.Credentials.Secret = APISecret + + os.Exit(m.Run()) } func TestGetTrades(t *testing.T) { t.Parallel() _, err := e.GetTrades("BTC_USD") if err != nil { - t.Errorf("Test failed. Err: %s", err) + t.Errorf("Err: %s", err) } } @@ -53,15 +58,15 @@ func TestGetOrderbook(t *testing.T) { t.Parallel() _, err := e.GetOrderbook("BTC_USD") if err != nil { - t.Errorf("Test failed. Err: %s", err) + t.Errorf("Err: %s", err) } } func TestGetTicker(t *testing.T) { t.Parallel() - _, err := e.GetTicker("BTC_USD") + _, err := e.GetTicker() if err != nil { - t.Errorf("Test failed. Err: %s", err) + t.Errorf("Err: %s", err) } } @@ -69,7 +74,7 @@ func TestGetPairSettings(t *testing.T) { t.Parallel() _, err := e.GetPairSettings() if err != nil { - t.Errorf("Test failed. Err: %s", err) + t.Errorf("Err: %s", err) } } @@ -77,31 +82,29 @@ func TestGetCurrency(t *testing.T) { t.Parallel() _, err := e.GetCurrency() if err != nil { - t.Errorf("Test failed. Err: %s", err) + t.Errorf("Err: %s", err) } } func TestGetUserInfo(t *testing.T) { t.Parallel() - if APIKey == "" || APISecret == "" { + if !areTestAPIKeysSet() { t.Skip() } - TestSetup(t) _, err := e.GetUserInfo() if err != nil { - t.Errorf("Test failed. Err: %s", err) + t.Errorf("Err: %s", err) } } func TestGetRequiredAmount(t *testing.T) { t.Parallel() - if APIKey == "" || APISecret == "" { + if !areTestAPIKeysSet() { t.Skip() } - TestSetup(t) _, err := e.GetRequiredAmount("BTC_USD", 100) if err != nil { - t.Errorf("Test failed. Err: %s", err) + t.Errorf("Err: %s", err) } } @@ -120,7 +123,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() e.GetFeeByType(feeBuilder) - if APIKey == "" || APISecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -132,8 +135,6 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - e.SetDefaults() - TestSetup(t) t.Parallel() var feeBuilder = setFeeBuilder() @@ -141,7 +142,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee Basic if resp, err := e.GetFee(feeBuilder); resp != float64(0.002) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity @@ -149,7 +150,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := e.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) t.Error(err) } @@ -157,7 +158,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := e.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) t.Error(err) } @@ -165,7 +166,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := e.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -173,7 +174,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := e.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) t.Error(err) } @@ -182,7 +183,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := e.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -190,7 +191,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := e.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -199,7 +200,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.RUB if resp, err := e.GetFee(feeBuilder); resp != float64(1600) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(1600), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(1600), resp) t.Error(err) } @@ -208,7 +209,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankDepositFee feeBuilder.FiatCurrency = currency.PLN if resp, err := e.GetFee(feeBuilder); resp != float64(30) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(30), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(30), resp) t.Error(err) } @@ -217,7 +218,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.PLN if resp, err := e.GetFee(feeBuilder); resp != float64(125) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(125), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(125), resp) t.Error(err) } @@ -226,7 +227,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.TRY if resp, err := e.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -235,7 +236,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.EUR if resp, err := e.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -244,28 +245,22 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.RUB if resp, err := e.GetFee(feeBuilder); resp != float64(3200) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(3200), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(3200), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - e.SetDefaults() expectedResult := exchange.AutoWithdrawCryptoWithSetupText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := e.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - e.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := e.GetActiveOrders(&getOrdersRequest) @@ -277,11 +272,8 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - e.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } currPair := currency.NewPair(currency.BTC, currency.USD) currPair.Delimiter = "_" @@ -298,26 +290,27 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if e.APIKey != "" && e.APIKey != "Key" && - e.APISecret != "" && e.APISecret != "Secret" { - return true - } - return false + return e.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - e.SetDefaults() - TestSetup(t) if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "_", - Base: currency.BTC, - Quote: currency.USD, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: "_", + Base: currency.BTC, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := e.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 10, "1234234") + response, err := e.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -326,15 +319,12 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - e.SetDefaults() - TestSetup(t) if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -351,15 +341,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - e.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -375,32 +362,35 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestModifyOrder(t *testing.T) { - _, err := e.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := e.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - e.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.LTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", - } - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", + } + _, err := e.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) if !areTestAPIKeysSet() && err == nil { t.Error("Expecting an error when no keys are set") @@ -411,15 +401,11 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - e.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := e.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -427,15 +413,11 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - e.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := e.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -446,12 +428,12 @@ func TestGetDepositAddress(t *testing.T) { if areTestAPIKeysSet() { _, err := e.GetDepositAddress(currency.LTC, "") if err != nil { - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) } } else { _, err := e.GetDepositAddress(currency.LTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error cannot be nil") + t.Error("GetDepositAddress() error cannot be nil") } } } diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index 5502096e..374cbf81 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -9,14 +9,119 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (e *EXMO) GetDefaultConfig() (*config.ExchangeConfig, error) { + e.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = e.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = e.BaseCurrencies + + err := e.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if e.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = e.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for exmo +func (e *EXMO) SetDefaults() { + e.Name = "EXMO" + e.Enabled = true + e.Verbose = true + e.API.CredentialsValidator.RequiresKey = true + e.API.CredentialsValidator.RequiresSecret = true + + e.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "_", + Uppercase: true, + Separator: ",", + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "_", + Uppercase: true, + }, + } + + e.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: false, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + SubmitOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + FiatDepositFee: true, + FiatWithdrawalFee: true, + CryptoDepositFee: true, + CryptoWithdrawalFee: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + e.Requester = request.New(e.Name, + request.NewRateLimit(time.Minute, exmoAuthRate), + request.NewRateLimit(time.Minute, exmoUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + e.API.Endpoints.URLDefault = exmoAPIURL + e.API.Endpoints.URL = e.API.Endpoints.URLDefault +} + +// Setup takes in the supplied exchange configuration details and sets params +func (e *EXMO) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + e.SetEnabled(false) + return nil + } + + return e.SetupDefaults(exch) +} + // Start starts the EXMO go routine func (e *EXMO) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -29,77 +134,91 @@ func (e *EXMO) Start(wg *sync.WaitGroup) { // Run implements the EXMO wrapper func (e *EXMO) Run() { if e.Verbose { - log.Debugf("%s polling delay: %ds.\n", e.GetName(), e.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", e.GetName(), len(e.EnabledPairs), e.EnabledPairs) + e.PrintEnabledPairs() } - exchangeProducts, err := e.GetPairSettings() + if !e.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := e.UpdateTradablePairs(false) if err != nil { - log.Errorf("%s Failed to get available products.\n", e.GetName()) - } else { - var currencies []string - for x := range exchangeProducts { - currencies = append(currencies, x) - } - - var newCurrencies currency.Pairs - for _, p := range currencies { - newCurrencies = append(newCurrencies, - currency.NewPairFromString(p)) - } - - err = e.UpdateCurrencies(newCurrencies, false, false) - if err != nil { - log.Errorf("%s Failed to update available currencies.\n", e.GetName()) - } + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", e.Name, err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (e *EXMO) FetchTradablePairs(asset asset.Item) ([]string, error) { + pairs, err := e.GetPairSettings() + if err != nil { + return nil, err + } + + var currencies []string + for x := range pairs { + currencies = append(currencies, x) + } + + return currencies, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (e *EXMO) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := e.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return e.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (e *EXMO) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (e *EXMO) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - pairsCollated, err := exchange.GetAndFormatExchangeCurrencies(e.Name, e.GetEnabledCurrencies()) + result, err := e.GetTicker() if err != nil { return tickerPrice, err } - - result, err := e.GetTicker(pairsCollated) - if err != nil { + if _, ok := result[p.String()]; !ok { return tickerPrice, err } - - for _, x := range e.GetEnabledCurrencies() { - currency := exchange.FormatExchangeCurrency(e.Name, x).String() - var tickerPrice ticker.Price - tickerPrice.Pair = x - tickerPrice.Last = result[currency].Last - tickerPrice.Ask = result[currency].Sell - tickerPrice.High = result[currency].High - tickerPrice.Bid = result[currency].Buy - tickerPrice.Last = result[currency].Last - tickerPrice.Low = result[currency].Low - tickerPrice.Volume = result[currency].Volume - - err = ticker.ProcessTicker(e.Name, &tickerPrice, assetType) - if err != nil { - return tickerPrice, err + pairs := e.GetEnabledPairs(assetType) + for i := range pairs { + for j := range result { + if !strings.EqualFold(pairs[i].String(), j) { + continue + } + tickerPrice = ticker.Price{ + Pair: pairs[i], + Last: result[j].Last, + Ask: result[j].Sell, + High: result[j].High, + Bid: result[j].Buy, + Low: result[j].Low, + Volume: result[j].Volume, + } + err = ticker.ProcessTicker(e.Name, &tickerPrice, assetType) + if err != nil { + log.Error(log.Ticker, err) + } } } return ticker.GetTicker(e.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (e *EXMO) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tick, err := ticker.GetTicker(e.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (e *EXMO) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tick, err := ticker.GetTicker(e.Name, p, assetType) if err != nil { return e.UpdateTicker(p, assetType) } return tick, nil } -// GetOrderbookEx returns the orderbook for a currency pair -func (e *EXMO) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(e.GetName(), p, assetType) +// FetchOrderbook returns the orderbook for a currency pair +func (e *EXMO) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(e.Name, p, assetType) if err != nil { return e.UpdateOrderbook(p, assetType) } @@ -107,9 +226,10 @@ func (e *EXMO) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (e *EXMO) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (e *EXMO) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - pairsCollated, err := exchange.GetAndFormatExchangeCurrencies(e.Name, e.GetEnabledCurrencies()) + pairsCollated, err := e.FormatExchangeCurrencies(e.GetEnabledPairs(assetType), + assetType) if err != nil { return orderBook, err } @@ -118,10 +238,10 @@ func (e *EXMO) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Bas if err != nil { return orderBook, err } - - for _, x := range e.GetEnabledCurrencies() { - currency := exchange.FormatExchangeCurrency(e.Name, x) - data, ok := result[currency.String()] + enabledPairs := e.GetEnabledPairs(assetType) + for i := range enabledPairs { + curr := e.FormatExchangeCurrency(enabledPairs[i], assetType) + data, ok := result[curr.String()] if !ok { continue } @@ -131,7 +251,8 @@ func (e *EXMO) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Bas z := data.Ask[y] price, _ := strconv.ParseFloat(z[0], 64) amount, _ := strconv.ParseFloat(z[1], 64) - obItems = append(obItems, orderbook.Item{Price: price, Amount: amount}) + obItems = append(obItems, + orderbook.Item{Price: price, Amount: amount}) } orderBook.Asks = obItems @@ -140,12 +261,13 @@ func (e *EXMO) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Bas z := data.Bid[y] price, _ := strconv.ParseFloat(z[0], 64) amount, _ := strconv.ParseFloat(z[1], 64) - obItems = append(obItems, orderbook.Item{Price: price, Amount: amount}) + obItems = append(obItems, + orderbook.Item{Price: price, Amount: amount}) } orderBook.Bids = obItems - orderBook.Pair = x - orderBook.ExchangeName = e.GetName() + orderBook.Pair = enabledPairs[i] + orderBook.ExchangeName = e.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -160,7 +282,7 @@ func (e *EXMO) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Bas // Exmo exchange func (e *EXMO) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = e.GetName() + response.Exchange = e.Name result, err := e.GetUserInfo() if err != nil { return response, err @@ -191,55 +313,59 @@ func (e *EXMO) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (e *EXMO) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (e *EXMO) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (e *EXMO) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (e *EXMO) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse +func (e *EXMO) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } + var oT string - - switch orderType { - case exchange.LimitOrderType: + switch s.OrderType { + case order.Limit: return submitOrderResponse, errors.New("unsupported order type") - case exchange.MarketOrderType: - oT = "market_buy" - if side == exchange.SellOrderSide { + case order.Market: + if s.OrderSide == order.Sell { oT = "market_sell" + } else { + oT = "market_buy" } - default: - return submitOrderResponse, errors.New("unsupported order type") } - response, err := e.CreateOrder(p.String(), oT, price, amount) - + response, err := e.CreateOrder(s.Pair.String(), + oT, + s.Price, + s.Amount) + if err != nil { + return submitOrderResponse, err + } if response > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response) + submitOrderResponse.OrderID = strconv.FormatInt(response, 10) } - if err == nil { - submitOrderResponse.IsOrderPlaced = true + submitOrderResponse.IsOrderPlaced = true + if s.OrderType == order.Market { + submitOrderResponse.FullyMatched = true } - - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (e *EXMO) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (e *EXMO) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (e *EXMO) CancelOrder(order *exchange.OrderCancellation) error { +func (e *EXMO) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) if err != nil { return err @@ -249,9 +375,9 @@ func (e *EXMO) CancelOrder(order *exchange.OrderCancellation) error { } // CancelAllOrders cancels all orders associated with a currency pair -func (e *EXMO) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (e *EXMO) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } openOrders, err := e.GetOpenOrders() @@ -259,10 +385,10 @@ func (e *EXMO) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAl return cancelAllOrdersResponse, err } - for _, order := range openOrders { - err = e.CancelExistingOrder(order.OrderID) + for i := range openOrders { + err = e.CancelExistingOrder(openOrders[i].OrderID) if err != nil { - cancelAllOrdersResponse.OrderStatus[strconv.FormatInt(order.OrderID, 10)] = err.Error() + cancelAllOrdersResponse.Status[strconv.FormatInt(openOrders[i].OrderID, 10)] = err.Error() } } @@ -270,8 +396,8 @@ func (e *EXMO) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAl } // GetOrderInfo returns information on a current open order -func (e *EXMO) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (e *EXMO) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -292,24 +418,24 @@ func (e *EXMO) GetDepositAddress(cryptocurrency currency.Code, _ string) (string // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (e *EXMO) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (e *EXMO) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { resp, err := e.WithdrawCryptocurrency(withdrawRequest.Currency.String(), withdrawRequest.Address, withdrawRequest.AddressTag, withdrawRequest.Amount) - return fmt.Sprintf("%v", resp), err + return strconv.FormatInt(resp, 10), err } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (e *EXMO) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (e *EXMO) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (e *EXMO) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (e *EXMO) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -320,7 +446,7 @@ func (e *EXMO) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (e *EXMO) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (e.APIKey == "" || e.APISecret == "") && // Todo check connection status + if !e.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -328,73 +454,69 @@ func (e *EXMO) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { } // GetActiveOrders retrieves any orders that are active/open -func (e *EXMO) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (e *EXMO) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := e.GetOpenOrders() if err != nil { return nil, err } - var orders []exchange.OrderDetail - for _, order := range resp { - symbol := currency.NewPairDelimiter(order.Pair, "_") - orderDate := time.Unix(order.Created, 0) - orderSide := exchange.OrderSide(strings.ToUpper(order.Type)) - orders = append(orders, exchange.OrderDetail{ - ID: fmt.Sprintf("%v", order.OrderID), - Amount: order.Quantity, + var orders []order.Detail + for i := range resp { + symbol := currency.NewPairDelimiter(resp[i].Pair, "_") + orderDate := time.Unix(resp[i].Created, 0) + orderSide := order.Side(strings.ToUpper(resp[i].Type)) + orders = append(orders, order.Detail{ + ID: strconv.FormatInt(resp[i].OrderID, 10), + Amount: resp[i].Quantity, OrderDate: orderDate, - Price: order.Price, + Price: resp[i].Price, OrderSide: orderSide, Exchange: e.Name, CurrencyPair: symbol, }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (e *EXMO) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - if len(getOrdersRequest.Currencies) == 0 { +func (e *EXMO) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + if len(req.Currencies) == 0 { return nil, errors.New("currency must be supplied") } var allTrades []UserTrades - for _, currency := range getOrdersRequest.Currencies { - resp, err := e.GetUserTrades(exchange.FormatExchangeCurrency(e.Name, currency).String(), "", "10000") + for i := range req.Currencies { + resp, err := e.GetUserTrades(e.FormatExchangeCurrency(req.Currencies[i], asset.Spot).String(), "", "10000") if err != nil { return nil, err } - for _, order := range resp { - allTrades = append(allTrades, order...) + for j := range resp { + allTrades = append(allTrades, resp[j]...) } } - var orders []exchange.OrderDetail - for _, order := range allTrades { - symbol := currency.NewPairDelimiter(order.Pair, "_") - orderDate := time.Unix(order.Date, 0) - orderSide := exchange.OrderSide(strings.ToUpper(order.Type)) - orders = append(orders, exchange.OrderDetail{ - ID: fmt.Sprintf("%v", order.TradeID), - Amount: order.Quantity, + var orders []order.Detail + for i := range allTrades { + symbol := currency.NewPairDelimiter(allTrades[i].Pair, "_") + orderDate := time.Unix(allTrades[i].Date, 0) + orderSide := order.Side(strings.ToUpper(allTrades[i].Type)) + orders = append(orders, order.Detail{ + ID: strconv.FormatInt(allTrades[i].TradeID, 10), + Amount: allTrades[i].Quantity, OrderDate: orderDate, - Price: order.Price, + Price: allTrades[i].Price, OrderSide: orderSide, Exchange: e.Name, CurrencyPair: symbol, }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } diff --git a/exchanges/gateio/README.md b/exchanges/gateio/README.md index 0b6b27ed..f4e9763c 100644 --- a/exchanges/gateio/README.md +++ b/exchanges/gateio/README.md @@ -47,22 +47,22 @@ main.go ```go var g exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "GateIO" { - g = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "GateIO" { + g = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := g.GetTickerPrice() +tick, err := g.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := g.GetOrderbookEx() +ob, err := g.FetchOrderbook() if err != nil { // Handle error } @@ -137,4 +137,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index 07d3069c..d9551eb9 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -7,16 +7,12 @@ import ( "net/http" "strconv" "strings" - "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( @@ -51,119 +47,10 @@ type Gateio struct { exchange.Base } -// SetDefaults sets default values for the exchange -func (g *Gateio) SetDefaults() { - g.Name = "GateIO" - g.Enabled = false - g.Verbose = false - g.RESTPollingDelay = 10 - g.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | - exchange.NoFiatWithdrawals - g.RequestCurrencyPairFormat.Delimiter = "_" - g.RequestCurrencyPairFormat.Uppercase = false - g.ConfigCurrencyPairFormat.Delimiter = "_" - g.ConfigCurrencyPairFormat.Uppercase = true - g.AssetTypes = []string{ticker.Spot} - g.SupportsAutoPairUpdating = true - g.SupportsRESTTickerBatching = true - g.Requester = request.New(g.Name, - request.NewRateLimit(time.Second*10, gateioAuthRate), - request.NewRateLimit(time.Second*10, gateioUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - g.APIUrlDefault = gateioTradeURL - g.APIUrl = g.APIUrlDefault - g.APIUrlSecondaryDefault = gateioMarketURL - g.APIUrlSecondary = g.APIUrlSecondaryDefault - g.Websocket = wshandler.New() - g.Websocket.Functionality = wshandler.WebsocketTickerSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketKlineSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported | - wshandler.WebsocketMessageCorrelationSupported - g.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - g.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - g.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup sets user configuration -func (g *Gateio) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - g.SetEnabled(false) - } else { - g.Enabled = true - g.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - g.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - g.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - g.APIAuthPEMKey = exch.APIAuthPEMKey - g.SetHTTPClientTimeout(exch.HTTPTimeout) - g.SetHTTPClientUserAgent(exch.HTTPUserAgent) - g.RESTPollingDelay = exch.RESTPollingDelay - g.Verbose = exch.Verbose - g.BaseCurrencies = exch.BaseCurrencies - g.AvailablePairs = exch.AvailablePairs - g.EnabledPairs = exch.EnabledPairs - g.WebsocketURL = gateioWebsocketEndpoint - g.HTTPDebugging = exch.HTTPDebugging - err := g.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = g.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = g.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = g.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = g.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = g.Websocket.Setup(g.WsConnect, - g.Subscribe, - g.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - gateioWebsocketEndpoint, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - g.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: g.Name, - URL: g.Websocket.GetWebsocketURL(), - ProxyURL: g.Websocket.GetProxyAddress(), - Verbose: g.Verbose, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - RateLimit: gateioWebsocketRateLimit, - } - g.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - false, - false, - false, - exch.Name) - } -} - // GetSymbols returns all supported symbols func (g *Gateio) GetSymbols() ([]string, error) { var result []string - - urlPath := fmt.Sprintf("%s/%s/%s", g.APIUrlSecondary, gateioAPIVersion, gateioSymbol) - + urlPath := fmt.Sprintf("%s/%s/%s", g.API.Endpoints.URLSecondary, gateioAPIVersion, gateioSymbol) err := g.SendHTTPRequest(urlPath, &result) if err != nil { return nil, nil @@ -179,8 +66,7 @@ func (g *Gateio) GetMarketInfo() (MarketInfoResponse, error) { Pairs []interface{} `json:"pairs"` } - urlPath := fmt.Sprintf("%s/%s/%s", g.APIUrlSecondary, gateioAPIVersion, gateioMarketInfo) - + urlPath := fmt.Sprintf("%s/%s/%s", g.API.Endpoints.URLSecondary, gateioAPIVersion, gateioMarketInfo) var res response var result MarketInfoResponse err := g.SendHTTPRequest(urlPath, &res) @@ -220,15 +106,14 @@ func (g *Gateio) GetLatestSpotPrice(symbol string) (float64, error) { // GetTicker returns a ticker for the supplied symbol // updated every 10 seconds func (g *Gateio) GetTicker(symbol string) (TickerResponse, error) { - urlPath := fmt.Sprintf("%s/%s/%s/%s", g.APIUrlSecondary, gateioAPIVersion, gateioTicker, symbol) + urlPath := fmt.Sprintf("%s/%s/%s/%s", g.API.Endpoints.URLSecondary, gateioAPIVersion, gateioTicker, symbol) var res TickerResponse return res, g.SendHTTPRequest(urlPath, &res) } // GetTickers returns tickers for all symbols func (g *Gateio) GetTickers() (map[string]TickerResponse, error) { - urlPath := fmt.Sprintf("%s/%s/%s", g.APIUrlSecondary, gateioAPIVersion, gateioTickers) - + urlPath := fmt.Sprintf("%s/%s/%s", g.API.Endpoints.URLSecondary, gateioAPIVersion, gateioTickers) resp := make(map[string]TickerResponse) err := g.SendHTTPRequest(urlPath, &resp) if err != nil { @@ -239,8 +124,7 @@ func (g *Gateio) GetTickers() (map[string]TickerResponse, error) { // GetOrderbook returns the orderbook data for a suppled symbol func (g *Gateio) GetOrderbook(symbol string) (Orderbook, error) { - urlPath := fmt.Sprintf("%s/%s/%s/%s", g.APIUrlSecondary, gateioAPIVersion, gateioOrderbook, symbol) - + urlPath := fmt.Sprintf("%s/%s/%s/%s", g.API.Endpoints.URLSecondary, gateioAPIVersion, gateioOrderbook, symbol) var resp OrderbookResponse err := g.SendHTTPRequest(urlPath, &resp) if err != nil { @@ -302,7 +186,7 @@ func (g *Gateio) GetOrderbook(symbol string) (Orderbook, error) { // GetSpotKline returns kline data for the most recent time period func (g *Gateio) GetSpotKline(arg KlinesRequestParams) ([]*KLineResponse, error) { urlPath := fmt.Sprintf("%s/%s/%s/%s?group_sec=%d&range_hour=%d", - g.APIUrlSecondary, + g.API.Endpoints.URLSecondary, gateioAPIVersion, gateioKline, arg.Symbol, @@ -328,31 +212,31 @@ func (g *Gateio) GetSpotKline(arg KlinesRequestParams) ([]*KLineResponse, error) for _, k := range rawKlineDatas { otString, _ := strconv.ParseFloat(k[0].(string), 64) - ot, err := common.TimeFromUnixTimestampFloat(otString) + ot, err := convert.TimeFromUnixTimestampFloat(otString) if err != nil { return nil, fmt.Errorf("cannot parse Kline.OpenTime. Err: %s", err) } - _vol, err := common.FloatFromString(k[1]) + _vol, err := convert.FloatFromString(k[1]) if err != nil { return nil, fmt.Errorf("cannot parse Kline.Volume. Err: %s", err) } - _id, err := common.FloatFromString(k[0]) + _id, err := convert.FloatFromString(k[0]) if err != nil { return nil, fmt.Errorf("cannot parse Kline.Id. Err: %s", err) } - _close, err := common.FloatFromString(k[2]) + _close, err := convert.FloatFromString(k[2]) if err != nil { return nil, fmt.Errorf("cannot parse Kline.Close. Err: %s", err) } - _high, err := common.FloatFromString(k[3]) + _high, err := convert.FloatFromString(k[3]) if err != nil { return nil, fmt.Errorf("cannot parse Kline.High. Err: %s", err) } - _low, err := common.FloatFromString(k[4]) + _low, err := convert.FloatFromString(k[4]) if err != nil { return nil, fmt.Errorf("cannot parse Kline.Low. Err: %s", err) } - _open, err := common.FloatFromString(k[5]) + _open, err := convert.FloatFromString(k[5]) if err != nil { return nil, fmt.Errorf("cannot parse Kline.Open. Err: %s", err) } @@ -500,25 +384,26 @@ func (g *Gateio) GetTradeHistory(symbol string) (TradHistoryResponse, error) { // GenerateSignature returns hash for authenticated requests func (g *Gateio) GenerateSignature(message string) []byte { - return common.GetHMAC(common.HashSHA512, []byte(message), []byte(g.APISecret)) + return crypto.GetHMAC(crypto.HashSHA512, []byte(message), + []byte(g.API.Credentials.Secret)) } // SendAuthenticatedHTTPRequest sends authenticated requests to the Gateio API // To use this you must setup an APIKey and APISecret from the exchange func (g *Gateio) SendAuthenticatedHTTPRequest(method, endpoint, param string, result interface{}) error { - if !g.AuthenticatedAPISupport { + if !g.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, g.Name) } headers := make(map[string]string) headers["Content-Type"] = "application/x-www-form-urlencoded" - headers["key"] = g.APIKey + headers["key"] = g.API.Credentials.Key hmac := g.GenerateSignature(param) - headers["sign"] = common.HexEncodeToString(hmac) + headers["sign"] = crypto.HexEncodeToString(hmac) - urlPath := fmt.Sprintf("%s/%s/%s", g.APIUrl, gateioAPIVersion, endpoint) + urlPath := fmt.Sprintf("%s/%s/%s", g.API.Endpoints.URL, gateioAPIVersion, endpoint) var intermidiary json.RawMessage @@ -542,7 +427,7 @@ func (g *Gateio) SendAuthenticatedHTTPRequest(method, endpoint, param string, re Message string `json:"message"` }{} - if err := common.JSONDecode(intermidiary, &errCap); err == nil { + if err := json.Unmarshal(intermidiary, &errCap); err == nil { if !errCap.Result { return fmt.Errorf("%s auth request error, code: %d message: %s", g.Name, @@ -551,7 +436,7 @@ func (g *Gateio) SendAuthenticatedHTTPRequest(method, endpoint, param string, re } } - return common.JSONDecode(intermidiary, result) + return json.Unmarshal(intermidiary, result) } // GetFee returns an estimate of fee based on type of transaction diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 8f2b418b..cfa6cf2f 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -1,7 +1,9 @@ package gateio import ( + "log" "net/http" + "os" "testing" "github.com/gorilla/websocket" @@ -9,6 +11,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -24,30 +27,35 @@ const ( var g Gateio var wsSetupRan bool -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { g.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") - gateioConfig, err := cfg.GetExchangeConfig("GateIO") + err := cfg.LoadConfig("../../testdata/configtest.json", true) if err != nil { - t.Error("Test Failed - GateIO Setup() init error") + log.Fatal("GateIO load config error", err) } - gateioConfig.AuthenticatedWebsocketAPISupport = true - gateioConfig.AuthenticatedAPISupport = true - gateioConfig.APIKey = apiKey - gateioConfig.APISecret = apiSecret + gConf, err := cfg.GetExchangeConfig("GateIO") + if err != nil { + log.Fatal("GateIO Setup() init error") + } + gConf.API.AuthenticatedSupport = true + gConf.API.AuthenticatedWebsocketSupport = true + gConf.API.Credentials.Key = apiKey + gConf.API.Credentials.Secret = apiSecret - g.Setup(&gateioConfig) + err = g.Setup(gConf) + if err != nil { + log.Fatal("GateIO setup error", err) + } + + os.Exit(m.Run()) } func TestGetSymbols(t *testing.T) { t.Parallel() _, err := g.GetSymbols() if err != nil { - t.Errorf("Test failed - Gateio TestGetSymbols: %s", err) + t.Errorf("Gateio TestGetSymbols: %s", err) } } @@ -55,7 +63,7 @@ func TestGetMarketInfo(t *testing.T) { t.Parallel() _, err := g.GetMarketInfo() if err != nil { - t.Errorf("Test failed - Gateio GetMarketInfo: %s", err) + t.Errorf("Gateio GetMarketInfo: %s", err) } } @@ -70,10 +78,10 @@ func TestSpotNewOrder(t *testing.T) { Symbol: "btc_usdt", Amount: 1.1, Price: 10.1, - Type: SpotNewOrderRequestParamsTypeSell, + Type: order.Sell.Lower(), }) if err != nil { - t.Errorf("Test failed - Gateio SpotNewOrder: %s", err) + t.Errorf("Gateio SpotNewOrder: %s", err) } } @@ -86,20 +94,20 @@ func TestCancelExistingOrder(t *testing.T) { _, err := g.CancelExistingOrder(917591554, "btc_usdt") if err != nil { - t.Errorf("Test failed - Gateio CancelExistingOrder: %s", err) + t.Errorf("Gateio CancelExistingOrder: %s", err) } } func TestGetBalances(t *testing.T) { t.Parallel() - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { t.Skip() } _, err := g.GetBalances() if err != nil { - t.Errorf("Test failed - Gateio GetBalances: %s", err) + t.Errorf("Gateio GetBalances: %s", err) } } @@ -107,7 +115,7 @@ func TestGetLatestSpotPrice(t *testing.T) { t.Parallel() _, err := g.GetLatestSpotPrice("btc_usdt") if err != nil { - t.Errorf("Test failed - Gateio GetLatestSpotPrice: %s", err) + t.Errorf("Gateio GetLatestSpotPrice: %s", err) } } @@ -115,7 +123,7 @@ func TestGetTicker(t *testing.T) { t.Parallel() _, err := g.GetTicker("btc_usdt") if err != nil { - t.Errorf("Test failed - Gateio GetTicker: %s", err) + t.Errorf("Gateio GetTicker: %s", err) } } @@ -123,7 +131,7 @@ func TestGetTickers(t *testing.T) { t.Parallel() _, err := g.GetTickers() if err != nil { - t.Errorf("Test failed - Gateio GetTicker: %s", err) + t.Errorf("Gateio GetTicker: %s", err) } } @@ -131,7 +139,7 @@ func TestGetOrderbook(t *testing.T) { t.Parallel() _, err := g.GetOrderbook("btc_usdt") if err != nil { - t.Errorf("Test failed - Gateio GetTicker: %s", err) + t.Errorf("Gateio GetTicker: %s", err) } } @@ -145,7 +153,7 @@ func TestGetSpotKline(t *testing.T) { }) if err != nil { - t.Errorf("Test failed - Gateio GetSpotKline: %s", err) + t.Errorf("Gateio GetSpotKline: %s", err) } } @@ -166,7 +174,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() g.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -178,15 +186,12 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - g.SetDefaults() - TestSetup(t) - var feeBuilder = setFeeBuilder() if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic if resp, err := g.GetFee(feeBuilder); resp != float64(0.002) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity @@ -194,7 +199,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := g.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) t.Error(err) } @@ -202,7 +207,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := g.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) t.Error(err) } @@ -210,7 +215,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } @@ -218,7 +223,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := g.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) t.Error(err) } @@ -227,7 +232,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -235,7 +240,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -243,7 +248,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -252,28 +257,22 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - g.SetDefaults() expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := g.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - g.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := g.GetActiveOrders(&getOrdersRequest) @@ -285,11 +284,8 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - g.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } currPair := currency.NewPair(currency.LTC, currency.BTC) @@ -307,27 +303,27 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if g.APIKey != "" && g.APIKey != "Key" && - g.APISecret != "" && g.APISecret != "Secret" { - return true - } - return false + return g.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - g.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip() } - var p = currency.Pair{ - Delimiter: "_", - Base: currency.LTC, - Quote: currency.BTC, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: "_", + Base: currency.LTC, + Quote: currency.BTC, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := g.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 10, "1234234") + response, err := g.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -336,16 +332,12 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - g.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip() } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -362,16 +354,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - g.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip() } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -387,8 +375,8 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } @@ -396,31 +384,34 @@ func TestGetAccountInfo(t *testing.T) { if apiSecret == "" || apiKey == "" { _, err := g.GetAccountInfo() if err == nil { - t.Error("Test Failed - GetAccountInfo() error") + t.Error("GetAccountInfo() Expected error") } } else { _, err := g.GetAccountInfo() if err != nil { - t.Error("Test Failed - GetAccountInfo() error", err) + t.Error("GetAccountInfo() error", err) } } } func TestModifyOrder(t *testing.T) { - _, err := g.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := g.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - g.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.LTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -437,15 +428,11 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - g.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := g.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -453,15 +440,11 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - g.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := g.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -482,9 +465,6 @@ func TestGetDepositAddress(t *testing.T) { } } func TestGetOrderInfo(t *testing.T) { - g.SetDefaults() - TestSetup(t) - if !areTestAPIKeysSet() { t.Skip("no API keys set skipping test") } @@ -499,9 +479,7 @@ func TestGetOrderInfo(t *testing.T) { // TestWsGetBalance dials websocket, sends balance request. func TestWsGetBalance(t *testing.T) { - g.SetDefaults() - TestSetup(t) - if !g.Websocket.IsEnabled() && !g.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + if !g.Websocket.IsEnabled() && !g.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } g.WebsocketConn = &wshandler.WebsocketConnection{ @@ -531,13 +509,15 @@ func TestWsGetBalance(t *testing.T) { if err != nil { t.Error(err) } + _, err = g.wsGetBalance([]string{}) + if err != nil { + t.Error(err) + } } // TestWsGetOrderInfo dials websocket, sends order info request. func TestWsGetOrderInfo(t *testing.T) { - g.SetDefaults() - TestSetup(t) - if !g.Websocket.IsEnabled() && !g.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + if !g.Websocket.IsEnabled() && !g.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } g.WebsocketConn = &wshandler.WebsocketConnection{ @@ -563,7 +543,7 @@ func TestWsGetOrderInfo(t *testing.T) { if resp.Result.Status != "success" { t.Fatal("Unsuccessful login") } - _, err = g.wsGetOrderInfo("EOS_USDT", 0, 10) + _, err = g.wsGetOrderInfo("EOS_USDT", 0, 1000) if err != nil { t.Error(err) } @@ -573,9 +553,7 @@ func setupWSTestAuth(t *testing.T) { if wsSetupRan { return } - g.SetDefaults() - TestSetup(t) - if !g.Websocket.IsEnabled() && !g.AuthenticatedWebsocketAPISupport { + if !g.Websocket.IsEnabled() && !g.API.AuthenticatedWebsocketSupport { t.Skip(wshandler.WebsocketNotEnabled) } g.WebsocketConn = &wshandler.WebsocketConnection{ diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index d6a2c4ea..283cfbf4 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -7,17 +7,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" ) -// SpotNewOrderRequestParamsType order type (buy or sell) -type SpotNewOrderRequestParamsType string - -var ( - // SpotNewOrderRequestParamsTypeBuy buy order - SpotNewOrderRequestParamsTypeBuy = SpotNewOrderRequestParamsType("buy") - - // SpotNewOrderRequestParamsTypeSell sell order - SpotNewOrderRequestParamsTypeSell = SpotNewOrderRequestParamsType("sell") -) - // TimeInterval Interval represents interval enum. type TimeInterval int @@ -81,15 +70,15 @@ type KLineResponse struct { // TickerResponse holds the ticker response data type TickerResponse struct { - Result string `json:"result"` - Volume float64 `json:"baseVolume,string"` // Trading volume - High float64 `json:"high24hr,string"` // 24 hour high price - Open float64 `json:"highestBid,string"` // Openening price - Last float64 `json:"last,string"` // Last price - Low float64 `json:"low24hr,string"` // 24 hour low price - Close float64 `json:"lowestAsk,string"` // Closing price - PercentChange float64 `json:"percentChange,string"` // Percentage change - QuoteVolume float64 `json:"quoteVolume,string"` // Quote currency volume + Period int64 `json:"period"` + BaseVolume float64 `json:"baseVolume,string"` + Change float64 `json:"change,string"` + Close float64 `json:"close,string"` + High float64 `json:"high,string"` + Last float64 `json:"last,string"` + Low float64 `json:"low,string"` + Open float64 `json:"open,string"` + QuoteVolume float64 `json:"quoteVolume,string"` } // OrderbookResponse stores the orderbook data @@ -116,10 +105,10 @@ type Orderbook struct { // SpotNewOrderRequestParams Order params type SpotNewOrderRequestParams struct { - Amount float64 `json:"amount"` // Order quantity - Price float64 `json:"price"` // Order price - Symbol string `json:"symbol"` // Trading pair; btc_usdt, eth_btc...... - Type SpotNewOrderRequestParamsType `json:"type"` // Order type (buy or sell), + Amount float64 `json:"amount"` // Order quantity + Price float64 `json:"price"` // Order price + Symbol string `json:"symbol"` // Trading pair; btc_usdt, eth_btc...... + Type string `json:"type"` // Order type (buy or sell), } // SpotNewOrderResponse Order response @@ -456,24 +445,24 @@ type WebSocketOrderQueryResult struct { // WebSocketOrderQueryRecords contains order information from a order.query websocket request type WebSocketOrderQueryRecords struct { - ID int `json:"id"` + ID int64 `json:"id"` Market string `json:"market"` - User int `json:"user"` + User int64 `json:"user"` Ctime float64 `json:"ctime"` Mtime float64 `json:"mtime"` - Price string `json:"price"` - Amount string `json:"amount"` - Left string `json:"left"` - DealFee string `json:"dealFee"` - OrderType int `json:"orderType"` - Type int `json:"type"` - FilledAmount string `json:"filledAmount"` - FilledTotal string `json:"filledTotal"` + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + Left float64 `json:"left,string"` + DealFee float64 `json:"dealFee,string"` + OrderType int64 `json:"orderType"` + Type int64 `json:"type"` + FilledAmount float64 `json:"filledAmount,string"` + FilledTotal float64 `json:"filledTotal,string"` } // WebsocketAuthenticationResponse contains the result of a login request type WebsocketAuthenticationResponse struct { - Error string `json:"error"` + Error string `json:"error,omitempty"` Result struct { Status string `json:"status"` } `json:"result"` @@ -484,14 +473,14 @@ type WebsocketAuthenticationResponse struct { type wsGetBalanceRequest struct { ID int64 `json:"id"` Method string `json:"method"` - Params []string `json:"params,omitempty"` + Params []string `json:"params"` } // WsGetBalanceResponse stores WS GetBalance response type WsGetBalanceResponse struct { - Error interface{} `json:"error"` - Result map[currency.Code]WsGetBalanceResponseData `json:"result,omitempty"` - ID int64 `json:"id"` + Error interface{} `json:"error"` + Result map[string]WsGetBalanceResponseData `json:"result"` + ID int64 `json:"id"` } // WsGetBalanceResponseData contains currency data diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index 753441df..8959f1f1 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -1,6 +1,7 @@ package gateio import ( + "encoding/json" "errors" "fmt" "net/http" @@ -9,9 +10,10 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" @@ -37,7 +39,8 @@ func (g *Gateio) WsConnect() error { go g.WsHandleData() _, err = g.wsServerSignIn() if err != nil { - log.Errorf("%v - authentication failed: %v", g.Name, err) + log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", g.Name, err) + g.Websocket.SetCanUseAuthenticatedEndpoints(false) } g.GenerateAuthenticatedSubscriptions() g.GenerateDefaultSubscriptions() @@ -50,11 +53,11 @@ func (g *Gateio) wsServerSignIn() (*WebsocketAuthenticationResponse, error) { } nonce := int(time.Now().Unix() * 1000) sigTemp := g.GenerateSignature(strconv.Itoa(nonce)) - signature := common.Base64Encode(sigTemp) + signature := crypto.Base64Encode(sigTemp) signinWsRequest := WebsocketRequest{ ID: g.WebsocketConn.GenerateMessageID(true), Method: "server.sign", - Params: []interface{}{g.APIKey, signature, nonce}, + Params: []interface{}{g.API.Credentials.Key, signature, nonce}, } resp, err := g.WebsocketConn.SendMessageReturnResponse(signinWsRequest.ID, signinWsRequest) if err != nil { @@ -62,7 +65,7 @@ func (g *Gateio) wsServerSignIn() (*WebsocketAuthenticationResponse, error) { return nil, err } var response WebsocketAuthenticationResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { g.Websocket.SetCanUseAuthenticatedEndpoints(false) return nil, err @@ -90,12 +93,12 @@ func (g *Gateio) WsHandleData() { default: resp, err := g.WebsocketConn.ReadMessage() if err != nil { - g.Websocket.DataHandler <- err + g.Websocket.ReadMessageErrors <- err return } g.Websocket.TrafficAlert <- struct{}{} var result WebsocketResponse - err = common.JSONDecode(resp.Raw, &result) + err = json.Unmarshal(resp.Raw, &result) if err != nil { g.Websocket.DataHandler <- err continue @@ -107,7 +110,7 @@ func (g *Gateio) WsHandleData() { } if result.Error.Code != 0 { - if common.StringContains(result.Error.Message, "authentication") { + if strings.Contains(result.Error.Message, "authentication") { g.Websocket.DataHandler <- fmt.Errorf("%v - authentication failed: %v", g.Name, err) g.Websocket.SetCanUseAuthenticatedEndpoints(false) continue @@ -118,43 +121,44 @@ func (g *Gateio) WsHandleData() { } switch { - case common.StringContains(result.Method, "ticker"): + case strings.Contains(result.Method, "ticker"): var ticker WebsocketTicker var c string - err = common.JSONDecode(result.Params[1], &ticker) + err = json.Unmarshal(result.Params[1], &ticker) if err != nil { g.Websocket.DataHandler <- err continue } - err = common.JSONDecode(result.Params[0], &c) + err = json.Unmarshal(result.Params[0], &c) if err != nil { g.Websocket.DataHandler <- err continue } g.Websocket.DataHandler <- wshandler.TickerData{ - Timestamp: time.Now(), - Pair: currency.NewPairFromString(c), - AssetType: orderbook.Spot, - Exchange: g.GetName(), - ClosePrice: ticker.Close, - Quantity: ticker.BaseVolume, - OpenPrice: ticker.Open, - HighPrice: ticker.High, - LowPrice: ticker.Low, + Exchange: g.Name, + Open: ticker.Open, + Close: ticker.Close, + Volume: ticker.BaseVolume, + QuoteVolume: ticker.QuoteVolume, + High: ticker.High, + Low: ticker.Low, + Last: ticker.Last, + AssetType: asset.Spot, + Pair: currency.NewPairFromString(c), } - case common.StringContains(result.Method, "trades"): + case strings.Contains(result.Method, "trades"): var trades []WebsocketTrade var c string - err = common.JSONDecode(result.Params[1], &trades) + err = json.Unmarshal(result.Params[1], &trades) if err != nil { g.Websocket.DataHandler <- err continue } - err = common.JSONDecode(result.Params[0], &c) + err = json.Unmarshal(result.Params[0], &c) if err != nil { g.Websocket.DataHandler <- err continue @@ -164,31 +168,31 @@ func (g *Gateio) WsHandleData() { g.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Now(), CurrencyPair: currency.NewPairFromString(c), - AssetType: orderbook.Spot, - Exchange: g.GetName(), + AssetType: asset.Spot, + Exchange: g.Name, Price: trades[i].Price, Amount: trades[i].Amount, Side: trades[i].Type, } } - case common.StringContains(result.Method, "depth"): + case strings.Contains(result.Method, "depth"): var IsSnapshot bool var c string var data = make(map[string][][]string) - err = common.JSONDecode(result.Params[0], &IsSnapshot) + err = json.Unmarshal(result.Params[0], &IsSnapshot) if err != nil { g.Websocket.DataHandler <- err continue } - err = common.JSONDecode(result.Params[2], &c) + err = json.Unmarshal(result.Params[2], &c) if err != nil { g.Websocket.DataHandler <- err continue } - err = common.JSONDecode(result.Params[1], &data) + err = json.Unmarshal(result.Params[1], &data) if err != nil { g.Websocket.DataHandler <- err continue @@ -232,22 +236,22 @@ func (g *Gateio) WsHandleData() { var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids - newOrderBook.AssetType = orderbook.Spot + newOrderBook.AssetType = asset.Spot newOrderBook.Pair = currency.NewPairFromString(c) + newOrderBook.ExchangeName = g.Name - err = g.Websocket.Orderbook.LoadSnapshot(&newOrderBook, - false) + err = g.Websocket.Orderbook.LoadSnapshot(&newOrderBook) if err != nil { g.Websocket.DataHandler <- err } } else { err = g.Websocket.Orderbook.Update( &wsorderbook.WebsocketOrderbookUpdate{ - Asks: asks, - Bids: bids, - CurrencyPair: currency.NewPairFromString(c), - UpdateTime: time.Now(), - AssetType: orderbook.Spot, + Asks: asks, + Bids: bids, + Pair: currency.NewPairFromString(c), + UpdateTime: time.Now(), + Asset: asset.Spot, }) if err != nil { g.Websocket.DataHandler <- err @@ -256,13 +260,13 @@ func (g *Gateio) WsHandleData() { g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: currency.NewPairFromString(c), - Asset: orderbook.Spot, - Exchange: g.GetName(), + Asset: asset.Spot, + Exchange: g.Name, } - case common.StringContains(result.Method, "kline"): + case strings.Contains(result.Method, "kline"): var data []interface{} - err = common.JSONDecode(result.Params[0], &data) + err = json.Unmarshal(result.Params[0], &data) if err != nil { g.Websocket.DataHandler <- err continue @@ -277,8 +281,8 @@ func (g *Gateio) WsHandleData() { g.Websocket.DataHandler <- wshandler.KlineData{ Timestamp: time.Now(), Pair: currency.NewPairFromString(data[7].(string)), - AssetType: orderbook.Spot, - Exchange: g.GetName(), + AssetType: asset.Spot, + Exchange: g.Name, OpenPrice: open, ClosePrice: closePrice, HighPrice: high, @@ -297,7 +301,7 @@ func (g *Gateio) GenerateAuthenticatedSubscriptions() { } var channels = []string{"balance.subscribe", "order.subscribe"} var subscriptions []wshandler.WebsocketChannelSubscription - enabledCurrencies := g.GetEnabledCurrencies() + enabledCurrencies := g.GetEnabledPairs(asset.Spot) for i := range channels { for j := range enabledCurrencies { subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ @@ -313,7 +317,7 @@ func (g *Gateio) GenerateAuthenticatedSubscriptions() { func (g *Gateio) GenerateDefaultSubscriptions() { var channels = []string{"ticker.subscribe", "trades.subscribe", "depth.subscribe", "kline.subscribe"} var subscriptions []wshandler.WebsocketChannelSubscription - enabledCurrencies := g.GetEnabledCurrencies() + enabledCurrencies := g.GetEnabledPairs(asset.Spot) for i := range channels { for j := range enabledCurrencies { params := make(map[string]interface{}) @@ -335,7 +339,9 @@ func (g *Gateio) GenerateDefaultSubscriptions() { // Subscribe sends a websocket message to receive data from the channel func (g *Gateio) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { - params := []interface{}{channelToSubscribe.Currency.String()} + params := []interface{}{g.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).Upper()} + for i := range channelToSubscribe.Params { params = append(params, channelToSubscribe.Params[i]) } @@ -351,7 +357,7 @@ func (g *Gateio) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscrip return err } var response WebsocketAuthenticationResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return err } @@ -367,14 +373,15 @@ func (g *Gateio) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscr subscribe := WebsocketRequest{ ID: g.WebsocketConn.GenerateMessageID(true), Method: unsbuscribeText, - Params: []interface{}{channelToSubscribe.Currency.String(), 1800}, + Params: []interface{}{g.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).Upper(), 1800}, } resp, err := g.WebsocketConn.SendMessageReturnResponse(subscribe.ID, subscribe) if err != nil { return err } var response WebsocketAuthenticationResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return err } @@ -398,7 +405,7 @@ func (g *Gateio) wsGetBalance(currencies []string) (*WsGetBalanceResponse, error return nil, err } var balance WsGetBalanceResponse - err = common.JSONDecode(resp, &balance) + err = json.Unmarshal(resp, &balance) if err != nil { return &balance, err } @@ -410,7 +417,7 @@ func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) (*WebSocketOrd if !g.Websocket.CanUseAuthenticatedEndpoints() { return nil, fmt.Errorf("%v not authorised to get order info", g.Name) } - order := WebsocketRequest{ + ord := WebsocketRequest{ ID: g.WebsocketConn.GenerateMessageID(true), Method: "order.query", Params: []interface{}{ @@ -419,12 +426,12 @@ func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) (*WebSocketOrd limit, }, } - resp, err := g.WebsocketConn.SendMessageReturnResponse(order.ID, order) + resp, err := g.WebsocketConn.SendMessageReturnResponse(ord.ID, ord) if err != nil { return nil, err } var orderQuery WebSocketOrderQueryResult - err = common.JSONDecode(resp, &orderQuery) + err = json.Unmarshal(resp, &orderQuery) if err != nil { return &orderQuery, err } diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 9cbfbaef..5d0d8384 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -9,14 +9,174 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (g *Gateio) GetDefaultConfig() (*config.ExchangeConfig, error) { + g.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = g.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = g.BaseCurrencies + + err := g.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if g.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = g.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets default values for the exchange +func (g *Gateio) SetDefaults() { + g.Name = "GateIO" + g.Enabled = true + g.Verbose = true + g.API.CredentialsValidator.RequiresKey = true + g.API.CredentialsValidator.RequiresSecret = true + + g.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "_", + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "_", + Uppercase: true, + }, + } + + g.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + OrderbookFetching: true, + TradeFetching: true, + KlineFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + MessageCorrelation: true, + GetOrder: true, + AccountBalance: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + g.Requester = request.New(g.Name, + request.NewRateLimit(time.Second*10, gateioAuthRate), + request.NewRateLimit(time.Second*10, gateioUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + g.API.Endpoints.URLDefault = gateioTradeURL + g.API.Endpoints.URL = g.API.Endpoints.URLDefault + g.API.Endpoints.URLSecondaryDefault = gateioMarketURL + g.API.Endpoints.URLSecondary = g.API.Endpoints.URLSecondaryDefault + g.API.Endpoints.WebsocketURL = gateioWebsocketEndpoint + g.Websocket = wshandler.New() + g.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + g.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + g.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup sets user configuration +func (g *Gateio) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + g.SetEnabled(false) + return nil + } + + err := g.SetupDefaults(exch) + if err != nil { + return err + } + + err = g.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: gateioWebsocketEndpoint, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: g.WsConnect, + Subscriber: g.Subscribe, + UnSubscriber: g.Unsubscribe, + Features: &g.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + g.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: g.Name, + URL: g.Websocket.GetWebsocketURL(), + ProxyURL: g.Websocket.GetProxyAddress(), + Verbose: g.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + RateLimit: gateioWebsocketRateLimit, + } + + g.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + true, + false, + false, + false, + exch.Name) + return nil +} + // Start starts the GateIO go routine func (g *Gateio) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -29,95 +189,112 @@ func (g *Gateio) Start(wg *sync.WaitGroup) { // Run implements the GateIO wrapper func (g *Gateio) Run() { if g.Verbose { - log.Debugf("%s Websocket: %s. (url: %s).\n", g.GetName(), common.IsEnabled(g.Websocket.IsEnabled()), g.WebsocketURL) - log.Debugf("%s polling delay: %ds.\n", g.GetName(), g.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", g.GetName(), len(g.EnabledPairs), g.EnabledPairs) + g.PrintEnabledPairs() } - symbols, err := g.GetSymbols() - if err != nil { - log.Errorf("%s Unable to fetch symbols.\n", g.GetName()) - } else { - var newCurrencies currency.Pairs - for _, p := range symbols { - newCurrencies = append(newCurrencies, - currency.NewPairFromString(p)) - } + if !g.GetEnabledFeatures().AutoPairUpdates { + return + } - err = g.UpdateCurrencies(newCurrencies, false, false) - if err != nil { - log.Errorf("%s Failed to update available currencies.\n", g.GetName()) - } + err := g.UpdateTradablePairs(false) + if err != nil { + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", g.Name, err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (g *Gateio) FetchTradablePairs(asset asset.Item) ([]string, error) { + return g.GetSymbols() +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (g *Gateio) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := g.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return g.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (g *Gateio) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (g *Gateio) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price result, err := g.GetTickers() if err != nil { return tickerPrice, err } - - for _, x := range g.GetEnabledCurrencies() { - currency := exchange.FormatExchangeCurrency(g.Name, x).String() - var tp ticker.Price - tp.Pair = x - tp.High = result[currency].High - tp.Last = result[currency].Last - tp.Last = result[currency].Last - tp.Low = result[currency].Low - tp.Volume = result[currency].Volume - - err = ticker.ProcessTicker(g.Name, &tp, assetType) - if err != nil { - return tickerPrice, err + pairs := g.GetEnabledPairs(assetType) + for i := range pairs { + for k := range result { + if !strings.EqualFold(k, pairs[i].String()) { + continue + } + tickerPrice = ticker.Price{ + Last: result[k].Last, + High: result[k].High, + Low: result[k].Low, + Volume: result[k].BaseVolume, + QuoteVolume: result[k].QuoteVolume, + Open: result[k].Open, + Close: result[k].Close, + Pair: pairs[i], + } + err = ticker.ProcessTicker(g.Name, &tickerPrice, assetType) + if err != nil { + log.Error(log.Ticker, err) + } } } return ticker.GetTicker(g.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (g *Gateio) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(g.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (g *Gateio) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(g.Name, p, assetType) if err != nil { return g.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (g *Gateio) GetOrderbookEx(currency currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(g.GetName(), currency, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (g *Gateio) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(g.Name, p, assetType) if err != nil { - return g.UpdateOrderbook(currency, assetType) + return g.UpdateOrderbook(p, assetType) } return ob, nil } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (g *Gateio) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (g *Gateio) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - currency := exchange.FormatExchangeCurrency(g.Name, p).String() + curr := g.FormatExchangeCurrency(p, assetType).String() - orderbookNew, err := g.GetOrderbook(currency) + orderbookNew, err := g.GetOrderbook(curr) if err != nil { return orderBook, err } for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data.Amount, Price: data.Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[x].Amount, + Price: orderbookNew.Bids[x].Price, + }) } for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data.Amount, Price: data.Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[x].Amount, + Price: orderbookNew.Asks[x].Price, + }) } orderBook.Pair = p - orderBook.ExchangeName = g.GetName() + orderBook.ExchangeName = g.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -132,63 +309,80 @@ func (g *Gateio) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.B // ZB exchange func (g *Gateio) GetAccountInfo() (exchange.AccountInfo, error) { var info exchange.AccountInfo - - balance, err := g.GetBalances() - if err != nil { - return info, err - } - var balances []exchange.AccountCurrencyInfo - switch l := balance.Locked.(type) { - case map[string]interface{}: - for x := range l { - lockedF, err := strconv.ParseFloat(l[x].(string), 64) - if err != nil { - return info, err - } - - balances = append(balances, exchange.AccountCurrencyInfo{ - CurrencyName: currency.NewCode(x), - Hold: lockedF, + if g.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + resp, err := g.wsGetBalance([]string{}) + if err != nil { + return info, err + } + var currData []exchange.AccountCurrencyInfo + for k := range resp.Result { + currData = append(currData, exchange.AccountCurrencyInfo{ + CurrencyName: currency.NewCode(k), + TotalValue: resp.Result[k].Available + resp.Result[k].Freeze, + Hold: resp.Result[k].Freeze, }) } - default: - break - } + info.Accounts = append(info.Accounts, exchange.Account{ + Currencies: currData, + }) + } else { + balance, err := g.GetBalances() + if err != nil { + return info, err + } - switch v := balance.Available.(type) { - case map[string]interface{}: - for x := range v { - availAmount, err := strconv.ParseFloat(v[x].(string), 64) - if err != nil { - return info, err - } - - var updated bool - for i := range balances { - if balances[i].CurrencyName == currency.NewCode(x) { - balances[i].TotalValue = balances[i].Hold + availAmount - updated = true - break + switch l := balance.Locked.(type) { + case map[string]interface{}: + for x := range l { + lockedF, err := strconv.ParseFloat(l[x].(string), 64) + if err != nil { + return info, err } - } - if !updated { + balances = append(balances, exchange.AccountCurrencyInfo{ CurrencyName: currency.NewCode(x), - TotalValue: availAmount, + Hold: lockedF, }) } + default: + break } - default: - break + + switch v := balance.Available.(type) { + case map[string]interface{}: + for x := range v { + availAmount, err := strconv.ParseFloat(v[x].(string), 64) + if err != nil { + return info, err + } + + var updated bool + for i := range balances { + if balances[i].CurrencyName == currency.NewCode(x) { + balances[i].TotalValue = balances[i].Hold + availAmount + updated = true + break + } + } + if !updated { + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: currency.NewCode(x), + TotalValue: availAmount, + }) + } + } + default: + break + } + + info.Accounts = append(info.Accounts, exchange.Account{ + Currencies: balances, + }) } - info.Accounts = append(info.Accounts, exchange.Account{ - Currencies: balances, - }) - - info.Exchange = g.GetName() + info.Exchange = g.Name return info, nil } @@ -196,71 +390,72 @@ func (g *Gateio) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (g *Gateio) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (g *Gateio) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (g *Gateio) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order // TODO: support multiple order types (IOC) -func (g *Gateio) SubmitOrder(p currency.Pair, side exchange.OrderSide, _ exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - var orderTypeFormat SpotNewOrderRequestParamsType +func (g *Gateio) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } - if side == exchange.BuyOrderSide { - orderTypeFormat = SpotNewOrderRequestParamsTypeBuy + var orderTypeFormat string + if s.OrderSide == order.Buy { + orderTypeFormat = order.Buy.Lower() } else { - orderTypeFormat = SpotNewOrderRequestParamsTypeSell + orderTypeFormat = order.Sell.Lower() } var spotNewOrderRequestParams = SpotNewOrderRequestParams{ - Amount: amount, - Price: price, - Symbol: p.String(), + Amount: s.Amount, + Price: s.Price, + Symbol: s.Pair.String(), Type: orderTypeFormat, } response, err := g.SpotNewOrder(spotNewOrderRequestParams) - + if err != nil { + return submitOrderResponse, err + } if response.OrderNumber > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response.OrderNumber) + submitOrderResponse.OrderID = strconv.FormatInt(response.OrderNumber, 10) } - - if err == nil { - submitOrderResponse.IsOrderPlaced = true + if response.LeftAmount == 0 { + submitOrderResponse.FullyMatched = true } + submitOrderResponse.IsOrderPlaced = true - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (g *Gateio) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (g *Gateio) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (g *Gateio) CancelOrder(order *exchange.OrderCancellation) error { +func (g *Gateio) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) - if err != nil { return err } - _, err = g.CancelExistingOrder(orderIDInt, exchange.FormatExchangeCurrency(g.Name, order.CurrencyPair).String()) - + _, err = g.CancelExistingOrder(orderIDInt, + g.FormatExchangeCurrency(order.CurrencyPair, order.AssetType).String()) return err } // CancelAllOrders cancels all orders associated with a currency pair -func (g *Gateio) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (g *Gateio) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } openOrders, err := g.GetOpenOrders("") if err != nil { @@ -275,7 +470,7 @@ func (g *Gateio) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cancel for unique := range uniqueSymbols { err = g.CancelAllExistingOrders(-1, unique) if err != nil { - cancelAllOrdersResponse.OrderStatus[unique] = err.Error() + cancelAllOrdersResponse.Status[unique] = err.Error() } } @@ -283,9 +478,8 @@ func (g *Gateio) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cancel } // GetOrderInfo returns information on a current open order -func (g *Gateio) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail - +func (g *Gateio) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail orders, err := g.GetOpenOrders("") if err != nil { return orderDetail, errors.New("failed to get open orders") @@ -294,19 +488,20 @@ func (g *Gateio) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { if orders.Orders[x].OrderNumber != orderID { continue } - orderDetail.Exchange = g.GetName() + orderDetail.Exchange = g.Name orderDetail.ID = orders.Orders[x].OrderNumber orderDetail.RemainingAmount = orders.Orders[x].InitialAmount - orders.Orders[x].FilledAmount orderDetail.ExecutedAmount = orders.Orders[x].FilledAmount orderDetail.Amount = orders.Orders[x].InitialAmount orderDetail.OrderDate = time.Unix(orders.Orders[x].Timestamp, 0) - orderDetail.Status = orders.Orders[x].Status + orderDetail.Status = order.Status(orders.Orders[x].Status) orderDetail.Price = orders.Orders[x].Rate - orderDetail.CurrencyPair = currency.NewPairDelimiter(orders.Orders[x].CurrencyPair, g.ConfigCurrencyPairFormat.Delimiter) - if strings.EqualFold(orders.Orders[x].Type, exchange.AskOrderSide.ToString()) { - orderDetail.OrderSide = exchange.AskOrderSide - } else if strings.EqualFold(orders.Orders[x].Type, exchange.BidOrderSide.ToString()) { - orderDetail.OrderSide = exchange.BuyOrderSide + orderDetail.CurrencyPair = currency.NewPairDelimiter(orders.Orders[x].CurrencyPair, + g.GetPairFormat(asset.Spot, false).Delimiter) + if strings.EqualFold(orders.Orders[x].Type, order.Ask.String()) { + orderDetail.OrderSide = order.Ask + } else if strings.EqualFold(orders.Orders[x].Type, order.Bid.String()) { + orderDetail.OrderSide = order.Buy } return orderDetail, nil } @@ -320,38 +515,28 @@ func (g *Gateio) GetDepositAddress(cryptocurrency currency.Code, _ string) (stri return "", err } - // Waits for new generated address if not created yet, its variable per - // currency if addr == gateioGenerateAddress { - time.Sleep(10 * time.Second) - addr, err = g.GetCryptoDepositAddress(cryptocurrency.String()) - if err != nil { - return "", err - } - if addr == gateioGenerateAddress { - return "", errors.New("new deposit address is being generated, please retry again shortly") - } - return addr, nil + return "", + errors.New("new deposit address is being generated, please retry again shortly") } - - return addr, err + return addr, nil } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (g *Gateio) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (g *Gateio) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { return g.WithdrawCrypto(withdrawRequest.Currency.String(), withdrawRequest.Address, withdrawRequest.Amount) } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (g *Gateio) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (g *Gateio) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (g *Gateio) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (g *Gateio) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -362,7 +547,7 @@ func (g *Gateio) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (g *Gateio) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (g.APIKey == "" || g.APISecret == "") && // Todo check connection status + if !g.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -370,66 +555,104 @@ func (g *Gateio) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) } // GetActiveOrders retrieves any orders that are active/open -func (g *Gateio) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (g *Gateio) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { + var orders []order.Detail var currPair string - if len(getOrdersRequest.Currencies) == 1 { - currPair = getOrdersRequest.Currencies[0].String() + if len(req.Currencies) == 1 { + currPair = req.Currencies[0].String() } + if g.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + for i := 0; ; i += 100 { + resp, err := g.wsGetOrderInfo(req.OrderType.String(), i, 100) + if err != nil { + return orders, err + } - resp, err := g.GetOpenOrders(currPair) - if err != nil { - return nil, err - } - - var orders []exchange.OrderDetail - for i := range resp.Orders { - if resp.Orders[i].Status != "open" { - continue + for j := range resp.WebSocketOrderQueryRecords { + orderSide := order.Buy + if resp.WebSocketOrderQueryRecords[j].Type == 1 { + orderSide = order.Sell + } + orderType := order.Market + if resp.WebSocketOrderQueryRecords[j].OrderType == 1 { + orderType = order.Limit + } + firstNum, decNum, err := convert.SplitFloatDecimals(resp.WebSocketOrderQueryRecords[j].Ctime) + if err != nil { + return orders, err + } + orderDate := time.Unix(firstNum, decNum) + orders = append(orders, order.Detail{ + Exchange: g.Name, + AccountID: strconv.FormatInt(resp.WebSocketOrderQueryRecords[j].User, 10), + ID: strconv.FormatInt(resp.WebSocketOrderQueryRecords[j].ID, 10), + CurrencyPair: currency.NewPairFromString(resp.WebSocketOrderQueryRecords[j].Market), + OrderSide: orderSide, + OrderType: orderType, + OrderDate: orderDate, + Price: resp.WebSocketOrderQueryRecords[j].Price, + Amount: resp.WebSocketOrderQueryRecords[j].Amount, + ExecutedAmount: resp.WebSocketOrderQueryRecords[j].FilledAmount, + RemainingAmount: resp.WebSocketOrderQueryRecords[j].Left, + Fee: resp.WebSocketOrderQueryRecords[j].DealFee, + }) + } + if len(resp.WebSocketOrderQueryRecords) < 100 { + break + } + } + } else { + resp, err := g.GetOpenOrders(currPair) + if err != nil { + return nil, err } - symbol := currency.NewPairDelimiter(resp.Orders[i].CurrencyPair, - g.ConfigCurrencyPairFormat.Delimiter) - side := exchange.OrderSide(strings.ToUpper(resp.Orders[i].Type)) - orderDate := time.Unix(resp.Orders[i].Timestamp, 0) + for i := range resp.Orders { + if resp.Orders[i].Status != "open" { + continue + } - orders = append(orders, exchange.OrderDetail{ - ID: resp.Orders[i].OrderNumber, - Amount: resp.Orders[i].Amount, - Price: resp.Orders[i].Rate, - RemainingAmount: resp.Orders[i].FilledAmount, - OrderDate: orderDate, - OrderSide: side, - Exchange: g.Name, - CurrencyPair: symbol, - Status: resp.Orders[i].Status, - }) + symbol := currency.NewPairDelimiter(resp.Orders[i].CurrencyPair, + g.GetPairFormat(asset.Spot, false).Delimiter) + side := order.Side(strings.ToUpper(resp.Orders[i].Type)) + orderDate := time.Unix(resp.Orders[i].Timestamp, 0) + orders = append(orders, order.Detail{ + ID: resp.Orders[i].OrderNumber, + Amount: resp.Orders[i].Amount, + Price: resp.Orders[i].Rate, + RemainingAmount: resp.Orders[i].FilledAmount, + OrderDate: orderDate, + OrderSide: side, + Exchange: g.Name, + CurrencyPair: symbol, + Status: order.Status(resp.Orders[i].Status), + }) + } } - - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (g *Gateio) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (g *Gateio) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { var trades []TradesResponse - for _, currency := range getOrdersRequest.Currencies { - resp, err := g.GetTradeHistory(currency.String()) + for i := range req.Currencies { + resp, err := g.GetTradeHistory(req.Currencies[i].String()) if err != nil { return nil, err } trades = append(trades, resp.Trades...) } - var orders []exchange.OrderDetail + var orders []order.Detail for _, trade := range trades { symbol := currency.NewPairDelimiter(trade.Pair, - g.ConfigCurrencyPairFormat.Delimiter) - side := exchange.OrderSide(strings.ToUpper(trade.Type)) + g.GetPairFormat(asset.Spot, false).Delimiter) + side := order.Side(strings.ToUpper(trade.Type)) orderDate := time.Unix(trade.TimeUnix, 0) - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ ID: strconv.FormatInt(trade.OrderID, 10), Amount: trade.Amount, Price: trade.Rate, @@ -440,10 +663,8 @@ func (g *Gateio) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } diff --git a/exchanges/gemini/README.md b/exchanges/gemini/README.md index e5f73771..7e528485 100644 --- a/exchanges/gemini/README.md +++ b/exchanges/gemini/README.md @@ -47,22 +47,22 @@ main.go ```go var g exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Gemini" { - g = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Gemini" { + g = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := g.GetTickerPrice() +tick, err := g.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := g.GetOrderbookEx() +ob, err := g.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +130,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/gemini/gemini.go b/exchanges/gemini/gemini.go index 516d71a2..cc5d96a6 100644 --- a/exchanges/gemini/gemini.go +++ b/exchanges/gemini/gemini.go @@ -1,18 +1,17 @@ package gemini import ( + "encoding/json" "errors" "fmt" "net/http" "net/url" "strconv" - "time" + "strings" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -61,162 +60,35 @@ const ( // AddSession, if sandbox test is needed append a new session with with the same // API keys and change the IsSandbox variable to true. type Gemini struct { + WebsocketConn *wshandler.WebsocketConnection AuthenticatedWebsocketConn *wshandler.WebsocketConnection exchange.Base Role string RequiresHeartBeat bool } -// SetDefaults sets package defaults for gemini exchange -func (g *Gemini) SetDefaults() { - g.Name = "Gemini" - g.Enabled = false - g.Verbose = false - g.RESTPollingDelay = 10 - g.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithAPIPermission | - exchange.AutoWithdrawCryptoWithSetup | - exchange.WithdrawFiatViaWebsiteOnly - g.RequestCurrencyPairFormat.Delimiter = "" - g.RequestCurrencyPairFormat.Uppercase = true - g.ConfigCurrencyPairFormat.Delimiter = "" - g.ConfigCurrencyPairFormat.Uppercase = true - g.AssetTypes = []string{ticker.Spot} - g.SupportsAutoPairUpdating = true - g.SupportsRESTTickerBatching = false - g.Requester = request.New(g.Name, - request.NewRateLimit(time.Second, geminiAuthRate), - request.NewRateLimit(time.Second, geminiUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - g.APIUrlDefault = geminiAPIURL - g.APIUrl = g.APIUrlDefault - g.Websocket = wshandler.New() - g.Websocket.Functionality = wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported | - wshandler.WebsocketSequenceNumberSupported - g.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - g.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - g.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup sets exchange configuration parameters -func (g *Gemini) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - g.SetEnabled(false) - } else { - g.Enabled = true - g.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - g.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - g.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - g.SetHTTPClientTimeout(exch.HTTPTimeout) - g.SetHTTPClientUserAgent(exch.HTTPUserAgent) - g.RESTPollingDelay = exch.RESTPollingDelay - g.Verbose = exch.Verbose - g.HTTPDebugging = exch.HTTPDebugging - g.BaseCurrencies = exch.BaseCurrencies - g.AvailablePairs = exch.AvailablePairs - g.EnabledPairs = exch.EnabledPairs - g.WebsocketURL = geminiWebsocketEndpoint - err := g.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = g.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = g.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = g.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - if exch.UseSandbox { - g.APIUrl = geminiSandboxAPIURL - g.WebsocketURL = geminiWebsocketSandboxEndpoint - } - err = g.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = g.Websocket.Setup(g.WsConnect, - nil, - nil, - exch.Name, - exch.Websocket, - exch.Verbose, - g.WebsocketURL, - g.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - responseCheckTimeout = exch.WebsocketResponseCheckTimeout - responseMaxLimit = exch.WebsocketResponseMaxLimit - g.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - true, - false, - false, - exch.Name) - } -} - // GetSymbols returns all available symbols for trading func (g *Gemini) GetSymbols() ([]string, error) { var symbols []string - path := fmt.Sprintf("%s/v%s/%s", g.APIUrl, geminiAPIVersion, geminiSymbols) - + path := fmt.Sprintf("%s/v%s/%s", g.API.Endpoints.URL, geminiAPIVersion, geminiSymbols) return symbols, g.SendHTTPRequest(path, &symbols) } // GetTicker returns information about recent trading activity for the symbol -func (g *Gemini) GetTicker(currencyPair string) (Ticker, error) { - type TickerResponse struct { - Ask float64 `json:"ask,string"` - Bid float64 `json:"bid,string"` - Last float64 `json:"last,string"` - Volume map[string]interface{} - Message string `json:"message"` - } - - ticker := Ticker{} - resp := TickerResponse{} - path := fmt.Sprintf("%s/v%s/%s/%s", g.APIUrl, geminiAPIVersion, geminiTicker, currencyPair) - - err := g.SendHTTPRequest(path, &resp) +func (g *Gemini) GetTicker(currencyPair string) (TickerV2, error) { + ticker := TickerV2{} + path := fmt.Sprintf("%s/v2/ticker/%s", g.API.Endpoints.URL, currencyPair) + err := g.SendHTTPRequest(path, &ticker) if err != nil { return ticker, err } - - if resp.Message != "" { - return ticker, errors.New(resp.Message) + if ticker.Result == "error" { + return ticker, fmt.Errorf("%v %v %v", + g.Name, + ticker.Reason, + ticker.Message) } - ticker.Ask = resp.Ask - ticker.Bid = resp.Bid - ticker.Last = resp.Last - - ticker.Volume.Currency, _ = strconv.ParseFloat(resp.Volume[currencyPair[0:3]].(string), 64) - - if common.StringContains(currencyPair, "USD") { - ticker.Volume.USD, _ = strconv.ParseFloat(resp.Volume["USD"].(string), 64) - } else { - if resp.Volume["ETH"] != nil { - ticker.Volume.ETH, _ = strconv.ParseFloat(resp.Volume["ETH"].(string), 64) - } - - if resp.Volume["BTC"] != nil { - ticker.Volume.BTC, _ = strconv.ParseFloat(resp.Volume["BTC"].(string), 64) - } - } - - time, _ := resp.Volume["timestamp"].(float64) - ticker.Volume.Timestamp = int64(time) - return ticker, nil } @@ -228,7 +100,7 @@ func (g *Gemini) GetTicker(currencyPair string) (Ticker, error) { func (g *Gemini) GetOrderbook(currencyPair string, params url.Values) (Orderbook, error) { path := common.EncodeURLValues( fmt.Sprintf("%s/v%s/%s/%s", - g.APIUrl, + g.API.Endpoints.URL, geminiAPIVersion, geminiOrderbook, currencyPair), @@ -248,7 +120,7 @@ func (g *Gemini) GetOrderbook(currencyPair string, params url.Values) (Orderbook // include_breaks boolean Optional. Whether to display broken trades. False by // default. Can be '1' or 'true' to activate func (g *Gemini) GetTrades(currencyPair string, params url.Values) ([]Trade, error) { - path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s", g.APIUrl, geminiAPIVersion, geminiTrades, currencyPair), params) + path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s", g.API.Endpoints.URL, geminiAPIVersion, geminiTrades, currencyPair), params) var trades []Trade return trades, g.SendHTTPRequest(path, &trades) @@ -256,7 +128,7 @@ func (g *Gemini) GetTrades(currencyPair string, params url.Values) ([]Trade, err // GetAuction returns auction information func (g *Gemini) GetAuction(currencyPair string) (Auction, error) { - path := fmt.Sprintf("%s/v%s/%s/%s", g.APIUrl, geminiAPIVersion, geminiAuction, currencyPair) + path := fmt.Sprintf("%s/v%s/%s/%s", g.API.Endpoints.URL, geminiAPIVersion, geminiAuction, currencyPair) auction := Auction{} return auction, g.SendHTTPRequest(path, &auction) @@ -274,9 +146,8 @@ func (g *Gemini) GetAuction(currencyPair string) (Auction, error) { // include_indicative - [bool] Whether to include publication of // indicative prices and quantities. func (g *Gemini) GetAuctionHistory(currencyPair string, params url.Values) ([]AuctionHistory, error) { - path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s/%s", g.APIUrl, geminiAPIVersion, geminiAuction, currencyPair, geminiAuctionHistory), params) + path := common.EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s/%s", g.API.Endpoints.URL, geminiAPIVersion, geminiAuction, currencyPair, geminiAuctionHistory), params) var auctionHist []AuctionHistory - return auctionHist, g.SendHTTPRequest(path, &auctionHist) } @@ -443,7 +314,7 @@ func (g *Gemini) WithdrawCrypto(address, currency string, amount float64) (Withd req["address"] = address req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) - err := g.SendAuthenticatedHTTPRequest(http.MethodPost, geminiWithdraw+common.StringToLower(currency), req, &response) + err := g.SendAuthenticatedHTTPRequest(http.MethodPost, geminiWithdraw+strings.ToLower(currency), req, &response) if err != nil { return response, err } @@ -489,7 +360,7 @@ func (g *Gemini) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to the // exchange and returns an error func (g *Gemini) SendAuthenticatedHTTPRequest(method, path string, params map[string]interface{}, result interface{}) (err error) { - if !g.AuthenticatedAPISupport { + if !g.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, g.Name) } @@ -501,33 +372,33 @@ func (g *Gemini) SendAuthenticatedHTTPRequest(method, path string, params map[st req[key] = value } - PayloadJSON, err := common.JSONEncode(req) + PayloadJSON, err := json.Marshal(req) if err != nil { return errors.New("sendAuthenticatedHTTPRequest: Unable to JSON request") } if g.Verbose { - log.Debugf("Request JSON: %s", PayloadJSON) + log.Debugf(log.ExchangeSys, "Request JSON: %s", PayloadJSON) } - PayloadBase64 := common.Base64Encode(PayloadJSON) - hmac := common.GetHMAC(common.HashSHA512_384, []byte(PayloadBase64), []byte(g.APISecret)) + PayloadBase64 := crypto.Base64Encode(PayloadJSON) + hmac := crypto.GetHMAC(crypto.HashSHA512_384, []byte(PayloadBase64), []byte(g.API.Credentials.Secret)) headers := make(map[string]string) headers["Content-Length"] = "0" headers["Content-Type"] = "text/plain" - headers["X-GEMINI-APIKEY"] = g.APIKey + headers["X-GEMINI-APIKEY"] = g.API.Credentials.Key headers["X-GEMINI-PAYLOAD"] = PayloadBase64 - headers["X-GEMINI-SIGNATURE"] = common.HexEncodeToString(hmac) + headers["X-GEMINI-SIGNATURE"] = crypto.HexEncodeToString(hmac) headers["Cache-Control"] = "no-cache" return g.SendPayload(method, - g.APIUrl+"/v1/"+path, + g.API.Endpoints.URL+"/v1/"+path, headers, nil, result, true, - false, + true, g.Verbose, g.HTTPDebugging, g.HTTPRecording) diff --git a/exchanges/gemini/gemini_live_test.go b/exchanges/gemini/gemini_live_test.go index 2f083e33..522fda2e 100644 --- a/exchanges/gemini/gemini_live_test.go +++ b/exchanges/gemini/gemini_live_test.go @@ -17,17 +17,23 @@ var mockTests = false func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Gemini load config error", err) + } geminiConfig, err := cfg.GetExchangeConfig("Gemini") if err != nil { - log.Fatal("Test Failed - Gemini Setup() init error", err) + log.Fatal("Gemini Setup() init error", err) } - geminiConfig.AuthenticatedAPISupport = true - geminiConfig.APIKey = apiKey - geminiConfig.APISecret = apiSecret + geminiConfig.API.AuthenticatedSupport = true + geminiConfig.API.Credentials.Key = apiKey + geminiConfig.API.Credentials.Secret = apiSecret g.SetDefaults() - g.Setup(&geminiConfig) - g.APIUrl = geminiSandboxAPIURL - log.Printf(sharedtestvalues.LiveTesting, g.GetName(), g.APIUrl) + err = g.Setup(geminiConfig) + if err != nil { + log.Fatal("Gemini setup error", err) + } + g.API.Endpoints.URL = geminiSandboxAPIURL + log.Printf(sharedtestvalues.LiveTesting, g.Name, g.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/gemini/gemini_mock_test.go b/exchanges/gemini/gemini_mock_test.go index 85312654..c7982e5c 100644 --- a/exchanges/gemini/gemini_mock_test.go +++ b/exchanges/gemini/gemini_mock_test.go @@ -20,25 +20,32 @@ var mockTests = true func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Gemini load config error", err) + } geminiConfig, err := cfg.GetExchangeConfig("Gemini") if err != nil { - log.Fatal("Test Failed - Mock server error", err) + log.Fatal("Mock server error", err) } - geminiConfig.AuthenticatedAPISupport = true - geminiConfig.APIKey = apiKey - geminiConfig.APISecret = apiSecret + g.SkipAuthCheck = true + geminiConfig.API.AuthenticatedSupport = true + geminiConfig.API.Credentials.Key = apiKey + geminiConfig.API.Credentials.Secret = apiSecret g.SetDefaults() - g.Setup(&geminiConfig) + err = g.Setup(geminiConfig) + if err != nil { + log.Fatal("Gemini setup error", err) + } serverDetails, newClient, err := mock.NewVCRServer(mockFile) if err != nil { - log.Fatalf("Test Failed - Mock server error %s", err) + log.Fatalf("Mock server error %s", err) } g.HTTPClient = newClient - g.APIUrl = serverDetails + g.API.Endpoints.URL = serverDetails - log.Printf(sharedtestvalues.MockTesting, g.GetName(), g.APIUrl) + log.Printf(sharedtestvalues.MockTesting, g.Name, g.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/gemini/gemini_test.go b/exchanges/gemini/gemini_test.go index 83e09aa5..02704567 100644 --- a/exchanges/gemini/gemini_test.go +++ b/exchanges/gemini/gemini_test.go @@ -9,6 +9,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -30,7 +31,7 @@ func TestGetSymbols(t *testing.T) { t.Parallel() _, err := g.GetSymbols() if err != nil { - t.Error("Test Failed - GetSymbols() error", err) + t.Error("GetSymbols() error", err) } } @@ -38,11 +39,11 @@ func TestGetTicker(t *testing.T) { t.Parallel() _, err := g.GetTicker("BTCUSD") if err != nil { - t.Error("Test Failed - GetTicker() error", err) + t.Error("GetTicker() error", err) } _, err = g.GetTicker("bla") if err == nil { - t.Error("Test Failed - GetTicker() error", err) + t.Error("GetTicker() Expected error") } } @@ -50,7 +51,7 @@ func TestGetOrderbook(t *testing.T) { t.Parallel() _, err := g.GetOrderbook(testCurrency, url.Values{}) if err != nil { - t.Error("Test Failed - GetOrderbook() error", err) + t.Error("GetOrderbook() error", err) } } @@ -58,7 +59,7 @@ func TestGetTrades(t *testing.T) { t.Parallel() _, err := g.GetTrades(testCurrency, url.Values{}) if err != nil { - t.Error("Test Failed - GetTrades() error", err) + t.Error("GetTrades() error", err) } } @@ -66,9 +67,9 @@ func TestGetNotionalVolume(t *testing.T) { t.Parallel() _, err := g.GetNotionalVolume() if err != nil && mockTests { - t.Error("Test Failed - GetNotionalVolume() error", err) + t.Error("GetNotionalVolume() error", err) } else if err == nil && !mockTests { - t.Error("Test Failed - GetNotionalVolume() error cannot be nil") + t.Error("GetNotionalVolume() error cannot be nil") } } @@ -76,7 +77,7 @@ func TestGetAuction(t *testing.T) { t.Parallel() _, err := g.GetAuction(testCurrency) if err != nil { - t.Error("Test Failed - GetAuction() error", err) + t.Error("GetAuction() error", err) } } @@ -84,17 +85,21 @@ func TestGetAuctionHistory(t *testing.T) { t.Parallel() _, err := g.GetAuctionHistory(testCurrency, url.Values{}) if err != nil { - t.Error("Test Failed - GetAuctionHistory() error", err) + t.Error("GetAuctionHistory() error", err) } } func TestNewOrder(t *testing.T) { t.Parallel() - _, err := g.NewOrder(testCurrency, 1, 9000, "buy", "exchange limit") + _, err := g.NewOrder(testCurrency, + 1, + 9000000, + order.Sell.Lower(), + "exchange limit") if err != nil && mockTests { - t.Error("Test Failed - NewOrder() error", err) + t.Error("NewOrder() error", err) } else if err == nil && !mockTests { - t.Error("Test Failed - NewOrder() error cannot be nil") + t.Error("NewOrder() error cannot be nil") } } @@ -102,9 +107,9 @@ func TestCancelExistingOrder(t *testing.T) { t.Parallel() _, err := g.CancelExistingOrder(265555413) if err != nil && mockTests { - t.Error("Test Failed - CancelExistingOrder() error", err) + t.Error("CancelExistingOrder() error", err) } else if err == nil && !mockTests { - t.Error("Test Failed - CancelExistingOrder() error cannot be nil") + t.Error("CancelExistingOrder() error cannot be nil") } } @@ -112,9 +117,9 @@ func TestCancelExistingOrders(t *testing.T) { t.Parallel() _, err := g.CancelExistingOrders(false) if err != nil && mockTests { - t.Error("Test Failed - CancelExistingOrders() error", err) + t.Error("CancelExistingOrders() error", err) } else if err == nil && !mockTests { - t.Error("Test Failed - CancelExistingOrders() error cannot be nil") + t.Error("CancelExistingOrders() error cannot be nil") } } @@ -122,9 +127,9 @@ func TestGetOrderStatus(t *testing.T) { t.Parallel() _, err := g.GetOrderStatus(265563260) if err != nil && mockTests { - t.Error("Test Failed - GetOrderStatus() error", err) + t.Error("GetOrderStatus() error", err) } else if err == nil && !mockTests { - t.Error("Test Failed - GetOrderStatus() error cannot be nil") + t.Error("GetOrderStatus() error cannot be nil") } } @@ -132,9 +137,9 @@ func TestGetOrders(t *testing.T) { t.Parallel() _, err := g.GetOrders() if err != nil && mockTests { - t.Error("Test Failed - GetOrders() error", err) + t.Error("GetOrders() error", err) } else if err == nil && !mockTests { - t.Error("Test Failed - GetOrders() error cannot be nil") + t.Error("GetOrders() error cannot be nil") } } @@ -142,9 +147,9 @@ func TestGetTradeHistory(t *testing.T) { t.Parallel() _, err := g.GetTradeHistory(testCurrency, 0) if err != nil && mockTests { - t.Error("Test Failed - GetTradeHistory() error", err) + t.Error("GetTradeHistory() error", err) } else if err == nil && !mockTests { - t.Error("Test Failed - GetTradeHistory() error cannot be nil") + t.Error("GetTradeHistory() error cannot be nil") } } @@ -152,9 +157,9 @@ func TestGetTradeVolume(t *testing.T) { t.Parallel() _, err := g.GetTradeVolume() if err != nil && mockTests { - t.Error("Test Failed - GetTradeVolume() error", err) + t.Error("GetTradeVolume() error", err) } else if err == nil && !mockTests { - t.Error("Test Failed - GetTradeVolume() error cannot be nil") + t.Error("GetTradeVolume() error cannot be nil") } } @@ -162,9 +167,9 @@ func TestGetBalances(t *testing.T) { t.Parallel() _, err := g.GetBalances() if err != nil && mockTests { - t.Error("Test Failed - GetBalances() error", err) + t.Error("GetBalances() error", err) } else if err == nil && !mockTests { - t.Error("Test Failed - GetBalances() error cannot be nil") + t.Error("GetBalances() error cannot be nil") } } @@ -172,7 +177,7 @@ func TestGetCryptoDepositAddress(t *testing.T) { t.Parallel() _, err := g.GetCryptoDepositAddress("LOL123", "btc") if err == nil { - t.Error("Test Failed - GetCryptoDepositAddress() error", err) + t.Error("GetCryptoDepositAddress() Expected error") } } @@ -180,7 +185,7 @@ func TestWithdrawCrypto(t *testing.T) { t.Parallel() _, err := g.WithdrawCrypto("LOL123", "btc", 1) if err == nil { - t.Error("Test Failed - WithdrawCrypto() error", err) + t.Error("WithdrawCrypto() Expected error") } } @@ -188,9 +193,9 @@ func TestPostHeartbeat(t *testing.T) { t.Parallel() _, err := g.PostHeartbeat() if err != nil && mockTests { - t.Error("Test Failed - PostHeartbeat() error", err) + t.Error("PostHeartbeat() error", err) } else if err == nil && !mockTests { - t.Error("Test Failed - PostHeartbeat() error cannot be nil") + t.Error("PostHeartbeat() error cannot be nil") } } @@ -234,7 +239,7 @@ func TestGetFee(t *testing.T) { if areTestAPIKeysSet() || mockTests { // CryptocurrencyTradeFee Basic if resp, err := g.GetFee(feeBuilder); resp != float64(0.0035) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0035), resp) t.Error(err) @@ -245,7 +250,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := g.GetFee(feeBuilder); resp != float64(3500) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(3500), resp) t.Error(err) @@ -255,7 +260,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := g.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) t.Error(err) @@ -265,7 +270,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) @@ -275,7 +280,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) @@ -286,7 +291,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) @@ -296,7 +301,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) @@ -306,7 +311,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) @@ -317,7 +322,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := g.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) @@ -331,9 +336,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { exchange.AutoWithdrawCryptoWithSetupText + " & " + exchange.WithdrawFiatViaWebsiteOnlyText - withdrawPermissions := g.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, @@ -343,8 +346,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{ currency.NewPair(currency.LTC, currency.BTC), }, @@ -363,8 +366,8 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, } @@ -382,11 +385,7 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if g.APIKey != "" && g.APIKey != "Key" && - g.APISecret != "" && g.APISecret != "Secret" { - return true - } - return false + return g.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { @@ -395,18 +394,20 @@ func TestSubmitOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "_", - Base: currency.LTC, - Quote: currency.BTC, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: "_", + Base: currency.LTC, + Quote: currency.BTC, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 10, + Amount: 1, + ClientID: "1234234", } - response, err := g.SubmitOrder(p, - exchange.BuyOrderSide, - exchange.LimitOrderType, - 1, - 10, - "1234234") + response, err := g.SubmitOrder(orderSubmission) switch { case areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced): t.Errorf("Order failed to be placed: %v", err) @@ -422,8 +423,7 @@ func TestCancelExchangeOrder(t *testing.T) { if areTestAPIKeysSet() && !canManipulateRealOrders && !mockTests { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "266029865", } @@ -445,8 +445,7 @@ func TestCancelAllExchangeOrders(t *testing.T) { } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -463,26 +462,28 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestModifyOrder(t *testing.T) { t.Parallel() - _, err := g.ModifyOrder(&exchange.ModifyOrder{}) + _, err := g.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { t.Parallel() - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders && !mockTests { @@ -507,7 +508,7 @@ func TestWithdrawFiat(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := g.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", @@ -522,7 +523,7 @@ func TestWithdrawInternationalBank(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := g.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", @@ -535,17 +536,17 @@ func TestGetDepositAddress(t *testing.T) { t.Parallel() _, err := g.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress error cannot be nil") + t.Error("GetDepositAddress error cannot be nil") } } // TestWsAuth dials websocket, sends login request. func TestWsAuth(t *testing.T) { t.Parallel() - g.WebsocketURL = geminiWebsocketSandboxEndpoint + g.API.Endpoints.WebsocketURL = geminiWebsocketSandboxEndpoint if !g.Websocket.IsEnabled() && - !g.AuthenticatedWebsocketAPISupport || + !g.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } diff --git a/exchanges/gemini/gemini_types.go b/exchanges/gemini/gemini_types.go index ebe461ec..de687b43 100644 --- a/exchanges/gemini/gemini_types.go +++ b/exchanges/gemini/gemini_types.go @@ -16,6 +16,21 @@ type Ticker struct { } } +// TickerV2 holds returned ticker data from the exchange +type TickerV2 struct { + Ask float64 `json:"ask,string"` + Bid float64 `json:"bid,string"` + Changes []string `json:"changes"` + Close float64 `json:"close,string"` + High float64 `json:"high,string"` + Low float64 `json:"low,string"` + Open float64 `json:"open,string"` + Message string `json:"message,omitempty"` + Reason string `json:"reason,omitempty"` + Result string `json:"result,omitempty"` + Symbol currency.Pair `json:"symbol"` +} + // Orderbook contains orderbook information for both bid and ask side type Orderbook struct { Bids []OrderbookEntry `json:"bids"` diff --git a/exchanges/gemini/gemini_websocket.go b/exchanges/gemini/gemini_websocket.go index bc1691d9..e4c23b06 100644 --- a/exchanges/gemini/gemini_websocket.go +++ b/exchanges/gemini/gemini_websocket.go @@ -3,16 +3,20 @@ package gemini import ( + "encoding/json" "errors" "fmt" "net/http" "net/url" + "strings" "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" @@ -28,7 +32,7 @@ const ( ) // Instantiates a communications channel between websocket connections -var comms = make(chan ReadData, 1) +var comms = make(chan ReadData) var responseMaxLimit time.Duration var responseCheckTimeout time.Duration @@ -50,21 +54,21 @@ func (g *Gemini) WsConnect() error { go g.WsHandleData() err := g.WsSecureSubscribe(&dialer, geminiWsOrderEvents) if err != nil { - log.Errorf("%v - authentication failed: %v", g.Name, err) + log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", g.Name, err) } return g.WsSubscribe(&dialer) } // WsSubscribe subscribes to the full websocket suite on gemini exchange func (g *Gemini) WsSubscribe(dialer *websocket.Dialer) error { - enabledCurrencies := g.GetEnabledCurrencies() - for i, c := range enabledCurrencies { + enabledCurrencies := g.GetEnabledPairs(asset.Spot) + for i := range enabledCurrencies { val := url.Values{} val.Set("heartbeat", "true") endpoint := fmt.Sprintf("%s%s/%s?%s", - g.WebsocketURL, + g.API.Endpoints.WebsocketURL, geminiWsMarketData, - c.String(), + enabledCurrencies[i].String(), val.Encode()) connection := &wshandler.WebsocketConnection{ ExchangeName: g.Name, @@ -75,9 +79,10 @@ func (g *Gemini) WsSubscribe(dialer *websocket.Dialer) error { } err := connection.Dial(dialer, http.Header{}) if err != nil { - return fmt.Errorf("%v Websocket connection %v error. Error %v", g.Name, endpoint, err) + return fmt.Errorf("%v Websocket connection %v error. Error %v", + g.Name, endpoint, err) } - go g.WsReadData(connection, c) + go g.WsReadData(connection, enabledCurrencies[i]) if len(enabledCurrencies)-1 == i { return nil } @@ -94,20 +99,20 @@ func (g *Gemini) WsSecureSubscribe(dialer *websocket.Dialer, url string) error { Request: fmt.Sprintf("/v1/%v", url), Nonce: time.Now().UnixNano(), } - PayloadJSON, err := common.JSONEncode(payload) + PayloadJSON, err := json.Marshal(payload) if err != nil { return fmt.Errorf("%v sendAuthenticatedHTTPRequest: Unable to JSON request", g.Name) } - endpoint := fmt.Sprintf("%v%v", g.WebsocketURL, url) - PayloadBase64 := common.Base64Encode(PayloadJSON) - hmac := common.GetHMAC(common.HashSHA512_384, []byte(PayloadBase64), []byte(g.APISecret)) + endpoint := g.API.Endpoints.WebsocketURL + url + PayloadBase64 := crypto.Base64Encode(PayloadJSON) + hmac := crypto.GetHMAC(crypto.HashSHA512_384, []byte(PayloadBase64), []byte(g.API.Credentials.Secret)) headers := http.Header{} headers.Add("Content-Length", "0") headers.Add("Content-Type", "text/plain") headers.Add("X-GEMINI-PAYLOAD", PayloadBase64) - headers.Add("X-GEMINI-APIKEY", g.APIKey) - headers.Add("X-GEMINI-SIGNATURE", common.HexEncodeToString(hmac)) + headers.Add("X-GEMINI-APIKEY", g.API.Credentials.Key) + headers.Add("X-GEMINI-SIGNATURE", crypto.HexEncodeToString(hmac)) headers.Add("Cache-Control", "no-cache") g.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{ @@ -161,7 +166,7 @@ func (g *Gemini) WsHandleData() { continue } var result map[string]interface{} - err := common.JSONDecode(resp.Raw, &result) + err := json.Unmarshal(resp.Raw, &result) if err != nil { g.Websocket.DataHandler <- fmt.Errorf("%v Error: %v, Raw: %v", g.Name, err, string(resp.Raw)) continue @@ -169,7 +174,7 @@ func (g *Gemini) WsHandleData() { switch result["type"] { case "subscription_ack": var result WsSubscriptionAcknowledgementResponse - err := common.JSONDecode(resp.Raw, &result) + err := json.Unmarshal(resp.Raw, &result) if err != nil { g.Websocket.DataHandler <- err continue @@ -177,7 +182,7 @@ func (g *Gemini) WsHandleData() { g.Websocket.DataHandler <- result case "initial": var result WsSubscriptionAcknowledgementResponse - err := common.JSONDecode(resp.Raw, &result) + err := json.Unmarshal(resp.Raw, &result) if err != nil { g.Websocket.DataHandler <- err continue @@ -185,7 +190,7 @@ func (g *Gemini) WsHandleData() { g.Websocket.DataHandler <- result case "accepted": var result WsActiveOrdersResponse - err := common.JSONDecode(resp.Raw, &result) + err := json.Unmarshal(resp.Raw, &result) if err != nil { g.Websocket.DataHandler <- err continue @@ -193,7 +198,7 @@ func (g *Gemini) WsHandleData() { g.Websocket.DataHandler <- result case "booked": var result WsOrderBookedResponse - err := common.JSONDecode(resp.Raw, &result) + err := json.Unmarshal(resp.Raw, &result) if err != nil { g.Websocket.DataHandler <- err continue @@ -201,7 +206,7 @@ func (g *Gemini) WsHandleData() { g.Websocket.DataHandler <- result case "fill": var result WsOrderFilledResponse - err := common.JSONDecode(resp.Raw, &result) + err := json.Unmarshal(resp.Raw, &result) if err != nil { g.Websocket.DataHandler <- err continue @@ -209,7 +214,7 @@ func (g *Gemini) WsHandleData() { g.Websocket.DataHandler <- result case "cancelled": var result WsOrderCancelledResponse - err := common.JSONDecode(resp.Raw, &result) + err := json.Unmarshal(resp.Raw, &result) if err != nil { g.Websocket.DataHandler <- err continue @@ -217,7 +222,7 @@ func (g *Gemini) WsHandleData() { g.Websocket.DataHandler <- result case "closed": var result WsOrderClosedResponse - err := common.JSONDecode(resp.Raw, &result) + err := json.Unmarshal(resp.Raw, &result) if err != nil { g.Websocket.DataHandler <- err continue @@ -225,7 +230,7 @@ func (g *Gemini) WsHandleData() { g.Websocket.DataHandler <- result case "heartbeat": var result WsHeartbeatResponse - err := common.JSONDecode(resp.Raw, &result) + err := json.Unmarshal(resp.Raw, &result) if err != nil { g.Websocket.DataHandler <- err continue @@ -238,7 +243,7 @@ func (g *Gemini) WsHandleData() { continue } var marketUpdate WsMarketUpdateResponse - err := common.JSONDecode(resp.Raw, &marketUpdate) + err := json.Unmarshal(resp.Raw, &marketUpdate) if err != nil { g.Websocket.DataHandler <- err continue @@ -276,17 +281,17 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids - newOrderBook.AssetType = orderbook.Spot + newOrderBook.AssetType = asset.Spot newOrderBook.Pair = pair - err := g.Websocket.Orderbook.LoadSnapshot(&newOrderBook, - false) + newOrderBook.ExchangeName = g.Name + err := g.Websocket.Orderbook.LoadSnapshot(&newOrderBook) if err != nil { g.Websocket.DataHandler <- err return } g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: pair, - Asset: orderbook.Spot, - Exchange: g.GetName()} + Asset: asset.Spot, + Exchange: g.Name} } else { var asks, bids []orderbook.Item for i := 0; i < len(result.Events); i++ { @@ -294,7 +299,7 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa g.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Now(), CurrencyPair: pair, - AssetType: orderbook.Spot, + AssetType: asset.Spot, Exchange: g.Name, EventTime: result.Timestamp, Price: result.Events[i].Price, @@ -306,7 +311,7 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa Amount: result.Events[i].Remaining, Price: result.Events[i].Price, } - if result.Events[i].Side == "ask" { + if strings.EqualFold(result.Events[i].Side, order.Ask.String()) { asks = append(asks, item) } else { bids = append(bids, item) @@ -314,17 +319,17 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa } } err := g.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ - Asks: asks, - Bids: bids, - CurrencyPair: pair, - UpdateTime: time.Unix(0, result.TimestampMS), - AssetType: orderbook.Spot, + Asks: asks, + Bids: bids, + Pair: pair, + UpdateTime: time.Unix(0, result.TimestampMS), + Asset: asset.Spot, }) if err != nil { g.Websocket.DataHandler <- fmt.Errorf("%v %v", g.Name, err) } g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: pair, - Asset: orderbook.Spot, - Exchange: g.GetName()} + Asset: asset.Spot, + Exchange: g.Name} } } diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index 74c576a7..d730b463 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -2,7 +2,6 @@ package gemini import ( "errors" - "fmt" "net/url" "strconv" "strings" @@ -10,14 +9,164 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (g *Gemini) GetDefaultConfig() (*config.ExchangeConfig, error) { + g.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = g.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = g.BaseCurrencies + + err := g.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if g.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = g.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets package defaults for gemini exchange +func (g *Gemini) SetDefaults() { + g.Name = "Gemini" + g.Enabled = true + g.Verbose = true + g.API.CredentialsValidator.RequiresKey = true + g.API.CredentialsValidator.RequiresSecret = true + + g.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + } + + g.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + FiatWithdrawalFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + OrderbookFetching: true, + TradeFetching: true, + AuthenticatedEndpoints: true, + MessageSequenceNumbers: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | + exchange.AutoWithdrawCryptoWithSetup | + exchange.WithdrawFiatViaWebsiteOnly, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + g.Requester = request.New(g.Name, + request.NewRateLimit(time.Minute, geminiAuthRate), + request.NewRateLimit(time.Minute, geminiUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + g.API.Endpoints.URLDefault = geminiAPIURL + g.API.Endpoints.URL = g.API.Endpoints.URLDefault + g.API.Endpoints.WebsocketURL = geminiWebsocketEndpoint + g.Websocket = wshandler.New() + g.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + g.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + g.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup sets exchange configuration parameters +func (g *Gemini) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + g.SetEnabled(false) + return nil + } + + err := g.SetupDefaults(exch) + if err != nil { + return err + } + + if exch.UseSandbox { + g.API.Endpoints.URL = geminiSandboxAPIURL + } + + err = g.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: geminiWebsocketEndpoint, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: g.WsConnect, + Features: &g.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + g.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: g.Name, + URL: g.Websocket.GetWebsocketURL(), + ProxyURL: g.Websocket.GetProxyAddress(), + Verbose: g.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + g.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + true, + true, + false, + false, + exch.Name) + return nil +} + // Start starts the Gemini go routine func (g *Gemini) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -30,32 +179,40 @@ func (g *Gemini) Start(wg *sync.WaitGroup) { // Run implements the Gemini wrapper func (g *Gemini) Run() { if g.Verbose { - log.Debugf("%s polling delay: %ds.\n", g.GetName(), g.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", g.GetName(), len(g.EnabledPairs), g.EnabledPairs) + g.PrintEnabledPairs() } - exchangeProducts, err := g.GetSymbols() + if !g.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := g.UpdateTradablePairs(false) if err != nil { - log.Errorf("%s Failed to get available symbols.\n", g.GetName()) - } else { - var newExchangeProducts currency.Pairs - for _, p := range exchangeProducts { - newExchangeProducts = append(newExchangeProducts, - currency.NewPairFromString(p)) - } - - err = g.UpdateCurrencies(newExchangeProducts, false, false) - if err != nil { - log.Errorf("%s Failed to update available currencies.\n", g.GetName()) - } + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", g.Name, err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (g *Gemini) FetchTradablePairs(asset asset.Item) ([]string, error) { + return g.GetSymbols() +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (g *Gemini) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := g.GetSymbols() + if err != nil { + return err + } + + return g.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + // GetAccountInfo Retrieves balances for all enabled currencies for the // Gemini exchange func (g *Gemini) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = g.GetName() + response.Exchange = g.Name accountBalance, err := g.GetBalances() if err != nil { return response, err @@ -78,19 +235,22 @@ func (g *Gemini) GetAccountInfo() (exchange.AccountInfo, error) { } // UpdateTicker updates and returns the ticker for a currency pair -func (g *Gemini) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (g *Gemini) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price tick, err := g.GetTicker(p.String()) if err != nil { return tickerPrice, err } - tickerPrice.Pair = p - tickerPrice.Ask = tick.Ask - tickerPrice.Bid = tick.Bid - tickerPrice.Last = tick.Last - tickerPrice.Volume = tick.Volume.USD - - err = ticker.ProcessTicker(g.GetName(), &tickerPrice, assetType) + tickerPrice = ticker.Price{ + High: tick.High, + Low: tick.Low, + Bid: tick.Bid, + Ask: tick.Ask, + Open: tick.Open, + Close: tick.Close, + Pair: p, + } + err = ticker.ProcessTicker(g.Name, &tickerPrice, assetType) if err != nil { return tickerPrice, err } @@ -98,18 +258,18 @@ func (g *Gemini) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, return ticker.GetTicker(g.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (g *Gemini) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(g.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (g *Gemini) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(g.Name, p, assetType) if err != nil { return g.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (g *Gemini) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(g.GetName(), p, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (g *Gemini) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(g.Name, p, assetType) if err != nil { return g.UpdateOrderbook(p, assetType) } @@ -117,7 +277,7 @@ func (g *Gemini) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Ba } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (g *Gemini) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (g *Gemini) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base orderbookNew, err := g.GetOrderbook(p.String(), url.Values{}) if err != nil { @@ -133,7 +293,7 @@ func (g *Gemini) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.B } orderBook.Pair = p - orderBook.ExchangeName = g.GetName() + orderBook.ExchangeName = g.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -147,51 +307,52 @@ func (g *Gemini) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.B // GetFundingHistory returns funding history, deposits and // withdrawals func (g *Gemini) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (g *Gemini) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (g *Gemini) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (g *Gemini) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - p = exchange.FormatExchangeCurrency(g.Name, p) - - if orderType != exchange.LimitOrderType { - return submitOrderResponse, errors.New("only limit orders are enabled through this API") +func (g *Gemini) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err } - response, err := g.NewOrder(p.String(), - amount, - price, - side.ToString(), + if s.OrderType != order.Limit { + return submitOrderResponse, + errors.New("only limit orders are enabled through this exchange") + } + + response, err := g.NewOrder( + g.FormatExchangeCurrency(s.Pair, asset.Spot).String(), + s.Amount, + s.Price, + s.OrderSide.String(), "exchange limit") - + if err != nil { + return submitOrderResponse, err + } if response > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response) + submitOrderResponse.OrderID = strconv.FormatInt(response, 10) } - if err == nil { - submitOrderResponse.IsOrderPlaced = true - } + submitOrderResponse.IsOrderPlaced = true - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (g *Gemini) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (g *Gemini) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (g *Gemini) CancelOrder(order *exchange.OrderCancellation) error { +func (g *Gemini) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) if err != nil { return err @@ -202,25 +363,25 @@ func (g *Gemini) CancelOrder(order *exchange.OrderCancellation) error { } // CancelAllOrders cancels all orders associated with a currency pair -func (g *Gemini) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (g *Gemini) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } resp, err := g.CancelExistingOrders(false) if err != nil { return cancelAllOrdersResponse, err } - for _, order := range resp.Details.CancelRejects { - cancelAllOrdersResponse.OrderStatus[order] = "Could not cancel order" + for i := range resp.Details.CancelRejects { + cancelAllOrdersResponse.Status[resp.Details.CancelRejects[i]] = "Could not cancel order" } return cancelAllOrdersResponse, nil } // GetOrderInfo returns information on a current open order -func (g *Gemini) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (g *Gemini) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -235,7 +396,7 @@ func (g *Gemini) GetDepositAddress(cryptocurrency currency.Code, _ string) (stri // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (g *Gemini) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (g *Gemini) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { resp, err := g.WithdrawCrypto(withdrawRequest.Address, withdrawRequest.Currency.String(), withdrawRequest.Amount) if err != nil { return "", err @@ -249,13 +410,13 @@ func (g *Gemini) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawR // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (g *Gemini) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (g *Gemini) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (g *Gemini) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (g *Gemini) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -266,7 +427,7 @@ func (g *Gemini) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (g *Gemini) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (g.APIKey == "" || g.APISecret == "") && // Todo check connection status + if (!g.AllowAuthenticatedRequest() || g.SkipAuthCheck) && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -274,30 +435,30 @@ func (g *Gemini) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) } // GetActiveOrders retrieves any orders that are active/open -func (g *Gemini) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (g *Gemini) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := g.GetOrders() if err != nil { return nil, err } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range resp { symbol := currency.NewPairDelimiter(resp[i].Symbol, - g.ConfigCurrencyPairFormat.Delimiter) - var orderType exchange.OrderType + g.GetPairFormat(asset.Spot, false).Delimiter) + var orderType order.Type if resp[i].Type == "exchange limit" { - orderType = exchange.LimitOrderType + orderType = order.Limit } else if resp[i].Type == "market buy" || resp[i].Type == "market sell" { - orderType = exchange.MarketOrderType + orderType = order.Market } - side := exchange.OrderSide(strings.ToUpper(resp[i].Type)) + side := order.Side(strings.ToUpper(resp[i].Type)) orderDate := time.Unix(resp[i].Timestamp, 0) - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ Amount: resp[i].OriginalAmount, RemainingAmount: resp[i].RemainingAmount, - ID: fmt.Sprintf("%v", resp[i].OrderID), + ID: strconv.FormatInt(resp[i].OrderID, 10), ExecutedAmount: resp[i].ExecutedAmount, Exchange: g.Name, OrderType: orderType, @@ -308,45 +469,44 @@ func (g *Gemini) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([ }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByType(&orders, getOrdersRequest.OrderType) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByType(&orders, req.OrderType) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (g *Gemini) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - if len(getOrdersRequest.Currencies) == 0 { +func (g *Gemini) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + if len(req.Currencies) == 0 { return nil, errors.New("currency must be supplied") } var trades []TradeHistory - for _, currency := range getOrdersRequest.Currencies { - resp, err := g.GetTradeHistory(exchange.FormatExchangeCurrency(g.Name, currency).String(), - getOrdersRequest.StartTicks.Unix()) + for j := range req.Currencies { + resp, err := g.GetTradeHistory(g.FormatExchangeCurrency(req.Currencies[j], + asset.Spot).String(), + req.StartTicks.Unix()) if err != nil { return nil, err } for i := range resp { - resp[i].BaseCurrency = currency.Base.String() - resp[i].QuoteCurrency = currency.Quote.String() + resp[i].BaseCurrency = req.Currencies[j].Base.String() + resp[i].QuoteCurrency = req.Currencies[j].Quote.String() trades = append(trades, resp[i]) } } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range trades { - side := exchange.OrderSide(strings.ToUpper(trades[i].Type)) + side := order.Side(strings.ToUpper(trades[i].Type)) orderDate := time.Unix(trades[i].Timestamp, 0) - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ Amount: trades[i].Amount, - ID: fmt.Sprintf("%v", trades[i].OrderID), + ID: strconv.FormatInt(trades[i].OrderID, 10), Exchange: g.Name, OrderDate: orderDate, OrderSide: side, @@ -354,14 +514,12 @@ func (g *Gemini) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ Price: trades[i].Price, CurrencyPair: currency.NewPairWithDelimiter(trades[i].BaseCurrency, trades[i].QuoteCurrency, - g.ConfigCurrencyPairFormat.Delimiter), + g.GetPairFormat(asset.Spot, false).Delimiter), }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } diff --git a/exchanges/hitbtc/README.md b/exchanges/hitbtc/README.md index 49e3c4c2..7acbaef6 100644 --- a/exchanges/hitbtc/README.md +++ b/exchanges/hitbtc/README.md @@ -48,22 +48,22 @@ main.go ```go var h exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "HitBTC" { - h = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "HitBTC" { + h = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := h.GetTickerPrice() +tick, err := h.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := h.GetOrderbookEx() +ob, err := h.FetchOrderbook() if err != nil { // Handle error } @@ -138,4 +138,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/hitbtc/hitbtc.go b/exchanges/hitbtc/hitbtc.go index 2b015163..63649393 100644 --- a/exchanges/hitbtc/hitbtc.go +++ b/exchanges/hitbtc/hitbtc.go @@ -7,16 +7,11 @@ import ( "net/http" "net/url" "strconv" - "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( @@ -40,8 +35,7 @@ const ( apiv2OpenOrders = "api/2/order" apiV2FeeInfo = "api/2/trading/fee" orders = "order" - orderBuy = "api/2/order" - orderSell = "sell" + apiOrder = "api/2/order" orderMove = "moveOrder" tradableBalances = "returnTradableBalances" transferBalance = "transferBalance" @@ -56,111 +50,6 @@ type HitBTC struct { WebsocketConn *wshandler.WebsocketConnection } -// SetDefaults sets default settings for hitbtc -func (h *HitBTC) SetDefaults() { - h.Name = "HitBTC" - h.Enabled = false - h.Fee = 0 - h.Verbose = false - h.RESTPollingDelay = 10 - h.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | - exchange.NoFiatWithdrawals - h.RequestCurrencyPairFormat.Delimiter = "" - h.RequestCurrencyPairFormat.Uppercase = true - h.ConfigCurrencyPairFormat.Delimiter = "-" - h.ConfigCurrencyPairFormat.Uppercase = true - h.AssetTypes = []string{ticker.Spot} - h.SupportsAutoPairUpdating = true - h.SupportsRESTTickerBatching = true - h.Requester = request.New(h.Name, - request.NewRateLimit(time.Second, hitbtcAuthRate), - request.NewRateLimit(time.Second, hitbtcUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - h.APIUrlDefault = apiURL - h.APIUrl = h.APIUrlDefault - h.Websocket = wshandler.New() - h.Websocket.Functionality = wshandler.WebsocketTickerSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported | - wshandler.WebsocketSubmitOrderSupported | - wshandler.WebsocketCancelOrderSupported | - wshandler.WebsocketMessageCorrelationSupported - h.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - h.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - h.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup sets user exchange configuration settings -func (h *HitBTC) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - h.SetEnabled(false) - } else { - h.Enabled = true - h.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - h.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - h.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - h.SetHTTPClientTimeout(exch.HTTPTimeout) - h.SetHTTPClientUserAgent(exch.HTTPUserAgent) - h.RESTPollingDelay = exch.RESTPollingDelay // Max 60000ms - h.Verbose = exch.Verbose - h.HTTPDebugging = exch.HTTPDebugging - h.Websocket.SetWsStatusAndConnection(exch.Websocket) - h.BaseCurrencies = exch.BaseCurrencies - h.AvailablePairs = exch.AvailablePairs - h.EnabledPairs = exch.EnabledPairs - err := h.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = h.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = h.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = h.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = h.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = h.Websocket.Setup(h.WsConnect, - h.Subscribe, - h.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - hitbtcWebsocketAddress, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - h.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: h.Name, - URL: h.Websocket.GetWebsocketURL(), - ProxyURL: h.Websocket.GetProxyAddress(), - Verbose: h.Verbose, - RateLimit: rateLimit, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - h.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - true, - true, - false, - exch.Name) - } -} - // Public Market Data // https://api.hitbtc.com/?python#market-data @@ -171,7 +60,7 @@ func (h *HitBTC) GetCurrencies() (map[string]Currencies, error) { Data []Currencies } resp := Response{} - path := fmt.Sprintf("%s/%s", h.APIUrl, apiV2Currency) + path := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, apiV2Currency) ret := make(map[string]Currencies) err := h.SendHTTPRequest(path, &resp.Data) @@ -192,7 +81,7 @@ func (h *HitBTC) GetCurrency(currency string) (Currencies, error) { Data Currencies } resp := Response{} - path := fmt.Sprintf("%s/%s/%s", h.APIUrl, apiV2Currency, currency) + path := fmt.Sprintf("%s/%s/%s", h.API.Endpoints.URL, apiV2Currency, currency) return resp.Data, h.SendHTTPRequest(path, &resp.Data) } @@ -204,7 +93,7 @@ func (h *HitBTC) GetCurrency(currency string) (Currencies, error) { // of the base currency. func (h *HitBTC) GetSymbols(symbol string) ([]string, error) { var resp []Symbol - path := fmt.Sprintf("%s/%s/%s", h.APIUrl, apiV2Symbol, symbol) + path := fmt.Sprintf("%s/%s/%s", h.API.Endpoints.URL, apiV2Symbol, symbol) ret := make([]string, 0, len(resp)) err := h.SendHTTPRequest(path, &resp) @@ -222,71 +111,22 @@ func (h *HitBTC) GetSymbols(symbol string) ([]string, error) { // all their details. func (h *HitBTC) GetSymbolsDetailed() ([]Symbol, error) { var resp []Symbol - path := fmt.Sprintf("%s/%s", h.APIUrl, apiV2Symbol) - + path := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, apiV2Symbol) return resp, h.SendHTTPRequest(path, &resp) } // GetTicker returns ticker information -func (h *HitBTC) GetTicker(symbol string) (map[string]Ticker, error) { - var resp1 []TickerResponse - resp2 := TickerResponse{} - ret := make(map[string]TickerResponse) - result := make(map[string]Ticker) - path := fmt.Sprintf("%s/%s/%s", h.APIUrl, apiV2Ticker, symbol) - var err error +func (h *HitBTC) GetTicker(symbol string) (TickerResponse, error) { + var resp TickerResponse + path := fmt.Sprintf("%s/%s/%s", h.API.Endpoints.URL, apiV2Ticker, symbol) + return resp, h.SendHTTPRequest(path, &resp) +} - if symbol == "" { - err = h.SendHTTPRequest(path, &resp1) - if err != nil { - return nil, err - } - - for i := range resp1 { - if resp1[i].Symbol != "" { - ret[resp1[i].Symbol] = resp1[i] - } - } - } else { - err = h.SendHTTPRequest(path, &resp2) - ret[resp2.Symbol] = resp2 - } - - if err == nil { - for i := range ret { - tick := Ticker{} - - ask, _ := strconv.ParseFloat(ret[i].Ask, 64) - tick.Ask = ask - - bid, _ := strconv.ParseFloat(ret[i].Bid, 64) - tick.Bid = bid - - high, _ := strconv.ParseFloat(ret[i].High, 64) - tick.High = high - - last, _ := strconv.ParseFloat(ret[i].Last, 64) - tick.Last = last - - low, _ := strconv.ParseFloat(ret[i].Low, 64) - tick.Low = low - - open, _ := strconv.ParseFloat(ret[i].Open, 64) - tick.Open = open - - vol, _ := strconv.ParseFloat(ret[i].Volume, 64) - tick.Volume = vol - - volQuote, _ := strconv.ParseFloat(ret[i].VolumeQuote, 64) - tick.VolumeQuote = volQuote - - tick.Symbol = ret[i].Symbol - tick.Timestamp = ret[i].Timestamp - result[i] = tick - } - } - - return result, err +// GetTickers returns ticker information +func (h *HitBTC) GetTickers() ([]TickerResponse, error) { + var resp []TickerResponse + path := fmt.Sprintf("%s/%s/", h.API.Endpoints.URL, apiV2Ticker) + return resp, h.SendHTTPRequest(path, &resp) } // GetTrades returns trades from hitbtc @@ -324,8 +164,7 @@ func (h *HitBTC) GetTrades(currencyPair, from, till, limit, offset, by, sort str } var resp []TradeHistory - path := fmt.Sprintf("%s/%s/%s?%s", h.APIUrl, apiV2Trades, currencyPair, vals.Encode()) - + path := fmt.Sprintf("%s/%s/%s?%s", h.API.Endpoints.URL, apiV2Trades, currencyPair, vals.Encode()) return resp, h.SendHTTPRequest(path, &resp) } @@ -340,7 +179,7 @@ func (h *HitBTC) GetOrderbook(currencyPair string, limit int) (Orderbook, error) } resp := OrderbookResponse{} - path := fmt.Sprintf("%s/%s/%s?%s", h.APIUrl, apiV2Orderbook, currencyPair, vals.Encode()) + path := fmt.Sprintf("%s/%s/%s?%s", h.API.Endpoints.URL, apiV2Orderbook, currencyPair, vals.Encode()) err := h.SendHTTPRequest(path, &resp) if err != nil { @@ -369,8 +208,7 @@ func (h *HitBTC) GetCandles(currencyPair, limit, period string) ([]ChartData, er } var resp []ChartData - path := fmt.Sprintf("%s/%s/%s?%s", h.APIUrl, apiV2Candles, currencyPair, vals.Encode()) - + path := fmt.Sprintf("%s/%s/%s?%s", h.API.Endpoints.URL, apiV2Candles, currencyPair, vals.Encode()) return resp, h.SendHTTPRequest(path, &resp) } @@ -477,7 +315,7 @@ func (h *HitBTC) GetOpenOrders(currency string) ([]OrderHistoryResponse, error) // PlaceOrder places an order on the exchange func (h *HitBTC) PlaceOrder(currency string, rate, amount float64, orderType, side string) (OrderResponse, error) { - result := OrderResponse{} + var result OrderResponse values := url.Values{} values.Set("symbol", currency) @@ -487,7 +325,7 @@ func (h *HitBTC) PlaceOrder(currency string, rate, amount float64, orderType, si values.Set("price", strconv.FormatFloat(rate, 'f', -1, 64)) values.Set("type", orderType) - return result, h.SendAuthenticatedHTTPRequest(http.MethodPost, orderBuy, values, &result) + return result, h.SendAuthenticatedHTTPRequest(http.MethodPost, apiOrder, values, &result) } // CancelExistingOrder cancels a specific order by OrderID @@ -495,7 +333,7 @@ func (h *HitBTC) CancelExistingOrder(orderID int64) (bool, error) { result := GenericResponse{} values := url.Values{} - err := h.SendAuthenticatedHTTPRequest(http.MethodDelete, orderBuy+"/"+strconv.FormatInt(orderID, 10), values, &result) + err := h.SendAuthenticatedHTTPRequest(http.MethodDelete, apiOrder+"/"+strconv.FormatInt(orderID, 10), values, &result) if err != nil { return false, err @@ -512,7 +350,7 @@ func (h *HitBTC) CancelExistingOrder(orderID int64) (bool, error) { func (h *HitBTC) CancelAllExistingOrders() ([]Order, error) { var result []Order values := url.Values{} - return result, h.SendAuthenticatedHTTPRequest(http.MethodDelete, orderBuy, values, &result) + return result, h.SendAuthenticatedHTTPRequest(http.MethodDelete, apiOrder, values, &result) } // MoveOrder generates a new move order @@ -636,14 +474,14 @@ func (h *HitBTC) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated http request func (h *HitBTC) SendAuthenticatedHTTPRequest(method, endpoint string, values url.Values, result interface{}) error { - if !h.AuthenticatedAPISupport { + if !h.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, h.Name) } headers := make(map[string]string) - headers["Authorization"] = "Basic " + common.Base64Encode([]byte(h.APIKey+":"+h.APISecret)) + headers["Authorization"] = "Basic " + crypto.Base64Encode([]byte(h.API.Credentials.Key+":"+h.API.Credentials.Secret)) - path := fmt.Sprintf("%s/%s", h.APIUrl, endpoint) + path := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, endpoint) return h.SendPayload(method, path, diff --git a/exchanges/hitbtc/hitbtc_test.go b/exchanges/hitbtc/hitbtc_test.go index 8860ef9e..208d5fdc 100644 --- a/exchanges/hitbtc/hitbtc_test.go +++ b/exchanges/hitbtc/hitbtc_test.go @@ -1,7 +1,9 @@ package hitbtc import ( + "log" "net/http" + "os" "testing" "time" @@ -10,6 +12,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -24,23 +28,28 @@ const ( canManipulateRealOrders = false ) -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { h.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("HitBTC load config error", err) + } hitbtcConfig, err := cfg.GetExchangeConfig("HitBTC") if err != nil { - t.Error("Test Failed - HitBTC Setup() init error") + log.Fatal("HitBTC Setup() init error") } - hitbtcConfig.AuthenticatedWebsocketAPISupport = true - hitbtcConfig.AuthenticatedAPISupport = true - hitbtcConfig.APIKey = apiKey - hitbtcConfig.APISecret = apiSecret + hitbtcConfig.API.AuthenticatedSupport = true + hitbtcConfig.API.AuthenticatedWebsocketSupport = true + hitbtcConfig.API.Credentials.Key = apiKey + hitbtcConfig.API.Credentials.Secret = apiSecret - h.Setup(&hitbtcConfig) + err = h.Setup(hitbtcConfig) + if err != nil { + log.Fatal("HitBTC setup error", err) + } + + os.Exit(m.Run()) } func TestGetOrderbook(t *testing.T) { @@ -86,7 +95,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() h.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -97,16 +106,40 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } } -func TestGetFee(t *testing.T) { - h.SetDefaults() - TestSetup(t) +func TestUpdateTicker(t *testing.T) { + h.CurrencyPairs.StorePairs(asset.Spot, currency.NewPairsFromStrings([]string{"BTC-USD", "XRP-USD"}), true) + _, err := h.UpdateTicker(currency.NewPair(currency.BTC, currency.USD), asset.Spot) + if err != nil { + t.Error(err) + } + _, err = h.FetchTicker(currency.NewPair(currency.XRP, currency.USD), asset.Spot) + if err != nil { + t.Error(err) + } +} + +func TestGetAllTickers(t *testing.T) { + _, err := h.GetTickers() + if err != nil { + t.Error(err) + } +} + +func TestGetSingularTicker(t *testing.T) { + _, err := h.GetTicker("BTCUSD") + if err != nil { + t.Error(err) + } +} + +func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic if resp, err := h.GetFee(feeBuilder); resp != float64(0.002) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity @@ -114,7 +147,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := h.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) t.Error(err) } @@ -122,7 +155,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := h.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) t.Error(err) } @@ -130,7 +163,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -138,7 +171,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := h.GetFee(feeBuilder); resp != float64(0.042800) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.042800), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.042800), resp) t.Error(err) } @@ -147,7 +180,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err == nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } @@ -158,7 +191,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.BTC feeBuilder.Pair.Quote = currency.LTC if resp, err := h.GetFee(feeBuilder); resp != float64(0.0006) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0006), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0006), resp) t.Error(err) } @@ -166,7 +199,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -175,28 +208,22 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - h.SetDefaults() expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := h.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - h.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.ETH, currency.BTC)}, } @@ -209,11 +236,8 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - h.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.ETH, currency.BTC)}, } @@ -228,27 +252,26 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if h.APIKey != "" && h.APIKey != "Key" && - h.APISecret != "" && h.APISecret != "Secret" { - return true - } - return false + return h.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - h.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.DGD, - Quote: currency.BTC, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.DGD, + Quote: currency.BTC, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := h.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 10, "1234234") + response, err := h.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -257,16 +280,12 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - h.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -283,16 +302,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - h.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -308,26 +323,29 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestModifyOrder(t *testing.T) { - _, err := h.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := h.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - h.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.LTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -344,15 +362,11 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - h.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := h.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -360,15 +374,11 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - h.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := h.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -379,12 +389,12 @@ func TestGetDepositAddress(t *testing.T) { if areTestAPIKeysSet() { _, err := h.GetDepositAddress(currency.BTC, "") if err != nil { - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) } } else { _, err := h.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error cannot be nil") + t.Error("GetDepositAddress() error cannot be nil") } } } @@ -392,9 +402,7 @@ func setupWsAuth(t *testing.T) { if wsSetupRan { return } - TestSetDefaults(t) - TestSetup(t) - if !h.Websocket.IsEnabled() && !h.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + if !h.Websocket.IsEnabled() && !h.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } h.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() @@ -441,7 +449,10 @@ func TestWsPlaceOrder(t *testing.T) { if !canManipulateRealOrders { t.Skip("canManipulateRealOrders false, skipping test") } - _, err := h.wsPlaceOrder(currency.NewPair(currency.LTC, currency.BTC), exchange.BuyOrderSide.ToString(), 1, 1) + _, err := h.wsPlaceOrder(currency.NewPair(currency.LTC, currency.BTC), + order.Buy.String(), + 1, + 1) if err != nil { t.Fatal(err) } diff --git a/exchanges/hitbtc/hitbtc_types.go b/exchanges/hitbtc/hitbtc_types.go index 5adc456b..104645a1 100644 --- a/exchanges/hitbtc/hitbtc_types.go +++ b/exchanges/hitbtc/hitbtc_types.go @@ -6,32 +6,18 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" ) -// Ticker holds ticker information -type Ticker struct { - Last float64 - Ask float64 - Bid float64 - Timestamp time.Time - Volume float64 - VolumeQuote float64 - Symbol string - High float64 - Low float64 - Open float64 -} - // TickerResponse is the response type type TickerResponse struct { - Last string `json:"last"` // Last trade price - Ask string `json:"ask"` // Best ask price - Bid string `json:"bid"` // Best bid price - Timestamp time.Time `json:"timestamp"` // Last update or refresh ticker timestamp - Volume string `json:"volume"` // Total trading amount within 24 hours in base currency - VolumeQuote string `json:"volumeQuote"` // Total trading amount within 24 hours in quote currency + Ask float64 `json:"ask,string"` + Bid float64 `json:"bid,string"` + High float64 `json:"high,string"` + Last float64 `json:"last,string"` + Low float64 `json:"low,string"` + Open float64 `json:"open,string"` + Volume float64 `json:"volume,string"` + VolumeQuote float64 `json:"volumeQuote,string"` Symbol string `json:"symbol"` - High string `json:"high"` // Highest trade price within 24 hours - Low string `json:"low"` // Lowest trade price within 24 hours - Open string `json:"open"` // Last trade price 24 hours ago + Timestamp time.Time `json:"timestamp"` } // Symbol holds symbol data @@ -401,20 +387,20 @@ type WsActiveOrdersResponse struct { // WsActiveOrdersResponseData Active order data for WsActiveOrdersResponse type WsActiveOrdersResponseData struct { - ID string `json:"id"` - ClientOrderID string `json:"clientOrderId,omitempty"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - Status string `json:"status"` - Type string `json:"type"` - TimeInForce string `json:"timeInForce"` - Quantity float64 `json:"quantity,string"` - Price float64 `json:"price,string"` - CumQuantity float64 `json:"cumQuantity,string"` - PostOnly bool `json:"postOnly"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - ReportType string `json:"reportType"` + ID string `json:"id"` + ClientOrderID string `json:"clientOrderId,omitempty"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Status string `json:"status"` + Type string `json:"type"` + TimeInForce string `json:"timeInForce"` + Quantity float64 `json:"quantity,string"` + Price float64 `json:"price,string"` + CumQuantity float64 `json:"cumQuantity,string"` + PostOnly bool `json:"postOnly"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + ReportType string `json:"reportType"` } // WsReportResponse report response for auth subscription to reports @@ -425,24 +411,24 @@ type WsReportResponse struct { // WsReportResponseData Report data for WsReportResponse type WsReportResponseData struct { - ID string `json:"id"` - ClientOrderID string `json:"clientOrderId,omitempty"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - Status string `json:"status"` - Type string `json:"type"` - TimeInForce string `json:"timeInForce"` - Quantity float64 `json:"quantity,string"` - Price float64 `json:"price,string"` - CumQuantity float64 `json:"cumQuantity,string"` - PostOnly bool `json:"postOnly"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - ReportType string `json:"reportType"` - TradeQuantity float64 `json:"tradeQuantity,string"` - TradePrice float64 `json:"tradePrice,string"` - TradeID int64 `json:"tradeId"` - TradeFee float64 `json:"tradeFee,string"` + ID string `json:"id"` + ClientOrderID string `json:"clientOrderId,omitempty"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Status string `json:"status"` + Type string `json:"type"` + TimeInForce string `json:"timeInForce"` + Quantity float64 `json:"quantity,string"` + Price float64 `json:"price,string"` + CumQuantity float64 `json:"cumQuantity,string"` + PostOnly bool `json:"postOnly"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + ReportType string `json:"reportType"` + TradeQuantity float64 `json:"tradeQuantity,string"` + TradePrice float64 `json:"tradePrice,string"` + TradeID int64 `json:"tradeId"` + TradeFee float64 `json:"tradeFee,string"` } // WsSubmitOrderRequest WS request @@ -454,11 +440,11 @@ type WsSubmitOrderRequest struct { // WsSubmitOrderRequestData WS request data type WsSubmitOrderRequestData struct { - ClientOrderID int64 `json:"clientOrderId,string,omitempty"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - Price float64 `json:"price,string"` - Quantity float64 `json:"quantity,string"` + ClientOrderID int64 `json:"clientOrderId,string,omitempty"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Price float64 `json:"price,string"` + Quantity float64 `json:"quantity,string"` } // WsSubmitOrderSuccessResponse WS response @@ -508,20 +494,20 @@ type WsCancelOrderResponse struct { // WsCancelOrderResponseData WS response data type WsCancelOrderResponseData struct { - ID string `json:"id"` - ClientOrderID string `json:"clientOrderId,omitempty"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - Status string `json:"status"` - Type string `json:"type"` - TimeInForce string `json:"timeInForce"` - Quantity float64 `json:"quantity,string"` - Price float64 `json:"price,string"` - CumQuantity float64 `json:"cumQuantity,string"` - PostOnly bool `json:"postOnly"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - ReportType string `json:"reportType"` + ID string `json:"id"` + ClientOrderID string `json:"clientOrderId,omitempty"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Status string `json:"status"` + Type string `json:"type"` + TimeInForce string `json:"timeInForce"` + Quantity float64 `json:"quantity,string"` + Price float64 `json:"price,string"` + CumQuantity float64 `json:"cumQuantity,string"` + PostOnly bool `json:"postOnly"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + ReportType string `json:"reportType"` } // WsReplaceOrderResponse WS response @@ -533,21 +519,21 @@ type WsReplaceOrderResponse struct { // WsReplaceOrderResponseData WS response data type WsReplaceOrderResponseData struct { - ID string `json:"id"` - ClientOrderID string `json:"clientOrderId,omitempty"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - Status string `json:"status"` - Type string `json:"type"` - TimeInForce string `json:"timeInForce"` - Quantity float64 `json:"quantity,string"` - Price float64 `json:"price,string"` - CumQuantity float64 `json:"cumQuantity,string"` - PostOnly bool `json:"postOnly"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - ReportType string `json:"reportType"` - OriginalRequestClientOrderID string `json:"originalRequestClientOrderId"` + ID string `json:"id"` + ClientOrderID string `json:"clientOrderId,omitempty"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Status string `json:"status"` + Type string `json:"type"` + TimeInForce string `json:"timeInForce"` + Quantity float64 `json:"quantity,string"` + Price float64 `json:"price,string"` + CumQuantity float64 `json:"cumQuantity,string"` + PostOnly bool `json:"postOnly"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + ReportType string `json:"reportType"` + OriginalRequestClientOrderID string `json:"originalRequestClientOrderId"` } // WsGetActiveOrdersResponse WS response @@ -559,21 +545,21 @@ type WsGetActiveOrdersResponse struct { // WsGetActiveOrdersResponseData WS response data type WsGetActiveOrdersResponseData struct { - ID string `json:"id"` - ClientOrderID string `json:"clientOrderId,omitempty"` - Symbol currency.Pair `json:"symbol"` - Side string `json:"side"` - Status string `json:"status"` - Type string `json:"type"` - TimeInForce string `json:"timeInForce"` - Quantity float64 `json:"quantity,string"` - Price float64 `json:"price,string"` - CumQuantity float64 `json:"cumQuantity,string"` - PostOnly bool `json:"postOnly"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - ReportType string `json:"reportType"` - OriginalRequestClientOrderID string `json:"originalRequestClientOrderId"` + ID string `json:"id"` + ClientOrderID string `json:"clientOrderId,omitempty"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Status string `json:"status"` + Type string `json:"type"` + TimeInForce string `json:"timeInForce"` + Quantity float64 `json:"quantity,string"` + Price float64 `json:"price,string"` + CumQuantity float64 `json:"cumQuantity,string"` + PostOnly bool `json:"postOnly"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + ReportType string `json:"reportType"` + OriginalRequestClientOrderID string `json:"originalRequestClientOrderId"` } // WsGetTradingBalanceResponse WS response @@ -660,7 +646,7 @@ type WsGetSymbolsRequest struct { // WsGetSymbolsRequestParameters request parameters type WsGetSymbolsRequestParameters struct { - Symbol currency.Pair `json:"symbol"` + Symbol string `json:"symbol"` } // WsGetSymbolsResponse symbol response @@ -672,7 +658,7 @@ type WsGetSymbolsResponse struct { // WsGetSymbolsResponseData symbol response data type WsGetSymbolsResponseData struct { - ID currency.Pair `json:"id"` + ID string `json:"id"` BaseCurrency currency.Code `json:"baseCurrency"` QuoteCurrency currency.Code `json:"quoteCurrency"` QuantityIncrement float64 `json:"quantityIncrement,string"` @@ -691,10 +677,10 @@ type WsGetTradesRequest struct { // WsGetTradesRequestParameters trade request params type WsGetTradesRequestParameters struct { - Symbol currency.Pair `json:"symbol"` - Limit int64 `json:"limit"` - Sort string `json:"sort"` - By string `json:"by"` + Symbol string `json:"symbol"` + Limit int64 `json:"limit"` + Sort string `json:"sort"` + By string `json:"by"` } // WsGetTradesResponse response diff --git a/exchanges/hitbtc/hitbtc_websocket.go b/exchanges/hitbtc/hitbtc_websocket.go index 67703c1f..aa24a6dc 100644 --- a/exchanges/hitbtc/hitbtc_websocket.go +++ b/exchanges/hitbtc/hitbtc_websocket.go @@ -1,16 +1,19 @@ package hitbtc import ( + "encoding/json" "errors" "fmt" "net/http" + "strconv" "strings" "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/nonce" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" @@ -39,7 +42,7 @@ func (h *HitBTC) WsConnect() error { go h.WsHandleData() err = h.wsLogin() if err != nil { - log.Errorf("%v - authentication failed: %v", h.Name, err) + log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", h.Name, err) } h.GenerateDefaultSubscriptions() @@ -63,13 +66,13 @@ func (h *HitBTC) WsHandleData() { default: resp, err := h.WebsocketConn.ReadMessage() if err != nil { - h.Websocket.DataHandler <- err + h.Websocket.ReadMessageErrors <- err return } h.Websocket.TrafficAlert <- struct{}{} var init capture - err = common.JSONDecode(resp.Raw, &init) + err = json.Unmarshal(resp.Raw, &init) if err != nil { h.Websocket.DataHandler <- err continue @@ -103,7 +106,7 @@ func (h *HitBTC) handleSubscriptionUpdates(resp wshandler.WebsocketResponse, ini switch init.Method { case "ticker": var ticker WsTicker - err := common.JSONDecode(resp.Raw, &ticker) + err := json.Unmarshal(resp.Raw, &ticker) if err != nil { h.Websocket.DataHandler <- err return @@ -114,18 +117,23 @@ func (h *HitBTC) handleSubscriptionUpdates(resp wshandler.WebsocketResponse, ini return } h.Websocket.DataHandler <- wshandler.TickerData{ - Exchange: h.GetName(), - AssetType: orderbook.Spot, - Pair: currency.NewPairFromString(ticker.Params.Symbol), - Quantity: ticker.Params.Volume, - Timestamp: ts, - OpenPrice: ticker.Params.Open, - HighPrice: ticker.Params.High, - LowPrice: ticker.Params.Low, + Exchange: h.Name, + Open: ticker.Params.Open, + Volume: ticker.Params.Volume, + QuoteVolume: ticker.Params.VolumeQuote, + High: ticker.Params.High, + Low: ticker.Params.Low, + Bid: ticker.Params.Bid, + Ask: ticker.Params.Ask, + Last: ticker.Params.Last, + Timestamp: ts, + AssetType: asset.Spot, + Pair: currency.NewPairFromFormattedPairs(ticker.Params.Symbol, + h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)), } case "snapshotOrderbook": var obSnapshot WsOrderbook - err := common.JSONDecode(resp.Raw, &obSnapshot) + err := json.Unmarshal(resp.Raw, &obSnapshot) if err != nil { h.Websocket.DataHandler <- err } @@ -135,33 +143,33 @@ func (h *HitBTC) handleSubscriptionUpdates(resp wshandler.WebsocketResponse, ini } case "updateOrderbook": var obUpdate WsOrderbook - err := common.JSONDecode(resp.Raw, &obUpdate) + err := json.Unmarshal(resp.Raw, &obUpdate) if err != nil { h.Websocket.DataHandler <- err } h.WsProcessOrderbookUpdate(obUpdate) case "snapshotTrades": var tradeSnapshot WsTrade - err := common.JSONDecode(resp.Raw, &tradeSnapshot) + err := json.Unmarshal(resp.Raw, &tradeSnapshot) if err != nil { h.Websocket.DataHandler <- err } case "updateTrades": var tradeUpdates WsTrade - err := common.JSONDecode(resp.Raw, &tradeUpdates) + err := json.Unmarshal(resp.Raw, &tradeUpdates) if err != nil { h.Websocket.DataHandler <- err } case "activeOrders": var activeOrders WsActiveOrdersResponse - err := common.JSONDecode(resp.Raw, &activeOrders) + err := json.Unmarshal(resp.Raw, &activeOrders) if err != nil { h.Websocket.DataHandler <- err } h.Websocket.DataHandler <- activeOrders case "report": var reportData WsReportResponse - err := common.JSONDecode(resp.Raw, &reportData) + err := json.Unmarshal(resp.Raw, &reportData) if err != nil { h.Websocket.DataHandler <- err } @@ -175,21 +183,21 @@ func (h *HitBTC) handleCommandResponses(resp wshandler.WebsocketResponse, init c switch resultType["reportType"].(string) { case "new": var response WsSubmitOrderSuccessResponse - err := common.JSONDecode(resp.Raw, &response) + err := json.Unmarshal(resp.Raw, &response) if err != nil { h.Websocket.DataHandler <- err } h.Websocket.DataHandler <- response case "canceled": var response WsCancelOrderResponse - err := common.JSONDecode(resp.Raw, &response) + err := json.Unmarshal(resp.Raw, &response) if err != nil { h.Websocket.DataHandler <- err } h.Websocket.DataHandler <- response case "replaced": var response WsReplaceOrderResponse - err := common.JSONDecode(resp.Raw, &response) + err := json.Unmarshal(resp.Raw, &response) if err != nil { h.Websocket.DataHandler <- err } @@ -203,14 +211,14 @@ func (h *HitBTC) handleCommandResponses(resp wshandler.WebsocketResponse, init c data := resultType[0].(map[string]interface{}) if _, ok := data["clientOrderId"]; ok { var response WsActiveOrdersResponse - err := common.JSONDecode(resp.Raw, &response) + err := json.Unmarshal(resp.Raw, &response) if err != nil { h.Websocket.DataHandler <- err } h.Websocket.DataHandler <- response } else if _, ok := data["available"]; ok { var response WsGetTradingBalanceResponse - err := common.JSONDecode(resp.Raw, &response) + err := json.Unmarshal(resp.Raw, &response) if err != nil { h.Websocket.DataHandler <- err } @@ -225,32 +233,35 @@ func (h *HitBTC) WsProcessOrderbookSnapshot(ob WsOrderbook) error { return errors.New("hitbtc.go error - no orderbooks to process") } - var bids []orderbook.Item - for i := range ob.Params.Bid { - bids = append(bids, orderbook.Item{Amount: ob.Params.Bid[i].Size, Price: ob.Params.Bid[i].Price}) - } - - var asks []orderbook.Item - for i := range ob.Params.Ask { - asks = append(asks, orderbook.Item{Amount: ob.Params.Ask[i].Size, Price: ob.Params.Ask[i].Price}) - } - - p := currency.NewPairFromString(ob.Params.Symbol) - var newOrderBook orderbook.Base - newOrderBook.Asks = asks - newOrderBook.Bids = bids - newOrderBook.AssetType = orderbook.Spot - newOrderBook.Pair = p + for i := range ob.Params.Bid { + newOrderBook.Bids = append(newOrderBook.Bids, orderbook.Item{ + Amount: ob.Params.Bid[i].Size, + Price: ob.Params.Bid[i].Price, + }) + } - err := h.Websocket.Orderbook.LoadSnapshot(&newOrderBook, false) + for i := range ob.Params.Ask { + newOrderBook.Asks = append(newOrderBook.Asks, orderbook.Item{ + Amount: ob.Params.Ask[i].Size, + Price: ob.Params.Ask[i].Price, + }) + } + + p := currency.NewPairFromFormattedPairs(ob.Params.Symbol, + h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)) + newOrderBook.AssetType = asset.Spot + newOrderBook.Pair = p + newOrderBook.ExchangeName = h.Name + + err := h.Websocket.Orderbook.LoadSnapshot(&newOrderBook) if err != nil { return err } h.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Exchange: h.GetName(), - Asset: orderbook.Spot, + Exchange: h.Name, + Asset: asset.Spot, Pair: p, } @@ -265,28 +276,35 @@ func (h *HitBTC) WsProcessOrderbookUpdate(update WsOrderbook) error { var bids, asks []orderbook.Item for i := range update.Params.Bid { - bids = append(bids, orderbook.Item{Price: update.Params.Bid[i].Price, Amount: update.Params.Bid[i].Size}) + bids = append(bids, orderbook.Item{ + Price: update.Params.Bid[i].Price, + Amount: update.Params.Bid[i].Size, + }) } for i := range update.Params.Ask { - asks = append(asks, orderbook.Item{Price: update.Params.Ask[i].Price, Amount: update.Params.Ask[i].Size}) + asks = append(asks, orderbook.Item{ + Price: update.Params.Ask[i].Price, + Amount: update.Params.Ask[i].Size, + }) } - p := currency.NewPairFromString(update.Params.Symbol) + p := currency.NewPairFromFormattedPairs(update.Params.Symbol, + h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)) err := h.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{ - Asks: asks, - Bids: bids, - CurrencyPair: p, - UpdateID: update.Params.Sequence, - AssetType: orderbook.Spot, + Asks: asks, + Bids: bids, + Pair: p, + UpdateID: update.Params.Sequence, + Asset: asset.Spot, }) if err != nil { return err } h.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Exchange: h.GetName(), - Asset: orderbook.Spot, + Exchange: h.Name, + Asset: asset.Spot, Pair: p, } return nil @@ -301,7 +319,7 @@ func (h *HitBTC) GenerateDefaultSubscriptions() { Channel: "subscribeReports", }) } - enabledCurrencies := h.GetEnabledCurrencies() + enabledCurrencies := h.GetEnabledPairs(asset.Spot) for i := range channels { for j := range enabledCurrencies { enabledCurrencies[j].Delimiter = "" @@ -321,17 +339,20 @@ func (h *HitBTC) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscrip } if channelToSubscribe.Currency.String() != "" { subscribe.Params = params{ - Symbol: channelToSubscribe.Currency.String(), + Symbol: h.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String(), } } if strings.EqualFold(channelToSubscribe.Channel, "subscribeTrades") { subscribe.Params = params{ - Symbol: channelToSubscribe.Currency.String(), - Limit: 100, + Symbol: h.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String(), + Limit: 100, } } else if strings.EqualFold(channelToSubscribe.Channel, "subscribeCandles") { subscribe.Params = params{ - Symbol: channelToSubscribe.Currency.String(), + Symbol: h.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String(), Period: "M30", Limit: 100, } @@ -347,17 +368,20 @@ func (h *HitBTC) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscr JSONRPCVersion: rpcVersion, Method: unsubscribeChannel, Params: params{ - Symbol: channelToSubscribe.Currency.String(), + Symbol: h.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String(), }, } if strings.EqualFold(unsubscribeChannel, "unsubscribeTrades") { subscribe.Params = params{ - Symbol: channelToSubscribe.Currency.String(), - Limit: 100, + Symbol: h.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String(), + Limit: 100, } } else if strings.EqualFold(unsubscribeChannel, "unsubscribeCandles") { subscribe.Params = params{ - Symbol: channelToSubscribe.Currency.String(), + Symbol: h.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String(), Period: "M30", Limit: 100, } @@ -372,15 +396,15 @@ func (h *HitBTC) wsLogin() error { return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", h.Name) } h.Websocket.SetCanUseAuthenticatedEndpoints(true) - nonce := fmt.Sprintf("%v", time.Now().Unix()) - hmac := common.GetHMAC(common.HashSHA256, []byte(nonce), []byte(h.APISecret)) + nonce := strconv.FormatInt(time.Now().Unix(), 10) + hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(nonce), []byte(h.API.Credentials.Secret)) request := WsLoginRequest{ Method: "login", Params: WsLoginData{ Algo: "HS256", - PKey: h.APIKey, + PKey: h.API.Credentials.Key, Nonce: nonce, - Signature: common.HexEncodeToString(hmac), + Signature: crypto.HexEncodeToString(hmac), }, } @@ -402,8 +426,8 @@ func (h *HitBTC) wsPlaceOrder(pair currency.Pair, side string, price, quantity f Method: "newOrder", Params: WsSubmitOrderRequestData{ ClientOrderID: id, - Symbol: pair, - Side: common.StringToLower(side), + Symbol: h.FormatExchangeCurrency(pair, asset.Spot).String(), + Side: strings.ToLower(side), Price: price, Quantity: quantity, }, @@ -414,7 +438,7 @@ func (h *HitBTC) wsPlaceOrder(pair currency.Pair, side string, price, quantity f return nil, fmt.Errorf("%v %v", h.Name, err) } var response WsSubmitOrderSuccessResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, fmt.Errorf("%v %v", h.Name, err) } @@ -441,7 +465,7 @@ func (h *HitBTC) wsCancelOrder(clientOrderID string) (*WsCancelOrderResponse, er return nil, fmt.Errorf("%v %v", h.Name, err) } var response WsCancelOrderResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, fmt.Errorf("%v %v", h.Name, err) } @@ -460,7 +484,7 @@ func (h *HitBTC) wsReplaceOrder(clientOrderID string, quantity, price float64) ( Method: "cancelReplaceOrder", Params: WsReplaceOrderRequestData{ ClientOrderID: clientOrderID, - RequestClientID: fmt.Sprintf("%v", time.Now().Unix()), + RequestClientID: strconv.FormatInt(time.Now().Unix(), 10), Quantity: quantity, Price: price, }, @@ -471,7 +495,7 @@ func (h *HitBTC) wsReplaceOrder(clientOrderID string, quantity, price float64) ( return nil, fmt.Errorf("%v %v", h.Name, err) } var response WsReplaceOrderResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, fmt.Errorf("%v %v", h.Name, err) } @@ -496,7 +520,7 @@ func (h *HitBTC) wsGetActiveOrders() (*WsActiveOrdersResponse, error) { return nil, fmt.Errorf("%v %v", h.Name, err) } var response WsActiveOrdersResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, fmt.Errorf("%v %v", h.Name, err) } @@ -521,7 +545,7 @@ func (h *HitBTC) wsGetTradingBalance() (*WsGetTradingBalanceResponse, error) { return nil, fmt.Errorf("%v %v", h.Name, err) } var response WsGetTradingBalanceResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, fmt.Errorf("%v %v", h.Name, err) } @@ -545,7 +569,7 @@ func (h *HitBTC) wsGetCurrencies(currencyItem currency.Code) (*WsGetCurrenciesRe return nil, fmt.Errorf("%v %v", h.Name, err) } var response WsGetCurrenciesResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, fmt.Errorf("%v %v", h.Name, err) } @@ -560,7 +584,7 @@ func (h *HitBTC) wsGetSymbols(currencyItem currency.Pair) (*WsGetSymbolsResponse request := WsGetSymbolsRequest{ Method: "getSymbol", Params: WsGetSymbolsRequestParameters{ - Symbol: currencyItem, + Symbol: h.FormatExchangeCurrency(currencyItem, asset.Spot).String(), }, ID: h.WebsocketConn.GenerateMessageID(false), } @@ -569,7 +593,7 @@ func (h *HitBTC) wsGetSymbols(currencyItem currency.Pair) (*WsGetSymbolsResponse return nil, fmt.Errorf("%v %v", h.Name, err) } var response WsGetSymbolsResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, fmt.Errorf("%v %v", h.Name, err) } @@ -584,7 +608,7 @@ func (h *HitBTC) wsGetTrades(currencyItem currency.Pair, limit int64, sort, by s request := WsGetTradesRequest{ Method: "getTrades", Params: WsGetTradesRequestParameters{ - Symbol: currencyItem, + Symbol: h.FormatExchangeCurrency(currencyItem, asset.Spot).String(), Limit: limit, Sort: sort, By: by, @@ -596,7 +620,7 @@ func (h *HitBTC) wsGetTrades(currencyItem currency.Pair, limit int64, sort, by s return nil, fmt.Errorf("%v %v", h.Name, err) } var response WsGetTradesResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, fmt.Errorf("%v %v", h.Name, err) } diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 105b3647..3acdf45d 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -6,16 +6,174 @@ import ( "strconv" "strings" "sync" + "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (h *HitBTC) GetDefaultConfig() (*config.ExchangeConfig, error) { + h.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = h.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = h.BaseCurrencies + + err := h.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if h.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = h.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets default settings for hitbtc +func (h *HitBTC) SetDefaults() { + h.Name = "HitBTC" + h.Enabled = true + h.Verbose = true + h.API.CredentialsValidator.RequiresKey = true + h.API.CredentialsValidator.RequiresSecret = true + + h.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + } + + h.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + ModifyOrder: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + CryptoDepositFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + SubmitOrder: true, + CancelOrder: true, + MessageSequenceNumbers: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + h.Requester = request.New(h.Name, + request.NewRateLimit(time.Second, hitbtcAuthRate), + request.NewRateLimit(time.Second, hitbtcUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + h.API.Endpoints.URLDefault = apiURL + h.API.Endpoints.URL = h.API.Endpoints.URLDefault + h.API.Endpoints.WebsocketURL = hitbtcWebsocketAddress + h.Websocket = wshandler.New() + h.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + h.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + h.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup sets user exchange configuration settings +func (h *HitBTC) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + h.SetEnabled(false) + return nil + } + + err := h.SetupDefaults(exch) + if err != nil { + return err + } + + err = h.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: hitbtcWebsocketAddress, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: h.WsConnect, + Subscriber: h.Subscribe, + UnSubscriber: h.Unsubscribe, + Features: &h.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + h.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: h.Name, + URL: h.Websocket.GetWebsocketURL(), + ProxyURL: h.Websocket.GetProxyAddress(), + Verbose: h.Verbose, + RateLimit: rateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + h.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + true, + true, + true, + false, + exch.Name) + return nil +} + // Start starts the HitBTC go routine func (h *HitBTC) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -28,114 +186,144 @@ func (h *HitBTC) Start(wg *sync.WaitGroup) { // Run implements the HitBTC wrapper func (h *HitBTC) Run() { if h.Verbose { - log.Debugf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket.IsEnabled()), hitbtcWebsocketAddress) - log.Debugf("%s polling delay: %ds.\n", h.GetName(), h.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", h.GetName(), len(h.EnabledPairs), h.EnabledPairs) + log.Debugf(log.ExchangeSys, "%s Websocket: %s (url: %s).\n", h.Name, common.IsEnabled(h.Websocket.IsEnabled()), hitbtcWebsocketAddress) + h.PrintEnabledPairs() } - exchangeProducts, err := h.GetSymbolsDetailed() - if err != nil { - log.Errorf("%s Failed to get available symbols.\n", h.GetName()) - } else { - forceUpgrade := false - if !common.StringDataContains(h.EnabledPairs.Strings(), "-") || - !common.StringDataContains(h.AvailablePairs.Strings(), "-") { - forceUpgrade = true - } - var currencies []string - for x := range exchangeProducts { - currencies = append(currencies, exchangeProducts[x].BaseCurrency+"-"+exchangeProducts[x].QuoteCurrency) - } + forceUpdate := false + if !common.StringDataContains(h.GetEnabledPairs(asset.Spot).Strings(), "-") || + !common.StringDataContains(h.GetAvailablePairs(asset.Spot).Strings(), "-") { + enabledPairs := []string{"BTC-USD"} + log.Warn(log.ExchangeSys, "Available pairs for HitBTC reset due to config upgrade, please enable the ones you would like again.") + forceUpdate = true - if forceUpgrade { - enabledPairs := currency.Pairs{currency.Pair{Base: currency.BTC, - Quote: currency.USD, Delimiter: "-"}} - - log.Warn("Available pairs for HitBTC reset due to config upgrade, please enable the ones you would like again.") - - err = h.UpdateCurrencies(enabledPairs, true, true) - if err != nil { - log.Errorf("%s Failed to update enabled currencies.\n", h.GetName()) - } - } - - var newCurrencies currency.Pairs - for _, p := range currencies { - newCurrencies = append(newCurrencies, - currency.NewPairFromString(p)) - } - - err = h.UpdateCurrencies(newCurrencies, false, forceUpgrade) + err := h.UpdatePairs(currency.NewPairsFromStrings(enabledPairs), asset.Spot, true, true) if err != nil { - log.Errorf("%s Failed to update available currencies.\n", h.GetName()) + log.Errorf(log.ExchangeSys, "%s failed to update enabled currencies.\n", h.Name) } } + + if !h.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { + return + } + + err := h.UpdateTradablePairs(forceUpdate) + if err != nil { + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", h.Name, err) + } } -// UpdateTicker updates and returns the ticker for a currency pair -func (h *HitBTC) UpdateTicker(currencyPair currency.Pair, assetType string) (ticker.Price, error) { - tick, err := h.GetTicker("") +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (h *HitBTC) FetchTradablePairs(asset asset.Item) ([]string, error) { + symbols, err := h.GetSymbolsDetailed() if err != nil { - return ticker.Price{}, err + return nil, err } - for _, x := range h.GetEnabledCurrencies() { - var tp ticker.Price - curr := exchange.FormatExchangeCurrency(h.GetName(), x).String() - tp.Pair = x - tp.Ask = tick[curr].Ask - tp.Bid = tick[curr].Bid - tp.High = tick[curr].High - tp.Last = tick[curr].Last - tp.Low = tick[curr].Low - tp.Volume = tick[curr].Volume + var pairs []string + for x := range symbols { + pairs = append(pairs, symbols[x].BaseCurrency+ + h.GetPairFormat(asset, false).Delimiter+symbols[x].QuoteCurrency) + } + return pairs, nil +} - err = ticker.ProcessTicker(h.GetName(), &tp, assetType) - if err != nil { - return ticker.Price{}, err +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (h *HitBTC) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := h.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return h.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + +// UpdateTicker updates and returns the ticker for a currency pair +func (h *HitBTC) UpdateTicker(currencyPair currency.Pair, assetType asset.Item) (ticker.Price, error) { + var tickerPrice ticker.Price + tick, err := h.GetTickers() + if err != nil { + return tickerPrice, err + } + pairs := h.GetEnabledPairs(assetType) + for i := range pairs { + for j := range tick { + pairFmt := h.FormatExchangeCurrency(pairs[i], assetType).String() + if tick[j].Symbol != pairFmt { + found := false + if strings.Contains(tick[j].Symbol, "USDT") { + if pairFmt == tick[j].Symbol[0:len(tick[j].Symbol)-1] { + found = true + } + } + if !found { + continue + } + } + tickerPrice := ticker.Price{ + Last: tick[j].Last, + High: tick[j].High, + Low: tick[j].Low, + Bid: tick[j].Bid, + Ask: tick[j].Ask, + Volume: tick[j].Volume, + QuoteVolume: tick[j].VolumeQuote, + Open: tick[j].Open, + Pair: pairs[i], + LastUpdated: tick[j].Timestamp, + } + err = ticker.ProcessTicker(h.Name, &tickerPrice, assetType) + if err != nil { + log.Error(log.Ticker, err) + } } } return ticker.GetTicker(h.Name, currencyPair, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (h *HitBTC) GetTickerPrice(currencyPair currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(h.GetName(), currencyPair, assetType) +// FetchTicker returns the ticker for a currency pair +func (h *HitBTC) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(h.Name, p, assetType) if err != nil { - return h.UpdateTicker(currencyPair, assetType) + return h.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (h *HitBTC) GetOrderbookEx(currencyPair currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(h.GetName(), currencyPair, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (h *HitBTC) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(h.Name, p, assetType) if err != nil { - return h.UpdateOrderbook(currencyPair, assetType) + return h.UpdateOrderbook(p, assetType) } return ob, nil } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (h *HitBTC) UpdateOrderbook(currencyPair currency.Pair, assetType string) (orderbook.Base, error) { +func (h *HitBTC) UpdateOrderbook(currencyPair currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := h.GetOrderbook(exchange.FormatExchangeCurrency(h.GetName(), currencyPair).String(), 1000) + orderbookNew, err := h.GetOrderbook(h.FormatExchangeCurrency(currencyPair, assetType).String(), 1000) if err != nil { return orderBook, err } for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data.Amount, Price: data.Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[x].Amount, + Price: orderbookNew.Bids[x].Price, + }) } for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data.Amount, Price: data.Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[x].Amount, + Price: orderbookNew.Asks[x].Price, + }) } orderBook.Pair = currencyPair - orderBook.ExchangeName = h.GetName() + orderBook.ExchangeName = h.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -150,24 +338,25 @@ func (h *HitBTC) UpdateOrderbook(currencyPair currency.Pair, assetType string) ( // HitBTC exchange func (h *HitBTC) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = h.GetName() + response.Exchange = h.Name accountBalance, err := h.GetBalances() if err != nil { return response, err } var currencies []exchange.AccountCurrencyInfo - for _, item := range accountBalance { + for i := range accountBalance { var exchangeCurrency exchange.AccountCurrencyInfo - exchangeCurrency.CurrencyName = currency.NewCode(item.Currency) - exchangeCurrency.TotalValue = item.Available - exchangeCurrency.Hold = item.Reserved + exchangeCurrency.CurrencyName = currency.NewCode(accountBalance[i].Currency) + exchangeCurrency.TotalValue = accountBalance[i].Available + exchangeCurrency.Hold = accountBalance[i].Reserved currencies = append(currencies, exchangeCurrency) } - response.Accounts = append(response.Accounts, exchange.Account{ - Currencies: currencies, - }) + response.Accounts = append(response.Accounts, + exchange.Account{ + Currencies: currencies, + }) return response, nil } @@ -175,60 +364,74 @@ func (h *HitBTC) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (h *HitBTC) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (h *HitBTC) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (h *HitBTC) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (h *HitBTC) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - response, err := h.PlaceOrder(p.String(), - price, - amount, - common.StringToLower(orderType.ToString()), - common.StringToLower(side.ToString())) - - if response.OrderNumber > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response.OrderNumber) +func (h *HitBTC) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + err := o.Validate() + if err != nil { + return submitOrderResponse, err } - - if err == nil { - submitOrderResponse.IsOrderPlaced = true + if h.Websocket.IsConnected() && h.Websocket.CanUseAuthenticatedEndpoints() { + var response *WsSubmitOrderSuccessResponse + response, err = h.wsPlaceOrder(o.Pair, o.OrderSide.String(), o.Amount, o.Price) + if err != nil { + return submitOrderResponse, err + } + submitOrderResponse.OrderID = strconv.FormatInt(response.ID, 10) + if response.Result.CumQuantity == o.Amount { + submitOrderResponse.FullyMatched = true + } + } else { + var response OrderResponse + response, err = h.PlaceOrder(o.Pair.String(), + o.Price, + o.Amount, + strings.ToLower(o.OrderType.String()), + strings.ToLower(o.OrderSide.String())) + if err != nil { + return submitOrderResponse, err + } + if response.OrderNumber > 0 { + submitOrderResponse.OrderID = strconv.FormatInt(response.OrderNumber, 10) + } + if o.OrderType == order.Market { + submitOrderResponse.FullyMatched = true + } } + submitOrderResponse.IsOrderPlaced = true - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (h *HitBTC) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (h *HitBTC) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (h *HitBTC) CancelOrder(order *exchange.OrderCancellation) error { +func (h *HitBTC) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) - if err != nil { return err } _, err = h.CancelExistingOrder(orderIDInt) - return err } // CancelAllOrders cancels all orders associated with a currency pair -func (h *HitBTC) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (h *HitBTC) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } resp, err := h.CancelAllExistingOrders() @@ -238,7 +441,7 @@ func (h *HitBTC) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cancel for i := range resp { if resp[i].Status != "canceled" { - cancelAllOrdersResponse.OrderStatus[strconv.FormatInt(resp[i].ID, 10)] = + cancelAllOrdersResponse.Status[strconv.FormatInt(resp[i].ID, 10)] = fmt.Sprintf("Could not cancel order %v. Status: %v", resp[i].ID, resp[i].Status) @@ -249,8 +452,8 @@ func (h *HitBTC) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cancel } // GetOrderInfo returns information on a current open order -func (h *HitBTC) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (h *HitBTC) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -266,21 +469,20 @@ func (h *HitBTC) GetDepositAddress(currency currency.Code, _ string) (string, er // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (h *HitBTC) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (h *HitBTC) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { _, err := h.Withdraw(withdrawRequest.Currency.String(), withdrawRequest.Address, withdrawRequest.Amount) - return "", err } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (h *HitBTC) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (h *HitBTC) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (h *HitBTC) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (h *HitBTC) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -291,7 +493,7 @@ func (h *HitBTC) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (h *HitBTC) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (h.APIKey == "" || h.APISecret == "") && // Todo check connection status + if !h.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -299,26 +501,26 @@ func (h *HitBTC) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) } // GetActiveOrders retrieves any orders that are active/open -func (h *HitBTC) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - if len(getOrdersRequest.Currencies) == 0 { +func (h *HitBTC) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { + if len(req.Currencies) == 0 { return nil, errors.New("currency must be supplied") } var allOrders []OrderHistoryResponse - for _, currency := range getOrdersRequest.Currencies { - resp, err := h.GetOpenOrders(currency.String()) + for i := range req.Currencies { + resp, err := h.GetOpenOrders(req.Currencies[i].String()) if err != nil { return nil, err } allOrders = append(allOrders, resp...) } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range allOrders { symbol := currency.NewPairDelimiter(allOrders[i].Symbol, - h.ConfigCurrencyPairFormat.Delimiter) - side := exchange.OrderSide(strings.ToUpper(allOrders[i].Side)) - orders = append(orders, exchange.OrderDetail{ + h.GetPairFormat(asset.Spot, false).Delimiter) + side := order.Side(strings.ToUpper(allOrders[i].Side)) + orders = append(orders, order.Detail{ ID: allOrders[i].ID, Amount: allOrders[i].Quantity, Exchange: h.Name, @@ -329,35 +531,33 @@ func (h *HitBTC) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([ }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (h *HitBTC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - if len(getOrdersRequest.Currencies) == 0 { +func (h *HitBTC) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + if len(req.Currencies) == 0 { return nil, errors.New("currency must be supplied") } var allOrders []OrderHistoryResponse - for _, currency := range getOrdersRequest.Currencies { - resp, err := h.GetOrders(currency.String()) + for i := range req.Currencies { + resp, err := h.GetOrders(req.Currencies[i].String()) if err != nil { return nil, err } allOrders = append(allOrders, resp...) } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range allOrders { symbol := currency.NewPairDelimiter(allOrders[i].Symbol, - h.ConfigCurrencyPairFormat.Delimiter) - side := exchange.OrderSide(strings.ToUpper(allOrders[i].Side)) - orders = append(orders, exchange.OrderDetail{ + h.GetPairFormat(asset.Spot, false).Delimiter) + side := order.Side(strings.ToUpper(allOrders[i].Side)) + orders = append(orders, order.Detail{ ID: allOrders[i].ID, Amount: allOrders[i].Quantity, Exchange: h.Name, @@ -368,9 +568,8 @@ func (h *HitBTC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } diff --git a/exchanges/huobi/README.md b/exchanges/huobi/README.md index fbc41a7d..6ef6ed77 100644 --- a/exchanges/huobi/README.md +++ b/exchanges/huobi/README.md @@ -47,22 +47,22 @@ main.go ```go var h exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Huobi" { - h = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Huobi" { + h = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := h.GetTickerPrice() +tick, err := h.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := h.GetOrderbookEx() +ob, err := h.FetchOrderbook() if err != nil { // Handle error } @@ -137,4 +137,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/huobi/huobi.go b/exchanges/huobi/huobi.go index 6a5afa82..22121506 100644 --- a/exchanges/huobi/huobi.go +++ b/exchanges/huobi/huobi.go @@ -17,13 +17,10 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( @@ -35,6 +32,7 @@ const ( huobiMarketDetailMerged = "market/detail/merged" huobiMarketDepth = "market/depth" huobiMarketTrade = "market/trade" + huobiMarketTickers = "market/tickers" huobiMarketTradeHistory = "market/history/trade" huobiSymbols = "common/symbols" huobiCurrencies = "common/currencys" @@ -46,7 +44,7 @@ const ( huobiOrderCancel = "order/orders/%s/submitcancel" huobiOrderCancelBatch = "order/orders/batchcancel" huobiBatchCancelOpenOrders = "order/orders/batchCancelOpenOrders" - huobiGetOrder = "order/orders/%s" + huobiGetOrder = "order/orders/getClientOrder" huobiGetOrderMatch = "order/orders/%s/matchresults" huobiGetOrders = "order/orders" huobiGetOpenOrders = "order/openOrders" @@ -72,122 +70,6 @@ type HUOBI struct { AuthenticatedWebsocketConn *wshandler.WebsocketConnection } -// SetDefaults sets default values for the exchange -func (h *HUOBI) SetDefaults() { - h.Name = "Huobi" - h.Enabled = false - h.Fee = 0 - h.Verbose = false - h.RESTPollingDelay = 10 - h.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithSetup | - exchange.NoFiatWithdrawals - h.RequestCurrencyPairFormat.Delimiter = "" - h.RequestCurrencyPairFormat.Uppercase = false - h.ConfigCurrencyPairFormat.Delimiter = "-" - h.ConfigCurrencyPairFormat.Uppercase = true - h.AssetTypes = []string{ticker.Spot} - h.SupportsAutoPairUpdating = true - h.SupportsRESTTickerBatching = false - h.Requester = request.New(h.Name, - request.NewRateLimit(time.Second*10, huobiAuthRate), - request.NewRateLimit(time.Second*10, huobiUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - h.APIUrlDefault = huobiAPIURL - h.APIUrl = h.APIUrlDefault - h.Websocket = wshandler.New() - h.Websocket.Functionality = wshandler.WebsocketKlineSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported | - wshandler.WebsocketAccountDataSupported | - wshandler.WebsocketMessageCorrelationSupported - h.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - h.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - h.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup sets user configuration -func (h *HUOBI) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - h.SetEnabled(false) - } else { - h.Enabled = true - h.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - h.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - h.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - h.APIAuthPEMKeySupport = exch.APIAuthPEMKeySupport - h.APIAuthPEMKey = exch.APIAuthPEMKey - h.SetHTTPClientTimeout(exch.HTTPTimeout) - h.SetHTTPClientUserAgent(exch.HTTPUserAgent) - h.RESTPollingDelay = exch.RESTPollingDelay - h.Verbose = exch.Verbose - h.HTTPDebugging = exch.HTTPDebugging - h.Websocket.SetWsStatusAndConnection(exch.Websocket) - h.BaseCurrencies = exch.BaseCurrencies - h.AvailablePairs = exch.AvailablePairs - h.EnabledPairs = exch.EnabledPairs - err := h.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = h.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = h.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = h.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = h.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = h.Websocket.Setup(h.WsConnect, - h.Subscribe, - h.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - wsMarketURL, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - h.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: h.Name, - URL: wsMarketURL, - ProxyURL: h.Websocket.GetProxyAddress(), - Verbose: h.Verbose, - RateLimit: rateLimit, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - h.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: h.Name, - URL: wsAccountsOrdersURL, - ProxyURL: h.Websocket.GetProxyAddress(), - Verbose: h.Verbose, - RateLimit: rateLimit, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - h.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - false, - false, - false, - false, - exch.Name) - } -} - // GetSpotKline returns kline data // KlinesRequestParams contains symbol, period and size func (h *HUOBI) GetSpotKline(arg KlinesRequestParams) ([]KlineItem, error) { @@ -205,7 +87,7 @@ func (h *HUOBI) GetSpotKline(arg KlinesRequestParams) ([]KlineItem, error) { } var result response - urlPath := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketHistoryKline) + urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketHistoryKline) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { @@ -214,6 +96,13 @@ func (h *HUOBI) GetSpotKline(arg KlinesRequestParams) ([]KlineItem, error) { return result.Data, err } +// GetTickers returns the ticker for the specified symbol +func (h *HUOBI) GetTickers() (Tickers, error) { + var result Tickers + urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketTickers) + return result, h.SendHTTPRequest(urlPath, &result) +} + // GetMarketDetailMerged returns the ticker for the specified symbol func (h *HUOBI) GetMarketDetailMerged(symbol string) (DetailMerged, error) { vals := url.Values{} @@ -225,7 +114,7 @@ func (h *HUOBI) GetMarketDetailMerged(symbol string) (DetailMerged, error) { } var result response - urlPath := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketDetailMerged) + urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketDetailMerged) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { @@ -249,7 +138,7 @@ func (h *HUOBI) GetDepth(obd OrderBookDataRequestParams) (Orderbook, error) { } var result response - urlPath := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketDepth) + urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketDepth) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { @@ -271,7 +160,7 @@ func (h *HUOBI) GetTrades(symbol string) ([]Trade, error) { } var result response - urlPath := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketTrade) + urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketTrade) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { @@ -311,7 +200,7 @@ func (h *HUOBI) GetTradeHistory(symbol, size string) ([]TradeHistory, error) { } var result response - urlPath := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketTradeHistory) + urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketTradeHistory) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { @@ -331,7 +220,7 @@ func (h *HUOBI) GetMarketDetail(symbol string) (Detail, error) { } var result response - urlPath := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketDetail) + urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketDetail) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { @@ -348,7 +237,7 @@ func (h *HUOBI) GetSymbols() ([]Symbol, error) { } var result response - urlPath := fmt.Sprintf("%s/v%s/%s", h.APIUrl, huobiAPIVersion, huobiSymbols) + urlPath := fmt.Sprintf("%s/v%s/%s", h.API.Endpoints.URL, huobiAPIVersion, huobiSymbols) err := h.SendHTTPRequest(urlPath, &result) if result.ErrorMessage != "" { @@ -365,7 +254,7 @@ func (h *HUOBI) GetCurrencies() ([]string, error) { } var result response - urlPath := fmt.Sprintf("%s/v%s/%s", h.APIUrl, huobiAPIVersion, huobiCurrencies) + urlPath := fmt.Sprintf("%s/v%s/%s", h.API.Endpoints.URL, huobiAPIVersion, huobiCurrencies) err := h.SendHTTPRequest(urlPath, &result) if result.ErrorMessage != "" { @@ -382,7 +271,7 @@ func (h *HUOBI) GetTimestamp() (int64, error) { } var result response - urlPath := fmt.Sprintf("%s/v%s/%s", h.APIUrl, huobiAPIVersion, huobiTimestamp) + urlPath := fmt.Sprintf("%s/v%s/%s", h.API.Endpoints.URL, huobiAPIVersion, huobiTimestamp) err := h.SendHTTPRequest(urlPath, &result) if result.ErrorMessage != "" { @@ -554,8 +443,9 @@ func (h *HUOBI) GetOrder(orderID int64) (OrderInfo, error) { } var result response - endpoint := fmt.Sprintf(huobiGetOrder, strconv.FormatInt(orderID, 10)) - err := h.SendAuthenticatedHTTPRequest(http.MethodGet, endpoint, url.Values{}, nil, &result) + urlVal := url.Values{} + urlVal.Set("clientOrderId", strconv.FormatInt(orderID, 10)) + err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOrder, urlVal, nil, &result) if result.ErrorMessage != "" { return result.Order, errors.New(result.ErrorMessage) @@ -625,7 +515,7 @@ func (h *HUOBI) GetOrders(symbol, types, start, end, states, from, direct, size } // GetOpenOrders returns a list of orders -func (h *HUOBI) GetOpenOrders(accountID, symbol, side string, size int) ([]OrderInfo, error) { +func (h *HUOBI) GetOpenOrders(accountID, symbol, side string, size int64) ([]OrderInfo, error) { type response struct { Response Orders []OrderInfo `json:"data"` @@ -637,7 +527,7 @@ func (h *HUOBI) GetOpenOrders(accountID, symbol, side string, size int) ([]Order if len(side) > 0 { vals.Set("side", side) } - vals.Set("size", fmt.Sprintf("%v", size)) + vals.Set("size", strconv.FormatInt(size, 10)) var result response err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOpenOrders, vals, nil, &result) @@ -909,7 +799,7 @@ func (h *HUOBI) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends authenticated requests to the HUOBI API func (h *HUOBI) SendAuthenticatedHTTPRequest(method, endpoint string, values url.Values, data, result interface{}) error { - if !h.AuthenticatedAPISupport { + if !h.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, h.Name) } @@ -917,7 +807,7 @@ func (h *HUOBI) SendAuthenticatedHTTPRequest(method, endpoint string, values url values = url.Values{} } - values.Set("AccessKeyId", h.APIKey) + values.Set("AccessKeyId", h.API.Credentials.Key) values.Set("SignatureMethod", "HmacSHA256") values.Set("SignatureVersion", "2") values.Set("Timestamp", time.Now().UTC().Format("2006-01-02T15:04:05")) @@ -934,12 +824,12 @@ func (h *HUOBI) SendAuthenticatedHTTPRequest(method, endpoint string, values url headers["Content-Type"] = "application/json" } - hmac := common.GetHMAC(common.HashSHA256, []byte(payload), []byte(h.APISecret)) - signature := common.Base64Encode(hmac) + hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(payload), []byte(h.API.Credentials.Secret)) + signature := crypto.Base64Encode(hmac) values.Set("Signature", signature) - if h.APIAuthPEMKeySupport { - pemKey := strings.NewReader(h.APIAuthPEMKey) + if h.API.Credentials.PEMKey != "" && h.API.PEMKeySupport { + pemKey := strings.NewReader(h.API.Credentials.PEMKey) pemBytes, err := ioutil.ReadAll(pemKey) if err != nil { return fmt.Errorf("%s unable to ioutil.ReadAll PEM key: %s", h.Name, err) @@ -956,19 +846,17 @@ func (h *HUOBI) SendAuthenticatedHTTPRequest(method, endpoint string, values url return fmt.Errorf("%s unable to ParseECPrivKey: %s", h.Name, err) } - r, s, err := ecdsa.Sign(rand.Reader, privKey, common.GetSHA256([]byte(signature))) + r, s, err := ecdsa.Sign(rand.Reader, privKey, crypto.GetSHA256([]byte(signature))) if err != nil { return fmt.Errorf("%s unable to sign: %s", h.Name, err) } privSig := r.Bytes() privSig = append(privSig, s.Bytes()...) - values.Set("PrivateSignature", common.Base64Encode(privSig)) + values.Set("PrivateSignature", crypto.Base64Encode(privSig)) } - urlPath := common.EncodeURLValues( - fmt.Sprintf("%s%s", h.APIUrl, endpoint), values, - ) + urlPath := h.API.Endpoints.URL + common.EncodeURLValues(endpoint, values) var body []byte diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index dd2f3452..714af623 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -6,15 +6,19 @@ import ( "crypto/x509" "encoding/pem" "io/ioutil" + "log" + "os" "strconv" "strings" "testing" "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -30,32 +34,35 @@ const ( var h HUOBI var wsSetupRan bool -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { h.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Huobi load config error", err) + } hConfig, err := cfg.GetExchangeConfig("Huobi") if err != nil { - t.Error("Test Failed - Huobi Setup() init error") + log.Fatal("Huobi Setup() init error") } - hConfig.AuthenticatedAPISupport = true - hConfig.AuthenticatedWebsocketAPISupport = true - hConfig.APIKey = apiKey - hConfig.APISecret = apiSecret + hConfig.API.AuthenticatedSupport = true + hConfig.API.AuthenticatedWebsocketSupport = true + hConfig.API.Credentials.Key = apiKey + hConfig.API.Credentials.Secret = apiSecret - h.Setup(&hConfig) + err = h.Setup(hConfig) + if err != nil { + log.Fatal("Huobi setup error", err) + } + + os.Exit(m.Run()) } func setupWsTests(t *testing.T) { if wsSetupRan { return } - TestSetDefaults(t) - TestSetup(t) - if !h.Websocket.IsEnabled() && !h.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + if !h.Websocket.IsEnabled() && !h.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } comms = make(chan WsMessage, sharedtestvalues.WebsocketChannelOverrideCapacity) @@ -97,7 +104,7 @@ func TestGetSpotKline(t *testing.T) { Size: 0, }) if err != nil { - t.Errorf("Test failed - Huobi TestGetSpotKline: %s", err) + t.Errorf("Huobi TestGetSpotKline: %s", err) } } @@ -105,7 +112,7 @@ func TestGetMarketDetailMerged(t *testing.T) { t.Parallel() _, err := h.GetMarketDetailMerged(testSymbol) if err != nil { - t.Errorf("Test failed - Huobi TestGetMarketDetailMerged: %s", err) + t.Errorf("Huobi TestGetMarketDetailMerged: %s", err) } } @@ -117,7 +124,7 @@ func TestGetDepth(t *testing.T) { }) if err != nil { - t.Errorf("Test failed - Huobi TestGetDepth: %s", err) + t.Errorf("Huobi TestGetDepth: %s", err) } } @@ -125,7 +132,7 @@ func TestGetTrades(t *testing.T) { t.Parallel() _, err := h.GetTrades(testSymbol) if err != nil { - t.Errorf("Test failed - Huobi TestGetTrades: %s", err) + t.Errorf("Huobi TestGetTrades: %s", err) } } @@ -133,7 +140,7 @@ func TestGetLatestSpotPrice(t *testing.T) { t.Parallel() _, err := h.GetLatestSpotPrice(testSymbol) if err != nil { - t.Errorf("Test failed - Huobi GetLatestSpotPrice: %s", err) + t.Errorf("Huobi GetLatestSpotPrice: %s", err) } } @@ -141,7 +148,7 @@ func TestGetTradeHistory(t *testing.T) { t.Parallel() _, err := h.GetTradeHistory(testSymbol, "50") if err != nil { - t.Errorf("Test failed - Huobi TestGetTradeHistory: %s", err) + t.Errorf("Huobi TestGetTradeHistory: %s", err) } } @@ -149,7 +156,7 @@ func TestGetMarketDetail(t *testing.T) { t.Parallel() _, err := h.GetMarketDetail(testSymbol) if err != nil { - t.Errorf("Test failed - Huobi TestGetTradeHistory: %s", err) + t.Errorf("Huobi TestGetTradeHistory: %s", err) } } @@ -157,7 +164,7 @@ func TestGetSymbols(t *testing.T) { t.Parallel() _, err := h.GetSymbols() if err != nil { - t.Errorf("Test failed - Huobi TestGetSymbols: %s", err) + t.Errorf("Huobi TestGetSymbols: %s", err) } } @@ -165,7 +172,14 @@ func TestGetCurrencies(t *testing.T) { t.Parallel() _, err := h.GetCurrencies() if err != nil { - t.Errorf("Test failed - Huobi TestGetCurrencies: %s", err) + t.Errorf("Huobi TestGetCurrencies: %s", err) + } +} + +func TestGetTicker(t *testing.T) { + _, err := h.GetTickers() + if err != nil { + t.Error(err) } } @@ -173,59 +187,55 @@ func TestGetTimestamp(t *testing.T) { t.Parallel() _, err := h.GetTimestamp() if err != nil { - t.Errorf("Test failed - Huobi TestGetTimestamp: %s", err) + t.Errorf("Huobi TestGetTimestamp: %s", err) } } func TestGetAccounts(t *testing.T) { t.Parallel() - - if h.APIKey == "" || h.APISecret == "" || h.APIAuthPEMKey == "" { + if !h.ValidateAPICredentials() || !canManipulateRealOrders { t.Skip() } _, err := h.GetAccounts() if err != nil { - t.Errorf("Test failed - Huobi GetAccounts: %s", err) + t.Errorf("Huobi GetAccounts: %s", err) } } func TestGetAccountBalance(t *testing.T) { t.Parallel() - - if h.APIKey == "" || h.APISecret == "" || h.APIAuthPEMKey == "" { + if !h.ValidateAPICredentials() || !canManipulateRealOrders { t.Skip() } result, err := h.GetAccounts() if err != nil { - t.Errorf("Test failed - Huobi GetAccounts: %s", err) + t.Errorf("Huobi GetAccounts: %s", err) } userID := strconv.FormatInt(result[0].ID, 10) _, err = h.GetAccountBalance(userID) if err != nil { - t.Errorf("Test failed - Huobi GetAccountBalance: %s", err) + t.Errorf("Huobi GetAccountBalance: %s", err) } } func TestGetAggregatedBalance(t *testing.T) { t.Parallel() - - if h.APIKey == "" || h.APISecret == "" || h.APIAuthPEMKey == "" { + if !h.ValidateAPICredentials() { t.Skip() } _, err := h.GetAggregatedBalance() if err != nil { - t.Errorf("Test failed - Huobi GetAggregatedBalance: %s", err) + t.Errorf("Huobi GetAggregatedBalance: %s", err) } } func TestSpotNewOrder(t *testing.T) { t.Parallel() - - if h.APIKey == "" || h.APISecret == "" || h.APIAuthPEMKey == "" { + if !h.ValidateAPICredentials() || !canManipulateRealOrders { t.Skip() } @@ -239,86 +249,89 @@ func TestSpotNewOrder(t *testing.T) { _, err := h.SpotNewOrder(arg) if err != nil { - t.Errorf("Test failed - Huobi SpotNewOrder: %s", err) + t.Errorf("Huobi SpotNewOrder: %s", err) } } func TestCancelExistingOrder(t *testing.T) { t.Parallel() - + if !h.ValidateAPICredentials() || !canManipulateRealOrders { + t.Skip() + } _, err := h.CancelExistingOrder(1337) if err == nil { - t.Error("Test failed - Huobi TestCancelExistingOrder: Invalid orderID returned true") + t.Error("Huobi TestCancelExistingOrder Expected error") } } func TestGetOrder(t *testing.T) { t.Parallel() - + if !h.ValidateAPICredentials() || !canManipulateRealOrders { + t.Skip() + } _, err := h.GetOrder(1337) - if err == nil { - t.Error("Test failed - Huobi TestCancelOrder: Invalid orderID returned true") + if err != nil { + t.Error(err) } } func TestGetMarginLoanOrders(t *testing.T) { t.Parallel() - - if h.APIKey == "" || h.APISecret == "" || h.APIAuthPEMKey == "" { + if !h.ValidateAPICredentials() { t.Skip() } _, err := h.GetMarginLoanOrders(testSymbol, "", "", "", "", "", "", "") if err != nil { - t.Errorf("Test failed - Huobi TestGetMarginLoanOrders: %s", err) + t.Errorf("Huobi TestGetMarginLoanOrders: %s", err) } } func TestGetMarginAccountBalance(t *testing.T) { t.Parallel() - - if h.APIKey == "" || h.APISecret == "" || h.APIAuthPEMKey == "" { + if !h.ValidateAPICredentials() { t.Skip() } _, err := h.GetMarginAccountBalance(testSymbol) if err != nil { - t.Errorf("Test failed - Huobi TestGetMarginAccountBalance: %s", err) + t.Errorf("Huobi TestGetMarginAccountBalance: %s", err) } } func TestCancelWithdraw(t *testing.T) { t.Parallel() - + if !h.ValidateAPICredentials() || !canManipulateRealOrders { + t.Skip() + } _, err := h.CancelWithdraw(1337) if err == nil { - t.Error("Test failed - Huobi TestCancelWithdraw: Invalid withdraw-ID was valid") + t.Error("Huobi TestCancelWithdraw Expected error") } } func TestPEMLoadAndSign(t *testing.T) { t.Parallel() - - pemKey := strings.NewReader(h.APIAuthPEMKey) + pemKey := strings.NewReader(h.API.Credentials.PEMKey) pemBytes, err := ioutil.ReadAll(pemKey) if err != nil { - t.Fatalf("Test Failed. TestPEMLoadAndSign Unable to ioutil.ReadAll PEM key: %s", err) + t.Fatalf("TestPEMLoadAndSign Unable to ioutil.ReadAll PEM key: %s", err) } block, _ := pem.Decode(pemBytes) if block == nil { - t.Fatalf("Test Failed. TestPEMLoadAndSign Block is nil") + t.Fatalf("TestPEMLoadAndSign Block is nil") } x509Encoded := block.Bytes privKey, err := x509.ParseECPrivateKey(x509Encoded) if err != nil { - t.Fatalf("Test Failed. TestPEMLoadAndSign Unable to ParseECPrivKey: %s", err) + t.Fatalf("TestPEMLoadAndSign Unable to ParseECPrivKey: %s", err) } - _, _, err = ecdsa.Sign(rand.Reader, privKey, common.GetSHA256([]byte("test"))) + _, _, err = ecdsa.Sign(rand.Reader, privKey, crypto.GetSHA256([]byte("test"))) if err != nil { - t.Fatalf("Test Failed. TestPEMLoadAndSign Unable to sign: %s", err) + t.Fatalf("TestPEMLoadAndSign Unable to sign: %s", err) } } @@ -339,7 +352,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() h.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -356,7 +369,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee Basic if resp, err := h.GetFee(feeBuilder); resp != float64(0.002) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity @@ -364,7 +377,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := h.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) t.Error(err) } @@ -372,7 +385,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := h.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) t.Error(err) } @@ -380,14 +393,14 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -396,7 +409,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -404,7 +417,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -412,7 +425,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -421,28 +434,22 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - h.SetDefaults() expectedResult := exchange.AutoWithdrawCryptoWithSetupText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := h.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - h.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.BTC, currency.USDT)}, } @@ -455,11 +462,8 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - h.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.BTC, currency.USDT)}, } @@ -474,56 +478,47 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if h.APIKey != "" && h.APIKey != "Key" && - h.APISecret != "" && h.APISecret != "Secret" { - return true - } - return false + return h.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - h.SetDefaults() - TestSetup(t) + if !h.ValidateAPICredentials() { + t.Skip() + } if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - if (h.APIKey == "" || h.APIKey == "Key") && - (h.APISecret == "" || h.APISecret == "Secret") { - t.Skip() - } - - var p = currency.Pair{ - Delimiter: "", - Base: currency.BTC, - Quote: currency.USDT, - } - accounts, err := h.GetAccounts() if err != nil { - t.Errorf("Failed to get accounts. Err: %s", err) + t.Fatalf("Failed to get accounts. Err: %s", err) } - response, err := h.SubmitOrder(p, exchange.BuyOrderSide, exchange.LimitOrderType, 1, 10, strconv.FormatInt(accounts[0].ID, 10)) + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.USDT, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: strconv.FormatInt(accounts[0].ID, 10), + } + response, err := h.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) - } else if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") } } func TestCancelExchangeOrder(t *testing.T) { - h.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -541,14 +536,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - h.SetDefaults() - TestSetup(t) if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - var orderCancellation = exchange.OrderCancellation{ + var orderCancellation = order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -564,40 +557,43 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestGetAccountInfo(t *testing.T) { - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { _, err := h.GetAccountInfo() if err == nil { - t.Error("Test Failed - GetAccountInfo() error") + t.Error("GetAccountInfo() Expected error") } } else { _, err := h.GetAccountInfo() if err != nil { - t.Error("Test Failed - GetAccountInfo() error", err) + t.Error("GetAccountInfo() error", err) } } } func TestModifyOrder(t *testing.T) { - _, err := h.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := h.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - h.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -614,15 +610,11 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - h.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := h.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -630,15 +622,11 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - h.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := h.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -648,14 +636,14 @@ func TestWithdrawInternationalBank(t *testing.T) { func TestGetDepositAddress(t *testing.T) { _, err := h.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error cannot be nil") + t.Error("GetDepositAddress() error cannot be nil") } } // TestWsGetAccountsList connects to WS, logs in, gets account list func TestWsGetAccountsList(t *testing.T) { setupWsTests(t) - resp, err := h.wsGetAccountsList(currency.NewPairFromString("ethbtc")) + resp, err := h.wsGetAccountsList() if err != nil { t.Fatal(err) } diff --git a/exchanges/huobi/huobi_types.go b/exchanges/huobi/huobi_types.go index 7e2d3ee0..9ad0f9f2 100644 --- a/exchanges/huobi/huobi_types.go +++ b/exchanges/huobi/huobi_types.go @@ -1,7 +1,5 @@ package huobi -import "github.com/thrasher-corp/gocryptotrader/currency" - // Response stores the Huobi response information type Response struct { Status string `json:"status"` @@ -19,7 +17,7 @@ type KlineItem struct { Low float64 `json:"low"` High float64 `json:"high"` Amount float64 `json:"amount"` - Vol float64 `json:"vol"` + Volume float64 `json:"vol"` Count int `json:"count"` } @@ -37,11 +35,28 @@ type CancelOpenOrdersBatch struct { // DetailMerged stores the ticker detail merged data type DetailMerged struct { Detail - Version int `json:"version"` + Version int64 `json:"version"` Ask []float64 `json:"ask"` Bid []float64 `json:"bid"` } +// Tickers contain all tickers +type Tickers struct { + Data []Ticker `json:"data"` +} + +// Ticker latest ticker data +type Ticker struct { + Amount float64 `json:"amount"` + Close float64 `json:"close"` + Count int64 `json:"count"` + High float64 `json:"high"` + Low float64 `json:"low"` + Open float64 `json:"open"` + Symbol string `json:"symbol"` + Volume float64 `json:"vol"` +} + // OrderBookDataRequestParamsType var for request param types type OrderBookDataRequestParamsType string @@ -93,7 +108,7 @@ type Detail struct { Close float64 `json:"close"` High float64 `json:"high"` Timestamp int64 `json:"timestamp"` - ID int `json:"id"` + ID int64 `json:"id"` Count int `json:"count"` Low float64 `json:"low"` Volume float64 `json:"vol"` @@ -101,11 +116,17 @@ type Detail struct { // Symbol stores the symbol data type Symbol struct { - BaseCurrency string `json:"base-currency"` - QuoteCurrency string `json:"quote-currency"` - PricePrecision int `json:"price-precision"` - AmountPrecision int `json:"amount-precision"` - SymbolPartition string `json:"symbol-partition"` + BaseCurrency string `json:"base-currency"` + QuoteCurrency string `json:"quote-currency"` + PricePrecision int `json:"price-precision"` + AmountPrecision int `json:"amount-precision"` + SymbolPartition string `json:"symbol-partition"` + Innovation string `json:"innovation"` + State string `json:"state"` + ValuePrecision int `json:"value-precision"` + MinimumOrderAmount float64 `json:"min-order-amt"` + MaximumOrderAmount float64 `json:"max-order-amt"` + MinimumOrderValue float64 `json:"min-order-value"` } // Account stores the account data @@ -149,24 +170,23 @@ type CancelOrderBatch struct { // OrderInfo stores the order info type OrderInfo struct { - ID int `json:"id"` + ID int64 `json:"id"` Symbol string `json:"symbol"` - AccountID float64 `json:"account-id"` + AccountID int64 `json:"account-id"` Amount float64 `json:"amount,string"` Price float64 `json:"price,string"` CreatedAt int64 `json:"created-at"` Type string `json:"type"` FieldAmount float64 `json:"field-amount,string"` FieldCashAmount float64 `json:"field-cash-amount,string"` - Fieldees float64 `json:"field-fees,string"` FilledAmount float64 `json:"filled-amount,string"` FilledCashAmount float64 `json:"filled-cash-amount,string"` FilledFees float64 `json:"filled-fees,string"` FinishedAt int64 `json:"finished-at"` - UserID int `json:"user-id"` + UserID int64 `json:"user-id"` Source string `json:"source"` State string `json:"state"` - CanceledAt int `json:"canceled-at"` + CanceledAt int64 `json:"canceled-at"` Exchange string `json:"exchange"` Batch string `json:"batch"` } @@ -299,10 +319,10 @@ type WsDepth struct { Channel string `json:"ch"` Timestamp int64 `json:"ts"` Tick struct { - Bids []interface{} `json:"bids"` - Asks []interface{} `json:"asks"` - Timestamp int64 `json:"ts"` - Version int64 `json:"version"` + Bids [][]interface{} `json:"bids"` + Asks [][]interface{} `json:"asks"` + Timestamp int64 `json:"ts"` + Version int64 `json:"version"` } `json:"tick"` } @@ -322,6 +342,23 @@ type WsKline struct { } } +// WsTick stores websocket ticker data +type WsTick struct { + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` + Tick struct { + Amount float64 `json:"amount"` + Close float64 `json:"close"` + Count float64 `json:"count"` + High float64 `json:"high"` + ID float64 `json:"id"` + Low float64 `json:"low"` + Open float64 `json:"open"` + Timestamp float64 `json:"ts"` + Volume float64 `json:"vol"` + } `json:"tick"` +} + // WsTrade defines market trade websocket response type WsTrade struct { Channel string `json:"ch"` @@ -370,15 +407,15 @@ type WsAuthenticatedSubscriptionRequest struct { // WsAuthenticatedAccountsListRequest request for account list authenticated connection type WsAuthenticatedAccountsListRequest struct { - Op string `json:"op"` - AccessKeyID string `json:"AccessKeyId"` - SignatureMethod string `json:"SignatureMethod"` - SignatureVersion string `json:"SignatureVersion"` - Timestamp string `json:"Timestamp"` - Signature string `json:"Signature"` - Topic string `json:"topic"` - Symbol currency.Pair `json:"symbol"` - ClientID int64 `json:"cid,string,omitempty"` + Op string `json:"op"` + AccessKeyID string `json:"AccessKeyId"` + SignatureMethod string `json:"SignatureMethod"` + SignatureVersion string `json:"SignatureVersion"` + Timestamp string `json:"Timestamp"` + Signature string `json:"Signature"` + Topic string `json:"topic"` + Symbol string `json:"symbol"` + ClientID int64 `json:"cid,string,omitempty"` } // WsAuthenticatedOrderDetailsRequest request for order details authenticated connection @@ -396,17 +433,17 @@ type WsAuthenticatedOrderDetailsRequest struct { // WsAuthenticatedOrdersListRequest request for orderslist authenticated connection type WsAuthenticatedOrdersListRequest struct { - Op string `json:"op"` - AccessKeyID string `json:"AccessKeyId"` - SignatureMethod string `json:"SignatureMethod"` - SignatureVersion string `json:"SignatureVersion"` - Timestamp string `json:"Timestamp"` - Signature string `json:"Signature"` - Topic string `json:"topic"` - States string `json:"states"` - AccountID int64 `json:"account-id"` - Symbol currency.Pair `json:"symbol"` - ClientID int64 `json:"cid,string,omitempty"` + Op string `json:"op"` + AccessKeyID string `json:"AccessKeyId"` + SignatureMethod string `json:"SignatureMethod"` + SignatureVersion string `json:"SignatureVersion"` + Timestamp string `json:"Timestamp"` + Signature string `json:"Signature"` + Topic string `json:"topic"` + States string `json:"states"` + AccountID int64 `json:"account-id"` + Symbol string `json:"symbol"` + ClientID int64 `json:"cid,string,omitempty"` } // WsAuthenticatedDataResponse response from authenticated connection @@ -448,15 +485,15 @@ type WsAuthenticatedOrdersUpdateResponse struct { // WsAuthenticatedOrdersUpdateResponseData order updatedata type WsAuthenticatedOrdersUpdateResponseData struct { - UnfilledAmount float64 `json:"unfilled-amount,string"` - FilledAmount float64 `json:"filled-amount,string"` - Price float64 `json:"price,string"` - OrderID int64 `json:"order-id"` - Symbol currency.Pair `json:"symbol"` - MatchID int64 `json:"match-id"` - FilledCashAmount float64 `json:"filled-cash-amount,string"` - Role string `json:"role"` - OrderState string `json:"order-state"` + UnfilledAmount float64 `json:"unfilled-amount,string"` + FilledAmount float64 `json:"filled-amount,string"` + Price float64 `json:"price,string"` + OrderID int64 `json:"order-id"` + Symbol string `json:"symbol"` + MatchID int64 `json:"match-id"` + FilledCashAmount float64 `json:"filled-cash-amount,string"` + Role string `json:"role"` + OrderState string `json:"order-state"` } // WsAuthenticatedOrdersResponse response from Orders authenticated subscription @@ -467,22 +504,22 @@ type WsAuthenticatedOrdersResponse struct { // WsAuthenticatedOrdersResponseData order data type WsAuthenticatedOrdersResponseData struct { - SeqID int64 `json:"seq-id"` - OrderID int64 `json:"order-id"` - Symbol currency.Pair `json:"symbol"` - AccountID int64 `json:"account-id"` - OrderAmount float64 `json:"order-amount,string"` - OrderPrice float64 `json:"order-price,string"` - CreatedAt int64 `json:"created-at"` - OrderType string `json:"order-type"` - OrderSource string `json:"order-source"` - OrderState string `json:"order-state"` - Role string `json:"role"` - Price float64 `json:"price,string"` - FilledAmount float64 `json:"filled-amount,string"` - UnfilledAmount float64 `json:"unfilled-amount,string"` - FilledCashAmount float64 `json:"filled-cash-amount,string"` - FilledFees float64 `json:"filled-fees,string"` + SeqID int64 `json:"seq-id"` + OrderID int64 `json:"order-id"` + Symbol string `json:"symbol"` + AccountID int64 `json:"account-id"` + OrderAmount float64 `json:"order-amount,string"` + OrderPrice float64 `json:"order-price,string"` + CreatedAt int64 `json:"created-at"` + OrderType string `json:"order-type"` + OrderSource string `json:"order-source"` + OrderState string `json:"order-state"` + Role string `json:"role"` + Price float64 `json:"price,string"` + FilledAmount float64 `json:"filled-amount,string"` + UnfilledAmount float64 `json:"unfilled-amount,string"` + FilledCashAmount float64 `json:"filled-cash-amount,string"` + FilledFees float64 `json:"filled-fees,string"` } // WsAuthenticatedAccountsListResponse response from AccountsList authenticated endpoint @@ -509,31 +546,13 @@ type WsAuthenticatedAccountsListResponseDataList struct { // WsAuthenticatedOrdersListResponse response from OrdersList authenticated endpoint type WsAuthenticatedOrdersListResponse struct { WsAuthenticatedDataResponse - Data []WsAuthenticatedOrdersListResponseData `json:"data"` -} - -// WsAuthenticatedOrdersListResponseData contains order details -type WsAuthenticatedOrdersListResponseData struct { - ID int64 `json:"id"` - Symbol currency.Pair `json:"symbol"` - AccountID int64 `json:"account-id"` - Amount float64 `json:"amount,string"` - Price float64 `json:"price,string"` - CreatedAt int64 `json:"created-at"` - Type string `json:"type"` - FilledAmount float64 `json:"filled-amount,string"` - FilledCashAmount float64 `json:"filled-cash-amount,string"` - FilledFees float64 `json:"filled-fees,string"` - FinishedAt int64 `json:"finished-at"` - Source string `json:"source"` - State string `json:"state"` - CanceledAt int64 `json:"canceled-at"` + Data []OrderInfo `json:"data"` } // WsAuthenticatedOrderDetailResponse response from OrderDetail authenticated endpoint type WsAuthenticatedOrderDetailResponse struct { WsAuthenticatedDataResponse - Data WsAuthenticatedOrdersListResponseData `json:"data"` + Data OrderInfo `json:"data"` } // WsPong sent for pong messages diff --git a/exchanges/huobi/huobi_websocket.go b/exchanges/huobi/huobi_websocket.go index 2000dfb1..93dda0a8 100644 --- a/exchanges/huobi/huobi_websocket.go +++ b/exchanges/huobi/huobi_websocket.go @@ -1,6 +1,7 @@ package huobi import ( + "encoding/json" "errors" "fmt" "net/http" @@ -9,9 +10,10 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" @@ -20,10 +22,11 @@ import ( const ( baseWSURL = "wss://api.huobi.pro" - wsMarketURL = baseWSURL + "/ws" - wsMarketKline = "market.%s.kline.1min" - wsMarketDepth = "market.%s.depth.step0" - wsMarketTrade = "market.%s.trade.detail" + wsMarketURL = baseWSURL + "/ws" + wsMarketKline = "market.%s.kline.1min" + wsMarketDepth = "market.%s.depth.step0" + wsMarketTrade = "market.%s.trade.detail" + wsMarketTicker = "market.%s.detail" wsAccountsOrdersEndPoint = "/ws/v1" wsAccountsList = "accounts.list" @@ -46,7 +49,7 @@ const ( ) // Instantiates a communications channel between websocket connections -var comms = make(chan WsMessage, 1) +var comms = make(chan WsMessage) // WsConnect initiates a new websocket connection func (h *HUOBI) WsConnect() error { @@ -60,11 +63,12 @@ func (h *HUOBI) WsConnect() error { } err = h.wsAuthenticatedDial(&dialer) if err != nil { - log.Errorf("%v - authenticated dial failed: %v", h.Name, err) + log.Errorf(log.ExchangeSys, "%v - authenticated dial failed: %v\n", h.Name, err) } err = h.wsLogin() if err != nil { - log.Errorf("%v - authentication failed: %v", h.Name, err) + log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", h.Name, err) + h.Websocket.SetCanUseAuthenticatedEndpoints(false) } go h.WsHandleData() @@ -135,7 +139,7 @@ func (h *HUOBI) WsHandleData() { func (h *HUOBI) wsHandleAuthenticatedData(resp WsMessage) { var init WsAuthenticatedDataResponse - err := common.JSONDecode(resp.Raw, &init) + err := json.Unmarshal(resp.Raw, &init) if err != nil { h.Websocket.DataHandler <- err return @@ -143,7 +147,7 @@ func (h *HUOBI) wsHandleAuthenticatedData(resp WsMessage) { if init.Ping != 0 { err = h.WebsocketConn.SendMessage(WsPong{Pong: init.Ping}) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) } return } @@ -152,7 +156,7 @@ func (h *HUOBI) wsHandleAuthenticatedData(resp WsMessage) { } if init.Op == "sub" { if h.Verbose { - log.Debugf("%v: %v: Successfully subscribed to %v", h.Name, resp.URL, init.Topic) + log.Debugf(log.ExchangeSys, "%v: %v: Successfully subscribed to %v", h.Name, resp.URL, init.Topic) } return } @@ -165,29 +169,29 @@ func (h *HUOBI) wsHandleAuthenticatedData(resp WsMessage) { case strings.EqualFold(init.Op, authOp): h.Websocket.SetCanUseAuthenticatedEndpoints(true) var response WsAuthenticatedDataResponse - err := common.JSONDecode(resp.Raw, &response) + err := json.Unmarshal(resp.Raw, &response) if err != nil { h.Websocket.DataHandler <- err } h.Websocket.DataHandler <- response case strings.EqualFold(init.Topic, "accounts"): var response WsAuthenticatedAccountsResponse - err := common.JSONDecode(resp.Raw, &response) + err := json.Unmarshal(resp.Raw, &response) if err != nil { h.Websocket.DataHandler <- err } h.Websocket.DataHandler <- response - case common.StringContains(init.Topic, "orders") && - common.StringContains(init.Topic, "update"): + case strings.Contains(init.Topic, "orders") && + strings.Contains(init.Topic, "update"): var response WsAuthenticatedOrdersUpdateResponse - err := common.JSONDecode(resp.Raw, &response) + err := json.Unmarshal(resp.Raw, &response) if err != nil { h.Websocket.DataHandler <- err } h.Websocket.DataHandler <- response - case common.StringContains(init.Topic, "orders"): + case strings.Contains(init.Topic, "orders"): var response WsAuthenticatedOrdersResponse - err := common.JSONDecode(resp.Raw, &response) + err := json.Unmarshal(resp.Raw, &response) if err != nil { h.Websocket.DataHandler <- err } @@ -197,7 +201,7 @@ func (h *HUOBI) wsHandleAuthenticatedData(resp WsMessage) { func (h *HUOBI) wsHandleMarketData(resp WsMessage) { var init WsResponse - err := common.JSONDecode(resp.Raw, &init) + err := json.Unmarshal(resp.Raw, &init) if err != nil { h.Websocket.DataHandler <- err return @@ -216,91 +220,130 @@ func (h *HUOBI) wsHandleMarketData(resp WsMessage) { if init.Ping != 0 { err = h.WebsocketConn.SendMessage(WsPong{Pong: init.Ping}) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) } return } switch { - case common.StringContains(init.Channel, "depth"): + case strings.Contains(init.Channel, "depth"): var depth WsDepth - err := common.JSONDecode(resp.Raw, &depth) + err := json.Unmarshal(resp.Raw, &depth) if err != nil { h.Websocket.DataHandler <- err return } - data := common.SplitStrings(depth.Channel, ".") - h.WsProcessOrderbook(&depth, data[1]) - case common.StringContains(init.Channel, "kline"): + + data := strings.Split(depth.Channel, ".") + err = h.WsProcessOrderbook(&depth, data[1]) + if err != nil { + h.Websocket.DataHandler <- err + return + } + + case strings.Contains(init.Channel, "kline"): var kline WsKline - err := common.JSONDecode(resp.Raw, &kline) + err := json.Unmarshal(resp.Raw, &kline) if err != nil { h.Websocket.DataHandler <- err return } - data := common.SplitStrings(kline.Channel, ".") + data := strings.Split(kline.Channel, ".") h.Websocket.DataHandler <- wshandler.KlineData{ - Timestamp: time.Unix(0, kline.Timestamp), - Exchange: h.GetName(), - AssetType: orderbook.Spot, - Pair: currency.NewPairFromString(data[1]), + Timestamp: time.Unix(0, kline.Timestamp), + Exchange: h.Name, + AssetType: asset.Spot, + Pair: currency.NewPairFromFormattedPairs(data[1], + h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)), OpenPrice: kline.Tick.Open, ClosePrice: kline.Tick.Close, HighPrice: kline.Tick.High, LowPrice: kline.Tick.Low, Volume: kline.Tick.Volume, } - case common.StringContains(init.Channel, "trade"): + case strings.Contains(init.Channel, "trade.detail"): var trade WsTrade - err := common.JSONDecode(resp.Raw, &trade) + err := json.Unmarshal(resp.Raw, &trade) if err != nil { h.Websocket.DataHandler <- err return } - data := common.SplitStrings(trade.Channel, ".") + data := strings.Split(trade.Channel, ".") h.Websocket.DataHandler <- wshandler.TradeData{ - Exchange: h.GetName(), - AssetType: orderbook.Spot, - CurrencyPair: currency.NewPairFromString(data[1]), - Timestamp: time.Unix(0, trade.Tick.Timestamp), + Exchange: h.Name, + AssetType: asset.Spot, + CurrencyPair: currency.NewPairFromFormattedPairs(data[1], + h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)), + Timestamp: time.Unix(0, trade.Tick.Timestamp), + } + case strings.Contains(init.Channel, "detail"): + var ticker WsTick + err := json.Unmarshal(resp.Raw, &ticker) + if err != nil { + h.Websocket.DataHandler <- err + return + } + data := strings.Split(ticker.Channel, ".") + h.Websocket.DataHandler <- wshandler.TickerData{ + Exchange: h.Name, + Open: ticker.Tick.Open, + Close: ticker.Tick.Close, + Volume: ticker.Tick.Amount, + QuoteVolume: ticker.Tick.Volume, + High: ticker.Tick.High, + Low: ticker.Tick.Low, + Timestamp: time.Unix(0, ticker.Timestamp), + AssetType: asset.Spot, + Pair: currency.NewPairFromFormattedPairs(data[1], + h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true)), } } } // WsProcessOrderbook processes new orderbook data func (h *HUOBI) WsProcessOrderbook(update *WsDepth, symbol string) error { - p := currency.NewPairFromString(symbol) + p := currency.NewPairFromFormattedPairs(symbol, + h.GetEnabledPairs(asset.Spot), + h.GetPairFormat(asset.Spot, true)) + var bids, asks []orderbook.Item - for i := 0; i < len(update.Tick.Bids); i++ { - bidLevel := update.Tick.Bids[i].([]interface{}) - bids = append(bids, orderbook.Item{Price: bidLevel[0].(float64), - Amount: bidLevel[0].(float64)}) + for i := range update.Tick.Bids { + bids = append(bids, orderbook.Item{ + Price: update.Tick.Bids[i][0].(float64), + Amount: update.Tick.Bids[i][1].(float64), + }) } - for i := 0; i < len(update.Tick.Asks); i++ { - askLevel := update.Tick.Asks[i].([]interface{}) - asks = append(asks, orderbook.Item{Price: askLevel[0].(float64), - Amount: askLevel[0].(float64)}) + + for i := range update.Tick.Asks { + asks = append(asks, orderbook.Item{ + Price: update.Tick.Asks[i][0].(float64), + Amount: update.Tick.Asks[i][1].(float64), + }) } + var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids newOrderBook.Pair = p - err := h.Websocket.Orderbook.LoadSnapshot(&newOrderBook, true) + newOrderBook.AssetType = asset.Spot + newOrderBook.ExchangeName = h.Name + + err := h.Websocket.Orderbook.LoadSnapshot(&newOrderBook) if err != nil { return err } + h.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: p, - Exchange: h.GetName(), - Asset: orderbook.Spot, + Exchange: h.Name, + Asset: asset.Spot, } - return nil } // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (h *HUOBI) GenerateDefaultSubscriptions() { - var channels = []string{wsMarketKline, wsMarketDepth, wsMarketTrade} + var channels = []string{wsMarketKline, wsMarketDepth, wsMarketTrade, wsMarketTicker} var subscriptions []wshandler.WebsocketChannelSubscription if h.Websocket.CanUseAuthenticatedEndpoints() { channels = append(channels, "orders.%v", "orders.%v.update") @@ -308,7 +351,7 @@ func (h *HUOBI) GenerateDefaultSubscriptions() { Channel: "accounts", }) } - enabledCurrencies := h.GetEnabledCurrencies() + enabledCurrencies := h.GetEnabledPairs(asset.Spot) for i := range channels { for j := range enabledCurrencies { enabledCurrencies[j].Delimiter = "" @@ -324,8 +367,8 @@ func (h *HUOBI) GenerateDefaultSubscriptions() { // Subscribe sends a websocket message to receive data from the channel func (h *HUOBI) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { - if common.StringContains(channelToSubscribe.Channel, "orders.") || - common.StringContains(channelToSubscribe.Channel, "accounts") { + if strings.Contains(channelToSubscribe.Channel, "orders.") || + strings.Contains(channelToSubscribe.Channel, "accounts") { return h.wsAuthenticatedSubscribe("sub", wsAccountsOrdersEndPoint+channelToSubscribe.Channel, channelToSubscribe.Channel) } return h.WebsocketConn.SendMessage(WsRequest{Subscribe: channelToSubscribe.Channel}) @@ -333,8 +376,8 @@ func (h *HUOBI) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscript // Unsubscribe sends a websocket message to stop receiving data from the channel func (h *HUOBI) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { - if common.StringContains(channelToSubscribe.Channel, "orders.") || - common.StringContains(channelToSubscribe.Channel, "accounts") { + if strings.Contains(channelToSubscribe.Channel, "orders.") || + strings.Contains(channelToSubscribe.Channel, "accounts") { return h.wsAuthenticatedSubscribe("unsub", wsAccountsOrdersEndPoint+channelToSubscribe.Channel, channelToSubscribe.Channel) } return h.WebsocketConn.SendMessage(WsRequest{Unsubscribe: channelToSubscribe.Channel}) @@ -342,14 +385,14 @@ func (h *HUOBI) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscri func (h *HUOBI) wsGenerateSignature(timestamp, endpoint string) []byte { values := url.Values{} - values.Set("AccessKeyId", h.APIKey) + values.Set("AccessKeyId", h.API.Credentials.Key) values.Set("SignatureMethod", signatureMethod) values.Set("SignatureVersion", signatureVersion) values.Set("Timestamp", timestamp) host := "api.huobi.pro" payload := fmt.Sprintf("%s\n%s\n%s\n%s", "GET", host, endpoint, values.Encode()) - return common.GetHMAC(common.HashSHA256, []byte(payload), []byte(h.APISecret)) + return crypto.GetHMAC(crypto.HashSHA256, []byte(payload), []byte(h.API.Credentials.Secret)) } func (h *HUOBI) wsLogin() error { @@ -360,13 +403,13 @@ func (h *HUOBI) wsLogin() error { timestamp := time.Now().UTC().Format(wsDateTimeFormatting) request := WsAuthenticationRequest{ Op: authOp, - AccessKeyID: h.APIKey, + AccessKeyID: h.API.Credentials.Key, SignatureMethod: signatureMethod, SignatureVersion: signatureVersion, Timestamp: timestamp, } hmac := h.wsGenerateSignature(timestamp, wsAccountsOrdersEndPoint) - request.Signature = common.Base64Encode(hmac) + request.Signature = crypto.Base64Encode(hmac) err := h.AuthenticatedWebsocketConn.SendMessage(request) if err != nil { h.Websocket.SetCanUseAuthenticatedEndpoints(false) @@ -381,40 +424,39 @@ func (h *HUOBI) wsAuthenticatedSubscribe(operation, endpoint, topic string) erro timestamp := time.Now().UTC().Format(wsDateTimeFormatting) request := WsAuthenticatedSubscriptionRequest{ Op: operation, - AccessKeyID: h.APIKey, + AccessKeyID: h.API.Credentials.Key, SignatureMethod: signatureMethod, SignatureVersion: signatureVersion, Timestamp: timestamp, Topic: topic, } hmac := h.wsGenerateSignature(timestamp, endpoint) - request.Signature = common.Base64Encode(hmac) + request.Signature = crypto.Base64Encode(hmac) return h.AuthenticatedWebsocketConn.SendMessage(request) } -func (h *HUOBI) wsGetAccountsList(pair currency.Pair) (*WsAuthenticatedAccountsListResponse, error) { +func (h *HUOBI) wsGetAccountsList() (*WsAuthenticatedAccountsListResponse, error) { if !h.Websocket.CanUseAuthenticatedEndpoints() { return nil, fmt.Errorf("%v not authenticated cannot get accounts list", h.Name) } timestamp := time.Now().UTC().Format(wsDateTimeFormatting) request := WsAuthenticatedAccountsListRequest{ Op: requestOp, - AccessKeyID: h.APIKey, + AccessKeyID: h.API.Credentials.Key, SignatureMethod: signatureMethod, SignatureVersion: signatureVersion, Timestamp: timestamp, Topic: wsAccountsList, - Symbol: pair, } hmac := h.wsGenerateSignature(timestamp, wsAccountListEndpoint) - request.Signature = common.Base64Encode(hmac) + request.Signature = crypto.Base64Encode(hmac) request.ClientID = h.AuthenticatedWebsocketConn.GenerateMessageID(true) resp, err := h.AuthenticatedWebsocketConn.SendMessageReturnResponse(request.ClientID, request) if err != nil { return nil, err } var response WsAuthenticatedAccountsListResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) return &response, err } @@ -425,24 +467,24 @@ func (h *HUOBI) wsGetOrdersList(accountID int64, pair currency.Pair) (*WsAuthent timestamp := time.Now().UTC().Format(wsDateTimeFormatting) request := WsAuthenticatedOrdersListRequest{ Op: requestOp, - AccessKeyID: h.APIKey, + AccessKeyID: h.API.Credentials.Key, SignatureMethod: signatureMethod, SignatureVersion: signatureVersion, Timestamp: timestamp, Topic: wsOrdersList, AccountID: accountID, - Symbol: pair.Lower(), + Symbol: h.FormatExchangeCurrency(pair, asset.Spot).String(), States: "submitted,partial-filled", } hmac := h.wsGenerateSignature(timestamp, wsOrdersListEndpoint) - request.Signature = common.Base64Encode(hmac) + request.Signature = crypto.Base64Encode(hmac) request.ClientID = h.AuthenticatedWebsocketConn.GenerateMessageID(true) resp, err := h.AuthenticatedWebsocketConn.SendMessageReturnResponse(request.ClientID, request) if err != nil { return nil, err } var response WsAuthenticatedOrdersResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) return &response, err } @@ -453,7 +495,7 @@ func (h *HUOBI) wsGetOrderDetails(orderID string) (*WsAuthenticatedOrderDetailRe timestamp := time.Now().UTC().Format(wsDateTimeFormatting) request := WsAuthenticatedOrderDetailsRequest{ Op: requestOp, - AccessKeyID: h.APIKey, + AccessKeyID: h.API.Credentials.Key, SignatureMethod: signatureMethod, SignatureVersion: signatureVersion, Timestamp: timestamp, @@ -461,13 +503,13 @@ func (h *HUOBI) wsGetOrderDetails(orderID string) (*WsAuthenticatedOrderDetailRe OrderID: orderID, } hmac := h.wsGenerateSignature(timestamp, wsOrdersDetailEndpoint) - request.Signature = common.Base64Encode(hmac) + request.Signature = crypto.Base64Encode(hmac) request.ClientID = h.AuthenticatedWebsocketConn.GenerateMessageID(true) resp, err := h.AuthenticatedWebsocketConn.SendMessageReturnResponse(request.ClientID, request) if err != nil { return nil, err } var response WsAuthenticatedOrderDetailResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) return &response, err } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 752a6baf..b224c629 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -12,12 +12,178 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (h *HUOBI) GetDefaultConfig() (*config.ExchangeConfig, error) { + h.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = h.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = h.BaseCurrencies + + err := h.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if h.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = h.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets default values for the exchange +func (h *HUOBI) SetDefaults() { + h.Name = "Huobi" + h.Enabled = true + h.Verbose = true + h.API.CredentialsValidator.RequiresKey = true + h.API.CredentialsValidator.RequiresSecret = true + + h.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: false, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "-", + Uppercase: true, + }, + } + + h.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + CryptoWithdrawal: true, + TradeFee: true, + }, + WebsocketCapabilities: protocol.Features{ + KlineFetching: true, + OrderbookFetching: true, + TradeFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + AccountInfo: true, + MessageCorrelation: true, + GetOrder: true, + GetOrders: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + h.Requester = request.New(h.Name, + request.NewRateLimit(time.Second*10, huobiAuthRate), + request.NewRateLimit(time.Second*10, huobiUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + h.API.Endpoints.URLDefault = huobiAPIURL + h.API.Endpoints.URL = h.API.Endpoints.URLDefault + h.API.Endpoints.WebsocketURL = wsMarketURL + h.Websocket = wshandler.New() + h.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + h.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + h.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup sets user configuration +func (h *HUOBI) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + h.SetEnabled(false) + return nil + } + + err := h.SetupDefaults(exch) + if err != nil { + return err + } + + h.API.PEMKeySupport = exch.API.PEMKeySupport + h.API.Credentials.PEMKey = exch.API.Credentials.PEMKey + + err = h.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: wsMarketURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: h.WsConnect, + Subscriber: h.Subscribe, + UnSubscriber: h.Unsubscribe, + Features: &h.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + h.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: h.Name, + URL: wsMarketURL, + ProxyURL: h.Websocket.GetProxyAddress(), + Verbose: h.Verbose, + RateLimit: rateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + h.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: h.Name, + URL: wsAccountsOrdersURL, + ProxyURL: h.Websocket.GetProxyAddress(), + Verbose: h.Verbose, + RateLimit: rateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + h.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + false, + false, + false, + false, + exch.Name) + return nil +} + // Start starts the HUOBI go routine func (h *HUOBI) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -30,114 +196,143 @@ func (h *HUOBI) Start(wg *sync.WaitGroup) { // Run implements the HUOBI wrapper func (h *HUOBI) Run() { if h.Verbose { - log.Debugf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket.IsEnabled()), wsMarketURL) - log.Debugf("%s polling delay: %ds.\n", h.GetName(), h.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", h.GetName(), len(h.EnabledPairs), h.EnabledPairs) + log.Debugf(log.ExchangeSys, + "%s Websocket: %s (url: %s).\n", + h.Name, + common.IsEnabled(h.Websocket.IsEnabled()), + wsMarketURL) + h.PrintEnabledPairs() } - exchangeProducts, err := h.GetSymbols() - if err != nil { - log.Errorf("%s Failed to get available symbols.\n", h.GetName()) - } else { - forceUpgrade := false - if common.StringDataContains(h.EnabledPairs.Strings(), "CNY") || - common.StringDataContains(h.AvailablePairs.Strings(), "CNY") { - forceUpgrade = true - } + var forceUpdate bool + if common.StringDataContains(h.GetEnabledPairs(asset.Spot).Strings(), "CNY") || + common.StringDataContains(h.GetAvailablePairs(asset.Spot).Strings(), "CNY") { + forceUpdate = true + } - if common.StringDataContains(h.BaseCurrencies.Strings(), "CNY") { - cfg := config.GetConfig() - exchCfg, errCNY := cfg.GetExchangeConfig(h.Name) - if errCNY != nil { - log.Errorf("%s failed to get exchange config. %s\n", h.Name, errCNY) - return - } - exchCfg.BaseCurrencies = currency.Currencies{currency.USD} - h.BaseCurrencies = currency.Currencies{currency.USD} - - errCNY = cfg.UpdateExchangeConfig(&exchCfg) - if errCNY != nil { - log.Errorf("%s failed to update config. %s\n", h.Name, errCNY) - return - } - } - - var currencies []string - for x := range exchangeProducts { - newCurrency := exchangeProducts[x].BaseCurrency + "-" + exchangeProducts[x].QuoteCurrency - currencies = append(currencies, newCurrency) - } - - if forceUpgrade { - enabledPairs := currency.Pairs{currency.Pair{ - Base: currency.BTC.Lower(), - Quote: currency.USDT.Lower(), - Delimiter: "-", - }, - } - log.Warn("Available and enabled pairs for Huobi reset due to config upgrade, please enable the ones you would like again") - - err = h.UpdateCurrencies(enabledPairs, true, true) - if err != nil { - log.Errorf("%s Failed to update enabled currencies.\n", h.GetName()) - } - } - - var newCurrencies currency.Pairs - for _, p := range currencies { - newCurrencies = append(newCurrencies, - currency.NewPairFromString(p)) - } - - err = h.UpdateCurrencies(newCurrencies, false, forceUpgrade) + if common.StringDataContains(h.BaseCurrencies.Strings(), "CNY") { + cfg := config.GetConfig() + exchCfg, err := cfg.GetExchangeConfig(h.Name) if err != nil { - log.Errorf("%s Failed to update available currencies.\n", h.GetName()) + log.Errorf(log.ExchangeSys, + "%s failed to get exchange config. %s\n", + h.Name, + err) + return } + exchCfg.BaseCurrencies = currency.Currencies{currency.USD} + h.BaseCurrencies = currency.Currencies{currency.USD} + } + + if forceUpdate { + enabledPairs := currency.Pairs{currency.Pair{ + Base: currency.BTC.Lower(), + Quote: currency.USDT.Lower(), + Delimiter: "-", + }, + } + log.Warn(log.ExchangeSys, + "Available and enabled pairs for Huobi reset due to config upgrade, please enable the ones you would like again") + + err := h.UpdatePairs(enabledPairs, asset.Spot, true, true) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s Failed to update enabled currencies.\n", + h.Name) + } + } + + if !h.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { + return + } + + err := h.UpdateTradablePairs(forceUpdate) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + h.Name, + err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (h *HUOBI) FetchTradablePairs(asset asset.Item) ([]string, error) { + symbols, err := h.GetSymbols() + if err != nil { + return nil, err + } + + var pairs []string + for x := range symbols { + if symbols[x].State != "online" { + continue + } + pairs = append(pairs, symbols[x].BaseCurrency+ + h.GetPairFormat(asset, false).Delimiter+ + symbols[x].QuoteCurrency) + } + + return pairs, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (h *HUOBI) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := h.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return h.UpdatePairs(currency.NewPairsFromStrings(pairs), + asset.Spot, + false, + forceUpdate) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (h *HUOBI) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (h *HUOBI) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - tick, err := h.GetMarketDetailMerged(exchange.FormatExchangeCurrency(h.Name, p).String()) + tickers, err := h.GetTickers() if err != nil { return tickerPrice, err } - - tickerPrice.Pair = p - tickerPrice.Low = tick.Low - tickerPrice.Last = tick.Close - tickerPrice.Volume = tick.Volume - tickerPrice.High = tick.High - - if len(tick.Ask) > 0 { - tickerPrice.Ask = tick.Ask[0] - } - - if len(tick.Bid) > 0 { - tickerPrice.Bid = tick.Bid[0] - } - - err = ticker.ProcessTicker(h.GetName(), &tickerPrice, assetType) - if err != nil { - return tickerPrice, err + pairs := h.GetEnabledPairs(assetType) + for i := range pairs { + for j := range tickers.Data { + pairFmt := h.FormatExchangeCurrency(pairs[i], assetType).String() + if pairFmt != tickers.Data[j].Symbol { + continue + } + tickerPrice := ticker.Price{ + High: tickers.Data[j].High, + Low: tickers.Data[j].Low, + Volume: tickers.Data[j].Volume, + Open: tickers.Data[j].Open, + Close: tickers.Data[j].Close, + Pair: pairs[i], + } + err = ticker.ProcessTicker(h.Name, &tickerPrice, assetType) + if err != nil { + log.Error(log.Ticker, err) + } + } } return ticker.GetTicker(h.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (h *HUOBI) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(h.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (h *HUOBI) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(h.Name, p, assetType) if err != nil { return h.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (h *HUOBI) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(h.GetName(), p, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (h *HUOBI) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(h.Name, p, assetType) if err != nil { return h.UpdateOrderbook(p, assetType) } @@ -145,28 +340,32 @@ func (h *HUOBI) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Bas } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (h *HUOBI) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (h *HUOBI) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base orderbookNew, err := h.GetDepth(OrderBookDataRequestParams{ - Symbol: exchange.FormatExchangeCurrency(h.Name, p).String(), - Type: OrderBookDataRequestParamsTypeStep1, + Symbol: h.FormatExchangeCurrency(p, assetType).String(), + Type: OrderBookDataRequestParamsTypeStep0, }) if err != nil { return orderBook, err } for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data[1], Price: data[0]}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[x][1], + Price: orderbookNew.Bids[x][0], + }) } for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data[1], Price: data[0]}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[x][1], + Price: orderbookNew.Asks[x][0], + }) } orderBook.Pair = p - orderBook.ExchangeName = h.GetName() + orderBook.ExchangeName = h.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -195,156 +394,179 @@ func (h *HUOBI) GetAccountID() ([]Account, error) { // HUOBI exchange - to-do func (h *HUOBI) GetAccountInfo() (exchange.AccountInfo, error) { var info exchange.AccountInfo - info.Exchange = h.GetName() - - accounts, err := h.GetAccountID() - if err != nil { - return info, err - } - - for _, account := range accounts { - var acc exchange.Account - - acc.ID = strconv.FormatInt(account.ID, 10) - - balances, err := h.GetAccountBalance(acc.ID) + info.Exchange = h.Name + if h.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + resp, err := h.wsGetAccountsList() if err != nil { return info, err } - var currencyDetails []exchange.AccountCurrencyInfo - for _, balance := range balances { - var frozen bool - if balance.Type == "frozen" { - frozen = true + for i := range resp.Data { + if len(resp.Data[i].List) == 0 { + continue + } + currData := exchange.AccountCurrencyInfo{ + CurrencyName: currency.NewCode(resp.Data[i].List[0].Currency), + TotalValue: resp.Data[i].List[0].Balance, + } + if len(resp.Data[i].List) > 1 && resp.Data[i].List[1].Type == "frozen" { + currData.Hold = resp.Data[i].List[1].Balance + } + currencyDetails = append(currencyDetails, currData) + } + var acc exchange.Account + acc.Currencies = currencyDetails + info.Accounts = append(info.Accounts, acc) + } else { + accounts, err := h.GetAccountID() + if err != nil { + return info, err + } + for i := range accounts { + var acc exchange.Account + acc.ID = strconv.FormatInt(accounts[i].ID, 10) + balances, err := h.GetAccountBalance(acc.ID) + if err != nil { + return info, err } - var updated bool - for i := range currencyDetails { - if currencyDetails[i].CurrencyName == currency.NewCode(balance.Currency) { - if frozen { - currencyDetails[i].Hold = balance.Balance - } else { - currencyDetails[i].TotalValue = balance.Balance + var currencyDetails []exchange.AccountCurrencyInfo + for j := range balances { + var frozen bool + if balances[j].Type == "frozen" { + frozen = true + } + + var updated bool + for i := range currencyDetails { + if currencyDetails[i].CurrencyName == currency.NewCode(balances[j].Currency) { + if frozen { + currencyDetails[i].Hold = balances[j].Balance + } else { + currencyDetails[i].TotalValue = balances[j].Balance + } + updated = true } - updated = true + } + + if updated { + continue + } + + if frozen { + currencyDetails = append(currencyDetails, + exchange.AccountCurrencyInfo{ + CurrencyName: currency.NewCode(balances[j].Currency), + Hold: balances[j].Balance, + }) + } else { + currencyDetails = append(currencyDetails, + exchange.AccountCurrencyInfo{ + CurrencyName: currency.NewCode(balances[j].Currency), + TotalValue: balances[j].Balance, + }) } } - if updated { - continue - } - - if frozen { - currencyDetails = append(currencyDetails, - exchange.AccountCurrencyInfo{ - CurrencyName: currency.NewCode(balance.Currency), - Hold: balance.Balance, - }) - } else { - currencyDetails = append(currencyDetails, - exchange.AccountCurrencyInfo{ - CurrencyName: currency.NewCode(balance.Currency), - TotalValue: balance.Balance, - }) - } + acc.Currencies = currencyDetails + info.Accounts = append(info.Accounts, acc) } - - acc.Currencies = currencyDetails - info.Accounts = append(info.Accounts, acc) } - return info, nil } // GetFundingHistory returns funding history, deposits and // withdrawals func (h *HUOBI) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (h *HUOBI) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (h *HUOBI) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (h *HUOBI) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - accountID, err := strconv.ParseInt(clientID, 10, 64) +func (h *HUOBI) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } + + accountID, err := strconv.ParseInt(s.ClientID, 10, 64) if err != nil { return submitOrderResponse, err } var formattedType SpotNewOrderRequestParamsType var params = SpotNewOrderRequestParams{ - Amount: amount, + Amount: s.Amount, Source: "api", - Symbol: common.StringToLower(p.String()), + Symbol: s.Pair.Lower().String(), AccountID: int(accountID), } switch { - case side == exchange.BuyOrderSide && orderType == exchange.MarketOrderType: + case s.OrderSide == order.Buy && s.OrderType == order.Market: formattedType = SpotNewOrderRequestTypeBuyMarket - case side == exchange.SellOrderSide && orderType == exchange.MarketOrderType: + case s.OrderSide == order.Sell && s.OrderType == order.Market: formattedType = SpotNewOrderRequestTypeSellMarket - case side == exchange.BuyOrderSide && orderType == exchange.LimitOrderType: + case s.OrderSide == order.Buy && s.OrderType == order.Limit: formattedType = SpotNewOrderRequestTypeBuyLimit - params.Price = price - case side == exchange.SellOrderSide && orderType == exchange.LimitOrderType: + params.Price = s.Price + case s.OrderSide == order.Sell && s.OrderType == order.Limit: formattedType = SpotNewOrderRequestTypeSellLimit - params.Price = price - default: - return submitOrderResponse, errors.New("unsupported order type") + params.Price = s.Price } params.Type = formattedType response, err := h.SpotNewOrder(params) + if err != nil { + return submitOrderResponse, err + } if response > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response) + submitOrderResponse.OrderID = strconv.FormatInt(response, 10) } - if err == nil { - submitOrderResponse.IsOrderPlaced = true + submitOrderResponse.IsOrderPlaced = true + if s.OrderType == order.Market { + submitOrderResponse.FullyMatched = true } - - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (h *HUOBI) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (h *HUOBI) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (h *HUOBI) CancelOrder(order *exchange.OrderCancellation) error { +func (h *HUOBI) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) - if err != nil { return err } _, err = h.CancelExistingOrder(orderIDInt) - return err } // CancelAllOrders cancels all orders associated with a currency pair -func (h *HUOBI) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - var cancelAllOrdersResponse exchange.CancelAllOrdersResponse - for _, currency := range h.GetEnabledCurrencies() { - resp, err := h.CancelOpenOrdersBatch(orderCancellation.AccountID, exchange.FormatExchangeCurrency(h.Name, currency).String()) +func (h *HUOBI) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) { + var cancelAllOrdersResponse order.CancelAllResponse + enabledPairs := h.GetEnabledPairs(asset.Spot) + for i := range enabledPairs { + resp, err := h.CancelOpenOrdersBatch(orderCancellation.AccountID, + h.FormatExchangeCurrency(enabledPairs[i], asset.Spot).String()) if err != nil { return cancelAllOrdersResponse, err } if resp.Data.FailedCount > 0 { - return cancelAllOrdersResponse, fmt.Errorf("%v orders failed to cancel", resp.Data.FailedCount) + return cancelAllOrdersResponse, + fmt.Errorf("%v orders failed to cancel", + resp.Data.FailedCount) } if resp.Status == "error" { @@ -356,9 +578,58 @@ func (h *HUOBI) CancelAllOrders(orderCancellation *exchange.OrderCancellation) ( } // GetOrderInfo returns information on a current open order -func (h *HUOBI) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail - return orderDetail, common.ErrNotYetImplemented +func (h *HUOBI) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail + var respData *OrderInfo + if h.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + resp, err := h.wsGetOrderDetails(orderID) + if err != nil { + return orderDetail, err + } + respData = &resp.Data + } else { + oID, err := strconv.ParseInt(orderID, 10, 64) + if err != nil { + return orderDetail, err + } + resp, err := h.GetOrder(oID) + if err != nil { + return orderDetail, err + } + respData = &resp + } + if respData.ID == 0 { + return orderDetail, fmt.Errorf("%s - order not found for orderid %s", h.Name, orderID) + } + + typeDetails := strings.Split(respData.Type, "-") + orderSide, err := order.StringToOrderSide(typeDetails[0]) + if err != nil { + return orderDetail, err + } + orderType, err := order.StringToOrderType(typeDetails[1]) + if err != nil { + return orderDetail, err + } + orderStatus, err := order.StringToOrderStatus(respData.State) + if err != nil { + return orderDetail, err + } + orderDetail = order.Detail{ + Exchange: h.Name, + ID: strconv.FormatInt(respData.ID, 10), + AccountID: strconv.FormatInt(respData.AccountID, 10), + CurrencyPair: currency.NewPairFromString(respData.Symbol), + OrderType: orderType, + OrderSide: orderSide, + OrderDate: time.Unix(respData.CreatedAt, 0), + Status: orderStatus, + Price: respData.Price, + Amount: respData.Amount, + ExecutedAmount: respData.FilledAmount, + Fee: respData.FilledFees, + } + return orderDetail, nil } // GetDepositAddress returns a deposit address for a specified currency @@ -368,20 +639,20 @@ func (h *HUOBI) GetDepositAddress(cryptocurrency currency.Code, accountID string // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (h *HUOBI) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (h *HUOBI) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { resp, err := h.Withdraw(withdrawRequest.Currency, withdrawRequest.Address, withdrawRequest.AddressTag, withdrawRequest.Amount, withdrawRequest.FeeAmount) - return fmt.Sprintf("%v", resp), err + return strconv.FormatInt(resp, 10), err } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (h *HUOBI) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (h *HUOBI) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (h *HUOBI) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (h *HUOBI) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -392,7 +663,7 @@ func (h *HUOBI) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (h *HUOBI) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (h.APIKey == "" || h.APISecret == "") && // Todo check connection status + if !h.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -400,63 +671,104 @@ func (h *HUOBI) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { } // GetActiveOrders retrieves any orders that are active/open -func (h *HUOBI) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - if len(getOrdersRequest.Currencies) == 0 { +func (h *HUOBI) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { + if len(req.Currencies) == 0 { return nil, errors.New("currency must be supplied") } side := "" - if getOrdersRequest.OrderSide == exchange.AnyOrderSide || getOrdersRequest.OrderSide == "" { + if req.OrderSide == order.AnySide || req.OrderSide == "" { side = "" - } else if getOrdersRequest.OrderSide == exchange.SellOrderSide { - side = strings.ToLower(string(getOrdersRequest.OrderSide)) + } else if req.OrderSide == order.Sell { + side = req.OrderSide.Lower() } - var orders []exchange.OrderDetail + var orders []order.Detail - for _, c := range getOrdersRequest.Currencies { - resp, err := h.GetOpenOrders(h.ClientID, c.Lower().String(), side, 500) - if err != nil { - return nil, err + if h.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + for i := range req.Currencies { + resp, err := h.wsGetOrdersList(-1, req.Currencies[i]) + if err != nil { + return orders, err + } + for j := range resp.Data { + sideData := strings.Split(resp.Data[j].OrderState, "-") + side = sideData[0] + orderSide, err := order.StringToOrderSide(side) + if err != nil { + return orders, err + } + orderType, err := order.StringToOrderType(sideData[1]) + if err != nil { + return orders, err + } + orderStatus, err := order.StringToOrderStatus(resp.Data[j].OrderState) + if err != nil { + return orders, err + } + orders = append(orders, order.Detail{ + Exchange: h.Name, + AccountID: strconv.FormatInt(resp.Data[j].AccountID, 10), + ID: strconv.FormatInt(resp.Data[j].OrderID, 10), + CurrencyPair: req.Currencies[i], + OrderType: orderType, + OrderSide: orderSide, + OrderDate: time.Unix(resp.Data[j].CreatedAt, 0), + Status: orderStatus, + Price: resp.Data[j].Price, + Amount: resp.Data[j].OrderAmount, + ExecutedAmount: resp.Data[j].FilledAmount, + RemainingAmount: resp.Data[j].UnfilledAmount, + Fee: resp.Data[j].FilledFees, + }) + } } - - for i := range resp { - orderDetail := exchange.OrderDetail{ - ID: fmt.Sprintf("%v", resp[i].ID), - Price: resp[i].Price, - Amount: resp[i].Amount, - CurrencyPair: c, - Exchange: h.Name, - ExecutedAmount: resp[i].FilledAmount, - OrderDate: time.Unix(0, resp[i].CreatedAt*int64(time.Millisecond)), - Status: resp[i].State, - AccountID: strconv.FormatFloat(resp[i].AccountID, 'f', -1, 64), - Fee: resp[i].FilledFees, + } else { + for i := range req.Currencies { + resp, err := h.GetOpenOrders(h.API.Credentials.ClientID, + req.Currencies[i].Lower().String(), + side, + 500) + if err != nil { + return nil, err } - setOrderSideAndType(resp[i].Type, &orderDetail) + for i := range resp { + orderDetail := order.Detail{ + ID: strconv.FormatInt(resp[i].ID, 10), + Price: resp[i].Price, + Amount: resp[i].Amount, + CurrencyPair: req.Currencies[i], + Exchange: h.Name, + ExecutedAmount: resp[i].FilledAmount, + OrderDate: time.Unix(0, resp[i].CreatedAt*int64(time.Millisecond)), + Status: order.Status(resp[i].State), + AccountID: strconv.FormatInt(resp[i].AccountID, 10), + Fee: resp[i].FilledFees, + } - orders = append(orders, orderDetail) + setOrderSideAndType(resp[i].Type, &orderDetail) + + orders = append(orders, orderDetail) + } } } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (h *HUOBI) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - if len(getOrdersRequest.Currencies) == 0 { +func (h *HUOBI) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + if len(req.Currencies) == 0 { return nil, errors.New("currency must be supplied") } states := "partial-canceled,filled,canceled" - var orders []exchange.OrderDetail - for _, c := range getOrdersRequest.Currencies { - resp, err := h.GetOrders(c.Lower().String(), + var orders []order.Detail + for i := range req.Currencies { + resp, err := h.GetOrders(req.Currencies[i].Lower().String(), "", "", "", @@ -469,16 +781,16 @@ func (h *HUOBI) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([] } for i := range resp { - orderDetail := exchange.OrderDetail{ - ID: fmt.Sprintf("%v", resp[i].ID), + orderDetail := order.Detail{ + ID: strconv.FormatInt(resp[i].ID, 10), Price: resp[i].Price, Amount: resp[i].Amount, - CurrencyPair: c, + CurrencyPair: req.Currencies[i], Exchange: h.Name, ExecutedAmount: resp[i].FilledAmount, OrderDate: time.Unix(0, resp[i].CreatedAt*int64(time.Millisecond)), - Status: resp[i].State, - AccountID: strconv.FormatFloat(resp[i].AccountID, 'f', -1, 64), + Status: order.Status(resp[i].State), + AccountID: strconv.FormatInt(resp[i].AccountID, 10), Fee: resp[i].FilledFees, } @@ -488,26 +800,24 @@ func (h *HUOBI) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([] } } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) return orders, nil } -func setOrderSideAndType(requestType string, orderDetail *exchange.OrderDetail) { +func setOrderSideAndType(requestType string, orderDetail *order.Detail) { switch SpotNewOrderRequestParamsType(requestType) { case SpotNewOrderRequestTypeBuyMarket: - orderDetail.OrderSide = exchange.BuyOrderSide - orderDetail.OrderType = exchange.MarketOrderType + orderDetail.OrderSide = order.Buy + orderDetail.OrderType = order.Market case SpotNewOrderRequestTypeSellMarket: - orderDetail.OrderSide = exchange.SellOrderSide - orderDetail.OrderType = exchange.MarketOrderType + orderDetail.OrderSide = order.Sell + orderDetail.OrderType = order.Market case SpotNewOrderRequestTypeBuyLimit: - orderDetail.OrderSide = exchange.BuyOrderSide - orderDetail.OrderType = exchange.LimitOrderType + orderDetail.OrderSide = order.Buy + orderDetail.OrderType = order.Limit case SpotNewOrderRequestTypeSellLimit: - orderDetail.OrderSide = exchange.SellOrderSide - orderDetail.OrderType = exchange.LimitOrderType + orderDetail.OrderSide = order.Sell + orderDetail.OrderType = order.Limit } } diff --git a/exchanges/interfaces.go b/exchanges/interfaces.go new file mode 100644 index 00000000..3f5723e3 --- /dev/null +++ b/exchanges/interfaces.go @@ -0,0 +1,70 @@ +package exchange + +import ( + "sync" + + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" +) + +// IBotExchange enforces standard functions for all exchanges supported in +// GoCryptoTrader +type IBotExchange interface { + Setup(exch *config.ExchangeConfig) error + Start(wg *sync.WaitGroup) + SetDefaults() + GetName() string + IsEnabled() bool + SetEnabled(bool) + FetchTicker(currency currency.Pair, assetType asset.Item) (ticker.Price, error) + UpdateTicker(currency currency.Pair, assetType asset.Item) (ticker.Price, error) + FetchOrderbook(currency currency.Pair, assetType asset.Item) (orderbook.Base, error) + UpdateOrderbook(currency currency.Pair, assetType asset.Item) (orderbook.Base, error) + FetchTradablePairs(assetType asset.Item) ([]string, error) + UpdateTradablePairs(forceUpdate bool) error + GetEnabledPairs(assetType asset.Item) currency.Pairs + GetAvailablePairs(assetType asset.Item) currency.Pairs + GetAccountInfo() (AccountInfo, error) + GetAuthenticatedAPISupport(endpoint uint8) bool + SetPairs(pairs currency.Pairs, assetType asset.Item, enabled bool) error + GetAssetTypes() asset.Items + GetExchangeHistory(currencyPair currency.Pair, assetType asset.Item) ([]TradeHistory, error) + SupportsAutoPairUpdates() bool + SupportsRESTTickerBatchUpdates() bool + GetFeeByType(feeBuilder *FeeBuilder) (float64, error) + GetLastPairsUpdateTime() int64 + GetWithdrawPermissions() uint32 + FormatWithdrawPermissions() string + SupportsWithdrawPermissions(permissions uint32) bool + GetFundingHistory() ([]FundHistory, error) + SubmitOrder(s *order.Submit) (order.SubmitResponse, error) + ModifyOrder(action *order.Modify) (string, error) + CancelOrder(order *order.Cancel) error + CancelAllOrders(orders *order.Cancel) (order.CancelAllResponse, error) + GetOrderInfo(orderID string) (order.Detail, error) + GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) + GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) + GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) + WithdrawCryptocurrencyFunds(withdrawRequest *CryptoWithdrawRequest) (string, error) + WithdrawFiatFunds(withdrawRequest *FiatWithdrawRequest) (string, error) + WithdrawFiatFundsToInternationalBank(withdrawRequest *FiatWithdrawRequest) (string, error) + SetHTTPClientUserAgent(ua string) + GetHTTPClientUserAgent() string + SetClientProxyAddress(addr string) error + SupportsWebsocket() bool + SupportsREST() bool + IsWebsocketEnabled() bool + GetWebsocket() (*wshandler.Websocket, error) + SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error + UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error + AuthenticateWebsocket() error + GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) + GetDefaultConfig() (*config.ExchangeConfig, error) + GetBase() *Base + SupportsAsset(assetType asset.Item) bool +} diff --git a/exchanges/itbit/README.md b/exchanges/itbit/README.md index 54d6d3a1..1c87aa4b 100644 --- a/exchanges/itbit/README.md +++ b/exchanges/itbit/README.md @@ -47,22 +47,22 @@ main.go ```go var i exchange.IBotExchange -for x := range bot.exchanges { - if bot.exchanges[x].GetName() == "Itbit" { - i = bot.exchanges[x] +for x := range bot.Exchanges { + if bot.Exchanges[x].GetName() == "Itbit" { + i = bot.Exchanges[x] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := i.GetTickerPrice() +tick, err := i.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := i.GetOrderbookEx() +ob, err := i.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +130,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/itbit/itbit.go b/exchanges/itbit/itbit.go index a7888d8b..555853f5 100644 --- a/exchanges/itbit/itbit.go +++ b/exchanges/itbit/itbit.go @@ -11,12 +11,9 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -43,76 +40,11 @@ type ItBit struct { exchange.Base } -// SetDefaults sets the defaults for the exchange -func (i *ItBit) SetDefaults() { - i.Name = "ITBIT" - i.Enabled = false - i.MakerFee = -0.10 - i.TakerFee = 0.50 - i.Verbose = false - i.RESTPollingDelay = 10 - i.APIWithdrawPermissions = exchange.WithdrawCryptoViaWebsiteOnly | - exchange.WithdrawFiatViaWebsiteOnly - i.RequestCurrencyPairFormat.Delimiter = "" - i.RequestCurrencyPairFormat.Uppercase = true - i.ConfigCurrencyPairFormat.Delimiter = "" - i.ConfigCurrencyPairFormat.Uppercase = true - i.AssetTypes = []string{ticker.Spot} - i.SupportsAutoPairUpdating = false - i.SupportsRESTTickerBatching = false - i.Requester = request.New(i.Name, - request.NewRateLimit(time.Second, itbitAuthRate), - request.NewRateLimit(time.Second, itbitUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - i.APIUrlDefault = itbitAPIURL - i.APIUrl = i.APIUrlDefault - i.Websocket = wshandler.New() -} - -// Setup sets the exchange parameters from exchange config -func (i *ItBit) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - i.SetEnabled(false) - } else { - i.Enabled = true - i.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - i.SetAPIKeys(exch.APIKey, exch.APISecret, exch.ClientID, false) - i.SetHTTPClientTimeout(exch.HTTPTimeout) - i.SetHTTPClientUserAgent(exch.HTTPUserAgent) - i.RESTPollingDelay = exch.RESTPollingDelay - i.Verbose = exch.Verbose - i.HTTPDebugging = exch.HTTPDebugging - i.BaseCurrencies = exch.BaseCurrencies - i.AvailablePairs = exch.AvailablePairs - i.EnabledPairs = exch.EnabledPairs - err := i.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = i.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = i.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = i.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = i.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - } -} - // GetTicker returns ticker info for a specified market. // currencyPair - example "XBTUSD" "XBTSGD" "XBTEUR" func (i *ItBit) GetTicker(currencyPair string) (Ticker, error) { var response Ticker - path := fmt.Sprintf("%s/%s/%s/%s", i.APIUrl, itbitMarkets, currencyPair, itbitTicker) + path := fmt.Sprintf("%s/%s/%s/%s", i.API.Endpoints.URL, itbitMarkets, currencyPair, itbitTicker) return response, i.SendHTTPRequest(path, &response) } @@ -121,7 +53,7 @@ func (i *ItBit) GetTicker(currencyPair string) (Ticker, error) { // currencyPair - example "XBTUSD" "XBTSGD" "XBTEUR" func (i *ItBit) GetOrderbook(currencyPair string) (OrderbookResponse, error) { response := OrderbookResponse{} - path := fmt.Sprintf("%s/%s/%s/%s", i.APIUrl, itbitMarkets, currencyPair, itbitOrderbook) + path := fmt.Sprintf("%s/%s/%s/%s", i.API.Endpoints.URL, itbitMarkets, currencyPair, itbitOrderbook) return response, i.SendHTTPRequest(path, &response) } @@ -133,7 +65,7 @@ func (i *ItBit) GetOrderbook(currencyPair string) (OrderbookResponse, error) { func (i *ItBit) GetTradeHistory(currencyPair, timestamp string) (Trades, error) { response := Trades{} req := "trades?since=" + timestamp - path := fmt.Sprintf("%s/%s/%s/%s", i.APIUrl, itbitMarkets, currencyPair, req) + path := fmt.Sprintf("%s/%s/%s/%s", i.API.Endpoints.URL, itbitMarkets, currencyPair, req) return response, i.SendHTTPRequest(path, &response) } @@ -145,9 +77,8 @@ func (i *ItBit) GetTradeHistory(currencyPair, timestamp string) (Trades, error) // perPage - [optional] items per page example 50, default 50 max 50 func (i *ItBit) GetWallets(params url.Values) ([]Wallet, error) { var resp []Wallet - params.Set("userId", i.ClientID) + params.Set("userId", i.API.Credentials.ClientID) path := fmt.Sprintf("/%s?%s", itbitWallets, params.Encode()) - return resp, i.SendAuthenticatedHTTPRequest(http.MethodGet, path, nil, &resp) } @@ -155,7 +86,7 @@ func (i *ItBit) GetWallets(params url.Values) ([]Wallet, error) { func (i *ItBit) CreateWallet(walletName string) (Wallet, error) { resp := Wallet{} params := make(map[string]interface{}) - params["userId"] = i.ClientID + params["userId"] = i.API.Credentials.ClientID params["name"] = walletName err := i.SendAuthenticatedHTTPRequest(http.MethodPost, "/"+itbitWallets, params, &resp) @@ -359,16 +290,12 @@ func (i *ItBit) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated request to itBit func (i *ItBit) SendAuthenticatedHTTPRequest(method, path string, params map[string]interface{}, result interface{}) error { - if !i.AuthenticatedAPISupport { + if !i.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, i.Name) } - if i.ClientID == "" { - return errors.New("client ID not set") - } - req := make(map[string]interface{}) - urlPath := i.APIUrl + path + urlPath := i.API.Endpoints.URL + path for key, value := range params { req[key] = value @@ -378,30 +305,29 @@ func (i *ItBit) SendAuthenticatedHTTPRequest(method, path string, params map[str var err error if params != nil { - PayloadJSON, err = common.JSONEncode(req) + PayloadJSON, err = json.Marshal(req) if err != nil { return err } if i.Verbose { - log.Debugf("Request JSON: %s\n", PayloadJSON) + log.Debugf(log.ExchangeSys, "Request JSON: %s\n", PayloadJSON) } } n := i.Requester.GetNonce(true).String() timestamp := strconv.FormatInt(time.Now().UnixNano()/1000000, 10) - - message, err := common.JSONEncode([]string{method, urlPath, string(PayloadJSON), n, timestamp}) + message, err := json.Marshal([]string{method, urlPath, string(PayloadJSON), n, timestamp}) if err != nil { return err } - hash := common.GetSHA256([]byte(n + string(message))) - hmac := common.GetHMAC(common.HashSHA512, []byte(urlPath+string(hash)), []byte(i.APISecret)) - signature := common.Base64Encode(hmac) + hash := crypto.GetSHA256([]byte(n + string(message))) + hmac := crypto.GetHMAC(crypto.HashSHA512, []byte(urlPath+string(hash)), []byte(i.API.Credentials.Secret)) + signature := crypto.Base64Encode(hmac) headers := make(map[string]string) - headers["Authorization"] = i.ClientID + ":" + signature + headers["Authorization"] = i.API.Credentials.ClientID + ":" + signature headers["X-Auth-Timestamp"] = timestamp headers["X-Auth-Nonce"] = n headers["Content-Type"] = "application/json" @@ -428,7 +354,7 @@ func (i *ItBit) SendAuthenticatedHTTPRequest(method, path string, params map[str return err } - err = common.JSONDecode(intermediary, &errCheck) + err = json.Unmarshal(intermediary, &errCheck) if err == nil { if errCheck.Code != 0 || errCheck.Description != "" { return fmt.Errorf("itbit.go SendAuthRequest error code: %d description: %s", @@ -437,7 +363,7 @@ func (i *ItBit) SendAuthenticatedHTTPRequest(method, path string, params map[str } } - return common.JSONDecode(intermediary, result) + return json.Unmarshal(intermediary, result) } // GetFee returns an estimate of fee based on type of transaction diff --git a/exchanges/itbit/itbit_test.go b/exchanges/itbit/itbit_test.go index f9581722..85cfd4d6 100644 --- a/exchanges/itbit/itbit_test.go +++ b/exchanges/itbit/itbit_test.go @@ -1,13 +1,16 @@ package itbit import ( + "log" "net/url" + "os" "testing" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) var i ItBit @@ -20,30 +23,35 @@ const ( canManipulateRealOrders = false ) -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { i.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Itbit load config error", err) + } itbitConfig, err := cfg.GetExchangeConfig("ITBIT") if err != nil { - t.Error("Test Failed - Gemini Setup() init error") + log.Fatal("Itbit Setup() init error") } - itbitConfig.AuthenticatedAPISupport = true - itbitConfig.APIKey = apiKey - itbitConfig.APISecret = apiSecret - itbitConfig.ClientID = clientID + itbitConfig.API.AuthenticatedSupport = true + itbitConfig.API.Credentials.Key = apiKey + itbitConfig.API.Credentials.Secret = apiSecret + itbitConfig.API.Credentials.ClientID = clientID - i.Setup(&itbitConfig) + err = i.Setup(itbitConfig) + if err != nil { + log.Fatal("Itbit setup error", err) + } + + os.Exit(m.Run()) } func TestGetTicker(t *testing.T) { t.Parallel() _, err := i.GetTicker("XBTUSD") if err != nil { - t.Error("Test Failed - GetTicker() error", err) + t.Error("GetTicker() error", err) } } @@ -51,7 +59,7 @@ func TestGetOrderbook(t *testing.T) { t.Parallel() _, err := i.GetOrderbook("XBTSGD") if err != nil { - t.Error("Test Failed - GetOrderbook() error", err) + t.Error("GetOrderbook() error", err) } } @@ -59,63 +67,65 @@ func TestGetTradeHistory(t *testing.T) { t.Parallel() _, err := i.GetTradeHistory("XBTUSD", "0") if err != nil { - t.Error("Test Failed - GetTradeHistory() error", err) + t.Error("GetTradeHistory() error", err) } } func TestGetWallets(t *testing.T) { _, err := i.GetWallets(url.Values{}) if err == nil { - t.Error("Test Failed - GetWallets() error", err) + t.Error("GetWallets() Expected error") } } func TestCreateWallet(t *testing.T) { _, err := i.CreateWallet("test") if err == nil { - t.Error("Test Failed - CreateWallet() error", err) + t.Error("CreateWallet() Expected error") } } func TestGetWallet(t *testing.T) { _, err := i.GetWallet("1337") if err == nil { - t.Error("Test Failed - GetWallet() error", err) + t.Error("GetWallet() Expected error") } } func TestGetWalletBalance(t *testing.T) { _, err := i.GetWalletBalance("1337", "XRT") if err == nil { - t.Error("Test Failed - GetWalletBalance() error", err) + t.Error("GetWalletBalance() Expected error") } } func TestGetWalletTrades(t *testing.T) { _, err := i.GetWalletTrades("1337", url.Values{}) if err == nil { - t.Error("Test Failed - GetWalletTrades() error", err) + t.Error("GetWalletTrades() Expected error") } } func TestGetFundingHistory(t *testing.T) { _, err := i.GetFundingHistoryForWallet("1337", url.Values{}) if err == nil { - t.Error("Test Failed - GetFundingHistory() error", err) + t.Error("GetFundingHistory() Expected error") } } func TestPlaceOrder(t *testing.T) { - _, err := i.PlaceOrder("1337", "buy", "limit", "USD", 1, 0.2, "banjo", "sauce") + _, err := i.PlaceOrder("1337", order.Buy.Lower(), + order.Limit.Lower(), "USD", 1, 0.2, "banjo", + "sauce") if err == nil { - t.Error("Test Failed - PlaceOrder() error", err) + t.Error("PlaceOrder() Expected error") } } func TestGetOrder(t *testing.T) { _, err := i.GetOrder("1337", url.Values{}) if err == nil { - t.Error("Test Failed - GetOrder() error", err) + t.Error("GetOrder() Expected error") } } @@ -123,21 +133,21 @@ func TestCancelExistingOrder(t *testing.T) { t.Skip() err := i.CancelExistingOrder("1337", "1337order") if err == nil { - t.Error("Test Failed - CancelOrder() error", err) + t.Error("CancelOrder() Expected error") } } func TestGetCryptoDepositAddress(t *testing.T) { _, err := i.GetCryptoDepositAddress("1337", "AUD") if err == nil { - t.Error("Test Failed - GetCryptoDepositAddress() error", err) + t.Error("GetCryptoDepositAddress() Expected error") } } func TestWalletTransfer(t *testing.T) { _, err := i.WalletTransfer("1337", "mywallet", "anotherwallet", 200, "USD") if err == nil { - t.Error("Test Failed - WalletTransfer() error", err) + t.Error("WalletTransfer() Expected error") } } @@ -158,7 +168,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() i.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -175,7 +185,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee Basic if resp, err := i.GetFee(feeBuilder); resp != float64(0.0035) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0035), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0035), resp) } // CryptocurrencyTradeFee High quantity @@ -183,7 +193,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := i.GetFee(feeBuilder); resp != float64(3500) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(3500), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(3500), resp) t.Error(err) } @@ -191,7 +201,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -199,14 +209,14 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -215,7 +225,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -223,7 +233,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -231,7 +241,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := i.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -240,28 +250,22 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := i.GetFee(feeBuilder); resp != float64(40) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(40), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(40), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - i.SetDefaults() expectedResult := exchange.WithdrawCryptoViaWebsiteOnlyText + " & " + exchange.WithdrawFiatViaWebsiteOnlyText - withdrawPermissions := i.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - i.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := i.GetActiveOrders(&getOrdersRequest) @@ -273,11 +277,8 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - i.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := i.GetOrderHistory(&getOrdersRequest) @@ -291,26 +292,26 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if i.APIKey != "" && i.APIKey != "Key" && - i.APISecret != "" && i.APISecret != "Secret" { - return true - } - return false + return i.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - i.SetDefaults() - TestSetup(t) if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.BTC, - Quote: currency.USDT, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := i.SubmitOrder(p, exchange.BuyOrderSide, exchange.LimitOrderType, 1, 10, "hi") + response, err := i.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -319,16 +320,12 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - i.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -346,16 +343,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - i.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -371,35 +364,38 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestGetAccountInfo(t *testing.T) { - if apiKey != "" || apiSecret != "" || clientID != "" { + if areTestAPIKeysSet() { _, err := i.GetAccountInfo() if err == nil { - t.Error("Test Failed - GetAccountInfo() error") + t.Error("GetAccountInfo() Expected error") } } } func TestModifyOrder(t *testing.T) { - _, err := i.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := i.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - i.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.LTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -413,15 +409,11 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - i.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := i.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -429,15 +421,11 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - i.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := i.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -447,6 +435,6 @@ func TestWithdrawInternationalBank(t *testing.T) { func TestGetDepositAddress(t *testing.T) { _, err := i.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error cannot be nil") + t.Error("GetDepositAddress() error cannot be nil") } } diff --git a/exchanges/itbit/itbit_types.go b/exchanges/itbit/itbit_types.go index c3d06b07..6408c3cf 100644 --- a/exchanges/itbit/itbit_types.go +++ b/exchanges/itbit/itbit_types.go @@ -1,5 +1,7 @@ package itbit +import "time" + // GeneralReturn is a generalized return type to capture any errors type GeneralReturn struct { Code int `json:"code"` @@ -9,23 +11,23 @@ type GeneralReturn struct { // Ticker holds returned ticker information type Ticker struct { - Pair string `json:"pair"` - Bid float64 `json:"bid,string"` - BidAmt float64 `json:"bidAmt,string"` - Ask float64 `json:"ask,string"` - AskAmt float64 `json:"askAmt,string"` - LastPrice float64 `json:"lastPrice,string"` - LastAmt float64 `json:"lastAmt,string"` - Volume24h float64 `json:"volume24h,string"` - VolumeToday float64 `json:"volumeToday,string"` - High24h float64 `json:"high24h,string"` - Low24h float64 `json:"low24h,string"` - HighToday float64 `json:"highToday,string"` - LowToday float64 `json:"lowToday,string"` - OpenToday float64 `json:"openToday,string"` - VwapToday float64 `json:"vwapToday,string"` - Vwap24h float64 `json:"vwap24h,string"` - ServertimeUTC string `json:"serverTimeUTC"` + Pair string `json:"pair"` + Bid float64 `json:"bid,string"` + BidAmt float64 `json:"bidAmt,string"` + Ask float64 `json:"ask,string"` + AskAmt float64 `json:"askAmt,string"` + LastPrice float64 `json:"lastPrice,string"` + LastAmt float64 `json:"lastAmt,string"` + Volume24h float64 `json:"volume24h,string"` + VolumeToday float64 `json:"volumeToday,string"` + High24h float64 `json:"high24h,string"` + Low24h float64 `json:"low24h,string"` + HighToday float64 `json:"highToday,string"` + LowToday float64 `json:"lowToday,string"` + OpenToday float64 `json:"openToday,string"` + VwapToday float64 `json:"vwapToday,string"` + Vwap24h float64 `json:"vwap24h,string"` + ServertimeUTC time.Time `json:"serverTimeUTC"` } // OrderbookResponse contains multi-arrayed strings of bid and ask side diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index a8dce7b0..918b877d 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -9,14 +9,110 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (i *ItBit) GetDefaultConfig() (*config.ExchangeConfig, error) { + i.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = i.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = i.BaseCurrencies + + err := i.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if i.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = i.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the defaults for the exchange +func (i *ItBit) SetDefaults() { + i.Name = "ITBIT" + i.Enabled = true + i.Verbose = true + i.API.CredentialsValidator.RequiresClientID = true + i.API.CredentialsValidator.RequiresSecret = true + + i.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + } + + i.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: false, + RESTCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + SubmitOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + CryptoDeposit: true, + TradeFee: true, + FiatWithdrawalFee: true, + }, + WithdrawPermissions: exchange.WithdrawCryptoViaWebsiteOnly | + exchange.WithdrawFiatViaWebsiteOnly, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: false, + }, + } + + i.Requester = request.New(i.Name, + request.NewRateLimit(time.Second, itbitAuthRate), + request.NewRateLimit(time.Second, itbitUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + i.API.Endpoints.URLDefault = itbitAPIURL + i.API.Endpoints.URL = i.API.Endpoints.URLDefault +} + +// Setup sets the exchange parameters from exchange config +func (i *ItBit) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + i.SetEnabled(false) + return nil + } + + return i.SetupDefaults(exch) +} + // Start starts the ItBit go routine func (i *ItBit) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -29,29 +125,40 @@ func (i *ItBit) Start(wg *sync.WaitGroup) { // Run implements the ItBit wrapper func (i *ItBit) Run() { if i.Verbose { - log.Debugf("%s polling delay: %ds.\n", i.GetName(), i.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", i.GetName(), len(i.EnabledPairs), i.EnabledPairs) + i.PrintEnabledPairs() } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (i *ItBit) FetchTradablePairs(asset asset.Item) ([]string, error) { + return nil, common.ErrFunctionNotSupported +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (i *ItBit) UpdateTradablePairs(forceUpdate bool) error { + return common.ErrFunctionNotSupported +} + // UpdateTicker updates and returns the ticker for a currency pair -func (i *ItBit) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (i *ItBit) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - tick, err := i.GetTicker(exchange.FormatExchangeCurrency(i.Name, - p).String()) + tick, err := i.GetTicker(i.FormatExchangeCurrency(p, assetType).String()) if err != nil { return tickerPrice, err } - - tickerPrice.Pair = p - tickerPrice.Ask = tick.Ask - tickerPrice.Bid = tick.Bid - tickerPrice.Last = tick.LastPrice - tickerPrice.High = tick.High24h - tickerPrice.Low = tick.Low24h - tickerPrice.Volume = tick.Volume24h - - err = ticker.ProcessTicker(i.GetName(), &tickerPrice, assetType) + tickerPrice = ticker.Price{ + Last: tick.LastPrice, + High: tick.High24h, + Low: tick.Low24h, + Bid: tick.Bid, + Ask: tick.Ask, + Volume: tick.Volume24h, + Open: tick.OpenToday, + Pair: p, + LastUpdated: tick.ServertimeUTC, + } + err = ticker.ProcessTicker(i.Name, &tickerPrice, assetType) if err != nil { return tickerPrice, err } @@ -59,18 +166,18 @@ func (i *ItBit) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, e return ticker.GetTicker(i.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (i *ItBit) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(i.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (i *ItBit) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(i.Name, p, assetType) if err != nil { return i.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (i *ItBit) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(i.GetName(), p, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (i *ItBit) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(i.Name, p, assetType) if err != nil { return i.UpdateOrderbook(p, assetType) } @@ -78,44 +185,49 @@ func (i *ItBit) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Bas } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (i *ItBit) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (i *ItBit) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := i.GetOrderbook(exchange.FormatExchangeCurrency(i.Name, - p).String()) + orderbookNew, err := i.GetOrderbook(i.FormatExchangeCurrency(p, assetType).String()) if err != nil { return orderBook, err } for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] var price, amount float64 - price, err = strconv.ParseFloat(data[0], 64) + price, err = strconv.ParseFloat(orderbookNew.Bids[x][0], 64) if err != nil { return orderBook, err } - amount, err = strconv.ParseFloat(data[1], 64) + amount, err = strconv.ParseFloat(orderbookNew.Bids[x][1], 64) if err != nil { return orderBook, err } - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: amount, Price: price}) + orderBook.Bids = append(orderBook.Bids, + orderbook.Item{ + Amount: amount, + Price: price, + }) } for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] var price, amount float64 - price, err = strconv.ParseFloat(data[0], 64) + price, err = strconv.ParseFloat(orderbookNew.Asks[x][0], 64) if err != nil { return orderBook, err } - amount, err = strconv.ParseFloat(data[1], 64) + amount, err = strconv.ParseFloat(orderbookNew.Asks[x][1], 64) if err != nil { return orderBook, err } - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: amount, Price: price}) + orderBook.Asks = append(orderBook.Asks, + orderbook.Item{ + Amount: amount, + Price: price, + }) } orderBook.Pair = p - orderBook.ExchangeName = i.GetName() + orderBook.ExchangeName = i.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -129,7 +241,7 @@ func (i *ItBit) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Ba // GetAccountInfo retrieves balances for all enabled currencies func (i *ItBit) GetAccountInfo() (exchange.AccountInfo, error) { var info exchange.AccountInfo - info.Exchange = i.GetName() + info.Exchange = i.Name wallets, err := i.GetWallets(url.Values{}) if err != nil { @@ -143,8 +255,8 @@ func (i *ItBit) GetAccountInfo() (exchange.AccountInfo, error) { var amounts = make(map[string]*balance) - for _, wallet := range wallets { - for _, cb := range wallet.Balances { + for x := range wallets { + for _, cb := range wallets[x].Balances { if _, ok := amounts[cb.Currency]; !ok { amounts[cb.Currency] = &balance{} } @@ -155,12 +267,11 @@ func (i *ItBit) GetAccountInfo() (exchange.AccountInfo, error) { } var fullBalance []exchange.AccountCurrencyInfo - - for key, data := range amounts { + for key := range amounts { fullBalance = append(fullBalance, exchange.AccountCurrencyInfo{ CurrencyName: currency.NewCode(key), - TotalValue: data.TotalValue, - Hold: data.Hold, + TotalValue: amounts[key].TotalValue, + Hold: amounts[key].Hold, }) } @@ -174,33 +285,33 @@ func (i *ItBit) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (i *ItBit) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (i *ItBit) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (i *ItBit) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (i *ItBit) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - var wallet string +func (i *ItBit) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } + var wallet string wallets, err := i.GetWallets(url.Values{}) if err != nil { return submitOrderResponse, err } // Determine what wallet ID to use if there is any actual available currency to make the trade! - for _, i := range wallets { - for j := range i.Balances { - if i.Balances[j].Currency == p.Base.String() && - i.Balances[j].AvailableBalance >= amount { - wallet = i.ID + for i := range wallets { + for j := range wallets[i].Balances { + if wallets[i].Balances[j].Currency == s.Pair.Base.String() && + wallets[i].Balances[j].AvailableBalance >= s.Amount { + wallet = wallets[i].ID } } } @@ -208,45 +319,47 @@ func (i *ItBit) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType if wallet == "" { return submitOrderResponse, fmt.Errorf("no wallet found with currency: %s with amount >= %v", - p.Base, - amount) + s.Pair.Base, + s.Amount) } response, err := i.PlaceOrder(wallet, - side.ToString(), - orderType.ToString(), - p.Base.String(), - amount, - price, - p.String(), + s.OrderSide.String(), + s.OrderType.String(), + s.Pair.Base.String(), + s.Amount, + s.Price, + s.Pair.String(), "") - + if err != nil { + return submitOrderResponse, err + } if response.ID != "" { submitOrderResponse.OrderID = response.ID } - if err == nil { - submitOrderResponse.IsOrderPlaced = true + if response.AmountFilled == s.Amount { + submitOrderResponse.FullyMatched = true } - - return submitOrderResponse, err + submitOrderResponse.IsOrderPlaced = true + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (i *ItBit) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (i *ItBit) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (i *ItBit) CancelOrder(order *exchange.OrderCancellation) error { +func (i *ItBit) CancelOrder(order *order.Cancel) error { return i.CancelExistingOrder(order.WalletAddress, order.OrderID) } // CancelAllOrders cancels all orders associated with a currency pair -func (i *ItBit) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (i *ItBit) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } openOrders, err := i.GetOrders(orderCancellation.WalletAddress, "", "open", 0, 0) if err != nil { @@ -256,7 +369,7 @@ func (i *ItBit) CancelAllOrders(orderCancellation *exchange.OrderCancellation) ( for j := range openOrders { err = i.CancelExistingOrder(orderCancellation.WalletAddress, openOrders[j].ID) if err != nil { - cancelAllOrdersResponse.OrderStatus[openOrders[j].ID] = err.Error() + cancelAllOrdersResponse.Status[openOrders[j].ID] = err.Error() } } @@ -264,8 +377,8 @@ func (i *ItBit) CancelAllOrders(orderCancellation *exchange.OrderCancellation) ( } // GetOrderInfo returns information on a current open order -func (i *ItBit) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (i *ItBit) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -279,19 +392,19 @@ func (i *ItBit) GetDepositAddress(cryptocurrency currency.Code, accountID string // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (i *ItBit) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (i *ItBit) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (i *ItBit) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (i *ItBit) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (i *ItBit) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (i *ItBit) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -302,7 +415,7 @@ func (i *ItBit) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (i *ItBit) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (i.APIKey == "" || i.APISecret == "") && // Todo check connection status + if !i.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -310,33 +423,37 @@ func (i *ItBit) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { } // GetActiveOrders retrieves any orders that are active/open -func (i *ItBit) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (i *ItBit) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { wallets, err := i.GetWallets(url.Values{}) if err != nil { return nil, err } var allOrders []Order - for _, wallet := range wallets { - resp, err := i.GetOrders(wallet.ID, "", "open", 0, 0) + for x := range wallets { + resp, err := i.GetOrders(wallets[x].ID, "", "open", 0, 0) if err != nil { return nil, err } allOrders = append(allOrders, resp...) } - var orders []exchange.OrderDetail + var orders []order.Detail for j := range allOrders { symbol := currency.NewPairDelimiter(allOrders[j].Instrument, - i.ConfigCurrencyPairFormat.Delimiter) - side := exchange.OrderSide(strings.ToUpper(allOrders[j].Side)) + i.GetPairFormat(asset.Spot, false).Delimiter) + side := order.Side(strings.ToUpper(allOrders[j].Side)) orderDate, err := time.Parse(time.RFC3339, allOrders[j].CreatedTime) if err != nil { - log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v", - i.Name, "GetActiveOrders", allOrders[j].ID, allOrders[j].CreatedTime) + log.Errorf(log.ExchangeSys, + "Exchange %v Func %v Order %v Could not parse date to unix with value of %v", + i.Name, + "GetActiveOrders", + allOrders[j].ID, + allOrders[j].CreatedTime) } - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ ID: allOrders[j].ID, OrderSide: side, Amount: allOrders[j].Amount, @@ -348,47 +465,49 @@ func (i *ItBit) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([] }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (i *ItBit) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (i *ItBit) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { wallets, err := i.GetWallets(url.Values{}) if err != nil { return nil, err } var allOrders []Order - for _, wallet := range wallets { - resp, err := i.GetOrders(wallet.ID, "", "", 0, 0) + for x := range wallets { + resp, err := i.GetOrders(wallets[x].ID, "", "", 0, 0) if err != nil { return nil, err } allOrders = append(allOrders, resp...) } - var orders []exchange.OrderDetail + var orders []order.Detail for j := range allOrders { if allOrders[j].Type == "open" { continue } symbol := currency.NewPairDelimiter(allOrders[j].Instrument, - i.ConfigCurrencyPairFormat.Delimiter) - side := exchange.OrderSide(strings.ToUpper(allOrders[j].Side)) + i.GetPairFormat(asset.Spot, false).Delimiter) + side := order.Side(strings.ToUpper(allOrders[j].Side)) orderDate, err := time.Parse(time.RFC3339, allOrders[j].CreatedTime) if err != nil { - log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v", - i.Name, "GetActiveOrders", allOrders[j].ID, allOrders[j].CreatedTime) + log.Errorf(log.ExchangeSys, + "Exchange %v Func %v Order %v Could not parse date to unix with value of %v", + i.Name, + "GetActiveOrders", + allOrders[j].ID, + allOrders[j].CreatedTime) } - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ ID: allOrders[j].ID, OrderSide: side, Amount: allOrders[j].Amount, @@ -400,11 +519,9 @@ func (i *ItBit) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([] }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } diff --git a/exchanges/kraken/README.md b/exchanges/kraken/README.md index dbcf0be9..7992a499 100644 --- a/exchanges/kraken/README.md +++ b/exchanges/kraken/README.md @@ -47,22 +47,22 @@ main.go ```go var k exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Kraken" { - k = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Kraken" { + k = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := k.GetTickerPrice() +tick, err := k.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := k.GetOrderbookEx() +ob, err := k.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +130,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/kraken/kraken.go b/exchanges/kraken/kraken.go index 91d365dd..ca487ee8 100644 --- a/exchanges/kraken/kraken.go +++ b/exchanges/kraken/kraken.go @@ -8,14 +8,11 @@ import ( "strconv" "strings" "sync" - "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -50,130 +47,25 @@ const ( krakenDepositAddresses = "DepositAddresses" krakenWithdrawStatus = "WithdrawStatus" krakenWithdrawCancel = "WithdrawCancel" + krakenWebsocketToken = "GetWebSocketsToken" krakenAuthRate = 0 krakenUnauthRate = 0 ) +var assetPairMap map[string]string + // Kraken is the overarching type across the alphapoint package type Kraken struct { exchange.Base - WebsocketConn *wshandler.WebsocketConnection - CryptoFee, FiatFee float64 - wsRequestMtx sync.Mutex -} - -// SetDefaults sets current default settings -func (k *Kraken) SetDefaults() { - k.Name = "Kraken" - k.Enabled = false - k.FiatFee = 0.35 - k.CryptoFee = 0.10 - k.Verbose = false - k.RESTPollingDelay = 10 - k.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithSetup | - exchange.WithdrawCryptoWith2FA | - exchange.AutoWithdrawFiatWithSetup | - exchange.WithdrawFiatWith2FA - k.RequestCurrencyPairFormat.Delimiter = "" - k.RequestCurrencyPairFormat.Uppercase = true - k.RequestCurrencyPairFormat.Separator = "," - k.ConfigCurrencyPairFormat.Delimiter = "-" - k.ConfigCurrencyPairFormat.Uppercase = true - k.AssetTypes = []string{ticker.Spot} - k.SupportsAutoPairUpdating = true - k.SupportsRESTTickerBatching = true - k.Requester = request.New(k.Name, - request.NewRateLimit(time.Second, krakenAuthRate), - request.NewRateLimit(time.Second, krakenUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - k.APIUrlDefault = krakenAPIURL - k.APIUrl = k.APIUrlDefault - k.Websocket = wshandler.New() - k.WebsocketURL = krakenWSURL - k.Websocket.Functionality = wshandler.WebsocketTickerSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketKlineSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketMessageCorrelationSupported - k.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - k.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - k.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup sets current exchange configuration -func (k *Kraken) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - k.SetEnabled(false) - } else { - k.Enabled = true - k.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - k.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - k.SetHTTPClientTimeout(exch.HTTPTimeout) - k.SetHTTPClientUserAgent(exch.HTTPUserAgent) - k.RESTPollingDelay = exch.RESTPollingDelay - k.Verbose = exch.Verbose - k.HTTPDebugging = exch.HTTPDebugging - k.Websocket.SetWsStatusAndConnection(exch.Websocket) - k.BaseCurrencies = exch.BaseCurrencies - k.AvailablePairs = exch.AvailablePairs - k.EnabledPairs = exch.EnabledPairs - err := k.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = k.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = k.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = k.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = k.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = k.Websocket.Setup(k.WsConnect, - k.Subscribe, - k.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - krakenWSURL, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - k.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: k.Name, - URL: k.Websocket.GetWebsocketURL(), - ProxyURL: k.Websocket.GetProxyAddress(), - Verbose: k.Verbose, - RateLimit: krakenWsRateLimit, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - k.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - true, - false, - false, - exch.Name) - } + WebsocketConn *wshandler.WebsocketConnection + AuthenticatedWebsocketConn *wshandler.WebsocketConnection + wsRequestMtx sync.Mutex } // GetServerTime returns current server time func (k *Kraken) GetServerTime() (TimeResponse, error) { - path := fmt.Sprintf("%s/%s/public/%s", k.APIUrl, krakenAPIVersion, krakenServerTime) + path := fmt.Sprintf("%s/%s/public/%s", k.API.Endpoints.URL, krakenAPIVersion, krakenServerTime) var response struct { Error []string `json:"error"` @@ -189,7 +81,7 @@ func (k *Kraken) GetServerTime() (TimeResponse, error) { // GetAssets returns a full asset list func (k *Kraken) GetAssets() (map[string]Asset, error) { - path := fmt.Sprintf("%s/%s/public/%s", k.APIUrl, krakenAPIVersion, krakenAssets) + path := fmt.Sprintf("%s/%s/public/%s", k.API.Endpoints.URL, krakenAPIVersion, krakenAssets) var response struct { Error []string `json:"error"` @@ -205,7 +97,7 @@ func (k *Kraken) GetAssets() (map[string]Asset, error) { // GetAssetPairs returns a full asset pair list func (k *Kraken) GetAssetPairs() (map[string]AssetPairs, error) { - path := fmt.Sprintf("%s/%s/public/%s", k.APIUrl, krakenAPIVersion, krakenAssetPairs) + path := fmt.Sprintf("%s/%s/public/%s", k.API.Endpoints.URL, krakenAPIVersion, krakenAssetPairs) var response struct { Error []string `json:"error"` @@ -215,6 +107,12 @@ func (k *Kraken) GetAssetPairs() (map[string]AssetPairs, error) { if err := k.SendHTTPRequest(path, &response); err != nil { return response.Result, err } + for i := range response.Result { + if assetPairMap == nil { + assetPairMap = make(map[string]string) + } + assetPairMap[i] = response.Result[i].Altname + } return response.Result, GetError(response.Error) } @@ -231,7 +129,7 @@ func (k *Kraken) GetTicker(symbol string) (Ticker, error) { } resp := Response{} - path := fmt.Sprintf("%s/%s/public/%s?%s", k.APIUrl, krakenAPIVersion, krakenTicker, values.Encode()) + path := fmt.Sprintf("%s/%s/public/%s?%s", k.API.Endpoints.URL, krakenAPIVersion, krakenTicker, values.Encode()) err := k.SendHTTPRequest(path, &resp) if err != nil { @@ -247,7 +145,7 @@ func (k *Kraken) GetTicker(symbol string) (Ticker, error) { tick.Bid, _ = strconv.ParseFloat(resp.Data[i].Bid[0], 64) tick.Last, _ = strconv.ParseFloat(resp.Data[i].Last[0], 64) tick.Volume, _ = strconv.ParseFloat(resp.Data[i].Volume[1], 64) - tick.VWAP, _ = strconv.ParseFloat(resp.Data[i].VWAP[1], 64) + tick.VolumeWeightedAveragePrice, _ = strconv.ParseFloat(resp.Data[i].VolumeWeightedAveragePrice[1], 64) tick.Trades = resp.Data[i].Trades[1] tick.Low, _ = strconv.ParseFloat(resp.Data[i].Low[1], 64) tick.High, _ = strconv.ParseFloat(resp.Data[i].High[1], 64) @@ -259,7 +157,7 @@ func (k *Kraken) GetTicker(symbol string) (Ticker, error) { // GetTickers supports fetching multiple tickers from Kraken // pairList must be in the format pairs separated by commas // ("LTCUSD,ETCUSD") -func (k *Kraken) GetTickers(pairList string) (Tickers, error) { +func (k *Kraken) GetTickers(pairList string) (map[string]Ticker, error) { values := url.Values{} values.Set("pair", pairList) @@ -269,7 +167,7 @@ func (k *Kraken) GetTickers(pairList string) (Tickers, error) { } resp := Response{} - path := fmt.Sprintf("%s/%s/public/%s?%s", krakenAPIURL, krakenAPIVersion, krakenTicker, values.Encode()) + path := fmt.Sprintf("%s/%s/public/%s?%s", k.API.Endpoints.URL, krakenAPIVersion, krakenTicker, values.Encode()) err := k.SendHTTPRequest(path, &resp) if err != nil { @@ -280,7 +178,7 @@ func (k *Kraken) GetTickers(pairList string) (Tickers, error) { return nil, fmt.Errorf("%s error: %s", k.Name, resp.Error) } - tickers := make(Tickers) + tickers := make(map[string]Ticker) for i := range resp.Data { tick := Ticker{} @@ -288,7 +186,7 @@ func (k *Kraken) GetTickers(pairList string) (Tickers, error) { tick.Bid, _ = strconv.ParseFloat(resp.Data[i].Bid[0], 64) tick.Last, _ = strconv.ParseFloat(resp.Data[i].Last[0], 64) tick.Volume, _ = strconv.ParseFloat(resp.Data[i].Volume[1], 64) - tick.VWAP, _ = strconv.ParseFloat(resp.Data[i].VWAP[1], 64) + tick.VolumeWeightedAveragePrice, _ = strconv.ParseFloat(resp.Data[i].VolumeWeightedAveragePrice[1], 64) tick.Trades = resp.Data[i].Trades[1] tick.Low, _ = strconv.ParseFloat(resp.Data[i].Low[1], 64) tick.High, _ = strconv.ParseFloat(resp.Data[i].High[1], 64) @@ -311,7 +209,7 @@ func (k *Kraken) GetOHLC(symbol string) ([]OpenHighLowClose, error) { var OHLC []OpenHighLowClose var result Response - path := fmt.Sprintf("%s/%s/public/%s?%s", k.APIUrl, krakenAPIVersion, krakenOHLC, values.Encode()) + path := fmt.Sprintf("%s/%s/public/%s?%s", k.API.Endpoints.URL, krakenAPIVersion, krakenOHLC, values.Encode()) err := k.SendHTTPRequest(path, &result) if err != nil { @@ -337,7 +235,7 @@ func (k *Kraken) GetOHLC(symbol string) ([]OpenHighLowClose, error) { case 4: o.Close, _ = strconv.ParseFloat(x.(string), 64) case 5: - o.Vwap, _ = strconv.ParseFloat(x.(string), 64) + o.VolumeWeightedAveragePrice, _ = strconv.ParseFloat(x.(string), 64) case 6: o.Volume, _ = strconv.ParseFloat(x.(string), 64) case 7: @@ -357,14 +255,21 @@ func (k *Kraken) GetDepth(symbol string) (Orderbook, error) { var result interface{} var orderBook Orderbook - path := fmt.Sprintf("%s/%s/public/%s?%s", k.APIUrl, krakenAPIVersion, krakenDepth, values.Encode()) + path := fmt.Sprintf("%s/%s/public/%s?%s", k.API.Endpoints.URL, krakenAPIVersion, krakenDepth, values.Encode()) err := k.SendHTTPRequest(path, &result) if err != nil { return orderBook, err } + if result == nil { + return orderBook, fmt.Errorf("%s GetDepth result is nil", k.Name) + } + data := result.(map[string]interface{}) + if data["result"] == nil { + return orderBook, fmt.Errorf("%s GetDepth data[result] is nil", k.Name) + } orderbookData := data["result"].(map[string]interface{}) var bidsData []interface{} @@ -412,7 +317,7 @@ func (k *Kraken) GetTrades(symbol string) ([]RecentTrades, error) { var recentTrades []RecentTrades var result interface{} - path := fmt.Sprintf("%s/%s/public/%s?%s", k.APIUrl, krakenAPIVersion, krakenTrades, values.Encode()) + path := fmt.Sprintf("%s/%s/public/%s?%s", k.API.Endpoints.URL, krakenAPIVersion, krakenTrades, values.Encode()) err := k.SendHTTPRequest(path, &result) if err != nil { @@ -453,7 +358,7 @@ func (k *Kraken) GetSpread(symbol string) ([]Spread, error) { var peanutButter []Spread var response interface{} - path := fmt.Sprintf("%s/%s/public/%s?%s", k.APIUrl, krakenAPIVersion, krakenSpread, values.Encode()) + path := fmt.Sprintf("%s/%s/public/%s?%s", k.API.Endpoints.URL, krakenAPIVersion, krakenSpread, values.Encode()) err := k.SendHTTPRequest(path, &response) if err != nil { @@ -855,12 +760,12 @@ func (k *Kraken) GetTradeVolume(feeinfo bool, symbol ...string) (TradeVolumeResp func (k *Kraken) AddOrder(symbol, side, orderType string, volume, price, price2, leverage float64, args *AddOrderOptions) (AddOrderResponse, error) { params := url.Values{ "pair": {symbol}, - "type": {common.StringToLower(side)}, - "ordertype": {common.StringToLower(orderType)}, + "type": {strings.ToLower(side)}, + "ordertype": {strings.ToLower(orderType)}, "volume": {strconv.FormatFloat(volume, 'f', -1, 64)}, } - if orderType == "limit" || price > 0 { + if orderType == order.Limit.Lower() || price > 0 { params.Set("price", strconv.FormatFloat(price, 'f', -1, 64)) } @@ -872,15 +777,15 @@ func (k *Kraken) AddOrder(symbol, side, orderType string, volume, price, price2, params.Set("leverage", strconv.FormatFloat(leverage, 'f', -1, 64)) } - if args.Oflags == "" { - params.Set("oflags", args.Oflags) + if args.OrderFlags != "" { + params.Set("oflags", args.OrderFlags) } - if args.StartTm == "" { + if args.StartTm != "" { params.Set("starttm", args.StartTm) } - if args.ExpireTm == "" { + if args.ExpireTm != "" { params.Set("expiretm", args.ExpireTm) } @@ -940,7 +845,7 @@ func GetError(apiErrors []string) error { for _, e := range apiErrors { switch e[0] { case 'W': - log.Warnf("%s API warning: %v\n", exchangeName, e[1:]) + log.Warnf(log.ExchangeSys, "%s API warning: %v\n", exchangeName, e[1:]) default: return fmt.Errorf("%s API error: %v", exchangeName, e[1:]) } @@ -965,39 +870,32 @@ func (k *Kraken) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated HTTP request func (k *Kraken) SendAuthenticatedHTTPRequest(method string, params url.Values, result interface{}) (err error) { - if !k.AuthenticatedAPISupport { + if !k.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, k.Name) } path := fmt.Sprintf("/%s/private/%s", krakenAPIVersion, method) - n := k.Requester.GetNonce(true).String() - params.Set("nonce", n) - - secret, err := common.Base64Decode(k.APISecret) - if err != nil { - return err - } - + params.Set("nonce", k.Requester.GetNonce(true).String()) encoded := params.Encode() - shasum := common.GetSHA256([]byte(params.Get("nonce") + encoded)) - signature := common.Base64Encode(common.GetHMAC(common.HashSHA512, - append([]byte(path), shasum...), secret)) + shasum := crypto.GetSHA256([]byte(params.Get("nonce") + encoded)) + signature := crypto.Base64Encode(crypto.GetHMAC(crypto.HashSHA512, + append([]byte(path), shasum...), []byte(k.API.Credentials.Secret))) if k.Verbose { - log.Debugf("Sending POST request to %s, path: %s, params: %s", - k.APIUrl, + log.Debugf(log.ExchangeSys, "Sending POST request to %s, path: %s, params: %s", + k.API.Endpoints.URL, path, encoded) } headers := make(map[string]string) - headers["API-Key"] = k.APIKey + headers["API-Key"] = k.API.Credentials.Key headers["API-Sign"] = signature return k.SendPayload(http.MethodPost, - k.APIUrl+path, + k.API.Endpoints.URL+path, headers, strings.NewReader(encoded), result, @@ -1140,3 +1038,15 @@ func (k *Kraken) WithdrawCancel(c currency.Code, refID string) (bool, error) { return response.Result, GetError(response.Error) } + +// GetWebsocketToken returns a websocket token +func (k *Kraken) GetWebsocketToken() (string, error) { + var response WsTokenResponse + if err := k.SendAuthenticatedHTTPRequest(krakenWebsocketToken, url.Values{}, &response); err != nil { + return "", err + } + if len(response.Error) > 0 { + return "", fmt.Errorf("%s - %v", k.Name, response.Error) + } + return response.Result.Token, nil +} diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index a2186444..227bde0b 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -1,13 +1,17 @@ package kraken import ( + "log" "net/http" + "os" + "strings" "testing" "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -23,26 +27,29 @@ const ( canManipulateRealOrders = false ) -// TestSetDefaults setup func -func TestSetDefaults(t *testing.T) { - k.SetDefaults() -} - // TestSetup setup func -func TestSetup(t *testing.T) { +func TestMain(m *testing.M) { + k.SetDefaults() cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Kraken load config error", err) + } krakenConfig, err := cfg.GetExchangeConfig("Kraken") if err != nil { - t.Error("Test Failed - kraken Setup() init error", err) + log.Fatal("kraken Setup() init error", err) } - krakenConfig.AuthenticatedAPISupport = true - krakenConfig.APIKey = apiKey - krakenConfig.APISecret = apiSecret - krakenConfig.ClientID = clientID - krakenConfig.WebsocketURL = k.WebsocketURL - subscribeToDefaultChannels = false - k.Setup(&krakenConfig) + krakenConfig.API.AuthenticatedSupport = true + krakenConfig.API.Credentials.Key = apiKey + krakenConfig.API.Credentials.Secret = apiSecret + krakenConfig.API.Credentials.ClientID = clientID + krakenConfig.API.Endpoints.WebsocketURL = k.API.Endpoints.WebsocketURL + err = k.Setup(krakenConfig) + if err != nil { + log.Fatal("Kraken setup error", err) + } + + os.Exit(m.Run()) } // TestGetServerTime API endpoint test @@ -50,7 +57,7 @@ func TestGetServerTime(t *testing.T) { t.Parallel() _, err := k.GetServerTime() if err != nil { - t.Error("Test Failed - GetServerTime() error", err) + t.Error("GetServerTime() error", err) } } @@ -59,7 +66,7 @@ func TestGetAssets(t *testing.T) { t.Parallel() _, err := k.GetAssets() if err != nil { - t.Error("Test Failed - GetAssets() error", err) + t.Error("GetAssets() error", err) } } @@ -68,7 +75,7 @@ func TestGetAssetPairs(t *testing.T) { t.Parallel() _, err := k.GetAssetPairs() if err != nil { - t.Error("Test Failed - GetAssetPairs() error", err) + t.Error("GetAssetPairs() error", err) } } @@ -77,7 +84,7 @@ func TestGetTicker(t *testing.T) { t.Parallel() _, err := k.GetTicker("BCHEUR") if err != nil { - t.Error("Test Failed - GetTicker() error", err) + t.Error("GetTicker() error", err) } } @@ -86,7 +93,7 @@ func TestGetTickers(t *testing.T) { t.Parallel() _, err := k.GetTickers("LTCUSD,ETCUSD") if err != nil { - t.Error("Test failed - GetTickers() error", err) + t.Error("GetTickers() error", err) } } @@ -95,7 +102,7 @@ func TestGetOHLC(t *testing.T) { t.Parallel() _, err := k.GetOHLC("BCHEUR") if err != nil { - t.Error("Test Failed - GetOHLC() error", err) + t.Error("GetOHLC() error", err) } } @@ -104,7 +111,7 @@ func TestGetDepth(t *testing.T) { t.Parallel() _, err := k.GetDepth("BCHEUR") if err != nil { - t.Error("Test Failed - GetDepth() error", err) + t.Error("GetDepth() error", err) } } @@ -113,7 +120,7 @@ func TestGetTrades(t *testing.T) { t.Parallel() _, err := k.GetTrades("BCHEUR") if err != nil { - t.Error("Test Failed - GetTrades() error", err) + t.Error("GetTrades() error", err) } } @@ -122,7 +129,7 @@ func TestGetSpread(t *testing.T) { t.Parallel() _, err := k.GetSpread("BCHEUR") if err != nil { - t.Error("Test Failed - GetSpread() error", err) + t.Error("GetSpread() error", err) } } @@ -131,7 +138,7 @@ func TestGetBalance(t *testing.T) { t.Parallel() _, err := k.GetBalance() if err == nil { - t.Error("Test Failed - GetBalance() error", err) + t.Error("GetBalance() Expected error") } } @@ -141,7 +148,7 @@ func TestGetTradeBalance(t *testing.T) { args := TradeBalanceOptions{Asset: "ZEUR"} _, err := k.GetTradeBalance(args) if err == nil { - t.Error("Test Failed - GetTradeBalance() error", err) + t.Error("GetTradeBalance() Expected error") } } @@ -151,7 +158,7 @@ func TestGetOpenOrders(t *testing.T) { args := OrderInfoOptions{Trades: true} _, err := k.GetOpenOrders(args) if err == nil { - t.Error("Test Failed - GetOpenOrders() error", err) + t.Error("GetOpenOrders() Expected error") } } @@ -161,7 +168,7 @@ func TestGetClosedOrders(t *testing.T) { args := GetClosedOrdersOptions{Trades: true, Start: "OE4KV4-4FVQ5-V7XGPU"} _, err := k.GetClosedOrders(args) if err == nil { - t.Error("Test Failed - GetClosedOrders() error", err) + t.Error("GetClosedOrders() Expected error") } } @@ -171,7 +178,7 @@ func TestQueryOrdersInfo(t *testing.T) { args := OrderInfoOptions{Trades: true} _, err := k.QueryOrdersInfo(args, "OR6ZFV-AA6TT-CKFFIW", "OAMUAJ-HLVKG-D3QJ5F") if err == nil { - t.Error("Test Failed - QueryOrdersInfo() error", err) + t.Error("QueryOrdersInfo() Expected error") } } @@ -181,7 +188,7 @@ func TestGetTradesHistory(t *testing.T) { args := GetTradesHistoryOptions{Trades: true, Start: "TMZEDR-VBJN2-NGY6DX", End: "TVRXG2-R62VE-RWP3UW"} _, err := k.GetTradesHistory(args) if err == nil { - t.Error("Test Failed - GetTradesHistory() error", err) + t.Error("GetTradesHistory() Expected error") } } @@ -190,7 +197,7 @@ func TestQueryTrades(t *testing.T) { t.Parallel() _, err := k.QueryTrades(true, "TMZEDR-VBJN2-NGY6DX", "TFLWIB-KTT7L-4TWR3L", "TDVRAH-2H6OS-SLSXRX") if err == nil { - t.Error("Test Failed - QueryTrades() error", err) + t.Error("QueryTrades() Expected error") } } @@ -199,7 +206,7 @@ func TestOpenPositions(t *testing.T) { t.Parallel() _, err := k.OpenPositions(false) if err == nil { - t.Error("Test Failed - OpenPositions() error", err) + t.Error("OpenPositions() Expected error") } } @@ -209,7 +216,7 @@ func TestGetLedgers(t *testing.T) { args := GetLedgersOptions{Start: "LRUHXI-IWECY-K4JYGO", End: "L5NIY7-JZQJD-3J4M2V", Ofs: 15} _, err := k.GetLedgers(args) if err == nil { - t.Error("Test Failed - GetLedgers() error", err) + t.Error("GetLedgers() Expected error") } } @@ -218,7 +225,7 @@ func TestQueryLedgers(t *testing.T) { t.Parallel() _, err := k.QueryLedgers("LVTSFS-NHZVM-EXNZ5M") if err == nil { - t.Error("Test Failed - QueryLedgers() error", err) + t.Error("QueryLedgers() Expected error") } } @@ -227,17 +234,19 @@ func TestGetTradeVolume(t *testing.T) { t.Parallel() _, err := k.GetTradeVolume(true, "OAVY7T-MV5VK-KHDF5X") if err == nil { - t.Error("Test Failed - GetTradeVolume() error", err) + t.Error("GetTradeVolume() Expected error") } } // TestAddOrder API endpoint test func TestAddOrder(t *testing.T) { t.Parallel() - args := AddOrderOptions{Oflags: "fcib"} - _, err := k.AddOrder("XXBTZUSD", "sell", "market", 0.00000001, 0, 0, 0, &args) + args := AddOrderOptions{OrderFlags: "fcib"} + _, err := k.AddOrder("XXBTZUSD", + order.Sell.Lower(), order.Limit.Lower(), + 0.00000001, 0, 0, 0, &args) if err == nil { - t.Error("Test Failed - AddOrder() error", err) + t.Error("AddOrder() Expected error") } } @@ -246,7 +255,7 @@ func TestCancelExistingOrder(t *testing.T) { t.Parallel() _, err := k.CancelExistingOrder("OAVY7T-MV5VK-KHDF5X") if err == nil { - t.Error("Test Failed - CancelExistingOrder() error", err) + t.Error("CancelExistingOrder() Expected error") } } @@ -267,7 +276,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() k.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -279,15 +288,13 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - k.SetDefaults() - TestSetup(t) var feeBuilder = setFeeBuilder() if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic if resp, err := k.GetFee(feeBuilder); resp != float64(0.0026) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0026), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0026), resp) } // CryptocurrencyTradeFee High quantity @@ -295,7 +302,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := k.GetFee(feeBuilder); resp != float64(2600) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2600), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2600), resp) t.Error(err) } @@ -303,7 +310,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := k.GetFee(feeBuilder); resp != float64(0.0016) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0016), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0016), resp) t.Error(err) } @@ -311,7 +318,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := k.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -319,7 +326,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := k.GetFee(feeBuilder); resp != float64(5) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(5), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(5), resp) t.Error(err) } } @@ -329,7 +336,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.CyptocurrencyDepositFee feeBuilder.Pair.Base = currency.XXBT if resp, err := k.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(5), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(5), resp) t.Error(err) } @@ -337,7 +344,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := k.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) t.Error(err) } @@ -346,7 +353,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := k.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -355,18 +362,15 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := k.GetFee(feeBuilder); resp != float64(5) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(5), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(5), resp) t.Error(err) } } // TestFormatWithdrawPermissions logic test func TestFormatWithdrawPermissions(t *testing.T) { - k.SetDefaults() expectedResult := exchange.AutoWithdrawCryptoWithSetupText + " & " + exchange.WithdrawCryptoWith2FAText + " & " + exchange.AutoWithdrawFiatWithSetupText + " & " + exchange.WithdrawFiatWith2FAText - withdrawPermissions := k.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } @@ -374,11 +378,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { // TestGetActiveOrders wrapper test func TestGetActiveOrders(t *testing.T) { - k.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := k.GetActiveOrders(&getOrdersRequest) @@ -391,11 +392,8 @@ func TestGetActiveOrders(t *testing.T) { // TestGetOrderHistory wrapper test func TestGetOrderHistory(t *testing.T) { - k.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := k.GetOrderHistory(&getOrdersRequest) @@ -406,31 +404,45 @@ func TestGetOrderHistory(t *testing.T) { } } -// Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them -// ---------------------------------------------------------------------------------------------------------------------------- -func areTestAPIKeysSet() bool { - if k.APIKey != "" && k.APIKey != "Key" && - k.APISecret != "" && k.APISecret != "Secret" { - return true - } - return false -} - -// TestSubmitOrder wrapper test -func TestSubmitOrder(t *testing.T) { - k.SetDefaults() - TestSetup(t) - +// TestGetOrderHistory wrapper test +func TestGetOrderInfo(t *testing.T) { if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.XBT, - Quote: currency.CAD, + _, err := k.GetOrderInfo("ImACoolOrderID") + if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting error") } - response, err := k.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 10, "hi") + if areTestAPIKeysSet() && !strings.Contains(err.Error(), "- Order ID not found:") { + t.Error("Expected Order ID not found error") + } +} + +// Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them +// ---------------------------------------------------------------------------------------------------------------------------- +func areTestAPIKeysSet() bool { + return k.ValidateAPICredentials() +} + +// TestSubmitOrder wrapper test +func TestSubmitOrder(t *testing.T) { + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.XBT, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", + } + response, err := k.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -440,16 +452,12 @@ func TestSubmitOrder(t *testing.T) { // TestCancelExchangeOrder wrapper test func TestCancelExchangeOrder(t *testing.T) { - k.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -467,16 +475,12 @@ func TestCancelExchangeOrder(t *testing.T) { // TestCancelAllExchangeOrders wrapper test func TestCancelAllExchangeOrders(t *testing.T) { - k.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -492,44 +496,47 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } // TestGetAccountInfo wrapper test func TestGetAccountInfo(t *testing.T) { - if apiKey != "" || apiSecret != "" || clientID != "" { + if areTestAPIKeysSet() || clientID != "" { _, err := k.GetAccountInfo() if err != nil { - t.Error("Test Failed - GetAccountInfo() error", err) + t.Error("GetAccountInfo() error", err) } } else { _, err := k.GetAccountInfo() if err == nil { - t.Error("Test Failed - GetAccountInfo() error") + t.Error("GetAccountInfo() Expected error") } } } // TestModifyOrder wrapper test func TestModifyOrder(t *testing.T) { - _, err := k.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := k.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } // TestWithdraw wrapper test func TestWithdraw(t *testing.T) { - k.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.XXBT, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "donation", - TradePassword: "Key", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.XXBT, + Description: "WITHDRAW IT ALL", + TradePassword: "Key", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -547,19 +554,17 @@ func TestWithdraw(t *testing.T) { // TestWithdrawFiat wrapper test func TestWithdrawFiat(t *testing.T) { - k.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.EUR, - Address: "", - Description: "donation", - TradePassword: "someBank", + var withdrawFiatRequest = exchange.FiatWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.EUR, + Description: "WITHDRAW IT ALL", + TradePassword: "someBank", + }, } _, err := k.WithdrawFiatFunds(&withdrawFiatRequest) @@ -573,19 +578,17 @@ func TestWithdrawFiat(t *testing.T) { // TestWithdrawInternationalBank wrapper test func TestWithdrawInternationalBank(t *testing.T) { - k.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.EUR, - Address: "", - Description: "donation", - TradePassword: "someBank", + var withdrawFiatRequest = exchange.FiatWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.EUR, + Description: "WITHDRAW IT ALL", + TradePassword: "someBank", + }, } _, err := k.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) @@ -602,43 +605,38 @@ func TestGetDepositAddress(t *testing.T) { if areTestAPIKeysSet() { _, err := k.GetDepositAddress(currency.BTC, "") if err != nil { - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) } } else { _, err := k.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error can not be nil") + t.Error("GetDepositAddress() error can not be nil") } } } // TestWithdrawStatus wrapper test func TestWithdrawStatus(t *testing.T) { - k.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() { _, err := k.WithdrawStatus(currency.BTC, "") if err != nil { - t.Error("Test Failed - WithdrawStatus() error", err) + t.Error("WithdrawStatus() error", err) } } else { _, err := k.WithdrawStatus(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error can not be nil", err) + t.Error("GetDepositAddress() error can not be nil") } } } // TestWithdrawCancel wrapper test func TestWithdrawCancel(t *testing.T) { - k.SetDefaults() - TestSetup(t) _, err := k.WithdrawCancel(currency.BTC, "") if areTestAPIKeysSet() && err == nil { - t.Error("Test Failed - WithdrawCancel() error cannot be nil") + t.Error("WithdrawCancel() error cannot be nil") } else if !areTestAPIKeysSet() && err == nil { - t.Errorf("Test Failed - WithdrawCancel() error - expecting an error when no keys are set but received nil") + t.Errorf("WithdrawCancel() error - expecting an error when no keys are set but received nil") } } @@ -648,12 +646,11 @@ func setupWsTests(t *testing.T) { if wsSetupRan { return } - TestSetDefaults(t) - TestSetup(t) - if !k.Websocket.IsEnabled() && !k.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + if !k.Websocket.IsEnabled() && !k.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } k.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + comms = make(chan wshandler.WebsocketResponse, sharedtestvalues.WebsocketChannelOverrideCapacity) k.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() k.WebsocketConn = &wshandler.WebsocketConnection{ ExchangeName: k.Name, @@ -662,12 +659,33 @@ func setupWsTests(t *testing.T) { ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } + k.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: k.Name, + URL: krakenAuthWSURL, + Verbose: k.Verbose, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, + } var dialer websocket.Dialer err := k.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { t.Fatal(err) } + err = k.AuthenticatedWebsocketConn.Dial(&dialer, http.Header{}) + if err != nil { + t.Fatal(err) + } + + token, err := k.GetWebsocketToken() + if err != nil { + t.Error(err) + } + authToken = token + + go k.WsReadData(k.WebsocketConn) + go k.WsReadData(k.AuthenticatedWebsocketConn) go k.WsHandleData() + go k.wsPingHandler() wsSetupRan = true } @@ -682,3 +700,38 @@ func TestWebsocketSubscribe(t *testing.T) { t.Error(err) } } + +func TestGetWSToken(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required, skipping") + } + resp, err := k.GetWebsocketToken() + if err != nil { + t.Error(err) + } + if resp == "" { + t.Error("Token not returned") + } +} + +func TestWsAddOrder(t *testing.T) { + setupWsTests(t) + _, err := k.wsAddOrder(&WsAddOrderRequest{ + OrderType: order.Limit.Lower(), + OrderSide: order.Buy.Lower(), + Pair: "XBT/USD", + Price: -100, + }) + if err != nil { + t.Error(err) + } +} + +func TestWsCancelOrder(t *testing.T) { + setupWsTests(t) + err := k.wsCancelOrders([]string{"1337"}) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/kraken/kraken_types.go b/exchanges/kraken/kraken_types.go index e815f651..d950a6a9 100644 --- a/exchanges/kraken/kraken_types.go +++ b/exchanges/kraken/kraken_types.go @@ -1,6 +1,10 @@ package kraken -import "github.com/thrasher-corp/gocryptotrader/currency" +import ( + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" +) // TimeResponse type type TimeResponse struct { @@ -38,15 +42,15 @@ type AssetPairs struct { // Ticker is a standard ticker type type Ticker struct { - Ask float64 - Bid float64 - Last float64 - Volume float64 - VWAP float64 - Trades int64 - Low float64 - High float64 - Open float64 + Ask float64 + Bid float64 + Last float64 + Volume float64 + VolumeWeightedAveragePrice float64 + Trades int64 + Low float64 + High float64 + Open float64 } // Tickers stores a map of tickers @@ -54,27 +58,27 @@ type Tickers map[string]Ticker // TickerResponse holds ticker information before its put into the Ticker struct type TickerResponse struct { - Ask []string `json:"a"` - Bid []string `json:"b"` - Last []string `json:"c"` - Volume []string `json:"v"` - VWAP []string `json:"p"` - Trades []int64 `json:"t"` - Low []string `json:"l"` - High []string `json:"h"` - Open string `json:"o"` + Ask []string `json:"a"` + Bid []string `json:"b"` + Last []string `json:"c"` + Volume []string `json:"v"` + VolumeWeightedAveragePrice []string `json:"p"` + Trades []int64 `json:"t"` + Low []string `json:"l"` + High []string `json:"h"` + Open string `json:"o"` } // OpenHighLowClose contains ticker event information type OpenHighLowClose struct { - Time float64 - Open float64 - High float64 - Low float64 - Close float64 - Vwap float64 - Volume float64 - Count float64 + Time float64 + Open float64 + High float64 + Low float64 + Close float64 + VolumeWeightedAveragePrice float64 + Volume float64 + Count float64 } // RecentTrades holds recent trade data @@ -125,13 +129,13 @@ type TradeBalanceInfo struct { // OrderInfo type type OrderInfo struct { - RefID string `json:"refid"` - UserRef int32 `json:"userref"` - Status string `json:"status"` - OpenTm float64 `json:"opentm"` - StartTm float64 `json:"starttm"` - ExpireTm float64 `json:"expiretm"` - Descr struct { + RefID string `json:"refid"` + UserRef int32 `json:"userref"` + Status string `json:"status"` + OpenTime float64 `json:"opentm"` + StartTime float64 `json:"starttm"` + ExpireTime float64 `json:"expiretm"` + Description struct { Pair string `json:"pair"` Type string `json:"type"` OrderType string `json:"ordertype"` @@ -141,16 +145,16 @@ type OrderInfo struct { Order string `json:"order"` Close string `json:"close"` } `json:"descr"` - Vol float64 `json:"vol,string"` - VolExec float64 `json:"vol_exec,string"` - Cost float64 `json:"cost,string"` - Fee float64 `json:"fee,string"` - Price float64 `json:"price,string"` - StopPrice float64 `json:"stopprice,string"` - LimitPrice float64 `json:"limitprice,string"` - Misc string `json:"misc"` - Oflags string `json:"oflags"` - Trades []string `json:"trades"` + Volume float64 `json:"vol,string"` + VolumeExecuted float64 `json:"vol_exec,string"` + Cost float64 `json:"cost,string"` + Fee float64 `json:"fee,string"` + Price float64 `json:"price,string"` + StopPrice float64 `json:"stopprice,string"` + LimitPrice float64 `json:"limitprice,string"` + Misc string `json:"misc"` + OrderFlags string `json:"oflags"` + Trades []string `json:"trades"` } // OpenOrders type @@ -198,44 +202,44 @@ type TradesHistory struct { // TradeInfo type type TradeInfo struct { - OrderTxID string `json:"ordertxid"` - Pair string `json:"pair"` - Time float64 `json:"time"` - Type string `json:"type"` - OrderType string `json:"ordertype"` - Price float64 `json:"price,string"` - Cost float64 `json:"cost,string"` - Fee float64 `json:"fee,string"` - Vol float64 `json:"vol,string"` - Margin float64 `json:"margin,string"` - Misc string `json:"misc"` - PosTxID string `json:"postxid"` - Cprice float64 `json:"cprice,string"` - Cfee float64 `json:"cfee,string"` - Cvol float64 `json:"cvol,string"` - Cmargin float64 `json:"cmargin,string"` - Trades []string `json:"trades"` - PosStatus string `json:"posstatus"` + OrderTxID string `json:"ordertxid"` + Pair string `json:"pair"` + Time float64 `json:"time"` + Type string `json:"type"` + OrderType string `json:"ordertype"` + Price float64 `json:"price,string"` + Cost float64 `json:"cost,string"` + Fee float64 `json:"fee,string"` + Volume float64 `json:"vol,string"` + Margin float64 `json:"margin,string"` + Misc string `json:"misc"` + PosTxID string `json:"postxid"` + ClosedPositionAveragePrice float64 `json:"cprice,string"` + ClosedPositionFee float64 `json:"cfee,string"` + ClosedPositionVolume float64 `json:"cvol,string"` + ClosedPositionMargin float64 `json:"cmargin,string"` + Trades []string `json:"trades"` + PosStatus string `json:"posstatus"` } // Position holds the opened position type Position struct { - Ordertxid string `json:"ordertxid"` - Pair string `json:"pair"` - Time float64 `json:"time"` - Type string `json:"type"` - OrderType string `json:"ordertype"` - Cost float64 `json:"cost,string"` - Fee float64 `json:"fee,string"` - Vol float64 `json:"vol,string"` - VolClosed float64 `json:"vol_closed,string"` - Margin float64 `json:"margin,string"` - Rollovertm int64 `json:"rollovertm,string"` - Misc string `json:"misc"` - Oflags string `json:"oflags"` - PosStatus string `json:"posstatus"` - Net string `json:"net"` - Terms string `json:"terms"` + Ordertxid string `json:"ordertxid"` + Pair string `json:"pair"` + Time float64 `json:"time"` + Type string `json:"type"` + OrderType string `json:"ordertype"` + Cost float64 `json:"cost,string"` + Fee float64 `json:"fee,string"` + Volume float64 `json:"vol,string"` + VolumeClosed float64 `json:"vol_closed,string"` + Margin float64 `json:"margin,string"` + RolloverTime int64 `json:"rollovertm,string"` + Misc string `json:"misc"` + OrderFlags string `json:"oflags"` + PositionStatus string `json:"posstatus"` + Net string `json:"net"` + Terms string `json:"terms"` } // GetLedgersOptions type @@ -314,7 +318,7 @@ type OrderDescription struct { // AddOrderOptions represents the AddOrder options type AddOrderOptions struct { UserRef int32 - Oflags string + OrderFlags string StartTm string ExpireTm string CloseOrderType string @@ -391,7 +395,7 @@ type WithdrawStatusResponse struct { type WebsocketSubscriptionEventRequest struct { Event string `json:"event"` // subscribe RequestID int64 `json:"reqid,omitempty"` // Optional, client originated ID reflected in response message. - Pairs []string `json:"pair"` // Array of currency pairs (pair1,pair2,pair3). + Pairs []string `json:"pair,omitempty"` // Array of currency pairs (pair1,pair2,pair3). Subscription WebsocketSubscriptionData `json:"subscription,omitempty"` } @@ -413,6 +417,8 @@ type WebsocketSubscriptionData struct { Name string `json:"name,omitempty"` // ticker|ohlc|trade|book|spread|*, * for all (ohlc interval value is 1 if all channels subscribed) Interval int64 `json:"interval,omitempty"` // Optional - Time interval associated with ohlc subscription in minutes. Default 1. Valid Interval values: 1|5|15|30|60|240|1440|10080|21600 Depth int64 `json:"depth,omitempty"` // Optional - depth associated with book subscription in number of levels each side, default 10. Valid Options are: 10, 25, 100, 500, 1000 + Token string `json:"token,omitempty"` // Optional used for authenticated requests + } // WebsocketEventResponse holds all data response types @@ -459,3 +465,103 @@ type WebsocketChannelData struct { Pair currency.Pair ChannelID int64 } + +// WsTokenResponse holds the WS auth token +type WsTokenResponse struct { + Error []string `json:"error"` + Result struct { + Expires int64 `json:"expires"` + Token string `json:"token"` + } `json:"result"` +} + +// WsOwnTrade ws auth owntrade data +type WsOwnTrade struct { + Cost float64 `json:"cost,string"` + Fee float64 `json:"fee,string"` + Margin float64 `json:"margin,string"` + OrderTransactionID string `json:"ordertxid"` + OrderType string `json:"ordertype"` + Pair string `json:"pair"` + PostTransactionID string `json:"postxid"` + Price float64 `json:"price,string"` + Time time.Time `json:"time"` + Type string `json:"type"` + Vol float64 `json:"vol,string"` +} + +// WsOpenOrders ws auth open order data +type WsOpenOrders struct { + Cost float64 `json:"cost,string"` + Description WsOpenOrderDescription `json:"descr"` + ExpireTime time.Time `json:"expiretm"` + Fee float64 `json:"fee,string"` + LimitPrice float64 `json:"limitprice,string"` + Misc string `json:"misc"` + OFlags string `json:"oflags"` + OpenTime time.Time `json:"opentm"` + Price float64 `json:"price,string"` + RefID string `json:"refid"` + StartTime time.Time `json:"starttm"` + Status string `json:"status"` + StopPrice float64 `json:"stopprice,string"` + UserReference float64 `json:"userref"` + Volume float64 `json:"vol,string"` + ExecutedVolume float64 `json:"vol_exec,string"` +} + +// WsOpenOrderDescription additional data for WsOpenOrders +type WsOpenOrderDescription struct { + Close string `json:"close"` + Leverage string `json:"leverage"` + Order string `json:"order"` + OrderType string `json:"ordertype"` + Pair string `json:"pair"` + Price float64 `json:"price,string"` + Price2 float64 `json:"price2,string"` + Type string `json:"type"` +} + +// WsAddOrderRequest request type for ws adding order +type WsAddOrderRequest struct { + Event string `json:"event"` + Token string `json:"token"` + OrderType string `json:"ordertype"` + OrderSide string `json:"type"` + Pair string `json:"pair"` + Price float64 `json:"price,omitempty"` // optional + Price2 float64 `json:"price2,omitempty"` // optional + Volume float64 `json:"volume,omitempty"` + Leverage float64 `json:"leverage,omitempty"` // optional + OFlags string `json:"oflags,omitempty"` // optional + StartTime string `json:"starttm,omitempty"` // optional + ExpireTime string `json:"expiretm,omitempty"` // optional + UserReferenceID string `json:"userref,omitempty"` // optional + Validate string `json:"validate,omitempty"` // optional + CloseOrderType string `json:"close[ordertype],omitempty"` // optional + ClosePrice float64 `json:"close[price],omitempty"` // optional + ClosePrice2 float64 `json:"close[price2],omitempty"` // optional +} + +// WsAddOrderResponse response data for ws order +type WsAddOrderResponse struct { + Description string `json:"descr"` + Event string `json:"event"` + Status string `json:"status"` + TransactionID string `json:"txid"` + ErrorMessage string `json:"errorMessage"` +} + +// WsCancelOrderRequest request for ws cancel order +type WsCancelOrderRequest struct { + Event string `json:"event"` + Token string `json:"token"` + TransactionIDs []string `json:"txid"` +} + +// WsCancelOrderResponse response data for ws cancel order +type WsCancelOrderResponse struct { + Event string `json:"event"` + Status string `json:"status"` + ErrorMessage string `json:"errorMessage"` +} diff --git a/exchanges/kraken/kraken_websocket.go b/exchanges/kraken/kraken_websocket.go index dc53c3a0..e6659a99 100644 --- a/exchanges/kraken/kraken_websocket.go +++ b/exchanges/kraken/kraken_websocket.go @@ -1,6 +1,7 @@ package kraken import ( + "encoding/json" "errors" "fmt" "math" @@ -9,8 +10,10 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" @@ -20,11 +23,9 @@ import ( // List of all websocket channels to subscribe to const ( krakenWSURL = "wss://ws.kraken.com" + krakenAuthWSURL = "wss://ws-auth.kraken.com" krakenWSSandboxURL = "wss://sandbox.kraken.com" - krakenWSSupportedVersion = "0.2.0" - // If a checksum fails, then resubscribing to the channel fails, fatal after these attempts - krakenWsResubscribeFailureLimit = 3 - krakenWsResubscribeDelayInSeconds = 3 + krakenWSSupportedVersion = "0.3.0" // WS endpoints krakenWsHeartbeat = "heartbeat" krakenWsPing = "ping" @@ -38,19 +39,22 @@ const ( krakenWsTrade = "trade" krakenWsSpread = "spread" krakenWsOrderbook = "book" - // Only supported asset type - krakenWsAssetType = orderbook.Spot - orderbookBufferLimit = 3 - krakenWsRateLimit = 50 + krakenWsOwnTrades = "ownTrades" + krakenWsOpenOrders = "openOrders" + krakenWsAddOrder = "addOrder" + krakenWsCancelOrder = "cancelOrder" + krakenWsRateLimit = 50 ) // orderbookMutex Ensures if two entries arrive at once, only one can be processed at a time var subscriptionChannelPair []WebsocketChannelData -var subscribeToDefaultChannels = true +var comms = make(chan wshandler.WebsocketResponse) +var authToken string // Channels require a topic and a currency // Format [[ticker,but-t4u],[orderbook,nce-btt]] var defaultSubscribedChannels = []string{krakenWsTicker, krakenWsTrade, krakenWsOrderbook, krakenWsOHLC, krakenWsSpread} +var authenticatedChannels = []string{krakenWsOwnTrades, krakenWsOpenOrders} // WsConnect initiates a websocket connection func (k *Kraken) WsConnect() error { @@ -62,36 +66,45 @@ func (k *Kraken) WsConnect() error { if err != nil { return err } + if k.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + authToken, err = k.GetWebsocketToken() + if err != nil { + k.Websocket.SetCanUseAuthenticatedEndpoints(false) + log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", k.Name, err) + } + err = k.AuthenticatedWebsocketConn.Dial(&dialer, http.Header{}) + if err != nil { + k.Websocket.SetCanUseAuthenticatedEndpoints(false) + log.Errorf(log.ExchangeSys, "%v - failed to connect to authenticated endpoint: %v\n", k.Name, err) + } + go k.WsReadData(k.AuthenticatedWebsocketConn) + k.GenerateAuthenticatedSubscriptions() + } + + go k.WsReadData(k.WebsocketConn) go k.WsHandleData() go k.wsPingHandler() - if subscribeToDefaultChannels { - k.GenerateDefaultSubscriptions() - } + k.GenerateDefaultSubscriptions() return nil } -// wsPingHandler sends a message "ping" every 27 to maintain the connection to the websocket -func (k *Kraken) wsPingHandler() { +// WsReadData funnels both auth and public ws data into one manageable place +func (k *Kraken) WsReadData(ws *wshandler.WebsocketConnection) { k.Websocket.Wg.Add(1) defer k.Websocket.Wg.Done() - ticker := time.NewTicker(time.Second * 27) - defer ticker.Stop() - for { select { case <-k.Websocket.ShutdownC: return - case <-ticker.C: - pingEvent := WebsocketBaseEventRequest{Event: krakenWsPing} - if k.Verbose { - log.Debugf("%v sending ping", - k.Name) - } - err := k.WebsocketConn.SendMessage(pingEvent) + default: + resp, err := ws.ReadMessage() if err != nil { k.Websocket.DataHandler <- err + return } + k.Websocket.TrafficAlert <- struct{}{} + comms <- resp } } } @@ -108,72 +121,97 @@ func (k *Kraken) WsHandleData() { case <-k.Websocket.ShutdownC: return default: - resp, err := k.WebsocketConn.ReadMessage() - if err != nil { - k.Websocket.DataHandler <- fmt.Errorf("%v WsHandleData: %v", - k.Name, - err) - return - } - k.Websocket.TrafficAlert <- struct{}{} + resp := <-comms // event response handling var eventResponse WebsocketEventResponse - err = common.JSONDecode(resp.Raw, &eventResponse) + err := json.Unmarshal(resp.Raw, &eventResponse) if err == nil && eventResponse.Event != "" { k.WsHandleEventResponse(&eventResponse, resp.Raw) continue } // Data response handling var dataResponse WebsocketDataResponse - err = common.JSONDecode(resp.Raw, &dataResponse) - if err == nil && dataResponse[0].(float64) >= 0 { - k.WsHandleDataResponse(dataResponse) + err = json.Unmarshal(resp.Raw, &dataResponse) + if err != nil { + log.Error(log.WebsocketMgr, fmt.Errorf("%s - unhandled websocket data: %v", k.Name, err)) continue } - continue + if _, ok := dataResponse[0].(float64); ok { + k.WsHandleDataResponse(dataResponse) + } + if _, ok := dataResponse[1].(string); ok { + k.wsHandleAuthDataResponse(dataResponse) + } + } + } +} + +// wsPingHandler sends a message "ping" every 27 to maintain the connection to the websocket +func (k *Kraken) wsPingHandler() { + k.Websocket.Wg.Add(1) + defer k.Websocket.Wg.Done() + ticker := time.NewTicker(time.Second * 27) + defer ticker.Stop() + + for { + select { + case <-k.Websocket.ShutdownC: + return + case <-ticker.C: + pingEvent := WebsocketBaseEventRequest{Event: krakenWsPing} + if k.Verbose { + log.Debugf(log.ExchangeSys, "%v sending ping", + k.Name) + } + err := k.WebsocketConn.SendMessage(pingEvent) + if err != nil { + k.Websocket.DataHandler <- err + } } } } // WsHandleDataResponse classifies the WS response and sends to appropriate handler func (k *Kraken) WsHandleDataResponse(response WebsocketDataResponse) { - channelID := int64(response[0].(float64)) - channelData := getSubscriptionChannelData(channelID) - switch channelData.Subscription { - case krakenWsTicker: - if k.Verbose { - log.Debugf("%v Websocket ticker data received", - k.Name) + if cID, ok := response[0].(float64); ok { + channelID := int64(cID) + channelData := getSubscriptionChannelData(channelID) + switch channelData.Subscription { + case krakenWsTicker: + if k.Verbose { + log.Debugf(log.ExchangeSys, "%v Websocket ticker data received", + k.Name) + } + k.wsProcessTickers(&channelData, response[1].(map[string]interface{})) + case krakenWsOHLC: + if k.Verbose { + log.Debugf(log.ExchangeSys, "%v Websocket OHLC data received", + k.Name) + } + k.wsProcessCandles(&channelData, response[1].([]interface{})) + case krakenWsOrderbook: + if k.Verbose { + log.Debugf(log.ExchangeSys, "%v Websocket Orderbook data received", + k.Name) + } + k.wsProcessOrderBook(&channelData, response[1].(map[string]interface{})) + case krakenWsSpread: + if k.Verbose { + log.Debugf(log.ExchangeSys, "%v Websocket Spread data received", + k.Name) + } + k.wsProcessSpread(&channelData, response[1].([]interface{})) + case krakenWsTrade: + if k.Verbose { + log.Debugf(log.ExchangeSys, "%v Websocket Trade data received", + k.Name) + } + k.wsProcessTrades(&channelData, response[1].([]interface{})) + default: + log.Errorf(log.ExchangeSys, "%v Unidentified websocket data received: %v", + k.Name, + response) } - k.wsProcessTickers(&channelData, response[1]) - case krakenWsOHLC: - if k.Verbose { - log.Debugf("%v Websocket OHLC data received", - k.Name) - } - k.wsProcessCandles(&channelData, response[1]) - case krakenWsOrderbook: - if k.Verbose { - log.Debugf("%v Websocket Orderbook data received", - k.Name) - } - k.wsProcessOrderBook(&channelData, response[1]) - case krakenWsSpread: - if k.Verbose { - log.Debugf("%v Websocket Spread data received", - k.Name) - } - k.wsProcessSpread(&channelData, response[1]) - case krakenWsTrade: - if k.Verbose { - log.Debugf("%v Websocket Trade data received", - k.Name) - } - k.wsProcessTrades(&channelData, response[1]) - default: - log.Errorf("%v Unidentified websocket data received: %v", - k.Name, - response) } } @@ -182,25 +220,25 @@ func (k *Kraken) WsHandleEventResponse(response *WebsocketEventResponse, rawResp switch response.Event { case krakenWsHeartbeat: if k.Verbose { - log.Debugf("%v Websocket heartbeat data received", + log.Debugf(log.ExchangeSys, "%v Websocket heartbeat data received", k.Name) } case krakenWsPong: if k.Verbose { - log.Debugf("%v Websocket pong data received", + log.Debugf(log.ExchangeSys, "%v Websocket pong data received", k.Name) } case krakenWsSystemStatus: if k.Verbose { - log.Debugf("%v Websocket status data received", + log.Debugf(log.ExchangeSys, "%v Websocket status data received", k.Name) } if response.Status != "online" { k.Websocket.DataHandler <- fmt.Errorf("%v Websocket status '%v'", k.Name, response.Status) } - if response.WebsocketStatusResponse.Version != krakenWSSupportedVersion { - log.Warnf("%v New version of Websocket API released. Was %v Now %v", + if response.WebsocketStatusResponse.Version > krakenWSSupportedVersion { + log.Warnf(log.ExchangeSys, "%v New version of Websocket API released. Was %v Now %v", k.Name, krakenWSSupportedVersion, response.WebsocketStatusResponse.Version) } case krakenWsSubscriptionStatus: @@ -211,7 +249,194 @@ func (k *Kraken) WsHandleEventResponse(response *WebsocketEventResponse, rawResp } addNewSubscriptionChannelData(response) default: - log.Errorf("%v Unidentified websocket data received: %v", k.Name, response) + log.Errorf(log.ExchangeSys, "%v Unidentified websocket data received: %v", + k.Name, response) + } +} + +func (k *Kraken) wsHandleAuthDataResponse(response WebsocketDataResponse) { + if chName, ok := response[1].(string); ok { + switch chName { + case krakenWsOwnTrades: + if k.Verbose { + log.Debugf(log.ExchangeSys, "%v Websocket auth own trade data received", + k.Name) + } + k.wsProcessOwnTrades(&response[0]) + case krakenWsOpenOrders: + if k.Verbose { + log.Debugf(log.ExchangeSys, "%v Websocket auth open order data received", + k.Name) + } + k.wsProcessOpenOrders(&response[0]) + } + } +} + +func (k *Kraken) wsProcessOwnTrades(ownOrders interface{}) { + if data, ok := ownOrders.([]interface{}); ok { + for i := range data { + ownTrade := data[i].(map[string]interface{}) + for _, val := range ownTrade { + tradeData := val.(map[string]interface{}) + cost, err := strconv.ParseFloat(tradeData["cost"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + fee, err := strconv.ParseFloat(tradeData["fee"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + margin, err := strconv.ParseFloat(tradeData["margin"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + vol, err := strconv.ParseFloat(tradeData["vol"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + price, err := strconv.ParseFloat(tradeData["price"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + timeTogether, err := strconv.ParseFloat(tradeData["time"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + first, second, err := convert.SplitFloatDecimals(timeTogether) + if err != nil { + k.Websocket.DataHandler <- err + } + k.Websocket.DataHandler <- WsOwnTrade{ + Cost: cost, + Fee: fee, + Margin: margin, + OrderTransactionID: tradeData["ordertxid"].(string), + OrderType: tradeData["ordertype"].(string), + Pair: tradeData["pair"].(string), + PostTransactionID: tradeData["postxid"].(string), + Price: price, + Time: time.Unix(first, second), + Type: tradeData["type"].(string), + Vol: vol, + } + } + } + } else { + k.Websocket.DataHandler <- errors.New(k.Name + " - Invalid own trades data") + } +} + +func (k *Kraken) wsProcessOpenOrders(ownOrders interface{}) { + if data, ok := ownOrders.([]interface{}); ok { + for i := range data { + ownTrade := data[i].(map[string]interface{}) + for key, val := range ownTrade { + tradeData := val.(map[string]interface{}) + if len(tradeData) == 1 { + // just a status update + if status, ok := tradeData["status"].(string); ok { + k.Websocket.DataHandler <- k.Name + " - Order " + key + " " + status + } + } + startTimeConv, err := strconv.ParseFloat(tradeData["starttm"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + startTime, startTimeNano, err := convert.SplitFloatDecimals(startTimeConv) + if err != nil { + k.Websocket.DataHandler <- err + } + openTimeConv, err := strconv.ParseFloat(tradeData["opentm"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + openTime, openTimeNano, err := convert.SplitFloatDecimals(openTimeConv) + if err != nil { + k.Websocket.DataHandler <- err + } + expireTimeConv, err := strconv.ParseFloat(tradeData["expiretm"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + expireTime, expireTimeNano, err := convert.SplitFloatDecimals(expireTimeConv) + if err != nil { + k.Websocket.DataHandler <- err + } + cost, err := strconv.ParseFloat(tradeData["cost"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + executedVolume, err := strconv.ParseFloat(tradeData["vol_exec"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + volume, err := strconv.ParseFloat(tradeData["vol"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + userReference, err := strconv.ParseFloat(tradeData["userref"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + stopPrice, err := strconv.ParseFloat(tradeData["stopprice"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + price, err := strconv.ParseFloat(tradeData["price"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + limitPrice, err := strconv.ParseFloat(tradeData["limitprice"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + fee, err := strconv.ParseFloat(tradeData["fee"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + descriptionSubData := tradeData["description"].(map[string]interface{}) + descriptionPrice, err := strconv.ParseFloat(descriptionSubData["price"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + descriptionPrice2, err := strconv.ParseFloat(descriptionSubData["price2"].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + } + description := WsOpenOrderDescription{ + Close: descriptionSubData["close"].(string), + Leverage: descriptionSubData["leverage"].(string), + Order: descriptionSubData["order"].(string), + OrderType: descriptionSubData["ordertype"].(string), + Pair: descriptionSubData["pair"].(string), + Price: descriptionPrice, + Price2: descriptionPrice2, + Type: descriptionSubData["type"].(string), + } + + k.Websocket.DataHandler <- WsOpenOrders{ + Cost: cost, + ExpireTime: time.Unix(expireTime, expireTimeNano), + Description: description, + Fee: fee, + LimitPrice: limitPrice, + Misc: tradeData["misc"].(string), + OFlags: tradeData["oflags"].(string), + OpenTime: time.Unix(openTime, openTimeNano), + Price: price, + RefID: tradeData["refid"].(string), + StartTime: time.Unix(startTime, startTimeNano), + Status: tradeData["status"].(string), + StopPrice: stopPrice, + UserReference: userReference, + Volume: volume, + ExecutedVolume: executedVolume, + } + } + } + } else { + k.Websocket.DataHandler <- errors.New(k.Name + " - Invalid own trades data") } } @@ -219,7 +444,8 @@ func (k *Kraken) WsHandleEventResponse(response *WebsocketEventResponse, rawResp // allowing correlation between subscriptions and returned data func addNewSubscriptionChannelData(response *WebsocketEventResponse) { // We change the / to - to maintain compatibility with REST/config - pair := currency.NewPairWithDelimiter(response.Pair.Base.String(), response.Pair.Quote.String(), "-") + pair := currency.NewPairWithDelimiter(response.Pair.Base.String(), + response.Pair.Quote.String(), "-") subscriptionChannelPair = append(subscriptionChannelPair, WebsocketChannelData{ Subscription: response.Subscription.Name, Pair: pair, @@ -238,44 +464,81 @@ func getSubscriptionChannelData(id int64) WebsocketChannelData { } // wsProcessTickers converts ticker data and sends it to the datahandler -func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data interface{}) { - tickerData := data.(map[string]interface{}) - closeData := tickerData["c"].([]interface{}) - openData := tickerData["o"].([]interface{}) - lowData := tickerData["l"].([]interface{}) - highData := tickerData["h"].([]interface{}) - volumeData := tickerData["v"].([]interface{}) - closePrice, _ := strconv.ParseFloat(closeData[0].(string), 64) - openPrice, _ := strconv.ParseFloat(openData[0].(string), 64) - highPrice, _ := strconv.ParseFloat(highData[0].(string), 64) - lowPrice, _ := strconv.ParseFloat(lowData[0].(string), 64) - quantity, _ := strconv.ParseFloat(volumeData[0].(string), 64) +func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data map[string]interface{}) { + closePrice, err := strconv.ParseFloat(data["c"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + openPrice, err := strconv.ParseFloat(data["o"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + highPrice, err := strconv.ParseFloat(data["h"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + lowPrice, err := strconv.ParseFloat(data["l"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + quantity, err := strconv.ParseFloat(data["v"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + ask, err := strconv.ParseFloat(data["a"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + bid, err := strconv.ParseFloat(data["b"].([]interface{})[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } k.Websocket.DataHandler <- wshandler.TickerData{ - Timestamp: time.Now(), - Exchange: k.Name, - AssetType: orderbook.Spot, - Pair: channelData.Pair, - ClosePrice: closePrice, - OpenPrice: openPrice, - HighPrice: highPrice, - LowPrice: lowPrice, - Quantity: quantity, + Exchange: k.Name, + Open: openPrice, + Close: closePrice, + Volume: quantity, + High: highPrice, + Low: lowPrice, + Bid: bid, + Ask: ask, + Timestamp: time.Now(), + AssetType: asset.Spot, + Pair: channelData.Pair, } } // wsProcessTickers converts ticker data and sends it to the datahandler -func (k *Kraken) wsProcessSpread(channelData *WebsocketChannelData, data interface{}) { - spreadData := data.([]interface{}) - bestBid := spreadData[0].(string) - bestAsk := spreadData[1].(string) - timeData, _ := strconv.ParseFloat(spreadData[2].(string), 64) - bidVolume := spreadData[3].(string) - askVolume := spreadData[4].(string) +func (k *Kraken) wsProcessSpread(channelData *WebsocketChannelData, data []interface{}) { + bestBid := data[0].(string) + bestAsk := data[1].(string) + timeData, err := strconv.ParseFloat(data[2].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + bidVolume := data[3].(string) + askVolume := data[4].(string) sec, dec := math.Modf(timeData) spreadTimestamp := time.Unix(int64(sec), int64(dec*(1e9))) if k.Verbose { - log.Debugf("%v Spread data for '%v' received. Best bid: '%v' Best ask: '%v' Time: '%v', Bid volume '%v', Ask volume '%v'", + log.Debugf(log.ExchangeSys, + "%v Spread data for '%v' received. Best bid: '%v' Best ask: '%v' Time: '%v', Bid volume '%v', Ask volume '%v'", k.Name, channelData.Pair, bestBid, @@ -287,17 +550,31 @@ func (k *Kraken) wsProcessSpread(channelData *WebsocketChannelData, data interfa } // wsProcessTrades converts trade data and sends it to the datahandler -func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data interface{}) { - tradeData := data.([]interface{}) - for i := range tradeData { - trade := tradeData[i].([]interface{}) - timeData, _ := strconv.ParseInt(trade[2].(string), 10, 64) - timeUnix := time.Unix(timeData, 0) - price, _ := strconv.ParseFloat(trade[0].(string), 64) - amount, _ := strconv.ParseFloat(trade[1].(string), 64) +func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data []interface{}) { + for i := range data { + trade := data[i].([]interface{}) + timeData, err := strconv.ParseFloat(trade[2].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + sec, dec := math.Modf(timeData) + timeUnix := time.Unix(int64(sec), int64(dec*(1e9))) + + price, err := strconv.ParseFloat(trade[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + amount, err := strconv.ParseFloat(trade[1].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } k.Websocket.DataHandler <- wshandler.TradeData{ - AssetType: orderbook.Spot, + AssetType: asset.Spot, CurrencyPair: channelData.Pair, EventTime: time.Now().Unix(), Exchange: k.Name, @@ -311,17 +588,17 @@ func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data interfa // wsProcessOrderBook determines if the orderbook data is partial or update // Then sends to appropriate fun -func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data interface{}) { - obData := data.(map[string]interface{}) - if _, ok := obData["as"]; ok { - k.wsProcessOrderBookPartial(channelData, obData) +func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[string]interface{}) { + if fullAsk, ok := data["as"].([]interface{}); ok { + fullBids := data["as"].([]interface{}) + k.wsProcessOrderBookPartial(channelData, fullAsk, fullBids) } else { - _, asksExist := obData["a"] - _, bidsExist := obData["b"] + askData, asksExist := data["a"].([]interface{}) + bidData, bidsExist := data["b"].([]interface{}) if asksExist || bidsExist { k.wsRequestMtx.Lock() defer k.wsRequestMtx.Unlock() - err := k.wsProcessOrderBookUpdate(channelData, obData) + err := k.wsProcessOrderBookUpdate(channelData, askData, bidData) if err != nil { subscriptionToRemove := wshandler.WebsocketChannelSubscription{ Channel: krakenWsOrderbook, @@ -334,40 +611,64 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data inte } // wsProcessOrderBookPartial creates a new orderbook entry for a given currency pair -func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, obData map[string]interface{}) { +func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, askData, bidData []interface{}) { base := orderbook.Base{ Pair: channelData.Pair, - AssetType: orderbook.Spot, + AssetType: asset.Spot, } - // Kraken ob data is timestamped per price, GCT orderbook data is timestamped per entry - // Using the highest last update time, we can attempt to respect both within a reasonable degree + // Kraken ob data is timestamped per price, GCT orderbook data is + // timestamped per entry using the highest last update time, we can attempt + // to respect both within a reasonable degree var highestLastUpdate time.Time - askData := obData["as"].([]interface{}) for i := range askData { asks := askData[i].([]interface{}) - price, _ := strconv.ParseFloat(asks[0].(string), 64) - amount, _ := strconv.ParseFloat(asks[1].(string), 64) + price, err := strconv.ParseFloat(asks[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + amount, err := strconv.ParseFloat(asks[1].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } base.Asks = append(base.Asks, orderbook.Item{ Amount: amount, Price: price, }) - timeData, _ := strconv.ParseFloat(asks[2].(string), 64) + timeData, err := strconv.ParseFloat(asks[2].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } sec, dec := math.Modf(timeData) askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9))) if highestLastUpdate.Before(askUpdatedTime) { highestLastUpdate = askUpdatedTime } } - bidData := obData["bs"].([]interface{}) + for i := range bidData { bids := bidData[i].([]interface{}) - price, _ := strconv.ParseFloat(bids[0].(string), 64) - amount, _ := strconv.ParseFloat(bids[1].(string), 64) + price, err := strconv.ParseFloat(bids[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + amount, err := strconv.ParseFloat(bids[1].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } base.Bids = append(base.Bids, orderbook.Item{ Amount: amount, Price: price, }) - timeData, _ := strconv.ParseFloat(bids[2].(string), 64) + timeData, err := strconv.ParseFloat(bids[2].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } sec, dec := math.Modf(timeData) bidUpdateTime := time.Unix(int64(sec), int64(dec*(1e9))) if highestLastUpdate.Before(bidUpdateTime) { @@ -375,61 +676,82 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, ob } } base.LastUpdated = highestLastUpdate - err := k.Websocket.Orderbook.LoadSnapshot(&base, true) + base.ExchangeName = k.Name + err := k.Websocket.Orderbook.LoadSnapshot(&base) if err != nil { k.Websocket.DataHandler <- err return } k.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: k.Name, - Asset: orderbook.Spot, + Asset: asset.Spot, Pair: channelData.Pair, } } // wsProcessOrderBookUpdate updates an orderbook entry for a given currency pair -func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, obData map[string]interface{}) error { +func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, askData, bidData []interface{}) error { update := wsorderbook.WebsocketOrderbookUpdate{ - AssetType: krakenWsAssetType, - CurrencyPair: channelData.Pair, + Asset: asset.Spot, + Pair: channelData.Pair, } + var highestLastUpdate time.Time // Ask data is not always sent - if _, ok := obData["a"]; ok { - askData := obData["a"].([]interface{}) - for i := range askData { - asks := askData[i].([]interface{}) - price, _ := strconv.ParseFloat(asks[0].(string), 64) - amount, _ := strconv.ParseFloat(asks[1].(string), 64) - update.Asks = append(update.Asks, orderbook.Item{ - Amount: amount, - Price: price, - }) - timeData, _ := strconv.ParseFloat(asks[2].(string), 64) - sec, dec := math.Modf(timeData) - askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9))) - if highestLastUpdate.Before(askUpdatedTime) { - highestLastUpdate = askUpdatedTime - } + for i := range askData { + asks := askData[i].([]interface{}) + price, err := strconv.ParseFloat(asks[0].(string), 64) + if err != nil { + return err + } + + amount, err := strconv.ParseFloat(asks[1].(string), 64) + if err != nil { + return err + } + + update.Asks = append(update.Asks, orderbook.Item{ + Amount: amount, + Price: price, + }) + timeData, err := strconv.ParseFloat(asks[2].(string), 64) + if err != nil { + return err + } + + sec, dec := math.Modf(timeData) + askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9))) + if highestLastUpdate.Before(askUpdatedTime) { + highestLastUpdate = askUpdatedTime } } + // Bid data is not always sent - if _, ok := obData["b"]; ok { - bidData := obData["b"].([]interface{}) - for i := range bidData { - bids := bidData[i].([]interface{}) - price, _ := strconv.ParseFloat(bids[0].(string), 64) - amount, _ := strconv.ParseFloat(bids[1].(string), 64) - update.Bids = append(update.Bids, orderbook.Item{ - Amount: amount, - Price: price, - }) - timeData, _ := strconv.ParseFloat(bids[2].(string), 64) - sec, dec := math.Modf(timeData) - bidUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9))) - if highestLastUpdate.Before(bidUpdatedTime) { - highestLastUpdate = bidUpdatedTime - } + for i := range bidData { + bids := bidData[i].([]interface{}) + price, err := strconv.ParseFloat(bids[0].(string), 64) + if err != nil { + return err + } + + amount, err := strconv.ParseFloat(bids[1].(string), 64) + if err != nil { + return err + } + + update.Bids = append(update.Bids, orderbook.Item{ + Amount: amount, + Price: price, + }) + timeData, err := strconv.ParseFloat(bids[2].(string), 64) + if err != nil { + return err + } + + sec, dec := math.Modf(timeData) + bidUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9))) + if highestLastUpdate.Before(bidUpdatedTime) { + highestLastUpdate = bidUpdatedTime } } update.UpdateTime = highestLastUpdate @@ -440,27 +762,62 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, obD } k.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: k.Name, - Asset: orderbook.Spot, + Asset: asset.Spot, Pair: channelData.Pair, } return nil } // wsProcessCandles converts candle data and sends it to the data handler -func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data interface{}) { - candleData := data.([]interface{}) - startTimeData, _ := strconv.ParseInt(candleData[0].(string), 10, 64) - startTimeUnix := time.Unix(startTimeData, 0) - endTimeData, _ := strconv.ParseInt(candleData[1].(string), 10, 64) - endTimeUnix := time.Unix(endTimeData, 0) - openPrice, _ := strconv.ParseFloat(candleData[2].(string), 64) - highPrice, _ := strconv.ParseFloat(candleData[3].(string), 64) - lowPrice, _ := strconv.ParseFloat(candleData[4].(string), 64) - closePrice, _ := strconv.ParseFloat(candleData[5].(string), 64) - volume, _ := strconv.ParseFloat(candleData[7].(string), 64) +func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data []interface{}) { + startTime, err := strconv.ParseFloat(data[0].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + sec, dec := math.Modf(startTime) + startTimeUnix := time.Unix(int64(sec), int64(dec*(1e9))) + + endTime, err := strconv.ParseFloat(data[1].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + sec, dec = math.Modf(endTime) + endTimeUnix := time.Unix(int64(sec), int64(dec*(1e9))) + + openPrice, err := strconv.ParseFloat(data[2].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + highPrice, err := strconv.ParseFloat(data[3].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + lowPrice, err := strconv.ParseFloat(data[4].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + closePrice, err := strconv.ParseFloat(data[5].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } + + volume, err := strconv.ParseFloat(data[7].(string), 64) + if err != nil { + k.Websocket.DataHandler <- err + return + } k.Websocket.DataHandler <- wshandler.KlineData{ - AssetType: orderbook.Spot, + AssetType: asset.Spot, Pair: channelData.Pair, Timestamp: time.Now(), Exchange: k.Name, @@ -478,7 +835,7 @@ func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data interf // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (k *Kraken) GenerateDefaultSubscriptions() { - enabledCurrencies := k.GetEnabledCurrencies() + enabledCurrencies := k.GetEnabledPairs(asset.Spot) var subscriptions []wshandler.WebsocketChannelSubscription for i := range defaultSubscribedChannels { for j := range enabledCurrencies { @@ -492,16 +849,39 @@ func (k *Kraken) GenerateDefaultSubscriptions() { k.Websocket.SubscribeToChannels(subscriptions) } +// GenerateAuthenticatedSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (k *Kraken) GenerateAuthenticatedSubscriptions() { + var subscriptions []wshandler.WebsocketChannelSubscription + for i := range authenticatedChannels { + params := make(map[string]interface{}) + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ + Channel: authenticatedChannels[i], + Params: params, + }) + } + k.Websocket.SubscribeToChannels(subscriptions) +} + // Subscribe sends a websocket message to receive data from the channel func (k *Kraken) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { resp := WebsocketSubscriptionEventRequest{ Event: krakenWsSubscribe, - Pairs: []string{channelToSubscribe.Currency.String()}, Subscription: WebsocketSubscriptionData{ Name: channelToSubscribe.Channel, }, - RequestID: k.WebsocketConn.GenerateMessageID(true), + RequestID: k.WebsocketConn.GenerateMessageID(false), } + if channelToSubscribe.Channel == "book" { + // TODO: Add ability to make depth customisable + resp.Subscription.Depth = 1000 + } + if !channelToSubscribe.Currency.IsEmpty() { + resp.Pairs = []string{channelToSubscribe.Currency.String()} + } + if channelToSubscribe.Params != nil { + resp.Subscription.Token = authToken + } + _, err := k.WebsocketConn.SendMessageReturnResponse(resp.RequestID, resp) return err } @@ -514,8 +894,37 @@ func (k *Kraken) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscr Subscription: WebsocketSubscriptionData{ Name: channelToSubscribe.Channel, }, - RequestID: k.WebsocketConn.GenerateMessageID(true), + RequestID: k.WebsocketConn.GenerateMessageID(false), } _, err := k.WebsocketConn.SendMessageReturnResponse(resp.RequestID, resp) return err } + +func (k *Kraken) wsAddOrder(request *WsAddOrderRequest) (string, error) { + id := k.AuthenticatedWebsocketConn.GenerateMessageID(false) + request.UserReferenceID = strconv.FormatInt(id, 10) + request.Event = krakenWsAddOrder + request.Token = authToken + jsonResp, err := k.AuthenticatedWebsocketConn.SendMessageReturnResponse(id, request) + if err != nil { + return "", err + } + var resp WsAddOrderResponse + err = json.Unmarshal(jsonResp, &resp) + if err != nil { + return "", err + } + if resp.ErrorMessage != "" { + return "", fmt.Errorf(k.Name + " - " + resp.ErrorMessage) + } + return resp.TransactionID, nil +} + +func (k *Kraken) wsCancelOrders(orderIDs []string) error { + request := WsCancelOrderRequest{ + Event: krakenWsCancelOrder, + Token: authToken, + TransactionIDs: orderIDs, + } + return k.AuthenticatedWebsocketConn.SendMessage(request) +} diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 2177f1a2..71c23b31 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -3,19 +3,198 @@ package kraken import ( "errors" "fmt" + "strconv" "strings" "sync" "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (k *Kraken) GetDefaultConfig() (*config.ExchangeConfig, error) { + k.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = k.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = k.BaseCurrencies + + err := k.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if k.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = k.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets current default settings +func (k *Kraken) SetDefaults() { + k.Name = "Kraken" + k.Enabled = true + k.Verbose = true + k.API.CredentialsValidator.RequiresKey = true + k.API.CredentialsValidator.RequiresSecret = true + k.API.CredentialsValidator.RequiresBase64DecodeSecret = true + + k.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Separator: ",", + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "-", + Separator: ",", + }, + } + + k.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + SubmitOrder: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + FiatDeposit: true, + FiatWithdraw: true, + TradeFee: true, + FiatDepositFee: true, + FiatWithdrawalFee: true, + CryptoDepositFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + KlineFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + MessageCorrelation: true, + SubmitOrder: true, + CancelOrder: true, + CancelOrders: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup | + exchange.WithdrawCryptoWith2FA | + exchange.AutoWithdrawFiatWithSetup | + exchange.WithdrawFiatWith2FA, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + k.Requester = request.New(k.Name, + request.NewRateLimit(time.Second, krakenAuthRate), + request.NewRateLimit(time.Second, krakenUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + k.API.Endpoints.URLDefault = krakenAPIURL + k.API.Endpoints.URL = k.API.Endpoints.URLDefault + k.Websocket = wshandler.New() + k.API.Endpoints.WebsocketURL = krakenWSURL + k.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + k.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + k.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup sets current exchange configuration +func (k *Kraken) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + k.SetEnabled(false) + return nil + } + + err := k.SetupDefaults(exch) + if err != nil { + return err + } + + err = k.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: krakenWSURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: k.WsConnect, + Subscriber: k.Subscribe, + UnSubscriber: k.Unsubscribe, + Features: &k.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + k.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: k.Name, + URL: k.Websocket.GetWebsocketURL(), + ProxyURL: k.Websocket.GetProxyAddress(), + Verbose: k.Verbose, + RateLimit: krakenWsRateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + k.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: k.Name, + URL: krakenAuthWSURL, + ProxyURL: k.Websocket.GetProxyAddress(), + Verbose: k.Verbose, + RateLimit: krakenWsRateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + k.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + true, + true, + false, + false, + exch.Name) + return nil +} + // Start starts the Kraken go routine func (k *Kraken) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -28,67 +207,82 @@ func (k *Kraken) Start(wg *sync.WaitGroup) { // Run implements the Kraken wrapper func (k *Kraken) Run() { if k.Verbose { - log.Debugf("%s polling delay: %ds.\n", k.GetName(), k.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", k.GetName(), len(k.EnabledPairs), k.EnabledPairs) + k.PrintEnabledPairs() } - assetPairs, err := k.GetAssetPairs() - if err != nil { - log.Errorf("%s Failed to get available symbols.\n", k.GetName()) - } else { - forceUpgrade := false - if !common.StringDataContains(k.EnabledPairs.Strings(), "-") || - !common.StringDataContains(k.AvailablePairs.Strings(), "-") { - forceUpgrade = true - } + forceUpdate := false + if !common.StringDataContains(k.GetEnabledPairs(asset.Spot).Strings(), k.GetPairFormat(asset.Spot, false).Delimiter) || + !common.StringDataContains(k.GetAvailablePairs(asset.Spot).Strings(), k.GetPairFormat(asset.Spot, false).Delimiter) { + enabledPairs := currency.NewPairsFromStrings([]string{fmt.Sprintf("XBT%vUSD", k.GetPairFormat(asset.Spot, false).Delimiter)}) + log.Warn(log.ExchangeSys, "Available pairs for Kraken reset due to config upgrade, please enable the ones you would like again") + forceUpdate = true - var exchangeProducts []string - for i := range assetPairs { - v := assetPairs[i] - if common.StringContains(v.Altname, ".d") { - continue - } - if v.Base[0] == 'X' { - if len(v.Base) > 3 { - v.Base = v.Base[1:] - } - } - if v.Quote[0] == 'Z' || v.Quote[0] == 'X' { - v.Quote = v.Quote[1:] - } - exchangeProducts = append(exchangeProducts, v.Base+"-"+v.Quote) - } - - if forceUpgrade { - enabledPairs := currency.Pairs{currency.Pair{ - Base: currency.XBT, Quote: currency.USD, Delimiter: "-"}} - - log.Warn("Available pairs for Kraken reset due to config upgrade, please enable the ones you would like again") - - err = k.UpdateCurrencies(enabledPairs, true, true) - if err != nil { - log.Errorf("%s Failed to get config.\n", k.GetName()) - } - } - - var newExchangeProducts currency.Pairs - for _, p := range exchangeProducts { - newExchangeProducts = append(newExchangeProducts, - currency.NewPairFromString(p)) - } - - err = k.UpdateCurrencies(newExchangeProducts, false, forceUpgrade) + err := k.UpdatePairs(enabledPairs, asset.Spot, true, true) if err != nil { - log.Errorf("%s Failed to get config.\n", k.GetName()) + log.Errorf(log.ExchangeSys, + "%s failed to update currencies. Err: %s\n", + k.Name, + err) } } + + if !k.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { + return + } + + err := k.UpdateTradablePairs(forceUpdate) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + k.Name, + err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (k *Kraken) FetchTradablePairs(asset asset.Item) ([]string, error) { + pairs, err := k.GetAssetPairs() + if err != nil { + return nil, err + } + + var products []string + for i := range pairs { + v := pairs[i] + if strings.Contains(v.Altname, ".d") { + continue + } + if v.Base[0] == 'X' { + if len(v.Base) > 3 { + v.Base = v.Base[1:] + } + } + if v.Quote[0] == 'Z' || v.Quote[0] == 'X' { + v.Quote = v.Quote[1:] + } + products = append(products, v.Base+ + k.GetPairFormat(asset, false).Delimiter+ + v.Quote) + } + return products, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (k *Kraken) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := k.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return k.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) } // UpdateTicker updates and returns the ticker for a currency pair -func (k *Kraken) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (k *Kraken) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - pairs := k.GetEnabledCurrencies() - pairsCollated, err := exchange.GetAndFormatExchangeCurrencies(k.Name, pairs) + pairs := k.GetEnabledPairs(assetType) + pairsCollated, err := k.FormatExchangeCurrencies(pairs, assetType) if err != nil { return tickerPrice, err } @@ -97,38 +291,50 @@ func (k *Kraken) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, return tickerPrice, err } - for _, x := range pairs { - for y, z := range tickers { - if !common.StringContains(y, x.Base.Upper().String()) || - !common.StringContains(y, x.Quote.Upper().String()) { - continue + for i := range pairs { + for c, t := range tickers { + pairFmt := k.FormatExchangeCurrency(pairs[i], assetType).String() + if !strings.EqualFold(pairFmt, c) { + altCurrency, ok := assetPairMap[c] + if !ok { + continue + } + if !strings.EqualFold(pairFmt, altCurrency) { + continue + } + } + + tickerPrice = ticker.Price{ + Last: t.Last, + High: t.High, + Low: t.Low, + Bid: t.Bid, + Ask: t.Ask, + Volume: t.Volume, + Open: t.Open, + Pair: pairs[i], + } + err = ticker.ProcessTicker(k.Name, &tickerPrice, assetType) + if err != nil { + log.Error(log.Ticker, err) } - var tp ticker.Price - tp.Pair = x - tp.Last = z.Last - tp.Ask = z.Ask - tp.Bid = z.Bid - tp.High = z.High - tp.Low = z.Low - tp.Volume = z.Volume - ticker.ProcessTicker(k.GetName(), &tp, assetType) } } - return ticker.GetTicker(k.GetName(), p, assetType) + return ticker.GetTicker(k.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (k *Kraken) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(k.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (k *Kraken) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(k.Name, p, assetType) if err != nil { return k.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (k *Kraken) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(k.GetName(), p, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (k *Kraken) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(k.Name, p, assetType) if err != nil { return k.UpdateOrderbook(p, assetType) } @@ -136,9 +342,10 @@ func (k *Kraken) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Ba } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (k *Kraken) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (k *Kraken) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := k.GetDepth(exchange.FormatExchangeCurrency(k.GetName(), p).String()) + orderbookNew, err := k.GetDepth(k.FormatExchangeCurrency(p, + assetType).String()) if err != nil { return orderBook, err } @@ -152,7 +359,7 @@ func (k *Kraken) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.B } orderBook.Pair = p - orderBook.ExchangeName = k.GetName() + orderBook.ExchangeName = k.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -167,7 +374,7 @@ func (k *Kraken) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.B // Kraken exchange - to-do func (k *Kraken) GetAccountInfo() (exchange.AccountInfo, error) { var info exchange.AccountInfo - info.Exchange = k.GetName() + info.Exchange = k.Name bal, err := k.GetBalance() if err != nil { @@ -175,10 +382,10 @@ func (k *Kraken) GetAccountInfo() (exchange.AccountInfo, error) { } var balances []exchange.AccountCurrencyInfo - for key, data := range bal { + for key := range bal { balances = append(balances, exchange.AccountCurrencyInfo{ CurrencyName: currency.NewCode(key), - TotalValue: data, + TotalValue: bal[key], }) } @@ -192,80 +399,153 @@ func (k *Kraken) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (k *Kraken) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (k *Kraken) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (k *Kraken) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (k *Kraken) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - var args = AddOrderOptions{} - - response, err := k.AddOrder(p.String(), - side.ToString(), - orderType.ToString(), - amount, - price, - 0, - 0, - &args) - - if len(response.TransactionIds) > 0 { - submitOrderResponse.OrderID = strings.Join(response.TransactionIds, ", ") +func (k *Kraken) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + err := s.Validate() + if err != nil { + return submitOrderResponse, err } - if err == nil { + if k.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + var resp string + resp, err = k.wsAddOrder(&WsAddOrderRequest{ + OrderType: s.OrderType.String(), + OrderSide: s.OrderSide.String(), + Pair: s.Pair.String(), + Price: s.Price, + Volume: s.Amount, + }) + if err != nil { + return submitOrderResponse, err + } + submitOrderResponse.OrderID = resp submitOrderResponse.IsOrderPlaced = true + } else { + var response AddOrderResponse + response, err = k.AddOrder(s.Pair.String(), + s.OrderSide.String(), + s.OrderType.String(), + s.Amount, + s.Price, + 0, + 0, + &AddOrderOptions{}) + if err != nil { + return submitOrderResponse, err + } + if len(response.TransactionIds) > 0 { + submitOrderResponse.OrderID = strings.Join(response.TransactionIds, ", ") + } } - - return submitOrderResponse, err + if s.OrderType == order.Market { + submitOrderResponse.FullyMatched = true + } + submitOrderResponse.IsOrderPlaced = true + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (k *Kraken) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (k *Kraken) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (k *Kraken) CancelOrder(order *exchange.OrderCancellation) error { +func (k *Kraken) CancelOrder(order *order.Cancel) error { + if k.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + return k.wsCancelOrders([]string{order.OrderID}) + } _, err := k.CancelExistingOrder(order.OrderID) return err } // CancelAllOrders cancels all orders associated with a currency pair -func (k *Kraken) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (k *Kraken) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } + var emptyOrderOptions OrderInfoOptions openOrders, err := k.GetOpenOrders(emptyOrderOptions) if err != nil { return cancelAllOrdersResponse, err } - for orderID := range openOrders.Open { - _, err = k.CancelExistingOrder(orderID) + var err error + if k.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + err = k.wsCancelOrders([]string{orderID}) + } else { + _, err = k.CancelExistingOrder(orderID) + } if err != nil { - cancelAllOrdersResponse.OrderStatus[orderID] = err.Error() + cancelAllOrdersResponse.Status[orderID] = err.Error() } } - return cancelAllOrdersResponse, nil } // GetOrderInfo returns information on a current open order -func (k *Kraken) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail - return orderDetail, common.ErrNotYetImplemented +func (k *Kraken) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail + var emptyOrderOptions OrderInfoOptions + openOrders, err := k.GetOpenOrders(emptyOrderOptions) + if err != nil { + return orderDetail, err + } + if orderInfo, ok := openOrders.Open[orderID]; ok { + var trades []order.TradeHistory + for i := range orderInfo.Trades { + trades = append(trades, order.TradeHistory{ + TID: orderInfo.Trades[i], + }) + } + firstNum, decNum, err := convert.SplitFloatDecimals(orderInfo.StartTime) + if err != nil { + return orderDetail, err + } + side, err := order.StringToOrderSide(orderInfo.Description.Type) + if err != nil { + return orderDetail, err + } + status, err := order.StringToOrderStatus(orderInfo.Status) + if err != nil { + return orderDetail, err + } + oType, err := order.StringToOrderType(orderInfo.Description.OrderType) + if err != nil { + return orderDetail, err + } + + orderDetail = order.Detail{ + Exchange: k.Name, + ID: orderID, + CurrencyPair: currency.NewPairFromString(orderInfo.Description.Pair), + OrderSide: side, + OrderType: oType, + OrderDate: time.Unix(firstNum, decNum), + Status: status, + Price: orderInfo.Price, + Amount: orderInfo.Volume, + ExecutedAmount: orderInfo.VolumeExecuted, + RemainingAmount: orderInfo.Volume - orderInfo.VolumeExecuted, + Fee: orderInfo.Fee, + Trades: trades, + } + } else { + return orderDetail, errors.New(k.Name + " - Order ID not found: " + orderID) + } + + return orderDetail, nil } // GetDepositAddress returns a deposit address for a specified currency @@ -289,20 +569,20 @@ func (k *Kraken) GetDepositAddress(cryptocurrency currency.Code, _ string) (stri // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal // Populate exchange.WithdrawRequest.TradePassword with withdrawal key name, as set up on your account -func (k *Kraken) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (k *Kraken) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { return k.Withdraw(withdrawRequest.Currency.String(), withdrawRequest.TradePassword, withdrawRequest.Amount) } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (k *Kraken) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { - return k.WithdrawCryptocurrencyFunds(withdrawRequest) +func (k *Kraken) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { + return k.Withdraw(withdrawRequest.Currency.String(), withdrawRequest.TradePassword, withdrawRequest.Amount) } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (k *Kraken) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { - return k.WithdrawCryptocurrencyFunds(withdrawRequest) +func (k *Kraken) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { + return k.Withdraw(withdrawRequest.Currency.String(), withdrawRequest.TradePassword, withdrawRequest.Amount) } // GetWebsocket returns a pointer to the exchange websocket @@ -312,7 +592,7 @@ func (k *Kraken) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (k *Kraken) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (k.APIKey == "" || k.APISecret == "") && // Todo check connection status + if !k.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -320,49 +600,48 @@ func (k *Kraken) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) } // GetActiveOrders retrieves any orders that are active/open -func (k *Kraken) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (k *Kraken) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := k.GetOpenOrders(OrderInfoOptions{}) if err != nil { return nil, err } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range resp.Open { - symbol := currency.NewPairDelimiter(resp.Open[i].Descr.Pair, - k.ConfigCurrencyPairFormat.Delimiter) - orderDate := time.Unix(int64(resp.Open[i].StartTm), 0) - side := exchange.OrderSide(strings.ToUpper(resp.Open[i].Descr.Type)) + symbol := currency.NewPairFromString(resp.Open[i].Description.Pair) + orderDate := time.Unix(int64(resp.Open[i].StartTime), 0) + side := order.Side(strings.ToUpper(resp.Open[i].Description.Type)) + orderType := order.Type(strings.ToUpper(resp.Open[i].Description.OrderType)) - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ ID: i, - Amount: resp.Open[i].Vol, - RemainingAmount: (resp.Open[i].Vol - resp.Open[i].VolExec), - ExecutedAmount: resp.Open[i].VolExec, + Amount: resp.Open[i].Volume, + RemainingAmount: (resp.Open[i].Volume - resp.Open[i].VolumeExecuted), + ExecutedAmount: resp.Open[i].VolumeExecuted, Exchange: k.Name, OrderDate: orderDate, - Price: resp.Open[i].Price, + Price: resp.Open[i].Description.Price, OrderSide: side, + OrderType: orderType, CurrencyPair: symbol, }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (k *Kraken) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (k *Kraken) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { req := GetClosedOrdersOptions{} if getOrdersRequest.StartTicks.Unix() > 0 { - req.Start = fmt.Sprintf("%v", getOrdersRequest.StartTicks.Unix()) + req.Start = strconv.FormatInt(getOrdersRequest.StartTicks.Unix(), 10) } if getOrdersRequest.EndTicks.Unix() > 0 { - req.End = fmt.Sprintf("%v", getOrdersRequest.EndTicks.Unix()) + req.End = strconv.FormatInt(getOrdersRequest.EndTicks.Unix(), 10) } resp, err := k.GetClosedOrders(req) @@ -370,29 +649,29 @@ func (k *Kraken) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ return nil, err } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range resp.Closed { - symbol := currency.NewPairDelimiter(resp.Closed[i].Descr.Pair, - k.ConfigCurrencyPairFormat.Delimiter) - orderDate := time.Unix(int64(resp.Closed[i].StartTm), 0) - side := exchange.OrderSide(strings.ToUpper(resp.Closed[i].Descr.Type)) + symbol := currency.NewPairFromString(resp.Closed[i].Description.Pair) + orderDate := time.Unix(int64(resp.Closed[i].StartTime), 0) + side := order.Side(strings.ToUpper(resp.Closed[i].Description.Type)) + orderType := order.Type(strings.ToUpper(resp.Closed[i].Description.OrderType)) - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ ID: i, - Amount: resp.Closed[i].Vol, - RemainingAmount: (resp.Closed[i].Vol - resp.Closed[i].VolExec), - ExecutedAmount: resp.Closed[i].VolExec, + Amount: resp.Closed[i].Volume, + RemainingAmount: (resp.Closed[i].Volume - resp.Closed[i].VolumeExecuted), + ExecutedAmount: resp.Closed[i].VolumeExecuted, Exchange: k.Name, OrderDate: orderDate, - Price: resp.Closed[i].Price, + Price: resp.Closed[i].Description.Price, OrderSide: side, + OrderType: orderType, CurrencyPair: symbol, }) } - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - + order.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) + order.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) return orders, nil } @@ -417,5 +696,9 @@ func (k *Kraken) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, e // AuthenticateWebsocket sends an authentication message to the websocket func (k *Kraken) AuthenticateWebsocket() error { - return common.ErrFunctionNotSupported + resp, err := k.GetWebsocketToken() + if resp != "" { + authToken = resp + } + return err } diff --git a/exchanges/lakebtc/README.md b/exchanges/lakebtc/README.md index f3839489..0a043476 100644 --- a/exchanges/lakebtc/README.md +++ b/exchanges/lakebtc/README.md @@ -23,6 +23,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader ### Current Features + REST Support ++ Websocket Support ### How to enable @@ -47,22 +48,22 @@ main.go ```go var l exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "LakeBTC" { - l = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "LakeBTC" { + l = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := l.GetTickerPrice() +tick, err := l.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := l.GetOrderbookEx() +ob, err := l.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +131,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/lakebtc/lakebtc.go b/exchanges/lakebtc/lakebtc.go index 14ec9bb6..1aef5c5b 100644 --- a/exchanges/lakebtc/lakebtc.go +++ b/exchanges/lakebtc/lakebtc.go @@ -1,20 +1,16 @@ package lakebtc import ( + "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" - "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -44,115 +40,13 @@ type LakeBTC struct { WebsocketConn } -// SetDefaults sets LakeBTC defaults -func (l *LakeBTC) SetDefaults() { - l.Name = "LakeBTC" - l.Enabled = false - l.TakerFee = 0.2 - l.MakerFee = 0.15 - l.Verbose = false - l.RESTPollingDelay = 10 - l.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | - exchange.WithdrawFiatViaWebsiteOnly - l.RequestCurrencyPairFormat.Delimiter = "" - l.RequestCurrencyPairFormat.Uppercase = true - l.ConfigCurrencyPairFormat.Delimiter = "" - l.ConfigCurrencyPairFormat.Uppercase = true - l.AssetTypes = []string{ticker.Spot} - l.SupportsAutoPairUpdating = true - l.SupportsRESTTickerBatching = true - l.Requester = request.New(l.Name, - request.NewRateLimit(time.Second, lakeBTCAuthRate), - request.NewRateLimit(time.Second, lakeBTCUnauth), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - l.APIUrlDefault = lakeBTCAPIURL - l.APIUrl = l.APIUrlDefault - l.Websocket = wshandler.New() - l.Websocket.Functionality = wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketSubscribeSupported - l.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup sets exchange configuration profile -func (l *LakeBTC) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - l.SetEnabled(false) - } else { - l.Enabled = true - l.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - l.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - l.SetHTTPClientTimeout(exch.HTTPTimeout) - l.SetHTTPClientUserAgent(exch.HTTPUserAgent) - l.RESTPollingDelay = exch.RESTPollingDelay - l.Verbose = exch.Verbose - l.HTTPDebugging = exch.HTTPDebugging - l.BaseCurrencies = exch.BaseCurrencies - l.AvailablePairs = exch.AvailablePairs - l.EnabledPairs = exch.EnabledPairs - err := l.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = l.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = l.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = l.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = l.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = l.Websocket.Setup(l.WsConnect, - l.Subscribe, - nil, - exch.Name, - exch.Websocket, - exch.Verbose, - lakeBTCWSURL, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - l.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - false, - false, - false, - false, - exch.Name) - } -} - -// GetTradablePairs returns a list of available pairs from the exchange -func (l *LakeBTC) GetTradablePairs() ([]string, error) { - result, err := l.GetTicker() - if err != nil { - return nil, err - } - - var currencies []string - for x := range result { - currencies = append(currencies, common.StringToUpper(x)) - } - - return currencies, nil -} - // GetTicker returns the current ticker from lakeBTC func (l *LakeBTC) GetTicker() (map[string]Ticker, error) { response := make(map[string]TickerResponse) - path := fmt.Sprintf("%s/%s", l.APIUrl, lakeBTCTicker) + path := fmt.Sprintf("%s/%s", l.API.Endpoints.URL, lakeBTCTicker) - if err := l.SendHTTPRequest(path, &response); err != nil { + err := l.SendHTTPRequest(path, &response) + if err != nil { return nil, err } @@ -160,24 +54,42 @@ func (l *LakeBTC) GetTicker() (map[string]Ticker, error) { for k, v := range response { var tick Ticker - key := common.StringToUpper(k) + key := strings.ToUpper(k) if v.Ask != nil { - tick.Ask, _ = strconv.ParseFloat(v.Ask.(string), 64) + tick.Ask, err = strconv.ParseFloat(v.Ask.(string), 64) + if err != nil { + return nil, err + } } if v.Bid != nil { - tick.Bid, _ = strconv.ParseFloat(v.Bid.(string), 64) + tick.Bid, err = strconv.ParseFloat(v.Bid.(string), 64) + if err != nil { + return nil, err + } } if v.High != nil { - tick.High, _ = strconv.ParseFloat(v.High.(string), 64) + tick.High, err = strconv.ParseFloat(v.High.(string), 64) + if err != nil { + return nil, err + } } if v.Last != nil { - tick.Last, _ = strconv.ParseFloat(v.Last.(string), 64) + tick.Last, err = strconv.ParseFloat(v.Last.(string), 64) + if err != nil { + return nil, err + } } if v.Low != nil { - tick.Low, _ = strconv.ParseFloat(v.Low.(string), 64) + tick.Low, err = strconv.ParseFloat(v.Low.(string), 64) + if err != nil { + return nil, err + } } if v.Volume != nil { - tick.Volume, _ = strconv.ParseFloat(v.Volume.(string), 64) + tick.Volume, err = strconv.ParseFloat(v.Volume.(string), 64) + if err != nil { + return nil, err + } } result[key] = tick } @@ -190,7 +102,7 @@ func (l *LakeBTC) GetOrderBook(currency string) (Orderbook, error) { Bids [][]string `json:"bids"` Asks [][]string `json:"asks"` } - path := fmt.Sprintf("%s/%s?symbol=%s", l.APIUrl, lakeBTCOrderbook, common.StringToLower(currency)) + path := fmt.Sprintf("%s/%s?symbol=%s", l.API.Endpoints.URL, lakeBTCOrderbook, strings.ToLower(currency)) resp := Response{} err := l.SendHTTPRequest(path, &resp) if err != nil { @@ -201,12 +113,12 @@ func (l *LakeBTC) GetOrderBook(currency string) (Orderbook, error) { for _, x := range resp.Bids { price, err := strconv.ParseFloat(x[0], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } amount, err := strconv.ParseFloat(x[1], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } orderbook.Bids = append(orderbook.Bids, OrderbookStructure{price, amount}) @@ -215,12 +127,12 @@ func (l *LakeBTC) GetOrderBook(currency string) (Orderbook, error) { for _, x := range resp.Asks { price, err := strconv.ParseFloat(x[0], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } amount, err := strconv.ParseFloat(x[1], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } orderbook.Asks = append(orderbook.Asks, OrderbookStructure{price, amount}) @@ -230,16 +142,14 @@ func (l *LakeBTC) GetOrderBook(currency string) (Orderbook, error) { // GetTradeHistory returns the trade history for a given currency pair func (l *LakeBTC) GetTradeHistory(currency string) ([]TradeHistory, error) { - path := fmt.Sprintf("%s/%s?symbol=%s", l.APIUrl, lakeBTCTrades, common.StringToLower(currency)) + path := fmt.Sprintf("%s/%s?symbol=%s", l.API.Endpoints.URL, lakeBTCTrades, strings.ToLower(currency)) var resp []TradeHistory - return resp, l.SendHTTPRequest(path, &resp) } // GetAccountInformation returns your current account information func (l *LakeBTC) GetAccountInformation() (AccountInfo, error) { resp := AccountInfo{} - return resp, l.SendAuthenticatedHTTPRequest(lakeBTCGetAccountInfo, "", &resp) } @@ -282,7 +192,7 @@ func (l *LakeBTC) GetOrders(orders []int64) ([]Orders, error) { var resp []Orders return resp, - l.SendAuthenticatedHTTPRequest(lakeBTCGetOrders, common.JoinStrings(ordersStr, ","), &resp) + l.SendAuthenticatedHTTPRequest(lakeBTCGetOrders, strings.Join(ordersStr, ","), &resp) } // CancelExistingOrder cancels an order by ID number and returns an error @@ -311,7 +221,7 @@ func (l *LakeBTC) CancelExistingOrders(orderIDs []string) error { } resp := Response{} - params := common.JoinStrings(orderIDs, ",") + params := strings.Join(orderIDs, ",") err := l.SendAuthenticatedHTTPRequest(lakeBTCCancelOrder, params, &resp) if err != nil { return err @@ -374,36 +284,36 @@ func (l *LakeBTC) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an autheticated HTTP request to a LakeBTC func (l *LakeBTC) SendAuthenticatedHTTPRequest(method, params string, result interface{}) (err error) { - if !l.AuthenticatedAPISupport { + if !l.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, l.Name) } n := l.Requester.GetNonce(true).String() - req := fmt.Sprintf("tonce=%s&accesskey=%s&requestmethod=post&id=1&method=%s¶ms=%s", n, l.APIKey, method, params) - hmac := common.GetHMAC(common.HashSHA1, []byte(req), []byte(l.APISecret)) + req := fmt.Sprintf("tonce=%s&accesskey=%s&requestmethod=post&id=1&method=%s¶ms=%s", n, l.API.Credentials.Key, method, params) + hmac := crypto.GetHMAC(crypto.HashSHA1, []byte(req), []byte(l.API.Credentials.Secret)) if l.Verbose { - log.Debugf("Sending POST request to %s calling method %s with params %s\n", l.APIUrl, method, req) + log.Debugf(log.ExchangeSys, "Sending POST request to %s calling method %s with params %s\n", l.API.Endpoints.URL, method, req) } postData := make(map[string]interface{}) postData["method"] = method postData["id"] = 1 - postData["params"] = common.SplitStrings(params, ",") + postData["params"] = strings.Split(params, ",") - data, err := common.JSONEncode(postData) + data, err := json.Marshal(postData) if err != nil { return err } headers := make(map[string]string) headers["Json-Rpc-Tonce"] = l.Nonce.String() - headers["Authorization"] = "Basic " + common.Base64Encode([]byte(l.APIKey+":"+common.HexEncodeToString(hmac))) + headers["Authorization"] = "Basic " + crypto.Base64Encode([]byte(l.API.Credentials.Key+":"+crypto.HexEncodeToString(hmac))) headers["Content-Type"] = "application/json-rpc" return l.SendPayload(http.MethodPost, - l.APIUrl, + l.API.Endpoints.URL, headers, strings.NewReader(string(data)), result, diff --git a/exchanges/lakebtc/lakebtc_test.go b/exchanges/lakebtc/lakebtc_test.go index 2903b6a4..afb75558 100644 --- a/exchanges/lakebtc/lakebtc_test.go +++ b/exchanges/lakebtc/lakebtc_test.go @@ -1,19 +1,21 @@ package lakebtc import ( - "fmt" + "log" + "os" "testing" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) var l LakeBTC -var setupRan bool // Please add your own APIkeys to do correct due diligence testing. const ( @@ -22,35 +24,35 @@ const ( canManipulateRealOrders = false ) -func TestSetDefaults(t *testing.T) { - if !setupRan { - l.SetDefaults() - } -} - -func TestSetup(t *testing.T) { - if !setupRan { - cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") - lakebtcConfig, err := cfg.GetExchangeConfig("LakeBTC") - if err != nil { - t.Error("Test Failed - LakeBTC Setup() init error") - } - lakebtcConfig.AuthenticatedAPISupport = true - lakebtcConfig.APIKey = apiKey - lakebtcConfig.APISecret = apiSecret - lakebtcConfig.Websocket = true - l.Setup(&lakebtcConfig) - l.WebsocketURL = lakeBTCWSURL - setupRan = true - } -} - -func TestGetTradablePairs(t *testing.T) { - t.Parallel() - _, err := l.GetTradablePairs() +func TestMain(m *testing.M) { + l.SetDefaults() + cfg := config.GetConfig() + err := cfg.LoadConfig("../../testdata/configtest.json", true) if err != nil { - t.Fatalf("Test failed. GetTradablePairs err: %s", err) + log.Fatal("LakeBTC load config error", err) + } + lakebtcConfig, err := cfg.GetExchangeConfig("LakeBTC") + if err != nil { + log.Fatal("LakeBTC Setup() init error") + } + lakebtcConfig.API.AuthenticatedSupport = true + lakebtcConfig.API.Credentials.Key = apiKey + lakebtcConfig.API.Credentials.Secret = apiSecret + lakebtcConfig.Features.Enabled.Websocket = true + err = l.Setup(lakebtcConfig) + if err != nil { + log.Fatal("LakeBTC setup error", err) + } + l.API.Endpoints.WebsocketURL = lakeBTCWSURL + + os.Exit(m.Run()) +} + +func TestFetchTradablePairs(t *testing.T) { + t.Parallel() + _, err := l.FetchTradablePairs(asset.Spot) + if err != nil { + t.Fatalf("GetTradablePairs err: %s", err) } } @@ -58,7 +60,7 @@ func TestGetTicker(t *testing.T) { t.Parallel() _, err := l.GetTicker() if err != nil { - t.Error("Test Failed - GetTicker() error", err) + t.Error("GetTicker() error", err) } } @@ -66,7 +68,7 @@ func TestGetOrderBook(t *testing.T) { t.Parallel() _, err := l.GetOrderBook("BTCUSD") if err != nil { - t.Error("Test Failed - GetOrderBook() error", err) + t.Error("GetOrderBook() error", err) } } @@ -74,73 +76,73 @@ func TestGetTradeHistory(t *testing.T) { t.Parallel() _, err := l.GetTradeHistory("BTCUSD") if err != nil { - t.Error("Test Failed - GetTradeHistory() error", err) + t.Error("GetTradeHistory() error", err) } } func TestTrade(t *testing.T) { t.Parallel() - if l.APIKey == "" || l.APISecret == "" { + if !l.ValidateAPICredentials() { t.Skip() } _, err := l.Trade(false, 0, 0, "USD") if err == nil { - t.Error("Test Failed - Trade() error", err) + t.Error("Trade() Expected error") } } func TestGetOpenOrders(t *testing.T) { t.Parallel() - if l.APIKey == "" || l.APISecret == "" { + if !l.ValidateAPICredentials() { t.Skip() } _, err := l.GetOpenOrders() if err == nil { - t.Error("Test Failed - GetOpenOrders() error", err) + t.Error("GetOpenOrders() Expected error") } } func TestGetOrders(t *testing.T) { t.Parallel() - if l.APIKey == "" || l.APISecret == "" { + if !l.ValidateAPICredentials() { t.Skip() } _, err := l.GetOrders([]int64{1, 2}) if err == nil { - t.Error("Test Failed - GetOrders() error", err) + t.Error("GetOrders() Expected error") } } func TestCancelOrder(t *testing.T) { t.Parallel() - if l.APIKey == "" || l.APISecret == "" { + if !l.ValidateAPICredentials() { t.Skip() } err := l.CancelExistingOrder(1337) if err == nil { - t.Error("Test Failed - CancelExistingOrder() error", err) + t.Error("CancelExistingOrder() Expected error") } } func TestGetTrades(t *testing.T) { t.Parallel() - if l.APIKey == "" || l.APISecret == "" { + if !l.ValidateAPICredentials() { t.Skip() } _, err := l.GetTrades(1337) if err == nil { - t.Error("Test Failed - GetTrades() error", err) + t.Error("GetTrades() Expected error") } } func TestGetExternalAccounts(t *testing.T) { t.Parallel() - if l.APIKey == "" || l.APISecret == "" { + if !l.ValidateAPICredentials() { t.Skip() } _, err := l.GetExternalAccounts() if err == nil { - t.Error("Test Failed - GetExternalAccounts() error", err) + t.Error("GetExternalAccounts() Expected error") } } @@ -162,7 +164,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() l.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -179,7 +181,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee Basic if resp, err := l.GetFee(feeBuilder); resp != float64(0.002) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } // CryptocurrencyTradeFee High quantity @@ -187,7 +189,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := l.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) t.Error(err) } @@ -195,7 +197,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := l.GetFee(feeBuilder); resp != float64(0.0015) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) t.Error(err) } @@ -203,14 +205,14 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -219,7 +221,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -227,7 +229,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := l.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) t.Error(err) } @@ -235,7 +237,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -244,28 +246,22 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - l.SetDefaults() expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.WithdrawFiatViaWebsiteOnlyText - withdrawPermissions := l.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - l.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := l.GetActiveOrders(&getOrdersRequest) @@ -277,11 +273,8 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - l.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := l.GetOrderHistory(&getOrdersRequest) @@ -295,27 +288,26 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if l.APIKey != "" && l.APIKey != "Key" && - l.APISecret != "" && l.APISecret != "Secret" { - return true - } - return false + return l.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - l.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.BTC, - Quote: currency.EUR, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.EUR, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := l.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 10, "hi") + response, err := l.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -324,16 +316,12 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - l.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -350,16 +338,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - l.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -375,26 +359,29 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestModifyOrder(t *testing.T) { - _, err := l.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := l.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - l.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "7860767916", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -411,15 +398,11 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - l.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := l.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -427,15 +410,11 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - l.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := l.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -446,20 +425,18 @@ func TestGetDepositAddress(t *testing.T) { if areTestAPIKeysSet() { _, err := l.GetDepositAddress(currency.BTC, "") if err != nil { - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) } } else { _, err := l.GetDepositAddress(currency.DASH, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error cannot be nil") + t.Error("GetDepositAddress() error cannot be nil") } } } // TestWsConn websocket connection test func TestWsConn(t *testing.T) { - TestSetDefaults(t) - TestSetup(t) if !l.Websocket.IsEnabled() { t.Skip(wshandler.WebsocketNotEnabled) } @@ -473,8 +450,6 @@ func TestWsConn(t *testing.T) { // TestWsTradeProcessing logic test func TestWsTradeProcessing(t *testing.T) { - TestSetDefaults(t) - TestSetup(t) l.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() l.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() json := `{"trades":[{"type":"sell","date":1564985787,"price":"11913.02","amount":"0.49"}]}` @@ -486,10 +461,9 @@ func TestWsTradeProcessing(t *testing.T) { // TestWsTickerProcessing logic test func TestWsTickerProcessing(t *testing.T) { - TestSetDefaults(t) - TestSetup(t) - l.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() - l.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() + const testChanSize = 26 + l.Websocket.DataHandler = make(chan interface{}, testChanSize) + l.Websocket.TrafficAlert = make(chan struct{}, testChanSize) json := `{"btcusd":{"low":"10990.05","high":"11966.24","last":"11903.29","volume":"1803.967079","sell":"11912.39","buy":"11902.2"},"btceur":{"low":"9886.87","high":"10732.72","last":"10691.44","volume":"87.994478","sell":"10711.62","buy":"10691.44"},"btchkd":{"low":null,"high":null,"last":"51776.98","volume":null,"sell":"93307.37","buy":"93177.56"},"btcjpy":{"low":"1176039.0","high":"1272246.0","last":"1265680.0","volume":"129.021421","sell":"1266764.0","buy":"1265680.0"},"btcgbp":{"low":"9157.12","high":"9953.43","last":"9941.28","volume":"10.4997","sell":"10007.89","buy":"9941.28"},"btcaud":{"low":"16102.57","high":"17594.22","last":"17548.16","volume":"7.338316","sell":"17616.67","buy":"17549.69"},"btccad":{"low":"14541.69","high":"15834.87","last":"15763.54","volume":"30.480309","sell":"15793.45","buy":"15756.13"},"btcsgd":{"low":"15133.82","high":"16501.62","last":"16455.53","volume":"4.044026","sell":"16484.37","buy":"16462.18"},"btcchf":{"low":"10800.58","high":"11526.24","last":"11526.24","volume":"0.1765","sell":"11675.34","buy":"11632.02"},"btcnzd":{"low":null,"high":null,"last":"8340.98","volume":null,"sell":"18315.49","buy":"18221.37"},"btcngn":{"low":null,"high":null,"last":"600000.0","volume":null,"sell":null,"buy":null},"eurusd":{"low":"1.1088","high":"1.1138","last":"1.1125","volume":"2680.105249","sell":"1.1142","buy":"1.1121"},"gbpusd":{"low":"1.1934","high":"1.1958","last":"1.1934","volume":"1493.923823","sell":"1.1979","buy":"1.1903"},"usdjpy":{"low":"105.26","high":"107.25","last":"106.33","volume":"114490.2179","sell":"106.34","buy":"106.27"},"usdhkd":{"low":null,"high":null,"last":"7.851","volume":null,"sell":"7.8328","buy":"7.8286"},"usdcad":{"low":"1.3225","high":"1.3272","last":"1.3255","volume":"11033.9877","sell":"1.3258","buy":"1.3238"},"usdsgd":{"low":"1.3776","high":"1.3839","last":"1.3838","volume":"2523.75","sell":"1.3838","buy":"1.3819"},"audusd":{"low":"0.6764","high":"0.6853","last":"0.6771","volume":"5442.608321","sell":"0.6782","buy":"0.6762"},"nzdusd":{"low":null,"high":null,"last":"0.6758","volume":null,"sell":"0.6532","buy":"0.6504"},"usdchf":{"low":"0.9838","high":"0.9838","last":"0.9838","volume":"108.3352","sell":"0.9801","buy":"0.9773"},"usdngn":{"low":null,"high":null,"last":"200.0","volume":null,"sell":null,"buy":null},"ethbtc":{"low":"0.0205","high":"0.025","last":"0.0205","volume":null,"sell":"0.03","buy":"0.0194"},"ltcbtc":{"low":null,"high":null,"last":"0.0114","volume":null,"sell":"0.009","buy":"0.0073"},"bchbtc":{"low":null,"high":null,"last":"0.0544","volume":null,"sell":"0.0322","buy":"0.0274"},"xrpbtc":{"low":"0.000042","high":"0.000042","last":"0.000042","volume":null,"sell":"0.000037","buy":"0.000022"},"baceth":{"low":"0.000035","high":"0.000035","last":"0.000035","volume":null,"sell":"0.0015","buy":null}}` err := l.processTicker(json) if err != nil { @@ -499,7 +473,7 @@ func TestWsTickerProcessing(t *testing.T) { func TestGetCurrencyFromChannel(t *testing.T) { curr := currency.NewPair(currency.LTC, currency.BTC) - result := l.getCurrencyFromChannel(fmt.Sprintf("%v%v%v", marketSubstring, curr, globalSubstring)) + result := l.getCurrencyFromChannel(marketSubstring + curr.String() + globalSubstring) if curr != result { t.Errorf("currency result is not equal. Expected %v", curr) } @@ -507,8 +481,6 @@ func TestGetCurrencyFromChannel(t *testing.T) { // TestWsOrderbookProcessing logic test func TestWsOrderbookProcessing(t *testing.T) { - TestSetDefaults(t) - TestSetup(t) l.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() l.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() json := `{"asks":[["11905.66","0.0019"],["11905.73","0.0015"],["11906.43","0.0013"],["11906.62","0.0019"],["11907.25","11.087"],["11907.66","0.0006"],["11907.73","0.3113"],["11907.84","0.0006"],["11908.37","0.0016"],["11908.86","10.3786"],["11909.54","4.2955"],["11910.15","0.0012"],["11910.56","13.5505"],["11911.06","0.0011"],["11911.37","0.0023"]],"bids":[["11905.55","0.0171"],["11904.43","0.0225"],["11903.31","0.0223"],["11902.2","0.0027"],["11901.92","1.002"],["11901.6","0.0015"],["11901.49","0.0012"],["11901.08","0.0227"],["11900.93","0.0009"],["11900.53","1.662"],["11900.08","0.001"],["11900.01","3.6745"],["11899.96","0.003"],["11899.91","0.0006"],["11899.44","0.0013"]]}` diff --git a/exchanges/lakebtc/lakebtc_websocket.go b/exchanges/lakebtc/lakebtc_websocket.go index 1cdac6a2..0bdf0955 100644 --- a/exchanges/lakebtc/lakebtc_websocket.go +++ b/exchanges/lakebtc/lakebtc_websocket.go @@ -1,14 +1,16 @@ package lakebtc import ( + "encoding/json" "errors" "fmt" "strconv" "strings" "time" - "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" @@ -20,9 +22,10 @@ const ( marketGlobalEndpoint = "market-global" marketSubstring = "market-" globalSubstring = "-global" - volumeString = "volume" - highString = "high" - lowString = "low" + tickerHighString = "high" + tickerLastString = "last" + tickerLowString = "low" + tickerVolumeString = "volume" wssSchem = "wss" ) @@ -53,15 +56,15 @@ func (l *LakeBTC) listenToEndpoints() error { var err error l.WebsocketConn.Ticker, err = l.WebsocketConn.Client.Bind("tickers") if err != nil { - return fmt.Errorf("%s Websocket Bind error: %s", l.GetName(), err) + return fmt.Errorf("%s Websocket Bind error: %s", l.Name, err) } l.WebsocketConn.Orderbook, err = l.WebsocketConn.Client.Bind("update") if err != nil { - return fmt.Errorf("%s Websocket Bind error: %s", l.GetName(), err) + return fmt.Errorf("%s Websocket Bind error: %s", l.Name, err) } l.WebsocketConn.Trade, err = l.WebsocketConn.Client.Bind("trades") if err != nil { - return fmt.Errorf("%s Websocket Bind error: %s", l.GetName(), err) + return fmt.Errorf("%s Websocket Bind error: %s", l.Name, err) } return nil } @@ -69,10 +72,14 @@ func (l *LakeBTC) listenToEndpoints() error { // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (l *LakeBTC) GenerateDefaultSubscriptions() { var subscriptions []wshandler.WebsocketChannelSubscription - enabledCurrencies := l.GetEnabledCurrencies() + enabledCurrencies := l.GetEnabledPairs(asset.Spot) + for j := range enabledCurrencies { enabledCurrencies[j].Delimiter = "" - channel := fmt.Sprintf("%v%v%v", marketSubstring, enabledCurrencies[j].Lower(), globalSubstring) + channel := marketSubstring + + enabledCurrencies[j].Lower().String() + + globalSubstring + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: channel, Currency: enabledCurrencies[j], @@ -96,7 +103,8 @@ func (l *LakeBTC) wsHandleIncomingData() { return case data := <-l.WebsocketConn.Ticker: if l.Verbose { - log.Debugf("%v Websocket message received: %v", l.Name, data) + log.Debugf(log.ExchangeSys, + "%v Websocket message received: %v", l.Name, data) } l.Websocket.TrafficAlert <- struct{}{} err := l.processTicker(data.Data) @@ -107,7 +115,8 @@ func (l *LakeBTC) wsHandleIncomingData() { case data := <-l.WebsocketConn.Trade: l.Websocket.TrafficAlert <- struct{}{} if l.Verbose { - log.Debugf("%v Websocket message received: %v", l.Name, data) + log.Debugf(log.ExchangeSys, + "%v Websocket message received: %v", l.Name, data) } err := l.processTrades(data.Data, data.Channel) if err != nil { @@ -117,7 +126,8 @@ func (l *LakeBTC) wsHandleIncomingData() { case data := <-l.WebsocketConn.Orderbook: l.Websocket.TrafficAlert <- struct{}{} if l.Verbose { - log.Debugf("%v Websocket message received: %v", l.Name, data) + log.Debugf(log.ExchangeSys, + "%v Websocket message received: %v", l.Name, data) } err := l.processOrderbook(data.Data, data.Channel) if err != nil { @@ -130,7 +140,7 @@ func (l *LakeBTC) wsHandleIncomingData() { func (l *LakeBTC) processTrades(data, channel string) error { var tradeData WsTrades - err := common.JSONDecode([]byte(data), &tradeData) + err := json.Unmarshal([]byte(data), &tradeData) if err != nil { return err } @@ -139,9 +149,9 @@ func (l *LakeBTC) processTrades(data, channel string) error { l.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Unix(tradeData.Trades[i].Date, 0), CurrencyPair: curr, - AssetType: orderbook.Spot, - Exchange: l.GetName(), - EventType: orderbook.Spot, + AssetType: asset.Spot, + Exchange: l.Name, + EventType: asset.Spot.String(), EventTime: tradeData.Trades[i].Date, Price: tradeData.Trades[i].Price, Amount: tradeData.Trades[i].Amount, @@ -153,14 +163,17 @@ func (l *LakeBTC) processTrades(data, channel string) error { func (l *LakeBTC) processOrderbook(obUpdate, channel string) error { var update WsOrderbookUpdate - err := common.JSONDecode([]byte(obUpdate), &update) + err := json.Unmarshal([]byte(obUpdate), &update) if err != nil { return err } + + p := l.getCurrencyFromChannel(channel) + book := orderbook.Base{ - Pair: l.getCurrencyFromChannel(channel), + Pair: p, LastUpdated: time.Now(), - AssetType: orderbook.Spot, + AssetType: asset.Spot, ExchangeName: l.Name, } @@ -181,6 +194,7 @@ func (l *LakeBTC) processOrderbook(obUpdate, channel string) error { Price: price, }) } + for i := 0; i < len(update.Bids); i++ { var amount, price float64 amount, err = strconv.ParseFloat(update.Bids[i][1], 64) @@ -198,7 +212,19 @@ func (l *LakeBTC) processOrderbook(obUpdate, channel string) error { Price: price, }) } - return l.Websocket.Orderbook.LoadSnapshot(&book, true) + + err = l.Websocket.Orderbook.LoadSnapshot(&book) + if err != nil { + return err + } + + l.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Pair: p, + Asset: asset.Spot, + Exchange: l.Name, + } + + return nil } func (l *LakeBTC) getCurrencyFromChannel(channel string) currency.Pair { @@ -209,39 +235,47 @@ func (l *LakeBTC) getCurrencyFromChannel(channel string) currency.Pair { func (l *LakeBTC) processTicker(ticker string) error { var tUpdate map[string]interface{} - err := common.JSONDecode([]byte(ticker), &tUpdate) + err := json.Unmarshal([]byte(ticker), &tUpdate) if err != nil { l.Websocket.DataHandler <- err return err } + + enabled := l.GetEnabledPairs(asset.Spot) for k, v := range tUpdate { + returnCurrency := currency.NewPairFromString(k) + if !enabled.Contains(returnCurrency, true) { + continue + } + tickerData := v.(map[string]interface{}) - if tickerData[highString] == nil || tickerData[lowString] == nil || tickerData[volumeString] == nil { - continue - } - high, err := strconv.ParseFloat(tickerData[highString].(string), 64) - if err != nil { - l.Websocket.DataHandler <- fmt.Errorf("%v error parsing ticker data 'high' %v", l.Name, tickerData) - continue - } - low, err := strconv.ParseFloat(tickerData[lowString].(string), 64) - if err != nil { - l.Websocket.DataHandler <- fmt.Errorf("%v error parsing ticker data 'low' %v", l.Name, tickerData) - continue - } - vol, err := strconv.ParseFloat(tickerData[volumeString].(string), 64) - if err != nil { - l.Websocket.DataHandler <- fmt.Errorf("%v error parsing ticker data 'volume' %v", l.Name, tickerData) - continue + processTickerItem := func(tick map[string]interface{}, item string) float64 { + if tick[item] == nil { + return 0 + } + + p, err := strconv.ParseFloat(tick[item].(string), 64) + if err != nil { + l.Websocket.DataHandler <- fmt.Errorf("%s error parsing ticker data '%s' %v", + l.Name, + item, + tickerData) + return 0 + } + + return p } + l.Websocket.DataHandler <- wshandler.TickerData{ - Timestamp: time.Now(), - Pair: currency.NewPairFromString(k), - AssetType: orderbook.Spot, - Exchange: l.GetName(), - Quantity: vol, - HighPrice: high, - LowPrice: low, + Exchange: l.Name, + Bid: processTickerItem(tickerData, order.Buy.Lower()), + High: processTickerItem(tickerData, tickerHighString), + Last: processTickerItem(tickerData, tickerLastString), + Low: processTickerItem(tickerData, tickerLowString), + Ask: processTickerItem(tickerData, order.Sell.Lower()), + Volume: processTickerItem(tickerData, tickerVolumeString), + AssetType: asset.Spot, + Pair: returnCurrency, } } return nil diff --git a/exchanges/lakebtc/lakebtc_wrapper.go b/exchanges/lakebtc/lakebtc_wrapper.go index b0e735b3..0e15e043 100644 --- a/exchanges/lakebtc/lakebtc_wrapper.go +++ b/exchanges/lakebtc/lakebtc_wrapper.go @@ -9,14 +9,151 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (l *LakeBTC) GetDefaultConfig() (*config.ExchangeConfig, error) { + l.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = l.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = l.BaseCurrencies + + err := l.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if l.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = l.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets LakeBTC defaults +func (l *LakeBTC) SetDefaults() { + l.Name = "LakeBTC" + l.Enabled = true + l.Verbose = true + l.API.CredentialsValidator.RequiresKey = true + l.API.CredentialsValidator.RequiresSecret = true + + l.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + } + + l.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + UserTradeHistory: true, + CryptoWithdrawal: true, + TradeFee: true, + CryptoDepositFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TradeFetching: true, + OrderbookFetching: true, + Subscribe: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.WithdrawFiatViaWebsiteOnly, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + l.Requester = request.New(l.Name, + request.NewRateLimit(time.Second, lakeBTCAuthRate), + request.NewRateLimit(time.Second, lakeBTCUnauth), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + l.API.Endpoints.URLDefault = lakeBTCAPIURL + l.API.Endpoints.URL = l.API.Endpoints.URLDefault + l.Websocket = wshandler.New() + l.API.Endpoints.WebsocketURL = lakeBTCWSURL + l.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + l.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + l.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup sets exchange configuration profile +func (l *LakeBTC) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + l.SetEnabled(false) + return nil + } + + err := l.SetupDefaults(exch) + if err != nil { + return err + } + + err = l.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: lakeBTCWSURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: l.WsConnect, + Subscriber: l.Subscribe, + Features: &l.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + l.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + false, + false, + false, + false, + exch.Name) + return nil +} + // Start starts the LakeBTC go routine func (l *LakeBTC) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -29,65 +166,88 @@ func (l *LakeBTC) Start(wg *sync.WaitGroup) { // Run implements the LakeBTC wrapper func (l *LakeBTC) Run() { if l.Verbose { - log.Debugf("%s polling delay: %ds.\n", l.GetName(), l.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", l.GetName(), len(l.EnabledPairs), l.EnabledPairs) + l.PrintEnabledPairs() } - exchangeProducts, err := l.GetTradablePairs() - if err != nil { - log.Errorf("%s Failed to get available products.\n", l.GetName()) - } else { - var newExchangeProducts currency.Pairs - for _, p := range exchangeProducts { - newExchangeProducts = append(newExchangeProducts, - currency.NewPairFromString(p)) - } + if !l.GetEnabledFeatures().AutoPairUpdates { + return + } - err = l.UpdateCurrencies(newExchangeProducts, false, false) - if err != nil { - log.Errorf("%s Failed to update available currencies.\n", l.GetName()) - } + err := l.UpdateTradablePairs(false) + if err != nil { + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", l.Name, err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (l *LakeBTC) FetchTradablePairs(asset asset.Item) ([]string, error) { + result, err := l.GetTicker() + if err != nil { + return nil, err + } + + var currencies []string + for x := range result { + currencies = append(currencies, strings.ToUpper(x)) + } + + return currencies, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (l *LakeBTC) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := l.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return l.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (l *LakeBTC) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { - tick, err := l.GetTicker() +func (l *LakeBTC) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + ticks, err := l.GetTicker() if err != nil { return ticker.Price{}, err } - for _, x := range l.GetEnabledCurrencies() { - currency := exchange.FormatExchangeCurrency(l.Name, x).String() - var tickerPrice ticker.Price - tickerPrice.Pair = x - tickerPrice.Ask = tick[currency].Ask - tickerPrice.Bid = tick[currency].Bid - tickerPrice.Volume = tick[currency].Volume - tickerPrice.High = tick[currency].High - tickerPrice.Low = tick[currency].Low - tickerPrice.Last = tick[currency].Last + pairs := l.GetEnabledPairs(assetType) + for i := range pairs { + c, ok := ticks[l.FormatExchangeCurrency(pairs[i], assetType).String()] + if !ok { + continue + } - err = ticker.ProcessTicker(l.GetName(), &tickerPrice, assetType) + var tickerPrice ticker.Price + tickerPrice.Pair = pairs[i] + tickerPrice.Ask = c.Ask + tickerPrice.Bid = c.Bid + tickerPrice.Volume = c.Volume + tickerPrice.High = c.High + tickerPrice.Low = c.Low + tickerPrice.Last = c.Last + + err = ticker.ProcessTicker(l.Name, &tickerPrice, assetType) if err != nil { - return tickerPrice, err + log.Error(log.Ticker, err) } } return ticker.GetTicker(l.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (l *LakeBTC) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(l.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (l *LakeBTC) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(l.Name, p, assetType) if err != nil { return l.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (l *LakeBTC) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(l.GetName(), p, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (l *LakeBTC) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(l.Name, p, assetType) if err != nil { return l.UpdateOrderbook(p, assetType) } @@ -95,7 +255,7 @@ func (l *LakeBTC) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.B } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (l *LakeBTC) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (l *LakeBTC) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base orderbookNew, err := l.GetOrderBook(p.String()) if err != nil { @@ -111,7 +271,7 @@ func (l *LakeBTC) UpdateOrderbook(p currency.Pair, assetType string) (orderbook. } orderBook.Pair = p - orderBook.ExchangeName = l.GetName() + orderBook.ExchangeName = l.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -126,7 +286,7 @@ func (l *LakeBTC) UpdateOrderbook(p currency.Pair, assetType string) (orderbook. // LakeBTC exchange func (l *LakeBTC) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = l.GetName() + response.Exchange = l.Name accountInfo, err := l.GetAccountInformation() if err != nil { return response, err @@ -156,42 +316,46 @@ func (l *LakeBTC) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (l *LakeBTC) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (l *LakeBTC) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (l *LakeBTC) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (l *LakeBTC) SubmitOrder(p currency.Pair, side exchange.OrderSide, _ exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - isBuyOrder := side == exchange.BuyOrderSide - response, err := l.Trade(isBuyOrder, amount, price, p.Lower().String()) +func (l *LakeBTC) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } + isBuyOrder := s.OrderSide == order.Buy + response, err := l.Trade(isBuyOrder, s.Amount, s.Price, + s.Pair.Lower().String()) + if err != nil { + return submitOrderResponse, err + } if response.ID > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response.ID) + submitOrderResponse.OrderID = strconv.FormatInt(response.ID, 10) } - if err == nil { - submitOrderResponse.IsOrderPlaced = true + submitOrderResponse.IsOrderPlaced = true + if s.OrderType == order.Market { + submitOrderResponse.FullyMatched = true } - - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (l *LakeBTC) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (l *LakeBTC) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (l *LakeBTC) CancelOrder(order *exchange.OrderCancellation) error { +func (l *LakeBTC) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) if err != nil { @@ -202,24 +366,24 @@ func (l *LakeBTC) CancelOrder(order *exchange.OrderCancellation) error { } // CancelAllOrders cancels all orders associated with a currency pair -func (l *LakeBTC) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - var cancelAllOrdersResponse exchange.CancelAllOrdersResponse +func (l *LakeBTC) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + var cancelAllOrdersResponse order.CancelAllResponse openOrders, err := l.GetOpenOrders() if err != nil { return cancelAllOrdersResponse, err } var ordersToCancel []string - for _, order := range openOrders { - ordersToCancel = append(ordersToCancel, strconv.FormatInt(order.ID, 10)) + for i := range openOrders { + ordersToCancel = append(ordersToCancel, strconv.FormatInt(openOrders[i].ID, 10)) } return cancelAllOrdersResponse, l.CancelExistingOrders(ordersToCancel) } // GetOrderInfo returns information on a current open order -func (l *LakeBTC) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (l *LakeBTC) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -240,7 +404,7 @@ func (l *LakeBTC) GetDepositAddress(cryptocurrency currency.Code, _ string) (str // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (l *LakeBTC) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (l *LakeBTC) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { if withdrawRequest.Currency != currency.BTC { return "", errors.New("only BTC supported for withdrawals") } @@ -250,18 +414,18 @@ func (l *LakeBTC) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.Withdraw return "", err } - return fmt.Sprintf("%v", resp.ID), nil + return strconv.FormatInt(resp.ID, 10), nil } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (l *LakeBTC) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (l *LakeBTC) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (l *LakeBTC) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (l *LakeBTC) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -272,7 +436,7 @@ func (l *LakeBTC) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (l *LakeBTC) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (l.APIKey == "" || l.APISecret == "") && // Todo check connection status + if !l.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -280,22 +444,23 @@ func (l *LakeBTC) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) } // GetActiveOrders retrieves any orders that are active/open -func (l *LakeBTC) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (l *LakeBTC) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := l.GetOpenOrders() if err != nil { return nil, err } - var orders []exchange.OrderDetail - for _, order := range resp { - symbol := currency.NewPairDelimiter(order.Symbol, l.ConfigCurrencyPairFormat.Delimiter) - orderDate := time.Unix(order.At, 0) - side := exchange.OrderSide(strings.ToUpper(order.Type)) + var orders []order.Detail + for i := range resp { + symbol := currency.NewPairDelimiter(resp[i].Symbol, + l.GetPairFormat(asset.Spot, false).Delimiter) + orderDate := time.Unix(resp[i].At, 0) + side := order.Side(strings.ToUpper(resp[i].Type)) - orders = append(orders, exchange.OrderDetail{ - Amount: order.Amount, - ID: fmt.Sprintf("%v", order.ID), - Price: order.Price, + orders = append(orders, order.Detail{ + Amount: resp[i].Amount, + ID: strconv.FormatInt(resp[i].ID, 10), + Price: resp[i].Price, OrderSide: side, OrderDate: orderDate, CurrencyPair: symbol, @@ -303,36 +468,36 @@ func (l *LakeBTC) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ( }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (l *LakeBTC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (l *LakeBTC) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := l.GetOrders([]int64{}) if err != nil { return nil, err } - var orders []exchange.OrderDetail - for _, order := range resp { - if order.State == "active" { + var orders []order.Detail + for i := range resp { + if resp[i].State == "active" { continue } - symbol := currency.NewPairDelimiter(order.Symbol, - l.ConfigCurrencyPairFormat.Delimiter) - orderDate := time.Unix(order.At, 0) - side := exchange.OrderSide(strings.ToUpper(order.Type)) + symbol := currency.NewPairDelimiter(resp[i].Symbol, + l.GetPairFormat(asset.Spot, false).Delimiter) + orderDate := time.Unix(resp[i].At, 0) + side := order.Side(strings.ToUpper(resp[i].Type)) - orders = append(orders, exchange.OrderDetail{ - Amount: order.Amount, - ID: fmt.Sprintf("%v", order.ID), - Price: order.Price, + orders = append(orders, order.Detail{ + Amount: resp[i].Amount, + ID: strconv.FormatInt(resp[i].ID, 10), + Price: resp[i].Price, OrderSide: side, OrderDate: orderDate, CurrencyPair: symbol, @@ -340,10 +505,9 @@ func (l *LakeBTC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) + order.FilterOrdersByCurrencies(&orders, req.Currencies) return orders, nil } diff --git a/exchanges/lbank/README.md b/exchanges/lbank/README.md index 8e025ed5..df025828 100644 --- a/exchanges/lbank/README.md +++ b/exchanges/lbank/README.md @@ -47,22 +47,22 @@ main.go ```go var l exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Lbank" { - l = bot.exchanges[i] +for i := range Bot.Exchanges { + if Bot.Exchanges[i].GetName() == "Lbank" { + l = Bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := l.GetTickerPrice() +tick, err := l.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := l.GetOrderbookEx() +ob, err := l.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +130,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/lbank/lbank.go b/exchanges/lbank/lbank.go index 9750135a..f7f7322b 100644 --- a/exchanges/lbank/lbank.go +++ b/exchanges/lbank/lbank.go @@ -14,15 +14,11 @@ import ( "net/url" "strconv" "strings" - "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + gctcrypto "github.com/thrasher-corp/gocryptotrader/common/crypto" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) // Lbank is the overarching type across this package @@ -63,84 +59,28 @@ const ( lbankRevokeWithdraw = "withdrawCancel.do" ) -// SetDefaults sets the basic defaults for Lbank -func (l *Lbank) SetDefaults() { - l.Name = "Lbank" - l.RESTPollingDelay = 10 - l.RequestCurrencyPairFormat.Delimiter = "_" - l.ConfigCurrencyPairFormat.Delimiter = "_" - l.AssetTypes = []string{ticker.Spot} - l.SupportsAutoPairUpdating = true - l.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithAPIPermission | exchange.NoFiatWithdrawals - l.Requester = request.New(l.Name, - request.NewRateLimit(time.Second, lbankAuthRateLimit), - request.NewRateLimit(time.Second, lbankUnAuthRateLimit), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - l.APIUrlDefault = lbankAPIURL - l.APIUrl = l.APIUrlDefault - l.Websocket = wshandler.New() -} - -// Setup takes in the supplied exchange configuration details and sets params -func (l *Lbank) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - l.SetEnabled(false) - } else { - l.Enabled = true - l.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - l.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - l.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - l.SetHTTPClientTimeout(exch.HTTPTimeout) - l.SetHTTPClientUserAgent(exch.HTTPUserAgent) - l.RESTPollingDelay = exch.RESTPollingDelay - l.Verbose = exch.Verbose - l.Websocket.SetWsStatusAndConnection(exch.Websocket) - l.BaseCurrencies = exch.BaseCurrencies - l.AvailablePairs = exch.AvailablePairs - l.EnabledPairs = exch.EnabledPairs - err := l.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = l.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = l.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = l.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = l.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - if l.AuthenticatedAPISupport { - err = l.loadPrivKey() - if err != nil { - l.AuthenticatedAPISupport = false - log.Errorf("couldnt load private key, setting authenticated support to false") - } - } - } -} - // GetTicker returns a ticker for the specified symbol // symbol: eth_btc func (l *Lbank) GetTicker(symbol string) (TickerResponse, error) { var t TickerResponse params := url.Values{} params.Set("symbol", symbol) - path := fmt.Sprintf("%s/v%s/%s?%s", l.APIUrl, lbankAPIVersion, lbankTicker, params.Encode()) + path := fmt.Sprintf("%s/v%s/%s?%s", l.API.Endpoints.URL, lbankAPIVersion, lbankTicker, params.Encode()) + return t, l.SendHTTPRequest(path, &t) +} + +// GetTickers returns all tickers +func (l *Lbank) GetTickers() ([]TickerResponse, error) { + var t []TickerResponse + params := url.Values{} + params.Set("symbol", "all") + path := fmt.Sprintf("%s/v%s/%s?%s", l.API.Endpoints.URL, lbankAPIVersion, lbankTicker, params.Encode()) return t, l.SendHTTPRequest(path, &t) } // GetCurrencyPairs returns a list of supported currency pairs by the exchange func (l *Lbank) GetCurrencyPairs() ([]string, error) { - path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, + path := fmt.Sprintf("%s/v%s/%s", l.API.Endpoints.URL, lbankAPIVersion, lbankCurrencyPairs) var result []string return result, l.SendHTTPRequest(path, &result) @@ -153,7 +93,7 @@ func (l *Lbank) GetMarketDepths(symbol, size, merge string) (MarketDepthResponse params.Set("symbol", symbol) params.Set("size", size) params.Set("merge", merge) - path := fmt.Sprintf("%s/v%s/%s?%s", l.APIUrl, lbankAPIVersion, lbankMarketDepths, params.Encode()) + path := fmt.Sprintf("%s/v%s/%s?%s", l.API.Endpoints.URL, lbankAPIVersion, lbankMarketDepths, params.Encode()) return m, l.SendHTTPRequest(path, &m) } @@ -164,7 +104,7 @@ func (l *Lbank) GetTrades(symbol, size, time string) ([]TradeResponse, error) { params.Set("symbol", symbol) params.Set("size", size) params.Set("time", time) - path := fmt.Sprintf("%s/v%s/%s?%s", l.APIUrl, lbankAPIVersion, lbankTrades, params.Encode()) + path := fmt.Sprintf("%s/v%s/%s?%s", l.API.Endpoints.URL, lbankAPIVersion, lbankTrades, params.Encode()) return g, l.SendHTTPRequest(path, &g) } @@ -177,7 +117,7 @@ func (l *Lbank) GetKlines(symbol, size, klineType, time string) ([]KlineResponse params.Set("size", size) params.Set("type", klineType) params.Set("time", time) - path := fmt.Sprintf("%s/v%s/%s?%s", l.APIUrl, lbankAPIVersion, lbankKlines, params.Encode()) + path := fmt.Sprintf("%s/v%s/%s?%s", l.API.Endpoints.URL, lbankAPIVersion, lbankKlines, params.Encode()) err := l.SendHTTPRequest(path, &klineTemp) if err != nil { return k, err @@ -240,7 +180,7 @@ func (l *Lbank) GetKlines(symbol, size, klineType, time string) ([]KlineResponse // GetUserInfo gets users account info func (l *Lbank) GetUserInfo() (InfoFinalResponse, error) { var resp InfoFinalResponse - path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankUserInfo) + path := fmt.Sprintf("%s/v%s/%s?", l.API.Endpoints.URL, lbankAPIVersion, lbankUserInfo) err := l.SendAuthHTTPRequest(http.MethodPost, path, nil, &resp) if err != nil { return resp, err @@ -256,7 +196,8 @@ func (l *Lbank) GetUserInfo() (InfoFinalResponse, error) { // CreateOrder creates an order func (l *Lbank) CreateOrder(pair, side string, amount, price float64) (CreateOrderResponse, error) { var resp CreateOrderResponse - if !strings.EqualFold(side, "buy") && !strings.EqualFold(side, "sell") { + if !strings.EqualFold(side, order.Buy.String()) && + !strings.EqualFold(side, order.Sell.String()) { return resp, errors.New("side type invalid can only be 'buy' or 'sell'") } if amount <= 0 { @@ -268,10 +209,10 @@ func (l *Lbank) CreateOrder(pair, side string, amount, price float64) (CreateOrd params := url.Values{} params.Set("symbol", pair) - params.Set("type", common.StringToLower(side)) + params.Set("type", strings.ToLower(side)) params.Set("price", strconv.FormatFloat(price, 'f', -1, 64)) params.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankPlaceOrder) + path := fmt.Sprintf("%s/v%s/%s?", l.API.Endpoints.URL, lbankAPIVersion, lbankPlaceOrder) err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) if err != nil { return resp, err @@ -290,7 +231,7 @@ func (l *Lbank) RemoveOrder(pair, orderID string) (RemoveOrderResponse, error) { params := url.Values{} params.Set("symbol", pair) params.Set("order_id", orderID) - path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, lbankCancelOrder) + path := fmt.Sprintf("%s/v%s/%s", l.API.Endpoints.URL, lbankAPIVersion, lbankCancelOrder) err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) if err != nil { return resp, err @@ -311,7 +252,7 @@ func (l *Lbank) QueryOrder(pair, orderIDs string) (QueryOrderFinalResponse, erro params := url.Values{} params.Set("symbol", pair) params.Set("order_id", orderIDs) - path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankQueryOrder) + path := fmt.Sprintf("%s/v%s/%s?", l.API.Endpoints.URL, lbankAPIVersion, lbankQueryOrder) err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &tempResp) if err != nil { return resp, err @@ -347,7 +288,7 @@ func (l *Lbank) QueryOrderHistory(pair, pageNumber, pageLength string) (OrderHis params.Set("symbol", pair) params.Set("current_page", pageNumber) params.Set("page_length", pageLength) - path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankQueryHistoryOrder) + path := fmt.Sprintf("%s/v%s/%s?", l.API.Endpoints.URL, lbankAPIVersion, lbankQueryHistoryOrder) err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &tempResp) if err != nil { return resp, err @@ -375,7 +316,7 @@ func (l *Lbank) QueryOrderHistory(pair, pageNumber, pageLength string) (OrderHis // GetPairInfo finds information about all trading pairs func (l *Lbank) GetPairInfo() ([]PairInfoResponse, error) { var resp []PairInfoResponse - path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankPairInfo) + path := fmt.Sprintf("%s/v%s/%s?", l.API.Endpoints.URL, lbankAPIVersion, lbankPairInfo) return resp, l.SendHTTPRequest(path, &resp) } @@ -385,7 +326,7 @@ func (l *Lbank) OrderTransactionDetails(symbol, orderID string) (TransactionHist params := url.Values{} params.Set("symbol", symbol) params.Set("order_id", orderID) - path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankOrderTransactionDetails) + path := fmt.Sprintf("%s/v%s/%s?", l.API.Endpoints.URL, lbankAPIVersion, lbankOrderTransactionDetails) err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) if err != nil { return resp, err @@ -409,7 +350,7 @@ func (l *Lbank) TransactionHistory(symbol, transactionType, startDate, endDate, params.Set("from", from) params.Set("direct", direct) params.Set("size", size) - path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankPastTransactions) + path := fmt.Sprintf("%s/v%s/%s?", l.API.Endpoints.URL, lbankAPIVersion, lbankPastTransactions) err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) if err != nil { return resp, err @@ -431,7 +372,7 @@ func (l *Lbank) GetOpenOrders(pair, pageNumber, pageLength string) (OpenOrderFin params.Set("symbol", pair) params.Set("current_page", pageNumber) params.Set("page_length", pageLength) - path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, lbankOpeningOrders) + path := fmt.Sprintf("%s/v%s/%s", l.API.Endpoints.URL, lbankAPIVersion, lbankOpeningOrders) err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &tempResp) if err != nil { return resp, err @@ -459,7 +400,7 @@ func (l *Lbank) GetOpenOrders(pair, pageNumber, pageLength string) (OpenOrderFin // USD2RMBRate finds USD-CNY Rate func (l *Lbank) USD2RMBRate() (ExchangeRateResponse, error) { var resp ExchangeRateResponse - path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, lbankUSD2CNYRate) + path := fmt.Sprintf("%s/v%s/%s", l.API.Endpoints.URL, lbankAPIVersion, lbankUSD2CNYRate) return resp, l.SendHTTPRequest(path, &resp) } @@ -468,7 +409,7 @@ func (l *Lbank) GetWithdrawConfig(assetCode string) ([]WithdrawConfigResponse, e var resp []WithdrawConfigResponse params := url.Values{} params.Set("assetCode", assetCode) - path := fmt.Sprintf("%s/v%s/%s?%s", l.APIUrl, lbankAPIVersion, lbankWithdrawConfig, params.Encode()) + path := fmt.Sprintf("%s/v%s/%s?%s", l.API.Endpoints.URL, lbankAPIVersion, lbankWithdrawConfig, params.Encode()) return resp, l.SendHTTPRequest(path, &resp) } @@ -488,7 +429,8 @@ func (l *Lbank) Withdraw(account, assetCode, amount, memo, mark, withdrawType st if withdrawType != "" { params.Set("type", withdrawType) } - path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, lbankWithdraw) + path := fmt.Sprintf("%s/v%s/%s", l.API.Endpoints.URL, lbankAPIVersion, + lbankWithdraw) err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) if err != nil { return resp, err @@ -508,7 +450,7 @@ func (l *Lbank) RevokeWithdraw(withdrawID string) (RevokeWithdrawResponse, error if withdrawID != "" { params.Set("withdrawId", withdrawID) } - path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankRevokeWithdraw) + path := fmt.Sprintf("%s/v%s/%s?", l.API.Endpoints.URL, lbankAPIVersion, lbankRevokeWithdraw) err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) if err != nil { return resp, err @@ -529,7 +471,7 @@ func (l *Lbank) GetWithdrawalRecords(assetCode, status, pageNo, pageSize string) params.Set("status", status) params.Set("pageNo", pageNo) params.Set("pageSize", pageSize) - path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, lbankWithdrawalRecords) + path := fmt.Sprintf("%s/v%s/%s", l.API.Endpoints.URL, lbankAPIVersion, lbankWithdrawalRecords) err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) if err != nil { return resp, err @@ -568,7 +510,7 @@ func (l *Lbank) SendHTTPRequest(path string, result interface{}) error { func (l *Lbank) loadPrivKey() error { key := strings.Join([]string{ "-----BEGIN RSA PRIVATE KEY-----", - l.APISecret, + l.API.Credentials.Secret, "-----END RSA PRIVATE KEY-----", }, "\n") @@ -594,23 +536,27 @@ func (l *Lbank) sign(data string) (string, error) { if l.privateKey == nil { return "", errors.New("private key not loaded") } - md5hash := common.GetMD5([]byte(data)) - m := common.StringToUpper(common.HexEncodeToString(md5hash)) - s := common.GetSHA256([]byte(m)) + md5hash := gctcrypto.GetMD5([]byte(data)) + m := strings.ToUpper(gctcrypto.HexEncodeToString(md5hash)) + s := gctcrypto.GetSHA256([]byte(m)) r, err := rsa.SignPKCS1v15(rand.Reader, l.privateKey, crypto.SHA256, s) if err != nil { return "", err } - return common.Base64Encode(r), nil + return gctcrypto.Base64Encode(r), nil } // SendAuthHTTPRequest sends an authenticated request func (l *Lbank) SendAuthHTTPRequest(method, endpoint string, vals url.Values, result interface{}) error { + if !l.AllowAuthenticatedRequest() { + return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, l.Name) + } + if vals == nil { vals = url.Values{} } - vals.Set("api_key", l.APIKey) + vals.Set("api_key", l.API.Credentials.Key) sig, err := l.sign(vals.Encode()) if err != nil { return err diff --git a/exchanges/lbank/lbank_test.go b/exchanges/lbank/lbank_test.go index 0f6afa01..4aa2c4a7 100644 --- a/exchanges/lbank/lbank_test.go +++ b/exchanges/lbank/lbank_test.go @@ -1,15 +1,17 @@ package lbank import ( - "fmt" "log" "os" + "strconv" "testing" "time" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // Please supply your own keys here for due diligence testing @@ -17,6 +19,7 @@ const ( testAPIKey = "" testAPISecret = "" canManipulateRealOrders = false + testCurrencyPair = "btc_usdt" ) var l Lbank @@ -24,7 +27,7 @@ var l Lbank func TestMain(m *testing.M) { l.SetDefaults() cfg := config.GetConfig() - err := cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) if err != nil { log.Fatal(err) } @@ -32,30 +35,39 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal(err) } - lbankConfig.Websocket = true - lbankConfig.AuthenticatedAPISupport = true - lbankConfig.APISecret = testAPISecret - lbankConfig.APIKey = testAPIKey - l.Setup(&lbankConfig) - + lbankConfig.API.AuthenticatedSupport = true + lbankConfig.API.Credentials.Key = testAPIKey + lbankConfig.API.Credentials.Secret = testAPISecret + err = l.Setup(lbankConfig) + if err != nil { + log.Fatal(err) + } os.Exit(m.Run()) } + func areTestAPIKeysSet() bool { - if l.APIKey != "" && l.APIKey != "Key" && - l.APISecret != "" && l.APISecret != "Secret" { - return true - } - return false + return l.AllowAuthenticatedRequest() } func TestGetTicker(t *testing.T) { t.Parallel() - _, err := l.GetTicker("btc_usdt") + _, err := l.GetTicker(testCurrencyPair) if err != nil { t.Error(err) } } +func TestGetTickers(t *testing.T) { + t.Parallel() + tickers, err := l.GetTickers() + if err != nil { + t.Fatal(err) + } + if len(tickers) <= 1 { + t.Errorf("expected multiple tickers, received %v", len(tickers)) + } +} + func TestGetCurrencyPairs(t *testing.T) { t.Parallel() _, err := l.GetCurrencyPairs() @@ -66,23 +78,24 @@ func TestGetCurrencyPairs(t *testing.T) { func TestGetMarketDepths(t *testing.T) { t.Parallel() - _, err := l.GetMarketDepths("btc_usdt", "600", "1") + _, err := l.GetMarketDepths(testCurrencyPair, "600", "1") if err != nil { - t.Errorf("GetMarketDepth failed: %v", err) + t.Fatal(err) } - a, _ := l.GetMarketDepths("btc_usdt", "4", "0") + a, _ := l.GetMarketDepths(testCurrencyPair, "4", "0") if len(a.Asks) != 4 { - t.Errorf("length requested doesnt match the output") + t.Errorf("asks length requested doesnt match the output") } } func TestGetTrades(t *testing.T) { t.Parallel() - _, err := l.GetTrades("btc_usdt", "600", fmt.Sprintf("%v", time.Now().Unix())) + _, err := l.GetTrades(testCurrencyPair, "600", + strconv.FormatInt(time.Now().Unix(), 10)) if err != nil { t.Error(err) } - a, err := l.GetTrades("btc_usdt", "600", "0") + a, err := l.GetTrades(testCurrencyPair, "600", "0") if len(a) != 600 && err != nil { t.Error(err) } @@ -90,7 +103,8 @@ func TestGetTrades(t *testing.T) { func TestGetKlines(t *testing.T) { t.Parallel() - _, err := l.GetKlines("btc_usdt", "600", "minute1", fmt.Sprintf("%v", time.Now().Unix())) + _, err := l.GetKlines(testCurrencyPair, "600", "minute1", + strconv.FormatInt(time.Now().Unix(), 10)) if err != nil { t.Error(err) } @@ -103,9 +117,9 @@ func TestUpdateOrderbook(t *testing.T) { Base: currency.ETH, Quote: currency.BTC} - _, err := l.UpdateOrderbook(p.Lower(), "spot") + _, err := l.UpdateOrderbook(p.Lower(), asset.Spot) if err != nil { - t.Errorf("Update for orderbook failed: %v", err) + t.Error(err) } } @@ -116,7 +130,7 @@ func TestGetUserInfo(t *testing.T) { } _, err := l.GetUserInfo() if err != nil { - t.Errorf("invalid key or sign: %v", err) + t.Error(err) } } @@ -130,15 +144,15 @@ func TestCreateOrder(t *testing.T) { if err == nil { t.Error("CreateOrder error cannot be nil") } - _, err = l.CreateOrder(cp.Lower().String(), "buy", 0, 0) + _, err = l.CreateOrder(cp.Lower().String(), order.Buy.Lower(), 0, 0) if err == nil { t.Error("CreateOrder error cannot be nil") } - _, err = l.CreateOrder(cp.Lower().String(), "sell", 1231, 0) + _, err = l.CreateOrder(cp.Lower().String(), order.Sell.Lower(), 1231, 0) if err == nil { t.Error("CreateOrder error cannot be nil") } - _, err = l.CreateOrder(cp.Lower().String(), "buy", 58, 681) + _, err = l.CreateOrder(cp.Lower().String(), order.Buy.Lower(), 58, 681) if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -152,7 +166,7 @@ func TestRemoveOrder(t *testing.T) { cp := currency.NewPairWithDelimiter(currency.ETH.String(), currency.BTC.String(), "_") _, err := l.RemoveOrder(cp.Lower().String(), "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23") if err != nil { - t.Errorf("unable to remove order: %v", err) + t.Error(err) } } @@ -164,7 +178,7 @@ func TestQueryOrder(t *testing.T) { cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") _, err := l.QueryOrder(cp.Lower().String(), "1") if err != nil { - t.Errorf("unexpected error: %v", err) + t.Error(err) } } @@ -184,7 +198,7 @@ func TestGetPairInfo(t *testing.T) { t.Parallel() _, err := l.GetPairInfo() if err != nil { - t.Errorf("couldnt get pair info: %v", err) + t.Error(err) } } @@ -193,9 +207,9 @@ func TestOrderTransactionDetails(t *testing.T) { if !areTestAPIKeysSet() { t.Skip("API keys required but not set, skipping test") } - _, err := l.OrderTransactionDetails("eth_btc", "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23") + _, err := l.OrderTransactionDetails(testCurrencyPair, "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23") if err != nil { - t.Errorf("couldnt get transaction details: %v", err) + t.Error(err) } } @@ -204,9 +218,9 @@ func TestTransactionHistory(t *testing.T) { if !areTestAPIKeysSet() { t.Skip("API keys required but not set, skipping test") } - _, err := l.TransactionHistory("btc_usdt", "", "", "", "", "", "") + _, err := l.TransactionHistory(testCurrencyPair, "", "", "", "", "", "") if err != nil { - t.Errorf("couldnt get transaction history: %v", err) + t.Error(err) } } @@ -218,7 +232,7 @@ func TestGetOpenOrders(t *testing.T) { cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") _, err := l.GetOpenOrders(cp.Lower().String(), "1", "50") if err != nil { - t.Error("unexpected error", err) + t.Error(err) } } @@ -226,15 +240,15 @@ func TestUSD2RMBRate(t *testing.T) { t.Parallel() _, err := l.USD2RMBRate() if err != nil { - t.Error("unable to acquire the rate") + t.Error(err) } } func TestGetWithdrawConfig(t *testing.T) { t.Parallel() - _, err := l.GetWithdrawConfig("eth") + _, err := l.GetWithdrawConfig(currency.ETH.Lower().String()) if err != nil { - t.Errorf("unable to get withdraw config: %v", err) + t.Error(err) } } @@ -245,7 +259,7 @@ func TestWithdraw(t *testing.T) { } _, err := l.Withdraw("", "", "", "", "", "") if err != nil { - t.Errorf("unable to withdraw: %v", err) + t.Error(err) } } @@ -254,9 +268,10 @@ func TestGetWithdrawRecords(t *testing.T) { if !areTestAPIKeysSet() { t.Skip("API keys required but not set, skipping test") } - _, err := l.GetWithdrawalRecords("eth", "0", "1", "20") + _, err := l.GetWithdrawalRecords(currency.ETH.Lower().String(), + "0", "1", "20") if err != nil { - t.Errorf("unable to get withdrawal records: %v", err) + t.Error(err) } } @@ -269,10 +284,10 @@ func TestLoadPrivKey(t *testing.T) { if err != nil { t.Error(err) } - l.APISecret = "errortest" + l.API.Credentials.Secret = "errortest" err = l.loadPrivKey() if err == nil { - t.Errorf("expected error due to pemblock nil, got err: %v", err) + t.Errorf("Expected error due to pemblock nil") } } @@ -281,7 +296,7 @@ func TestSign(t *testing.T) { if !areTestAPIKeysSet() { t.Skip("API keys required but not set, skipping test") } - l.APISecret = testAPISecret + l.API.Credentials.Secret = testAPISecret l.loadPrivKey() _, err := l.sign("hello123") if err != nil { @@ -291,13 +306,27 @@ func TestSign(t *testing.T) { func TestSubmitOrder(t *testing.T) { t.Parallel() - if !areTestAPIKeysSet() { - t.Skip("API keys required but not set, skipping test") + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") - _, err := l.SubmitOrder(cp.Lower(), "BUY", "ANY", 2, 1312, "") - if err != nil { - t.Error(err) + + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.USDT, + Delimiter: "_", + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", + } + response, err := l.SubmitOrder(orderSubmission) + if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { + t.Errorf("Order failed to be placed: %v", err) + } else if !areTestAPIKeysSet() && err == nil { + t.Error("Expecting an error when no keys are set") } } @@ -307,7 +336,7 @@ func TestCancelOrder(t *testing.T) { t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") } cp := currency.NewPairWithDelimiter(currency.ETH.String(), currency.BTC.String(), "_") - var a exchange.OrderCancellation + var a order.Cancel a.CurrencyPair = cp a.OrderID = "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23" err := l.CancelOrder(&a) @@ -347,10 +376,10 @@ func TestGetFeeByType(t *testing.T) { input.Pair = cp a, err := l.GetFeeByType(&input) if err != nil { - t.Errorf("couldnt get fee: %v", err) + t.Error(err) } if a != 0.0005 { - t.Errorf("testGetFeeByType failed. Expected: 0.0005, Received: %v", a) + t.Errorf("expected: 0.0005, received: %v", a) } } @@ -370,8 +399,8 @@ func TestGetOrderHistory(t *testing.T) { if !areTestAPIKeysSet() { t.Skip("API keys required but not set, skipping test") } - var input exchange.GetOrdersRequest - input.OrderSide = exchange.BuyOrderSide + var input order.GetOrdersRequest + input.OrderSide = order.Buy _, err := l.GetOrderHistory(&input) if err != nil { t.Error(err) diff --git a/exchanges/lbank/lbank_types.go b/exchanges/lbank/lbank_types.go index cb6653ac..71ce1df2 100644 --- a/exchanges/lbank/lbank_types.go +++ b/exchanges/lbank/lbank_types.go @@ -2,6 +2,8 @@ package lbank import ( "encoding/json" + + "github.com/thrasher-corp/gocryptotrader/currency" ) // Ticker stores the ticker price data for a currency pair @@ -16,9 +18,9 @@ type Ticker struct { // TickerResponse stores the ticker price data and timestamp for a currency pair type TickerResponse struct { - Symbol string `json:"symbol"` - Timestamp int64 `json:"timestamp"` - Ticker Ticker `json:"ticker"` + Symbol currency.Pair `json:"symbol"` + Timestamp int64 `json:"timestamp"` + Ticker Ticker `json:"ticker"` } // MarketDepthResponse stores arrays for asks, bids and a timestamp for a currecy pair diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index 2d1e22d1..d5eab666 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -8,15 +8,125 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) -// Start starts the Lbank go routine +// GetDefaultConfig returns a default exchange config +func (l *Lbank) GetDefaultConfig() (*config.ExchangeConfig, error) { + l.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = l.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = l.BaseCurrencies + + err := l.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if l.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = l.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for Lbank +func (l *Lbank) SetDefaults() { + l.Name = "Lbank" + l.Enabled = true + l.Verbose = true + l.API.CredentialsValidator.RequiresKey = true + l.API.CredentialsValidator.RequiresSecret = true + + l.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "_", + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "_", + }, + } + + l.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + SubmitOrder: true, + WithdrawalHistory: true, + UserTradeHistory: true, + CryptoWithdrawal: true, + TradeFee: true, + CryptoWithdrawalFee: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + l.Requester = request.New(l.Name, + request.NewRateLimit(time.Second, lbankAuthRateLimit), + request.NewRateLimit(time.Second, lbankUnAuthRateLimit), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + l.API.Endpoints.URLDefault = lbankAPIURL + l.API.Endpoints.URL = l.API.Endpoints.URLDefault +} + +// Setup sets exchange configuration profile +func (l *Lbank) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + l.SetEnabled(false) + return nil + } + + err := l.SetupDefaults(exch) + if err != nil { + return err + } + + if l.API.AuthenticatedSupport { + err = l.loadPrivKey() + if err != nil { + l.API.AuthenticatedSupport = false + log.Errorf(log.ExchangeSys, "%s couldn't load private key, setting authenticated support to false", l.Name) + } + } + return nil +} + +// Start starts the LakeBTC go routine func (l *Lbank) Start(wg *sync.WaitGroup) { wg.Add(1) go func() { @@ -28,58 +138,81 @@ func (l *Lbank) Start(wg *sync.WaitGroup) { // Run implements the Lbank wrapper func (l *Lbank) Run() { if l.Verbose { - log.Debugf("%s Websocket: %s. (url: %s).\n", l.Name, common.IsEnabled(l.Websocket.IsEnabled()), l.Websocket.GetWebsocketURL()) - log.Debugf("%s polling delay: %ds.\n", l.Name, l.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", l.Name, len(l.EnabledPairs), l.EnabledPairs) + l.PrintEnabledPairs() } - exchangeCurrencies, err := l.GetCurrencyPairs() + + if !l.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := l.UpdateTradablePairs(false) if err != nil { - log.Errorf("%s Failed to get available symbols.\n", l.Name) - } else { - var newExchangeCurrencies currency.Pairs - for _, p := range exchangeCurrencies { - newExchangeCurrencies = append(newExchangeCurrencies, - currency.NewPairFromString(p)) - } - err = l.UpdateCurrencies(newExchangeCurrencies, false, true) - if err != nil { - log.Errorf("%s Failed to update available currencies %s.\n", l.Name, err) - } + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", l.Name, err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (l *Lbank) FetchTradablePairs(asset asset.Item) ([]string, error) { + currencies, err := l.GetCurrencyPairs() + if err != nil { + return nil, err + } + return currencies, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (l *Lbank) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := l.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return l.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (l *Lbank) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (l *Lbank) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - tickerInfo, err := l.GetTicker(exchange.FormatExchangeCurrency(l.Name, p).String()) + tickerInfo, err := l.GetTickers() if err != nil { return tickerPrice, err } - tickerPrice.Pair = p - tickerPrice.Last = tickerInfo.Ticker.Latest - tickerPrice.High = tickerInfo.Ticker.High - tickerPrice.Volume = tickerInfo.Ticker.Volume - tickerPrice.Low = tickerInfo.Ticker.Low - - err = ticker.ProcessTicker(l.Name, &tickerPrice, assetType) - if err != nil { - return tickerPrice, err + pairs := l.GetEnabledPairs(assetType) + for i := range pairs { + for j := range tickerInfo { + if !pairs[i].Equal(tickerInfo[j].Symbol) { + continue + } + tickerPrice = ticker.Price{ + Last: tickerInfo[j].Ticker.Latest, + High: tickerInfo[j].Ticker.High, + Low: tickerInfo[j].Ticker.Low, + Volume: tickerInfo[j].Ticker.Volume, + Pair: tickerInfo[j].Symbol, + LastUpdated: time.Unix(0, tickerInfo[j].Timestamp), + } + err = ticker.ProcessTicker(l.Name, &tickerPrice, assetType) + if err != nil { + log.Error(log.Ticker, err) + } + } } - return ticker.GetTicker(l.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (l *Lbank) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(l.Name, exchange.FormatExchangeCurrency(l.Name, p), assetType) +// FetchTicker returns the ticker for a currency pair +func (l *Lbank) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(l.Name, + l.FormatExchangeCurrency(p, assetType), assetType) if err != nil { return l.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (l *Lbank) GetOrderbookEx(currency currency.Pair, assetType string) (orderbook.Base, error) { +// FetchOrderbook returns orderbook base on the currency pair +func (l *Lbank) FetchOrderbook(currency currency.Pair, assetType asset.Item) (orderbook.Base, error) { ob, err := orderbook.Get(l.Name, currency, assetType) if err != nil { return l.UpdateOrderbook(currency, assetType) @@ -88,9 +221,9 @@ func (l *Lbank) GetOrderbookEx(currency currency.Pair, assetType string) (orderb } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (l *Lbank) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (l *Lbank) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - a, err := l.GetMarketDepths(exchange.FormatExchangeCurrency(l.Name, p).String(), "60", "1") + a, err := l.GetMarketDepths(l.FormatExchangeCurrency(p, assetType).String(), "60", "1") if err != nil { return orderBook, err } @@ -156,40 +289,54 @@ func (l *Lbank) GetFundingHistory() ([]exchange.FundHistory, error) { } // GetExchangeHistory returns historic trade data since exchange opening. -func (l *Lbank) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { +func (l *Lbank) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { return nil, common.ErrFunctionNotSupported } // SubmitOrder submits a new order -func (l *Lbank) SubmitOrder(p currency.Pair, side exchange.OrderSide, _ exchange.OrderType, amount, price float64, clientID string) (exchange.SubmitOrderResponse, error) { - var resp exchange.SubmitOrderResponse - if side != exchange.BuyOrderSide && side != exchange.SellOrderSide { - return resp, fmt.Errorf("%s orderside is not supported by the exchange", side) +func (l *Lbank) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var resp order.SubmitResponse + if err := s.Validate(); err != nil { + return resp, err } - tempResp, err := l.CreateOrder(exchange.FormatExchangeCurrency(l.Name, p).String(), side.ToString(), amount, price) + + if s.OrderSide != order.Buy && s.OrderSide != order.Sell { + return resp, + fmt.Errorf("%s order side is not supported by the exchange", + s.OrderSide) + } + tempResp, err := l.CreateOrder( + l.FormatExchangeCurrency(s.Pair, asset.Spot).String(), + s.OrderSide.String(), + s.Amount, + s.Price) if err != nil { return resp, err } resp.IsOrderPlaced = true resp.OrderID = tempResp.OrderID + if s.OrderType == order.Market { + resp.FullyMatched = true + } return resp, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (l *Lbank) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (l *Lbank) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (l *Lbank) CancelOrder(order *exchange.OrderCancellation) error { - _, err := l.RemoveOrder(exchange.FormatExchangeCurrency(l.Name, order.CurrencyPair).String(), order.OrderID) +func (l *Lbank) CancelOrder(order *order.Cancel) error { + _, err := l.RemoveOrder(l.FormatExchangeCurrency(order.CurrencyPair, + order.AssetType).String(), order.OrderID) return err } // CancelAllOrders cancels all orders associated with a currency pair -func (l *Lbank) CancelAllOrders(orders *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - var resp exchange.CancelAllOrdersResponse +func (l *Lbank) CancelAllOrders(orders *order.Cancel) (order.CancelAllResponse, error) { + var resp order.CancelAllResponse orderIDs, err := l.getAllOpenOrderID() if err != nil { return resp, nil @@ -214,11 +361,11 @@ func (l *Lbank) CancelAllOrders(orders *exchange.OrderCancellation) (exchange.Ca } tempStringSuccess := strings.Split(CancelResponse.Success, ",") for k := range tempStringSuccess { - resp.OrderStatus[tempStringSuccess[k]] = "Cancelled" + resp.Status[tempStringSuccess[k]] = "Cancelled" } tempStringError := strings.Split(CancelResponse.Err, ",") for l := range tempStringError { - resp.OrderStatus[tempStringError[l]] = "Failed" + resp.Status[tempStringError[l]] = "Failed" } tempSlice = tempSlice[:0] y++ @@ -232,11 +379,11 @@ func (l *Lbank) CancelAllOrders(orders *exchange.OrderCancellation) (exchange.Ca } tempStringSuccess := strings.Split(CancelResponse.Success, ",") for k := range tempStringSuccess { - resp.OrderStatus[tempStringSuccess[k]] = "Cancelled" + resp.Status[tempStringSuccess[k]] = "Cancelled" } tempStringError := strings.Split(CancelResponse.Err, ",") for l := range tempStringError { - resp.OrderStatus[tempStringError[l]] = "Failed" + resp.Status[tempStringError[l]] = "Failed" } tempSlice = tempSlice[:0] } @@ -245,8 +392,8 @@ func (l *Lbank) CancelAllOrders(orders *exchange.OrderCancellation) (exchange.Ca } // GetOrderInfo returns information on a current open order -func (l *Lbank) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var resp exchange.OrderDetail +func (l *Lbank) GetOrderInfo(orderID string) (order.Detail, error) { + var resp order.Detail orderIDs, err := l.getAllOpenOrderID() if err != nil { return resp, err @@ -263,10 +410,10 @@ func (l *Lbank) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { } resp.Exchange = l.Name resp.CurrencyPair = currency.NewPairFromString(key) - if strings.EqualFold(tempResp.Orders[0].Type, "buy") { - resp.OrderSide = exchange.BuyOrderSide + if strings.EqualFold(tempResp.Orders[0].Type, order.Buy.String()) { + resp.OrderSide = order.Buy } else { - resp.OrderSide = exchange.SellOrderSide + resp.OrderSide = order.Sell } z := tempResp.Orders[0].Status switch { @@ -306,7 +453,7 @@ func (l *Lbank) GetDepositAddress(cryptocurrency currency.Code, accountID string // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (l *Lbank) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (l *Lbank) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { resp, err := l.Withdraw(withdrawRequest.Address, withdrawRequest.Currency.String(), strconv.FormatFloat(withdrawRequest.Amount, 'f', -1, 64), "", withdrawRequest.Description, "") @@ -315,13 +462,13 @@ func (l *Lbank) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRe // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is // submitted -func (l *Lbank) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (l *Lbank) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted -func (l *Lbank) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (l *Lbank) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -331,9 +478,9 @@ func (l *Lbank) GetWebsocket() (*wshandler.Websocket, error) { } // GetActiveOrders retrieves any orders that are active/open -func (l *Lbank) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var finalResp []exchange.OrderDetail - var resp exchange.OrderDetail +func (l *Lbank) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { + var finalResp []order.Detail + var resp order.Detail tempData, err := l.getAllOpenOrderID() if err != nil { return finalResp, err @@ -347,10 +494,10 @@ func (l *Lbank) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([] } resp.Exchange = l.Name resp.CurrencyPair = currency.NewPairFromString(key) - if strings.EqualFold(tempResp.Orders[0].Type, "buy") { - resp.OrderSide = exchange.BuyOrderSide + if strings.EqualFold(tempResp.Orders[0].Type, order.Buy.String()) { + resp.OrderSide = order.Buy } else { - resp.OrderSide = exchange.SellOrderSide + resp.OrderSide = order.Sell } z := tempResp.Orders[0].Status switch { @@ -387,7 +534,8 @@ func (l *Lbank) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([] finalResp = append(finalResp, resp) continue } - if strings.EqualFold(getOrdersRequest.OrderSide.ToString(), tempResp.Orders[0].Type) { + if strings.EqualFold(getOrdersRequest.OrderSide.String(), + tempResp.Orders[0].Type) { finalResp = append(finalResp, resp) } } @@ -398,34 +546,34 @@ func (l *Lbank) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([] // GetOrderHistory retrieves account order information * // Can Limit response to specific order status -func (l *Lbank) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var finalResp []exchange.OrderDetail - var resp exchange.OrderDetail +func (l *Lbank) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { + var finalResp []order.Detail + var resp order.Detail var tempCurr currency.Pairs if len(getOrdersRequest.Currencies) == 0 { - tempCurr = l.GetEnabledCurrencies() + tempCurr = l.GetEnabledPairs(asset.Spot) } else { tempCurr = getOrdersRequest.Currencies } for a := range tempCurr { - p := exchange.FormatExchangeCurrency(l.Name, tempCurr[a]) + p := l.FormatExchangeCurrency(tempCurr[a], asset.Spot).String() b := int64(1) - tempResp, err := l.QueryOrderHistory(exchange.FormatExchangeCurrency(l.Name, p).String(), strconv.FormatInt(b, 10), "200") + tempResp, err := l.QueryOrderHistory(p, strconv.FormatInt(b, 10), "200") if err != nil { return finalResp, err } for len(tempResp.Orders) != 0 { - tempResp, err = l.QueryOrderHistory(exchange.FormatExchangeCurrency(l.Name, p).String(), strconv.FormatInt(b, 10), "200") + tempResp, err = l.QueryOrderHistory(p, strconv.FormatInt(b, 10), "200") if err != nil { return finalResp, err } for x := 0; x < len(tempResp.Orders); x++ { resp.Exchange = l.Name resp.CurrencyPair = currency.NewPairFromString(tempResp.Orders[x].Symbol) - if strings.EqualFold(tempResp.Orders[x].Type, "buy") { - resp.OrderSide = exchange.BuyOrderSide + if strings.EqualFold(tempResp.Orders[x].Type, order.Buy.String()) { + resp.OrderSide = order.Buy } else { - resp.OrderSide = exchange.SellOrderSide + resp.OrderSide = order.Sell } z := tempResp.Orders[x].Status switch { @@ -466,7 +614,7 @@ func (l *Lbank) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([] func (l *Lbank) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { var resp float64 if feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { - return feeBuilder.Amount * feeBuilder.PurchasePrice * l.Fee, nil + return feeBuilder.Amount * feeBuilder.PurchasePrice * 0.002, nil } if feeBuilder.FeeType == exchange.CryptocurrencyWithdrawalFee { withdrawalFee, err := l.GetWithdrawConfig(feeBuilder.Pair.Base.Lower().String()) @@ -490,18 +638,18 @@ func (l *Lbank) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { // GetAllOpenOrderID returns all open orders by currency pairs func (l *Lbank) getAllOpenOrderID() (map[string][]string, error) { - allPairs := l.GetEnabledCurrencies() + allPairs := l.GetEnabledPairs(asset.Spot) resp := make(map[string][]string) for a := range allPairs { - p := exchange.FormatExchangeCurrency(l.Name, allPairs[a]) + p := l.FormatExchangeCurrency(allPairs[a], asset.Spot).String() b := int64(1) - tempResp, err := l.GetOpenOrders(exchange.FormatExchangeCurrency(l.Name, p).String(), strconv.FormatInt(b, 10), "200") + tempResp, err := l.GetOpenOrders(p, strconv.FormatInt(b, 10), "200") if err != nil { return resp, err } tempData := len(tempResp.Orders) for tempData != 0 { - tempResp, err = l.GetOpenOrders(exchange.FormatExchangeCurrency(l.Name, p).String(), strconv.FormatInt(b, 10), "200") + tempResp, err = l.GetOpenOrders(p, strconv.FormatInt(b, 10), "200") if err != nil { return resp, err } @@ -511,7 +659,7 @@ func (l *Lbank) getAllOpenOrderID() (map[string][]string, error) { } for c := 0; c < tempData; c++ { - resp[exchange.FormatExchangeCurrency(l.Name, p).String()] = append(resp[exchange.FormatExchangeCurrency(l.Name, p).String()], tempResp.Orders[c].OrderID) + resp[p] = append(resp[p], tempResp.Orders[c].OrderID) } tempData = len(tempResp.Orders) b++ diff --git a/exchanges/localbitcoins/README.md b/exchanges/localbitcoins/README.md index b99bd59c..22123daa 100644 --- a/exchanges/localbitcoins/README.md +++ b/exchanges/localbitcoins/README.md @@ -47,22 +47,22 @@ main.go ```go var l exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "LocalBitcoins" { - l = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "LocalBitcoins" { + l = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := l.GetTickerPrice() +tick, err := l.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := l.GetOrderbookEx() +ob, err := l.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +130,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/localbitcoins/localbitcoins.go b/exchanges/localbitcoins/localbitcoins.go index 6f323e03..f84f0728 100644 --- a/exchanges/localbitcoins/localbitcoins.go +++ b/exchanges/localbitcoins/localbitcoins.go @@ -8,13 +8,10 @@ import ( "net/url" "strconv" "strings" - "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -115,65 +112,6 @@ type LocalBitcoins struct { exchange.Base } -// SetDefaults sets the package defaults for localbitcoins -func (l *LocalBitcoins) SetDefaults() { - l.Name = "LocalBitcoins" - l.Enabled = false - l.Verbose = false - l.Verbose = false - l.RESTPollingDelay = 10 - l.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | - exchange.WithdrawFiatViaWebsiteOnly - l.RequestCurrencyPairFormat.Delimiter = "" - l.RequestCurrencyPairFormat.Uppercase = true - l.ConfigCurrencyPairFormat.Delimiter = "" - l.ConfigCurrencyPairFormat.Uppercase = true - l.SupportsAutoPairUpdating = true - l.SupportsRESTTickerBatching = true - l.Requester = request.New(l.Name, - request.NewRateLimit(time.Millisecond*500, localbitcoinsAuthRate), - request.NewRateLimit(time.Millisecond*500, localbitcoinsUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - l.APIUrlDefault = localbitcoinsAPIURL - l.APIUrl = l.APIUrlDefault - l.Websocket = wshandler.New() -} - -// Setup sets exchange configuration parameters -func (l *LocalBitcoins) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - l.SetEnabled(false) - } else { - l.Enabled = true - l.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - l.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - l.SetHTTPClientTimeout(exch.HTTPTimeout) - l.SetHTTPClientUserAgent(exch.HTTPUserAgent) - l.RESTPollingDelay = exch.RESTPollingDelay - l.Verbose = exch.Verbose - l.HTTPDebugging = exch.HTTPDebugging - l.BaseCurrencies = exch.BaseCurrencies - l.AvailablePairs = exch.AvailablePairs - l.EnabledPairs = exch.EnabledPairs - err := l.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = l.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = l.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = l.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - } -} - // GetAccountInformation lets you retrieve the public user information on a // LocalBitcoins user. The response contains the same information that is found // on an account's public profile page. @@ -189,7 +127,7 @@ func (l *LocalBitcoins) GetAccountInformation(username string, self bool) (Accou return resp.Data, err } } else { - path := fmt.Sprintf("%s/%s/%s/", l.APIUrl, localbitcoinsAPIAccountInfo, username) + path := fmt.Sprintf("%s/%s/%s/", l.API.Endpoints.URL, localbitcoinsAPIAccountInfo, username) err := l.SendHTTPRequest(path, &resp) if err != nil { return resp.Data, err @@ -397,14 +335,14 @@ func (l *LocalBitcoins) GetTradeInfo(contactID string) (dbi DashBoardInfo, err e // GetCountryCodes returns a list of valid and recognized countrycodes func (l *LocalBitcoins) GetCountryCodes() error { - return l.SendHTTPRequest(l.APIUrl+localbitcoinsAPICountryCodes, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPICountryCodes, nil) } // GetCurrencies returns a list of valid and recognized fiat currencies. Also // contains human readable name for every currency and boolean that tells if // currency is an altcoin. func (l *LocalBitcoins) GetCurrencies() error { - return l.SendHTTPRequest(l.APIUrl+localbitcoinsAPICurrencies, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPICurrencies, nil) } // GetDashboardInfo returns a list of trades on the data key contact_list. This @@ -532,13 +470,13 @@ func (l *LocalBitcoins) MarkNotifications() error { // and code for payment methods, and possible limitations in currencies and bank // name choices. func (l *LocalBitcoins) GetPaymentMethods() error { - return l.SendHTTPRequest(l.APIUrl+localbitcoinsAPIPaymentMethods, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIPaymentMethods, nil) } // GetPaymentMethodsByCountry returns a list of valid payment methods filtered // by countrycodes. func (l *LocalBitcoins) GetPaymentMethodsByCountry(countryCode string) error { - return l.SendHTTPRequest(l.APIUrl+localbitcoinsAPIPaymentMethods+countryCode, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIPaymentMethods+countryCode, nil) } // CheckPincode checks the given PIN code against the token owners currently @@ -573,7 +511,7 @@ func (l *LocalBitcoins) CheckPincode(pin int) (bool, error) { // sell listings for each. // TODO func (l *LocalBitcoins) GetPlaces() error { - return l.SendHTTPRequest(l.APIUrl+localbitcoinsAPIPlaces, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIPlaces, nil) } // VerifyUsername returns list of real name verifiers for the user. Returns a @@ -641,7 +579,7 @@ func (l *LocalBitcoins) WalletSend(address string, amount float64, pin int64) er path := localbitcoinsAPIWalletSend if pin > 0 { - values.Set("pincode", fmt.Sprintf("%v", pin)) + values.Set("pincode", strconv.FormatInt(pin, 10)) path = localbitcoinsAPIWalletSendPin } @@ -663,11 +601,11 @@ func (l *LocalBitcoins) WalletSend(address string, amount float64, pin int64) er if resp.Data.Message != "Money is being sent" { if len(resp.Error.Errors) != 0 { - var details string - for _, val := range resp.Error.Errors { - details += val + var details strings.Builder + for x := range resp.Error.Errors { + details.WriteString(resp.Error.Errors[x]) } - return errors.New(details) + return errors.New(details.String()) } return errors.New(resp.Data.Message) } @@ -701,20 +639,20 @@ func (l *LocalBitcoins) GetWalletAddress() (string, error) { // GetBitcoinsWithCashAd returns buy or sell as cash local advertisements. // TODO func (l *LocalBitcoins) GetBitcoinsWithCashAd() error { - return l.SendHTTPRequest(l.APIUrl+localbitcoinsAPICashBuy, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPICashBuy, nil) } // GetBitcoinsOnlineAd this API returns buy or sell Bitcoin online ads. // TODO func (l *LocalBitcoins) GetBitcoinsOnlineAd() error { - return l.SendHTTPRequest(l.APIUrl+localbitcoinsAPIOnlineBuy, nil) + return l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPIOnlineBuy, nil) } // GetTicker returns list of all completed trades. func (l *LocalBitcoins) GetTicker() (map[string]Ticker, error) { result := make(map[string]Ticker) - return result, l.SendHTTPRequest(l.APIUrl+localbitcoinsAPITicker, &result) + return result, l.SendHTTPRequest(l.API.Endpoints.URL+localbitcoinsAPITicker, &result) } // GetTradableCurrencies returns a list of tradable fiat currencies @@ -735,9 +673,8 @@ func (l *LocalBitcoins) GetTradableCurrencies() ([]string, error) { // GetTrades returns all closed trades in online buy and online sell categories, // updated every 15 minutes. func (l *LocalBitcoins) GetTrades(currency string, values url.Values) ([]Trade, error) { - path := common.EncodeURLValues(fmt.Sprintf("%s/%s/trades.json", l.APIUrl+localbitcoinsAPIBitcoincharts, currency), values) + path := common.EncodeURLValues(fmt.Sprintf("%s/%s/trades.json", l.API.Endpoints.URL+localbitcoinsAPIBitcoincharts, currency), values) var result []Trade - return result, l.SendHTTPRequest(path, &result) } @@ -751,7 +688,7 @@ func (l *LocalBitcoins) GetOrderbook(currency string) (Orderbook, error) { Asks [][]string `json:"asks"` } - path := fmt.Sprintf("%s/%s/orderbook.json", l.APIUrl+localbitcoinsAPIBitcoincharts, currency) + path := fmt.Sprintf("%s/%s/orderbook.json", l.API.Endpoints.URL+localbitcoinsAPIBitcoincharts, currency) resp := response{} err := l.SendHTTPRequest(path, &resp) @@ -764,12 +701,12 @@ func (l *LocalBitcoins) GetOrderbook(currency string) (Orderbook, error) { for _, x := range resp.Bids { price, err := strconv.ParseFloat(x[0], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } amount, err := strconv.ParseFloat(x[1], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } orderbook.Bids = append(orderbook.Bids, Price{price, amount}) @@ -778,12 +715,12 @@ func (l *LocalBitcoins) GetOrderbook(currency string) (Orderbook, error) { for _, x := range resp.Asks { price, err := strconv.ParseFloat(x[0], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } amount, err := strconv.ParseFloat(x[1], 64) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) continue } orderbook.Asks = append(orderbook.Asks, Price{price, amount}) @@ -809,7 +746,7 @@ func (l *LocalBitcoins) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to // localbitcoins func (l *LocalBitcoins) SendAuthenticatedHTTPRequest(method, path string, params url.Values, result interface{}) (err error) { - if !l.AuthenticatedAPISupport { + if !l.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, l.Name) } @@ -817,16 +754,16 @@ func (l *LocalBitcoins) SendAuthenticatedHTTPRequest(method, path string, params path = "/api/" + path encoded := params.Encode() - message := n + l.APIKey + path + encoded - hmac := common.GetHMAC(common.HashSHA256, []byte(message), []byte(l.APISecret)) + message := n + l.API.Credentials.Key + path + encoded + hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(message), []byte(l.API.Credentials.Secret)) headers := make(map[string]string) - headers["Apiauth-Key"] = l.APIKey + headers["Apiauth-Key"] = l.API.Credentials.Key headers["Apiauth-Nonce"] = n - headers["Apiauth-Signature"] = common.StringToUpper(common.HexEncodeToString(hmac)) + headers["Apiauth-Signature"] = strings.ToUpper(crypto.HexEncodeToString(hmac)) headers["Content-Type"] = "application/x-www-form-urlencoded" if l.Verbose { - log.Debugf("Sending POST request to `%s`, path: `%s`, params: `%s`.", l.APIUrl, path, encoded) + log.Debugf(log.ExchangeSys, "Sending POST request to `%s`, path: `%s`, params: `%s`.", l.API.Endpoints.URL, path, encoded) } if method == http.MethodGet && len(encoded) > 0 { @@ -834,7 +771,7 @@ func (l *LocalBitcoins) SendAuthenticatedHTTPRequest(method, path string, params } return l.SendPayload(method, - l.APIUrl+path, + l.API.Endpoints.URL+path, headers, bytes.NewBufferString(encoded), result, diff --git a/exchanges/localbitcoins/localbitcoins_live_test.go b/exchanges/localbitcoins/localbitcoins_live_test.go index 17365f9b..be9fe318 100644 --- a/exchanges/localbitcoins/localbitcoins_live_test.go +++ b/exchanges/localbitcoins/localbitcoins_live_test.go @@ -17,16 +17,22 @@ var mockTests = false func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("LocalBitcoins load config error", err) + } localbitcoinsConfig, err := cfg.GetExchangeConfig("LocalBitcoins") if err != nil { - log.Fatal("Test Failed - LocalBitcoins Setup() init error", err) + log.Fatal("LocalBitcoins Setup() init error", err) } - localbitcoinsConfig.AuthenticatedAPISupport = true - localbitcoinsConfig.APIKey = apiKey - localbitcoinsConfig.APISecret = apiSecret + localbitcoinsConfig.API.AuthenticatedSupport = true + localbitcoinsConfig.API.Credentials.Key = apiKey + localbitcoinsConfig.API.Credentials.Secret = apiSecret l.SetDefaults() - l.Setup(&localbitcoinsConfig) - log.Printf(sharedtestvalues.LiveTesting, l.GetName(), l.APIUrl) + err = l.Setup(localbitcoinsConfig) + if err != nil { + log.Fatal("Localbitcoins setup error", err) + } + log.Printf(sharedtestvalues.LiveTesting, l.Name, l.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/localbitcoins/localbitcoins_mock_test.go b/exchanges/localbitcoins/localbitcoins_mock_test.go index 39de1ed4..83c0b23d 100644 --- a/exchanges/localbitcoins/localbitcoins_mock_test.go +++ b/exchanges/localbitcoins/localbitcoins_mock_test.go @@ -20,25 +20,32 @@ var mockTests = true func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Localbitcoins load config error", err) + } localbitcoinsConfig, err := cfg.GetExchangeConfig("LocalBitcoins") if err != nil { - log.Fatal("Test Failed - Localbitcoins Setup() init error", err) + log.Fatal("Localbitcoins Setup() init error", err) } - localbitcoinsConfig.AuthenticatedAPISupport = true - localbitcoinsConfig.APIKey = apiKey - localbitcoinsConfig.APISecret = apiSecret + l.SkipAuthCheck = true + localbitcoinsConfig.API.AuthenticatedSupport = true + localbitcoinsConfig.API.Credentials.Key = apiKey + localbitcoinsConfig.API.Credentials.Secret = apiSecret l.SetDefaults() - l.Setup(&localbitcoinsConfig) + err = l.Setup(localbitcoinsConfig) + if err != nil { + log.Fatal("Localbitcoins setup error", err) + } serverDetails, newClient, err := mock.NewVCRServer(mockfile) if err != nil { - log.Fatalf("Test Failed - Mock server error %s", err) + log.Fatalf("Mock server error %s", err) } l.HTTPClient = newClient - l.APIUrl = serverDetails + l.API.Endpoints.URL = serverDetails - log.Printf(sharedtestvalues.MockTesting, l.GetName(), l.APIUrl) + log.Printf(sharedtestvalues.MockTesting, l.Name, l.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/localbitcoins/localbitcoins_test.go b/exchanges/localbitcoins/localbitcoins_test.go index 34eab667..d1c6bc0e 100644 --- a/exchanges/localbitcoins/localbitcoins_test.go +++ b/exchanges/localbitcoins/localbitcoins_test.go @@ -6,6 +6,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // Please supply your own APIKEYS here for due diligence testing @@ -23,7 +24,7 @@ func TestGetTicker(t *testing.T) { _, err := l.GetTicker() if err != nil { - t.Errorf("Test failed - GetTicker() returned: %s", err) + t.Errorf("GetTicker() returned: %s", err) } } @@ -32,13 +33,12 @@ func TestGetTradableCurrencies(t *testing.T) { _, err := l.GetTradableCurrencies() if err != nil { - t.Errorf("Test failed - GetTradableCurrencies() returned: %s", err) + t.Errorf("GetTradableCurrencies() returned: %s", err) } } func TestGetAccountInfo(t *testing.T) { t.Parallel() - _, err := l.GetAccountInformation("", true) switch { case areTestAPIKeysSet() && err != nil && !mockTests: @@ -52,7 +52,6 @@ func TestGetAccountInfo(t *testing.T) { func TestGetads(t *testing.T) { t.Parallel() - _, err := l.Getads("") switch { case areTestAPIKeysSet() && err != nil && !mockTests: @@ -97,7 +96,7 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { t.Parallel() var feeBuilder = setFeeBuilder() l.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -114,7 +113,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee Basic if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) } // CryptocurrencyTradeFee High quantity @@ -122,7 +121,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -130,7 +129,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -138,14 +137,14 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -154,7 +153,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -162,7 +161,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -170,7 +169,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -179,7 +178,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := l.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } @@ -190,7 +189,6 @@ func TestFormatWithdrawPermissions(t *testing.T) { expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.WithdrawFiatViaWebsiteOnlyText - withdrawPermissions := l.FormatWithdrawPermissions() if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", @@ -202,8 +200,8 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := l.GetActiveOrders(&getOrdersRequest) @@ -220,8 +218,8 @@ func TestGetActiveOrders(t *testing.T) { func TestGetOrderHistory(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := l.GetOrderHistory(&getOrdersRequest) @@ -238,11 +236,7 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if l.APIKey != "" && l.APIKey != "Key" && - l.APISecret != "" && l.APISecret != "Secret" { - return true - } - return false + return l.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { @@ -252,12 +246,18 @@ func TestSubmitOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var p = currency.Pair{ - Delimiter: "", - Base: currency.BTC, - Quote: currency.EUR, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.EUR, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := l.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 10, "hi") + response, err := l.SubmitOrder(orderSubmission) switch { case areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) && !mockTests: t.Errorf("Order failed to be placed: %v", err) @@ -274,8 +274,7 @@ func TestCancelExchangeOrder(t *testing.T) { if areTestAPIKeysSet() && !canManipulateRealOrders && !mockTests { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -299,8 +298,7 @@ func TestCancelAllExchangeOrders(t *testing.T) { if areTestAPIKeysSet() && !canManipulateRealOrders && !mockTests { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -317,28 +315,30 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestModifyOrder(t *testing.T) { t.Parallel() - _, err := l.ModifyOrder(&exchange.ModifyOrder{}) + _, err := l.ModifyOrder(&order.Modify{}) if err != common.ErrFunctionNotSupported { - t.Error("Test failed - ModifyOrder() error", err) + t.Error("ModifyOrder() error", err) } } func TestWithdraw(t *testing.T) { t.Parallel() - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.LTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders && !mockTests { @@ -359,7 +359,7 @@ func TestWithdraw(t *testing.T) { func TestWithdrawFiat(t *testing.T) { t.Parallel() - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := l.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", @@ -371,7 +371,7 @@ func TestWithdrawFiat(t *testing.T) { func TestWithdrawInternationalBank(t *testing.T) { t.Parallel() - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := l.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", @@ -386,10 +386,10 @@ func TestGetDepositAddress(t *testing.T) { _, err := l.GetDepositAddress(currency.BTC, "") switch { case areTestAPIKeysSet() && err != nil && !mockTests: - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) case !areTestAPIKeysSet() && err == nil && !mockTests: - t.Error("Test Failed - GetDepositAddress() expecting an error when no APIKeys are set") + t.Error("GetDepositAddress() expecting an error when no APIKeys are set") case mockTests && err != nil: - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() error", err) } } diff --git a/exchanges/localbitcoins/localbitcoins_types.go b/exchanges/localbitcoins/localbitcoins_types.go index 6d145505..5ac68ced 100644 --- a/exchanges/localbitcoins/localbitcoins_types.go +++ b/exchanges/localbitcoins/localbitcoins_types.go @@ -312,7 +312,8 @@ type WalletBalanceInfo struct { // Ticker contains ticker information type Ticker struct { Avg12h float64 `json:"avg_12h,string"` - Avg1h float64 `json:"avg_1h,string"` + Avg1h float64 `json:"avg_1h,string,omitempty"` + Avg6h float64 `json:"avg_6h,string,omitempty"` Avg24h float64 `json:"avg_24h,string"` Rates struct { Last float64 `json:"last,string"` diff --git a/exchanges/localbitcoins/localbitcoins_wrapper.go b/exchanges/localbitcoins/localbitcoins_wrapper.go index 6e8ccb6b..1e1bf0a2 100644 --- a/exchanges/localbitcoins/localbitcoins_wrapper.go +++ b/exchanges/localbitcoins/localbitcoins_wrapper.go @@ -10,14 +10,109 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (l *LocalBitcoins) GetDefaultConfig() (*config.ExchangeConfig, error) { + l.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = l.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = l.BaseCurrencies + + err := l.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if l.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = l.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the package defaults for localbitcoins +func (l *LocalBitcoins) SetDefaults() { + l.Name = "LocalBitcoins" + l.Enabled = true + l.Verbose = true + l.API.CredentialsValidator.RequiresKey = true + l.API.CredentialsValidator.RequiresSecret = true + + l.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + } + + l.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: false, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + CancelOrder: true, + SubmitOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.WithdrawFiatViaWebsiteOnly, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + l.Requester = request.New(l.Name, + request.NewRateLimit(time.Second*0, localbitcoinsAuthRate), + request.NewRateLimit(time.Second*0, localbitcoinsUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + l.API.Endpoints.URLDefault = localbitcoinsAPIURL + l.API.Endpoints.URL = l.API.Endpoints.URLDefault +} + +// Setup sets exchange configuration parameters +func (l *LocalBitcoins) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + l.SetEnabled(false) + return nil + } + + return l.SetupDefaults(exch) +} + // Start starts the LocalBitcoins go routine func (l *LocalBitcoins) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -30,14 +125,24 @@ func (l *LocalBitcoins) Start(wg *sync.WaitGroup) { // Run implements the LocalBitcoins wrapper func (l *LocalBitcoins) Run() { if l.Verbose { - log.Debugf("%s polling delay: %ds.\n", l.GetName(), l.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", l.GetName(), len(l.EnabledPairs), l.EnabledPairs) + l.PrintEnabledPairs() } + if !l.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := l.UpdateTradablePairs(false) + if err != nil { + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", l.Name, err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (l *LocalBitcoins) FetchTradablePairs(asset asset.Item) ([]string, error) { currencies, err := l.GetTradableCurrencies() if err != nil { - log.Errorf("%s failed to obtain available tradable currencies. Err: %s", l.Name, err) - return + return nil, err } var pairs []string @@ -45,54 +150,59 @@ func (l *LocalBitcoins) Run() { pairs = append(pairs, "BTC"+currencies[x]) } - var newExchangeProducts currency.Pairs - for _, p := range pairs { - newExchangeProducts = append(newExchangeProducts, - currency.NewPairFromString(p)) - } + return pairs, nil +} - err = l.UpdateCurrencies(newExchangeProducts, false, false) +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (l *LocalBitcoins) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := l.FetchTradablePairs(asset.Spot) if err != nil { - log.Errorf("%s failed to update available currencies. Err %s", l.Name, err) + return err } + return l.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) } // UpdateTicker updates and returns the ticker for a currency pair -func (l *LocalBitcoins) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (l *LocalBitcoins) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price tick, err := l.GetTicker() if err != nil { return tickerPrice, err } - for _, x := range l.GetEnabledCurrencies() { - currency := x.Quote.String() + pairs := l.GetEnabledPairs(assetType) + for i := range pairs { + curr := pairs[i].Quote.String() + if _, ok := tick[curr]; !ok { + continue + } var tp ticker.Price - tp.Pair = x - tp.Last = tick[currency].Avg24h - tp.Volume = tick[currency].VolumeBTC + tp.Pair = pairs[i] + tp.Last = tick[curr].Avg24h + tp.Volume = tick[curr].VolumeBTC - err = ticker.ProcessTicker(l.GetName(), &tp, assetType) + err = ticker.ProcessTicker(l.Name, &tp, assetType) if err != nil { - return tickerPrice, err + log.Error(log.Ticker, err) } } - return ticker.GetTicker(l.GetName(), p, assetType) + return ticker.GetTicker(l.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (l *LocalBitcoins) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(l.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (l *LocalBitcoins) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(l.Name, p, assetType) if err != nil { return l.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (l *LocalBitcoins) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(l.GetName(), p, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (l *LocalBitcoins) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(l.Name, p, assetType) if err != nil { return l.UpdateOrderbook(p, assetType) } @@ -100,7 +210,7 @@ func (l *LocalBitcoins) GetOrderbookEx(p currency.Pair, assetType string) (order } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (l *LocalBitcoins) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (l *LocalBitcoins) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base orderbookNew, err := l.GetOrderbook(p.Quote.String()) if err != nil { @@ -108,17 +218,21 @@ func (l *LocalBitcoins) UpdateOrderbook(p currency.Pair, assetType string) (orde } for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data.Amount / data.Price, Price: data.Price}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[x].Amount / orderbookNew.Bids[x].Price, + Price: orderbookNew.Bids[x].Price, + }) } for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data.Amount / data.Price, Price: data.Price}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[x].Amount / orderbookNew.Asks[x].Price, + Price: orderbookNew.Asks[x].Price, + }) } orderBook.Pair = p - orderBook.ExchangeName = l.GetName() + orderBook.ExchangeName = l.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -133,7 +247,7 @@ func (l *LocalBitcoins) UpdateOrderbook(p currency.Pair, assetType string) (orde // LocalBitcoins exchange func (l *LocalBitcoins) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = l.GetName() + response.Exchange = l.Name accountBalance, err := l.GetWalletBalance() if err != nil { return response, err @@ -151,20 +265,21 @@ func (l *LocalBitcoins) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (l *LocalBitcoins) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (l *LocalBitcoins) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (l *LocalBitcoins) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (l *LocalBitcoins) SubmitOrder(p currency.Pair, side exchange.OrderSide, _ exchange.OrderType, amount, _ float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse +func (l *LocalBitcoins) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } + // These are placeholder details // TODO store a user's localbitcoin details to use here var params = AdCreate{ @@ -174,17 +289,17 @@ func (l *LocalBitcoins) SubmitOrder(p currency.Pair, side exchange.OrderSide, _ City: "City", Location: "Location", CountryCode: "US", - Currency: p.Quote.String(), + Currency: s.Pair.Quote.String(), AccountInfo: "-", BankName: "Bank", - MSG: side.ToString(), + MSG: s.OrderSide.String(), SMSVerficationRequired: true, TrackMaxAmount: true, RequireTrustedByAdvertiser: true, RequireIdentification: true, OnlineProvider: "", TradeType: "", - MinAmount: int(math.Round(amount)), + MinAmount: int(math.Round(s.Amount)), } // Does not return any orderID, so create the add, then get the order @@ -214,8 +329,8 @@ func (l *LocalBitcoins) SubmitOrder(p currency.Pair, side exchange.OrderSide, _ ads.AdList[i].Data.RequireTrustedByAdvertiser == params.RequireTrustedByAdvertiser && ads.AdList[i].Data.OnlineProvider == params.OnlineProvider && ads.AdList[i].Data.TradeType == params.TradeType && - ads.AdList[i].Data.MinAmount == fmt.Sprintf("%v", params.MinAmount) { - adID = fmt.Sprintf("%v", ads.AdList[i].Data.AdID) + ads.AdList[i].Data.MinAmount == strconv.FormatInt(int64(params.MinAmount), 10) { + adID = strconv.FormatInt(ads.AdList[i].Data.AdID, 10) } } @@ -230,19 +345,19 @@ func (l *LocalBitcoins) SubmitOrder(p currency.Pair, side exchange.OrderSide, _ // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (l *LocalBitcoins) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (l *LocalBitcoins) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (l *LocalBitcoins) CancelOrder(order *exchange.OrderCancellation) error { +func (l *LocalBitcoins) CancelOrder(order *order.Cancel) error { return l.DeleteAd(order.OrderID) } // CancelAllOrders cancels all orders associated with a currency pair -func (l *LocalBitcoins) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (l *LocalBitcoins) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } ads, err := l.Getads() if err != nil { @@ -253,7 +368,7 @@ func (l *LocalBitcoins) CancelAllOrders(_ *exchange.OrderCancellation) (exchange adIDString := strconv.FormatInt(ads.AdList[i].Data.AdID, 10) err = l.DeleteAd(adIDString) if err != nil { - cancelAllOrdersResponse.OrderStatus[adIDString] = err.Error() + cancelAllOrdersResponse.Status[adIDString] = err.Error() } } @@ -261,8 +376,8 @@ func (l *LocalBitcoins) CancelAllOrders(_ *exchange.OrderCancellation) (exchange } // GetOrderInfo returns information on a current open order -func (l *LocalBitcoins) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (l *LocalBitcoins) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -278,7 +393,7 @@ func (l *LocalBitcoins) GetDepositAddress(cryptocurrency currency.Code, _ string // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (l *LocalBitcoins) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (l *LocalBitcoins) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { return "", l.WalletSend(withdrawRequest.Address, withdrawRequest.Amount, @@ -287,13 +402,13 @@ func (l *LocalBitcoins) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.Wi // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (l *LocalBitcoins) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (l *LocalBitcoins) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (l *LocalBitcoins) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (l *LocalBitcoins) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -304,7 +419,7 @@ func (l *LocalBitcoins) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (l *LocalBitcoins) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (l.APIKey == "" || l.APISecret == "") && // Todo check connection status + if (!l.AllowAuthenticatedRequest() || l.SkipAuthCheck) && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -312,54 +427,54 @@ func (l *LocalBitcoins) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, } // GetActiveOrders retrieves any orders that are active/open -func (l *LocalBitcoins) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (l *LocalBitcoins) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := l.GetDashboardInfo() if err != nil { return nil, err } - var orders []exchange.OrderDetail + var orders []order.Detail for i := range resp { orderDate, err := time.Parse(time.RFC3339, resp[i].Data.CreatedAt) if err != nil { - log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v", + log.Errorf(log.ExchangeSys, "Exchange %v Func %v Order %v Could not parse date to unix with value of %v", l.Name, "GetActiveOrders", resp[i].Data.Advertisement.ID, resp[i].Data.CreatedAt) } - var side exchange.OrderSide + var side order.Side if resp[i].Data.IsBuying { - side = exchange.BuyOrderSide + side = order.Buy } else if resp[i].Data.IsSelling { - side = exchange.SellOrderSide + side = order.Sell } - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ Amount: resp[i].Data.AmountBTC, Price: resp[i].Data.Amount, - ID: fmt.Sprintf("%v", resp[i].Data.Advertisement.ID), + ID: strconv.FormatInt(int64(resp[i].Data.Advertisement.ID), 10), OrderDate: orderDate, Fee: resp[i].Data.FeeBTC, OrderSide: side, CurrencyPair: currency.NewPairWithDelimiter(currency.BTC.String(), resp[i].Data.Currency, - l.ConfigCurrencyPairFormat.Delimiter), + l.GetPairFormat(asset.Spot, false).Delimiter), Exchange: l.Name, }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, + order.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) + order.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (l *LocalBitcoins) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (l *LocalBitcoins) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { var allTrades []DashBoardInfo resp, err := l.GetDashboardCancelledTrades() if err != nil { @@ -379,53 +494,57 @@ func (l *LocalBitcoins) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequ } allTrades = append(allTrades, resp...) - var orders []exchange.OrderDetail + var orders []order.Detail for i := range allTrades { orderDate, err := time.Parse(time.RFC3339, allTrades[i].Data.CreatedAt) if err != nil { - log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v", + log.Errorf(log.ExchangeSys, + "Exchange %v Func %v Order %v Could not parse date to unix with value of %v", l.Name, "GetActiveOrders", allTrades[i].Data.Advertisement.ID, allTrades[i].Data.CreatedAt) } - var side exchange.OrderSide + var side order.Side if allTrades[i].Data.IsBuying { - side = exchange.BuyOrderSide + side = order.Buy } else if allTrades[i].Data.IsSelling { - side = exchange.SellOrderSide + side = order.Sell } status := "" switch { - case allTrades[i].Data.ReleasedAt != "" && allTrades[i].Data.ReleasedAt != null: + case allTrades[i].Data.ReleasedAt != "" && + allTrades[i].Data.ReleasedAt != null: status = "Released" - case allTrades[i].Data.CanceledAt != "" && allTrades[i].Data.CanceledAt != null: + case allTrades[i].Data.CanceledAt != "" && + allTrades[i].Data.CanceledAt != null: status = "Cancelled" - case allTrades[i].Data.ClosedAt != "" && allTrades[i].Data.ClosedAt != null: + case allTrades[i].Data.ClosedAt != "" && + allTrades[i].Data.ClosedAt != null: status = "Closed" } - orders = append(orders, exchange.OrderDetail{ + orders = append(orders, order.Detail{ Amount: allTrades[i].Data.AmountBTC, Price: allTrades[i].Data.Amount, - ID: fmt.Sprintf("%v", allTrades[i].Data.Advertisement.ID), + ID: strconv.FormatInt(int64(allTrades[i].Data.Advertisement.ID), 10), OrderDate: orderDate, Fee: allTrades[i].Data.FeeBTC, OrderSide: side, - Status: status, + Status: order.Status(status), CurrencyPair: currency.NewPairWithDelimiter(currency.BTC.String(), allTrades[i].Data.Currency, - l.ConfigCurrencyPairFormat.Delimiter), + l.GetPairFormat(asset.Spot, false).Delimiter), Exchange: l.Name, }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, + order.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) + order.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) return orders, nil } diff --git a/exchanges/mock/README.md b/exchanges/mock/README.md index 8734035c..9f9e7422 100644 --- a/exchanges/mock/README.md +++ b/exchanges/mock/README.md @@ -56,14 +56,14 @@ func TestMain(m *testing.M) { cfg.LoadConfig("../../testdata/configtest.json") your_current_exchange_nameConfig, err := cfg.GetExchangeConfig("your_current_exchange_name") if err != nil { - log.Fatal("Test Failed - your_current_exchange_name Setup() init error", err) + log.Fatal("your_current_exchange_name Setup() init error", err) } your_current_exchange_nameConfig.AuthenticatedAPISupport = true your_current_exchange_nameConfig.APIKey = apiKey your_current_exchange_nameConfig.APISecret = apiSecret l.SetDefaults() l.Setup(&your_current_exchange_nameConfig) - log.Printf(sharedtestvalues.LiveTesting, l.GetName(), l.APIUrl) + log.Printf(sharedtestvalues.LiveTesting, l.Name, l.APIUrl) os.Exit(m.Run()) } ``` @@ -96,7 +96,7 @@ func TestMain(m *testing.M) { cfg.LoadConfig("../../testdata/configtest.json") your_current_exchange_nameConfig, err := cfg.GetExchangeConfig("your_current_exchange_name") if err != nil { - log.Fatal("Test Failed - your_current_exchange_name Setup() init error", err) + log.Fatal("your_current_exchange_name Setup() init error", err) } your_current_exchange_nameConfig.AuthenticatedAPISupport = true your_current_exchange_nameConfig.APIKey = apiKey @@ -106,13 +106,13 @@ func TestMain(m *testing.M) { serverDetails, newClient, err := mock.NewVCRServer(mockfile) if err != nil { - log.Fatalf("Test Failed - Mock server error %s", err) + log.Fatalf("Mock server error %s", err) } g.HTTPClient = newClient g.APIUrl = serverDetails - log.Printf(sharedtestvalues.MockTesting, l.GetName(), l.APIUrl) + log.Printf(sharedtestvalues.MockTesting, l.Name, l.APIUrl) os.Exit(m.Run()) } diff --git a/exchanges/mock/common_test.go b/exchanges/mock/common_test.go index d06269dc..ae7ede80 100644 --- a/exchanges/mock/common_test.go +++ b/exchanges/mock/common_test.go @@ -19,42 +19,42 @@ func TestMatchURLVals(t *testing.T) { var expected = false received := MatchURLVals(testVal, emptyVal) if received != expected { - t.Errorf("Test Failed - MatchURLVals error expected %v received %v", + t.Errorf("MatchURLVals error expected %v received %v", expected, received) } received = MatchURLVals(emptyVal, testVal) if received != expected { - t.Errorf("Test Failed - MatchURLVals error expected %v received %v", + t.Errorf("MatchURLVals error expected %v received %v", expected, received) } received = MatchURLVals(testVal, testVal2) if received != expected { - t.Errorf("Test Failed - MatchURLVals error expected %v received %v", + t.Errorf("MatchURLVals error expected %v received %v", expected, received) } received = MatchURLVals(testVal2, testVal) if received != expected { - t.Errorf("Test Failed - MatchURLVals error expected %v received %v", + t.Errorf("MatchURLVals error expected %v received %v", expected, received) } received = MatchURLVals(testVal, testVal3) if received != expected { - t.Errorf("Test Failed - MatchURLVals error expected %v received %v", + t.Errorf("MatchURLVals error expected %v received %v", expected, received) } received = MatchURLVals(nonceVal1, testVal2) if received != expected { - t.Errorf("Test Failed - MatchURLVals error expected %v received %v", + t.Errorf("MatchURLVals error expected %v received %v", expected, received) } @@ -62,21 +62,21 @@ func TestMatchURLVals(t *testing.T) { expected = true received = MatchURLVals(emptyVal, emptyVal) if received != expected { - t.Errorf("Test Failed - MatchURLVals error expected %v received %v", + t.Errorf("MatchURLVals error expected %v received %v", expected, received) } received = MatchURLVals(testVal, testVal) if received != expected { - t.Errorf("Test Failed - MatchURLVals error expected %v received %v", + t.Errorf("MatchURLVals error expected %v received %v", expected, received) } received = MatchURLVals(nonceVal1, nonceVal2) if received != expected { - t.Errorf("Test Failed - MatchURLVals error expected %v received %v", + t.Errorf("MatchURLVals error expected %v received %v", expected, received) } @@ -105,12 +105,12 @@ func TestDeriveURLValsFromJSON(t *testing.T) { payload, err := json.Marshal(test1) if err != nil { - t.Error("Test Failed - marshal error", err) + t.Error("marshal error", err) } _, err = DeriveURLValsFromJSONMap(payload) if err != nil { - t.Error("Test Failed - DeriveURLValsFromJSON error", err) + t.Error("DeriveURLValsFromJSON error", err) } test2 := map[string]string{ @@ -125,16 +125,16 @@ func TestDeriveURLValsFromJSON(t *testing.T) { payload, err = json.Marshal(test2) if err != nil { - t.Error("Test Failed - marshal error", err) + t.Error("marshal error", err) } vals, err := DeriveURLValsFromJSONMap(payload) if err != nil { - t.Error("Test Failed - DeriveURLValsFromJSON error", err) + t.Error("DeriveURLValsFromJSON error", err) } if vals["val"][0] != "1" { - t.Error("Test Failed - DeriveURLValsFromJSON unexpected value", + t.Error("DeriveURLValsFromJSON unexpected value", vals["val"][0]) } } diff --git a/exchanges/mock/recording.go b/exchanges/mock/recording.go index 5cbaa6ab..e561e1f0 100644 --- a/exchanges/mock/recording.go +++ b/exchanges/mock/recording.go @@ -13,7 +13,8 @@ import ( "strings" "sync" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/common/file" ) // HTTPResponse defines expected response from the end point including request @@ -47,7 +48,7 @@ func HTTPRecord(res *http.Response, service string, respContents []byte) error { fileout := filepath.Join(DefaultDirectory, service, service+".json") - contents, err := common.ReadFile(fileout) + contents, err := ioutil.ReadFile(fileout) if err != nil { return err } @@ -100,7 +101,7 @@ func HTTPRecord(res *http.Response, service string, respContents []byte) error { case textPlain: payload := res.Request.Header.Get("X-Gemini-Payload") - j, dErr := common.Base64Decode(payload) + j, dErr := crypto.Base64Decode(payload) if dErr != nil { return dErr } @@ -211,7 +212,7 @@ func HTTPRecord(res *http.Response, service string, respContents []byte) error { return err } - return common.WriteFile(fileout, payload) + return file.Write(fileout, payload) } // GetFilteredHeader filters excluded http headers for insertion into a mock diff --git a/exchanges/mock/recording_test.go b/exchanges/mock/recording_test.go index 978c0c2b..3198e32f 100644 --- a/exchanges/mock/recording_test.go +++ b/exchanges/mock/recording_test.go @@ -19,7 +19,7 @@ func TestGetFilteredHeader(t *testing.T) { } if fMap.Get("Key") != "" { - t.Error("Test Failed - risky vals where not replaced correctly") + t.Error("risky vals where not replaced correctly") } } @@ -29,11 +29,11 @@ func TestGetFilteredURLVals(t *testing.T) { shadyVals.Set("real_name", superSecretData) cleanVals, err := GetFilteredURLVals(shadyVals) if err != nil { - t.Error("Test Failed - GetFilteredURLVals error", err) + t.Error("GetFilteredURLVals error", err) } if strings.Contains(cleanVals, superSecretData) { - t.Error("Test Failed - Super secret data found") + t.Error("Super secret data found") } } @@ -46,12 +46,12 @@ func TestCheckResponsePayload(t *testing.T) { payload, err := json.Marshal(testbody) if err != nil { - t.Fatal("Test Failed - json marshal error", err) + t.Fatal("json marshal error", err) } data, err := CheckResponsePayload(payload) if err != nil { - t.Error("Test Failed - CheckBody error", err) + t.Error("CheckBody error", err) } expected := `{ @@ -126,23 +126,23 @@ func TestCheckJSON(t *testing.T) { exclusionList, err := GetExcludedItems() if err != nil { - t.Error("Test Failed - GetExcludedItems error", err) + t.Error("GetExcludedItems error", err) } vals, err := CheckJSON(testVal, &exclusionList) if err != nil { - t.Error("Test Failed - Check JSON error", err) + t.Error("Check JSON error", err) } payload, err := json.Marshal(vals) if err != nil { - t.Fatal("Test Failed - json marshal error", err) + t.Fatal("json marshal error", err) } newStruct := TestStructLevel0{} err = json.Unmarshal(payload, &newStruct) if err != nil { - t.Fatal("Test Failed - Umarshal error", err) + t.Fatal("Umarshal error", err) } if newStruct.StructVal.BadVal != "" { @@ -173,14 +173,14 @@ func TestCheckJSON(t *testing.T) { func TestGetExcludedItems(t *testing.T) { exclusionList, err := GetExcludedItems() if err != nil { - t.Error("Test Failed - GetExcludedItems error", err) + t.Error("GetExcludedItems error", err) } if len(exclusionList.Headers) == 0 { - t.Error("Test Failed - Header exclusion list not popoulated") + t.Error("Header exclusion list not popoulated") } if len(exclusionList.Variables) == 0 { - t.Error("Test Failed - Variable exclusion list not popoulated") + t.Error("Variable exclusion list not popoulated") } } diff --git a/exchanges/mock/server.go b/exchanges/mock/server.go index 1f06980f..3965ffec 100644 --- a/exchanges/mock/server.go +++ b/exchanges/mock/server.go @@ -14,6 +14,8 @@ import ( "strings" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/common/file" ) // DefaultDirectory defines the main mock directory @@ -42,9 +44,9 @@ func NewVCRServer(path string) (string, *http.Client, error) { contents, err := ioutil.ReadFile(path) if err != nil { - pathing := common.SplitStrings(path, "/") + pathing := strings.Split(path, "/") dirPathing := pathing[:len(pathing)-1] - dir := common.JoinStrings(dirPathing, "/") + dir := strings.Join(dirPathing, "/") err = common.CreateDir(dir) if err != nil { return "", nil, err @@ -55,7 +57,7 @@ func NewVCRServer(path string) (string, *http.Client, error) { return "", nil, jErr } - err = common.WriteFile(path, data) + err = file.Write(path, data) if err != nil { return "", nil, err } @@ -178,7 +180,7 @@ func RegisterHandler(pattern string, mock map[string][]HTTPResponse, mux *http.S base64data := strings.Join(headerData, "") - jsonThings, err := common.Base64Decode(base64data) + jsonThings, err := crypto.Base64Decode(base64data) if err != nil { log.Fatal("Mock Test Failure - ", err) } diff --git a/exchanges/mock/server_test.go b/exchanges/mock/server_test.go index 370d1a31..68042057 100644 --- a/exchanges/mock/server_test.go +++ b/exchanges/mock/server_test.go @@ -24,7 +24,7 @@ const testFile = "test.json" func TestNewVCRServer(t *testing.T) { _, _, err := NewVCRServer("") if err == nil { - t.Error("Test Failed - NewVCRServer error cannot be nil") + t.Error("NewVCRServer error cannot be nil") } // Set up mock data @@ -36,7 +36,7 @@ func TestNewVCRServer(t *testing.T) { Amount: 1, Currency: "bitcoin"}) if err != nil { - t.Fatal("Test Failed - marshal error", err) + t.Fatal("marshal error", err) } testValue := HTTPResponse{Data: rp, QueryString: queryString, BodyParams: queryString} @@ -44,17 +44,17 @@ func TestNewVCRServer(t *testing.T) { payload, err := json.Marshal(test1) if err != nil { - t.Fatal("Test Failed - marshal error", err) + t.Fatal("marshal error", err) } err = ioutil.WriteFile(testFile, payload, os.ModePerm) if err != nil { - t.Fatal("Test Failed - marshal error", err) + t.Fatal("marshal error", err) } deets, client, err := NewVCRServer(testFile) if err != nil { - t.Error("Test Failed - NewVCRServer error", err) + t.Error("NewVCRServer error", err) } common.HTTPClient = client // Set common package global HTTP Client @@ -64,7 +64,7 @@ func TestNewVCRServer(t *testing.T) { nil, bytes.NewBufferString("")) if err == nil { - t.Error("Test Failed - Sending http request expected an error") + t.Error("Sending http request expected an error") } // Expected good outcome @@ -73,11 +73,11 @@ func TestNewVCRServer(t *testing.T) { nil, bytes.NewBufferString("")) if err != nil { - t.Error("Test Failed - Sending http request error", err) + t.Error("Sending http request error", err) } if !strings.Contains(r, "404 page not found") { - t.Error("Test Failed - Was not expecting any value returned:", r) + t.Error("Was not expecting any value returned:", r) } r, err = common.SendHTTPRequest(http.MethodGet, @@ -85,33 +85,33 @@ func TestNewVCRServer(t *testing.T) { nil, bytes.NewBufferString("")) if err != nil { - t.Error("Test Failed - Sending http request error", err) + t.Error("Sending http request error", err) } var res responsePayload err = json.Unmarshal([]byte(r), &res) if err != nil { - t.Error("Test Failed - unmarshal error", err) + t.Error("unmarshal error", err) } if res.Price != 8000 { - t.Error("Test Failed - response error expected 8000 but received:", + t.Error("response error expected 8000 but received:", res.Price) } if res.Amount != 1 { - t.Error("Test Failed - response error expected 1 but received:", + t.Error("response error expected 1 but received:", res.Amount) } if res.Currency != "bitcoin" { - t.Error("Test Failed - response error expected \"bitcoin\" but received:", + t.Error("response error expected \"bitcoin\" but received:", res.Currency) } // clean up test.json file err = os.Remove(testFile) if err != nil { - t.Fatal("Test Failed - Remove error", err) + t.Fatal("Remove error", err) } } diff --git a/exchanges/nonce/README.md b/exchanges/nonce/README.md index db4073fa..a30717dd 100644 --- a/exchanges/nonce/README.md +++ b/exchanges/nonce/README.md @@ -42,4 +42,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/nonce/nonce.go b/exchanges/nonce/nonce.go index 2a863a16..fd358b9f 100644 --- a/exchanges/nonce/nonce.go +++ b/exchanges/nonce/nonce.go @@ -2,22 +2,27 @@ package nonce import ( "strconv" - "sync/atomic" + "sync" ) // Nonce struct holds the nonce value type Nonce struct { n int64 + m sync.Mutex } // Inc increments the nonce value func (n *Nonce) Inc() { - atomic.AddInt64(&n.n, 1) + n.m.Lock() + n.n++ + n.m.Unlock() } // Get retrives the nonce value func (n *Nonce) Get() Value { - return Value(atomic.LoadInt64(&n.n)) + n.m.Lock() + defer n.m.Unlock() + return Value(n.n) } // GetInc increments and returns the value of the nonce @@ -28,7 +33,9 @@ func (n *Nonce) GetInc() Value { // Set sets the nonce value func (n *Nonce) Set(val int64) { - atomic.StoreInt64(&n.n, val) + n.m.Lock() + n.n = val + n.m.Unlock() } // String returns a string version of the nonce diff --git a/exchanges/nonce/nonce_test.go b/exchanges/nonce/nonce_test.go index a4923bed..3ddc290b 100644 --- a/exchanges/nonce/nonce_test.go +++ b/exchanges/nonce/nonce_test.go @@ -12,7 +12,7 @@ func TestInc(t *testing.T) { expected := Value(2) result := nonce.Get() if result != expected { - t.Errorf("Test failed. Expected %d got %d", expected, result) + t.Errorf("Expected %d got %d", expected, result) } } @@ -22,7 +22,7 @@ func TestGet(t *testing.T) { expected := Value(112321313) result := nonce.Get() if expected != result { - t.Errorf("Test failed. Expected %d got %d", expected, result) + t.Errorf("Expected %d got %d", expected, result) } } @@ -32,7 +32,7 @@ func TestGetInc(t *testing.T) { expected := Value(2) result := nonce.GetInc() if expected != result { - t.Errorf("Test failed. Expected %d got %d", expected, result) + t.Errorf("Expected %d got %d", expected, result) } } @@ -42,7 +42,7 @@ func TestSet(t *testing.T) { expected := Value(1) result := nonce.Get() if expected != result { - t.Errorf("Test failed. Expected %d got %d", expected, result) + t.Errorf("Expected %d got %d", expected, result) } } @@ -52,12 +52,12 @@ func TestString(t *testing.T) { expected := "12312313131" result := nonce.String() if expected != result { - t.Errorf("Test failed. Expected %s got %s", expected, result) + t.Errorf("Expected %s got %s", expected, result) } v := nonce.Get() if expected != v.String() { - t.Errorf("Test failed. Expected %s got %s", expected, result) + t.Errorf("Expected %s got %s", expected, result) } } @@ -75,6 +75,6 @@ func TestNonceConcurrency(t *testing.T) { result := nonce.Get() expected := Value(12312 + 1000) if expected != result { - t.Errorf("Test failed. Expected %d got %d", expected, result) + t.Errorf("Expected %d got %d", expected, result) } } diff --git a/exchanges/okcoin/README.md b/exchanges/okcoin/README.md index f40ca757..578b12de 100644 --- a/exchanges/okcoin/README.md +++ b/exchanges/okcoin/README.md @@ -48,22 +48,22 @@ main.go ```go var o exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "OKCoin" { - y = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "OKCoin" { + y = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := o.GetTickerPrice() +tick, err := o.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := o.GetOrderbookEx() +ob, err := o.FetchOrderbook() if err != nil { // Handle error } @@ -138,4 +138,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/okcoin/okcoin.go b/exchanges/okcoin/okcoin.go index 896b3496..51df8211 100644 --- a/exchanges/okcoin/okcoin.go +++ b/exchanges/okcoin/okcoin.go @@ -1,14 +1,7 @@ package okcoin import ( - "time" - - "github.com/thrasher-corp/gocryptotrader/common" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/okgroup" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) const ( @@ -25,42 +18,3 @@ const ( type OKCoin struct { okgroup.OKGroup } - -// SetDefaults method assignes the default values for OKEX -func (o *OKCoin) SetDefaults() { - o.SetErrorDefaults() - o.SetCheckVarDefaults() - o.Name = okCoinExchangeName - o.Enabled = false - o.Verbose = false - o.RESTPollingDelay = 10 - o.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | - exchange.NoFiatWithdrawals - o.RequestCurrencyPairFormat.Delimiter = "_" - o.RequestCurrencyPairFormat.Uppercase = false - o.ConfigCurrencyPairFormat.Delimiter = "_" - o.ConfigCurrencyPairFormat.Uppercase = true - o.SupportsAutoPairUpdating = true - o.SupportsRESTTickerBatching = false - o.Requester = request.New(o.Name, - request.NewRateLimit(time.Second, okCoinAuthRate), - request.NewRateLimit(time.Second, okCoinUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - o.APIUrlDefault = okCoinAPIURL - o.APIUrl = okCoinAPIURL - o.AssetTypes = []string{ticker.Spot} - o.Websocket = wshandler.New() - o.WebsocketURL = okCoinWebsocketURL - o.APIVersion = okCoinAPIVersion - o.Websocket.Functionality = wshandler.WebsocketTickerSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketKlineSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported | - wshandler.WebsocketMessageCorrelationSupported - o.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - o.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - o.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} diff --git a/exchanges/okcoin/okcoin_test.go b/exchanges/okcoin/okcoin_test.go index 558fc1f0..2d212f0e 100644 --- a/exchanges/okcoin/okcoin_test.go +++ b/exchanges/okcoin/okcoin_test.go @@ -1,7 +1,10 @@ package okcoin import ( + "encoding/json" + "log" "net/http" + "os" "strings" "sync" "testing" @@ -12,8 +15,9 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/okgroup" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -32,63 +36,49 @@ var testSetupRan bool var spotCurrency = currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-").Lower().String() var websocketEnabled bool -// TestSetDefaults Sets standard default settings for running a test -func TestSetDefaults(t *testing.T) { - if o.Name != OKGroupExchange { - o.SetDefaults() - } - if o.GetName() != OKGroupExchange { - t.Errorf("Test Failed - %v - SetDefaults() error", OKGroupExchange) - } - TestSetup(t) -} - // TestSetRealOrderDefaults Sets test defaults when test can impact real money/orders func TestSetRealOrderDefaults(t *testing.T) { - TestSetDefaults(t) if !areTestAPIKeysSet() || !canManipulateRealOrders { t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") } } // TestSetup Sets defaults for test environment -func TestSetup(t *testing.T) { - if testSetupRan { - return - } - if o.APIKey == apiKey && o.APISecret == apiSecret && - o.ClientID == passphrase && apiKey != "" && apiSecret != "" && passphrase != "" { - return - } +func TestMain(m *testing.M) { + o.SetDefaults() o.ExchangeName = OKGroupExchange cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Okcoin load config error", err) + } okcoinConfig, err := cfg.GetExchangeConfig(OKGroupExchange) if err != nil { - t.Fatalf("Test Failed - %v Setup() init error", OKGroupExchange) + log.Fatalf("%v Setup() init error", OKGroupExchange) } - if okcoinConfig.Websocket { + if okcoinConfig.Features.Enabled.Websocket { websocketEnabled = true } - okcoinConfig.AuthenticatedAPISupport = true - okcoinConfig.AuthenticatedWebsocketAPISupport = true - okcoinConfig.APIKey = apiKey - okcoinConfig.APISecret = apiSecret - okcoinConfig.ClientID = passphrase - okcoinConfig.WebsocketURL = o.WebsocketURL - o.Setup(&okcoinConfig) + okcoinConfig.API.AuthenticatedSupport = true + okcoinConfig.API.AuthenticatedWebsocketSupport = true + okcoinConfig.API.Credentials.Key = apiKey + okcoinConfig.API.Credentials.Secret = apiSecret + okcoinConfig.API.Credentials.ClientID = passphrase + okcoinConfig.API.Endpoints.WebsocketURL = o.API.Endpoints.WebsocketURL + err = o.Setup(okcoinConfig) + if err != nil { + log.Fatal("OKCoin setup error", err) + } testSetupRan = true o.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() o.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() + + os.Exit(m.Run()) } func areTestAPIKeysSet() bool { - if o.APIKey != "" && o.APIKey != "Key" && - o.APISecret != "" && o.APISecret != "Secret" { - return true - } - return false + return o.ValidateAPICredentials() } func testStandardErrorHandling(t *testing.T, err error) { @@ -102,14 +92,12 @@ func testStandardErrorHandling(t *testing.T, err error) { // TestGetAccountCurrencies API endpoint test func TestGetAccountCurrencies(t *testing.T) { - TestSetDefaults(t) _, err := o.GetAccountCurrencies() testStandardErrorHandling(t, err) } // TestGetAccountWalletInformation API endpoint test func TestGetAccountWalletInformation(t *testing.T) { - TestSetDefaults(t) resp, err := o.GetAccountWalletInformation("") if areTestAPIKeysSet() { if err != nil { @@ -125,7 +113,6 @@ func TestGetAccountWalletInformation(t *testing.T) { // TestGetAccountWalletInformationForCurrency API endpoint test func TestGetAccountWalletInformationForCurrency(t *testing.T) { - TestSetDefaults(t) resp, err := o.GetAccountWalletInformation(currency.BTC.String()) if areTestAPIKeysSet() { if err != nil { @@ -143,7 +130,7 @@ func TestGetAccountWalletInformationForCurrency(t *testing.T) { func TestTransferAccountFunds(t *testing.T) { TestSetRealOrderDefaults(t) request := okgroup.TransferAccountFundsRequest{ - Amount: 10, + Amount: -10, Currency: currency.BTC.String(), From: 6, To: 1, @@ -156,7 +143,7 @@ func TestTransferAccountFunds(t *testing.T) { func TestAccountWithdrawRequest(t *testing.T) { TestSetRealOrderDefaults(t) request := okgroup.AccountWithdrawRequest{ - Amount: 10, + Amount: -10, Currency: currency.BTC.String(), TradePwd: "1234", Destination: 4, @@ -169,7 +156,6 @@ func TestAccountWithdrawRequest(t *testing.T) { // TestGetAccountWithdrawalFee API endpoint test func TestGetAccountWithdrawalFee(t *testing.T) { - TestSetDefaults(t) resp, err := o.GetAccountWithdrawalFee("") if areTestAPIKeysSet() { if err != nil { @@ -185,7 +171,6 @@ func TestGetAccountWithdrawalFee(t *testing.T) { // TestGetWithdrawalFeeForCurrency API endpoint test func TestGetAccountWithdrawalFeeForCurrency(t *testing.T) { - TestSetDefaults(t) resp, err := o.GetAccountWithdrawalFee(currency.BTC.String()) if areTestAPIKeysSet() { if err != nil { @@ -201,63 +186,54 @@ func TestGetAccountWithdrawalFeeForCurrency(t *testing.T) { // TestGetAccountWithdrawalHistory API endpoint test func TestGetAccountWithdrawalHistory(t *testing.T) { - TestSetDefaults(t) _, err := o.GetAccountWithdrawalHistory("") testStandardErrorHandling(t, err) } // TestGetAccountWithdrawalHistoryForCurrency API endpoint test func TestGetAccountWithdrawalHistoryForCurrency(t *testing.T) { - TestSetDefaults(t) _, err := o.GetAccountWithdrawalHistory(currency.BTC.String()) testStandardErrorHandling(t, err) } // TestGetAccountBillDetails API endpoint test func TestGetAccountBillDetails(t *testing.T) { - TestSetDefaults(t) _, err := o.GetAccountBillDetails(okgroup.GetAccountBillDetailsRequest{}) testStandardErrorHandling(t, err) } // TestGetAccountDepositAddressForCurrency API endpoint test func TestGetAccountDepositAddressForCurrency(t *testing.T) { - TestSetDefaults(t) _, err := o.GetAccountDepositAddressForCurrency(currency.BTC.String()) testStandardErrorHandling(t, err) } // TestGetAccountDepositHistory API endpoint test func TestGetAccountDepositHistory(t *testing.T) { - TestSetDefaults(t) _, err := o.GetAccountDepositHistory("") testStandardErrorHandling(t, err) } // TestGetAccountDepositHistoryForCurrency API endpoint test func TestGetAccountDepositHistoryForCurrency(t *testing.T) { - TestSetDefaults(t) _, err := o.GetAccountDepositHistory(currency.BTC.String()) testStandardErrorHandling(t, err) } // TestGetSpotTradingAccounts API endpoint test func TestGetSpotTradingAccounts(t *testing.T) { - TestSetDefaults(t) _, err := o.GetSpotTradingAccounts() testStandardErrorHandling(t, err) } // TestGetSpotTradingAccountsForCurrency API endpoint test func TestGetSpotTradingAccountsForCurrency(t *testing.T) { - TestSetDefaults(t) _, err := o.GetSpotTradingAccountForCurrency(currency.BTC.String()) testStandardErrorHandling(t, err) } // TestGetSpotBillDetailsForCurrency API endpoint test func TestGetSpotBillDetailsForCurrency(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotBillDetailsForCurrencyRequest{ Currency: currency.BTC.String(), Limit: 100, @@ -268,7 +244,6 @@ func TestGetSpotBillDetailsForCurrency(t *testing.T) { // TestGetSpotBillDetailsForCurrencyBadLimit API logic test func TestGetSpotBillDetailsForCurrencyBadLimit(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotBillDetailsForCurrencyRequest{ Currency: currency.BTC.String(), Limit: -1, @@ -282,13 +257,12 @@ func TestGetSpotBillDetailsForCurrencyBadLimit(t *testing.T) { // TestPlaceSpotOrderLimit API endpoint test func TestPlaceSpotOrderLimit(t *testing.T) { TestSetRealOrderDefaults(t) - request := okgroup.PlaceSpotOrderRequest{ - InstrumentID: spotCurrency, - Type: "limit", - Side: "buy", - MarginTrading: "1", - Price: "100", - Size: "100", + request := okgroup.PlaceOrderRequest{ + InstrumentID: spotCurrency, + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), + Price: "-100", + Size: "100", } _, err := o.PlaceSpotOrder(&request) @@ -298,13 +272,12 @@ func TestPlaceSpotOrderLimit(t *testing.T) { // TestPlaceSpotOrderMarket API endpoint test func TestPlaceSpotOrderMarket(t *testing.T) { TestSetRealOrderDefaults(t) - request := okgroup.PlaceSpotOrderRequest{ - InstrumentID: spotCurrency, - Type: "market", - Side: "buy", - MarginTrading: "1", - Size: "100", - Notional: "100", + request := okgroup.PlaceOrderRequest{ + InstrumentID: spotCurrency, + Type: order.Market.Lower(), + Side: order.Buy.Lower(), + Size: "-100", + Notional: "100", } _, err := o.PlaceSpotOrder(&request) @@ -314,17 +287,16 @@ func TestPlaceSpotOrderMarket(t *testing.T) { // TestPlaceMultipleSpotOrders API endpoint test func TestPlaceMultipleSpotOrders(t *testing.T) { TestSetRealOrderDefaults(t) - order := okgroup.PlaceSpotOrderRequest{ - InstrumentID: spotCurrency, - Type: "market", - Side: "buy", - MarginTrading: "1", - Size: "100", - Notional: "100", + ord := okgroup.PlaceOrderRequest{ + InstrumentID: spotCurrency, + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), + Size: "-100", + Price: "1", } - request := []okgroup.PlaceSpotOrderRequest{ - order, + request := []okgroup.PlaceOrderRequest{ + ord, } _, errs := o.PlaceMultipleSpotOrders(request) @@ -335,22 +307,20 @@ func TestPlaceMultipleSpotOrders(t *testing.T) { // TestPlaceMultipleSpotOrdersOverCurrencyLimits API logic test func TestPlaceMultipleSpotOrdersOverCurrencyLimits(t *testing.T) { - TestSetDefaults(t) - order := okgroup.PlaceSpotOrderRequest{ - InstrumentID: spotCurrency, - Type: "market", - Side: "buy", - MarginTrading: "1", - Size: "100", - Notional: "100", + ord := okgroup.PlaceOrderRequest{ + InstrumentID: spotCurrency, + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), + Size: "-100", + Price: "1", } - request := []okgroup.PlaceSpotOrderRequest{ - order, - order, - order, - order, - order, + request := []okgroup.PlaceOrderRequest{ + ord, + ord, + ord, + ord, + ord, } _, errs := o.PlaceMultipleSpotOrders(request) @@ -361,28 +331,29 @@ func TestPlaceMultipleSpotOrdersOverCurrencyLimits(t *testing.T) { // TestPlaceMultipleSpotOrdersOverPairLimits API logic test func TestPlaceMultipleSpotOrdersOverPairLimits(t *testing.T) { - TestSetDefaults(t) - order := okgroup.PlaceSpotOrderRequest{ - InstrumentID: spotCurrency, - Type: "market", - Side: "buy", - MarginTrading: "1", - Size: "100", - Notional: "100", + ord := okgroup.PlaceOrderRequest{ + InstrumentID: spotCurrency, + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), + Size: "-100", + Price: "1", } - request := []okgroup.PlaceSpotOrderRequest{ - order, + request := []okgroup.PlaceOrderRequest{ + ord, } - order.InstrumentID = currency.NewPairWithDelimiter(currency.LTC.String(), currency.USD.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.DOGE.String(), currency.USD.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.XMR.String(), currency.USD.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.BCH.String(), currency.USD.String(), "-").Lower().String() - request = append(request, order) + pairs := currency.Pairs{ + currency.NewPair(currency.LTC, currency.USDT), + currency.NewPair(currency.ETH, currency.USDT), + currency.NewPair(currency.BCH, currency.USDT), + currency.NewPair(currency.XMR, currency.USDT), + } + + for x := range pairs { + ord.InstrumentID = pairs[x].Format("-", false).String() + request = append(request, ord) + } _, errs := o.PlaceMultipleSpotOrders(request) if errs[0].Error() != "up to 4 trading pairs" { @@ -437,7 +408,6 @@ func TestCancelMultipleSpotOrdersOverCurrencyLimits(t *testing.T) { // TestGetSpotOrders API endpoint test func TestGetSpotOrders(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotOrdersRequest{ InstrumentID: spotCurrency, Status: "all", @@ -448,7 +418,6 @@ func TestGetSpotOrders(t *testing.T) { // TestGetSpotOpenOrders API endpoint test func TestGetSpotOpenOrders(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotOpenOrdersRequest{} _, err := o.GetSpotOpenOrders(request) testStandardErrorHandling(t, err) @@ -456,7 +425,6 @@ func TestGetSpotOpenOrders(t *testing.T) { // TestGetSpotOrder API endpoint test func TestGetSpotOrder(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotOrderRequest{ OrderID: "-1234", InstrumentID: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-").Upper().String(), @@ -467,7 +435,6 @@ func TestGetSpotOrder(t *testing.T) { // TestGetSpotTransactionDetails API endpoint test func TestGetSpotTransactionDetails(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotTransactionDetailsRequest{ OrderID: 1234, InstrumentID: spotCurrency, @@ -478,28 +445,14 @@ func TestGetSpotTransactionDetails(t *testing.T) { // TestGetSpotTokenPairDetails API endpoint test func TestGetSpotTokenPairDetails(t *testing.T) { - TestSetDefaults(t) _, err := o.GetSpotTokenPairDetails() if err != nil { t.Error(err) } } -// TestGetSpotOrderBook API endpoint test -func TestGetSpotOrderBook(t *testing.T) { - TestSetDefaults(t) - request := okgroup.GetSpotOrderBookRequest{ - InstrumentID: spotCurrency, - } - _, err := o.GetSpotOrderBook(request) - if err != nil { - t.Error(err) - } -} - // TestGetSpotAllTokenPairsInformation API endpoint test func TestGetSpotAllTokenPairsInformation(t *testing.T) { - TestSetDefaults(t) _, err := o.GetSpotAllTokenPairsInformation() if err != nil { t.Error(err) @@ -508,7 +461,6 @@ func TestGetSpotAllTokenPairsInformation(t *testing.T) { // TestGetSpotAllTokenPairsInformationForCurrency API endpoint test func TestGetSpotAllTokenPairsInformationForCurrency(t *testing.T) { - TestSetDefaults(t) _, err := o.GetSpotAllTokenPairsInformationForCurrency(spotCurrency) if err != nil { t.Error(err) @@ -517,7 +469,6 @@ func TestGetSpotAllTokenPairsInformationForCurrency(t *testing.T) { // TestGetSpotFilledOrdersInformation API endpoint test func TestGetSpotFilledOrdersInformation(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotFilledOrdersInformationRequest{ InstrumentID: spotCurrency, } @@ -529,7 +480,6 @@ func TestGetSpotFilledOrdersInformation(t *testing.T) { // TestGetSpotMarketData API endpoint test func TestGetSpotMarketData(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotMarketDataRequest{ InstrumentID: spotCurrency, Granularity: 604800, @@ -542,21 +492,18 @@ func TestGetSpotMarketData(t *testing.T) { // TestGetMarginTradingAccounts API endpoint test func TestGetMarginTradingAccounts(t *testing.T) { - TestSetDefaults(t) _, err := o.GetMarginTradingAccounts() testStandardErrorHandling(t, err) } // TestGetMarginTradingAccountsForCurrency API endpoint test func TestGetMarginTradingAccountsForCurrency(t *testing.T) { - TestSetDefaults(t) _, err := o.GetMarginTradingAccountsForCurrency(spotCurrency) testStandardErrorHandling(t, err) } // TestGetMarginBillDetails API endpoint test func TestGetMarginBillDetails(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetMarginBillDetailsRequest{ InstrumentID: spotCurrency, Limit: 100, @@ -567,14 +514,12 @@ func TestGetMarginBillDetails(t *testing.T) { // TestGetMarginAccountSettings API endpoint test func TestGetMarginAccountSettings(t *testing.T) { - TestSetDefaults(t) _, err := o.GetMarginAccountSettings("") testStandardErrorHandling(t, err) } // TestGetMarginAccountSettingsForCurrency API endpoint test func TestGetMarginAccountSettingsForCurrency(t *testing.T) { - TestSetDefaults(t) _, err := o.GetMarginAccountSettings(spotCurrency) testStandardErrorHandling(t, err) } @@ -583,7 +528,7 @@ func TestGetMarginAccountSettingsForCurrency(t *testing.T) { func TestOpenMarginLoan(t *testing.T) { TestSetRealOrderDefaults(t) request := okgroup.OpenMarginLoanRequest{ - Amount: 100, + Amount: -100, InstrumentID: spotCurrency, QuoteCurrency: currency.USD.String(), } @@ -596,7 +541,7 @@ func TestOpenMarginLoan(t *testing.T) { func TestRepayMarginLoan(t *testing.T) { TestSetRealOrderDefaults(t) request := okgroup.RepayMarginLoanRequest{ - Amount: 100, + Amount: -100, InstrumentID: spotCurrency, QuoteCurrency: currency.USD.String(), BorrowID: 1, @@ -609,12 +554,12 @@ func TestRepayMarginLoan(t *testing.T) { // TestPlaceMarginOrderLimit API endpoint test func TestPlaceMarginOrderLimit(t *testing.T) { TestSetRealOrderDefaults(t) - request := okgroup.PlaceSpotOrderRequest{ + request := okgroup.PlaceOrderRequest{ InstrumentID: spotCurrency, - Type: "limit", - Side: "buy", + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), MarginTrading: "2", - Price: "100", + Price: "-100", Size: "100", } @@ -625,12 +570,12 @@ func TestPlaceMarginOrderLimit(t *testing.T) { // TestPlaceMarginOrderMarket API endpoint test func TestPlaceMarginOrderMarket(t *testing.T) { TestSetRealOrderDefaults(t) - request := okgroup.PlaceSpotOrderRequest{ + request := okgroup.PlaceOrderRequest{ InstrumentID: spotCurrency, - Type: "market", - Side: "buy", + Type: order.Market.Lower(), + Side: order.Buy.Lower(), MarginTrading: "2", - Size: "100", + Size: "-100", Notional: "100", } @@ -641,17 +586,17 @@ func TestPlaceMarginOrderMarket(t *testing.T) { // TestPlaceMultipleMarginOrders API endpoint test func TestPlaceMultipleMarginOrders(t *testing.T) { TestSetRealOrderDefaults(t) - order := okgroup.PlaceSpotOrderRequest{ + ord := okgroup.PlaceOrderRequest{ InstrumentID: spotCurrency, - Type: "market", - Side: "buy", + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), MarginTrading: "1", - Size: "100", + Size: "-100", Notional: "100", } - request := []okgroup.PlaceSpotOrderRequest{ - order, + request := []okgroup.PlaceOrderRequest{ + ord, } _, errs := o.PlaceMultipleMarginOrders(request) @@ -662,22 +607,21 @@ func TestPlaceMultipleMarginOrders(t *testing.T) { // TestPlaceMultipleMarginOrdersOverCurrencyLimits API logic test func TestPlaceMultipleMarginOrdersOverCurrencyLimits(t *testing.T) { - TestSetDefaults(t) - order := okgroup.PlaceSpotOrderRequest{ + ord := okgroup.PlaceOrderRequest{ InstrumentID: spotCurrency, - Type: "market", - Side: "buy", + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), MarginTrading: "1", - Size: "100", + Size: "-100", Notional: "100", } - request := []okgroup.PlaceSpotOrderRequest{ - order, - order, - order, - order, - order, + request := []okgroup.PlaceOrderRequest{ + ord, + ord, + ord, + ord, + ord, } _, errs := o.PlaceMultipleMarginOrders(request) @@ -688,28 +632,30 @@ func TestPlaceMultipleMarginOrdersOverCurrencyLimits(t *testing.T) { // TestPlaceMultipleMarginOrdersOverPairLimits API logic test func TestPlaceMultipleMarginOrdersOverPairLimits(t *testing.T) { - TestSetDefaults(t) - order := okgroup.PlaceSpotOrderRequest{ + ord := okgroup.PlaceOrderRequest{ InstrumentID: spotCurrency, - Type: "market", - Side: "buy", + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), MarginTrading: "1", - Size: "100", + Size: "-100", Notional: "100", } - request := []okgroup.PlaceSpotOrderRequest{ - order, + request := []okgroup.PlaceOrderRequest{ + ord, } - order.InstrumentID = currency.NewPairWithDelimiter(currency.LTC.String(), currency.USD.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.DOGE.String(), currency.USD.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.XMR.String(), currency.USD.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.BCH.String(), currency.USD.String(), "-").Lower().String() - request = append(request, order) + pairs := currency.Pairs{ + currency.NewPair(currency.LTC, currency.USDT), + currency.NewPair(currency.ETH, currency.USDT), + currency.NewPair(currency.BCH, currency.USDT), + currency.NewPair(currency.XMR, currency.USDT), + } + + for x := range pairs { + ord.InstrumentID = pairs[x].Format("-", false).String() + request = append(request, ord) + } _, errs := o.PlaceMultipleMarginOrders(request) if errs[0].Error() != "up to 4 trading pairs" { @@ -759,7 +705,6 @@ func TestCancelMultipleMarginOrdersOverCurrencyLimits(t *testing.T) { // TestGetMarginOrders API endpoint test func TestGetMarginOrders(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotOrdersRequest{ InstrumentID: spotCurrency, Status: "all", @@ -770,7 +715,6 @@ func TestGetMarginOrders(t *testing.T) { // TestGetMarginOpenOrders API endpoint test func TestGetMarginOpenOrders(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotOpenOrdersRequest{} _, err := o.GetMarginOpenOrders(request) testStandardErrorHandling(t, err) @@ -778,7 +722,6 @@ func TestGetMarginOpenOrders(t *testing.T) { // TestGetMarginOrder API endpoint test func TestGetMarginOrder(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotOrderRequest{ OrderID: "1234", InstrumentID: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-").Upper().String(), @@ -789,7 +732,6 @@ func TestGetMarginOrder(t *testing.T) { // TestGetMarginTransactionDetails API endpoint test func TestGetMarginTransactionDetails(t *testing.T) { - TestSetDefaults(t) request := okgroup.GetSpotTransactionDetailsRequest{ OrderID: 1234, InstrumentID: spotCurrency, @@ -804,8 +746,7 @@ func TestGetMarginTransactionDetails(t *testing.T) { // Attempts to subscribe to a channel that doesn't exist // Will log in if credentials are present func TestSendWsMessages(t *testing.T) { - TestSetDefaults(t) - if !o.Websocket.IsEnabled() && !o.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + if !o.Websocket.IsEnabled() && !o.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } var ok bool @@ -850,14 +791,13 @@ func TestSendWsMessages(t *testing.T) { func TestGetAssetTypeFromTableName(t *testing.T) { str := "spot/candle300s:BTC-USDT" spot := o.GetAssetTypeFromTableName(str) - if spot != orderbook.Spot { + if !strings.EqualFold(spot.String(), asset.Spot.String()) { t.Errorf("Error, expected 'SPOT', received: '%v'", spot) } } // TestGetWsChannelWithoutOrderType logic test func TestGetWsChannelWithoutOrderType(t *testing.T) { - TestSetDefaults(t) str := "spot/depth5:BTC-USDT" expected := "depth5" resp := o.GetWsChannelWithoutOrderType(str) @@ -879,14 +819,13 @@ func TestGetWsChannelWithoutOrderType(t *testing.T) { // TestOrderBookUpdateChecksumCalculator logic test func TestOrderBookUpdateChecksumCalculator(t *testing.T) { - TestSetDefaults(t) if !websocketEnabled { t.Skip("Websocket not enabled, skipping") } original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0.145",1],["3864.7682","0.005",1],["3864.9851","0.57",1],["3864.9852","0.30137754",1],["3864.9986","2.81818419",1],["3864.9995","0.002",1],["3865","0.0597",1],["3865.0309","0.4",1],["3865.1995","0.004",1],["3865.3995","0.004",1],["3865.5995","0.004",1],["3865.7995","0.004",1],["3865.9995","0.004",1],["3866.0961","0.25865886",1],["3866.1995","0.004",1],["3866.3995","0.004",1],["3866.4004","0.3243",2],["3866.5995","0.004",1],["3866.7633","0.44247086",1],["3866.7995","0.004",1],["3866.9197","0.511",1],["3867.256","0.51716256",1],["3867.3951","0.02588112",1],["3867.4014","0.025",1],["3867.4566","0.02499999",1],["3867.4675","4.01155057",5],["3867.5515","1.1",1],["3867.6113","0.009",1],["3867.7349","0.026",1],["3867.7781","0.03738652",1],["3867.9163","0.0521",1],["3868.0381","0.34354941",1],["3868.0436","0.051",1],["3868.0657","0.90552172",3],["3868.1819","0.03863346",1],["3868.2013","0.194",1],["3868.346","0.051",1],["3868.3863","0.01155",1],["3868.7716","0.009",1],["3868.947","0.025",1],["3868.98","0.001",1],["3869.0764","1.03487931",1],["3869.2773","0.07724578",1],["3869.4039","0.025",1],["3869.4068","1.03",1],["3869.7068","2.06976398",1],["3870","0.5",1],["3870.0465","0.01",1],["3870.7042","0.02099651",1],["3870.9451","2.07047375",1],["3871.5254","1.2",1],["3871.5596","0.001",1],["3871.6605","0.01035032",1],["3871.7179","2.07047375",1],["3871.8816","0.51751625",1],["3872.1","0.75",1],["3872.2464","0.0646",1],["3872.3747","0.283",1],["3872.4039","0.2",1],["3872.7655","0.23179307",1],["3872.8005","2.06976398",1],["3873.1509","2",1],["3873.3215","0.26",1],["3874.1392","0.001",1],["3874.1487","3.88224364",4],["3874.1685","1.8",1],["3874.5571","0.08974762",1],["3874.734","2.06976398",1],["3874.99","0.3",1],["3875","1.001",2],["3875.0041","1.03505051",1],["3875.45","0.3",1],["3875.4766","0.15",1],["3875.7057","0.51751625",1],["3876","0.001",1],["3876.68","0.3",1],["3876.7188","0.001",1],["3877","0.75",1],["3877.31","0.035",1],["3877.38","0.3",1],["3877.7","0.3",1],["3877.88","0.3",1],["3878.0364","0.34770122",1],["3878.4525","0.48579748",1],["3878.4955","0.02812511",1],["3878.8855","0.00258579",1],["3878.9605","0.895",1],["3879","0.001",1],["3879.2984","0.002",2],["3879.432","0.001",1],["3879.6313","6",1],["3879.9999","0.002",2],["3880","1.25132834",5],["3880.2526","0.04075162",1],["3880.7145","0.0647",1],["3881.2469","1.883",1],["3881.878","0.002",2],["3884.4576","0.002",2],["3885","0.002",2],["3885.2233","0.28304103",1],["3885.7416","18",1],["3886","0.001",1],["3886.1554","5.4",1],["3887","0.001",1],["3887.0372","0.002",2],["3887.2559","0.05214011",1],["3887.9238","0.0019",1],["3888","0.15810538",4],["3889","0.001",1],["3889.5175","0.50510653",1],["3889.6168","0.002",2],["3889.9999","0.001",1],["3890","2.34968109",4],["3890.5222","0.00257806",1],["3891.2659","5",1],["3891.9999","0.00893897",1],["3892.1964","0.002",2],["3892.4358","0.0176",1],["3893.1388","1.4279",1],["3894","0.0026321",1],["3894.776","0.001",1],["3895","1.501",2],["3895.379","0.25881288",1],["3897","0.05",1],["3897.3556","0.001",1],["3897.8432","0.73708079",1],["3898","3.31353018",7],["3898.4462","4.757",1],["3898.6","0.47159638",1],["3898.8769","0.0129",1],["3899","6",2],["3899.6516","0.025",1],["3899.9352","0.001",1],["3899.9999","0.013",2],["3900","22.37447743",24],["3900.9999","0.07763916",1],["3901","0.10192487",1],["3902.1937","0.00257034",1],["3902.3991","1.5532141",1],["3902.5148","0.001",1],["3904","1.49331984",1],["3904.9999","0.95905447",1],["3905","0.501",2],["3905.0944","0.001",1],["3905.61","0.099",1],["3905.6801","0.54343686",1],["3906.2901","0.0258",1],["3907.674","0.001",1],["3907.85","1.35778084",1],["3908","0.03846153",1],["3908.23","1.95189531",1],["3908.906","0.03148978",1],["3909","0.001",1],["3909.9999","0.01398721",2],["3910","0.016",2],["3910.2536","0.001",1],["3912.5406","0.88270517",1],["3912.8332","0.001",1],["3913","1.2640608",1],["3913.87","1.69114184",1],["3913.9003","0.00256266",1],["3914","1.21766411",1],["3915","0.001",1],["3915.4128","0.001",1],["3915.7425","6.848",1],["3916","0.0050949",1],["3917.36","1.28658296",1],["3917.9924","0.001",1],["3919","0.001",1],["3919.9999","0.001",1],["3920","1.21171832",3],["3920.0002","0.20217038",1],["3920.572","0.001",1],["3921","0.128",1],["3923.0756","0.00148064",1],["3923.1516","0.001",1],["3923.86","1.38831714",1],["3925","0.01867801",2],["3925.642","0.00255499",1],["3925.7312","0.001",1],["3926","0.04290757",1],["3927","0.023",1],["3927.3175","0.01212865",1],["3927.65","1.51375612",1],["3928","0.5",1],["3928.3108","0.001",1],["3929","0.001",1],["3929.9999","0.01519338",2],["3930","0.0174985",3],["3930.21","1.49335799",1],["3930.8904","0.001",1],["3932.2999","0.01953",1],["3932.8962","7.96",1],["3933.0387","11.808",1],["3933.47","0.001",1],["3934","1.40839932",1],["3935","0.001",1],["3936.8","0.62879518",1],["3937.23","1.56977841",1],["3937.4189","0.00254735",1]],"bids":[["3864.5217","0.00540709",1],["3864.5216","0.14068758",2],["3864.2275","0.01033576",1],["3864.0989","0.00825047",1],["3864.0273","0.38",1],["3864.0272","0.4",1],["3863.9957","0.01083539",1],["3863.9184","0.01653723",1],["3863.8282","0.25588165",1],["3863.8153","0.154",1],["3863.7791","1.14122492",1],["3863.6866","0.01733662",1],["3863.6093","0.02645958",1],["3863.3775","0.02773862",1],["3863.0297","0.513",1],["3863.0286","1.1028564",2],["3862.8489","0.01",1],["3862.5972","0.01890179",1],["3862.3431","0.01152944",1],["3862.313","0.009",1],["3862.2445","0.90551002",3],["3862.0734","0.014",1],["3862.0539","0.64976067",1],["3861.8586","0.025",1],["3861.7888","0.025",1],["3861.7673","0.008",1],["3861.5785","0.01",1],["3861.3895","0.005",1],["3861.3338","0.25875855",1],["3861.161","0.01",1],["3861.1111","0.03863352",1],["3861.0732","0.51703882",1],["3860.9116","0.17754895",1],["3860.75","0.19",1],["3860.6554","0.015",1],["3860.6172","0.005",1],["3860.6088","0.008",1],["3860.4724","0.12940042",1],["3860.4424","0.25880084",1],["3860.42","0.01",1],["3860.3725","0.51760102",1],["3859.8449","0.005",1],["3859.8285","0.03738652",1],["3859.7638","0.07726703",1],["3859.4502","0.008",1],["3859.3772","0.05173471",1],["3859.3409","0.194",1],["3859","5",1],["3858.827","0.0521",1],["3858.8208","0.001",1],["3858.679","0.26",1],["3858.4814","0.07477305",1],["3858.1669","1.03503422",1],["3857.6005","0.006",1],["3857.4005","0.004",1],["3857.2005","0.004",1],["3857.1871","1.218",1],["3857.0005","0.004",1],["3856.8135","0.0646",1],["3856.8005","0.004",1],["3856.2412","0.001",1],["3856.2349","1.03503422",1],["3856.0197","0.01037339",1],["3855.8781","0.23178117",1],["3855.8005","0.004",1],["3855.7165","0.00259355",1],["3855.4858","0.25875855",1],["3854.4584","0.01",1],["3853.6616","0.001",1],["3853.1373","0.92",1],["3852.5072","0.48599702",1],["3851.3926","0.13008333",1],["3851.082","0.001",1],["3850.9317","2",1],["3850.6359","0.34770165",1],["3850.2058","0.51751624",1],["3850.0823","0.15",1],["3850.0042","0.5175171",1],["3850","0.001",1],["3849.6325","1.8",1],["3849.41","0.3",1],["3848.9686","1.85",1],["3848.7426","0.18511466",1],["3848.52","0.3",1],["3848.5024","0.001",1],["3848.42","0.3",1],["3848.1618","2.204",1],["3847.77","0.3",1],["3847.48","0.3",1],["3847.3581","2.05",1],["3846.8259","0.0646",1],["3846.59","0.3",1],["3846.49","0.3",1],["3845.9228","0.001",1],["3844.184","0.00260133",1],["3844.0092","6.3",1],["3843.3432","0.001",1],["3841","0.06300963",1],["3840.7636","0.001",1],["3840","0.201",3],["3839.7681","18",1],["3839.5328","0.05214011",1],["3838.184","0.001",1],["3837.2344","0.27589557",1],["3836.6479","5.2",1],["3836","2.37196773",3],["3835.6044","0.001",1],["3833.6053","0.25873556",1],["3833.0248","0.001",1],["3833","0.8726502",1],["3832.6859","0.00260913",1],["3832","0.007",1],["3831.637","6",1],["3831.0602","0.001",1],["3830.4452","0.001",1],["3830","0.20375718",4],["3829.7125","0.07833486",1],["3829.6283","0.3519681",1],["3829","0.0039261",1],["3827.8656","0.001",1],["3826.0001","0.53251232",1],["3826","0.0509",1],["3825.7834","0.00698562",1],["3825.286","0.001",1],["3823.0001","0.03010127",1],["3822.8014","0.00261588",1],["3822.7064","0.001",1],["3822.2","1",1],["3822.1121","0.35994101",1],["3821.2222","0.00261696",1],["3821","0.001",1],["3820.1268","0.001",1],["3820","1.12992803",4],["3819","0.01331195",2],["3817.5472","0.001",1],["3816","1.13807184",2],["3815.8343","0.32463428",1],["3815.7834","0.00525295",1],["3815","28.99386799",4],["3814.9676","0.001",1],["3813","0.91303023",4],["3812.388","0.002",2],["3811.2257","0.07",1],["3810","0.32573997",2],["3809.8084","0.001",1],["3809.7928","0.00262481",1],["3807.2288","0.001",1],["3806.8421","0.07003461",1],["3806","0.19",1],["3805.8041","0.05678805",1],["3805","1.01",2],["3804.6492","0.001",1],["3804.3551","0.1",1],["3803","0.005",1],["3802.22","2.05042631",1],["3802.0696","0.001",1],["3802","1.63290092",1],["3801.2257","0.07",1],["3801","57.4",3],["3800.9853","0.02492278",1],["3800.8421","0.06503533",1],["3800.7844","0.02812628",1],["3800.0001","0.00409473",1],["3800","17.91401074",15],["3799.49","0.001",1],["3799","0.1",1],["3796.9104","0.001",1],["3796","9.00128053",2],["3795.5441","0.0028",1],["3794.3308","0.001",1],["3791","55",1],["3790.7777","0.07",1],["3790","12.03238184",7],["3789","1",1],["3788","0.21110454",2],["3787.2959","9",1],["3786.592","0.001",1],["3786","9.01916822",2],["3785","12.87914268",5],["3784.0124","0.001",1],["3781.4328","0.002",2],["3781","56.3",2],["3780.7777","0.07",1],["3780","23.41537654",10],["3778.8532","0.002",2],["3776","9",1],["3774","0.003",1],["3772.2481","0.06901672",1],["3771","55.1",2],["3770.7777","0.07",1],["3770","7.30268416",5],["3769","0.25",1],["3768","1.3725",3],["3766.66","0.02",1],["3766","7.64837924",2],["3765.58","1.22775492",1],["3762.58","1.22873383",1],["3761","51.68262164",1],["3760.8031","0.0399",1],["3760.7777","0.07",1]],"timestamp":"2019-03-06T23:19:17.705Z","checksum":-1785549915}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0",0],["3864.9852","0",0],["3865.9994","0.48402971",1],["3866.4004","0.001",1],["3866.7995","0.3273",2],["3867.4566","0",0],["3867.7031","0.025",1],["3868.0436","0",0],["3868.346","0",0],["3868.3695","0.051",1],["3870.9243","0.642",1],["3874.9942","0.51751796",1],["3875.7057","0",0],["3939","0.001",1]],"bids":[["3864.55","0.0565449",1],["3863.8282","0",0],["3863.8153","0",0],["3863.7898","0.01320077",1],["3863.4807","0.02112123",1],["3863.3002","0.04233533",1],["3863.1717","0.03379397",1],["3863.0685","0.04438179",1],["3863.0286","0.7362564",1],["3862.9912","0.06773651",1],["3862.8626","0.05407035",1],["3862.7595","0.07101087",1],["3862.313","0.3756",2],["3862.1848","0.012",1],["3862.0734","0",0],["3861.8391","0.025",1],["3861.7888","0",0],["3856.6716","0.38893641",1],["3768","0",0],["3766.66","0",0],["3766","0",0],["3765.58","0",0],["3762.58","0",0],["3761","0",0],["3760.8031","0",0],["3760.7777","0",0]],"timestamp":"2019-03-06T23:19:18.239Z","checksum":-1587788848}]}` var dataResponse okgroup.WebsocketDataResponse - err := common.JSONDecode([]byte(original), &dataResponse) + err := json.Unmarshal([]byte(original), &dataResponse) if err != nil { t.Error(err) } @@ -896,7 +835,7 @@ func TestOrderBookUpdateChecksumCalculator(t *testing.T) { return } var updateResponse okgroup.WebsocketDataResponse - err = common.JSONDecode([]byte(update), &updateResponse) + err = json.Unmarshal([]byte(update), &updateResponse) if err != nil { t.Error(err) } @@ -909,14 +848,13 @@ func TestOrderBookUpdateChecksumCalculator(t *testing.T) { // TestOrderBookUpdateChecksumCalculatorWithDash logic test func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { - TestSetDefaults(t) if !websocketEnabled { t.Skip("Websocket not enabled, skipping") } original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000714","1.15414979",1],["0.000715","3.3",2],["0.000717","426.71348",2],["0.000719","140.84507042",1],["0.00072","590.77",1],["0.000721","991.77",1],["0.000724","0.3532032",1],["0.000725","58.82698567",1],["0.000726","1033.15469748",2],["0.000729","0.35320321",1],["0.00073","352.77",1],["0.000735","0.38469748",1],["0.000736","625.77",1],["0.00075191","152.44796961",1],["0.00075192","114.3359772",1],["0.00075193","85.7519829",1],["0.00075194","64.31398718",1],["0.00075195","48.23549038",1],["0.00075196","36.17661779",1],["0.00075199","61.04804253",1],["0.0007591","70.71318474",1],["0.0007621","53.03488855",1],["0.00076211","39.77616642",1],["0.00076212","29.83212481",1],["0.0007635","22.37409361",1],["0.00076351","29.36599786",2],["0.00076352","9.43907074",1],["0.00076353","7.07930306",1],["0.00076354","14.15860612",1],["0.00076355","3.53965153",1],["0.00076369","3.53965153",1],["0.0008","34.36841101",1],["0.00082858","1.69936503",1],["0.00083232","2.8",1],["0.00084","15.69220129",1],["0.00085","4.42785042",1],["0.00088","0.1",1],["0.000891","0.1",1],["0.0009","12.41486491",2],["0.00093","5",1],["0.0012","12.31486492",1],["0.00531314","6.91803114",1],["0.00799999","0.02",1],["0.0084","0.05989",1],["0.00931314","5.18852336",1],["0.0799999","0.02",1],["0.499","6.00423396",1],["0.5","0.4995",1],["0.799999","0.02",1],["4.99","2",1],["5","3.98583144",1],["7.99999999","0.02",1],["79.99999999","0.02",1],["799.99999999","0.02986704",1]],"bids":[["0.000709","222.91679881",3],["0.000703","0.47161952",1],["0.000701","140.73015789",2],["0.0007","0.3",1],["0.000699","401",1],["0.000698","232.61801667",2],["0.000689","0.71396896",1],["0.000688","0.69910125",1],["0.000613","227.54771052",1],["0.0005","0.01",1],["0.00026789","3.69905341",1],["0.000238","2.4",1],["0.00022","0.53",1],["0.0000055","374.09871696",1],["0.00000056","222",1],["0.00000055","736.84761363",1],["0.0000002","999",1],["0.00000009","1222.22222417",1],["0.00000008","20868.64520447",1],["0.00000002","110000",1],["0.00000001","10000",1]],"timestamp":"2019-03-12T22:22:42.274Z","checksum":1319037905}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000715","100.48199596",3],["0.000716","62.21679881",1]],"bids":[["0.000713","38.95772168",1]],"timestamp":"2019-03-12T22:22:42.938Z","checksum":-131160897}]}` var dataResponse okgroup.WebsocketDataResponse - err := common.JSONDecode([]byte(original), &dataResponse) + err := json.Unmarshal([]byte(original), &dataResponse) if err != nil { t.Error(err) } @@ -926,7 +864,7 @@ func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { return } var updateResponse okgroup.WebsocketDataResponse - err = common.JSONDecode([]byte(update), &updateResponse) + err = json.Unmarshal([]byte(update), &updateResponse) if err != nil { t.Error(err) } @@ -941,7 +879,7 @@ func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { func TestOrderBookPartialChecksumCalculator(t *testing.T) { orderbookPartialJSON := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"EOS-USDT","asks":[["3.5196","0.1077",1],["3.5198","21.71",1],["3.5199","51.1805",1],["3.5208","75.09",1],["3.521","196.3333",1],["3.5213","0.1",1],["3.5218","39.276",2],["3.5219","395.6334",1],["3.522","27.956",1],["3.5222","404.9595",1],["3.5225","300",1],["3.5227","143.5442",2],["3.523","42.4746",1],["3.5231","852.64",2],["3.5235","34.9602",1],["3.5237","442.0918",2],["3.5238","352.8404",2],["3.5239","341.6759",2],["3.524","84.9493",1],["3.5241","148.4882",1],["3.5242","261.64",1],["3.5243","142.045",1],["3.5246","10",1],["3.5247","284.0788",1],["3.5248","720",1],["3.5249","89.2518",2],["3.5251","1201.8965",2],["3.5254","426.2938",1],["3.5255","213.0863",1],["3.5257","568.1576",1],["3.5258","0.3",1],["3.5259","34.4602",1],["3.526","0.1",1],["3.5263","850.771",1],["3.5265","5.9",1],["3.5268","10.5064",2],["3.5272","1136.8965",1],["3.5274","255.1481",1],["3.5276","29.5374",1],["3.5278","50",1],["3.5282","284.1797",1],["3.5283","1136.8965",1],["3.5284","0.4275",1],["3.5285","100",1],["3.5292","90.9",1],["3.5298","0.2",1],["3.5303","568.1576",1],["3.5305","279.9999",1],["3.532","0.409",1],["3.5321","568.1576",1],["3.5326","6016.8756",1],["3.5328","4.9849",1],["3.533","92.88",2],["3.5343","1200.2383",2],["3.5344","100",1],["3.535","359.7047",1],["3.5354","100",1],["3.5355","100",1],["3.5356","10",1],["3.5358","200",2],["3.5362","435.139",1],["3.5365","2152",1],["3.5366","284.1756",1],["3.5367","568.4644",1],["3.5369","33.9878",1],["3.537","337.1191",2],["3.5373","0.4045",1],["3.5383","1136.7188",1],["3.5386","12.1614",1],["3.5387","90.89",1],["3.54","4.54",1],["3.5423","90.8",1],["3.5436","0.1",1],["3.5454","853.4156",1],["3.5468","142.0656",1],["3.5491","0.0008",1],["3.55","14478.8206",6],["3.5537","21521",1],["3.5555","11.53",1],["3.5573","50.6001",1],["3.5599","4591.4221",1],["3.56","1227.0002",4],["3.5603","2670",1],["3.5608","58.6638",1],["3.5613","0.1",1],["3.5621","45.9473",1],["3.57","2141.7274",3],["3.5712","2956.9816",1],["3.5717","27.9978",1],["3.5718","0.9285",1],["3.5739","299.73",1],["3.5761","864",1],["3.579","22.5225",1],["3.5791","38.26",2],["3.58","7618.4634",5],["3.5801","457.2184",1],["3.582","24.5",1],["3.5822","1572.6425",1],["3.5845","14.1438",1],["3.585","527.169",1],["3.5865","20",1],["3.5867","4490",1],["3.5876","39.0493",1],["3.5879","392.9083",1],["3.5888","436.42",2],["3.5896","50",1],["3.59","2608.9128",8],["3.5913","19.5246",1],["3.5938","7082",1],["3.597","0.1",1],["3.5979","399",1],["3.5995","315.1509",1],["3.5999","2566.2648",1],["3.6","18511.2292",35],["3.603","22.3379",2],["3.605","499.5",1],["3.6055","100",1],["3.6058","499.5",1],["3.608","1021.1485",1],["3.61","11755.4596",13],["3.611","42.8571",1],["3.6131","6690",1],["3.6157","19.5247",1],["3.618","2500",1],["3.6197","525.7146",1],["3.6198","0.4455",1],["3.62","6440.6295",8],["3.6219","0.4175",1],["3.6237","168",1],["3.6265","0.1001",1],["3.628","64.9345",1],["3.63","4435.4985",6],["3.6308","1.7815",1],["3.6331","0.1",1],["3.6338","355.527",2],["3.6358","50",1],["3.6363","2074.7096",1],["3.6376","4000",1],["3.6396","11090",1],["3.6399","0.4055",1],["3.64","4161.9805",4],["3.6437","117.6524",1],["3.648","190",1],["3.6488","200",1],["3.65","11740.5045",25],["3.6512","0.1",1],["3.6521","728",1],["3.6555","100",1],["3.6598","36.6914",1],["3.66","4331.2148",6],["3.6638","200",1],["3.6673","100",1],["3.6679","38",1],["3.6688","2",1],["3.6695","0.1",1],["3.67","7984.698",6],["3.672","300",1],["3.6777","257.8247",1],["3.6789","393.4217",2],["3.68","9202.3222",11],["3.6818","500",1],["3.6823","299.7",1],["3.6839","422.3748",1],["3.685","100",1],["3.6878","0.1",1],["3.6888","72.0958",2],["3.6889","2876",1],["3.689","28",1],["3.6891","28",1],["3.6892","28",1],["3.6895","28",1],["3.6898","28",1],["3.69","643.96",7],["3.6908","118",2],["3.691","28",1],["3.6916","28",1],["3.6918","28",1],["3.6926","28",1],["3.6928","28",1],["3.6932","28",1],["3.6933","200",1],["3.6935","28",1],["3.6936","28",1],["3.6938","28",1],["3.694","28",1],["3.698","1498.5",1],["3.6988","2014.2004",2],["3.7","21904.2689",22],["3.7029","71.95",1],["3.704","3690.1362",1],["3.7055","100",1],["3.7063","0.1",1],["3.71","4421.3468",4],["3.719","17.3491",1],["3.72","1304.5995",3],["3.7211","10",1],["3.7248","0.1",1],["3.725","1900",1],["3.73","31.1785",2],["3.7375","38",1]],"bids":[["3.5182","151.5343",6],["3.5181","0.3691",1],["3.518","271.3967",2],["3.5179","257.8352",1],["3.5178","12.3811",1],["3.5173","34.1921",2],["3.5171","1013.8256",2],["3.517","272.1119",2],["3.5168","395.3376",1],["3.5166","317.1756",2],["3.5165","348.302",3],["3.5164","142.0414",1],["3.5163","96.8933",2],["3.516","600.1034",3],["3.5159","27.481",1],["3.5158","27.33",1],["3.5157","583.1898",2],["3.5156","24.6819",2],["3.5154","25",1],["3.5153","0.429",1],["3.5152","453.9204",3],["3.5151","2131.592",4],["3.515","335",3],["3.5149","37.1586",1],["3.5147","41.6759",1],["3.5146","54.569",1],["3.5145","70.3515",1],["3.5143","68.206",3],["3.5142","359.4538",2],["3.5139","45.4123",2],["3.5137","71.673",2],["3.5136","25",1],["3.5135","300",1],["3.5134","442.57",2],["3.5132","83.3518",1],["3.513","1245.2529",3],["3.5127","20",1],["3.512","284.1353",1],["3.5119","1136.8319",1],["3.5113","56.9351",1],["3.5111","588.1898",2],["3.5109","255.0946",1],["3.5105","48.65",1],["3.5103","50.2",1],["3.5098","720",1],["3.5096","148.95",1],["3.5094","570.5758",2],["3.509","2.386",1],["3.5089","0.4065",1],["3.5087","282.3859",2],["3.5086","145.036",2],["3.5084","2.386",1],["3.5082","90.98",1],["3.5081","2.386",1],["3.5079","2.386",1],["3.5078","857.6229",2],["3.5075","2.386",1],["3.5074","284.1877",1],["3.5073","100",1],["3.5071","100",1],["3.507","768.4159",3],["3.5069","313.0863",2],["3.5068","426.2938",1],["3.5066","568.3594",1],["3.5063","1136.6865",1],["3.5059","0.3",1],["3.5054","9.9999",1],["3.5053","0.2",1],["3.5051","392.428",1],["3.505","13.79",1],["3.5048","99.5497",2],["3.5047","78.5331",2],["3.5046","2153",1],["3.5041","5983.999",1],["3.5037","668.5682",1],["3.5036","160.5948",1],["3.5024","534.8075",1],["3.5014","28.5604",1],["3.5011","91",1],["3.5","1058.8771",2],["3.4997","50.2",1],["3.4985","3430.0414",1],["3.4949","232.0591",1],["3.4942","21521",1],["3.493","2",1],["3.4928","2",1],["3.4925","0.44",1],["3.4917","142.0656",1],["3.49","2051.8826",4],["3.488","280.7459",1],["3.4852","643.4038",1],["3.4851","86.0807",1],["3.485","213.2436",1],["3.484","0.1",1],["3.4811","144.3399",1],["3.4808","89",1],["3.4803","12.1999",1],["3.4801","2390",1],["3.48","930.8453",9],["3.4791","310",1],["3.4768","206",1],["3.4767","0.9415",1],["3.4754","1.4387",1],["3.4728","20",1],["3.4701","1219.2873",1],["3.47","1904.3139",7],["3.468","0.4035",1],["3.4667","0.1",1],["3.4666","3020.0101",1],["3.465","10",1],["3.464","0.4485",1],["3.462","2119.6556",1],["3.46","1305.6113",8],["3.4589","8.0228",1],["3.457","100",1],["3.456","70.3859",2],["3.4538","20",1],["3.4536","4323.9486",2],["3.4531","827.0427",1],["3.4528","0.439",1],["3.4522","8.0381",1],["3.4513","441.1873",1],["3.4512","50.707",1],["3.451","87.0902",1],["3.4509","200",1],["3.4506","100",1],["3.4505","86.4045",2],["3.45","12409.4595",28],["3.4494","0.5365",2],["3.449","10761",1],["3.4482","8.0476",1],["3.4469","0.449",1],["3.445","2000",1],["3.4427","14",1],["3.4421","100",1],["3.4416","8.0631",1],["3.4404","1",1],["3.44","4580.733",11],["3.4388","1868.2085",1],["3.438","937.7246",2],["3.4367","1500",1],["3.4366","62",1],["3.436","29.8743",1],["3.4356","25.4801",1],["3.4349","4.3086",1],["3.4343","43.2402",1],["3.433","2.0688",1],["3.4322","2.7335",2],["3.432","93.3233",1],["3.4302","328.8301",2],["3.43","4440.8158",11],["3.4288","754.574",2],["3.4283","125.7043",2],["3.428","744.3154",2],["3.4273","5460",1],["3.4258","50",1],["3.4255","109.005",1],["3.4248","100",1],["3.4241","129.2048",2],["3.4233","5.3598",1],["3.4228","4498.866",1],["3.4222","3.5435",1],["3.4217","404.3252",2],["3.4211","1000",1],["3.4208","31",1],["3.42","1834.024",9],["3.4175","300",1],["3.4162","400",1],["3.4152","0.1",1],["3.4151","4.3336",1],["3.415","1.5974",1],["3.414","1146",1],["3.4134","306.4246",1],["3.4129","7.5556",1],["3.4111","198.5188",1],["3.4109","500",1],["3.4106","4305",1],["3.41","2150.7635",13],["3.4085","4.342",1],["3.4054","5.6985",1],["3.4019","5.438",1],["3.4015","1010.846",1],["3.4009","8610",1],["3.4005","1.9122",1],["3.4004","1",1],["3.4","27081.1806",67],["3.3955","3.2682",1],["3.3953","5.4486",1],["3.3937","1591.3805",1],["3.39","3221.4155",8],["3.3899","3.2736",1],["3.3888","1500",2],["3.3887","5.4592",1],["3.385","117.0969",2],["3.3821","5.4699",1],["3.382","100.0529",1],["3.3818","172.0164",1],["3.3815","165.6288",1],["3.381","887.3115",1],["3.3808","100",1]],"timestamp":"2019-03-04T00:15:04.155Z","checksum":-2036653089}]}` var dataResponse okgroup.WebsocketDataResponse - err := common.JSONDecode([]byte(orderbookPartialJSON), &dataResponse) + err := json.Unmarshal([]byte(orderbookPartialJSON), &dataResponse) if err != nil { t.Error(err) } @@ -971,7 +909,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() o.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -983,47 +921,46 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - TestSetDefaults(t) var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic if resp, err := o.GetFee(feeBuilder); resp != float64(0.0015) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) } // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := o.GetFee(feeBuilder); resp != float64(1500) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(1500), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(1500), resp) t.Error(err) } // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := o.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) t.Error(err) } // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } // CyptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } // InternationalBankWithdrawalFee Basic @@ -1031,14 +968,13 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } // TestFormatWithdrawPermissions helper test func TestFormatWithdrawPermissions(t *testing.T) { - TestSetDefaults(t) expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.NoFiatWithdrawalsText withdrawPermissions := o.FormatWithdrawPermissions() if withdrawPermissions != expectedResult { @@ -1051,12 +987,18 @@ func TestFormatWithdrawPermissions(t *testing.T) { // TestSubmitOrder Wrapper test func TestSubmitOrder(t *testing.T) { TestSetRealOrderDefaults(t) - var p = currency.Pair{ - Delimiter: "", - Base: currency.BTC, - Quote: currency.EUR, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: -1, + Amount: 1, + ClientID: "meowOrder", } - response, err := o.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 10, "hi") + response, err := o.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -1068,7 +1010,7 @@ func TestSubmitOrder(t *testing.T) { func TestCancelExchangeOrder(t *testing.T) { TestSetRealOrderDefaults(t) currencyPair := currency.NewPair(currency.LTC, currency.BTC) - var orderCancellation = exchange.OrderCancellation{ + var orderCancellation = order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -1083,7 +1025,7 @@ func TestCancelExchangeOrder(t *testing.T) { func TestCancelAllExchangeOrders(t *testing.T) { TestSetRealOrderDefaults(t) currencyPair := currency.NewPair(currency.LTC, currency.BTC) - var orderCancellation = exchange.OrderCancellation{ + var orderCancellation = order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -1092,8 +1034,8 @@ func TestCancelAllExchangeOrders(t *testing.T) { resp, err := o.CancelAllOrders(&orderCancellation) testStandardErrorHandling(t, err) - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } @@ -1106,7 +1048,7 @@ func TestGetAccountInfo(t *testing.T) { // TestModifyOrder Wrapper test func TestModifyOrder(t *testing.T) { TestSetRealOrderDefaults(t) - _, err := o.ModifyOrder(&exchange.ModifyOrder{}) + _, err := o.ModifyOrder(&order.Modify{}) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) } @@ -1115,14 +1057,18 @@ func TestModifyOrder(t *testing.T) { // TestWithdraw Wrapper test func TestWithdraw(t *testing.T) { TestSetRealOrderDefaults(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", - TradePassword: "Password", - FeeAmount: 1, + + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + TradePassword: "Password", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", + FeeAmount: 1, } + _, err := o.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) testStandardErrorHandling(t, err) } @@ -1130,7 +1076,7 @@ func TestWithdraw(t *testing.T) { // TestWithdrawFiat Wrapper test func TestWithdrawFiat(t *testing.T) { TestSetRealOrderDefaults(t) - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := o.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -1140,9 +1086,31 @@ func TestWithdrawFiat(t *testing.T) { // TestSubmitOrder Wrapper test func TestWithdrawInternationalBank(t *testing.T) { TestSetRealOrderDefaults(t) - var withdrawFiatRequest = exchange.WithdrawRequest{} + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := o.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) } } + +// TestGetOrderbook logic test +func TestGetOrderbook(t *testing.T) { + t.Parallel() + _, err := o.GetOrderBook(okgroup.GetOrderBookRequest{InstrumentID: "BTC-USDT"}, + asset.Spot) + if err != nil { + t.Error(err) + } + + _, err = o.GetOrderBook(okgroup.GetOrderBookRequest{InstrumentID: "Payload"}, + asset.Futures) + if err == nil { + t.Error("error cannot be nil") + } + + _, err = o.GetOrderBook(okgroup.GetOrderBookRequest{InstrumentID: "BTC-USD-SWAP"}, + asset.PerpetualSwap) + if err == nil { + t.Error("error cannot be nil") + } +} diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go new file mode 100644 index 00000000..eb75893d --- /dev/null +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -0,0 +1,260 @@ +package okcoin + +import ( + "fmt" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// GetDefaultConfig returns a default exchange config +func (o *OKCoin) GetDefaultConfig() (*config.ExchangeConfig, error) { + o.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = o.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = o.BaseCurrencies + + err := o.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if o.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = o.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults method assignes the default values for OKEX +func (o *OKCoin) SetDefaults() { + o.SetErrorDefaults() + o.SetCheckVarDefaults() + o.Name = okCoinExchangeName + o.Enabled = true + o.Verbose = true + + o.API.CredentialsValidator.RequiresKey = true + o.API.CredentialsValidator.RequiresSecret = true + o.API.CredentialsValidator.RequiresClientID = true + + o.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + asset.Margin, + }, + + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "-", + }, + + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "-", + }, + } + + o.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + CancelOrders: true, + SubmitOrder: true, + SubmitOrders: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + KlineFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + MessageCorrelation: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + o.Requester = request.New(o.Name, + request.NewRateLimit(time.Second, okCoinAuthRate), + request.NewRateLimit(time.Second, okCoinUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), + ) + + o.API.Endpoints.URLDefault = okCoinAPIURL + o.API.Endpoints.URL = okCoinAPIURL + o.API.Endpoints.WebsocketURL = okCoinWebsocketURL + o.APIVersion = okCoinAPIVersion + o.Websocket = wshandler.New() + o.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + o.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + o.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Start starts the OKGroup go routine +func (o *OKCoin) Start(wg *sync.WaitGroup) { + wg.Add(1) + go func() { + o.Run() + wg.Done() + }() +} + +// Run implements the OKEX wrapper +func (o *OKCoin) Run() { + if o.Verbose { + log.Debugf(log.ExchangeSys, + "%s Websocket: %s. (url: %s).\n", + o.Name, + common.IsEnabled(o.Websocket.IsEnabled()), + o.WebsocketURL) + } + + forceUpdate := false + delim := o.GetPairFormat(asset.Spot, false).Delimiter + if !common.StringDataContains(o.CurrencyPairs.GetPairs(asset.Spot, + true).Strings(), delim) || + !common.StringDataContains(o.CurrencyPairs.GetPairs(asset.Spot, + false).Strings(), delim) { + enabledPairs := currency.NewPairsFromStrings([]string{ + fmt.Sprintf("BTC%sUSD", delim), + }) + log.Warnf(log.ExchangeSys, + "Enabled pairs for %v reset due to config upgrade, please enable the ones you would like again.\n", + o.Name) + forceUpdate = true + + err := o.UpdatePairs(enabledPairs, asset.Spot, true, true) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update currencies.\n", + o.Name) + return + } + } + + if !o.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { + return + } + + err := o.UpdateTradablePairs(forceUpdate) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + o.Name, + err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (o *OKCoin) FetchTradablePairs(asset asset.Item) ([]string, error) { + prods, err := o.GetSpotTokenPairDetails() + if err != nil { + return nil, err + } + + var pairs []string + for x := range prods { + pairs = append(pairs, prods[x].BaseCurrency+ + o.GetPairFormat(asset, false).Delimiter+ + prods[x].QuoteCurrency) + } + + return pairs, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (o *OKCoin) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := o.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return o.UpdatePairs(currency.NewPairsFromStrings(pairs), + asset.Spot, false, forceUpdate) +} + +// UpdateTicker updates and returns the ticker for a currency pair +func (o *OKCoin) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + var tickerData ticker.Price + if assetType == asset.Spot { + resp, err := o.GetSpotAllTokenPairsInformation() + if err != nil { + return tickerData, err + } + pairs := o.GetEnabledPairs(assetType) + for i := range pairs { + for j := range resp { + if !pairs[i].Equal(resp[j].InstrumentID) { + continue + } + tickerData = ticker.Price{ + Last: resp[j].Last, + High: resp[j].High24h, + Low: resp[j].Low24h, + Bid: resp[j].BestBid, + Ask: resp[j].BestAsk, + Volume: resp[j].BaseVolume24h, + QuoteVolume: resp[j].QuoteVolume24h, + Open: resp[j].Open24h, + Pair: pairs[i], + LastUpdated: resp[j].Timestamp, + } + err = ticker.ProcessTicker(o.Name, &tickerData, assetType) + if err != nil { + log.Error(log.Ticker, err) + } + } + } + } + return ticker.GetTicker(o.Name, p, assetType) +} + +// FetchTicker returns the ticker for a currency pair +func (o *OKCoin) FetchTicker(p currency.Pair, assetType asset.Item) (tickerData ticker.Price, err error) { + tickerData, err = ticker.GetTicker(o.Name, p, assetType) + if err != nil { + return o.UpdateTicker(p, assetType) + } + return +} diff --git a/exchanges/okex/README.md b/exchanges/okex/README.md index 478280dd..5a6715db 100644 --- a/exchanges/okex/README.md +++ b/exchanges/okex/README.md @@ -47,22 +47,22 @@ main.go ```go var o exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "OKex" { - y = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "OKex" { + y = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := o.GetTickerPrice() +tick, err := o.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := o.GetOrderbookEx() +ob, err := o.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +130,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/okex/okex.go b/exchanges/okex/okex.go index 45418360..51a4d2bc 100644 --- a/exchanges/okex/okex.go +++ b/exchanges/okex/okex.go @@ -3,15 +3,9 @@ package okex import ( "fmt" "net/http" - "strconv" - "time" "github.com/thrasher-corp/gocryptotrader/common" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/okgroup" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) const ( @@ -22,7 +16,7 @@ const ( okExAPIVersion = "/v3/" okExExchangeName = "OKEX" // OkExWebsocketURL WebsocketURL - OkExWebsocketURL = "wss://real.okex.com:10442/ws/v3" + OkExWebsocketURL = "wss://real.okex.com:8443/ws/v3" // API subsections okGroupFuturesSubsection = "futures" okGroupSwapSubsection = "swap" @@ -51,45 +45,6 @@ type OKEX struct { okgroup.OKGroup } -// SetDefaults method assignes the default values for OKEX -func (o *OKEX) SetDefaults() { - o.SetErrorDefaults() - o.SetCheckVarDefaults() - o.Name = okExExchangeName - o.Enabled = false - o.Verbose = false - o.RESTPollingDelay = 10 - o.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | - exchange.NoFiatWithdrawals - o.RequestCurrencyPairFormat.Delimiter = "_" - o.RequestCurrencyPairFormat.Uppercase = false - o.ConfigCurrencyPairFormat.Delimiter = "_" - o.ConfigCurrencyPairFormat.Uppercase = true - o.SupportsAutoPairUpdating = true - o.SupportsRESTTickerBatching = false - o.Requester = request.New(o.Name, - request.NewRateLimit(time.Second, okExAuthRate), - request.NewRateLimit(time.Second, okExUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - o.APIUrlDefault = okExAPIURL - o.APIUrl = okExAPIURL - o.AssetTypes = []string{ticker.Spot} - o.Websocket = wshandler.New() - o.APIVersion = okExAPIVersion - o.WebsocketURL = OkExWebsocketURL - o.Websocket.Functionality = wshandler.WebsocketTickerSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketKlineSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported | - wshandler.WebsocketMessageCorrelationSupported - o.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - o.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - o.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - // GetFuturesPostions Get the information of all holding positions in futures trading. // Due to high energy consumption, you are advised to capture data with the "Futures Account of a Currency" API instead. func (o *OKEX) GetFuturesPostions() (resp okgroup.GetFuturesPositionsResponse, _ error) { @@ -184,79 +139,16 @@ func (o *OKEX) GetFuturesContractInformation() (resp []okgroup.GetFuturesContrac return resp, o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, okgroup.OKGroupInstruments, nil, &resp, false) } -// GetFuturesOrderBook List all contracts. This request does not support pagination. The full list will be returned for a request. -func (o *OKEX) GetFuturesOrderBook(request okgroup.GetFuturesOrderBookRequest) (resp okgroup.GetFuturesOrderBookResponse, err error) { - requestURL := fmt.Sprintf("%v/%v/%v%v", okgroup.OKGroupInstruments, request.InstrumentID, okgroup.OKGroupGetSpotOrderBook, okgroup.FormatParameters(request)) - - type tempOB struct { - Bids [][]string `json:"bids"` - Asks [][]string `json:"asks"` - Timestamp time.Time `json:"timestamp"` - } - - var tmpOB tempOB - err = o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, requestURL, nil, &tmpOB, true) - if err != nil { - return resp, err - } - - processOB := func(ob [][]string) ([]okgroup.FuturesOrderbookItem, error) { - var processedOB []okgroup.FuturesOrderbookItem - for x := range ob { - price, convErr := strconv.ParseFloat(ob[x][0], 64) - if err != nil { - return nil, convErr - } - - size, convErr := strconv.ParseInt(ob[x][1], 10, 64) - if err != nil { - return nil, convErr - } - - liqOrders, convErr := strconv.ParseInt(ob[x][2], 10, 64) - if err != nil { - return nil, convErr - } - - numOrders, convErr := strconv.ParseInt(ob[x][3], 10, 64) - if err != nil { - return nil, convErr - } - - processedOB = append(processedOB, okgroup.FuturesOrderbookItem{ - Price: price, - Size: size, - ForceLiquidatedOrders: liqOrders, - NumberOrders: numOrders, - }) - } - return processedOB, nil - } - - resp.Bids, err = processOB(tmpOB.Bids) - if err != nil { - return - } - - resp.Asks, err = processOB(tmpOB.Asks) - if err != nil { - return - } - - resp.Timestamp = tmpOB.Timestamp - return resp, nil -} - // GetAllFuturesTokenInfo Get the last traded price, best bid/ask price, 24 hour trading volume and more info of all contracts. func (o *OKEX) GetAllFuturesTokenInfo() (resp []okgroup.GetFuturesTokenInfoResponse, _ error) { requestURL := fmt.Sprintf("%v/%v", okgroup.OKGroupInstruments, okgroup.OKGroupTicker) - return resp, o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, requestURL, nil, &resp, true) + return resp, o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, requestURL, nil, &resp, false) } // GetFuturesTokenInfoForCurrency Get the last traded price, best bid/ask price, 24 hour trading volume and more info of a contract. func (o *OKEX) GetFuturesTokenInfoForCurrency(instrumentID string) (resp okgroup.GetFuturesTokenInfoResponse, _ error) { requestURL := fmt.Sprintf("%v/%v/%v", okgroup.OKGroupInstruments, instrumentID, okgroup.OKGroupTicker) - return resp, o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, requestURL, nil, &resp, true) + return resp, o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, requestURL, nil, &resp, false) } // GetFuturesFilledOrder Get the recent 300 transactions of all contracts. Pagination is not supported here. @@ -416,12 +308,6 @@ func (o *OKEX) GetSwapContractInformation() (resp []okgroup.GetSwapContractInfor return resp, o.SendHTTPRequest(http.MethodGet, okGroupSwapSubsection, okgroup.OKGroupInstruments, nil, &resp, false) } -// GetSwapOrderBook Get the charts of the trading pairs. -func (o *OKEX) GetSwapOrderBook(request okgroup.GetSwapOrderBookRequest) (resp okgroup.GetSwapOrderBookResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v/%v%v", okgroup.OKGroupInstruments, request.InstrumentID, okGroupDepth, okgroup.FormatParameters(request)) - return resp, o.SendHTTPRequest(http.MethodGet, okGroupSwapSubsection, requestURL, nil, &resp, false) -} - // GetAllSwapTokensInformation Get the last traded price, best bid/ask price, 24 hour trading volume and more info of all contracts. func (o *OKEX) GetAllSwapTokensInformation() (resp []okgroup.GetAllSwapTokensInformationResponse, _ error) { requestURL := fmt.Sprintf("%v/%v", okgroup.OKGroupInstruments, okgroup.OKGroupTicker) diff --git a/exchanges/okex/okex_test.go b/exchanges/okex/okex_test.go index b1d5aff5..70ae6246 100644 --- a/exchanges/okex/okex_test.go +++ b/exchanges/okex/okex_test.go @@ -1,8 +1,12 @@ package okex import ( + "encoding/json" "fmt" + "log" "net/http" + "os" + "strconv" "strings" "sync" "testing" @@ -13,8 +17,9 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/okgroup" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -28,68 +33,52 @@ const ( canManipulateRealOrders = false ) -var testSetupRan bool -var o = OKEX{} +var o OKEX var spotCurrency = currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "-").Lower().String() var websocketEnabled bool -// TestSetDefaults Sets standard default settings for running a test -func TestSetDefaults(t *testing.T) { - if o.Name != OKGroupExchange { - o.SetDefaults() - } - if o.GetName() != OKGroupExchange { - t.Errorf("Test Failed - %v - SetDefaults() error", OKGroupExchange) - } - TestSetup(t) -} - // TestSetRealOrderDefaults Sets test defaults when test can impact real money/orders func TestSetRealOrderDefaults(t *testing.T) { - TestSetDefaults(t) if !areTestAPIKeysSet() || !canManipulateRealOrders { t.Skip("Ensure canManipulateRealOrders is true and your API keys are set") } } // TestSetup Sets defaults for test environment -func TestSetup(t *testing.T) { - if testSetupRan { - return - } - if o.APIKey == apiKey && o.APISecret == apiSecret && - o.ClientID == passphrase && apiKey != "" && apiSecret != "" && passphrase != "" { - return - } +func TestMain(m *testing.M) { + o.SetDefaults() o.ExchangeName = OKGroupExchange cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Okex load config error", err) + } okexConfig, err := cfg.GetExchangeConfig(OKGroupExchange) if err != nil { - t.Fatalf("Test Failed - %v Setup() init error", OKGroupExchange) + log.Fatalf("%v Setup() init error", OKGroupExchange) } - if okexConfig.Websocket { + if okexConfig.Features.Enabled.Websocket { websocketEnabled = true } - okexConfig.AuthenticatedAPISupport = true - okexConfig.AuthenticatedWebsocketAPISupport = true - okexConfig.APIKey = apiKey - okexConfig.APISecret = apiSecret - okexConfig.ClientID = passphrase - okexConfig.WebsocketURL = o.WebsocketURL - o.Setup(&okexConfig) - testSetupRan = true + okexConfig.API.AuthenticatedSupport = true + okexConfig.API.AuthenticatedWebsocketSupport = true + okexConfig.API.Credentials.Key = apiKey + okexConfig.API.Credentials.Secret = apiSecret + okexConfig.API.Credentials.ClientID = passphrase + okexConfig.API.Endpoints.WebsocketURL = o.API.Endpoints.WebsocketURL + err = o.Setup(okexConfig) + if err != nil { + log.Fatal("Okex setup error", err) + } o.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() o.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() + + os.Exit(m.Run()) } func areTestAPIKeysSet() bool { - if o.APIKey != "" && o.APIKey != "Key" && - o.APISecret != "" && o.APISecret != "Secret" { - return true - } - return false + return o.ValidateAPICredentials() } func testStandardErrorHandling(t *testing.T, err error) { @@ -103,7 +92,6 @@ func testStandardErrorHandling(t *testing.T, err error) { // TestGetAccountCurrencies API endpoint test func TestGetAccountCurrencies(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetAccountCurrencies() testStandardErrorHandling(t, err) @@ -111,7 +99,6 @@ func TestGetAccountCurrencies(t *testing.T) { // TestGetAccountWalletInformation API endpoint test func TestGetAccountWalletInformation(t *testing.T) { - TestSetDefaults(t) t.Parallel() resp, err := o.GetAccountWalletInformation("") if areTestAPIKeysSet() { @@ -128,7 +115,6 @@ func TestGetAccountWalletInformation(t *testing.T) { // TestGetAccountWalletInformationForCurrency API endpoint test func TestGetAccountWalletInformationForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() resp, err := o.GetAccountWalletInformation(currency.BTC.String()) if areTestAPIKeysSet() { @@ -151,7 +137,7 @@ func TestTransferAccountFunds(t *testing.T) { Amount: 10, Currency: currency.BTC.String(), From: 6, - To: 1, + To: -1, } _, err := o.TransferAccountFunds(request) @@ -163,7 +149,7 @@ func TestAccountWithdrawRequest(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() request := okgroup.AccountWithdrawRequest{ - Amount: 10, + Amount: -1, Currency: currency.BTC.String(), TradePwd: "1234", Destination: 4, @@ -177,7 +163,6 @@ func TestAccountWithdrawRequest(t *testing.T) { // TestGetAccountWithdrawalFee API endpoint test func TestGetAccountWithdrawalFee(t *testing.T) { - TestSetDefaults(t) t.Parallel() resp, err := o.GetAccountWithdrawalFee("") if areTestAPIKeysSet() { @@ -194,7 +179,6 @@ func TestGetAccountWithdrawalFee(t *testing.T) { // TestGetWithdrawalFeeForCurrency API endpoint test func TestGetAccountWithdrawalFeeForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() resp, err := o.GetAccountWithdrawalFee(currency.BTC.String()) if areTestAPIKeysSet() { @@ -211,7 +195,6 @@ func TestGetAccountWithdrawalFeeForCurrency(t *testing.T) { // TestGetAccountWithdrawalHistory API endpoint test func TestGetAccountWithdrawalHistory(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetAccountWithdrawalHistory("") testStandardErrorHandling(t, err) @@ -219,7 +202,6 @@ func TestGetAccountWithdrawalHistory(t *testing.T) { // TestGetAccountWithdrawalHistoryForCurrency API endpoint test func TestGetAccountWithdrawalHistoryForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetAccountWithdrawalHistory(currency.BTC.String()) testStandardErrorHandling(t, err) @@ -227,7 +209,6 @@ func TestGetAccountWithdrawalHistoryForCurrency(t *testing.T) { // TestGetAccountBillDetails API endpoint test func TestGetAccountBillDetails(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetAccountBillDetails(okgroup.GetAccountBillDetailsRequest{}) testStandardErrorHandling(t, err) @@ -235,7 +216,6 @@ func TestGetAccountBillDetails(t *testing.T) { // TestGetAccountDepositAddressForCurrency API endpoint test func TestGetAccountDepositAddressForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetAccountDepositAddressForCurrency(currency.BTC.String()) testStandardErrorHandling(t, err) @@ -243,7 +223,6 @@ func TestGetAccountDepositAddressForCurrency(t *testing.T) { // TestGetAccountDepositHistory API endpoint test func TestGetAccountDepositHistory(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetAccountDepositHistory("") testStandardErrorHandling(t, err) @@ -251,7 +230,6 @@ func TestGetAccountDepositHistory(t *testing.T) { // TestGetAccountDepositHistoryForCurrency API endpoint test func TestGetAccountDepositHistoryForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetAccountDepositHistory(currency.BTC.String()) testStandardErrorHandling(t, err) @@ -259,7 +237,6 @@ func TestGetAccountDepositHistoryForCurrency(t *testing.T) { // TestGetSpotTradingAccounts API endpoint test func TestGetSpotTradingAccounts(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSpotTradingAccounts() testStandardErrorHandling(t, err) @@ -267,7 +244,6 @@ func TestGetSpotTradingAccounts(t *testing.T) { // TestGetSpotTradingAccountsForCurrency API endpoint test func TestGetSpotTradingAccountsForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSpotTradingAccountForCurrency(currency.BTC.String()) testStandardErrorHandling(t, err) @@ -275,7 +251,6 @@ func TestGetSpotTradingAccountsForCurrency(t *testing.T) { // TestGetSpotBillDetailsForCurrency API endpoint test func TestGetSpotBillDetailsForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotBillDetailsForCurrencyRequest{ Currency: currency.BTC.String(), @@ -287,7 +262,6 @@ func TestGetSpotBillDetailsForCurrency(t *testing.T) { // TestGetSpotBillDetailsForCurrencyBadLimit API logic test func TestGetSpotBillDetailsForCurrencyBadLimit(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotBillDetailsForCurrencyRequest{ Currency: currency.BTC.String(), @@ -303,13 +277,12 @@ func TestGetSpotBillDetailsForCurrencyBadLimit(t *testing.T) { func TestPlaceSpotOrderLimit(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() - request := okgroup.PlaceSpotOrderRequest{ - InstrumentID: spotCurrency, - Type: "limit", - Side: "buy", - MarginTrading: "1", - Price: "100", - Size: "100", + request := okgroup.PlaceOrderRequest{ + InstrumentID: spotCurrency, + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), + Price: "-1", + Size: "0.001", } _, err := o.PlaceSpotOrder(&request) @@ -320,13 +293,12 @@ func TestPlaceSpotOrderLimit(t *testing.T) { func TestPlaceSpotOrderMarket(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() - request := okgroup.PlaceSpotOrderRequest{ - InstrumentID: spotCurrency, - Type: "market", - Side: "buy", - MarginTrading: "1", - Size: "100", - Notional: "100", + request := okgroup.PlaceOrderRequest{ + InstrumentID: spotCurrency, + Type: order.Market.Lower(), + Side: order.Buy.Lower(), + Size: "-100", + Notional: "100", } _, err := o.PlaceSpotOrder(&request) @@ -337,17 +309,16 @@ func TestPlaceSpotOrderMarket(t *testing.T) { func TestPlaceMultipleSpotOrders(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() - order := okgroup.PlaceSpotOrderRequest{ - InstrumentID: spotCurrency, - Type: "market", - Side: "buy", - MarginTrading: "1", - Size: "100", - Notional: "100", + ord := okgroup.PlaceOrderRequest{ + InstrumentID: spotCurrency, + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), + Size: "-100", + Price: "1", } - request := []okgroup.PlaceSpotOrderRequest{ - order, + request := []okgroup.PlaceOrderRequest{ + ord, } _, errs := o.PlaceMultipleSpotOrders(request) @@ -358,23 +329,21 @@ func TestPlaceMultipleSpotOrders(t *testing.T) { // TestPlaceMultipleSpotOrdersOverCurrencyLimits API logic test func TestPlaceMultipleSpotOrdersOverCurrencyLimits(t *testing.T) { - TestSetDefaults(t) t.Parallel() - order := okgroup.PlaceSpotOrderRequest{ - InstrumentID: spotCurrency, - Type: "market", - Side: "buy", - MarginTrading: "1", - Size: "100", - Notional: "100", + ord := okgroup.PlaceOrderRequest{ + InstrumentID: spotCurrency, + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), + Size: "-100", + Price: "1", } - request := []okgroup.PlaceSpotOrderRequest{ - order, - order, - order, - order, - order, + request := []okgroup.PlaceOrderRequest{ + ord, + ord, + ord, + ord, + ord, } _, errs := o.PlaceMultipleSpotOrders(request) @@ -385,29 +354,30 @@ func TestPlaceMultipleSpotOrdersOverCurrencyLimits(t *testing.T) { // TestPlaceMultipleSpotOrdersOverPairLimits API logic test func TestPlaceMultipleSpotOrdersOverPairLimits(t *testing.T) { - TestSetDefaults(t) t.Parallel() - order := okgroup.PlaceSpotOrderRequest{ - InstrumentID: spotCurrency, - Type: "market", - Side: "buy", - MarginTrading: "1", - Size: "100", - Notional: "100", + ord := okgroup.PlaceOrderRequest{ + InstrumentID: spotCurrency, + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), + Size: "-1", + Price: "1", } - request := []okgroup.PlaceSpotOrderRequest{ - order, + request := []okgroup.PlaceOrderRequest{ + ord, } - order.InstrumentID = currency.NewPairWithDelimiter(currency.LTC.String(), currency.USDT.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.DOGE.String(), currency.USDT.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.XMR.String(), currency.USDT.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.BCH.String(), currency.USDT.String(), "-").Lower().String() - request = append(request, order) + pairs := currency.Pairs{ + currency.NewPair(currency.LTC, currency.USDT), + currency.NewPair(currency.ETH, currency.USDT), + currency.NewPair(currency.BCH, currency.USDT), + currency.NewPair(currency.XMR, currency.USDT), + } + + for x := range pairs { + ord.InstrumentID = pairs[x].Format("-", false).String() + request = append(request, ord) + } _, errs := o.PlaceMultipleSpotOrders(request) if errs[0].Error() != "up to 4 trading pairs" { @@ -465,7 +435,6 @@ func TestCancelMultipleSpotOrdersOverCurrencyLimits(t *testing.T) { // TestGetSpotOrders API endpoint test func TestGetSpotOrders(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotOrdersRequest{ InstrumentID: spotCurrency, @@ -478,7 +447,6 @@ func TestGetSpotOrders(t *testing.T) { // TestGetSpotOpenOrders API endpoint test func TestGetSpotOpenOrders(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotOpenOrdersRequest{} _, err := o.GetSpotOpenOrders(request) @@ -487,7 +455,6 @@ func TestGetSpotOpenOrders(t *testing.T) { // TestGetSpotOrder API endpoint test func TestGetSpotOrder(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotOrderRequest{ OrderID: "-1234", @@ -499,7 +466,6 @@ func TestGetSpotOrder(t *testing.T) { // TestGetSpotTransactionDetails API endpoint test func TestGetSpotTransactionDetails(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotTransactionDetailsRequest{ OrderID: 1234, @@ -511,7 +477,6 @@ func TestGetSpotTransactionDetails(t *testing.T) { // TestGetSpotTokenPairDetails API endpoint test func TestGetSpotTokenPairDetails(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSpotTokenPairDetails() if err != nil { @@ -519,22 +484,8 @@ func TestGetSpotTokenPairDetails(t *testing.T) { } } -// TestGetSpotOrderBook API endpoint test -func TestGetSpotOrderBook(t *testing.T) { - TestSetDefaults(t) - t.Parallel() - request := okgroup.GetSpotOrderBookRequest{ - InstrumentID: spotCurrency, - } - _, err := o.GetSpotOrderBook(request) - if err != nil { - t.Error(err) - } -} - // TestGetSpotAllTokenPairsInformation API endpoint test func TestGetSpotAllTokenPairsInformation(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSpotAllTokenPairsInformation() if err != nil { @@ -544,7 +495,6 @@ func TestGetSpotAllTokenPairsInformation(t *testing.T) { // TestGetSpotAllTokenPairsInformationForCurrency API endpoint test func TestGetSpotAllTokenPairsInformationForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSpotAllTokenPairsInformationForCurrency(spotCurrency) if err != nil { @@ -554,7 +504,6 @@ func TestGetSpotAllTokenPairsInformationForCurrency(t *testing.T) { // TestGetSpotFilledOrdersInformation API endpoint test func TestGetSpotFilledOrdersInformation(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotFilledOrdersInformationRequest{ InstrumentID: spotCurrency, @@ -567,7 +516,6 @@ func TestGetSpotFilledOrdersInformation(t *testing.T) { // TestGetSpotMarketData API endpoint test func TestGetSpotMarketData(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotMarketDataRequest{ InstrumentID: spotCurrency, @@ -581,7 +529,6 @@ func TestGetSpotMarketData(t *testing.T) { // TestGetMarginTradingAccounts API endpoint test func TestGetMarginTradingAccounts(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetMarginTradingAccounts() testStandardErrorHandling(t, err) @@ -589,7 +536,6 @@ func TestGetMarginTradingAccounts(t *testing.T) { // TestGetMarginTradingAccountsForCurrency API endpoint test func TestGetMarginTradingAccountsForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetMarginTradingAccountsForCurrency(spotCurrency) testStandardErrorHandling(t, err) @@ -597,7 +543,6 @@ func TestGetMarginTradingAccountsForCurrency(t *testing.T) { // TestGetMarginBillDetails API endpoint test func TestGetMarginBillDetails(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetMarginBillDetailsRequest{ InstrumentID: spotCurrency, @@ -610,7 +555,6 @@ func TestGetMarginBillDetails(t *testing.T) { // TestGetMarginAccountSettings API endpoint test func TestGetMarginAccountSettings(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetMarginAccountSettings("") testStandardErrorHandling(t, err) @@ -618,7 +562,6 @@ func TestGetMarginAccountSettings(t *testing.T) { // TestGetMarginAccountSettingsForCurrency API endpoint test func TestGetMarginAccountSettingsForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetMarginAccountSettings(spotCurrency) testStandardErrorHandling(t, err) @@ -629,7 +572,7 @@ func TestOpenMarginLoan(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() request := okgroup.OpenMarginLoanRequest{ - Amount: 100, + Amount: -100, InstrumentID: spotCurrency, QuoteCurrency: currency.USDT.String(), } @@ -643,7 +586,7 @@ func TestRepayMarginLoan(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() request := okgroup.RepayMarginLoanRequest{ - Amount: 100, + Amount: -100, InstrumentID: spotCurrency, QuoteCurrency: currency.USDT.String(), BorrowID: 1, @@ -657,13 +600,13 @@ func TestRepayMarginLoan(t *testing.T) { func TestPlaceMarginOrderLimit(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() - request := okgroup.PlaceSpotOrderRequest{ - InstrumentID: spotCurrency, - Type: "limit", - Side: "buy", - MarginTrading: "2", - Price: "100", - Size: "100", + request := okgroup.PlaceOrderRequest{ + InstrumentID: spotCurrency, + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), + OrderType: strconv.Itoa(okgroup.NormalOrder), + Price: "-100", + Size: "100", } _, err := o.PlaceMarginOrder(&request) @@ -674,12 +617,12 @@ func TestPlaceMarginOrderLimit(t *testing.T) { func TestPlaceMarginOrderMarket(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() - request := okgroup.PlaceSpotOrderRequest{ + request := okgroup.PlaceOrderRequest{ InstrumentID: spotCurrency, - Type: "market", - Side: "buy", + Type: order.Market.Lower(), + Side: order.Buy.Lower(), MarginTrading: "2", - Size: "100", + Size: "-100", Notional: "100", } @@ -691,17 +634,17 @@ func TestPlaceMarginOrderMarket(t *testing.T) { func TestPlaceMultipleMarginOrders(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() - order := okgroup.PlaceSpotOrderRequest{ + ord := okgroup.PlaceOrderRequest{ InstrumentID: spotCurrency, - Type: "market", - Side: "buy", + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), MarginTrading: "1", - Size: "100", + Size: "-100", Notional: "100", } - request := []okgroup.PlaceSpotOrderRequest{ - order, + request := []okgroup.PlaceOrderRequest{ + ord, } _, errs := o.PlaceMultipleMarginOrders(request) @@ -712,23 +655,22 @@ func TestPlaceMultipleMarginOrders(t *testing.T) { // TestPlaceMultipleMarginOrdersOverCurrencyLimits API logic test func TestPlaceMultipleMarginOrdersOverCurrencyLimits(t *testing.T) { - TestSetDefaults(t) t.Parallel() - order := okgroup.PlaceSpotOrderRequest{ + ord := okgroup.PlaceOrderRequest{ InstrumentID: spotCurrency, - Type: "market", - Side: "buy", + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), MarginTrading: "1", - Size: "100", + Size: "-100", Notional: "100", } - request := []okgroup.PlaceSpotOrderRequest{ - order, - order, - order, - order, - order, + request := []okgroup.PlaceOrderRequest{ + ord, + ord, + ord, + ord, + ord, } _, errs := o.PlaceMultipleMarginOrders(request) @@ -739,29 +681,31 @@ func TestPlaceMultipleMarginOrdersOverCurrencyLimits(t *testing.T) { // TestPlaceMultipleMarginOrdersOverPairLimits API logic test func TestPlaceMultipleMarginOrdersOverPairLimits(t *testing.T) { - TestSetDefaults(t) t.Parallel() - order := okgroup.PlaceSpotOrderRequest{ + ord := okgroup.PlaceOrderRequest{ InstrumentID: spotCurrency, - Type: "market", - Side: "buy", + Type: order.Limit.Lower(), + Side: order.Buy.Lower(), MarginTrading: "1", - Size: "100", + Size: "-100", Notional: "100", } - request := []okgroup.PlaceSpotOrderRequest{ - order, + request := []okgroup.PlaceOrderRequest{ + ord, } - order.InstrumentID = currency.NewPairWithDelimiter(currency.LTC.String(), currency.USDT.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.DOGE.String(), currency.USDT.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.XMR.String(), currency.USDT.String(), "-").Lower().String() - request = append(request, order) - order.InstrumentID = currency.NewPairWithDelimiter(currency.BCH.String(), currency.USDT.String(), "-").Lower().String() - request = append(request, order) + pairs := currency.Pairs{ + currency.NewPair(currency.LTC, currency.USDT), + currency.NewPair(currency.ETH, currency.USDT), + currency.NewPair(currency.BCH, currency.USDT), + currency.NewPair(currency.XMR, currency.USDT), + } + + for x := range pairs { + ord.InstrumentID = pairs[x].Format("-", false).String() + request = append(request, ord) + } _, errs := o.PlaceMultipleMarginOrders(request) if errs[0].Error() != "up to 4 trading pairs" { @@ -814,7 +758,6 @@ func TestCancelMultipleMarginOrdersOverCurrencyLimits(t *testing.T) { // TestGetMarginOrders API endpoint test func TestGetMarginOrders(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotOrdersRequest{ InstrumentID: spotCurrency, @@ -826,7 +769,6 @@ func TestGetMarginOrders(t *testing.T) { // TestGetMarginOpenOrders API endpoint test func TestGetMarginOpenOrders(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotOpenOrdersRequest{} _, err := o.GetMarginOpenOrders(request) @@ -835,7 +777,6 @@ func TestGetMarginOpenOrders(t *testing.T) { // TestGetMarginOrder API endpoint test func TestGetMarginOrder(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotOrderRequest{ OrderID: "1234", @@ -847,7 +788,6 @@ func TestGetMarginOrder(t *testing.T) { // TestGetMarginTransactionDetails API endpoint test func TestGetMarginTransactionDetails(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSpotTransactionDetailsRequest{ OrderID: 1234, @@ -876,7 +816,6 @@ func getFutureInstrumentID() string { // TestGetFuturesPostions API endpoint test func TestGetFuturesPostions(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetFuturesPostions() testStandardErrorHandling(t, err) @@ -884,7 +823,6 @@ func TestGetFuturesPostions(t *testing.T) { // TestGetFuturesPostionsForCurrency API endpoint test func TestGetFuturesPostionsForCurrency(t *testing.T) { - TestSetDefaults(t) currencyContract := getFutureInstrumentID() _, err := o.GetFuturesPostionsForCurrency(currencyContract) testStandardErrorHandling(t, err) @@ -892,7 +830,6 @@ func TestGetFuturesPostionsForCurrency(t *testing.T) { // TestGetFuturesAccountOfAllCurrencies API endpoint test func TestGetFuturesAccountOfAllCurrencies(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetFuturesAccountOfAllCurrencies() testStandardErrorHandling(t, err) @@ -900,7 +837,6 @@ func TestGetFuturesAccountOfAllCurrencies(t *testing.T) { // TestGetFuturesAccountOfACurrency API endpoint test func TestGetFuturesAccountOfACurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetFuturesAccountOfACurrency(currency.BTC.String()) testStandardErrorHandling(t, err) @@ -908,7 +844,6 @@ func TestGetFuturesAccountOfACurrency(t *testing.T) { // TestGetFuturesLeverage API endpoint test func TestGetFuturesLeverage(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetFuturesLeverage(currency.BTC.String()) testStandardErrorHandling(t, err) @@ -929,7 +864,6 @@ func TestSetFuturesLeverage(t *testing.T) { // TestGetFuturesBillDetails API endpoint test func TestGetFuturesBillDetails(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetFuturesBillDetails(okgroup.GetSpotBillDetailsForCurrencyRequest{ Currency: currency.BTC.String(), @@ -945,7 +879,7 @@ func TestPlaceFuturesOrder(t *testing.T) { Leverage: 10, Type: 1, Size: 2, - Price: 432.11, + Price: -432.11, ClientOid: "12233456", }) testStandardErrorHandling(t, err) @@ -961,7 +895,7 @@ func TestPlaceFuturesOrderBatch(t *testing.T) { { ClientOid: "1", MatchPrice: "0", - Price: "100", + Price: "-100", Size: "100", Type: "1", }, @@ -994,7 +928,6 @@ func TestCancelMultipleFuturesOrders(t *testing.T) { // TestGetFuturesOrderList API endpoint test func TestGetFuturesOrderList(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesOrderList(okgroup.GetFuturesOrdersListRequest{ InstrumentID: getFutureInstrumentID(), Status: 6, @@ -1004,7 +937,6 @@ func TestGetFuturesOrderList(t *testing.T) { // TestGetFuturesOrderDetails API endpoint test func TestGetFuturesOrderDetails(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesOrderDetails(okgroup.GetFuturesOrderDetailsRequest{ InstrumentID: getFutureInstrumentID(), OrderID: 1, @@ -1014,7 +946,6 @@ func TestGetFuturesOrderDetails(t *testing.T) { // TestGetFuturesTransactionDetails API endpoint test func TestGetFuturesTransactionDetails(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesTransactionDetails(okgroup.GetFuturesTransactionDetailsRequest{ InstrumentID: getFutureInstrumentID(), OrderID: 1, @@ -1024,7 +955,6 @@ func TestGetFuturesTransactionDetails(t *testing.T) { // TestGetFuturesContractInformation API endpoint test func TestGetFuturesContractInformation(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetFuturesContractInformation() if err != nil { @@ -1032,21 +962,8 @@ func TestGetFuturesContractInformation(t *testing.T) { } } -// TestGetFuturesContractInformation API endpoint test -func TestGetFuturesOrderBook(t *testing.T) { - TestSetDefaults(t) - _, err := o.GetFuturesOrderBook(okgroup.GetFuturesOrderBookRequest{ - InstrumentID: getFutureInstrumentID(), - Size: 10, - }) - if err != nil { - t.Error(err) - } -} - // TestGetAllFuturesTokenInfo API endpoint test func TestGetAllFuturesTokenInfo(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetAllFuturesTokenInfo() if err != nil { @@ -1056,7 +973,6 @@ func TestGetAllFuturesTokenInfo(t *testing.T) { // TestGetAllFuturesTokenInfo API endpoint test func TestGetFuturesTokenInfoForCurrency(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesTokenInfoForCurrency(getFutureInstrumentID()) if err != nil { t.Error(err) @@ -1065,25 +981,20 @@ func TestGetFuturesTokenInfoForCurrency(t *testing.T) { // TestGetFuturesFilledOrder API endpoint test func TestGetFuturesFilledOrder(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesFilledOrder(okgroup.GetFuturesFilledOrderRequest{ InstrumentID: getFutureInstrumentID(), }) - if err != nil { - t.Error(err) - } + testStandardErrorHandling(t, err) } // TestGetFuturesHoldAmount API endpoint test func TestGetFuturesHoldAmount(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesHoldAmount(getFutureInstrumentID()) testStandardErrorHandling(t, err) } // TestGetFuturesHoldAmount API endpoint test func TestGetFuturesIndices(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesIndices(getFutureInstrumentID()) if err != nil { t.Error(err) @@ -1092,7 +1003,6 @@ func TestGetFuturesIndices(t *testing.T) { // TestGetFuturesHoldAmount API endpoint test func TestGetFuturesExchangeRates(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetFuturesExchangeRates() if err != nil { @@ -1102,7 +1012,6 @@ func TestGetFuturesExchangeRates(t *testing.T) { // TestGetFuturesHoldAmount API endpoint test func TestGetFuturesEstimatedDeliveryPrice(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesEstimatedDeliveryPrice(getFutureInstrumentID()) if err != nil { t.Error(err) @@ -1111,7 +1020,6 @@ func TestGetFuturesEstimatedDeliveryPrice(t *testing.T) { // TestGetFuturesOpenInterests API endpoint test func TestGetFuturesOpenInterests(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesOpenInterests(getFutureInstrumentID()) if err != nil { t.Error(err) @@ -1120,7 +1028,6 @@ func TestGetFuturesOpenInterests(t *testing.T) { // TestGetFuturesOpenInterests API endpoint test func TestGetFuturesCurrentPriceLimit(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesCurrentPriceLimit(getFutureInstrumentID()) if err != nil { t.Error(err) @@ -1129,7 +1036,6 @@ func TestGetFuturesCurrentPriceLimit(t *testing.T) { // TestGetFuturesCurrentMarkPrice API endpoint test func TestGetFuturesCurrentMarkPrice(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesCurrentMarkPrice(getFutureInstrumentID()) if err != nil { t.Error(err) @@ -1138,7 +1044,6 @@ func TestGetFuturesCurrentMarkPrice(t *testing.T) { // TestGetFuturesForceLiquidatedOrders API endpoint test func TestGetFuturesForceLiquidatedOrders(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesForceLiquidatedOrders(okgroup.GetFuturesForceLiquidatedOrdersRequest{ InstrumentID: getFutureInstrumentID(), Status: "1", @@ -1150,14 +1055,12 @@ func TestGetFuturesForceLiquidatedOrders(t *testing.T) { // TestGetFuturesTagPrice API endpoint test func TestGetFuturesTagPrice(t *testing.T) { - TestSetDefaults(t) _, err := o.GetFuturesTagPrice(getFutureInstrumentID()) testStandardErrorHandling(t, err) } // TestGetSwapPostions API endpoint test func TestGetSwapPostions(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapPostions() testStandardErrorHandling(t, err) @@ -1165,7 +1068,6 @@ func TestGetSwapPostions(t *testing.T) { // TestGetSwapPostionsForContract API endpoint test func TestGetSwapPostionsForContract(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapPostionsForContract(fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD)) testStandardErrorHandling(t, err) @@ -1173,7 +1075,6 @@ func TestGetSwapPostionsForContract(t *testing.T) { // TestGetSwapAccountOfAllCurrency API endpoint test func TestGetSwapAccountOfAllCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapAccountOfAllCurrency() testStandardErrorHandling(t, err) @@ -1181,7 +1082,6 @@ func TestGetSwapAccountOfAllCurrency(t *testing.T) { // TestGetSwapAccountSettingsOfAContract API endpoint test func TestGetSwapAccountSettingsOfAContract(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapAccountSettingsOfAContract(fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD)) testStandardErrorHandling(t, err) @@ -1189,7 +1089,6 @@ func TestGetSwapAccountSettingsOfAContract(t *testing.T) { // TestSetSwapLeverageLevelOfAContract API endpoint test func TestSetSwapLeverageLevelOfAContract(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.SetSwapLeverageLevelOfAContract(okgroup.SetSwapLeverageLevelOfAContractRequest{ InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD), @@ -1202,7 +1101,6 @@ func TestSetSwapLeverageLevelOfAContract(t *testing.T) { // TestGetSwapAccountSettingsOfAContract API endpoint test func TestGetSwapBillDetails(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapBillDetails(okgroup.GetSpotBillDetailsForCurrencyRequest{ Currency: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD), @@ -1236,13 +1134,13 @@ func TestPlaceMultipleSwapOrders(t *testing.T) { ClientOID: "hello", MatchPrice: "0", Price: "10", - Size: "1", + Size: "-1", Type: "1", }, { ClientOID: "hello2", MatchPrice: "0", Price: "10", - Size: "1", + Size: "-1", Type: "1", }}, }) @@ -1273,7 +1171,6 @@ func TestCancelMultipleSwapOrders(t *testing.T) { // TestGetSwapOrderList API endpoint test func TestGetSwapOrderList(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapOrderList(okgroup.GetSwapOrderListRequest{ InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD), @@ -1284,7 +1181,6 @@ func TestGetSwapOrderList(t *testing.T) { // TestGetSwapOrderDetails API endpoint test func TestGetSwapOrderDetails(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapOrderDetails(okgroup.GetSwapOrderDetailsRequest{ InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD), @@ -1295,7 +1191,6 @@ func TestGetSwapOrderDetails(t *testing.T) { // TestGetSwapTransactionDetails API endpoint test func TestGetSwapTransactionDetails(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapTransactionDetails(okgroup.GetSwapTransactionDetailsRequest{ InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD), @@ -1306,7 +1201,6 @@ func TestGetSwapTransactionDetails(t *testing.T) { // TestGetSwapContractInformation API endpoint test func TestGetSwapContractInformation(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapContractInformation() if err != nil { @@ -1314,22 +1208,8 @@ func TestGetSwapContractInformation(t *testing.T) { } } -// TestGetSwapOrderBook API endpoint test -func TestGetSwapOrderBook(t *testing.T) { - TestSetDefaults(t) - t.Parallel() - _, err := o.GetSwapOrderBook(okgroup.GetSwapOrderBookRequest{ - InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD), - Size: 200, - }) - if err != nil { - t.Error(err) - } -} - // TestGetAllSwapTokensInformation API endpoint test func TestGetAllSwapTokensInformation(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetAllSwapTokensInformation() if err != nil { @@ -1339,7 +1219,6 @@ func TestGetAllSwapTokensInformation(t *testing.T) { // TestGetSwapTokensInformationForCurrency API endpoint test func TestGetSwapTokensInformationForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapTokensInformationForCurrency(fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD)) if err != nil { @@ -1349,7 +1228,6 @@ func TestGetSwapTokensInformationForCurrency(t *testing.T) { // TestGetSwapFilledOrdersData API endpoint test func TestGetSwapFilledOrdersData(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapFilledOrdersData(&okgroup.GetSwapFilledOrdersDataRequest{ InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD), @@ -1362,7 +1240,6 @@ func TestGetSwapFilledOrdersData(t *testing.T) { // TestGetSwapMarketData API endpoint test func TestGetSwapMarketData(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetSwapMarketDataRequest{ InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD), @@ -1376,7 +1253,6 @@ func TestGetSwapMarketData(t *testing.T) { // TestGetSwapIndeces API endpoint test func TestGetSwapIndeces(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapIndices(fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD)) if err != nil { @@ -1386,7 +1262,6 @@ func TestGetSwapIndeces(t *testing.T) { // TestGetSwapExchangeRates API endpoint test func TestGetSwapExchangeRates(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapExchangeRates() if err != nil { @@ -1396,7 +1271,6 @@ func TestGetSwapExchangeRates(t *testing.T) { // TestGetSwapOpenInterest API endpoint test func TestGetSwapOpenInterest(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapOpenInterest(fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD)) if err != nil { @@ -1406,7 +1280,6 @@ func TestGetSwapOpenInterest(t *testing.T) { // TestGetSwapCurrentPriceLimits API endpoint test func TestGetSwapCurrentPriceLimits(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapCurrentPriceLimits(fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD)) if err != nil { @@ -1416,7 +1289,6 @@ func TestGetSwapCurrentPriceLimits(t *testing.T) { // TestGetSwapForceLiquidatedOrders API endpoint test func TestGetSwapForceLiquidatedOrders(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapForceLiquidatedOrders(okgroup.GetSwapForceLiquidatedOrdersRequest{ InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD), @@ -1429,7 +1301,6 @@ func TestGetSwapForceLiquidatedOrders(t *testing.T) { // TestGetSwapOnHoldAmountForOpenOrders API endpoint test func TestGetSwapOnHoldAmountForOpenOrders(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapOnHoldAmountForOpenOrders(fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD)) testStandardErrorHandling(t, err) @@ -1437,7 +1308,6 @@ func TestGetSwapOnHoldAmountForOpenOrders(t *testing.T) { // TestGetSwapNextSettlementTime API endpoint test func TestGetSwapNextSettlementTime(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapNextSettlementTime(fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD)) if err != nil { @@ -1447,7 +1317,6 @@ func TestGetSwapNextSettlementTime(t *testing.T) { // TestGetSwapMarkPrice API endpoint test func TestGetSwapMarkPrice(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapMarkPrice(fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD)) if err != nil { @@ -1457,7 +1326,6 @@ func TestGetSwapMarkPrice(t *testing.T) { // TestGetSwapFundingRateHistory API endpoint test func TestGetSwapFundingRateHistory(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetSwapFundingRateHistory(okgroup.GetSwapFundingRateHistoryRequest{ InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD), @@ -1470,7 +1338,6 @@ func TestGetSwapFundingRateHistory(t *testing.T) { // TestGetETT API endpoint test func TestGetETT(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetETT() testStandardErrorHandling(t, err) @@ -1478,7 +1345,6 @@ func TestGetETT(t *testing.T) { // TestGetETTAccountInformationForCurrency API endpoint test func TestGetETTAccountInformationForCurrency(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetETTBillsDetails(currency.BTC.String()) testStandardErrorHandling(t, err) @@ -1486,7 +1352,6 @@ func TestGetETTAccountInformationForCurrency(t *testing.T) { // TestGetETTBillsDetails API endpoint test func TestGetETTBillsDetails(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetETTBillsDetails(currency.BTC.String()) testStandardErrorHandling(t, err) @@ -1500,7 +1365,7 @@ func TestPlaceETTOrder(t *testing.T) { QuoteCurrency: spotCurrency, Type: 0, Size: "100", - Amount: 1, + Amount: -1, ETT: "OK06", } @@ -1521,7 +1386,6 @@ func TestCancelETTOrder(t *testing.T) { // Or when it is submitted as URL params // Unsure how to fix func TestGetETTOrderList(t *testing.T) { - TestSetDefaults(t) t.Parallel() request := okgroup.GetETTOrderListRequest{ Type: 1, @@ -1535,7 +1399,6 @@ func TestGetETTOrderList(t *testing.T) { // TestGetETTOrderDetails API endpoint test func TestGetETTOrderDetails(t *testing.T) { - TestSetDefaults(t) t.Parallel() _, err := o.GetETTOrderDetails("888845020785408") testStandardErrorHandling(t, err) @@ -1544,7 +1407,6 @@ func TestGetETTOrderDetails(t *testing.T) { // TestGetETTConstituents API endpoint test func TestGetETTConstituents(t *testing.T) { t.Skip("ETT currently unavailable") - TestSetDefaults(t) t.Parallel() _, err := o.GetETTConstituents("OK06ETT") if err != nil { @@ -1555,7 +1417,6 @@ func TestGetETTConstituents(t *testing.T) { // TestGetETTSettlementPriceHistory API endpoint test func TestGetETTSettlementPriceHistory(t *testing.T) { t.Skip("ETT currently unavailable") - TestSetDefaults(t) t.Parallel() _, err := o.GetETTSettlementPriceHistory("OK06ETT") if err != nil { @@ -1569,8 +1430,7 @@ func TestGetETTSettlementPriceHistory(t *testing.T) { // Attempts to subscribe to a channel that doesn't exist // Will log in if credentials are present func TestSendWsMessages(t *testing.T) { - TestSetDefaults(t) - if !o.Websocket.IsEnabled() && !o.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + if !o.Websocket.IsEnabled() && !o.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } var ok bool @@ -1615,14 +1475,13 @@ func TestSendWsMessages(t *testing.T) { func TestGetAssetTypeFromTableName(t *testing.T) { str := "spot/candle300s:BTC-USDT" spot := o.GetAssetTypeFromTableName(str) - if spot != orderbook.Spot { + if !strings.EqualFold(spot.String(), asset.Spot.String()) { t.Errorf("Error, expected 'SPOT', received: '%v'", spot) } } // TestGetWsChannelWithoutOrderType logic test func TestGetWsChannelWithoutOrderType(t *testing.T) { - TestSetDefaults(t) t.Parallel() str := "spot/depth5:BTC-USDT" expected := "depth5" @@ -1645,11 +1504,10 @@ func TestGetWsChannelWithoutOrderType(t *testing.T) { // TestOrderBookUpdateChecksumCalculator logic test func TestOrderBookUpdateChecksumCalculator(t *testing.T) { - TestSetDefaults(t) original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0.145",1],["3864.7682","0.005",1],["3864.9851","0.57",1],["3864.9852","0.30137754",1],["3864.9986","2.81818419",1],["3864.9995","0.002",1],["3865","0.0597",1],["3865.0309","0.4",1],["3865.1995","0.004",1],["3865.3995","0.004",1],["3865.5995","0.004",1],["3865.7995","0.004",1],["3865.9995","0.004",1],["3866.0961","0.25865886",1],["3866.1995","0.004",1],["3866.3995","0.004",1],["3866.4004","0.3243",2],["3866.5995","0.004",1],["3866.7633","0.44247086",1],["3866.7995","0.004",1],["3866.9197","0.511",1],["3867.256","0.51716256",1],["3867.3951","0.02588112",1],["3867.4014","0.025",1],["3867.4566","0.02499999",1],["3867.4675","4.01155057",5],["3867.5515","1.1",1],["3867.6113","0.009",1],["3867.7349","0.026",1],["3867.7781","0.03738652",1],["3867.9163","0.0521",1],["3868.0381","0.34354941",1],["3868.0436","0.051",1],["3868.0657","0.90552172",3],["3868.1819","0.03863346",1],["3868.2013","0.194",1],["3868.346","0.051",1],["3868.3863","0.01155",1],["3868.7716","0.009",1],["3868.947","0.025",1],["3868.98","0.001",1],["3869.0764","1.03487931",1],["3869.2773","0.07724578",1],["3869.4039","0.025",1],["3869.4068","1.03",1],["3869.7068","2.06976398",1],["3870","0.5",1],["3870.0465","0.01",1],["3870.7042","0.02099651",1],["3870.9451","2.07047375",1],["3871.5254","1.2",1],["3871.5596","0.001",1],["3871.6605","0.01035032",1],["3871.7179","2.07047375",1],["3871.8816","0.51751625",1],["3872.1","0.75",1],["3872.2464","0.0646",1],["3872.3747","0.283",1],["3872.4039","0.2",1],["3872.7655","0.23179307",1],["3872.8005","2.06976398",1],["3873.1509","2",1],["3873.3215","0.26",1],["3874.1392","0.001",1],["3874.1487","3.88224364",4],["3874.1685","1.8",1],["3874.5571","0.08974762",1],["3874.734","2.06976398",1],["3874.99","0.3",1],["3875","1.001",2],["3875.0041","1.03505051",1],["3875.45","0.3",1],["3875.4766","0.15",1],["3875.7057","0.51751625",1],["3876","0.001",1],["3876.68","0.3",1],["3876.7188","0.001",1],["3877","0.75",1],["3877.31","0.035",1],["3877.38","0.3",1],["3877.7","0.3",1],["3877.88","0.3",1],["3878.0364","0.34770122",1],["3878.4525","0.48579748",1],["3878.4955","0.02812511",1],["3878.8855","0.00258579",1],["3878.9605","0.895",1],["3879","0.001",1],["3879.2984","0.002",2],["3879.432","0.001",1],["3879.6313","6",1],["3879.9999","0.002",2],["3880","1.25132834",5],["3880.2526","0.04075162",1],["3880.7145","0.0647",1],["3881.2469","1.883",1],["3881.878","0.002",2],["3884.4576","0.002",2],["3885","0.002",2],["3885.2233","0.28304103",1],["3885.7416","18",1],["3886","0.001",1],["3886.1554","5.4",1],["3887","0.001",1],["3887.0372","0.002",2],["3887.2559","0.05214011",1],["3887.9238","0.0019",1],["3888","0.15810538",4],["3889","0.001",1],["3889.5175","0.50510653",1],["3889.6168","0.002",2],["3889.9999","0.001",1],["3890","2.34968109",4],["3890.5222","0.00257806",1],["3891.2659","5",1],["3891.9999","0.00893897",1],["3892.1964","0.002",2],["3892.4358","0.0176",1],["3893.1388","1.4279",1],["3894","0.0026321",1],["3894.776","0.001",1],["3895","1.501",2],["3895.379","0.25881288",1],["3897","0.05",1],["3897.3556","0.001",1],["3897.8432","0.73708079",1],["3898","3.31353018",7],["3898.4462","4.757",1],["3898.6","0.47159638",1],["3898.8769","0.0129",1],["3899","6",2],["3899.6516","0.025",1],["3899.9352","0.001",1],["3899.9999","0.013",2],["3900","22.37447743",24],["3900.9999","0.07763916",1],["3901","0.10192487",1],["3902.1937","0.00257034",1],["3902.3991","1.5532141",1],["3902.5148","0.001",1],["3904","1.49331984",1],["3904.9999","0.95905447",1],["3905","0.501",2],["3905.0944","0.001",1],["3905.61","0.099",1],["3905.6801","0.54343686",1],["3906.2901","0.0258",1],["3907.674","0.001",1],["3907.85","1.35778084",1],["3908","0.03846153",1],["3908.23","1.95189531",1],["3908.906","0.03148978",1],["3909","0.001",1],["3909.9999","0.01398721",2],["3910","0.016",2],["3910.2536","0.001",1],["3912.5406","0.88270517",1],["3912.8332","0.001",1],["3913","1.2640608",1],["3913.87","1.69114184",1],["3913.9003","0.00256266",1],["3914","1.21766411",1],["3915","0.001",1],["3915.4128","0.001",1],["3915.7425","6.848",1],["3916","0.0050949",1],["3917.36","1.28658296",1],["3917.9924","0.001",1],["3919","0.001",1],["3919.9999","0.001",1],["3920","1.21171832",3],["3920.0002","0.20217038",1],["3920.572","0.001",1],["3921","0.128",1],["3923.0756","0.00148064",1],["3923.1516","0.001",1],["3923.86","1.38831714",1],["3925","0.01867801",2],["3925.642","0.00255499",1],["3925.7312","0.001",1],["3926","0.04290757",1],["3927","0.023",1],["3927.3175","0.01212865",1],["3927.65","1.51375612",1],["3928","0.5",1],["3928.3108","0.001",1],["3929","0.001",1],["3929.9999","0.01519338",2],["3930","0.0174985",3],["3930.21","1.49335799",1],["3930.8904","0.001",1],["3932.2999","0.01953",1],["3932.8962","7.96",1],["3933.0387","11.808",1],["3933.47","0.001",1],["3934","1.40839932",1],["3935","0.001",1],["3936.8","0.62879518",1],["3937.23","1.56977841",1],["3937.4189","0.00254735",1]],"bids":[["3864.5217","0.00540709",1],["3864.5216","0.14068758",2],["3864.2275","0.01033576",1],["3864.0989","0.00825047",1],["3864.0273","0.38",1],["3864.0272","0.4",1],["3863.9957","0.01083539",1],["3863.9184","0.01653723",1],["3863.8282","0.25588165",1],["3863.8153","0.154",1],["3863.7791","1.14122492",1],["3863.6866","0.01733662",1],["3863.6093","0.02645958",1],["3863.3775","0.02773862",1],["3863.0297","0.513",1],["3863.0286","1.1028564",2],["3862.8489","0.01",1],["3862.5972","0.01890179",1],["3862.3431","0.01152944",1],["3862.313","0.009",1],["3862.2445","0.90551002",3],["3862.0734","0.014",1],["3862.0539","0.64976067",1],["3861.8586","0.025",1],["3861.7888","0.025",1],["3861.7673","0.008",1],["3861.5785","0.01",1],["3861.3895","0.005",1],["3861.3338","0.25875855",1],["3861.161","0.01",1],["3861.1111","0.03863352",1],["3861.0732","0.51703882",1],["3860.9116","0.17754895",1],["3860.75","0.19",1],["3860.6554","0.015",1],["3860.6172","0.005",1],["3860.6088","0.008",1],["3860.4724","0.12940042",1],["3860.4424","0.25880084",1],["3860.42","0.01",1],["3860.3725","0.51760102",1],["3859.8449","0.005",1],["3859.8285","0.03738652",1],["3859.7638","0.07726703",1],["3859.4502","0.008",1],["3859.3772","0.05173471",1],["3859.3409","0.194",1],["3859","5",1],["3858.827","0.0521",1],["3858.8208","0.001",1],["3858.679","0.26",1],["3858.4814","0.07477305",1],["3858.1669","1.03503422",1],["3857.6005","0.006",1],["3857.4005","0.004",1],["3857.2005","0.004",1],["3857.1871","1.218",1],["3857.0005","0.004",1],["3856.8135","0.0646",1],["3856.8005","0.004",1],["3856.2412","0.001",1],["3856.2349","1.03503422",1],["3856.0197","0.01037339",1],["3855.8781","0.23178117",1],["3855.8005","0.004",1],["3855.7165","0.00259355",1],["3855.4858","0.25875855",1],["3854.4584","0.01",1],["3853.6616","0.001",1],["3853.1373","0.92",1],["3852.5072","0.48599702",1],["3851.3926","0.13008333",1],["3851.082","0.001",1],["3850.9317","2",1],["3850.6359","0.34770165",1],["3850.2058","0.51751624",1],["3850.0823","0.15",1],["3850.0042","0.5175171",1],["3850","0.001",1],["3849.6325","1.8",1],["3849.41","0.3",1],["3848.9686","1.85",1],["3848.7426","0.18511466",1],["3848.52","0.3",1],["3848.5024","0.001",1],["3848.42","0.3",1],["3848.1618","2.204",1],["3847.77","0.3",1],["3847.48","0.3",1],["3847.3581","2.05",1],["3846.8259","0.0646",1],["3846.59","0.3",1],["3846.49","0.3",1],["3845.9228","0.001",1],["3844.184","0.00260133",1],["3844.0092","6.3",1],["3843.3432","0.001",1],["3841","0.06300963",1],["3840.7636","0.001",1],["3840","0.201",3],["3839.7681","18",1],["3839.5328","0.05214011",1],["3838.184","0.001",1],["3837.2344","0.27589557",1],["3836.6479","5.2",1],["3836","2.37196773",3],["3835.6044","0.001",1],["3833.6053","0.25873556",1],["3833.0248","0.001",1],["3833","0.8726502",1],["3832.6859","0.00260913",1],["3832","0.007",1],["3831.637","6",1],["3831.0602","0.001",1],["3830.4452","0.001",1],["3830","0.20375718",4],["3829.7125","0.07833486",1],["3829.6283","0.3519681",1],["3829","0.0039261",1],["3827.8656","0.001",1],["3826.0001","0.53251232",1],["3826","0.0509",1],["3825.7834","0.00698562",1],["3825.286","0.001",1],["3823.0001","0.03010127",1],["3822.8014","0.00261588",1],["3822.7064","0.001",1],["3822.2","1",1],["3822.1121","0.35994101",1],["3821.2222","0.00261696",1],["3821","0.001",1],["3820.1268","0.001",1],["3820","1.12992803",4],["3819","0.01331195",2],["3817.5472","0.001",1],["3816","1.13807184",2],["3815.8343","0.32463428",1],["3815.7834","0.00525295",1],["3815","28.99386799",4],["3814.9676","0.001",1],["3813","0.91303023",4],["3812.388","0.002",2],["3811.2257","0.07",1],["3810","0.32573997",2],["3809.8084","0.001",1],["3809.7928","0.00262481",1],["3807.2288","0.001",1],["3806.8421","0.07003461",1],["3806","0.19",1],["3805.8041","0.05678805",1],["3805","1.01",2],["3804.6492","0.001",1],["3804.3551","0.1",1],["3803","0.005",1],["3802.22","2.05042631",1],["3802.0696","0.001",1],["3802","1.63290092",1],["3801.2257","0.07",1],["3801","57.4",3],["3800.9853","0.02492278",1],["3800.8421","0.06503533",1],["3800.7844","0.02812628",1],["3800.0001","0.00409473",1],["3800","17.91401074",15],["3799.49","0.001",1],["3799","0.1",1],["3796.9104","0.001",1],["3796","9.00128053",2],["3795.5441","0.0028",1],["3794.3308","0.001",1],["3791","55",1],["3790.7777","0.07",1],["3790","12.03238184",7],["3789","1",1],["3788","0.21110454",2],["3787.2959","9",1],["3786.592","0.001",1],["3786","9.01916822",2],["3785","12.87914268",5],["3784.0124","0.001",1],["3781.4328","0.002",2],["3781","56.3",2],["3780.7777","0.07",1],["3780","23.41537654",10],["3778.8532","0.002",2],["3776","9",1],["3774","0.003",1],["3772.2481","0.06901672",1],["3771","55.1",2],["3770.7777","0.07",1],["3770","7.30268416",5],["3769","0.25",1],["3768","1.3725",3],["3766.66","0.02",1],["3766","7.64837924",2],["3765.58","1.22775492",1],["3762.58","1.22873383",1],["3761","51.68262164",1],["3760.8031","0.0399",1],["3760.7777","0.07",1]],"timestamp":"2019-03-06T23:19:17.705Z","checksum":-1785549915}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0",0],["3864.9852","0",0],["3865.9994","0.48402971",1],["3866.4004","0.001",1],["3866.7995","0.3273",2],["3867.4566","0",0],["3867.7031","0.025",1],["3868.0436","0",0],["3868.346","0",0],["3868.3695","0.051",1],["3870.9243","0.642",1],["3874.9942","0.51751796",1],["3875.7057","0",0],["3939","0.001",1]],"bids":[["3864.55","0.0565449",1],["3863.8282","0",0],["3863.8153","0",0],["3863.7898","0.01320077",1],["3863.4807","0.02112123",1],["3863.3002","0.04233533",1],["3863.1717","0.03379397",1],["3863.0685","0.04438179",1],["3863.0286","0.7362564",1],["3862.9912","0.06773651",1],["3862.8626","0.05407035",1],["3862.7595","0.07101087",1],["3862.313","0.3756",2],["3862.1848","0.012",1],["3862.0734","0",0],["3861.8391","0.025",1],["3861.7888","0",0],["3856.6716","0.38893641",1],["3768","0",0],["3766.66","0",0],["3766","0",0],["3765.58","0",0],["3762.58","0",0],["3761","0",0],["3760.8031","0",0],["3760.7777","0",0]],"timestamp":"2019-03-06T23:19:18.239Z","checksum":-1587788848}]}` var dataResponse okgroup.WebsocketDataResponse - err := common.JSONDecode([]byte(original), &dataResponse) + err := json.Unmarshal([]byte(original), &dataResponse) if err != nil { t.Error(err) } @@ -1659,7 +1517,7 @@ func TestOrderBookUpdateChecksumCalculator(t *testing.T) { return } var updateResponse okgroup.WebsocketDataResponse - err = common.JSONDecode([]byte(update), &updateResponse) + err = json.Unmarshal([]byte(update), &updateResponse) if err != nil { t.Error(err) } @@ -1672,11 +1530,10 @@ func TestOrderBookUpdateChecksumCalculator(t *testing.T) { // TestOrderBookUpdateChecksumCalculatorWithDash logic test func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { - TestSetDefaults(t) original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000714","1.15414979",1],["0.000715","3.3",2],["0.000717","426.71348",2],["0.000719","140.84507042",1],["0.00072","590.77",1],["0.000721","991.77",1],["0.000724","0.3532032",1],["0.000725","58.82698567",1],["0.000726","1033.15469748",2],["0.000729","0.35320321",1],["0.00073","352.77",1],["0.000735","0.38469748",1],["0.000736","625.77",1],["0.00075191","152.44796961",1],["0.00075192","114.3359772",1],["0.00075193","85.7519829",1],["0.00075194","64.31398718",1],["0.00075195","48.23549038",1],["0.00075196","36.17661779",1],["0.00075199","61.04804253",1],["0.0007591","70.71318474",1],["0.0007621","53.03488855",1],["0.00076211","39.77616642",1],["0.00076212","29.83212481",1],["0.0007635","22.37409361",1],["0.00076351","29.36599786",2],["0.00076352","9.43907074",1],["0.00076353","7.07930306",1],["0.00076354","14.15860612",1],["0.00076355","3.53965153",1],["0.00076369","3.53965153",1],["0.0008","34.36841101",1],["0.00082858","1.69936503",1],["0.00083232","2.8",1],["0.00084","15.69220129",1],["0.00085","4.42785042",1],["0.00088","0.1",1],["0.000891","0.1",1],["0.0009","12.41486491",2],["0.00093","5",1],["0.0012","12.31486492",1],["0.00531314","6.91803114",1],["0.00799999","0.02",1],["0.0084","0.05989",1],["0.00931314","5.18852336",1],["0.0799999","0.02",1],["0.499","6.00423396",1],["0.5","0.4995",1],["0.799999","0.02",1],["4.99","2",1],["5","3.98583144",1],["7.99999999","0.02",1],["79.99999999","0.02",1],["799.99999999","0.02986704",1]],"bids":[["0.000709","222.91679881",3],["0.000703","0.47161952",1],["0.000701","140.73015789",2],["0.0007","0.3",1],["0.000699","401",1],["0.000698","232.61801667",2],["0.000689","0.71396896",1],["0.000688","0.69910125",1],["0.000613","227.54771052",1],["0.0005","0.01",1],["0.00026789","3.69905341",1],["0.000238","2.4",1],["0.00022","0.53",1],["0.0000055","374.09871696",1],["0.00000056","222",1],["0.00000055","736.84761363",1],["0.0000002","999",1],["0.00000009","1222.22222417",1],["0.00000008","20868.64520447",1],["0.00000002","110000",1],["0.00000001","10000",1]],"timestamp":"2019-03-12T22:22:42.274Z","checksum":1319037905}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000715","100.48199596",3],["0.000716","62.21679881",1]],"bids":[["0.000713","38.95772168",1]],"timestamp":"2019-03-12T22:22:42.938Z","checksum":-131160897}]}` var dataResponse okgroup.WebsocketDataResponse - err := common.JSONDecode([]byte(original), &dataResponse) + err := json.Unmarshal([]byte(original), &dataResponse) if err != nil { t.Error(err) } @@ -1686,7 +1543,7 @@ func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { return } var updateResponse okgroup.WebsocketDataResponse - err = common.JSONDecode([]byte(update), &updateResponse) + err = json.Unmarshal([]byte(update), &updateResponse) if err != nil { t.Error(err) } @@ -1701,7 +1558,7 @@ func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { func TestOrderBookPartialChecksumCalculator(t *testing.T) { orderbookPartialJSON := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"EOS-USDT","asks":[["3.5196","0.1077",1],["3.5198","21.71",1],["3.5199","51.1805",1],["3.5208","75.09",1],["3.521","196.3333",1],["3.5213","0.1",1],["3.5218","39.276",2],["3.5219","395.6334",1],["3.522","27.956",1],["3.5222","404.9595",1],["3.5225","300",1],["3.5227","143.5442",2],["3.523","42.4746",1],["3.5231","852.64",2],["3.5235","34.9602",1],["3.5237","442.0918",2],["3.5238","352.8404",2],["3.5239","341.6759",2],["3.524","84.9493",1],["3.5241","148.4882",1],["3.5242","261.64",1],["3.5243","142.045",1],["3.5246","10",1],["3.5247","284.0788",1],["3.5248","720",1],["3.5249","89.2518",2],["3.5251","1201.8965",2],["3.5254","426.2938",1],["3.5255","213.0863",1],["3.5257","568.1576",1],["3.5258","0.3",1],["3.5259","34.4602",1],["3.526","0.1",1],["3.5263","850.771",1],["3.5265","5.9",1],["3.5268","10.5064",2],["3.5272","1136.8965",1],["3.5274","255.1481",1],["3.5276","29.5374",1],["3.5278","50",1],["3.5282","284.1797",1],["3.5283","1136.8965",1],["3.5284","0.4275",1],["3.5285","100",1],["3.5292","90.9",1],["3.5298","0.2",1],["3.5303","568.1576",1],["3.5305","279.9999",1],["3.532","0.409",1],["3.5321","568.1576",1],["3.5326","6016.8756",1],["3.5328","4.9849",1],["3.533","92.88",2],["3.5343","1200.2383",2],["3.5344","100",1],["3.535","359.7047",1],["3.5354","100",1],["3.5355","100",1],["3.5356","10",1],["3.5358","200",2],["3.5362","435.139",1],["3.5365","2152",1],["3.5366","284.1756",1],["3.5367","568.4644",1],["3.5369","33.9878",1],["3.537","337.1191",2],["3.5373","0.4045",1],["3.5383","1136.7188",1],["3.5386","12.1614",1],["3.5387","90.89",1],["3.54","4.54",1],["3.5423","90.8",1],["3.5436","0.1",1],["3.5454","853.4156",1],["3.5468","142.0656",1],["3.5491","0.0008",1],["3.55","14478.8206",6],["3.5537","21521",1],["3.5555","11.53",1],["3.5573","50.6001",1],["3.5599","4591.4221",1],["3.56","1227.0002",4],["3.5603","2670",1],["3.5608","58.6638",1],["3.5613","0.1",1],["3.5621","45.9473",1],["3.57","2141.7274",3],["3.5712","2956.9816",1],["3.5717","27.9978",1],["3.5718","0.9285",1],["3.5739","299.73",1],["3.5761","864",1],["3.579","22.5225",1],["3.5791","38.26",2],["3.58","7618.4634",5],["3.5801","457.2184",1],["3.582","24.5",1],["3.5822","1572.6425",1],["3.5845","14.1438",1],["3.585","527.169",1],["3.5865","20",1],["3.5867","4490",1],["3.5876","39.0493",1],["3.5879","392.9083",1],["3.5888","436.42",2],["3.5896","50",1],["3.59","2608.9128",8],["3.5913","19.5246",1],["3.5938","7082",1],["3.597","0.1",1],["3.5979","399",1],["3.5995","315.1509",1],["3.5999","2566.2648",1],["3.6","18511.2292",35],["3.603","22.3379",2],["3.605","499.5",1],["3.6055","100",1],["3.6058","499.5",1],["3.608","1021.1485",1],["3.61","11755.4596",13],["3.611","42.8571",1],["3.6131","6690",1],["3.6157","19.5247",1],["3.618","2500",1],["3.6197","525.7146",1],["3.6198","0.4455",1],["3.62","6440.6295",8],["3.6219","0.4175",1],["3.6237","168",1],["3.6265","0.1001",1],["3.628","64.9345",1],["3.63","4435.4985",6],["3.6308","1.7815",1],["3.6331","0.1",1],["3.6338","355.527",2],["3.6358","50",1],["3.6363","2074.7096",1],["3.6376","4000",1],["3.6396","11090",1],["3.6399","0.4055",1],["3.64","4161.9805",4],["3.6437","117.6524",1],["3.648","190",1],["3.6488","200",1],["3.65","11740.5045",25],["3.6512","0.1",1],["3.6521","728",1],["3.6555","100",1],["3.6598","36.6914",1],["3.66","4331.2148",6],["3.6638","200",1],["3.6673","100",1],["3.6679","38",1],["3.6688","2",1],["3.6695","0.1",1],["3.67","7984.698",6],["3.672","300",1],["3.6777","257.8247",1],["3.6789","393.4217",2],["3.68","9202.3222",11],["3.6818","500",1],["3.6823","299.7",1],["3.6839","422.3748",1],["3.685","100",1],["3.6878","0.1",1],["3.6888","72.0958",2],["3.6889","2876",1],["3.689","28",1],["3.6891","28",1],["3.6892","28",1],["3.6895","28",1],["3.6898","28",1],["3.69","643.96",7],["3.6908","118",2],["3.691","28",1],["3.6916","28",1],["3.6918","28",1],["3.6926","28",1],["3.6928","28",1],["3.6932","28",1],["3.6933","200",1],["3.6935","28",1],["3.6936","28",1],["3.6938","28",1],["3.694","28",1],["3.698","1498.5",1],["3.6988","2014.2004",2],["3.7","21904.2689",22],["3.7029","71.95",1],["3.704","3690.1362",1],["3.7055","100",1],["3.7063","0.1",1],["3.71","4421.3468",4],["3.719","17.3491",1],["3.72","1304.5995",3],["3.7211","10",1],["3.7248","0.1",1],["3.725","1900",1],["3.73","31.1785",2],["3.7375","38",1]],"bids":[["3.5182","151.5343",6],["3.5181","0.3691",1],["3.518","271.3967",2],["3.5179","257.8352",1],["3.5178","12.3811",1],["3.5173","34.1921",2],["3.5171","1013.8256",2],["3.517","272.1119",2],["3.5168","395.3376",1],["3.5166","317.1756",2],["3.5165","348.302",3],["3.5164","142.0414",1],["3.5163","96.8933",2],["3.516","600.1034",3],["3.5159","27.481",1],["3.5158","27.33",1],["3.5157","583.1898",2],["3.5156","24.6819",2],["3.5154","25",1],["3.5153","0.429",1],["3.5152","453.9204",3],["3.5151","2131.592",4],["3.515","335",3],["3.5149","37.1586",1],["3.5147","41.6759",1],["3.5146","54.569",1],["3.5145","70.3515",1],["3.5143","68.206",3],["3.5142","359.4538",2],["3.5139","45.4123",2],["3.5137","71.673",2],["3.5136","25",1],["3.5135","300",1],["3.5134","442.57",2],["3.5132","83.3518",1],["3.513","1245.2529",3],["3.5127","20",1],["3.512","284.1353",1],["3.5119","1136.8319",1],["3.5113","56.9351",1],["3.5111","588.1898",2],["3.5109","255.0946",1],["3.5105","48.65",1],["3.5103","50.2",1],["3.5098","720",1],["3.5096","148.95",1],["3.5094","570.5758",2],["3.509","2.386",1],["3.5089","0.4065",1],["3.5087","282.3859",2],["3.5086","145.036",2],["3.5084","2.386",1],["3.5082","90.98",1],["3.5081","2.386",1],["3.5079","2.386",1],["3.5078","857.6229",2],["3.5075","2.386",1],["3.5074","284.1877",1],["3.5073","100",1],["3.5071","100",1],["3.507","768.4159",3],["3.5069","313.0863",2],["3.5068","426.2938",1],["3.5066","568.3594",1],["3.5063","1136.6865",1],["3.5059","0.3",1],["3.5054","9.9999",1],["3.5053","0.2",1],["3.5051","392.428",1],["3.505","13.79",1],["3.5048","99.5497",2],["3.5047","78.5331",2],["3.5046","2153",1],["3.5041","5983.999",1],["3.5037","668.5682",1],["3.5036","160.5948",1],["3.5024","534.8075",1],["3.5014","28.5604",1],["3.5011","91",1],["3.5","1058.8771",2],["3.4997","50.2",1],["3.4985","3430.0414",1],["3.4949","232.0591",1],["3.4942","21521",1],["3.493","2",1],["3.4928","2",1],["3.4925","0.44",1],["3.4917","142.0656",1],["3.49","2051.8826",4],["3.488","280.7459",1],["3.4852","643.4038",1],["3.4851","86.0807",1],["3.485","213.2436",1],["3.484","0.1",1],["3.4811","144.3399",1],["3.4808","89",1],["3.4803","12.1999",1],["3.4801","2390",1],["3.48","930.8453",9],["3.4791","310",1],["3.4768","206",1],["3.4767","0.9415",1],["3.4754","1.4387",1],["3.4728","20",1],["3.4701","1219.2873",1],["3.47","1904.3139",7],["3.468","0.4035",1],["3.4667","0.1",1],["3.4666","3020.0101",1],["3.465","10",1],["3.464","0.4485",1],["3.462","2119.6556",1],["3.46","1305.6113",8],["3.4589","8.0228",1],["3.457","100",1],["3.456","70.3859",2],["3.4538","20",1],["3.4536","4323.9486",2],["3.4531","827.0427",1],["3.4528","0.439",1],["3.4522","8.0381",1],["3.4513","441.1873",1],["3.4512","50.707",1],["3.451","87.0902",1],["3.4509","200",1],["3.4506","100",1],["3.4505","86.4045",2],["3.45","12409.4595",28],["3.4494","0.5365",2],["3.449","10761",1],["3.4482","8.0476",1],["3.4469","0.449",1],["3.445","2000",1],["3.4427","14",1],["3.4421","100",1],["3.4416","8.0631",1],["3.4404","1",1],["3.44","4580.733",11],["3.4388","1868.2085",1],["3.438","937.7246",2],["3.4367","1500",1],["3.4366","62",1],["3.436","29.8743",1],["3.4356","25.4801",1],["3.4349","4.3086",1],["3.4343","43.2402",1],["3.433","2.0688",1],["3.4322","2.7335",2],["3.432","93.3233",1],["3.4302","328.8301",2],["3.43","4440.8158",11],["3.4288","754.574",2],["3.4283","125.7043",2],["3.428","744.3154",2],["3.4273","5460",1],["3.4258","50",1],["3.4255","109.005",1],["3.4248","100",1],["3.4241","129.2048",2],["3.4233","5.3598",1],["3.4228","4498.866",1],["3.4222","3.5435",1],["3.4217","404.3252",2],["3.4211","1000",1],["3.4208","31",1],["3.42","1834.024",9],["3.4175","300",1],["3.4162","400",1],["3.4152","0.1",1],["3.4151","4.3336",1],["3.415","1.5974",1],["3.414","1146",1],["3.4134","306.4246",1],["3.4129","7.5556",1],["3.4111","198.5188",1],["3.4109","500",1],["3.4106","4305",1],["3.41","2150.7635",13],["3.4085","4.342",1],["3.4054","5.6985",1],["3.4019","5.438",1],["3.4015","1010.846",1],["3.4009","8610",1],["3.4005","1.9122",1],["3.4004","1",1],["3.4","27081.1806",67],["3.3955","3.2682",1],["3.3953","5.4486",1],["3.3937","1591.3805",1],["3.39","3221.4155",8],["3.3899","3.2736",1],["3.3888","1500",2],["3.3887","5.4592",1],["3.385","117.0969",2],["3.3821","5.4699",1],["3.382","100.0529",1],["3.3818","172.0164",1],["3.3815","165.6288",1],["3.381","887.3115",1],["3.3808","100",1]],"timestamp":"2019-03-04T00:15:04.155Z","checksum":-2036653089}]}` var dataResponse okgroup.WebsocketDataResponse - err := common.JSONDecode([]byte(orderbookPartialJSON), &dataResponse) + err := json.Unmarshal([]byte(orderbookPartialJSON), &dataResponse) if err != nil { t.Error(err) } @@ -1730,7 +1587,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() o.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -1742,13 +1599,12 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - TestSetDefaults(t) t.Parallel() var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic if resp, err := o.GetFee(feeBuilder); resp != float64(0.0015) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) } // CryptocurrencyTradeFee High quantity @@ -1756,7 +1612,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := o.GetFee(feeBuilder); resp != float64(1500) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(1500), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(1500), resp) t.Error(err) } @@ -1764,7 +1620,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := o.GetFee(feeBuilder); resp != float64(0.0005) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0005), resp) t.Error(err) } @@ -1772,7 +1628,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -1780,7 +1636,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -1788,7 +1644,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -1797,14 +1653,13 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := o.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } // TestFormatWithdrawPermissions helper test func TestFormatWithdrawPermissions(t *testing.T) { - TestSetDefaults(t) t.Parallel() expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.NoFiatWithdrawalsText withdrawPermissions := o.FormatWithdrawPermissions() @@ -1819,12 +1674,18 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestSubmitOrder(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() - var p = currency.Pair{ - Delimiter: "", - Base: currency.BTC, - Quote: currency.USDT, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.BTC, + Quote: currency.USDT, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := o.SubmitOrder(p, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 10, "hi") + response, err := o.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -1837,7 +1698,7 @@ func TestCancelExchangeOrder(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() currencyPair := currency.NewPair(currency.LTC, currency.BTC) - var orderCancellation = exchange.OrderCancellation{ + var orderCancellation = order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -1853,7 +1714,7 @@ func TestCancelAllExchangeOrders(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() currencyPair := currency.NewPair(currency.LTC, currency.BTC) - var orderCancellation = exchange.OrderCancellation{ + var orderCancellation = order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -1863,8 +1724,8 @@ func TestCancelAllExchangeOrders(t *testing.T) { resp, err := o.CancelAllOrders(&orderCancellation) testStandardErrorHandling(t, err) - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%d orders failed to cancel", len(resp.Status)) } } @@ -1878,9 +1739,11 @@ func TestGetAccountInfo(t *testing.T) { func TestModifyOrder(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() - _, err := o.ModifyOrder(&exchange.ModifyOrder{}) + _, err := o.ModifyOrder(&order.Modify{}) if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) + t.Errorf("Expected '%v', received: '%v'", + common.ErrFunctionNotSupported, + err) } } @@ -1888,13 +1751,15 @@ func TestModifyOrder(t *testing.T) { func TestWithdraw(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", - TradePassword: "Password", - FeeAmount: 1, + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + TradePassword: "Password", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", + FeeAmount: 1, } _, err := o.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) testStandardErrorHandling(t, err) @@ -1904,11 +1769,12 @@ func TestWithdraw(t *testing.T) { func TestWithdrawFiat(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := o.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) + t.Errorf("Expected '%v', received: '%v'", + common.ErrFunctionNotSupported, + err) } } @@ -1916,10 +1782,33 @@ func TestWithdrawFiat(t *testing.T) { func TestWithdrawInternationalBank(t *testing.T) { TestSetRealOrderDefaults(t) t.Parallel() - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := o.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) + t.Errorf("Expected '%v', received: '%v'", + common.ErrFunctionNotSupported, + err) + } +} + +// TestGetOrderbook logic test +func TestGetOrderbook(t *testing.T) { + t.Parallel() + _, err := o.GetOrderBook(okgroup.GetOrderBookRequest{InstrumentID: "BTC-USDT"}, + asset.Spot) + if err != nil { + t.Error(err) + } + contract := getFutureInstrumentID() + _, err = o.GetOrderBook(okgroup.GetOrderBookRequest{InstrumentID: contract}, + asset.Futures) + if err != nil { + t.Error(err) + } + + _, err = o.GetOrderBook(okgroup.GetOrderBookRequest{InstrumentID: "BTC-USD-SWAP"}, + asset.PerpetualSwap) + if err != nil { + t.Error(err) } } diff --git a/exchanges/okex/okex_wrapper.go b/exchanges/okex/okex_wrapper.go new file mode 100644 index 00000000..8fb0c00b --- /dev/null +++ b/exchanges/okex/okex_wrapper.go @@ -0,0 +1,451 @@ +package okex + +import ( + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +const ( + delimiterDash = "-" + delimiterUnderscore = "_" +) + +// GetDefaultConfig returns a default exchange config +func (o *OKEX) GetDefaultConfig() (*config.ExchangeConfig, error) { + o.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = o.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = o.BaseCurrencies + + err := o.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if o.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = o.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults method assignes the default values for OKEX +func (o *OKEX) SetDefaults() { + o.SetErrorDefaults() + o.SetCheckVarDefaults() + o.Name = okExExchangeName + o.Enabled = true + o.Verbose = true + o.API.CredentialsValidator.RequiresKey = true + o.API.CredentialsValidator.RequiresSecret = true + o.API.CredentialsValidator.RequiresClientID = true + + o.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + asset.Futures, + asset.PerpetualSwap, + asset.Index, + }, + UseGlobalFormat: false, + } + // Same format used for perpetual swap and futures + fmt1 := currency.PairStore{ + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: delimiterDash, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: delimiterUnderscore, + }, + } + o.CurrencyPairs.Store(asset.PerpetualSwap, fmt1) + o.CurrencyPairs.Store(asset.Futures, fmt1) + + index := currency.PairStore{ + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: delimiterDash, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + }, + } + + spot := currency.PairStore{ + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: delimiterDash, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: delimiterDash, + }, + } + o.CurrencyPairs.Store(asset.Spot, spot) + o.CurrencyPairs.Store(asset.Index, index) + + o.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + CancelOrders: true, + SubmitOrder: true, + SubmitOrders: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + KlineFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + MessageCorrelation: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + o.Requester = request.New(o.Name, + request.NewRateLimit(time.Second, okExAuthRate), + request.NewRateLimit(time.Second, okExUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), + ) + + o.API.Endpoints.URLDefault = okExAPIURL + o.API.Endpoints.URL = okExAPIURL + o.API.Endpoints.WebsocketURL = OkExWebsocketURL + o.Websocket = wshandler.New() + o.APIVersion = okExAPIVersion + o.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + o.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + o.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Start starts the OKGroup go routine +func (o *OKEX) Start(wg *sync.WaitGroup) { + wg.Add(1) + go func() { + o.Run() + wg.Done() + }() +} + +// Run implements the OKEX wrapper +func (o *OKEX) Run() { + if o.Verbose { + log.Debugf(log.ExchangeSys, + "%s Websocket: %s. (url: %s).\n", + o.Name, + common.IsEnabled(o.Websocket.IsEnabled()), + o.API.Endpoints.WebsocketURL) + } + + if o.Config.CurrencyPairs.Pairs[asset.Spot].ConfigFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.Spot].RequestFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.Index].ConfigFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.Index].RequestFormat == nil { + currFmt := currency.PairStore{ + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: delimiterDash, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: delimiterDash, + }, + } + o.CurrencyPairs.Store(asset.Spot, currFmt) + o.Config.CurrencyPairs.Store(asset.Spot, currFmt) + o.CurrencyPairs.Store(asset.Index, currFmt) + o.Config.CurrencyPairs.Store(asset.Index, currFmt) + } + + if o.Config.CurrencyPairs.Pairs[asset.Futures].ConfigFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.Futures].RequestFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.PerpetualSwap].ConfigFormat == nil || + o.Config.CurrencyPairs.Pairs[asset.PerpetualSwap].RequestFormat == nil { + currFmt := currency.PairStore{ + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: delimiterDash, + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: delimiterUnderscore, + }, + } + o.CurrencyPairs.Store(asset.Futures, currFmt) + o.Config.CurrencyPairs.Store(asset.Futures, currFmt) + o.CurrencyPairs.Store(asset.PerpetualSwap, currFmt) + o.Config.CurrencyPairs.Store(asset.PerpetualSwap, currFmt) + } + + if !common.StringDataContains(o.Config.CurrencyPairs.Pairs[asset.Spot].Enabled.Strings(), + o.CurrencyPairs.Pairs[asset.Spot].RequestFormat.Delimiter) { + enabledPairs := currency.NewPairsFromStrings([]string{"EOS-USDT"}) + log.Warnf(log.ExchangeSys, + "Enabled pairs for %v reset due to config upgrade, please enable the ones you would like again.", + o.Name) + + err := o.UpdatePairs(enabledPairs, asset.Spot, true, true) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update currencies.\n", + o.Name) + return + } + } + + if !o.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := o.UpdateTradablePairs(false) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + o.Name, + err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (o *OKEX) FetchTradablePairs(i asset.Item) ([]string, error) { + var pairs []string + switch i { + case asset.Spot: + prods, err := o.GetSpotTokenPairDetails() + if err != nil { + return nil, err + } + + for x := range prods { + pairs = append(pairs, + currency.NewPairWithDelimiter(prods[x].BaseCurrency, + prods[x].QuoteCurrency, + o.GetPairFormat(i, false).Delimiter).String()) + } + return pairs, nil + case asset.Futures: + prods, err := o.GetFuturesContractInformation() + if err != nil { + return nil, err + } + + for x := range prods { + p := strings.Split(prods[x].InstrumentID, delimiterDash) + pairs = append(pairs, + p[0]+delimiterDash+p[1]+o.GetPairFormat(i, false).Delimiter+p[2]) + } + return pairs, nil + + case asset.PerpetualSwap: + prods, err := o.GetSwapContractInformation() + if err != nil { + return nil, err + } + + for x := range prods { + pairs = append(pairs, + prods[x].UnderlyingIndex+ + delimiterDash+ + prods[x].QuoteCurrency+ + o.GetPairFormat(i, false).Delimiter+ + "SWAP") + } + return pairs, nil + case asset.Index: + // This is updated in futures index + return nil, errors.New("index updated in futures") + } + + return nil, fmt.Errorf("%s invalid asset type", o.Name) +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (o *OKEX) UpdateTradablePairs(forceUpdate bool) error { + for x := range o.CurrencyPairs.AssetTypes { + if o.CurrencyPairs.AssetTypes[x] == asset.Index { + // Update from futures + continue + } + + pairs, err := o.FetchTradablePairs(o.CurrencyPairs.AssetTypes[x]) + if err != nil { + return err + } + + if o.CurrencyPairs.AssetTypes[x] == asset.Futures { + var indexPairs []string + for i := range pairs { + indexPairs = append(indexPairs, + strings.Split(pairs[i], delimiterUnderscore)[0]) + } + err = o.UpdatePairs(currency.NewPairsFromStrings(indexPairs), + asset.Index, + false, + forceUpdate) + if err != nil { + return err + } + } + + err = o.UpdatePairs(currency.NewPairsFromStrings(pairs), + o.CurrencyPairs.AssetTypes[x], false, forceUpdate) + if err != nil { + return err + } + } + return nil +} + +// UpdateTicker updates and returns the ticker for a currency pair +func (o *OKEX) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + var tickerData ticker.Price + switch assetType { + case asset.Spot: + resp, err := o.GetSpotAllTokenPairsInformation() + if err != nil { + return tickerData, err + } + for j := range resp { + if !o.GetEnabledPairs(assetType).Contains(resp[j].InstrumentID, true) { + continue + } + tickerData = ticker.Price{ + Last: resp[j].Last, + High: resp[j].High24h, + Low: resp[j].Low24h, + Bid: resp[j].BestBid, + Ask: resp[j].BestAsk, + Volume: resp[j].BaseVolume24h, + QuoteVolume: resp[j].QuoteVolume24h, + Open: resp[j].Open24h, + Pair: resp[j].InstrumentID, + LastUpdated: resp[j].Timestamp, + } + err = ticker.ProcessTicker(o.Name, &tickerData, assetType) + if err != nil { + log.Error(log.Ticker, err) + } + } + + case asset.PerpetualSwap: + resp, err := o.GetAllSwapTokensInformation() + if err != nil { + return tickerData, err + } + + for j := range resp { + p := strings.Split(resp[j].InstrumentID, delimiterDash) + nC := currency.NewPairWithDelimiter(p[0]+delimiterDash+p[1], + p[2], + delimiterUnderscore) + if !o.GetEnabledPairs(assetType).Contains(nC, true) { + continue + } + tickerData = ticker.Price{ + Last: resp[j].Last, + High: resp[j].High24H, + Low: resp[j].Low24H, + Bid: resp[j].BestBid, + Ask: resp[j].BestAsk, + Volume: resp[j].Volume24H, + Pair: nC, + LastUpdated: resp[j].Timestamp, + } + err = ticker.ProcessTicker(o.Name, &tickerData, assetType) + if err != nil { + log.Error(log.Ticker, err) + } + } + + case asset.Futures: + resp, err := o.GetAllFuturesTokenInfo() + if err != nil { + return tickerData, err + } + + for j := range resp { + p := strings.Split(resp[j].InstrumentID, delimiterDash) + nC := currency.NewPairWithDelimiter(p[0]+delimiterDash+p[1], + p[2], + delimiterUnderscore) + if !o.GetEnabledPairs(assetType).Contains(nC, true) { + continue + } + tickerData = ticker.Price{ + Last: resp[j].Last, + High: resp[j].High24h, + Low: resp[j].Low24h, + Bid: resp[j].BestBid, + Ask: resp[j].BestAsk, + Volume: resp[j].Volume24h, + Pair: nC, + LastUpdated: resp[j].Timestamp, + } + err = ticker.ProcessTicker(o.Name, &tickerData, assetType) + if err != nil { + log.Error(log.Ticker, err) + } + } + } + + return ticker.GetTicker(o.Name, p, assetType) +} + +// FetchTicker returns the ticker for a currency pair +func (o *OKEX) FetchTicker(p currency.Pair, assetType asset.Item) (tickerData ticker.Price, err error) { + if assetType == asset.Index { + return tickerData, errors.New("ticker fetching not supported for index") + } + tickerData, err = ticker.GetTicker(o.Name, p, assetType) + if err != nil { + return o.UpdateTicker(p, assetType) + } + return +} diff --git a/exchanges/okgroup/README.md b/exchanges/okgroup/README.md index 32b623af..d9f96ea3 100644 --- a/exchanges/okgroup/README.md +++ b/exchanges/okgroup/README.md @@ -47,22 +47,22 @@ main.go ```go var o exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "OKex" { - y = bot.exchanges[i] +for i := range Bot.Exchanges { + if Bot.Exchanges[i].GetName() == "OKex" { + y = Bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := o.GetTickerPrice() +tick, err := o.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := o.GetOrderbookEx() +ob, err := o.FetchOrderbook() if err != nil { // Handle error } diff --git a/exchanges/okgroup/okgroup.go b/exchanges/okgroup/okgroup.go index 5d2aeb27..0b6b7c93 100644 --- a/exchanges/okgroup/okgroup.go +++ b/exchanges/okgroup/okgroup.go @@ -13,9 +13,9 @@ import ( "time" "github.com/google/go-querystring/query" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -90,89 +90,19 @@ type OKGroup struct { // Spot and contract market error codes as per https://www.okex.com/rest_request.html ErrorCodes map[string]error // Stores for corresponding variable checks - ContractTypes []string - CurrencyPairs []string - ContractPosition []string - Types []string + ContractTypes []string + CurrencyPairsDefaults []string + ContractPosition []string + Types []string // URLs to be overridden by implementations of OKGroup APIURL string APIVersion string WebsocketURL string } -// Setup method sets current configuration details if enabled -func (o *OKGroup) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - o.SetEnabled(false) - } else { - o.Name = exch.Name - o.Enabled = true - o.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - o.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - o.SetAPIKeys(exch.APIKey, exch.APISecret, exch.ClientID, false) - o.SetHTTPClientTimeout(exch.HTTPTimeout) - o.SetHTTPClientUserAgent(exch.HTTPUserAgent) - o.RESTPollingDelay = exch.RESTPollingDelay - o.Verbose = exch.Verbose - o.HTTPDebugging = exch.HTTPDebugging - o.Websocket.SetWsStatusAndConnection(exch.Websocket) - o.BaseCurrencies = exch.BaseCurrencies - o.AvailablePairs = exch.AvailablePairs - o.EnabledPairs = exch.EnabledPairs - err := o.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = o.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = o.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = o.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = o.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = o.Websocket.Setup(o.WsConnect, - o.Subscribe, - o.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - o.WebsocketURL, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - o.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: o.Name, - URL: o.Websocket.GetWebsocketURL(), - ProxyURL: o.Websocket.GetProxyAddress(), - Verbose: o.Verbose, - RateLimit: okGroupWsRateLimit, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - o.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - false, - false, - false, - false, - exch.Name) - } -} - // GetAccountCurrencies returns a list of tradable spot instruments and their properties func (o *OKGroup) GetAccountCurrencies() (resp []GetAccountCurrenciesResponse, _ error) { - return resp, o.SendHTTPRequest(http.MethodGet, okGroupAccountSubsection, okGroupGetAccountCurrencies, nil, &resp, false) + return resp, o.SendHTTPRequest(http.MethodGet, okGroupAccountSubsection, okGroupGetAccountCurrencies, nil, &resp, true) } // GetAccountWalletInformation returns a list of wallets and their properties @@ -242,7 +172,7 @@ func (o *OKGroup) GetAccountDepositHistory(currency string) (resp []GetAccountDe if currency != "" { requestURL = fmt.Sprintf("%v/%v", OKGroupGetAccountDepositHistory, currency) } else { - requestURL = okGroupGetWithdrawalHistory + requestURL = OKGroupGetAccountDepositHistory } return resp, o.SendHTTPRequest(http.MethodGet, okGroupAccountSubsection, requestURL, nil, &resp, true) } @@ -267,17 +197,23 @@ func (o *OKGroup) GetSpotBillDetailsForCurrency(request GetSpotBillDetailsForCur // PlaceSpotOrder token trading only supports limit and market orders (more order types will become available in the future). // You can place an order only if you have enough funds. // Once your order is placed, the amount will be put on hold. -func (o *OKGroup) PlaceSpotOrder(request *PlaceSpotOrderRequest) (resp PlaceSpotOrderResponse, _ error) { +func (o *OKGroup) PlaceSpotOrder(request *PlaceOrderRequest) (resp PlaceOrderResponse, _ error) { + if request.OrderType == "" { + request.OrderType = strconv.Itoa(NormalOrder) + } return resp, o.SendHTTPRequest(http.MethodPost, okGroupTokenSubsection, OKGroupOrders, request, &resp, true) } // PlaceMultipleSpotOrders supports placing multiple orders for specific trading pairs // up to 4 trading pairs, maximum 4 orders for each pair -func (o *OKGroup) PlaceMultipleSpotOrders(request []PlaceSpotOrderRequest) (map[string][]PlaceSpotOrderResponse, []error) { +func (o *OKGroup) PlaceMultipleSpotOrders(request []PlaceOrderRequest) (map[string][]PlaceOrderResponse, []error) { currencyPairOrders := make(map[string]int) - resp := make(map[string][]PlaceSpotOrderResponse) + resp := make(map[string][]PlaceOrderResponse) for i := range request { + if request[i].OrderType == "" { + request[i].OrderType = strconv.Itoa(NormalOrder) + } currencyPairOrders[request[i].InstrumentID]++ } @@ -297,8 +233,8 @@ func (o *OKGroup) PlaceMultipleSpotOrders(request []PlaceSpotOrderRequest) (map[ var orderErrors []error for currency, orderResponse := range resp { - for _, order := range orderResponse { - if !order.Result { + for i := range orderResponse { + if !orderResponse[i].Result { orderErrors = append(orderErrors, fmt.Errorf("order for currency %v failed to be placed", currency)) } } @@ -326,15 +262,15 @@ func (o *OKGroup) CancelMultipleSpotOrders(request CancelMultipleSpotOrdersReque } for currency, orderResponse := range resp { - for _, order := range orderResponse { + for i := range orderResponse { cancellationResponse := CancelMultipleSpotOrdersResponse{ - OrderID: order.OrderID, - Result: order.Result, - ClientOID: order.ClientOID, + OrderID: orderResponse[i].OrderID, + Result: orderResponse[i].Result, + ClientOID: orderResponse[i].ClientOID, } - if !order.Result { - cancellationResponse.Error = fmt.Errorf("order %v for currency %v failed to be cancelled", order.OrderID, currency) + if !orderResponse[i].Result { + cancellationResponse.Error = fmt.Errorf("order %v for currency %v failed to be cancelled", orderResponse[i].OrderID, currency) } resp[currency] = append(resp[currency], cancellationResponse) @@ -377,11 +313,35 @@ func (o *OKGroup) GetSpotTokenPairDetails() (resp []GetSpotTokenPairDetailsRespo return resp, o.SendHTTPRequest(http.MethodGet, okGroupTokenSubsection, OKGroupInstruments, nil, &resp, false) } -// GetSpotOrderBook Getting the order book of a trading pair. Pagination is not supported here. -// The whole book will be returned for one request. Websocket is recommended here. -func (o *OKGroup) GetSpotOrderBook(request GetSpotOrderBookRequest) (resp GetSpotOrderBookResponse, _ error) { - requestURL := fmt.Sprintf("%v/%v/%v%v", OKGroupInstruments, request.InstrumentID, OKGroupGetSpotOrderBook, FormatParameters(request)) - return resp, o.SendHTTPRequest(http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, false) +// GetOrderBook Getting the order book of a trading pair. Pagination is not +// supported here. The whole book will be returned for one request. Websocket is +// recommended here. +func (o *OKGroup) GetOrderBook(request GetOrderBookRequest, a asset.Item) (resp GetOrderBookResponse, _ error) { + var requestType, endpoint string + switch a { + case asset.Spot: + endpoint = OKGroupGetSpotOrderBook + requestType = okGroupTokenSubsection + case asset.Futures: + endpoint = OKGroupGetSpotOrderBook + requestType = "futures" + case asset.PerpetualSwap: + endpoint = "depth" + requestType = "swap" + default: + return resp, errors.New("unhandled asset type") + } + requestURL := fmt.Sprintf("%v/%v/%v/%v", + OKGroupInstruments, + request.InstrumentID, + endpoint, + FormatParameters(request)) + return resp, o.SendHTTPRequest(http.MethodGet, + requestType, + requestURL, + nil, + &resp, + false) } // GetSpotAllTokenPairsInformation Get the last traded price, best bid/ask price, 24 hour trading volume and more info of all trading pairs. @@ -467,14 +427,14 @@ func (o *OKGroup) RepayMarginLoan(request RepayMarginLoanRequest) (resp RepayMar // PlaceMarginOrder OKEx API only supports limit and market orders (more orders will become available in the future). // You can place an order only if you have enough funds. Once your order is placed, the amount will be put on hold. -func (o *OKGroup) PlaceMarginOrder(request *PlaceSpotOrderRequest) (resp PlaceSpotOrderResponse, _ error) { +func (o *OKGroup) PlaceMarginOrder(request *PlaceOrderRequest) (resp PlaceOrderResponse, _ error) { return resp, o.SendHTTPRequest(http.MethodPost, okGroupMarginTradingSubsection, OKGroupOrders, request, &resp, true) } // PlaceMultipleMarginOrders Place multiple orders for specific trading pairs (up to 4 trading pairs, maximum 4 orders each) -func (o *OKGroup) PlaceMultipleMarginOrders(request []PlaceSpotOrderRequest) (map[string][]PlaceSpotOrderResponse, []error) { +func (o *OKGroup) PlaceMultipleMarginOrders(request []PlaceOrderRequest) (map[string][]PlaceOrderResponse, []error) { currencyPairOrders := make(map[string]int) - resp := make(map[string][]PlaceSpotOrderResponse) + resp := make(map[string][]PlaceOrderResponse) for i := range request { currencyPairOrders[request[i].InstrumentID]++ } @@ -494,8 +454,8 @@ func (o *OKGroup) PlaceMultipleMarginOrders(request []PlaceSpotOrderRequest) (ma var orderErrors []error for currency, orderResponse := range resp { - for _, order := range orderResponse { - if !order.Result { + for i := range orderResponse { + if !orderResponse[i].Result { orderErrors = append(orderErrors, fmt.Errorf("order for currency %v failed to be placed", currency)) } } @@ -524,9 +484,9 @@ func (o *OKGroup) CancelMultipleMarginOrders(request CancelMultipleSpotOrdersReq var orderErrors []error for currency, orderResponse := range resp { - for _, order := range orderResponse { - if !order.Result { - orderErrors = append(orderErrors, fmt.Errorf("order %v for currency %v failed to be cancelled", order.OrderID, currency)) + for i := range orderResponse { + if !orderResponse[i].Result { + orderErrors = append(orderErrors, fmt.Errorf("order %v for currency %v failed to be cancelled", orderResponse[i].OrderID, currency)) } } } @@ -563,7 +523,7 @@ func (o *OKGroup) GetMarginTransactionDetails(request GetSpotTransactionDetailsR func FormatParameters(request interface{}) (parameters string) { v, err := query.Values(request) if err != nil { - log.Errorf("Could not parse %v to URL values. Check that the type has url fields", reflect.TypeOf(request).Name()) + log.Errorf(log.ExchangeSys, "Could not parse %v to URL values. Check that the type has url fields", reflect.TypeOf(request).Name()) return } urlEncodedValues := v.Encode() @@ -596,42 +556,42 @@ func (o *OKGroup) GetErrorCode(code interface{}) error { // path with a JSON payload (of present) // URL arguments must be in the request path and not as url.URL values func (o *OKGroup) SendHTTPRequest(httpMethod, requestType, requestPath string, data, result interface{}, authenticated bool) (err error) { - if authenticated && !o.AuthenticatedAPISupport { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, o.Name) + if authenticated && !o.AllowAuthenticatedRequest() { + return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, + o.Name) } - utcTime := time.Now().UTC() - iso := utcTime.String() - isoBytes := []byte(iso) - iso = string(isoBytes[:10]) + "T" + string(isoBytes[11:23]) + "Z" + utcTime := time.Now().UTC().Format(time.RFC3339) payload := []byte("") if data != nil { - payload, err = common.JSONEncode(data) + payload, err = json.Marshal(data) if err != nil { return errors.New("sendHTTPRequest: Unable to JSON request") } if o.Verbose { - log.Debugf("Request JSON: %s\n", payload) + log.Debugf(log.ExchangeSys, "Request JSON: %s\n", payload) } } - path := o.APIUrl + requestType + o.APIVersion + requestPath + path := o.API.Endpoints.URL + requestType + o.APIVersion + requestPath if o.Verbose { - log.Debugf("Sending %v request to %s \n", requestType, path) + log.Debugf(log.ExchangeSys, "Sending %v request to %s \n", requestType, path) } headers := make(map[string]string) headers["Content-Type"] = "application/json" if authenticated { - signPath := fmt.Sprintf("/%v%v%v%v", OKGroupAPIPath, requestType, o.APIVersion, requestPath) - hmac := common.GetHMAC(common.HashSHA256, []byte(iso+httpMethod+signPath+string(payload)), []byte(o.APISecret)) - base64 := common.Base64Encode(hmac) - headers["OK-ACCESS-KEY"] = o.APIKey - headers["OK-ACCESS-SIGN"] = base64 - headers["OK-ACCESS-TIMESTAMP"] = iso - headers["OK-ACCESS-PASSPHRASE"] = o.ClientID + signPath := fmt.Sprintf("/%v%v%v%v", OKGroupAPIPath, + requestType, o.APIVersion, requestPath) + hmac := crypto.GetHMAC(crypto.HashSHA256, + []byte(utcTime+httpMethod+signPath+string(payload)), + []byte(o.API.Credentials.Secret)) + headers["OK-ACCESS-KEY"] = o.API.Credentials.Key + headers["OK-ACCESS-SIGN"] = crypto.Base64Encode(hmac) + headers["OK-ACCESS-TIMESTAMP"] = utcTime + headers["OK-ACCESS-PASSPHRASE"] = o.API.Credentials.ClientID } var intermediary json.RawMessage @@ -656,7 +616,7 @@ func (o *OKGroup) SendHTTPRequest(httpMethod, requestType, requestPath string, d return err } - err = common.JSONDecode(intermediary, &errCap) + err = json.Unmarshal(intermediary, &errCap) if err == nil { if errCap.ErrorMessage != "" { return fmt.Errorf("error: %v", errCap.ErrorMessage) @@ -670,7 +630,7 @@ func (o *OKGroup) SendHTTPRequest(httpMethod, requestType, requestPath string, d } } - return common.JSONDecode(intermediary, result) + return json.Unmarshal(intermediary, result) } // SetCheckVarDefaults sets main variables that will be used in requests because @@ -678,7 +638,7 @@ func (o *OKGroup) SendHTTPRequest(httpMethod, requestType, requestPath string, d // to check on this, this end. func (o *OKGroup) SetCheckVarDefaults() { o.ContractTypes = []string{"this_week", "next_week", "quarter"} - o.CurrencyPairs = []string{"btc_usd", "ltc_usd", "eth_usd", "etc_usd", "bch_usd"} + o.CurrencyPairsDefaults = []string{"btc_usd", "ltc_usd", "eth_usd", "etc_usd", "bch_usd"} o.Types = []string{"1min", "3min", "5min", "15min", "30min", "1day", "3day", "1week", "1hour", "2hour", "4hour", "6hour", "12hour"} o.ContractPosition = []string{"1", "2", "3", "4"} diff --git a/exchanges/okgroup/okgroup_types.go b/exchanges/okgroup/okgroup_types.go index 8cfbf904..6a64b4a1 100644 --- a/exchanges/okgroup/okgroup_types.go +++ b/exchanges/okgroup/okgroup_types.go @@ -2,15 +2,25 @@ package okgroup import ( "time" + + "github.com/thrasher-corp/gocryptotrader/currency" +) + +// Order types +const ( + NormalOrder = iota + PostOnlyOrder + FillOrKillOrder + ImmediateOrCancelOrder ) // GetAccountCurrenciesResponse response data for GetAccountCurrencies type GetAccountCurrenciesResponse struct { - CanDeposit int64 `json:"can_deposit"` - CanWithdraw int64 `json:"can_withdraw"` - Currency string `json:"currency"` - MinWithdrawal float64 `json:"min_withdrawal"` Name string `json:"name"` + Currency string `json:"currency"` + CanDeposit int `json:"can_deposit,string"` + CanWithdraw int `json:"can_withdraw,string"` + MinWithdrawal float64 `json:"min_withdrawal,string"` } // WalletInformationResponse response data for WalletInformation @@ -62,22 +72,22 @@ type AccountWithdrawResponse struct { // GetAccountWithdrawalFeeResponse response data for GetAccountWithdrawalFee type GetAccountWithdrawalFeeResponse struct { Currency string `json:"currency"` - MinFee float64 `json:"min_fee"` - MaxFee float64 `json:"max_fee"` + MinFee float64 `json:"min_fee,string"` + MaxFee float64 `json:"max_fee,string"` } // WithdrawalHistoryResponse response data for WithdrawalHistoryResponse type WithdrawalHistoryResponse struct { - Amount float64 `json:"amount"` - Currency string `json:"currency"` - Fee string `json:"fee"` - From string `json:"from"` - Status int64 `json:"status"` - Timestamp time.Time `json:"timestamp"` - To string `json:"to"` - Txid string `json:"txid"` - PaymentID string `json:"payment_id"` - Tag string `json:"tag"` + Amount float64 `json:"amount,string"` + Currency string `json:"currency"` + Fee string `json:"fee"` + From string `json:"from"` + Status int64 `json:"status,string"` + Timestamp time.Time `json:"timestamp"` + To string `json:"to"` + TransactionID string `json:"txid"` + PaymentID string `json:"payment_id"` + Tag string `json:"tag"` } // GetAccountBillDetailsRequest request data for GetAccountBillDetailsRequest @@ -110,11 +120,12 @@ type GetDepositAddressResponse struct { // GetAccountDepositHistoryResponse response data for GetAccountDepositHistory type GetAccountDepositHistoryResponse struct { - Amount float64 `json:"amount"` + Amount float64 `json:"amount,string"` Currency string `json:"currency"` - Status int64 `json:"status"` - Timestamp time.Time `json:"timestamp"` + From string `json:"from"` To string `json:"to"` + Timestamp time.Time `json:"timestamp"` + Status int64 `json:"status,string"` TransactionID string `json:"txid"` } @@ -154,20 +165,21 @@ type SpotBillDetails struct { InstrumentID string `json:"instrument_id"` } -// PlaceSpotOrderRequest request data for PlaceSpotOrder -type PlaceSpotOrderRequest struct { +// PlaceOrderRequest request data for placing an order +type PlaceOrderRequest struct { ClientOID string `json:"client_oid,omitempty"` // the order ID customized by yourself Type string `json:"type"` // limit / market(default: limit) Side string `json:"side"` // buy or sell InstrumentID string `json:"instrument_id"` // trading pair - MarginTrading string `json:"margin_trading"` // order type (The request value is 1) + MarginTrading string `json:"margin_trading"` // margin trading + OrderType string `json:"order_type"` // order type (0: Normal order (Unfilled and 0 imply normal limit order) 1: Post only 2: Fill or Kill 3: Immediate Or Cancel Size string `json:"size"` Notional string `json:"notional,omitempty"` // Price string `json:"price,omitempty"` // price (Limit order only) } -// PlaceSpotOrderResponse response data for PlaceSpotOrder -type PlaceSpotOrderResponse struct { +// PlaceOrderResponse response data for PlaceSpotOrder +type PlaceOrderResponse struct { ClientOid string `json:"client_oid"` OrderID string `json:"order_id"` Result bool `json:"result"` @@ -272,15 +284,15 @@ type GetSpotTokenPairDetailsResponse struct { TickSize string `json:"tick_size"` } -// GetSpotOrderBookRequest request data for GetSpotOrderBook -type GetSpotOrderBookRequest struct { +// GetOrderBookRequest request data for GetOrderBook +type GetOrderBookRequest struct { Size int64 `url:"size,string,omitempty"` // [optional] number of results per request. Maximum 200 Depth float64 `url:"depth,string,omitempty"` // [optional] the aggregation of the book. e.g . 0.1,0.001 InstrumentID string `url:"-"` // [required] trading pairs } -// GetSpotOrderBookResponse response data for GetSpotOrderBook -type GetSpotOrderBookResponse struct { +// GetOrderBookResponse response data +type GetOrderBookResponse struct { Timestamp time.Time `json:"timestamp"` Asks [][]string `json:"asks"` // [[0]: "Price", [1]: "Size", [2]: "Num_orders"], ... Bids [][]string `json:"bids"` // [[0]: "Price", [1]: "Size", [2]: "Num_orders"], ... @@ -288,16 +300,16 @@ type GetSpotOrderBookResponse struct { // GetSpotTokenPairsInformationResponse response data for GetSpotTokenPairsInformation type GetSpotTokenPairsInformationResponse struct { - BaseVolume24h float64 `json:"base_volume_24h,string"` // 24 trading volume of the base currency - BestAsk float64 `json:"best_ask,string"` // best ask price - BestBid float64 `json:"best_bid,string"` // best bid price - High24h float64 `json:"high_24h,string"` // 24 hour high - InstrumentID string `json:"instrument_id"` // trading pair - Last float64 `json:"last,string"` // last traded price - Low24h float64 `json:"low_24h,string"` // 24 hour low - Open24h float64 `json:"open_24h,string"` // 24 hour open - QuoteVolume24h float64 `json:"quote_volume_24h,string"` // 24 trading volume of the quote currency - Timestamp time.Time `json:"timestamp"` + BaseVolume24h float64 `json:"base_volume_24h,string"` // 24 trading volume of the base currency + BestAsk float64 `json:"best_ask,string"` // best ask price + BestBid float64 `json:"best_bid,string"` // best bid price + High24h float64 `json:"high_24h,string"` // 24 hour high + InstrumentID currency.Pair `json:"instrument_id"` // trading pair + Last float64 `json:"last,string"` // last traded price + Low24h float64 `json:"low_24h,string"` // 24 hour low + Open24h float64 `json:"open_24h,string"` // 24 hour open + QuoteVolume24h float64 `json:"quote_volume_24h,string"` // 24 trading volume of the quote currency + Timestamp time.Time `json:"timestamp"` } // GetSpotFilledOrdersInformationRequest request data for GetSpotFilledOrdersInformation @@ -682,27 +694,6 @@ type GetFuturesContractInformationResponse struct { UnderlyingIndex string `json:"underlying_index"` } -// GetFuturesOrderBookRequest request data for GetFuturesOrderBook -type GetFuturesOrderBookRequest struct { - InstrumentID string `url:"-"` // [required] Contract ID, e.g. "BTC-USD-180213" - Size int64 `url:"size,omitempty"` // [optional] The size of the price range (max: 200) -} - -// FuturesOrderbookItem stores an individual futures orderbook item -type FuturesOrderbookItem struct { - Price float64 - Size int64 - ForceLiquidatedOrders int64 // Number of force liquidated orders - NumberOrders int64 // Number of orders on the price -} - -// GetFuturesOrderBookResponse response data for GetFuturesOrderBook -type GetFuturesOrderBookResponse struct { - Asks []FuturesOrderbookItem - Bids []FuturesOrderbookItem - Timestamp time.Time -} - // GetFuturesTokenInfoResponse response data for GetFuturesOrderBook type GetFuturesTokenInfoResponse struct { BestAsk float64 `json:"best_ask,string"` @@ -712,7 +703,7 @@ type GetFuturesTokenInfoResponse struct { Last float64 `json:"last,string"` Low24h float64 `json:"low_24h,string"` Timestamp time.Time `json:"timestamp"` - Volume24h int64 `json:"volume_24h,string"` + Volume24h float64 `json:"volume_24h,string"` } // GetFuturesFilledOrderRequest request data for GetFuturesFilledOrder @@ -1346,12 +1337,14 @@ type WebsocketDataWrapper struct { // WebsocketTickerData contains formatted data for ticker related websocket responses type WebsocketTickerData struct { - High24H float64 `json:"high_24h,string,omitempty"` - Last float64 `json:"last,string,omitempty"` - BestBid float64 `json:"best_bid,string,omitempty"` - BestAsk float64 `json:"best_ask,string,omitempty"` - Low24H float64 `json:"low_24h,string,omitempty"` - Volume24H float64 `json:"volume_24h,string,omitempty"` + BaseVolume24h float64 `json:"base_volume_24h,string,omitempty"` + BestAsk float64 `json:"best_ask,string,omitempty"` + BestBid float64 `json:"best_bid,string,omitempty"` + High24h float64 `json:"high_24h,string,omitempty"` + Last float64 `json:"last,string,omitempty"` + Low24h float64 `json:"low_24h,string,omitempty"` + Open24h float64 `json:"open_24h,string,omitempty"` + QuoteVolume24h float64 `json:"quote_volume_24h,string,omitempty"` } // WebsocketTradeResponse contains formatted data for trade related websocket responses @@ -1514,7 +1507,7 @@ type WebsocketSpotOrderResponse struct { Notional float64 `json:"notional,string"` Size float64 `json:"size,string"` Status string `json:"status"` - MarginTrading int64 `json:"margin_trading"` + MarginTrading int64 `json:"margin_trading,omitempty"` Type string `json:"type"` // Price A member, but part already exists as part of WebsocketDataResponse // InstrumentID A member, but part already exists as part of WebsocketDataResponse diff --git a/exchanges/okgroup/okgroup_websocket.go b/exchanges/okgroup/okgroup_websocket.go index 41b44ee3..c1ae5fb5 100644 --- a/exchanges/okgroup/okgroup_websocket.go +++ b/exchanges/okgroup/okgroup_websocket.go @@ -1,6 +1,7 @@ package okgroup import ( + "encoding/json" "errors" "fmt" "hash/crc32" @@ -11,9 +12,10 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" @@ -138,11 +140,36 @@ const ( okGroupWsFuturesOrder = okGroupWsFuturesSubsection + okGroupWsOrder okGroupWsRateLimit = 30 + + allowableIterations = 25 + delimiterColon = ":" + delimiterDash = "-" + delimiterUnderscore = "_" ) -// orderbookMutex Ensures if two entries arrive at once, only one can be processed at a time +// orderbookMutex Ensures if two entries arrive at once, only one can be +// processed at a time var orderbookMutex sync.Mutex -var defaultSubscribedChannels = []string{okGroupWsSpotDepth, okGroupWsSpotCandle300s, okGroupWsSpotTicker, okGroupWsSpotTrade} + +var defaultSpotSubscribedChannels = []string{okGroupWsSpotDepth, + okGroupWsSpotCandle300s, + okGroupWsSpotTicker, + okGroupWsSpotTrade} + +var defaultFuturesSubscribedChannels = []string{okGroupWsFuturesDepth, + okGroupWsFuturesCandle300s, + okGroupWsFuturesTicker, + okGroupWsFuturesTrade} + +var defaultIndexSubscribedChannels = []string{okGroupWsIndexCandle300s, + okGroupWsIndexTicker} + +var defaultSwapSubscribedChannels = []string{okGroupWsSwapDepth, + okGroupWsSwapCandle300s, + okGroupWsSwapTicker, + okGroupWsSwapTrade, + okGroupWsSwapFundingRate, + okGroupWsSwapMarkPrice} // WsConnect initiates a websocket connection func (o *OKGroup) WsConnect() error { @@ -155,17 +182,19 @@ func (o *OKGroup) WsConnect() error { return err } if o.Verbose { - log.Debugf("Successful connection to %v", + log.Debugf(log.ExchangeSys, "Successful connection to %v\n", o.Websocket.GetWebsocketURL()) } wg := sync.WaitGroup{} - wg.Add(2) + wg.Add(1) go o.WsHandleData(&wg) - go o.wsPingHandler(&wg) if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { err = o.WsLogin() if err != nil { - log.Errorf("%v - authentication failed: %v", o.Name, err) + log.Errorf(log.ExchangeSys, + "%v - authentication failed: %v\n", + o.Name, + err) } } @@ -175,33 +204,6 @@ func (o *OKGroup) WsConnect() error { return nil } -// wsPingHandler sends a message "ping" every 27 to maintain the connection to the websocket -func (o *OKGroup) wsPingHandler(wg *sync.WaitGroup) { - o.Websocket.Wg.Add(1) - defer o.Websocket.Wg.Done() - - ticker := time.NewTicker(time.Second * 27) - defer ticker.Stop() - - wg.Done() - - for { - select { - case <-o.Websocket.ShutdownC: - return - - case <-ticker.C: - err := o.WebsocketConn.SendMessage("ping") - if o.Verbose { - log.Debugf("%v sending ping", o.GetName()) - } - if err != nil { - o.Websocket.DataHandler <- err - } - } - } -} - // WsHandleData handles the read data from the websocket connection func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { o.Websocket.Wg.Add(1) @@ -219,12 +221,12 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { default: resp, err := o.WebsocketConn.ReadMessage() if err != nil { - o.Websocket.DataHandler <- err + o.Websocket.ReadMessageErrors <- err return } o.Websocket.TrafficAlert <- struct{}{} var dataResponse WebsocketDataResponse - err = common.JSONDecode(resp.Raw, &dataResponse) + err = json.Unmarshal(resp.Raw, &dataResponse) if err == nil && dataResponse.Table != "" { if len(dataResponse.Data) > 0 { o.WsHandleDataResponse(&dataResponse) @@ -232,25 +234,31 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { continue } var errorResponse WebsocketErrorResponse - err = common.JSONDecode(resp.Raw, &errorResponse) + err = json.Unmarshal(resp.Raw, &errorResponse) if err == nil && errorResponse.ErrorCode > 0 { if o.Verbose { - log.Debugf("WS Error Event: %v Message: %v", errorResponse.Event, errorResponse.Message) + log.Debugf(log.ExchangeSys, + "WS Error Event: %v Message: %v for %s", + errorResponse.Event, + errorResponse.Message, + o.Name) } o.WsHandleErrorResponse(errorResponse) continue } var eventResponse WebsocketEventResponse - err = common.JSONDecode(resp.Raw, &eventResponse) + err = json.Unmarshal(resp.Raw, &eventResponse) if err == nil && eventResponse.Event != "" { if eventResponse.Event == "login" { o.Websocket.SetCanUseAuthenticatedEndpoints(eventResponse.Success) } if o.Verbose { - log.Debugf("WS Event: %v on Channel: %v", eventResponse.Event, eventResponse.Channel) + log.Debugf(log.ExchangeSys, + "WS Event: %v on Channel: %v for %s", + eventResponse.Event, + eventResponse.Channel, + o.Name) } - o.Websocket.DataHandler <- eventResponse - continue } } } @@ -259,14 +267,21 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { // WsLogin sends a login request to websocket to enable access to authenticated endpoints func (o *OKGroup) WsLogin() error { o.Websocket.SetCanUseAuthenticatedEndpoints(true) - utcTime := time.Now().UTC() - unixTime := utcTime.Unix() + unixTime := time.Now().UTC().Unix() signPath := "/users/self/verify" - hmac := common.GetHMAC(common.HashSHA256, []byte(fmt.Sprintf("%v", unixTime)+http.MethodGet+signPath), []byte(o.APISecret)) - base64 := common.Base64Encode(hmac) + hmac := crypto.GetHMAC(crypto.HashSHA256, + []byte(strconv.FormatInt(unixTime, 10)+http.MethodGet+signPath), + []byte(o.API.Credentials.Secret), + ) + base64 := crypto.Base64Encode(hmac) request := WebsocketEventRequest{ Operation: "login", - Arguments: []string{o.APIKey, o.ClientID, fmt.Sprintf("%v", unixTime), base64}, + Arguments: []string{ + o.API.Credentials.Key, + o.API.Credentials.ClientID, + strconv.FormatInt(unixTime, 10), + base64, + }, } err := o.WebsocketConn.SendMessage(request) if err != nil { @@ -279,9 +294,11 @@ func (o *OKGroup) WsLogin() error { // WsHandleErrorResponse sends an error message to ws handler func (o *OKGroup) WsHandleErrorResponse(event WebsocketErrorResponse) { errorMessage := fmt.Sprintf("%v error - %v message: %s ", - o.GetName(), event.ErrorCode, event.Message) + o.Name, + event.ErrorCode, + event.Message) if o.Verbose { - log.Error(errorMessage) + log.Error(log.ExchangeSys, errorMessage) } o.Websocket.DataHandler <- fmt.Errorf(errorMessage) } @@ -305,29 +322,56 @@ func (o *OKGroup) GetWsChannelWithoutOrderType(table string) string { // GetAssetTypeFromTableName gets the asset type from the table name // eg "spot/ticker:BTCUSD" results in "SPOT" -func (o *OKGroup) GetAssetTypeFromTableName(table string) string { +func (o *OKGroup) GetAssetTypeFromTableName(table string) asset.Item { assetIndex := strings.Index(table, "/") - return strings.ToUpper(table[:assetIndex]) + switch table[:assetIndex] { + case asset.Futures.String(): + return asset.Futures + case asset.Spot.String(): + return asset.Spot + case "swap": + return asset.PerpetualSwap + case asset.Index.String(): + return asset.Index + default: + log.Warnf(log.ExchangeSys, "%s unhandled asset type %s", + o.Name, + table[:assetIndex]) + return asset.Item(table[:assetIndex]) + } } // WsHandleDataResponse classifies the WS response and sends to appropriate handler func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) { switch o.GetWsChannelWithoutOrderType(response.Table) { - case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s, okGroupWsCandle900s, - okGroupWsCandle1800s, okGroupWsCandle3600s, okGroupWsCandle7200s, okGroupWsCandle14400s, - okGroupWsCandle21600s, okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s: + case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s, + okGroupWsCandle900s, okGroupWsCandle1800s, okGroupWsCandle3600s, + okGroupWsCandle7200s, okGroupWsCandle14400s, okGroupWsCandle21600s, + okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s: o.wsProcessCandles(response) case okGroupWsDepth, okGroupWsDepth5: // Locking, orderbooks cannot be processed out of order orderbookMutex.Lock() err := o.WsProcessOrderBook(response) if err != nil { - pair := currency.NewPairDelimiter(response.Data[0].InstrumentID, "-") - channelToResubscribe := wshandler.WebsocketChannelSubscription{ - Channel: response.Table, - Currency: pair, + for i := range response.Data { + a := o.GetAssetTypeFromTableName(response.Table) + var c currency.Pair + switch a { + case asset.Futures, asset.PerpetualSwap: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterDash) + default: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) + } + + channelToResubscribe := wshandler.WebsocketChannelSubscription{ + Channel: response.Table, + Currency: c, + } + o.Websocket.ResubscribeToChannel(channelToResubscribe) } - o.Websocket.ResubscribeToChannel(channelToResubscribe) } orderbookMutex.Unlock() case okGroupWsTicker: @@ -335,34 +379,51 @@ func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) { case okGroupWsTrade: o.wsProcessTrades(response) default: - logDataResponse(response) + logDataResponse(response, o.Name) } } // logDataResponse will log the details of any websocket data event // where there is no websocket datahandler for it -func logDataResponse(response *WebsocketDataResponse) { +func logDataResponse(response *WebsocketDataResponse, exchangeName string) { for i := range response.Data { - log.Errorf("Unhandled channel: '%v'. Instrument '%v' Timestamp '%v', Data '%v", + log.Warnf(log.ExchangeSys, + "%s Unhandled channel: '%v'. Instrument '%v' Timestamp '%v'", + exchangeName, response.Table, response.Data[i].InstrumentID, - response.Data[i].Timestamp, - response.Data[i]) + response.Data[i].Timestamp) } } // wsProcessTickers converts ticker data and sends it to the datahandler func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) { for i := range response.Data { - instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-") + a := o.GetAssetTypeFromTableName(response.Table) + var c currency.Pair + switch a { + case asset.Futures, asset.PerpetualSwap: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore) + default: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) + } + o.Websocket.DataHandler <- wshandler.TickerData{ - Timestamp: response.Data[i].Timestamp, - Exchange: o.GetName(), - AssetType: o.GetAssetTypeFromTableName(response.Table), - HighPrice: response.Data[i].High24H, - LowPrice: response.Data[i].Low24H, - ClosePrice: response.Data[i].Last, - Pair: instrument, + Exchange: o.Name, + Open: response.Data[i].Open24h, + Close: response.Data[i].Last, + Volume: response.Data[i].BaseVolume24h, + QuoteVolume: response.Data[i].QuoteVolume24h, + High: response.Data[i].High24h, + Low: response.Data[i].Low24h, + Bid: response.Data[i].BestBid, + Ask: response.Data[i].BestAsk, + Last: response.Data[i].Last, + Timestamp: response.Data[i].Timestamp, + AssetType: o.GetAssetTypeFromTableName(response.Table), + Pair: c, } } } @@ -370,13 +431,23 @@ func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) { // wsProcessTrades converts trade data and sends it to the datahandler func (o *OKGroup) wsProcessTrades(response *WebsocketDataResponse) { for i := range response.Data { - instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-") + a := o.GetAssetTypeFromTableName(response.Table) + var c currency.Pair + switch a { + case asset.Futures, asset.PerpetualSwap: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore) + default: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) + } + o.Websocket.DataHandler <- wshandler.TradeData{ Amount: response.Data[i].Size, AssetType: o.GetAssetTypeFromTableName(response.Table), - CurrencyPair: instrument, + CurrencyPair: c, EventTime: time.Now().Unix(), - Exchange: o.GetName(), + Exchange: o.Name, Price: response.Data[i].WebsocketTradeResponse.Price, Side: response.Data[i].Side, Timestamp: response.Data[i].Timestamp, @@ -387,10 +458,24 @@ func (o *OKGroup) wsProcessTrades(response *WebsocketDataResponse) { // wsProcessCandles converts candle data and sends it to the data handler func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) { for i := range response.Data { - instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-") - timeData, err := time.Parse(time.RFC3339Nano, response.Data[i].WebsocketCandleResponse.Candle[0]) + a := o.GetAssetTypeFromTableName(response.Table) + var c currency.Pair + switch a { + case asset.Futures, asset.PerpetualSwap: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore) + default: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) + } + + timeData, err := time.Parse(time.RFC3339Nano, + response.Data[i].WebsocketCandleResponse.Candle[0]) if err != nil { - log.Warnf("%v Time data could not be parsed: %v", o.GetName(), response.Data[i].Candle[0]) + log.Errorf(log.ExchangeSys, + "%v Time data could not be parsed: %v", + o.Name, + response.Data[i].Candle[0]) } candleIndex := strings.LastIndex(response.Table, okGroupWsCandle) @@ -402,16 +487,36 @@ func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) { klineData := wshandler.KlineData{ AssetType: o.GetAssetTypeFromTableName(response.Table), - Pair: instrument, - Exchange: o.GetName(), + Pair: c, + Exchange: o.Name, Timestamp: timeData, Interval: candleInterval, } - klineData.OpenPrice, _ = strconv.ParseFloat(response.Data[i].Candle[1], 64) - klineData.HighPrice, _ = strconv.ParseFloat(response.Data[i].Candle[2], 64) - klineData.LowPrice, _ = strconv.ParseFloat(response.Data[i].Candle[3], 64) - klineData.ClosePrice, _ = strconv.ParseFloat(response.Data[i].Candle[4], 64) - klineData.Volume, _ = strconv.ParseFloat(response.Data[i].Candle[5], 64) + klineData.OpenPrice, err = strconv.ParseFloat(response.Data[i].Candle[1], 64) + if err != nil { + o.Websocket.DataHandler <- err + continue + } + klineData.HighPrice, err = strconv.ParseFloat(response.Data[i].Candle[2], 64) + if err != nil { + o.Websocket.DataHandler <- err + continue + } + klineData.LowPrice, err = strconv.ParseFloat(response.Data[i].Candle[3], 64) + if err != nil { + o.Websocket.DataHandler <- err + continue + } + klineData.ClosePrice, err = strconv.ParseFloat(response.Data[i].Candle[4], 64) + if err != nil { + o.Websocket.DataHandler <- err + continue + } + klineData.Volume, err = strconv.ParseFloat(response.Data[i].Candle[5], 64) + if err != nil { + o.Websocket.DataHandler <- err + continue + } o.Websocket.DataHandler <- klineData } @@ -420,57 +525,96 @@ func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) { // WsProcessOrderBook Validates the checksum and updates internal orderbook values func (o *OKGroup) WsProcessOrderBook(response *WebsocketDataResponse) (err error) { for i := range response.Data { - instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-") + a := o.GetAssetTypeFromTableName(response.Table) + var c currency.Pair + switch a { + case asset.Futures, asset.PerpetualSwap: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore) + default: + f := strings.Split(response.Data[i].InstrumentID, delimiterDash) + c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash) + } + if response.Action == okGroupWsOrderbookPartial { - err = o.WsProcessPartialOrderBook(&response.Data[i], instrument, response.Table) + err = o.WsProcessPartialOrderBook(&response.Data[i], c, a) + if err != nil { + return + } } else if response.Action == okGroupWsOrderbookUpdate { - err = o.WsProcessUpdateOrderbook(&response.Data[i], instrument, response.Table) + if len(response.Data[i].Asks) == 0 && len(response.Data[i].Bids) == 0 { + continue + } + err = o.WsProcessUpdateOrderbook(&response.Data[i], c, a) + if err != nil { + return + } } } return } // AppendWsOrderbookItems adds websocket orderbook data bid/asks into an orderbook item array -func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) (orderbookItems []orderbook.Item) { +func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) ([]orderbook.Item, error) { + var items []orderbook.Item for j := range entries { - amount, _ := strconv.ParseFloat(entries[j][1].(string), 64) - price, _ := strconv.ParseFloat(entries[j][0].(string), 64) - orderbookItems = append(orderbookItems, orderbook.Item{ - Amount: amount, - Price: price, - }) + amount, err := strconv.ParseFloat(entries[j][1].(string), 64) + if err != nil { + return nil, err + } + price, err := strconv.ParseFloat(entries[j][0].(string), 64) + if err != nil { + return nil, err + } + items = append(items, orderbook.Item{Amount: amount, Price: price}) } - return + return items, nil } // WsProcessPartialOrderBook takes websocket orderbook data and creates an orderbook // Calculates checksum to ensure it is valid -func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, tableName string) error { +func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, a asset.Item) error { signedChecksum := o.CalculatePartialOrderbookChecksum(wsEventData) if signedChecksum != wsEventData.Checksum { - return fmt.Errorf("channel: %v. Orderbook partial for %v checksum invalid", tableName, instrument) + return fmt.Errorf("%s channel: %s. Orderbook partial for %v checksum invalid", + o.Name, + a, + instrument) } if o.Verbose { - log.Debug("Passed checksum!") - } - asks := o.AppendWsOrderbookItems(wsEventData.Asks) - bids := o.AppendWsOrderbookItems(wsEventData.Bids) - newOrderBook := orderbook.Base{ - Asks: asks, - Bids: bids, - AssetType: o.GetAssetTypeFromTableName(tableName), - LastUpdated: wsEventData.Timestamp, - Pair: instrument, - ExchangeName: o.GetName(), + log.Debugf(log.ExchangeSys, + "%s passed checksum for instrument %s", + o.Name, + instrument) } - err := o.Websocket.Orderbook.LoadSnapshot(&newOrderBook, true) + asks, err := o.AppendWsOrderbookItems(wsEventData.Asks) if err != nil { return err } + + bids, err := o.AppendWsOrderbookItems(wsEventData.Bids) + if err != nil { + return err + } + + newOrderBook := orderbook.Base{ + Asks: asks, + Bids: bids, + AssetType: a, + LastUpdated: wsEventData.Timestamp, + Pair: instrument, + ExchangeName: o.Name, + } + + err = o.Websocket.Orderbook.LoadSnapshot(&newOrderBook) + if err != nil { + return err + } + o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Exchange: o.GetName(), - Asset: o.GetAssetTypeFromTableName(tableName), + Exchange: o.Name, + Asset: a, Pair: instrument, } return nil @@ -478,121 +622,209 @@ func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketDataWrapper, i // WsProcessUpdateOrderbook updates an existing orderbook using websocket data // After merging WS data, it will sort, validate and finally update the existing orderbook -func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, tableName string) error { +func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, a asset.Item) error { update := wsorderbook.WebsocketOrderbookUpdate{ - AssetType: orderbook.Spot, - CurrencyPair: instrument, - UpdateTime: wsEventData.Timestamp, + Asset: a, + Pair: instrument, + UpdateTime: wsEventData.Timestamp, } - update.Asks = o.AppendWsOrderbookItems(wsEventData.Asks) - update.Bids = o.AppendWsOrderbookItems(wsEventData.Bids) - err := o.Websocket.Orderbook.Update(&update) + + var err error + update.Asks, err = o.AppendWsOrderbookItems(wsEventData.Asks) if err != nil { - log.Error(err) + return err } - updatedOb := o.Websocket.Orderbook.GetOrderbook(instrument, orderbook.Spot) + update.Bids, err = o.AppendWsOrderbookItems(wsEventData.Bids) + if err != nil { + return err + } + + err = o.Websocket.Orderbook.Update(&update) + if err != nil { + return err + } + + updatedOb := o.Websocket.Orderbook.GetOrderbook(instrument, a) checksum := o.CalculateUpdateOrderbookChecksum(updatedOb) - if checksum == wsEventData.Checksum { - if o.Verbose { - log.Debug("Orderbook valid") - } - o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Exchange: o.GetName(), - Asset: o.GetAssetTypeFromTableName(tableName), - Pair: instrument, - } - } else { - if o.Verbose { - log.Debug("Orderbook invalid") - } - return fmt.Errorf("channel: %v. Orderbook update for %v checksum invalid. Received %v Calculated %v", tableName, instrument, wsEventData.Checksum, checksum) + + if checksum != wsEventData.Checksum { + // re-sub + log.Warnf(log.ExchangeSys, "%s checksum failure for item %s", + o.Name, + wsEventData.InstrumentID) + return errors.New("checksum failed") } + + o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Exchange: o.Name, + Asset: a, + Pair: instrument, + } + return nil } -// CalculatePartialOrderbookChecksum alternates over the first 25 bid and ask entries from websocket data -// The checksum is made up of the price and the quantity with a semicolon (:) deliminating them -// This will also work when there are less than 25 entries (for whatever reason) +// CalculatePartialOrderbookChecksum alternates over the first 25 bid and ask +// entries from websocket data. The checksum is made up of the price and the +// quantity with a semicolon (:) deliminating them. This will also work when +// there are less than 25 entries (for whatever reason) // eg Bid:Ask:Bid:Ask:Ask:Ask func (o *OKGroup) CalculatePartialOrderbookChecksum(orderbookData *WebsocketDataWrapper) int32 { - var checksum string - iterations := 25 - for i := 0; i < iterations; i++ { - bidsMessage := "" - askMessage := "" + var checksum strings.Builder + for i := 0; i < allowableIterations; i++ { if len(orderbookData.Bids)-1 >= i { - bidsMessage = fmt.Sprintf("%v:%v:", orderbookData.Bids[i][0], orderbookData.Bids[i][1]) + checksum.WriteString(orderbookData.Bids[i][0].(string) + + delimiterColon + + orderbookData.Bids[i][1].(string) + + delimiterColon) } if len(orderbookData.Asks)-1 >= i { - askMessage = fmt.Sprintf("%v:%v:", orderbookData.Asks[i][0], orderbookData.Asks[i][1]) - } - if checksum == "" { - checksum = fmt.Sprintf("%v%v", bidsMessage, askMessage) - } else { - checksum = fmt.Sprintf("%v%v%v", checksum, bidsMessage, askMessage) + checksum.WriteString(orderbookData.Asks[i][0].(string) + + delimiterColon + + orderbookData.Asks[i][1].(string) + + delimiterColon) } } - checksum = strings.TrimSuffix(checksum, ":") - return int32(crc32.ChecksumIEEE([]byte(checksum))) + checksumStr := strings.TrimSuffix(checksum.String(), delimiterColon) + return int32(crc32.ChecksumIEEE([]byte(checksumStr))) } -// CalculateUpdateOrderbookChecksum alternates over the first 25 bid and ask entries of a merged orderbook -// The checksum is made up of the price and the quantity with a semicolon (:) deliminating them -// This will also work when there are less than 25 entries (for whatever reason) +// CalculateUpdateOrderbookChecksum alternates over the first 25 bid and ask +// entries of a merged orderbook. The checksum is made up of the price and the +// quantity with a semicolon (:) deliminating them. This will also work when +// there are less than 25 entries (for whatever reason) // eg Bid:Ask:Bid:Ask:Ask:Ask func (o *OKGroup) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base) int32 { - var checksum string - iterations := 25 - for i := 0; i < iterations; i++ { - bidsMessage := "" - askMessage := "" + var checksum strings.Builder + for i := 0; i < allowableIterations; i++ { if len(orderbookData.Bids)-1 >= i { price := strconv.FormatFloat(orderbookData.Bids[i].Price, 'f', -1, 64) amount := strconv.FormatFloat(orderbookData.Bids[i].Amount, 'f', -1, 64) - bidsMessage = fmt.Sprintf("%v:%v:", price, amount) + checksum.WriteString(price + delimiterColon + amount + delimiterColon) } if len(orderbookData.Asks)-1 >= i { price := strconv.FormatFloat(orderbookData.Asks[i].Price, 'f', -1, 64) amount := strconv.FormatFloat(orderbookData.Asks[i].Amount, 'f', -1, 64) - askMessage = fmt.Sprintf("%v:%v:", price, amount) - } - if checksum == "" { - checksum = fmt.Sprintf("%v%v", bidsMessage, askMessage) - } else { - checksum = fmt.Sprintf("%v%v%v", checksum, bidsMessage, askMessage) + checksum.WriteString(price + delimiterColon + amount + delimiterColon) } } - checksum = strings.TrimSuffix(checksum, ":") - return int32(crc32.ChecksumIEEE([]byte(checksum))) + checksumStr := strings.TrimSuffix(checksum.String(), delimiterColon) + return int32(crc32.ChecksumIEEE([]byte(checksumStr))) } -// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be +// handled by ManageSubscriptions() func (o *OKGroup) GenerateDefaultSubscriptions() { - enabledCurrencies := o.GetEnabledCurrencies() var subscriptions []wshandler.WebsocketChannelSubscription - if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - defaultSubscribedChannels = append(defaultSubscribedChannels, okGroupWsSpotMarginAccount, okGroupWsSpotAccount, okGroupWsSpotOrder) - } - for i := range defaultSubscribedChannels { - for j := range enabledCurrencies { - enabledCurrencies[j].Delimiter = "-" - subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: defaultSubscribedChannels[i], - Currency: enabledCurrencies[j], - }) + assets := o.GetAssetTypes() + for x := range assets { + enabledCurrencies := o.GetEnabledPairs(assets[x]) + if len(enabledCurrencies) == 0 { + continue + } + + switch assets[x] { + case asset.Spot: + for i := range enabledCurrencies { + for y := range defaultSpotSubscribedChannels { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: defaultSpotSubscribedChannels[y], + Currency: o.FormatExchangeCurrency(enabledCurrencies[i], + asset.Spot), + }) + } + } + + if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSpotMarginAccount, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSpotAccount, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSpotOrder, + }) + } + case asset.Futures: + for i := range enabledCurrencies { + for y := range defaultFuturesSubscribedChannels { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: defaultFuturesSubscribedChannels[y], + Currency: o.FormatExchangeCurrency(enabledCurrencies[i], + asset.Futures), + }) + } + } + + if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsFuturesAccount, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsFuturesPosition, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsFuturesOrder, + }) + } + case asset.PerpetualSwap: + for i := range enabledCurrencies { + for y := range defaultSwapSubscribedChannels { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: defaultSwapSubscribedChannels[y], + Currency: o.FormatExchangeCurrency(enabledCurrencies[i], + asset.PerpetualSwap), + }) + } + } + + if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSwapAccount, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSwapPosition, + }, + wshandler.WebsocketChannelSubscription{ + Channel: okGroupWsSwapOrder, + }) + } + case asset.Index: + for i := range enabledCurrencies { + for y := range defaultIndexSubscribedChannels { + subscriptions = append(subscriptions, + wshandler.WebsocketChannelSubscription{ + Channel: defaultIndexSubscribedChannels[y], + Currency: o.FormatExchangeCurrency(enabledCurrencies[i], asset.Index), + }) + } + } + default: + o.Websocket.DataHandler <- errors.New("unhandled asset type") } } + o.Websocket.SubscribeToChannels(subscriptions) } // Subscribe sends a websocket message to receive data from the channel func (o *OKGroup) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { + c := channelToSubscribe.Currency.String() request := WebsocketEventRequest{ Operation: "subscribe", - Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())}, + Arguments: []string{channelToSubscribe.Channel + delimiterColon + c}, } if strings.EqualFold(channelToSubscribe.Channel, okGroupWsSpotAccount) { - request.Arguments = []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.Base.String())} + request.Arguments = []string{channelToSubscribe.Channel + + delimiterColon + + channelToSubscribe.Currency.Base.String()} } return o.WebsocketConn.SendMessage(request) @@ -602,7 +834,9 @@ func (o *OKGroup) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscri func (o *OKGroup) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { request := WebsocketEventRequest{ Operation: "unsubscribe", - Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())}, + Arguments: []string{channelToSubscribe.Channel + + delimiterColon + + channelToSubscribe.Currency.String()}, } return o.WebsocketConn.SendMessage(request) } diff --git a/exchanges/okgroup/okgroup_wrapper.go b/exchanges/okgroup/okgroup_wrapper.go index 6f95e380..c3dcca42 100644 --- a/exchanges/okgroup/okgroup_wrapper.go +++ b/exchanges/okgroup/okgroup_wrapper.go @@ -1,16 +1,18 @@ package okgroup import ( + "errors" "fmt" "strconv" "strings" - "sync" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -19,74 +21,58 @@ import ( // Therefore this OKGroup_Wrapper can be shared between OKEX and OKCoin. // When circumstances change, wrapper funcs can be split appropriately -// Start starts the OKGroup go routine -func (o *OKGroup) Start(wg *sync.WaitGroup) { - wg.Add(1) - go func() { - o.Run() - wg.Done() - }() +// Setup sets user exchange configuration settings +func (o *OKGroup) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + o.SetEnabled(false) + return nil + } + + err := o.SetupDefaults(exch) + if err != nil { + return err + } + + err = o.Websocket.Setup(&wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: o.API.Endpoints.WebsocketURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: o.WsConnect, + Subscriber: o.Subscribe, + UnSubscriber: o.Unsubscribe, + Features: &o.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + o.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: o.Name, + URL: o.Websocket.GetWebsocketURL(), + ProxyURL: o.Websocket.GetProxyAddress(), + Verbose: o.Verbose, + RateLimit: okGroupWsRateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + o.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + false, + false, + false, + false, + exch.Name) + return nil } -// Run implements the OKEX wrapper -func (o *OKGroup) Run() { - if o.Verbose { - log.Debugf("%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket.IsEnabled()), o.WebsocketURL) - log.Debugf("%s polling delay: %ds.\n", o.GetName(), o.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", o.GetName(), len(o.EnabledPairs), o.EnabledPairs) - } - - prods, err := o.GetSpotTokenPairDetails() - if err != nil { - log.Errorf("%v failed to obtain available spot instruments. Err: %s", o.Name, err) - return - } - - var pairs currency.Pairs - for x := range prods { - pairs = append(pairs, currency.NewPairFromString(prods[x].BaseCurrency+"_"+prods[x].QuoteCurrency)) - } - - err = o.UpdateCurrencies(pairs, false, false) - if err != nil { - log.Errorf("%v failed to update available currencies. Err: %s", o.Name, err) - return - } -} - -// UpdateTicker updates and returns the ticker for a currency pair -func (o *OKGroup) UpdateTicker(p currency.Pair, assetType string) (tickerData ticker.Price, err error) { - resp, err := o.GetSpotAllTokenPairsInformationForCurrency(exchange.FormatExchangeCurrency(o.Name, p).String()) - if err != nil { - return - } - tickerData = ticker.Price{ - Ask: resp.BestAsk, - Bid: resp.BestBid, - High: resp.High24h, - Last: resp.Last, - LastUpdated: resp.Timestamp, - Low: resp.Low24h, - Pair: exchange.FormatExchangeCurrency(o.Name, p), - Volume: resp.BaseVolume24h, - } - - err = ticker.ProcessTicker(o.Name, &tickerData, assetType) - return -} - -// GetTickerPrice returns the ticker for a currency pair -func (o *OKGroup) GetTickerPrice(p currency.Pair, assetType string) (tickerData ticker.Price, err error) { - tickerData, err = ticker.GetTicker(o.GetName(), p, assetType) - if err != nil { - return o.UpdateTicker(p, assetType) - } - return -} - -// GetOrderbookEx returns orderbook base on the currency pair -func (o *OKGroup) GetOrderbookEx(p currency.Pair, assetType string) (resp orderbook.Base, err error) { - ob, err := orderbook.Get(o.GetName(), p, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (o *OKGroup) FetchOrderbook(p currency.Pair, assetType asset.Item) (resp orderbook.Base, err error) { + ob, err := orderbook.Get(o.Name, p, assetType) if err != nil { return o.UpdateOrderbook(p, assetType) } @@ -94,54 +80,93 @@ func (o *OKGroup) GetOrderbookEx(p currency.Pair, assetType string) (resp orderb } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (o *OKGroup) UpdateOrderbook(p currency.Pair, assetType string) (resp orderbook.Base, err error) { - orderbookNew, err := o.GetSpotOrderBook(GetSpotOrderBookRequest{ - InstrumentID: exchange.FormatExchangeCurrency(o.Name, p).String(), - }) +func (o *OKGroup) UpdateOrderbook(p currency.Pair, a asset.Item) (orderbook.Base, error) { + var resp orderbook.Base + if a == asset.Index { + return resp, errors.New("no orderbooks for index") + } + + orderbookNew, err := o.GetOrderBook(GetOrderBookRequest{ + InstrumentID: o.FormatExchangeCurrency(p, a).String(), + }, a) if err != nil { - return + return resp, err } for x := range orderbookNew.Bids { amount, convErr := strconv.ParseFloat(orderbookNew.Bids[x][1], 64) if convErr != nil { - log.Errorf("Could not convert %v to float64", orderbookNew.Bids[x][1]) + return resp, err } price, convErr := strconv.ParseFloat(orderbookNew.Bids[x][0], 64) if convErr != nil { - log.Errorf("Could not convert %v to float64", orderbookNew.Bids[x][0]) + return resp, err } + + var liquidationOrders, orderCount int64 + // Contract specific variables + if len(orderbookNew.Bids[x]) == 4 { + liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Bids[x][2], 10, 64) + if convErr != nil { + return resp, err + } + + orderCount, convErr = strconv.ParseInt(orderbookNew.Bids[x][3], 10, 64) + if convErr != nil { + return resp, err + } + } + resp.Bids = append(resp.Bids, orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + Price: price, + LiquidationOrders: liquidationOrders, + OrderCount: orderCount, }) } for x := range orderbookNew.Asks { amount, convErr := strconv.ParseFloat(orderbookNew.Asks[x][1], 64) if convErr != nil { - log.Errorf("Could not convert %v to float64", orderbookNew.Asks[x][1]) + return resp, err } price, convErr := strconv.ParseFloat(orderbookNew.Asks[x][0], 64) if convErr != nil { - log.Errorf("Could not convert %v to float64", orderbookNew.Asks[x][0]) + return resp, err } + + var liquidationOrders, orderCount int64 + // Contract specific variables + if len(orderbookNew.Asks[x]) == 4 { + liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Asks[x][2], 10, 64) + if convErr != nil { + return resp, err + } + + orderCount, convErr = strconv.ParseInt(orderbookNew.Asks[x][3], 10, 64) + if convErr != nil { + return resp, err + } + } + resp.Asks = append(resp.Asks, orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + Price: price, + LiquidationOrders: liquidationOrders, + OrderCount: orderCount, }) } resp.Pair = p - resp.AssetType = assetType + resp.AssetType = a resp.ExchangeName = o.Name err = resp.Process() if err != nil { - return + return resp, err } - return orderbook.Get(o.Name, p, assetType) + return orderbook.Get(o.Name, p, a) } // GetAccountInfo retrieves balances for all enabled currencies @@ -150,20 +175,25 @@ func (o *OKGroup) GetAccountInfo() (resp exchange.AccountInfo, err error) { currencies, err := o.GetSpotTradingAccounts() currencyAccount := exchange.Account{} - for _, curr := range currencies { - hold, err := strconv.ParseFloat(curr.Hold, 64) + for i := range currencies { + hold, err := strconv.ParseFloat(currencies[i].Hold, 64) if err != nil { - log.Errorf("Could not convert %v to float64", curr.Hold) + log.Errorf(log.ExchangeSys, + "Could not convert %v to float64", + currencies[i].Hold) } - totalValue, err := strconv.ParseFloat(curr.Balance, 64) + totalValue, err := strconv.ParseFloat(currencies[i].Balance, 64) if err != nil { - log.Errorf("Could not convert %v to float64", curr.Balance) + log.Errorf(log.ExchangeSys, + "Could not convert %v to float64", + currencies[i].Balance) } - currencyAccount.Currencies = append(currencyAccount.Currencies, exchange.AccountCurrencyInfo{ - CurrencyName: currency.NewCode(curr.Currency), - Hold: hold, - TotalValue: totalValue, - }) + currencyAccount.Currencies = append(currencyAccount.Currencies, + exchange.AccountCurrencyInfo{ + CurrencyName: currency.NewCode(currencies[i].Currency), + Hold: hold, + TotalValue: totalValue, + }) } resp.Accounts = append(resp.Accounts, currencyAccount) @@ -177,9 +207,9 @@ func (o *OKGroup) GetFundingHistory() (resp []exchange.FundHistory, err error) { if err != nil { return } - for _, deposit := range accountDepositHistory { + for x := range accountDepositHistory { orderStatus := "" - switch deposit.Status { + switch accountDepositHistory[x].Status { case 0: orderStatus = "waiting" case 1: @@ -189,12 +219,12 @@ func (o *OKGroup) GetFundingHistory() (resp []exchange.FundHistory, err error) { } resp = append(resp, exchange.FundHistory{ - Amount: deposit.Amount, - Currency: deposit.Currency, + Amount: accountDepositHistory[x].Amount, + Currency: accountDepositHistory[x].Currency, ExchangeName: o.Name, Status: orderStatus, - Timestamp: deposit.Timestamp, - TransferID: deposit.TransactionID, + Timestamp: accountDepositHistory[x].Timestamp, + TransferID: accountDepositHistory[x].TransactionID, TransferType: "deposit", }) } @@ -206,7 +236,7 @@ func (o *OKGroup) GetFundingHistory() (resp []exchange.FundHistory, err error) { ExchangeName: o.Name, Status: OrderStatus[accountWithdrawlHistory[i].Status], Timestamp: accountWithdrawlHistory[i].Timestamp, - TransferID: accountWithdrawlHistory[i].Txid, + TransferID: accountWithdrawlHistory[i].TransactionID, TransferType: "withdrawal", }) } @@ -214,81 +244,92 @@ func (o *OKGroup) GetFundingHistory() (resp []exchange.FundHistory, err error) { } // GetExchangeHistory returns historic trade data since exchange opening. -func (o *OKGroup) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { +func (o *OKGroup) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (o *OKGroup) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (resp exchange.SubmitOrderResponse, err error) { - request := PlaceSpotOrderRequest{ - ClientOID: clientID, - InstrumentID: exchange.FormatExchangeCurrency(o.Name, p).String(), - Side: strings.ToLower(side.ToString()), - Type: strings.ToLower(orderType.ToString()), - Size: strconv.FormatFloat(amount, 'f', -1, 64), +func (o *OKGroup) SubmitOrder(s *order.Submit) (resp order.SubmitResponse, err error) { + err = s.Validate() + if err != nil { + return resp, err } - if orderType == exchange.LimitOrderType { - request.Price = strconv.FormatFloat(price, 'f', -1, 64) + + request := PlaceOrderRequest{ + ClientOID: s.ClientID, + InstrumentID: o.FormatExchangeCurrency(s.Pair, asset.Spot).String(), + Side: s.OrderSide.Lower(), + Type: s.OrderType.Lower(), + Size: strconv.FormatFloat(s.Amount, 'f', -1, 64), + } + if s.OrderType == order.Limit { + request.Price = strconv.FormatFloat(s.Price, 'f', -1, 64) } orderResponse, err := o.PlaceSpotOrder(&request) if err != nil { return } + resp.IsOrderPlaced = orderResponse.Result resp.OrderID = orderResponse.OrderID - + if s.OrderType == order.Market { + resp.FullyMatched = true + } return } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (o *OKGroup) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (o *OKGroup) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (o *OKGroup) CancelOrder(orderCancellation *exchange.OrderCancellation) (err error) { +func (o *OKGroup) CancelOrder(orderCancellation *order.Cancel) (err error) { orderID, err := strconv.ParseInt(orderCancellation.OrderID, 10, 64) if err != nil { return } orderCancellationResponse, err := o.CancelSpotOrder(CancelSpotOrderRequest{ - InstrumentID: exchange.FormatExchangeCurrency(o.Name, orderCancellation.CurrencyPair).String(), - OrderID: orderID, + InstrumentID: o.FormatExchangeCurrency(orderCancellation.CurrencyPair, + asset.Spot).String(), + OrderID: orderID, }) if !orderCancellationResponse.Result { - err = fmt.Errorf("order %v failed to be cancelled", orderCancellationResponse.OrderID) + err = fmt.Errorf("order %d failed to be cancelled", + orderCancellationResponse.OrderID) } return } // CancelAllOrders cancels all orders associated with a currency pair -func (o *OKGroup) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (resp exchange.CancelAllOrdersResponse, err error) { +func (o *OKGroup) CancelAllOrders(orderCancellation *order.Cancel) (resp order.CancelAllResponse, err error) { orderIDs := strings.Split(orderCancellation.OrderID, ",") - resp.OrderStatus = make(map[string]string) + resp.Status = make(map[string]string) var orderIDNumbers []int64 - for _, i := range orderIDs { - orderIDNumber, strConvErr := strconv.ParseInt(i, 10, 64) + for i := range orderIDs { + orderIDNumber, strConvErr := strconv.ParseInt(orderIDs[i], 10, 64) if strConvErr != nil { - resp.OrderStatus[i] = strConvErr.Error() + resp.Status[orderIDs[i]] = strConvErr.Error() continue } orderIDNumbers = append(orderIDNumbers, orderIDNumber) } cancelOrdersResponse, err := o.CancelMultipleSpotOrders(CancelMultipleSpotOrdersRequest{ - InstrumentID: exchange.FormatExchangeCurrency(o.Name, orderCancellation.CurrencyPair).String(), - OrderIDs: orderIDNumbers, + InstrumentID: o.FormatExchangeCurrency(orderCancellation.CurrencyPair, + asset.Spot).String(), + OrderIDs: orderIDNumbers, }) if err != nil { return } - for _, orderMap := range cancelOrdersResponse { - for _, cancelledOrder := range orderMap { - resp.OrderStatus[fmt.Sprintf("%v", cancelledOrder.OrderID)] = fmt.Sprintf("%v", cancelledOrder.Result) + for x := range cancelOrdersResponse { + for y := range cancelOrdersResponse[x] { + resp.Status[strconv.FormatInt(cancelOrdersResponse[x][y].OrderID, 10)] = strconv.FormatBool(cancelOrdersResponse[x][y].Result) } } @@ -296,36 +337,36 @@ func (o *OKGroup) CancelAllOrders(orderCancellation *exchange.OrderCancellation) } // GetOrderInfo returns information on a current open order -func (o *OKGroup) GetOrderInfo(orderID string) (resp exchange.OrderDetail, err error) { - order, err := o.GetSpotOrder(GetSpotOrderRequest{OrderID: orderID}) +func (o *OKGroup) GetOrderInfo(orderID string) (resp order.Detail, err error) { + mOrder, err := o.GetSpotOrder(GetSpotOrderRequest{OrderID: orderID}) if err != nil { return } - resp = exchange.OrderDetail{ - Amount: order.Size, - CurrencyPair: currency.NewPairDelimiter(order.InstrumentID, - o.ConfigCurrencyPairFormat.Delimiter), + resp = order.Detail{ + Amount: mOrder.Size, + CurrencyPair: currency.NewPairDelimiter(mOrder.InstrumentID, + o.GetPairFormat(asset.Spot, false).Delimiter), Exchange: o.Name, - OrderDate: order.Timestamp, - ExecutedAmount: order.FilledSize, - Status: order.Status, - OrderSide: exchange.OrderSide(order.Side), + OrderDate: mOrder.Timestamp, + ExecutedAmount: mOrder.FilledSize, + Status: order.Status(mOrder.Status), + OrderSide: order.Side(mOrder.Side), } return } // GetDepositAddress returns a deposit address for a specified currency -func (o *OKGroup) GetDepositAddress(p currency.Code, accountID string) (_ string, err error) { +func (o *OKGroup) GetDepositAddress(p currency.Code, accountID string) (string, error) { wallet, err := o.GetAccountDepositAddressForCurrency(p.Lower().String()) - if err != nil { - return + if err != nil || len(wallet) == 0 { + return "", err } return wallet[0].Address, nil } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (o *OKGroup) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (o *OKGroup) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { withdrawal, err := o.AccountWithdraw(AccountWithdrawRequest{ Amount: withdrawRequest.Amount, Currency: withdrawRequest.Currency.Lower().String(), @@ -338,45 +379,49 @@ func (o *OKGroup) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.Withdraw return "", err } if !withdrawal.Result { - return fmt.Sprintf("%v", withdrawal.WithdrawalID), fmt.Errorf("could not withdraw currency %v to %v, no error specified", withdrawRequest.Currency.String(), withdrawRequest.Address) + return strconv.FormatInt(withdrawal.WithdrawalID, 10), + fmt.Errorf("could not withdraw currency %s to %s, no error specified", + withdrawRequest.Currency, + withdrawRequest.Address) } - return fmt.Sprintf("%v", withdrawal.WithdrawalID), nil + return strconv.FormatInt(withdrawal.WithdrawalID, 10), nil } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (o *OKGroup) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (o *OKGroup) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (o *OKGroup) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (o *OKGroup) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // GetActiveOrders retrieves any orders that are active/open -func (o *OKGroup) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) (resp []exchange.OrderDetail, err error) { - for _, currency := range getOrdersRequest.Currencies { +func (o *OKGroup) GetActiveOrders(req *order.GetOrdersRequest) (resp []order.Detail, err error) { + for x := range req.Currencies { spotOpenOrders, err := o.GetSpotOpenOrders(GetSpotOpenOrdersRequest{ - InstrumentID: exchange.FormatExchangeCurrency(o.Name, currency).String(), + InstrumentID: o.FormatExchangeCurrency(req.Currencies[x], + asset.Spot).String(), }) if err != nil { return resp, err } for i := range spotOpenOrders { - resp = append(resp, exchange.OrderDetail{ + resp = append(resp, order.Detail{ ID: spotOpenOrders[i].OrderID, Price: spotOpenOrders[i].Price, Amount: spotOpenOrders[i].Size, - CurrencyPair: currency, + CurrencyPair: req.Currencies[x], Exchange: o.Name, - OrderSide: exchange.OrderSide(spotOpenOrders[i].Side), - OrderType: exchange.OrderType(spotOpenOrders[i].Type), + OrderSide: order.Side(spotOpenOrders[i].Side), + OrderType: order.Type(spotOpenOrders[i].Type), ExecutedAmount: spotOpenOrders[i].FilledSize, OrderDate: spotOpenOrders[i].Timestamp, - Status: spotOpenOrders[i].Status, + Status: order.Status(spotOpenOrders[i].Status), }) } } @@ -386,27 +431,28 @@ func (o *OKGroup) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ( // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (o *OKGroup) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) (resp []exchange.OrderDetail, err error) { - for _, currency := range getOrdersRequest.Currencies { +func (o *OKGroup) GetOrderHistory(req *order.GetOrdersRequest) (resp []order.Detail, err error) { + for x := range req.Currencies { spotOpenOrders, err := o.GetSpotOrders(GetSpotOrdersRequest{ - Status: strings.Join([]string{"filled", "cancelled", "failure"}, "|"), - InstrumentID: exchange.FormatExchangeCurrency(o.Name, currency).String(), + Status: strings.Join([]string{"filled", "cancelled", "failure"}, "|"), + InstrumentID: o.FormatExchangeCurrency(req.Currencies[x], + asset.Spot).String(), }) if err != nil { return resp, err } for i := range spotOpenOrders { - resp = append(resp, exchange.OrderDetail{ + resp = append(resp, order.Detail{ ID: spotOpenOrders[i].OrderID, Price: spotOpenOrders[i].Price, Amount: spotOpenOrders[i].Size, - CurrencyPair: currency, + CurrencyPair: req.Currencies[x], Exchange: o.Name, - OrderSide: exchange.OrderSide(spotOpenOrders[i].Side), - OrderType: exchange.OrderType(spotOpenOrders[i].Type), + OrderSide: order.Side(spotOpenOrders[i].Side), + OrderType: order.Type(spotOpenOrders[i].Type), ExecutedAmount: spotOpenOrders[i].FilledSize, OrderDate: spotOpenOrders[i].Timestamp, - Status: spotOpenOrders[i].Status, + Status: order.Status(spotOpenOrders[i].Status), }) } } @@ -421,7 +467,7 @@ func (o *OKGroup) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (o *OKGroup) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (o.APIKey == "" || o.APISecret == "") && // Todo check connection status + if !o.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } diff --git a/exchanges/orders/README.md b/exchanges/order/README.md similarity index 100% rename from exchanges/orders/README.md rename to exchanges/order/README.md diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go new file mode 100644 index 00000000..ed019e9d --- /dev/null +++ b/exchanges/order/order_test.go @@ -0,0 +1,536 @@ +package order + +import ( + "errors" + "strings" + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" +) + +func TestValidate(t *testing.T) { + testPair := currency.NewPair(currency.BTC, currency.LTC) + tester := []struct { + Pair currency.Pair + Side + Type + Amount float64 + Price float64 + ExpectedErr error + }{ + { + ExpectedErr: ErrPairIsEmpty, + }, // empty pair + { + Pair: testPair, + ExpectedErr: ErrSideIsInvalid, + }, // valid pair but invalid order side + { + Pair: testPair, + Side: Buy, + ExpectedErr: ErrTypeIsInvalid, + }, // valid pair and order side but invalid order type + { + Pair: testPair, + Side: Sell, + ExpectedErr: ErrTypeIsInvalid, + }, // valid pair and order side but invalid order type + { + Pair: testPair, + Side: Bid, + ExpectedErr: ErrTypeIsInvalid, + }, // valid pair and order side but invalid order type + { + Pair: testPair, + Side: Ask, + ExpectedErr: ErrTypeIsInvalid, + }, // valid pair and order side but invalid order type + { + Pair: testPair, + Side: Ask, + Type: Market, + ExpectedErr: ErrAmountIsInvalid, + }, // valid pair, order side, type but invalid amount + { + Pair: testPair, + Side: Ask, + Type: Limit, + Amount: 1, + ExpectedErr: ErrPriceMustBeSetIfLimitOrder, + }, // valid pair, order side, type, amount but invalid price + { + Pair: testPair, + Side: Ask, + Type: Limit, + Amount: 1, + Price: 1000, + ExpectedErr: nil, + }, // valid order! + } + + for x := range tester { + s := Submit{ + Pair: tester[x].Pair, + OrderSide: tester[x].Side, + OrderType: tester[x].Type, + Amount: tester[x].Amount, + Price: tester[x].Price, + } + if err := s.Validate(); err != tester[x].ExpectedErr { + t.Errorf("Unexpected result. Got: %s, want: %s", err, tester[x].ExpectedErr) + } + } +} + +func TestOrderSides(t *testing.T) { + t.Parallel() + + var os = Buy + if os.String() != "BUY" { + t.Errorf("unexpected string %s", os.String()) + } + + if os.Lower() != "buy" { + t.Errorf("unexpected string %s", os.String()) + } +} + +func TestOrderTypes(t *testing.T) { + t.Parallel() + + var ot Type = "Mo'Money" + + if ot.String() != "Mo'Money" { + t.Errorf("unexpected string %s", ot.String()) + } + + if ot.Lower() != "mo'money" { + t.Errorf("unexpected string %s", ot.Lower()) + } +} + +func TestFilterOrdersByType(t *testing.T) { + t.Parallel() + + var orders = []Detail{ + { + OrderType: ImmediateOrCancel, + }, + { + OrderType: Limit, + }, + } + + FilterOrdersByType(&orders, AnyType) + if len(orders) != 2 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders)) + } + + FilterOrdersByType(&orders, Limit) + if len(orders) != 1 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) + } + + FilterOrdersByType(&orders, Stop) + if len(orders) != 0 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders)) + } +} + +func TestFilterOrdersBySide(t *testing.T) { + t.Parallel() + + var orders = []Detail{ + { + OrderSide: Buy, + }, + { + OrderSide: Sell, + }, + {}, + } + + FilterOrdersBySide(&orders, AnySide) + if len(orders) != 3 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) + } + + FilterOrdersBySide(&orders, Buy) + if len(orders) != 1 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) + } + + FilterOrdersBySide(&orders, Sell) + if len(orders) != 0 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders)) + } +} + +func TestFilterOrdersByTickRange(t *testing.T) { + t.Parallel() + + var orders = []Detail{ + { + OrderDate: time.Unix(100, 0), + }, + { + OrderDate: time.Unix(110, 0), + }, + { + OrderDate: time.Unix(111, 0), + }, + } + + FilterOrdersByTickRange(&orders, time.Unix(0, 0), time.Unix(0, 0)) + if len(orders) != 3 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) + } + + FilterOrdersByTickRange(&orders, time.Unix(100, 0), time.Unix(111, 0)) + if len(orders) != 3 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) + } + + FilterOrdersByTickRange(&orders, time.Unix(101, 0), time.Unix(111, 0)) + if len(orders) != 2 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders)) + } + + FilterOrdersByTickRange(&orders, time.Unix(200, 0), time.Unix(300, 0)) + if len(orders) != 0 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders)) + } +} + +func TestFilterOrdersByCurrencies(t *testing.T) { + t.Parallel() + + var orders = []Detail{ + { + CurrencyPair: currency.NewPair(currency.BTC, currency.USD), + }, + { + CurrencyPair: currency.NewPair(currency.LTC, currency.EUR), + }, + { + CurrencyPair: currency.NewPair(currency.DOGE, currency.RUB), + }, + } + + currencies := []currency.Pair{currency.NewPair(currency.BTC, currency.USD), + currency.NewPair(currency.LTC, currency.EUR), + currency.NewPair(currency.DOGE, currency.RUB)} + FilterOrdersByCurrencies(&orders, currencies) + if len(orders) != 3 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) + } + + currencies = []currency.Pair{currency.NewPair(currency.BTC, currency.USD), + currency.NewPair(currency.LTC, currency.EUR)} + FilterOrdersByCurrencies(&orders, currencies) + if len(orders) != 2 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders)) + } + + currencies = []currency.Pair{currency.NewPair(currency.BTC, currency.USD)} + FilterOrdersByCurrencies(&orders, currencies) + if len(orders) != 1 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) + } + + currencies = []currency.Pair{} + FilterOrdersByCurrencies(&orders, currencies) + if len(orders) != 1 { + t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) + } +} + +func TestSortOrdersByPrice(t *testing.T) { + t.Parallel() + + orders := []Detail{ + { + Price: 100, + }, { + Price: 0, + }, { + Price: 50, + }, + } + + SortOrdersByPrice(&orders, false) + if orders[0].Price != 0 { + t.Errorf("Expected: '%v', received: '%v'", 0, orders[0].Price) + } + + SortOrdersByPrice(&orders, true) + if orders[0].Price != 100 { + t.Errorf("Expected: '%v', received: '%v'", 100, orders[0].Price) + } +} + +func TestSortOrdersByDate(t *testing.T) { + t.Parallel() + + orders := []Detail{ + { + OrderDate: time.Unix(0, 0), + }, { + OrderDate: time.Unix(1, 0), + }, { + OrderDate: time.Unix(2, 0), + }, + } + + SortOrdersByDate(&orders, false) + if orders[0].OrderDate.Unix() != time.Unix(0, 0).Unix() { + t.Errorf("Expected: '%v', received: '%v'", + time.Unix(0, 0).Unix(), + orders[0].OrderDate.Unix()) + } + + SortOrdersByDate(&orders, true) + if orders[0].OrderDate.Unix() != time.Unix(2, 0).Unix() { + t.Errorf("Expected: '%v', received: '%v'", + time.Unix(2, 0).Unix(), + orders[0].OrderDate.Unix()) + } +} + +func TestSortOrdersByCurrency(t *testing.T) { + t.Parallel() + + orders := []Detail{ + { + CurrencyPair: currency.NewPairWithDelimiter(currency.BTC.String(), + currency.USD.String(), + "-"), + }, { + CurrencyPair: currency.NewPairWithDelimiter(currency.DOGE.String(), + currency.USD.String(), + "-"), + }, { + CurrencyPair: currency.NewPairWithDelimiter(currency.BTC.String(), + currency.RUB.String(), + "-"), + }, { + CurrencyPair: currency.NewPairWithDelimiter(currency.LTC.String(), + currency.EUR.String(), + "-"), + }, { + CurrencyPair: currency.NewPairWithDelimiter(currency.LTC.String(), + currency.AUD.String(), + "-"), + }, + } + + SortOrdersByCurrency(&orders, false) + if orders[0].CurrencyPair.String() != currency.BTC.String()+"-"+currency.RUB.String() { + t.Errorf("Expected: '%v', received: '%v'", + currency.BTC.String()+"-"+currency.RUB.String(), + orders[0].CurrencyPair.String()) + } + + SortOrdersByCurrency(&orders, true) + if orders[0].CurrencyPair.String() != currency.LTC.String()+"-"+currency.EUR.String() { + t.Errorf("Expected: '%v', received: '%v'", + currency.LTC.String()+"-"+currency.EUR.String(), + orders[0].CurrencyPair.String()) + } +} + +func TestSortOrdersByOrderSide(t *testing.T) { + t.Parallel() + + orders := []Detail{ + { + OrderSide: Buy, + }, { + OrderSide: Sell, + }, { + OrderSide: Sell, + }, { + OrderSide: Buy, + }, + } + + SortOrdersBySide(&orders, false) + if !strings.EqualFold(orders[0].OrderSide.String(), Buy.String()) { + t.Errorf("Expected: '%v', received: '%v'", + Buy, + orders[0].OrderSide) + } + + SortOrdersBySide(&orders, true) + if !strings.EqualFold(orders[0].OrderSide.String(), Sell.String()) { + t.Errorf("Expected: '%v', received: '%v'", + Sell, + orders[0].OrderSide) + } +} + +func TestSortOrdersByOrderType(t *testing.T) { + t.Parallel() + + orders := []Detail{ + { + OrderType: Market, + }, { + OrderType: Limit, + }, { + OrderType: ImmediateOrCancel, + }, { + OrderType: TrailingStop, + }, + } + + SortOrdersByType(&orders, false) + if !strings.EqualFold(orders[0].OrderType.String(), ImmediateOrCancel.String()) { + t.Errorf("Expected: '%v', received: '%v'", + ImmediateOrCancel, + orders[0].OrderType) + } + + SortOrdersByType(&orders, true) + if !strings.EqualFold(orders[0].OrderType.String(), TrailingStop.String()) { + t.Errorf("Expected: '%v', received: '%v'", + TrailingStop, + orders[0].OrderType) + } +} + +var stringsToOrderSide = []struct { + in string + out Side + err error +}{ + {"buy", Buy, nil}, + {"BUY", Buy, nil}, + {"bUy", Buy, nil}, + {"sell", Sell, nil}, + {"SELL", Sell, nil}, + {"sElL", Sell, nil}, + {"bid", Bid, nil}, + {"BID", Bid, nil}, + {"bId", Bid, nil}, + {"ask", Ask, nil}, + {"ASK", Ask, nil}, + {"aSk", Ask, nil}, + {"any", AnySide, nil}, + {"ANY", AnySide, nil}, + {"aNy", AnySide, nil}, + {"woahMan", Buy, errors.New("woahMan not recognised as side type")}, +} + +func TestStringToOrderSide(t *testing.T) { + for i := 0; i < len(stringsToOrderSide); i++ { + testData := &stringsToOrderSide[i] + t.Run(testData.in, func(t *testing.T) { + out, err := StringToOrderSide(testData.in) + if err != nil { + if err.Error() != testData.err.Error() { + t.Error("Unexpected error", err) + } + } else if out != testData.out { + t.Errorf("Unexpected output %v. Expected %v", out, testData.out) + } + }) + } +} + +var stringsToOrderType = []struct { + in string + out Type + err error +}{ + {"limit", Limit, nil}, + {"LIMIT", Limit, nil}, + {"lImIt", Limit, nil}, + {"market", Market, nil}, + {"MARKET", Market, nil}, + {"mArKeT", Market, nil}, + {"immediate_or_cancel", ImmediateOrCancel, nil}, + {"IMMEDIATE_OR_CANCEL", ImmediateOrCancel, nil}, + {"iMmEdIaTe_Or_CaNcEl", ImmediateOrCancel, nil}, + {"stop", Stop, nil}, + {"STOP", Stop, nil}, + {"sToP", Stop, nil}, + {"trailingstop", TrailingStop, nil}, + {"TRAILINGSTOP", TrailingStop, nil}, + {"tRaIlInGsToP", TrailingStop, nil}, + {"any", AnyType, nil}, + {"ANY", AnyType, nil}, + {"aNy", AnyType, nil}, + {"woahMan", Unknown, errors.New("woahMan not recognised as order type")}, +} + +func TestStringToOrderType(t *testing.T) { + for i := 0; i < len(stringsToOrderType); i++ { + testData := &stringsToOrderType[i] + t.Run(testData.in, func(t *testing.T) { + out, err := StringToOrderType(testData.in) + if err != nil { + if err.Error() != testData.err.Error() { + t.Error("Unexpected error", err) + } + } else if out != testData.out { + t.Errorf("Unexpected output %v. Expected %v", out, testData.out) + } + }) + } +} + +var stringsToOrderStatus = []struct { + in string + out Status + err error +}{ + {"any", AnyStatus, nil}, + {"ANY", AnyStatus, nil}, + {"aNy", AnyStatus, nil}, + {"new", New, nil}, + {"NEW", New, nil}, + {"nEw", New, nil}, + {"active", Active, nil}, + {"ACTIVE", Active, nil}, + {"aCtIvE", Active, nil}, + {"partially_filled", PartiallyFilled, nil}, + {"PARTIALLY_FILLED", PartiallyFilled, nil}, + {"pArTiAlLy_FiLlEd", PartiallyFilled, nil}, + {"filled", Filled, nil}, + {"FILLED", Filled, nil}, + {"fIlLeD", Filled, nil}, + {"canceled", Cancelled, nil}, + {"CANCELED", Cancelled, nil}, + {"cAnCelEd", Cancelled, nil}, + {"pending_cancel", PendingCancel, nil}, + {"PENDING_CANCEL", PendingCancel, nil}, + {"pENdInG_cAnCeL", PendingCancel, nil}, + {"rejected", Rejected, nil}, + {"REJECTED", Rejected, nil}, + {"rEjEcTeD", Rejected, nil}, + {"expired", Expired, nil}, + {"EXPIRED", Expired, nil}, + {"eXpIrEd", Expired, nil}, + {"hidden", Hidden, nil}, + {"HIDDEN", Hidden, nil}, + {"hIdDeN", Hidden, nil}, + {"woahMan", UnknownStatus, errors.New("woahMan not recognised as order STATUS")}, +} + +func TestStringToOrderStatus(t *testing.T) { + for i := 0; i < len(stringsToOrderStatus); i++ { + testData := &stringsToOrderStatus[i] + t.Run(testData.in, func(t *testing.T) { + out, err := StringToOrderStatus(testData.in) + if err != nil { + if err.Error() != testData.err.Error() { + t.Error("Unexpected error", err) + } + } else if out != testData.out { + t.Errorf("Unexpected output %v. Expected %v", out, testData.out) + } + }) + } +} diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go new file mode 100644 index 00000000..ef869e02 --- /dev/null +++ b/exchanges/order/order_types.go @@ -0,0 +1,194 @@ +package order + +import ( + "errors" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +const ( + limitOrder = iota + marketOrder +) + +// Orders variable holds an array of pointers to order structs +var Orders []*Order + +// Order struct holds order values +type Order struct { + OrderID int + Exchange string + Type int + Amount float64 + Price float64 +} + +// vars related to orders +var ( + ErrSubmissionIsNil = errors.New("order submission is nil") + ErrPairIsEmpty = errors.New("order pair is empty") + ErrSideIsInvalid = errors.New("order side is invalid") + ErrTypeIsInvalid = errors.New("order type is invalid") + ErrAmountIsInvalid = errors.New("order amount is invalid") + ErrPriceMustBeSetIfLimitOrder = errors.New("order price must be set if limit order type is desired") +) + +// Submit contains the order submission data +type Submit struct { + Pair currency.Pair + OrderType Type + OrderSide Side + TriggerPrice float64 + TargetAmount float64 + Price float64 + Amount float64 + ClientID string +} + +// SubmitResponse is what is returned after submitting an order to an exchange +type SubmitResponse struct { + IsOrderPlaced bool + FullyMatched bool + OrderID string +} + +// Modify is an order modifyer +type Modify struct { + OrderID string + Type + Side + Price float64 + Amount float64 + LimitPriceUpper float64 + LimitPriceLower float64 + CurrencyPair currency.Pair + ImmediateOrCancel bool + HiddenOrder bool + FillOrKill bool + PostOnly bool +} + +// ModifyResponse is an order modifying return type +type ModifyResponse struct { + OrderID string +} + +// CancelAllResponse returns the status from attempting to cancel all orders on +// an exchagne +type CancelAllResponse struct { + Status map[string]string +} + +// Type enforces a standard for order types across the code base +type Type string + +// Defined package order types +const ( + AnyType Type = "ANY" + Limit Type = "LIMIT" + Market Type = "MARKET" + ImmediateOrCancel Type = "IMMEDIATE_OR_CANCEL" + Stop Type = "STOP" + TrailingStop Type = "TRAILINGSTOP" + Unknown Type = "UNKNOWN" +) + +// Side enforces a standard for order sides across the code base +type Side string + +// Order side types +const ( + AnySide Side = "ANY" + Buy Side = "BUY" + Sell Side = "SELL" + Bid Side = "BID" + Ask Side = "ASK" +) + +// Detail holds order detail data +type Detail struct { + Exchange string + AccountID string + ID string + CurrencyPair currency.Pair + OrderSide Side + OrderType Type + OrderDate time.Time + Status Status + Price float64 + Amount float64 + ExecutedAmount float64 + RemainingAmount float64 + Fee float64 + Trades []TradeHistory +} + +// TradeHistory holds exchange history data +type TradeHistory struct { + Timestamp time.Time + TID string + Price float64 + Amount float64 + Exchange string + Type Type + Side Side + Fee float64 + Description string +} + +// Cancel type required when requesting to cancel an order +type Cancel struct { + AccountID string + OrderID string + CurrencyPair currency.Pair + AssetType asset.Item + WalletAddress string + Side Side +} + +// GetOrdersRequest used for GetOrderHistory and GetOpenOrders wrapper functions +type GetOrdersRequest struct { + OrderType Type + OrderSide Side + StartTicks time.Time + EndTicks time.Time + // Currencies Empty array = all currencies. Some endpoints only support + // singular currency enquiries + Currencies []currency.Pair +} + +// Status defines order status types +type Status string + +// All order status types +const ( + AnyStatus Status = "ANY" + New Status = "NEW" + Active Status = "ACTIVE" + PartiallyCancelled Status = "PARTIALLY_CANCELLED" + PartiallyFilled Status = "PARTIALLY_FILLED" + Filled Status = "FILLED" + Cancelled Status = "CANCELED" + PendingCancel Status = "PENDING_CANCEL" + Rejected Status = "REJECTED" + Expired Status = "EXPIRED" + Hidden Status = "HIDDEN" + UnknownStatus Status = "UNKNOWN" +) + +// ByPrice used for sorting orders by price +type ByPrice []Detail + +// ByOrderType used for sorting orders by order type +type ByOrderType []Detail + +// ByCurrency used for sorting orders by order currency +type ByCurrency []Detail + +// ByDate used for sorting orders by order date +type ByDate []Detail + +// ByOrderSide used for sorting orders by order side (buy sell) +type ByOrderSide []Detail diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go new file mode 100644 index 00000000..1fb1d2c7 --- /dev/null +++ b/exchanges/order/orders.go @@ -0,0 +1,372 @@ +package order + +import ( + "fmt" + "sort" + "strings" + "time" + + "github.com/thrasher-corp/gocryptotrader/currency" +) + +// NewOrder creates a new order and returns a an orderID +func NewOrder(exchangeName string, amount, price float64) int { + ord := &Order{} + if len(Orders) == 0 { + ord.OrderID = 0 + } else { + ord.OrderID = len(Orders) + } + + ord.Exchange = exchangeName + ord.Amount = amount + ord.Price = price + Orders = append(Orders, ord) + return ord.OrderID +} + +// DeleteOrder deletes orders by ID and returns state +func DeleteOrder(orderID int) bool { + for i := range Orders { + if Orders[i].OrderID == orderID { + Orders = append(Orders[:i], Orders[i+1:]...) + return true + } + } + return false +} + +// GetOrdersByExchange returns order pointer grouped by exchange +func GetOrdersByExchange(exchange string) []*Order { + var orders []*Order + for i := range Orders { + if Orders[i].Exchange == exchange { + orders = append(orders, Orders[i]) + } + } + if len(orders) > 0 { + return orders + } + return nil +} + +// GetOrderByOrderID returns order pointer by ID +func GetOrderByOrderID(orderID int) *Order { + for i := range Orders { + if Orders[i].OrderID == orderID { + return Orders[i] + } + } + return nil +} + +// Validate checks the supplied data and returns whether or not it's valid +func (s *Submit) Validate() error { + if s == nil { + return ErrSubmissionIsNil + } + + if s.Pair.IsEmpty() { + return ErrPairIsEmpty + } + + if s.OrderSide != Buy && + s.OrderSide != Sell && + s.OrderSide != Bid && + s.OrderSide != Ask { + return ErrSideIsInvalid + } + + if s.OrderType != Market && s.OrderType != Limit { + return ErrTypeIsInvalid + } + + if s.Amount <= 0 { + return ErrAmountIsInvalid + } + + if s.OrderType == Limit && s.Price <= 0 { + return ErrPriceMustBeSetIfLimitOrder + } + + return nil +} + +// String implements the stringer interface +func (t Type) String() string { + return string(t) +} + +// Lower returns the type lower case string +func (t Type) Lower() string { + return strings.ToLower(string(t)) +} + +// String implements the stringer interface +func (s Side) String() string { + return string(s) +} + +// Lower returns the side lower case string +func (s Side) Lower() string { + return strings.ToLower(string(s)) +} + +// String implements the stringer interface +func (s Status) String() string { + return string(s) +} + +// FilterOrdersBySide removes any order details that don't match the +// order status provided +func FilterOrdersBySide(orders *[]Detail, side Side) { + if side == "" || side == AnySide { + return + } + + var filteredOrders []Detail + for i := range *orders { + if strings.EqualFold(string((*orders)[i].OrderSide), string(side)) { + filteredOrders = append(filteredOrders, (*orders)[i]) + } + } + + *orders = filteredOrders +} + +// FilterOrdersByType removes any order details that don't match the order type +// provided +func FilterOrdersByType(orders *[]Detail, orderType Type) { + if orderType == "" || orderType == AnyType { + return + } + + var filteredOrders []Detail + for i := range *orders { + if strings.EqualFold(string((*orders)[i].OrderType), string(orderType)) { + filteredOrders = append(filteredOrders, (*orders)[i]) + } + } + + *orders = filteredOrders +} + +// FilterOrdersByTickRange removes any OrderDetails outside of the tick range +func FilterOrdersByTickRange(orders *[]Detail, startTicks, endTicks time.Time) { + if startTicks.IsZero() || + endTicks.IsZero() || + startTicks.Unix() == 0 || + endTicks.Unix() == 0 || + endTicks.Before(startTicks) { + return + } + + var filteredOrders []Detail + for i := range *orders { + if (*orders)[i].OrderDate.Unix() >= startTicks.Unix() && + (*orders)[i].OrderDate.Unix() <= endTicks.Unix() { + filteredOrders = append(filteredOrders, (*orders)[i]) + } + } + + *orders = filteredOrders +} + +// FilterOrdersByCurrencies removes any order details that do not match the +// provided currency list. It is forgiving in that the provided currencies can +// match quote or base currencies +func FilterOrdersByCurrencies(orders *[]Detail, currencies []currency.Pair) { + if len(currencies) == 0 { + return + } + + var filteredOrders []Detail + for i := range *orders { + matchFound := false + for _, c := range currencies { + if !matchFound && (*orders)[i].CurrencyPair.EqualIncludeReciprocal(c) { + matchFound = true + } + } + + if matchFound { + filteredOrders = append(filteredOrders, (*orders)[i]) + } + } + + *orders = filteredOrders +} + +func (b ByPrice) Len() int { + return len(b) +} + +func (b ByPrice) Less(i, j int) bool { + return b[i].Price < b[j].Price +} + +func (b ByPrice) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} + +// SortOrdersByPrice the caller function to sort orders +func SortOrdersByPrice(orders *[]Detail, reverse bool) { + if reverse { + sort.Sort(sort.Reverse(ByPrice(*orders))) + } else { + sort.Sort(ByPrice(*orders)) + } +} + +func (b ByOrderType) Len() int { + return len(b) +} + +func (b ByOrderType) Less(i, j int) bool { + return b[i].OrderType.String() < b[j].OrderType.String() +} + +func (b ByOrderType) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} + +// SortOrdersByType the caller function to sort orders +func SortOrdersByType(orders *[]Detail, reverse bool) { + if reverse { + sort.Sort(sort.Reverse(ByOrderType(*orders))) + } else { + sort.Sort(ByOrderType(*orders)) + } +} + +func (b ByCurrency) Len() int { + return len(b) +} + +func (b ByCurrency) Less(i, j int) bool { + return b[i].CurrencyPair.String() < b[j].CurrencyPair.String() +} + +func (b ByCurrency) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} + +// SortOrdersByCurrency the caller function to sort orders +func SortOrdersByCurrency(orders *[]Detail, reverse bool) { + if reverse { + sort.Sort(sort.Reverse(ByCurrency(*orders))) + } else { + sort.Sort(ByCurrency(*orders)) + } +} + +func (b ByDate) Len() int { + return len(b) +} + +func (b ByDate) Less(i, j int) bool { + return b[i].OrderDate.Unix() < b[j].OrderDate.Unix() +} + +func (b ByDate) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} + +// SortOrdersByDate the caller function to sort orders +func SortOrdersByDate(orders *[]Detail, reverse bool) { + if reverse { + sort.Sort(sort.Reverse(ByDate(*orders))) + } else { + sort.Sort(ByDate(*orders)) + } +} + +func (b ByOrderSide) Len() int { + return len(b) +} + +func (b ByOrderSide) Less(i, j int) bool { + return b[i].OrderSide.String() < b[j].OrderSide.String() +} + +func (b ByOrderSide) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} + +// SortOrdersBySide the caller function to sort orders +func SortOrdersBySide(orders *[]Detail, reverse bool) { + if reverse { + sort.Sort(sort.Reverse(ByOrderSide(*orders))) + } else { + sort.Sort(ByOrderSide(*orders)) + } +} + +// StringToOrderSide for converting case insensitive order side +// and returning a real Side +func StringToOrderSide(side string) (Side, error) { + switch { + case strings.EqualFold(side, Buy.String()): + return Buy, nil + case strings.EqualFold(side, Sell.String()): + return Sell, nil + case strings.EqualFold(side, Bid.String()): + return Bid, nil + case strings.EqualFold(side, Ask.String()): + return Ask, nil + case strings.EqualFold(side, AnySide.String()): + return AnySide, nil + default: + return Side(""), fmt.Errorf("%s not recognised as side type", side) + } +} + +// StringToOrderType for converting case insensitive order type +// and returning a real Type +func StringToOrderType(oType string) (Type, error) { + switch { + case strings.EqualFold(oType, Limit.String()): + return Limit, nil + case strings.EqualFold(oType, Market.String()): + return Market, nil + case strings.EqualFold(oType, ImmediateOrCancel.String()): + return ImmediateOrCancel, nil + case strings.EqualFold(oType, Stop.String()): + return Stop, nil + case strings.EqualFold(oType, TrailingStop.String()): + return TrailingStop, nil + case strings.EqualFold(oType, AnyType.String()): + return AnyType, nil + default: + return Unknown, fmt.Errorf("%s not recognised as order type", oType) + } +} + +// StringToOrderStatus for converting case insensitive order status +// and returning a real Status +func StringToOrderStatus(status string) (Status, error) { + switch { + case strings.EqualFold(status, AnyStatus.String()): + return AnyStatus, nil + case strings.EqualFold(status, New.String()): + return New, nil + case strings.EqualFold(status, Active.String()): + return Active, nil + case strings.EqualFold(status, PartiallyFilled.String()): + return PartiallyFilled, nil + case strings.EqualFold(status, Filled.String()): + return Filled, nil + case strings.EqualFold(status, Cancelled.String()): + return Cancelled, nil + case strings.EqualFold(status, PendingCancel.String()): + return PendingCancel, nil + case strings.EqualFold(status, Rejected.String()): + return Rejected, nil + case strings.EqualFold(status, Expired.String()): + return Expired, nil + case strings.EqualFold(status, Hidden.String()): + return Hidden, nil + default: + return UnknownStatus, fmt.Errorf("%s not recognised as order STATUS", status) + } +} diff --git a/exchanges/orders/orders_test.go b/exchanges/order/orders_test.go similarity index 55% rename from exchanges/orders/orders_test.go rename to exchanges/order/orders_test.go index 4237165f..13c92bb8 100644 --- a/exchanges/orders/orders_test.go +++ b/exchanges/order/orders_test.go @@ -1,4 +1,4 @@ -package orders +package order import ( "testing" @@ -7,31 +7,31 @@ import ( func TestNewOrder(t *testing.T) { ID := NewOrder("ANX", 2000, 20.00) if ID != 0 { - t.Error("Test Failed - Orders_test.go NewOrder() - Error") + t.Error("Orders_test.go NewOrder() - Error") } ID = NewOrder("BATMAN", 400, 25.00) if ID != 1 { - t.Error("Test Failed - Orders_test.go NewOrder() - Error") + t.Error("Orders_test.go NewOrder() - Error") } } func TestDeleteOrder(t *testing.T) { if value := DeleteOrder(0); !value { - t.Error("Test Failed - Orders_test.go DeleteOrder() - Error") + t.Error("Orders_test.go DeleteOrder() - Error") } if value := DeleteOrder(100); value { - t.Error("Test Failed - Orders_test.go DeleteOrder() - Error") + t.Error("Orders_test.go DeleteOrder() - Error") } } func TestGetOrdersByExchange(t *testing.T) { if value := GetOrdersByExchange("ANX"); len(value) != 0 { - t.Error("Test Failed - Orders_test.go GetOrdersByExchange() - Error") + t.Error("Orders_test.go GetOrdersByExchange() - Error") } } func TestGetOrderByOrderID(t *testing.T) { if value := GetOrderByOrderID(69); value != nil { - t.Error("Test Failed - Orders_test.go GetOrdersByExchange() - Error") + t.Error("Orders_test.go GetOrdersByExchange() - Error") } } diff --git a/exchanges/orderbook/README.md b/exchanges/orderbook/README.md index c9c43871..e9befce8 100644 --- a/exchanges/orderbook/README.md +++ b/exchanges/orderbook/README.md @@ -34,7 +34,7 @@ exchange interface system set by exchange wrapper orderbook functions in Examples below: ```go -ob, err := yobitExchange.GetOrderbookEx() +ob, err := yobitExchange.FetchOrderbook() if err != nil { // Handle error } @@ -73,4 +73,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/orderbook/calculator.go b/exchanges/orderbook/calculator.go new file mode 100644 index 00000000..18e474e5 --- /dev/null +++ b/exchanges/orderbook/calculator.go @@ -0,0 +1,226 @@ +package orderbook + +import ( + "errors" + "fmt" + "sort" + + math "github.com/thrasher-corp/gocryptotrader/common/math" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// WhaleBombResult returns the whale bomb result +type WhaleBombResult struct { + Amount float64 + MinimumPrice float64 + MaximumPrice float64 + PercentageGainOrLoss float64 + Orders orderSummary + Status string +} + +// WhaleBomb finds the amount required to target a price +func (b *Base) WhaleBomb(priceTarget float64, buy bool) (*WhaleBombResult, error) { + if priceTarget < 0 { + return nil, errors.New("price target is invalid") + } + if buy { + a, orders := b.findAmount(priceTarget, true) + min, max := orders.MinimumPrice(false), orders.MaximumPrice(true) + var err error + if max < priceTarget { + err = errors.New("unable to hit price target due to insufficient orderbook items") + } + status := fmt.Sprintf("Buying %.2f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.", + a, b.Pair.Quote.String(), b.Pair.Base.String(), min, max, + math.CalculatePercentageGainOrLoss(max, min), len(orders)) + return &WhaleBombResult{ + Amount: a, + Orders: orders, + MinimumPrice: min, + MaximumPrice: max, + Status: status, + }, err + } + + a, orders := b.findAmount(priceTarget, false) + min, max := orders.MinimumPrice(false), orders.MaximumPrice(true) + var err error + if min > priceTarget { + err = errors.New("unable to hit price target due to insufficient orderbook items") + } + status := fmt.Sprintf("Selling %.2f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.", + a, b.Pair.Base.String(), b.Pair.Quote.String(), max, min, + math.CalculatePercentageGainOrLoss(min, max), len(orders)) + return &WhaleBombResult{ + Amount: a, + Orders: orders, + MinimumPrice: min, + MaximumPrice: max, + Status: status, + }, err +} + +// OrderSimulationResult returns the order simulation result +type OrderSimulationResult WhaleBombResult + +// SimulateOrder simulates an order +func (b *Base) SimulateOrder(amount float64, buy bool) *OrderSimulationResult { + if buy { + orders, amt := b.buy(amount) + min, max := orders.MinimumPrice(false), orders.MaximumPrice(true) + pct := math.CalculatePercentageGainOrLoss(max, min) + status := fmt.Sprintf("Buying %.2f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.", + amount, b.Pair.Quote.String(), b.Pair.Base.String(), min, max, + pct, len(orders)) + return &OrderSimulationResult{ + Orders: orders, + Amount: amt, + MinimumPrice: min, + MaximumPrice: max, + PercentageGainOrLoss: pct, + Status: status, + } + } + orders, amt := b.sell(amount) + min, max := orders.MinimumPrice(false), orders.MaximumPrice(true) + pct := math.CalculatePercentageGainOrLoss(min, max) + status := fmt.Sprintf("Selling %f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.", + amount, b.Pair.Base.String(), b.Pair.Quote.String(), max, min, + pct, len(orders)) + return &OrderSimulationResult{ + Orders: orders, + Amount: amt, + MinimumPrice: min, + MaximumPrice: max, + PercentageGainOrLoss: pct, + Status: status, + } +} + +type orderSummary []Item + +func (o orderSummary) Print() { + for x := range o { + log.Debugf(log.OrderBook, "Order: Price: %f Amount: %f", o[x].Price, o[x].Amount) + } +} + +func (o orderSummary) MinimumPrice(reverse bool) float64 { + if len(o) != 0 { + sortOrdersByPrice(&o, reverse) + return o[0].Price + } + return 0 +} + +func (o orderSummary) MaximumPrice(reverse bool) float64 { + if len(o) != 0 { + sortOrdersByPrice(&o, reverse) + return o[0].Price + } + return 0 +} + +// ByPrice used for sorting orders by order date +type ByPrice orderSummary + +func (b ByPrice) Len() int { return len(b) } +func (b ByPrice) Less(i, j int) bool { return b[i].Price < b[j].Price } +func (b ByPrice) Swap(i, j int) { b[i], b[j] = b[j], b[i] } + +// sortOrdersByPrice the caller function to sort orders +func sortOrdersByPrice(o *orderSummary, reverse bool) { + if reverse { + sort.Sort(sort.Reverse(ByPrice(*o))) + } else { + sort.Sort(ByPrice(*o)) + } +} + +func (b *Base) findAmount(price float64, buy bool) (float64, orderSummary) { + var orders orderSummary + var amt float64 + + if buy { + for x := range b.Asks { + if b.Asks[x].Price >= price { + amt += b.Asks[x].Price * b.Asks[x].Amount + orders = append(orders, Item{ + Price: b.Asks[x].Price, + Amount: b.Asks[x].Amount, + }) + return amt, orders + } + orders = append(orders, Item{ + Price: b.Asks[x].Price, + Amount: b.Asks[x].Amount, + }) + amt += b.Asks[x].Price * b.Asks[x].Amount + } + return amt, orders + } + + for x := range b.Bids { + if b.Bids[x].Price <= price { + amt += b.Bids[x].Amount + orders = append(orders, Item{ + Price: b.Bids[x].Price, + Amount: b.Bids[x].Amount, + }) + break + } + orders = append(orders, Item{ + Price: b.Bids[x].Price, + Amount: b.Bids[x].Amount, + }) + amt += b.Bids[x].Amount + } + return amt, orders +} + +func (b *Base) buy(amount float64) (orders orderSummary, baseAmount float64) { + var processedAmt float64 + for x := range b.Asks { + subtotal := b.Asks[x].Price * b.Asks[x].Amount + if processedAmt+subtotal >= amount { + diff := amount - processedAmt + subAmt := diff / b.Asks[x].Price + orders = append(orders, Item{ + Price: b.Asks[x].Price, + Amount: subAmt, + }) + baseAmount += subAmt + break + } + processedAmt += subtotal + baseAmount += b.Asks[x].Amount + orders = append(orders, Item{ + Price: b.Asks[x].Price, + Amount: b.Asks[x].Amount, + }) + } + return +} + +func (b *Base) sell(amount float64) (orders orderSummary, quoteAmount float64) { + var processedAmt float64 + for x := range b.Bids { + if processedAmt+b.Bids[x].Amount >= amount { + diff := amount - processedAmt + orders = append(orders, Item{ + Price: b.Bids[x].Price, + Amount: diff, + }) + quoteAmount += diff * b.Bids[x].Price + break + } + processedAmt += b.Bids[x].Amount + quoteAmount += b.Bids[x].Amount * b.Bids[x].Price + orders = append(orders, Item{ + Price: b.Bids[x].Price, + Amount: b.Bids[x].Amount, + }) + } + return +} diff --git a/exchanges/orderbook/calculator_test.go b/exchanges/orderbook/calculator_test.go new file mode 100644 index 00000000..315a349e --- /dev/null +++ b/exchanges/orderbook/calculator_test.go @@ -0,0 +1,79 @@ +package orderbook + +import ( + "testing" + + "github.com/thrasher-corp/gocryptotrader/currency" +) + +func testSetup() Base { + return Base{ + ExchangeName: "a", + Pair: currency.NewPair(currency.BTC, currency.USD), + Asks: []Item{ + {Price: 7000, Amount: 1}, + {Price: 7001, Amount: 2}, + }, + Bids: []Item{ + {Price: 6999, Amount: 1}, + {Price: 6998, Amount: 2}, + }, + } +} + +func TestWhaleBomb(t *testing.T) { + t.Parallel() + b := testSetup() + + // invalid price amout + _, err := b.WhaleBomb(-1, true) + if err == nil { + t.Error("unexpected result") + } + + // valid + b.WhaleBomb(7001, true) + // invalid + b.WhaleBomb(7002, true) + + // valid + b.WhaleBomb(6998, false) + // invalid + b.WhaleBomb(6997, false) +} + +func TestSimulateOrder(t *testing.T) { + t.Parallel() + b := testSetup() + b.SimulateOrder(8000, true) + b.SimulateOrder(1.5, false) +} + +func TestOrderSummary(t *testing.T) { + var o orderSummary + if p := o.MaximumPrice(false); p != 0 { + t.Error("unexpected result") + } + if p := o.MinimumPrice(false); p != 0 { + t.Error("unexpected result") + } + + o = orderSummary{ + {Price: 1337, Amount: 1}, + {Price: 9001, Amount: 1}, + } + if p := o.MaximumPrice(false); p != 1337 { + t.Error("unexpected result") + } + if p := o.MaximumPrice(true); p != 9001 { + t.Error("unexpected result") + } + if p := o.MinimumPrice(false); p != 1337 { + t.Error("unexpected result") + } + if p := o.MinimumPrice(true); p != 9001 { + t.Error("unexpected result") + } + + o.Print() +} diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index 572c98e5..4d9392f6 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -2,195 +2,270 @@ package orderbook import ( "errors" - "sync" + "fmt" + "sort" + "strings" "time" + "github.com/gofrs/uuid" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/dispatch" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) -// const values for orderbook package -const ( - errExchangeOrderbookNotFound = "orderbook for exchange does not exist" - errPairNotSet = "orderbook currency pair not set" - errAssetTypeNotSet = "orderbook asset type not set" - errBaseCurrencyNotFound = "orderbook base currency not found" - errQuoteCurrencyNotFound = "orderbook quote currency not found" - - Spot = "SPOT" - Swap = "SWAP" -) - -// Vars for the orderbook package -var ( - Orderbooks []Orderbook - m sync.Mutex -) - -// Item stores the amount and price values -type Item struct { - Amount float64 - Price float64 - ID int64 +// Get checks and returns the orderbook given an exchange name and currency pair +// if it exists +func Get(exchange string, p currency.Pair, a asset.Item) (Base, error) { + o, err := service.Retrieve(exchange, p, a) + if err != nil { + return Base{}, err + } + return *o, nil } -// Base holds the fields for the orderbook base -type Base struct { - Pair currency.Pair `json:"pair"` - Bids []Item `json:"bids"` - Asks []Item `json:"asks"` - LastUpdated time.Time `json:"lastUpdated"` - AssetType string `json:"assetType"` - ExchangeName string `json:"exchangeName"` +// SubscribeOrderbook subcribes to an orderbook and returns a communication +// channel to stream orderbook data updates +func SubscribeOrderbook(exchange string, p currency.Pair, a asset.Item) (dispatch.Pipe, error) { + exchange = strings.ToLower(exchange) + service.RLock() + defer service.RUnlock() + book, ok := service.Books[exchange][p.Base.Item][p.Quote.Item][a] + if !ok { + return dispatch.Pipe{}, fmt.Errorf("orderbook item not found for %s %s %s", + exchange, + p, + a) + } + + return service.mux.Subscribe(book.Main) } -// Orderbook holds the orderbook information for a currency pair and type -type Orderbook struct { - Orderbook map[*currency.Item]map[*currency.Item]map[string]Base - ExchangeName string +// SubscribeToExchangeOrderbooks subcribes to all orderbooks on an exchange +func SubscribeToExchangeOrderbooks(exchange string) (dispatch.Pipe, error) { + exchange = strings.ToLower(exchange) + service.RLock() + defer service.RUnlock() + id, ok := service.Exchange[exchange] + if !ok { + return dispatch.Pipe{}, fmt.Errorf("%s exchange orderbooks not found", + exchange) + } + + return service.mux.Subscribe(id) +} + +// Update stores orderbook data +func (s *Service) Update(b *Base) error { + var ids []uuid.UUID + + s.Lock() + switch { + case s.Books[b.ExchangeName] == nil: + s.Books[b.ExchangeName] = make(map[*currency.Item]map[*currency.Item]map[asset.Item]*Book) + s.Books[b.ExchangeName][b.Pair.Base.Item] = make(map[*currency.Item]map[asset.Item]*Book) + s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item] = make(map[asset.Item]*Book) + err := s.SetNewData(b) + if err != nil { + s.Unlock() + return err + } + + case s.Books[b.ExchangeName][b.Pair.Base.Item] == nil: + s.Books[b.ExchangeName][b.Pair.Base.Item] = make(map[*currency.Item]map[asset.Item]*Book) + s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item] = make(map[asset.Item]*Book) + err := s.SetNewData(b) + if err != nil { + s.Unlock() + return err + } + + case s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item] == nil: + s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item] = make(map[asset.Item]*Book) + err := s.SetNewData(b) + if err != nil { + s.Unlock() + return err + } + + case s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType] == nil: + err := s.SetNewData(b) + if err != nil { + s.Unlock() + return err + } + + default: + book := s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType] + book.b.Bids = b.Bids + book.b.Asks = b.Asks + book.b.LastUpdated = b.LastUpdated + ids = book.Assoc + ids = append(ids, book.Main) + } + s.Unlock() + return s.mux.Publish(ids, b) +} + +// SetNewData sets new data +func (s *Service) SetNewData(b *Base) error { + ids, err := s.GetAssociations(b) + if err != nil { + return err + } + singleID, err := s.mux.GetID() + if err != nil { + return err + } + + // Below instigates orderbook item separation so we can ensure, in the event + // of a simultaneous update via websocket/rest/fix, we don't affect package + // scoped orderbook data which could result in a potential panic + cpyBook := *b + cpyBook.Bids = make([]Item, len(b.Bids)) + copy(cpyBook.Bids, b.Bids) + cpyBook.Asks = make([]Item, len(b.Asks)) + copy(cpyBook.Asks, b.Asks) + + s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType] = &Book{ + b: &cpyBook, + Main: singleID, + Assoc: ids} + return nil +} + +// GetAssociations links a singular book with it's dispatch associations +func (s *Service) GetAssociations(b *Base) ([]uuid.UUID, error) { + if b == nil { + return nil, errors.New("orderbook is nil") + } + + var ids []uuid.UUID + exchangeID, ok := s.Exchange[b.ExchangeName] + if !ok { + var err error + exchangeID, err = s.mux.GetID() + if err != nil { + return nil, err + } + s.Exchange[b.ExchangeName] = exchangeID + } + + ids = append(ids, exchangeID) + return ids, nil +} + +// Retrieve gets orderbook data from the slice +func (s *Service) Retrieve(exchange string, p currency.Pair, a asset.Item) (*Base, error) { + exchange = strings.ToLower(exchange) + s.RLock() + defer s.RUnlock() + if s.Books[exchange] == nil { + return nil, fmt.Errorf("no orderbooks for %s exchange", exchange) + } + + if s.Books[exchange][p.Base.Item] == nil { + return nil, fmt.Errorf("no orderbooks associated with base currency %s", + p.Base) + } + + if s.Books[exchange][p.Base.Item][p.Quote.Item] == nil { + return nil, fmt.Errorf("no orderbooks associated with quote currency %s", + p.Quote) + } + + if s.Books[exchange][p.Base.Item][p.Quote.Item][a] == nil { + return nil, fmt.Errorf("no orderbooks associated with asset type %s", + a) + } + + return s.Books[exchange][p.Base.Item][p.Quote.Item][a].b, nil } // TotalBidsAmount returns the total amount of bids and the total orderbook // bids value -func (o *Base) TotalBidsAmount() (amountCollated, total float64) { - for _, x := range o.Bids { - amountCollated += x.Amount - total += x.Amount * x.Price +func (b *Base) TotalBidsAmount() (amountCollated, total float64) { + for x := range b.Bids { + amountCollated += b.Bids[x].Amount + total += b.Bids[x].Amount * b.Bids[x].Price } return amountCollated, total } // TotalAsksAmount returns the total amount of asks and the total orderbook // asks value -func (o *Base) TotalAsksAmount() (amountCollated, total float64) { - for _, x := range o.Asks { - amountCollated += x.Amount - total += x.Amount * x.Price +func (b *Base) TotalAsksAmount() (amountCollated, total float64) { + for y := range b.Asks { + amountCollated += b.Asks[y].Amount + total += b.Asks[y].Amount * b.Asks[y].Price } return amountCollated, total } // Update updates the bids and asks -func (o *Base) Update(bids, asks []Item) { - o.Bids = bids - o.Asks = asks - o.LastUpdated = time.Now() +func (b *Base) Update(bids, asks []Item) { + b.Bids = bids + b.Asks = asks + b.LastUpdated = time.Now() } -// Get checks and returns the orderbook given an exchange name and currency pair -// if it exists -func Get(exchange string, p currency.Pair, orderbookType string) (Base, error) { - orderbook, err := GetByExchange(exchange) - if err != nil { - return Base{}, err - } - - if !BaseCurrencyExists(exchange, p.Base) { - return Base{}, errors.New(errBaseCurrencyNotFound) - } - - if !QuoteCurrencyExists(exchange, p) { - return Base{}, errors.New(errQuoteCurrencyNotFound) - } - - return orderbook.Orderbook[p.Base.Item][p.Quote.Item][orderbookType], nil -} - -// GetByExchange returns an exchange orderbook -func GetByExchange(exchange string) (*Orderbook, error) { - m.Lock() - defer m.Unlock() - for x := range Orderbooks { - if Orderbooks[x].ExchangeName == exchange { - return &Orderbooks[x], nil +// Verify ensures that the orderbook items are correctly sorted +// Bids should always go from a high price to a low price and +// asks should always go from a low price to a higher price +func (b *Base) Verify() { + var lastPrice float64 + var sortBids, sortAsks bool + for x := range b.Bids { + if lastPrice != 0 && b.Bids[x].Price >= lastPrice { + sortBids = true + break } + lastPrice = b.Bids[x].Price } - return nil, errors.New(errExchangeOrderbookNotFound) -} -// BaseCurrencyExists checks to see if the base currency of the orderbook map -// exists -func BaseCurrencyExists(exchange string, currency currency.Code) bool { - m.Lock() - defer m.Unlock() - for _, y := range Orderbooks { - if y.ExchangeName == exchange { - if _, ok := y.Orderbook[currency.Item]; ok { - return true - } + lastPrice = 0 + for x := range b.Asks { + if lastPrice != 0 && b.Asks[x].Price <= lastPrice { + sortAsks = true + break } + lastPrice = b.Asks[x].Price } - return false -} -// QuoteCurrencyExists checks to see if the quote currency of the orderbook -// map exists -func QuoteCurrencyExists(exchange string, p currency.Pair) bool { - m.Lock() - defer m.Unlock() - for _, y := range Orderbooks { - if y.ExchangeName == exchange { - if _, ok := y.Orderbook[p.Base.Item]; ok { - if _, ok := y.Orderbook[p.Base.Item][p.Quote.Item]; ok { - return true - } - } - } + if sortBids { + sort.Sort(sort.Reverse(byOBPrice(b.Bids))) } - return false -} -// CreateNewOrderbook creates a new orderbook -func CreateNewOrderbook(exchangeName string, orderbookNew *Base, orderbookType string) *Orderbook { - m.Lock() - defer m.Unlock() - orderbook := Orderbook{} - orderbook.ExchangeName = exchangeName - orderbook.Orderbook = make(map[*currency.Item]map[*currency.Item]map[string]Base) - a := make(map[*currency.Item]map[string]Base) - b := make(map[string]Base) - b[orderbookType] = *orderbookNew - a[orderbookNew.Pair.Quote.Item] = b - orderbook.Orderbook[orderbookNew.Pair.Base.Item] = a - Orderbooks = append(Orderbooks, orderbook) - return &orderbook + if sortAsks { + sort.Sort((byOBPrice(b.Asks))) + } } // Process processes incoming orderbooks, creating or updating the orderbook // list -func (o *Base) Process() error { - if o.Pair.IsEmpty() { +func (b *Base) Process() error { + if b.ExchangeName == "" { + return errors.New(errExchangeNameUnset) + } + + b.ExchangeName = strings.ToLower(b.ExchangeName) + + if b.Pair.IsEmpty() { return errors.New(errPairNotSet) } - if o.AssetType == "" { + if b.AssetType.String() == "" { return errors.New(errAssetTypeNotSet) } - if o.LastUpdated.IsZero() { - o.LastUpdated = time.Now() + if len(b.Asks) == 0 && len(b.Bids) == 0 { + return errors.New(errNoOrderbook) } - orderbook, err := GetByExchange(o.ExchangeName) - if err != nil { - CreateNewOrderbook(o.ExchangeName, o, o.AssetType) - return nil + if b.LastUpdated.IsZero() { + b.LastUpdated = time.Now() } - if BaseCurrencyExists(o.ExchangeName, o.Pair.Base) { - m.Lock() - a := make(map[string]Base) - a[o.AssetType] = *o - orderbook.Orderbook[o.Pair.Base.Item][o.Pair.Quote.Item] = a - m.Unlock() - return nil - } + b.Verify() - m.Lock() - a := make(map[*currency.Item]map[string]Base) - b := make(map[string]Base) - b[o.AssetType] = *o - a[o.Pair.Quote.Item] = b - orderbook.Orderbook[o.Pair.Base.Item] = a - m.Unlock() - return nil + return service.Update(b) } diff --git a/exchanges/orderbook/orderbook_test.go b/exchanges/orderbook/orderbook_test.go index ebbd2110..8c767abf 100644 --- a/exchanges/orderbook/orderbook_test.go +++ b/exchanges/orderbook/orderbook_test.go @@ -1,51 +1,196 @@ package orderbook import ( + "log" "math/rand" + "os" "strconv" "sync" "testing" "time" "github.com/thrasher-corp/gocryptotrader/currency" - log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/gocryptotrader/dispatch" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) +func TestMain(m *testing.M) { + err := dispatch.Start(1, dispatch.DefaultJobsLimit) + if err != nil { + log.Fatal(err) + } + + cpyMux = service.mux + + os.Exit(m.Run()) +} + +var cpyMux *dispatch.Mux + +func TestSubscribeOrderbook(t *testing.T) { + _, err := SubscribeOrderbook("", currency.Pair{}, asset.Item("")) + if err == nil { + t.Error("error cannot be nil") + } + + p := currency.NewPair(currency.BTC, currency.USD) + + b := Base{ + Pair: p, + AssetType: asset.Spot, + } + + err = b.Process() + if err == nil { + t.Error("error cannot be nil") + } + + b.ExchangeName = "SubscribeOBTest" + + err = b.Process() + if err == nil { + t.Error("error cannot be nil") + } + + b.Bids = []Item{{}} + + err = b.Process() + if err != nil { + t.Error("process error", err) + } + + _, err = SubscribeOrderbook("SubscribeOBTest", p, asset.Spot) + if err != nil { + t.Error("error cannot be nil") + } + + // process redundant update + err = b.Process() + if err != nil { + t.Error("process error", err) + } +} + +func TestUpdateBooks(t *testing.T) { + p := currency.NewPair(currency.BTC, currency.USD) + + b := Base{ + Pair: p, + AssetType: asset.Spot, + ExchangeName: "UpdateTest", + } + + service.mux = nil + + err := service.Update(&b) + if err == nil { + t.Error("error cannot be nil") + } + + b.Pair.Base = currency.CYC + err = service.Update(&b) + if err == nil { + t.Error("error cannot be nil") + } + + b.Pair.Quote = currency.ENAU + err = service.Update(&b) + if err == nil { + t.Error("error cannot be nil") + } + + b.AssetType = "unicorns" + err = service.Update(&b) + if err == nil { + t.Error("error cannot be nil") + } + + service.mux = cpyMux +} + +func TestSubscribeToExchangeOrderbooks(t *testing.T) { + _, err := SubscribeToExchangeOrderbooks("") + if err == nil { + t.Error("error cannot be nil") + } + + p := currency.NewPair(currency.BTC, currency.USD) + + b := Base{ + Pair: p, + AssetType: asset.Spot, + ExchangeName: "SubscribeToExchangeOrderbooks", + Bids: []Item{{}}, + } + + err = b.Process() + if err != nil { + t.Error("", err) + } + + _, err = SubscribeToExchangeOrderbooks("SubscribeToExchangeOrderbooks") + if err != nil { + t.Error(err) + } +} + +func TestVerify(t *testing.T) { + t.Parallel() + b := Base{ + ExchangeName: "TestExchange", + Pair: currency.NewPair(currency.BTC, currency.USD), + Bids: []Item{ + {Price: 100}, {Price: 101}, {Price: 99}, + }, + Asks: []Item{ + {Price: 100}, {Price: 99}, {Price: 101}, + }, + } + + b.Verify() + if r := b.Bids[1].Price; r != 100 { + t.Error("unexpected result") + } + if r := b.Asks[1].Price; r != 100 { + t.Error("unexpected result") + } +} + func TestCalculateTotalBids(t *testing.T) { t.Parallel() - currency := currency.NewPairFromStrings("BTC", "USD") + curr := currency.NewPairFromStrings("BTC", "USD") base := Base{ - Pair: currency, + Pair: curr, Bids: []Item{{Price: 100, Amount: 10}}, LastUpdated: time.Now(), } a, b := base.TotalBidsAmount() if a != 10 && b != 1000 { - t.Fatal("Test failed. TestCalculateTotalBids expected a = 10 and b = 1000") + t.Fatal("TestCalculateTotalBids expected a = 10 and b = 1000") } } func TestCalculateTotaAsks(t *testing.T) { t.Parallel() - currency := currency.NewPairFromStrings("BTC", "USD") + curr := currency.NewPairFromStrings("BTC", "USD") base := Base{ - Pair: currency, + Pair: curr, Asks: []Item{{Price: 100, Amount: 10}}, } a, b := base.TotalAsksAmount() if a != 10 && b != 1000 { - t.Fatal("Test failed. TestCalculateTotalAsks expected a = 10 and b = 1000") + t.Fatal("TestCalculateTotalAsks expected a = 10 and b = 1000") } } func TestUpdate(t *testing.T) { t.Parallel() - currency := currency.NewPairFromStrings("BTC", "USD") + curr := currency.NewPairFromStrings("BTC", "USD") timeNow := time.Now() base := Base{ - Pair: currency, + Pair: curr, Asks: []Item{{Price: 100, Amount: 10}}, Bids: []Item{{Price: 200, Amount: 10}}, LastUpdated: timeNow, @@ -57,156 +202,114 @@ func TestUpdate(t *testing.T) { base.Update(bids, asks) if !base.LastUpdated.After(timeNow) { - t.Fatal("test failed. TestUpdate expected LastUpdated to be greater then original time") + t.Fatal("TestUpdate expected LastUpdated to be greater then original time") } a, b := base.TotalAsksAmount() if a != 100 && b != 20200 { - t.Fatal("Test failed. TestUpdate expected a = 100 and b = 20100") + t.Fatal("TestUpdate expected a = 100 and b = 20100") } a, b = base.TotalBidsAmount() if a != 100 && b != 20100 { - t.Fatal("Test failed. TestUpdate expected a = 100 and b = 20100") + t.Fatal("TestUpdate expected a = 100 and b = 20100") } } func TestGetOrderbook(t *testing.T) { c := currency.NewPairFromStrings("BTC", "USD") - base := Base{ - Pair: c, - Asks: []Item{{Price: 100, Amount: 10}}, - Bids: []Item{{Price: 200, Amount: 10}}, + base := &Base{ + Pair: c, + Asks: []Item{{Price: 100, Amount: 10}}, + Bids: []Item{{Price: 200, Amount: 10}}, + ExchangeName: "Exchange", + AssetType: asset.Spot, } - CreateNewOrderbook("Exchange", &base, Spot) - - result, err := Get("Exchange", c, Spot) + err := base.Process() if err != nil { - t.Fatalf("Test failed. TestGetOrderbook failed to get orderbook. Error %s", + t.Fatal(err) + } + + result, err := Get("Exchange", c, asset.Spot) + if err != nil { + t.Fatalf("TestGetOrderbook failed to get orderbook. Error %s", err) } if !result.Pair.Equal(c) { - t.Fatal("Test failed. TestGetOrderbook failed. Mismatched pairs") + t.Fatal("TestGetOrderbook failed. Mismatched pairs") } - _, err = Get("nonexistent", c, Spot) + _, err = Get("nonexistent", c, asset.Spot) if err == nil { - t.Fatal("Test failed. TestGetOrderbook retrieved non-existent orderbook") + t.Fatal("TestGetOrderbook retrieved non-existent orderbook") } c.Base = currency.NewCode("blah") - _, err = Get("Exchange", c, Spot) + _, err = Get("Exchange", c, asset.Spot) if err == nil { - t.Fatal("Test failed. TestGetOrderbook retrieved non-existent orderbook using invalid first currency") + t.Fatal("TestGetOrderbook retrieved non-existent orderbook using invalid first currency") } newCurrency := currency.NewPairFromStrings("BTC", "AUD") - _, err = Get("Exchange", newCurrency, Spot) + _, err = Get("Exchange", newCurrency, asset.Spot) if err == nil { - t.Fatal("Test failed. TestGetOrderbook retrieved non-existent orderbook using invalid second currency") - } -} - -func TestGetOrderbookByExchange(t *testing.T) { - currency := currency.NewPairFromStrings("BTC", "USD") - base := Base{ - Pair: currency, - Asks: []Item{{Price: 100, Amount: 10}}, - Bids: []Item{{Price: 200, Amount: 10}}, + t.Fatal("TestGetOrderbook retrieved non-existent orderbook using invalid second currency") } - CreateNewOrderbook("Exchange", &base, Spot) - - _, err := GetByExchange("Exchange") + base.Pair = newCurrency + err = base.Process() if err != nil { - t.Fatalf("Test failed. TestGetOrderbookByExchange failed to get orderbook. Error %s", - err) + t.Error(err) } - _, err = GetByExchange("nonexistent") + _, err = Get("Exchange", newCurrency, "meowCats") if err == nil { - t.Fatal("Test failed. TestGetOrderbookByExchange retrieved non-existent orderbook") - } -} - -func TestFirstCurrencyExists(t *testing.T) { - c := currency.NewPairFromStrings("BTC", "AUD") - base := Base{ - Pair: c, - Asks: []Item{{Price: 100, Amount: 10}}, - Bids: []Item{{Price: 200, Amount: 10}}, - } - - CreateNewOrderbook("Exchange", &base, Spot) - - if !BaseCurrencyExists("Exchange", c.Base) { - t.Fatal("Test failed. TestFirstCurrencyExists expected first currency doesn't exist") - } - - var item = currency.NewCode("blah") - if BaseCurrencyExists("Exchange", item) { - t.Fatal("Test failed. TestFirstCurrencyExists unexpected first currency exists") - } -} - -func TestSecondCurrencyExists(t *testing.T) { - c := currency.NewPairFromStrings("BTC", "USD") - base := Base{ - Pair: c, - Asks: []Item{{Price: 100, Amount: 10}}, - Bids: []Item{{Price: 200, Amount: 10}}, - } - - CreateNewOrderbook("Exchange", &base, Spot) - - if !QuoteCurrencyExists("Exchange", c) { - t.Fatal("Test failed. TestSecondCurrencyExists expected first currency doesn't exist") - } - - c.Quote = currency.NewCode("blah") - if QuoteCurrencyExists("Exchange", c) { - t.Fatal("Test failed. TestSecondCurrencyExists unexpected first currency exists") + t.Error("error cannot be nil") } } func TestCreateNewOrderbook(t *testing.T) { c := currency.NewPairFromStrings("BTC", "USD") - base := Base{ - Pair: c, - Asks: []Item{{Price: 100, Amount: 10}}, - Bids: []Item{{Price: 200, Amount: 10}}, + base := &Base{ + Pair: c, + Asks: []Item{{Price: 100, Amount: 10}}, + Bids: []Item{{Price: 200, Amount: 10}}, + ExchangeName: "testCreateNewOrderbook", + AssetType: asset.Spot, } - CreateNewOrderbook("Exchange", &base, Spot) - - result, err := Get("Exchange", c, Spot) + err := base.Process() if err != nil { - t.Fatal("Test failed. TestCreateNewOrderbook failed to create new orderbook") + t.Fatal(err) + } + + result, err := Get("testCreateNewOrderbook", c, asset.Spot) + if err != nil { + t.Fatal("TestCreateNewOrderbook failed to create new orderbook", err) } if !result.Pair.Equal(c) { - t.Fatal("Test failed. TestCreateNewOrderbook result pair is incorrect") + t.Fatal("TestCreateNewOrderbook result pair is incorrect") } a, b := result.TotalAsksAmount() if a != 10 && b != 1000 { - t.Fatal("Test failed. TestCreateNewOrderbook CalculateTotalAsks value is incorrect") + t.Fatal("TestCreateNewOrderbook CalculateTotalAsks value is incorrect") } a, b = result.TotalBidsAmount() if a != 10 && b != 2000 { - t.Fatal("Test failed. TestCreateNewOrderbook CalculateTotalBids value is incorrect") + t.Fatal("TestCreateNewOrderbook CalculateTotalBids value is incorrect") } } func TestProcessOrderbook(t *testing.T) { - Orderbooks = []Orderbook{} c := currency.NewPairFromStrings("BTC", "USD") base := Base{ Asks: []Item{{Price: 100, Amount: 10}}, Bids: []Item{{Price: 200, Amount: 10}}, - ExchangeName: "Exchange", + ExchangeName: "ProcessOrderbook", } // test for empty pair @@ -224,17 +327,17 @@ func TestProcessOrderbook(t *testing.T) { } // now process a valid orderbook - base.AssetType = Spot + base.AssetType = asset.Spot err = base.Process() if err != nil { t.Error("unexpcted result: ", err) } - result, err := Get("Exchange", c, Spot) + result, err := Get("ProcessOrderbook", c, asset.Spot) if err != nil { - t.Fatal("Test failed. TestProcessOrderbook failed to create new orderbook") + t.Fatal("TestProcessOrderbook failed to create new orderbook") } if !result.Pair.Equal(c) { - t.Fatal("Test failed. TestProcessOrderbook result pair is incorrect") + t.Fatal("TestProcessOrderbook result pair is incorrect") } // now test for processing a pair with a different quote currency @@ -242,14 +345,14 @@ func TestProcessOrderbook(t *testing.T) { base.Pair = c err = base.Process() if err != nil { - t.Error("Test Failed - Process() error", err) + t.Error("Process() error", err) } - result, err = Get("Exchange", c, Spot) + result, err = Get("ProcessOrderbook", c, asset.Spot) if err != nil { - t.Fatal("Test failed. TestProcessOrderbook failed to retrieve new orderbook") + t.Fatal("TestProcessOrderbook failed to retrieve new orderbook") } if !result.Pair.Equal(c) { - t.Fatal("Test failed. TestProcessOrderbook result pair is incorrect") + t.Fatal("TestProcessOrderbook result pair is incorrect") } // now test for processing a pair which has a different base currency @@ -257,31 +360,31 @@ func TestProcessOrderbook(t *testing.T) { base.Pair = c err = base.Process() if err != nil { - t.Error("Test Failed - Process() error", err) + t.Error("Process() error", err) } - result, err = Get("Exchange", c, Spot) + result, err = Get("ProcessOrderbook", c, asset.Spot) if err != nil { - t.Fatal("Test failed. TestProcessOrderbook failed to retrieve new orderbook") + t.Fatal("TestProcessOrderbook failed to retrieve new orderbook") } if !result.Pair.Equal(c) { - t.Fatal("Test failed. TestProcessOrderbook result pair is incorrect") + t.Fatal("TestProcessOrderbook result pair is incorrect") } base.Asks = []Item{{Price: 200, Amount: 200}} base.AssetType = "monthly" err = base.Process() if err != nil { - t.Error("Test Failed - Process() error", err) + t.Error("Process() error", err) } - result, err = Get("Exchange", c, "monthly") + result, err = Get("ProcessOrderbook", c, "monthly") if err != nil { - t.Fatal("Test failed. TestProcessOrderbook failed to retrieve new orderbook") + t.Fatal("TestProcessOrderbook failed to retrieve new orderbook") } a, b := result.TotalAsksAmount() if a != 200 && b != 40000 { - t.Fatal("Test failed. TestProcessOrderbook CalculateTotalsAsks incorrect values") + t.Fatal("TestProcessOrderbook CalculateTotalsAsks incorrect values") } base.Bids = []Item{{Price: 420, Amount: 200}} @@ -289,16 +392,16 @@ func TestProcessOrderbook(t *testing.T) { base.AssetType = "quarterly" err = base.Process() if err != nil { - t.Error("Test Failed - Process() error", err) + t.Error("Process() error", err) } result, err = Get("Blah", c, "quarterly") if err != nil { - t.Fatal("Test failed. TestProcessOrderbook failed to create new orderbook") + t.Fatal("TestProcessOrderbook failed to create new orderbook") } if a != 200 && b != 84000 { - t.Fatal("Test failed. TestProcessOrderbook CalculateTotalsBids incorrect values") + t.Fatal("TestProcessOrderbook CalculateTotalsBids incorrect values") } type quick struct { @@ -335,13 +438,13 @@ func TestProcessOrderbook(t *testing.T) { Asks: asks, Bids: bids, ExchangeName: newName, - AssetType: Spot, + AssetType: asset.Spot, } m.Lock() err = base.Process() if err != nil { - log.Error(err) + t.Error(err) catastrophicFailure = true return } @@ -353,7 +456,7 @@ func TestProcessOrderbook(t *testing.T) { } if catastrophicFailure { - t.Fatal("Test Failed - Process() error", err) + t.Fatal("Process() error", err) } wg.Wait() @@ -362,27 +465,41 @@ func TestProcessOrderbook(t *testing.T) { wg.Add(1) fatalErr := false go func(test quick) { - result, err := Get(test.Name, test.P, Spot) + result, err := Get(test.Name, test.P, asset.Spot) if err != nil { fatalErr = true return } if result.Asks[0] != test.Asks[0] { - t.Error("Test failed. TestProcessOrderbook failed bad values") + t.Error("TestProcessOrderbook failed bad values") } if result.Bids[0] != test.Bids[0] { - t.Error("Test failed. TestProcessOrderbook failed bad values") + t.Error("TestProcessOrderbook failed bad values") } wg.Done() }(test) if fatalErr { - t.Fatal("Test failed. TestProcessOrderbook failed to retrieve new orderbook") + t.Fatal("TestProcessOrderbook failed to retrieve new orderbook") } } wg.Wait() } + +func TestSetNewData(t *testing.T) { + err := service.SetNewData(nil) + if err == nil { + t.Error("error cannot be nil") + } +} + +func TestGetAssociations(t *testing.T) { + _, err := service.GetAssociations(nil) + if err == nil { + t.Error("error cannot be nil") + } +} diff --git a/exchanges/orderbook/orderbook_types.go b/exchanges/orderbook/orderbook_types.go new file mode 100644 index 00000000..3686adcc --- /dev/null +++ b/exchanges/orderbook/orderbook_types.go @@ -0,0 +1,73 @@ +package orderbook + +import ( + "sync" + "time" + + "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/dispatch" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +// const values for orderbook package +const ( + errExchangeNameUnset = "orderbook exchange name not set" + errPairNotSet = "orderbook currency pair not set" + errAssetTypeNotSet = "orderbook asset type not set" + errNoOrderbook = "orderbook bids and asks are empty" +) + +// Vars for the orderbook package +var ( + service *Service +) + +func init() { + service = new(Service) + service.mux = dispatch.GetNewMux() + service.Books = make(map[string]map[*currency.Item]map[*currency.Item]map[asset.Item]*Book) + service.Exchange = make(map[string]uuid.UUID) +} + +// Book defines an orderbook with its links to different dispatch outputs +type Book struct { + b *Base + Main uuid.UUID + Assoc []uuid.UUID +} + +// Service holds orderbook information for each individual exchange +type Service struct { + Books map[string]map[*currency.Item]map[*currency.Item]map[asset.Item]*Book + Exchange map[string]uuid.UUID + mux *dispatch.Mux + sync.RWMutex +} + +// Item stores the amount and price values +type Item struct { + Amount float64 + Price float64 + ID int64 + + // Contract variables + LiquidationOrders int64 + OrderCount int64 +} + +// Base holds the fields for the orderbook base +type Base struct { + Pair currency.Pair `json:"pair"` + Bids []Item `json:"bids"` + Asks []Item `json:"asks"` + LastUpdated time.Time `json:"lastUpdated"` + AssetType asset.Item `json:"assetType"` + ExchangeName string `json:"exchangeName"` +} + +type byOBPrice []Item + +func (a byOBPrice) Len() int { return len(a) } +func (a byOBPrice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byOBPrice) Less(i, j int) bool { return a[i].Price < a[j].Price } diff --git a/exchanges/orderbook/simulator/simulator_test.go b/exchanges/orderbook/simulator/simulator_test.go new file mode 100644 index 00000000..14e5ec02 --- /dev/null +++ b/exchanges/orderbook/simulator/simulator_test.go @@ -0,0 +1,24 @@ +package simulator + +import ( + "testing" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/bitstamp" +) + +func TestSimulate(t *testing.T) { + b := bitstamp.Bitstamp{} + b.SetDefaults() + b.Verbose = false + o, err := b.FetchOrderbook(currency.NewPair(currency.BTC, currency.USD), asset.Spot) + if err != nil { + t.Error(err) + } + + r := o.SimulateOrder(10000000, true) + t.Log(r.Status) + r = o.SimulateOrder(2171, false) + t.Log(r.Status) +} diff --git a/exchanges/orders/orders.go b/exchanges/orders/orders.go deleted file mode 100644 index 8308b4b0..00000000 --- a/exchanges/orders/orders.go +++ /dev/null @@ -1,69 +0,0 @@ -package orders - -const ( - limitOrder = iota - marketOrder -) - -// Orders variable holds an array of pointers to order structs -var Orders []*Order - -// Order struct holds order values -type Order struct { - OrderID int - Exchange string - Type int - Amount float64 - Price float64 -} - -// NewOrder creates a new order and returns a an orderID -func NewOrder(exchangeName string, amount, price float64) int { - order := &Order{} - if len(Orders) == 0 { - order.OrderID = 0 - } else { - order.OrderID = len(Orders) - } - - order.Exchange = exchangeName - order.Amount = amount - order.Price = price - Orders = append(Orders, order) - return order.OrderID -} - -// DeleteOrder deletes orders by ID and returns state -func DeleteOrder(orderID int) bool { - for i := range Orders { - if Orders[i].OrderID == orderID { - Orders = append(Orders[:i], Orders[i+1:]...) - return true - } - } - return false -} - -// GetOrdersByExchange returns order pointer grouped by exchange -func GetOrdersByExchange(exchange string) []*Order { - var orders []*Order - for i := range Orders { - if Orders[i].Exchange == exchange { - orders = append(orders, Orders[i]) - } - } - if len(orders) > 0 { - return orders - } - return nil -} - -// GetOrderByOrderID returns order pointer by ID -func GetOrderByOrderID(orderID int) *Order { - for i := range Orders { - if Orders[i].OrderID == orderID { - return Orders[i] - } - } - return nil -} diff --git a/exchanges/poloniex/README.md b/exchanges/poloniex/README.md index 6e0ce789..743045d7 100644 --- a/exchanges/poloniex/README.md +++ b/exchanges/poloniex/README.md @@ -48,22 +48,22 @@ main.go ```go var p exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Poloniex" { - y = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Poloniex" { + y = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := p.GetTickerPrice() +tick, err := p.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := p.GetOrderbookEx() +ob, err := p.FetchOrderbook() if err != nil { // Handle error } @@ -138,4 +138,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/poloniex/poloniex.go b/exchanges/poloniex/poloniex.go index b5282294..eb28f579 100644 --- a/exchanges/poloniex/poloniex.go +++ b/exchanges/poloniex/poloniex.go @@ -10,14 +10,11 @@ import ( "strconv" "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( @@ -31,8 +28,6 @@ const ( poloniexDepositsWithdrawals = "returnDepositsWithdrawals" poloniexOrders = "returnOpenOrders" poloniexTradeHistory = "returnTradeHistory" - poloniexOrderBuy = "buy" - poloniexOrderSell = "sell" poloniexOrderCancel = "cancelOrder" poloniexOrderMove = "moveOrder" poloniexWithdraw = "withdraw" @@ -64,108 +59,6 @@ type Poloniex struct { WebsocketConn *wshandler.WebsocketConnection } -// SetDefaults sets default settings for poloniex -func (p *Poloniex) SetDefaults() { - p.Name = "Poloniex" - p.Enabled = false - p.Fee = 0 - p.Verbose = false - p.RESTPollingDelay = 10 - p.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithAPIPermission | - exchange.NoFiatWithdrawals - p.RequestCurrencyPairFormat.Delimiter = "_" - p.RequestCurrencyPairFormat.Uppercase = true - p.ConfigCurrencyPairFormat.Delimiter = "_" - p.ConfigCurrencyPairFormat.Uppercase = true - p.AssetTypes = []string{ticker.Spot} - p.SupportsAutoPairUpdating = true - p.SupportsRESTTickerBatching = true - p.Requester = request.New(p.Name, - request.NewRateLimit(time.Second, poloniexAuthRate), - request.NewRateLimit(time.Second, poloniexUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - p.APIUrlDefault = poloniexAPIURL - p.APIUrl = p.APIUrlDefault - p.Websocket = wshandler.New() - p.Websocket.Functionality = wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketTickerSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketUnsubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported - p.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - p.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - p.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup sets user exchange configuration settings -func (p *Poloniex) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - p.SetEnabled(false) - } else { - p.Enabled = true - p.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - p.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - p.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - p.SetHTTPClientTimeout(exch.HTTPTimeout) - p.SetHTTPClientUserAgent(exch.HTTPUserAgent) - p.RESTPollingDelay = exch.RESTPollingDelay - p.Verbose = exch.Verbose - p.HTTPDebugging = exch.HTTPDebugging - p.Websocket.SetWsStatusAndConnection(exch.Websocket) - p.BaseCurrencies = exch.BaseCurrencies - p.AvailablePairs = exch.AvailablePairs - p.EnabledPairs = exch.EnabledPairs - err := p.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = p.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = p.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = p.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = p.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = p.Websocket.Setup(p.WsConnect, - p.Subscribe, - p.Unsubscribe, - exch.Name, - exch.Websocket, - exch.Verbose, - poloniexWebsocketAddress, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - p.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: p.Name, - URL: p.Websocket.GetWebsocketURL(), - ProxyURL: p.Websocket.GetProxyAddress(), - Verbose: p.Verbose, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - p.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - true, - true, - true, - false, - exch.Name) - } -} - // GetTicker returns current ticker information func (p *Poloniex) GetTicker() (map[string]Ticker, error) { type response struct { @@ -173,7 +66,7 @@ func (p *Poloniex) GetTicker() (map[string]Ticker, error) { } resp := response{} - path := fmt.Sprintf("%s/public?command=returnTicker", p.APIUrl) + path := fmt.Sprintf("%s/public?command=returnTicker", p.API.Endpoints.URL) return resp.Data, p.SendHTTPRequest(path, &resp.Data) } @@ -181,7 +74,7 @@ func (p *Poloniex) GetTicker() (map[string]Ticker, error) { // GetVolume returns a list of currencies with associated volume func (p *Poloniex) GetVolume() (interface{}, error) { var resp interface{} - path := fmt.Sprintf("%s/public?command=return24hVolume", p.APIUrl) + path := fmt.Sprintf("%s/public?command=return24hVolume", p.API.Endpoints.URL) return resp, p.SendHTTPRequest(path, &resp) } @@ -198,7 +91,7 @@ func (p *Poloniex) GetOrderbook(currencyPair string, depth int) (OrderbookAll, e if currencyPair != "" { vals.Set("currencyPair", currencyPair) resp := OrderbookResponse{} - path := fmt.Sprintf("%s/public?command=returnOrderBook&%s", p.APIUrl, vals.Encode()) + path := fmt.Sprintf("%s/public?command=returnOrderBook&%s", p.API.Endpoints.URL, vals.Encode()) err := p.SendHTTPRequest(path, &resp) if err != nil { return oba, err @@ -230,31 +123,33 @@ func (p *Poloniex) GetOrderbook(currencyPair string, depth int) (OrderbookAll, e } else { vals.Set("currencyPair", "all") resp := OrderbookResponseAll{} - path := fmt.Sprintf("%s/public?command=returnOrderBook&%s", p.APIUrl, vals.Encode()) + path := fmt.Sprintf("%s/public?command=returnOrderBook&%s", p.API.Endpoints.URL, vals.Encode()) err := p.SendHTTPRequest(path, &resp.Data) if err != nil { return oba, err } for currency, orderbook := range resp.Data { - ob := Orderbook{} + var ob Orderbook for x := range orderbook.Asks { - data := orderbook.Asks[x] - price, err := strconv.ParseFloat(data[0].(string), 64) + price, err := strconv.ParseFloat(orderbook.Asks[x][0].(string), 64) if err != nil { return oba, err } - amount := data[1].(float64) - ob.Asks = append(ob.Asks, OrderbookItem{Price: price, Amount: amount}) + ob.Asks = append(ob.Asks, OrderbookItem{ + Price: price, + Amount: orderbook.Asks[x][1].(float64), + }) } for x := range orderbook.Bids { - data := orderbook.Bids[x] - price, err := strconv.ParseFloat(data[0].(string), 64) + price, err := strconv.ParseFloat(orderbook.Bids[x][0].(string), 64) if err != nil { return oba, err } - amount := data[1].(float64) - ob.Bids = append(ob.Bids, OrderbookItem{Price: price, Amount: amount}) + ob.Asks = append(ob.Asks, OrderbookItem{ + Price: price, + Amount: orderbook.Bids[x][1].(float64), + }) } oba.Data[currency] = Orderbook{Bids: ob.Bids, Asks: ob.Asks} } @@ -276,7 +171,7 @@ func (p *Poloniex) GetTradeHistory(currencyPair, start, end string) ([]TradeHist } var resp []TradeHistory - path := fmt.Sprintf("%s/public?command=returnTradeHistory&%s", p.APIUrl, vals.Encode()) + path := fmt.Sprintf("%s/public?command=returnTradeHistory&%s", p.API.Endpoints.URL, vals.Encode()) return resp, p.SendHTTPRequest(path, &resp) } @@ -299,7 +194,7 @@ func (p *Poloniex) GetChartData(currencyPair, start, end, period string) ([]Char } var resp []ChartData - path := fmt.Sprintf("%s/public?command=returnChartData&%s", p.APIUrl, vals.Encode()) + path := fmt.Sprintf("%s/public?command=returnChartData&%s", p.API.Endpoints.URL, vals.Encode()) err := p.SendHTTPRequest(path, &resp) if err != nil { @@ -315,32 +210,16 @@ func (p *Poloniex) GetCurrencies() (map[string]Currencies, error) { Data map[string]Currencies } resp := Response{} - path := fmt.Sprintf("%s/public?command=returnCurrencies", p.APIUrl) + path := fmt.Sprintf("%s/public?command=returnCurrencies", p.API.Endpoints.URL) return resp.Data, p.SendHTTPRequest(path, &resp.Data) } -// GetExchangeCurrencies returns a list of currencies using the GetTicker API -// as the GetExchangeCurrencies information doesn't return currency pair information -func (p *Poloniex) GetExchangeCurrencies() ([]string, error) { - response, err := p.GetTicker() - if err != nil { - return nil, err - } - - var currencies []string - for x := range response { - currencies = append(currencies, x) - } - - return currencies, nil -} - // GetLoanOrders returns the list of loan offers and demands for a given // currency, specified by the "currency" GET parameter. func (p *Poloniex) GetLoanOrders(currency string) (LoanOrders, error) { resp := LoanOrders{} - path := fmt.Sprintf("%s/public?command=returnLoanOrders¤cy=%s", p.APIUrl, currency) + path := fmt.Sprintf("%s/public?command=returnLoanOrders¤cy=%s", p.API.Endpoints.URL, currency) return resp, p.SendHTTPRequest(path, &resp) } @@ -534,9 +413,9 @@ func (p *Poloniex) PlaceOrder(currency string, rate, amount float64, immediate, var orderType string if buy { - orderType = poloniexOrderBuy + orderType = order.Buy.Lower() } else { - orderType = poloniexOrderSell + orderType = order.Sell.Lower() } values.Set("currencyPair", currency) @@ -888,24 +767,24 @@ func (p *Poloniex) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated HTTP request func (p *Poloniex) SendAuthenticatedHTTPRequest(method, endpoint string, values url.Values, result interface{}) error { - if !p.AuthenticatedAPISupport { + if !p.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, p.Name) } headers := make(map[string]string) headers["Content-Type"] = "application/x-www-form-urlencoded" - headers["Key"] = p.APIKey + headers["Key"] = p.API.Credentials.Key values.Set("nonce", p.Requester.GetNonce(true).String()) values.Set("command", endpoint) - hmac := common.GetHMAC(common.HashSHA512, + hmac := crypto.GetHMAC(crypto.HashSHA512, []byte(values.Encode()), - []byte(p.APISecret)) + []byte(p.API.Credentials.Secret)) - headers["Sign"] = common.HexEncodeToString(hmac) + headers["Sign"] = crypto.HexEncodeToString(hmac) - path := fmt.Sprintf("%s/%s", p.APIUrl, poloniexAPITradingEndpoint) + path := fmt.Sprintf("%s/%s", p.API.Endpoints.URL, poloniexAPITradingEndpoint) return p.SendPayload(method, path, diff --git a/exchanges/poloniex/poloniex_live_test.go b/exchanges/poloniex/poloniex_live_test.go index b99df606..f2ad34f2 100644 --- a/exchanges/poloniex/poloniex_live_test.go +++ b/exchanges/poloniex/poloniex_live_test.go @@ -17,16 +17,22 @@ var mockTests = false func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Poloniex load config error", err) + } poloniexConfig, err := cfg.GetExchangeConfig("Poloniex") if err != nil { - log.Fatal("Test Failed - Poloniex Setup() init error", err) + log.Fatal("Poloniex Setup() init error", err) } - poloniexConfig.AuthenticatedAPISupport = true - poloniexConfig.APIKey = apiKey - poloniexConfig.APISecret = apiSecret + poloniexConfig.API.AuthenticatedSupport = true + poloniexConfig.API.Credentials.Key = apiKey + poloniexConfig.API.Credentials.Secret = apiSecret p.SetDefaults() - p.Setup(&poloniexConfig) - log.Printf(sharedtestvalues.LiveTesting, p.GetName(), p.APIUrl) + err = p.Setup(poloniexConfig) + if err != nil { + log.Fatal("Poloniex setup error", err) + } + log.Printf(sharedtestvalues.LiveTesting, p.Name, p.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/poloniex/poloniex_mock_test.go b/exchanges/poloniex/poloniex_mock_test.go index b5395af8..546ee7d3 100644 --- a/exchanges/poloniex/poloniex_mock_test.go +++ b/exchanges/poloniex/poloniex_mock_test.go @@ -20,25 +20,32 @@ var mockTests = true func TestMain(m *testing.M) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Poloniex load config error", err) + } poloniexConfig, err := cfg.GetExchangeConfig("Poloniex") if err != nil { - log.Fatal("Test Failed - Poloniex Setup() init error", err) + log.Fatal("Poloniex Setup() init error", err) } - poloniexConfig.AuthenticatedAPISupport = true - poloniexConfig.APIKey = apiKey - poloniexConfig.APISecret = apiSecret + p.SkipAuthCheck = true + poloniexConfig.API.AuthenticatedSupport = true + poloniexConfig.API.Credentials.Key = apiKey + poloniexConfig.API.Credentials.Secret = apiSecret p.SetDefaults() - p.Setup(&poloniexConfig) + err = p.Setup(poloniexConfig) + if err != nil { + log.Fatal("Poloniex setup error", err) + } serverDetails, newClient, err := mock.NewVCRServer(mockfile) if err != nil { - log.Fatalf("Test Failed - Mock server error %s", err) + log.Fatalf("Mock server error %s", err) } p.HTTPClient = newClient - p.APIUrl = serverDetails + p.API.Endpoints.URL = serverDetails - log.Printf(sharedtestvalues.MockTesting, p.GetName(), p.APIUrl) + log.Printf(sharedtestvalues.MockTesting, p.Name, p.API.Endpoints.URL) os.Exit(m.Run()) } diff --git a/exchanges/poloniex/poloniex_test.go b/exchanges/poloniex/poloniex_test.go index 0e369175..cc939aca 100644 --- a/exchanges/poloniex/poloniex_test.go +++ b/exchanges/poloniex/poloniex_test.go @@ -1,6 +1,7 @@ package poloniex import ( + "encoding/json" "net/http" "testing" "time" @@ -9,6 +10,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -23,18 +25,14 @@ const ( var p Poloniex func areTestAPIKeysSet() bool { - if p.APIKey != "" && p.APIKey != "Key" && - p.APISecret != "" && p.APISecret != "Secret" { - return true - } - return false + return p.ValidateAPICredentials() } func TestGetTicker(t *testing.T) { t.Parallel() _, err := p.GetTicker() if err != nil { - t.Error("Test Failed - Poloniex GetTicker() error", err) + t.Error("Poloniex GetTicker() error", err) } } @@ -105,7 +103,7 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() p.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, @@ -128,7 +126,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee Basic if resp, err := p.GetFee(feeBuilder); resp != float64(0.0025) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0025), resp) } @@ -137,7 +135,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := p.GetFee(feeBuilder); resp != float64(2500) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2500), resp) t.Error(err) } @@ -146,7 +144,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := p.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -155,7 +153,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := p.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) t.Error(err) } @@ -165,7 +163,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := p.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -174,7 +172,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := p.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -183,7 +181,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := p.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -193,7 +191,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := p.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -204,9 +202,7 @@ func TestFormatWithdrawPermissions(t *testing.T) { expectedResult := exchange.AutoWithdrawCryptoWithAPIPermissionText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := p.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, @@ -216,25 +212,25 @@ func TestFormatWithdrawPermissions(t *testing.T) { func TestGetActiveOrders(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := p.GetActiveOrders(&getOrdersRequest) switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - GetActiveOrders() error", err) + t.Error("GetActiveOrders() error", err) case !areTestAPIKeysSet() && !mockTests && err == nil: - t.Error("Test Failed - Expecting an error when no keys are set") + t.Error("Expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock GetActiveOrders() err", err) + t.Error("Mock GetActiveOrders() err", err) } } func TestGetOrderHistory(t *testing.T) { t.Parallel() - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, } _, err := p.GetOrderHistory(&getOrdersRequest) @@ -257,25 +253,27 @@ func TestSubmitOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var pair = currency.Pair{ - Delimiter: "_", - Base: currency.BTC, - Quote: currency.LTC, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: delimiterUnderscore, + Base: currency.BTC, + Quote: currency.LTC, + }, + OrderSide: order.Buy, + OrderType: order.Market, + Price: 10, + Amount: 10000000, + ClientID: "hi", } - response, err := p.SubmitOrder(pair, - exchange.BuyOrderSide, - exchange.MarketOrderType, - 1, - 10, - "hi") + response, err := p.SubmitOrder(orderSubmission) switch { case areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced): t.Errorf("Order failed to be placed: %v", err) case !areTestAPIKeysSet() && !mockTests && err == nil: t.Error("Expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock SubmitOrder() err", err) + t.Error("Mock SubmitOrder() err", err) } } @@ -284,8 +282,7 @@ func TestCancelExchangeOrder(t *testing.T) { if areTestAPIKeysSet() && !canManipulateRealOrders && !mockTests { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -299,7 +296,7 @@ func TestCancelExchangeOrder(t *testing.T) { case areTestAPIKeysSet() && err != nil: t.Errorf("Could not cancel orders: %v", err) case mockTests && err != nil: - t.Error("Test Failed - Mock CancelExchangeOrder() err", err) + t.Error("Mock CancelExchangeOrder() err", err) } } @@ -310,8 +307,7 @@ func TestCancelAllExchangeOrders(t *testing.T) { } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -325,10 +321,10 @@ func TestCancelAllExchangeOrders(t *testing.T) { case areTestAPIKeysSet() && err != nil: t.Errorf("Could not cancel orders: %v", err) case mockTests && err != nil: - t.Error("Test Failed - Mock CancelAllExchangeOrders() err", err) + t.Error("Mock CancelAllExchangeOrders() err", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } @@ -338,24 +334,26 @@ func TestModifyOrder(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - _, err := p.ModifyOrder(&exchange.ModifyOrder{OrderID: "1337", Price: 1337}) + _, err := p.ModifyOrder(&order.Modify{OrderID: "1337", Price: 1337}) switch { case areTestAPIKeysSet() && err != nil && mockTests: - t.Error("Test Failed - ModifyOrder() error", err) + t.Error("ModifyOrder() error", err) case !areTestAPIKeysSet() && !mockTests && err == nil: - t.Error("Test Failed - ModifyOrder() error cannot be nil") + t.Error("ModifyOrder() error cannot be nil") case mockTests && err != nil: - t.Error("Test Failed - Mock ModifyOrder() err", err) + t.Error("Mock ModifyOrder() err", err) } } func TestWithdraw(t *testing.T) { t.Parallel() - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 0, - Currency: currency.LTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: 0, + Currency: currency.LTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders && !mockTests { @@ -369,7 +367,7 @@ func TestWithdraw(t *testing.T) { case !areTestAPIKeysSet() && !mockTests && err == nil: t.Error("Expecting an error when no keys are set") case mockTests && err != nil: - t.Error("Test Failed - Mock Withdraw() err", err) + t.Error("Mock Withdraw() err", err) } } @@ -379,7 +377,7 @@ func TestWithdrawFiat(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest exchange.WithdrawRequest + var withdrawFiatRequest exchange.FiatWithdrawRequest _, err := p.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", @@ -393,7 +391,7 @@ func TestWithdrawInternationalBank(t *testing.T) { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest exchange.WithdrawRequest + var withdrawFiatRequest exchange.FiatWithdrawRequest _, err := p.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", @@ -406,11 +404,11 @@ func TestGetDepositAddress(t *testing.T) { _, err := p.GetDepositAddress(currency.DASH, "") switch { case areTestAPIKeysSet() && err != nil: - t.Error("Test Failed - GetDepositAddress()", err) + t.Error("GetDepositAddress()", err) case !areTestAPIKeysSet() && !mockTests && err == nil: - t.Error("Test Failed - GetDepositAddress() cannot be nil") + t.Error("GetDepositAddress() cannot be nil") case mockTests && err != nil: - t.Error("Test Failed - Mock GetDepositAddress() err", err) + t.Error("Mock GetDepositAddress() err", err) } } @@ -424,7 +422,7 @@ func TestWsHandleAccountData(t *testing.T) { } for i := range jsons { var result [][]interface{} - err := common.JSONDecode([]byte(jsons[i]), &result) + err := json.Unmarshal([]byte(jsons[i]), &result) if err != nil { t.Error(err) } @@ -436,7 +434,7 @@ func TestWsHandleAccountData(t *testing.T) { // Will receive a message only on failure func TestWsAuth(t *testing.T) { t.Parallel() - if !p.Websocket.IsEnabled() && !p.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + if !p.Websocket.IsEnabled() && !p.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() { t.Skip(wshandler.WebsocketNotEnabled) } p.WebsocketConn = &wshandler.WebsocketConnection{ diff --git a/exchanges/poloniex/poloniex_types.go b/exchanges/poloniex/poloniex_types.go index d664f880..e296ef16 100644 --- a/exchanges/poloniex/poloniex_types.go +++ b/exchanges/poloniex/poloniex_types.go @@ -8,6 +8,7 @@ import ( // Ticker holds ticker data type Ticker struct { + ID int `json:"id"` Last float64 `json:"last,string"` LowestAsk float64 `json:"lowestAsk,string"` HighestBid float64 `json:"highestBid,string"` diff --git a/exchanges/poloniex/poloniex_websocket.go b/exchanges/poloniex/poloniex_websocket.go index 04739489..18b91525 100644 --- a/exchanges/poloniex/poloniex_websocket.go +++ b/exchanges/poloniex/poloniex_websocket.go @@ -1,6 +1,7 @@ package poloniex import ( + "encoding/json" "errors" "fmt" "net/http" @@ -9,9 +10,11 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" @@ -24,11 +27,12 @@ const ( wsTickerDataID = 1002 ws24HourExchangeVolumeID = 1003 wsHeartbeat = 1010 + delimiterUnderscore = "_" ) var ( - // CurrencyIDMap stores a map of currencies associated with their ID - CurrencyIDMap map[string]int + // currencyIDMap stores a map of currencies associated with their ID + currencyIDMap map[int]string ) // WsConnect initiates a websocket connection @@ -42,15 +46,15 @@ func (p *Poloniex) WsConnect() error { return err } - if CurrencyIDMap == nil { - CurrencyIDMap = make(map[string]int) - resp, err := p.GetCurrencies() + if currencyIDMap == nil { + currencyIDMap = make(map[int]string) + resp, err := p.GetTicker() if err != nil { return err } for k, v := range resp { - CurrencyIDMap[k] = v.ID + currencyIDMap[v.ID] = k } } @@ -86,12 +90,12 @@ func (p *Poloniex) WsHandleData() { default: resp, err := p.WebsocketConn.ReadMessage() if err != nil { - p.Websocket.DataHandler <- err + p.Websocket.ReadMessageErrors <- err return } p.Websocket.TrafficAlert <- struct{}{} var result interface{} - err = common.JSONDecode(resp.Raw, &result) + err = json.Unmarshal(resp.Raw, &result) if err != nil { p.Websocket.DataHandler <- err continue @@ -105,10 +109,15 @@ func (p *Poloniex) WsHandleData() { if len(data) == 2 && chanID != wsHeartbeat { if checkSubscriptionSuccess(data) { if p.Verbose { - log.Debugf("poloniex websocket subscribed to channel successfully. %d", chanID) + log.Debugf(log.ExchangeSys, + "%s websocket subscribed to channel successfully. %d", + p.Name, + chanID) } } else { - p.Websocket.DataHandler <- fmt.Errorf("poloniex websocket subscription to channel failed. %d", chanID) + p.Websocket.DataHandler <- fmt.Errorf("%s websocket subscription to channel failed. %d", + p.Name, + chanID) } continue } @@ -133,53 +142,66 @@ func (p *Poloniex) WsHandleData() { dataL3map := dataL3[1].(map[string]interface{}) currencyPair, ok := dataL3map["currencyPair"].(string) if !ok { - p.Websocket.DataHandler <- errors.New("poloniex.go error - could not find currency pair in map") + p.Websocket.DataHandler <- fmt.Errorf("%s websocket could not find currency pair in map", + p.Name) continue } orderbookData, ok := dataL3map["orderBook"].([]interface{}) if !ok { - p.Websocket.DataHandler <- errors.New("poloniex.go error - could not find orderbook data in map") + p.Websocket.DataHandler <- fmt.Errorf("%s websocket could not find orderbook data in map", + p.Name) continue } - err := p.WsProcessOrderbookSnapshot(orderbookData, currencyPair) + err = p.WsProcessOrderbookSnapshot(orderbookData, + currencyPair) if err != nil { p.Websocket.DataHandler <- err continue } p.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Exchange: p.GetName(), - Asset: orderbook.Spot, + Exchange: p.Name, + Asset: asset.Spot, Pair: currency.NewPairFromString(currencyPair), } case "o": - currencyPair := CurrencyPairID[chanID] - err := p.WsProcessOrderbookUpdate(int64(data[1].(float64)), dataL3, currencyPair) + currencyPair := currencyIDMap[chanID] + err = p.WsProcessOrderbookUpdate(int64(data[1].(float64)), + dataL3, + currencyPair) if err != nil { p.Websocket.DataHandler <- err continue } p.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ - Exchange: p.GetName(), - Asset: orderbook.Spot, + Exchange: p.Name, + Asset: asset.Spot, Pair: currency.NewPairFromString(currencyPair), } case "t": - currencyPair := CurrencyPairID[chanID] + currencyPair := currencyIDMap[chanID] var trade WsTrade - trade.Symbol = CurrencyPairID[chanID] + trade.Symbol = currencyIDMap[chanID] trade.TradeID, _ = strconv.ParseInt(dataL3[1].(string), 10, 64) // 1 for buy 0 for sell - side := "buy" + side := order.Buy if dataL3[2].(float64) != 1 { - side = "sell" + side = order.Sell + } + trade.Side = side.Lower() + trade.Volume, err = strconv.ParseFloat(dataL3[3].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + continue + } + trade.Price, err = strconv.ParseFloat(dataL3[4].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + continue } - trade.Side = side - trade.Volume, _ = strconv.ParseFloat(dataL3[3].(string), 64) - trade.Price, _ = strconv.ParseFloat(dataL3[4].(string), 64) trade.Timestamp = int64(dataL3[5].(float64)) p.Websocket.DataHandler <- wshandler.TradeData{ @@ -201,26 +223,73 @@ func (p *Poloniex) WsHandleData() { func (p *Poloniex) wsHandleTickerData(data []interface{}) { tickerData := data[2].([]interface{}) var t WsTicker - t.LastPrice, _ = strconv.ParseFloat(tickerData[1].(string), 64) - t.LowestAsk, _ = strconv.ParseFloat(tickerData[2].(string), 64) - t.HighestBid, _ = strconv.ParseFloat(tickerData[3].(string), 64) - t.PercentageChange, _ = strconv.ParseFloat(tickerData[4].(string), 64) - t.BaseCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[5].(string), 64) - t.QuoteCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[6].(string), 64) - isFrozen := false - if tickerData[7].(float64) == 1 { - isFrozen = true + currencyPair := currency.NewPairDelimiter(currencyIDMap[int(tickerData[0].(float64))], delimiterUnderscore) + if !p.GetEnabledPairs(asset.Spot).Contains(currencyPair, true) { + return + } + + var err error + t.LastPrice, err = strconv.ParseFloat(tickerData[1].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.LowestAsk, err = strconv.ParseFloat(tickerData[2].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.HighestBid, err = strconv.ParseFloat(tickerData[3].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.PercentageChange, err = strconv.ParseFloat(tickerData[4].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.BaseCurrencyVolume24H, err = strconv.ParseFloat(tickerData[5].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.QuoteCurrencyVolume24H, err = strconv.ParseFloat(tickerData[6].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.IsFrozen = tickerData[7].(float64) == 1 + t.HighestTradeIn24H, err = strconv.ParseFloat(tickerData[8].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + t.LowestTradePrice24H, err = strconv.ParseFloat(tickerData[9].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return } - t.IsFrozen = isFrozen - t.HighestTradeIn24H, _ = strconv.ParseFloat(tickerData[8].(string), 64) - t.LowestTradePrice24H, _ = strconv.ParseFloat(tickerData[9].(string), 64) p.Websocket.DataHandler <- wshandler.TickerData{ - Timestamp: time.Now(), - Exchange: p.GetName(), - AssetType: orderbook.Spot, - LowPrice: t.LowestAsk, - HighPrice: t.HighestBid, + Exchange: p.Name, + Volume: t.BaseCurrencyVolume24H, + QuoteVolume: t.QuoteCurrencyVolume24H, + High: t.HighestBid, + Low: t.LowestAsk, + Bid: t.HighestBid, + Ask: t.LowestAsk, + Last: t.LastPrice, + Timestamp: time.Now(), + AssetType: asset.Spot, + Pair: currencyPair, } } @@ -229,7 +298,12 @@ func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) { for i := range accountData { switch accountData[i][0].(string) { case "b": - amount, _ := strconv.ParseFloat(accountData[i][3].(string), 64) + amount, err := strconv.ParseFloat(accountData[i][3].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + response := WsAccountBalanceUpdateResponse{ currencyID: accountData[i][1].(float64), wallet: accountData[i][2].(string), @@ -237,9 +311,23 @@ func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) { } p.Websocket.DataHandler <- response case "n": - timeParse, _ := time.Parse("2006-01-02 15:04:05", accountData[i][6].(string)) - rate, _ := strconv.ParseFloat(accountData[i][4].(string), 64) - amount, _ := strconv.ParseFloat(accountData[i][5].(string), 64) + timeParse, err := time.Parse("2006-01-02 15:04:05", accountData[i][6].(string)) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + rate, err := strconv.ParseFloat(accountData[i][4].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + amount, err := strconv.ParseFloat(accountData[i][5].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } response := WsNewLimitOrderResponse{ currencyID: accountData[i][1].(float64), @@ -257,11 +345,35 @@ func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) { } p.Websocket.DataHandler <- response case "t": - timeParse, _ := time.Parse("2006-01-02 15:04:05", accountData[i][8].(string)) - rate, _ := strconv.ParseFloat(accountData[i][2].(string), 64) - amount, _ := strconv.ParseFloat(accountData[i][3].(string), 64) - feeMultiplier, _ := strconv.ParseFloat(accountData[i][4].(string), 64) - totalFee, _ := strconv.ParseFloat(accountData[i][7].(string), 64) + timeParse, err := time.Parse("2006-01-02 15:04:05", accountData[i][8].(string)) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + rate, err := strconv.ParseFloat(accountData[i][2].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + amount, err := strconv.ParseFloat(accountData[i][3].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + feeMultiplier, err := strconv.ParseFloat(accountData[i][4].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } + + totalFee, err := strconv.ParseFloat(accountData[i][7].(string), 64) + if err != nil { + p.Websocket.DataHandler <- err + return + } response := WsTradeNotificationResponse{ TradeID: accountData[i][1].(float64), @@ -322,15 +434,15 @@ func (p *Poloniex) WsProcessOrderbookSnapshot(ob []interface{}, symbol string) e var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids - newOrderBook.AssetType = orderbook.Spot + newOrderBook.AssetType = asset.Spot newOrderBook.Pair = currency.NewPairFromString(symbol) + newOrderBook.ExchangeName = p.Name - return p.Websocket.Orderbook.LoadSnapshot(&newOrderBook, false) + return p.Websocket.Orderbook.LoadSnapshot(&newOrderBook) } // WsProcessOrderbookUpdate processes new orderbook updates func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber int64, target []interface{}, symbol string) error { - sideCheck := target[1].(float64) cP := currency.NewPairFromString(symbol) price, err := strconv.ParseFloat(target[2].(string), 64) if err != nil { @@ -341,11 +453,11 @@ func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber int64, target []inter return err } update := &wsorderbook.WebsocketOrderbookUpdate{ - CurrencyPair: cP, - AssetType: orderbook.Spot, - UpdateID: sequenceNumber, + Pair: cP, + Asset: asset.Spot, + UpdateID: sequenceNumber, } - if sideCheck == 0 { + if target[1].(float64) == 1 { update.Bids = []orderbook.Item{{Price: price, Amount: volume}} } else { update.Asks = []orderbook.Item{{Price: price, Amount: volume}} @@ -353,132 +465,22 @@ func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber int64, target []inter return p.Websocket.Orderbook.Update(update) } -// CurrencyPairID contains a list of IDS for currency pairs. -var CurrencyPairID = map[int]string{ - 7: "BTC_BCN", - 14: "BTC_BTS", - 15: "BTC_BURST", - 20: "BTC_CLAM", - 25: "BTC_DGB", - 27: "BTC_DOGE", - 24: "BTC_DASH", - 38: "BTC_GAME", - 43: "BTC_HUC", - 50: "BTC_LTC", - 51: "BTC_MAID", - 58: "BTC_OMNI", - 61: "BTC_NAV", - 64: "BTC_NMC", - 69: "BTC_NXT", - 75: "BTC_PPC", - 89: "BTC_STR", - 92: "BTC_SYS", - 97: "BTC_VIA", - 100: "BTC_VTC", - 108: "BTC_XCP", - 114: "BTC_XMR", - 116: "BTC_XPM", - 117: "BTC_XRP", - 112: "BTC_XEM", - 148: "BTC_ETH", - 150: "BTC_SC", - 153: "BTC_EXP", - 155: "BTC_FCT", - 160: "BTC_AMP", - 162: "BTC_DCR", - 163: "BTC_LSK", - 167: "BTC_LBC", - 168: "BTC_STEEM", - 170: "BTC_SBD", - 171: "BTC_ETC", - 174: "BTC_REP", - 177: "BTC_ARDR", - 178: "BTC_ZEC", - 182: "BTC_STRAT", // nolint: misspell - 184: "BTC_PASC", - 185: "BTC_GNT", - 187: "BTC_GNO", - 189: "BTC_BCH", - 192: "BTC_ZRX", - 194: "BTC_CVC", - 196: "BTC_OMG", - 198: "BTC_GAS", - 200: "BTC_STORJ", - 201: "BTC_EOS", - 204: "BTC_SNT", - 207: "BTC_KNC", - 210: "BTC_BAT", - 213: "BTC_LOOM", - 221: "BTC_QTUM", - 121: "USDT_BTC", - 216: "USDT_DOGE", - 122: "USDT_DASH", - 123: "USDT_LTC", - 124: "USDT_NXT", - 125: "USDT_STR", - 126: "USDT_XMR", - 127: "USDT_XRP", - 149: "USDT_ETH", - 219: "USDT_SC", - 218: "USDT_LSK", - 173: "USDT_ETC", - 175: "USDT_REP", - 180: "USDT_ZEC", - 217: "USDT_GNT", - 191: "USDT_BCH", - 220: "USDT_ZRX", - 203: "USDT_EOS", - 206: "USDT_SNT", - 209: "USDT_KNC", - 212: "USDT_BAT", - 215: "USDT_LOOM", - 223: "USDT_QTUM", - 129: "XMR_BCN", - 132: "XMR_DASH", - 137: "XMR_LTC", - 138: "XMR_MAID", - 140: "XMR_NXT", - 181: "XMR_ZEC", - 166: "ETH_LSK", - 169: "ETH_STEEM", - 172: "ETH_ETC", - 176: "ETH_REP", - 179: "ETH_ZEC", - 186: "ETH_GNT", - 188: "ETH_GNO", - 190: "ETH_BCH", - 193: "ETH_ZRX", - 195: "ETH_CVC", - 197: "ETH_OMG", - 199: "ETH_GAS", - 202: "ETH_EOS", - 205: "ETH_SNT", - 208: "ETH_KNC", - 211: "ETH_BAT", - 214: "ETH_LOOM", - 222: "ETH_QTUM", - 224: "USDC_BTC", - 226: "USDC_USDT", - 225: "USDC_ETH", -} - // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (p *Poloniex) GenerateDefaultSubscriptions() { var subscriptions []wshandler.WebsocketChannelSubscription - // Tickerdata is its own channel subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: fmt.Sprintf("%v", wsTickerDataID), + Channel: strconv.FormatInt(wsTickerDataID, 10), }) if p.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: fmt.Sprintf("%v", wsAccountNotificationID), + Channel: strconv.FormatInt(wsAccountNotificationID, 10), }) } - enabledCurrencies := p.GetEnabledCurrencies() + enabledCurrencies := p.GetEnabledPairs(asset.Spot) for j := range enabledCurrencies { - enabledCurrencies[j].Delimiter = "_" + enabledCurrencies[j].Delimiter = delimiterUnderscore subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: "orderbook", Currency: enabledCurrencies[j], @@ -493,9 +495,9 @@ func (p *Poloniex) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscr Command: "subscribe", } switch { - case strings.EqualFold(fmt.Sprintf("%v", wsAccountNotificationID), channelToSubscribe.Channel): + case strings.EqualFold(strconv.FormatInt(wsAccountNotificationID, 10), channelToSubscribe.Channel): return p.wsSendAuthorisedCommand("subscribe") - case strings.EqualFold(fmt.Sprintf("%v", wsTickerDataID), channelToSubscribe.Channel): + case strings.EqualFold(strconv.FormatInt(wsTickerDataID, 10), channelToSubscribe.Channel): subscriptionRequest.Channel = wsTickerDataID default: subscriptionRequest.Channel = channelToSubscribe.Currency.String() @@ -509,9 +511,9 @@ func (p *Poloniex) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubs Command: "unsubscribe", } switch { - case strings.EqualFold(fmt.Sprintf("%v", wsAccountNotificationID), channelToSubscribe.Channel): + case strings.EqualFold(strconv.FormatInt(wsAccountNotificationID, 10), channelToSubscribe.Channel): return p.wsSendAuthorisedCommand("unsubscribe") - case strings.EqualFold(fmt.Sprintf("%v", wsTickerDataID), channelToSubscribe.Channel): + case strings.EqualFold(strconv.FormatInt(wsTickerDataID, 10), channelToSubscribe.Channel): unsubscriptionRequest.Channel = wsTickerDataID default: unsubscriptionRequest.Channel = channelToSubscribe.Currency.String() @@ -521,12 +523,12 @@ func (p *Poloniex) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubs func (p *Poloniex) wsSendAuthorisedCommand(command string) error { nonce := fmt.Sprintf("nonce=%v", time.Now().UnixNano()) - hmac := common.GetHMAC(common.HashSHA512, []byte(nonce), []byte(p.APISecret)) + hmac := crypto.GetHMAC(crypto.HashSHA512, []byte(nonce), []byte(p.API.Credentials.Secret)) request := WsAuthorisationRequest{ Command: command, Channel: 1000, - Sign: common.HexEncodeToString(hmac), - Key: p.APIKey, + Sign: crypto.HexEncodeToString(hmac), + Key: p.API.Credentials.Key, Payload: nonce, } return p.WebsocketConn.SendMessage(request) diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index f88061f2..496e1e63 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -8,14 +8,169 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (p *Poloniex) GetDefaultConfig() (*config.ExchangeConfig, error) { + p.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = p.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = p.BaseCurrencies + + err := p.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if p.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = p.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets default settings for poloniex +func (p *Poloniex) SetDefaults() { + p.Name = "Poloniex" + p.Enabled = true + p.Verbose = true + p.API.CredentialsValidator.RequiresKey = true + p.API.CredentialsValidator.RequiresSecret = true + + p.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: delimiterUnderscore, + Uppercase: true, + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: delimiterUnderscore, + Uppercase: true, + }, + } + + p.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + KlineFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + CancelOrders: true, + SubmitOrder: true, + DepositHistory: true, + WithdrawalHistory: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + p.Requester = request.New(p.Name, + request.NewRateLimit(time.Second, poloniexAuthRate), + request.NewRateLimit(time.Second, poloniexUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + p.API.Endpoints.URLDefault = poloniexAPIURL + p.API.Endpoints.URL = p.API.Endpoints.URLDefault + p.API.Endpoints.WebsocketURL = poloniexWebsocketAddress + p.Websocket = wshandler.New() + p.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + p.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + p.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup sets user exchange configuration settings +func (p *Poloniex) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + p.SetEnabled(false) + return nil + } + + err := p.SetupDefaults(exch) + if err != nil { + return err + } + + err = p.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: poloniexWebsocketAddress, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: p.WsConnect, + Subscriber: p.Subscribe, + UnSubscriber: p.Unsubscribe, + Features: &p.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + p.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: p.Name, + URL: p.Websocket.GetWebsocketURL(), + ProxyURL: p.Websocket.GetProxyAddress(), + Verbose: p.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + p.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + false, + true, + true, + false, + exch.Name) + return nil +} + // Start starts the Poloniex go routine func (p *Poloniex) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -28,74 +183,97 @@ func (p *Poloniex) Start(wg *sync.WaitGroup) { // Run implements the Poloniex wrapper func (p *Poloniex) Run() { if p.Verbose { - log.Debugf("%s Websocket: %s (url: %s).\n", p.GetName(), common.IsEnabled(p.Websocket.IsEnabled()), poloniexWebsocketAddress) - log.Debugf("%s polling delay: %ds.\n", p.GetName(), p.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", p.GetName(), len(p.EnabledPairs), p.EnabledPairs) + log.Debugf(log.ExchangeSys, "%s Websocket: %s (url: %s).\n", p.Name, common.IsEnabled(p.Websocket.IsEnabled()), poloniexWebsocketAddress) + p.PrintEnabledPairs() } - exchangeCurrencies, err := p.GetExchangeCurrencies() + forceUpdate := false + if common.StringDataCompare(p.GetAvailablePairs(asset.Spot).Strings(), "BTC_USDT") { + log.Warnf(log.ExchangeSys, "%s contains invalid pair, forcing upgrade of available currencies.\n", + p.Name) + forceUpdate = true + } + + if !p.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { + return + } + + err := p.UpdateTradablePairs(forceUpdate) if err != nil { - log.Errorf("%s Failed to get available symbols.\n", p.GetName()) - } else { - forceUpdate := false - if common.StringDataCompare(p.AvailablePairs.Strings(), "BTC_USDT") { - log.Warnf("%s contains invalid pair, forcing upgrade of available currencies.\n", - p.GetName()) - forceUpdate = true - } - - var newExchangeCurrencies currency.Pairs - for _, p := range exchangeCurrencies { - newExchangeCurrencies = append(newExchangeCurrencies, - currency.NewPairFromString(p)) - } - - err = p.UpdateCurrencies(newExchangeCurrencies, false, forceUpdate) - if err != nil { - log.Errorf("%s Failed to update available currencies %s.\n", p.GetName(), err) - } + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", p.Name, err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (p *Poloniex) FetchTradablePairs(asset asset.Item) ([]string, error) { + resp, err := p.GetTicker() + if err != nil { + return nil, err + } + + var currencies []string + for x := range resp { + currencies = append(currencies, x) + } + + return currencies, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (p *Poloniex) UpdateTradablePairs(forceUpgrade bool) error { + pairs, err := p.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return p.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpgrade) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (p *Poloniex) UpdateTicker(currencyPair currency.Pair, assetType string) (ticker.Price, error) { +func (p *Poloniex) UpdateTicker(currencyPair currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price tick, err := p.GetTicker() if err != nil { return tickerPrice, err } - for _, x := range p.GetEnabledCurrencies() { + enabledPairs := p.GetEnabledPairs(assetType) + for i := range enabledPairs { var tp ticker.Price - curr := exchange.FormatExchangeCurrency(p.GetName(), x).String() - tp.Pair = x + curr := p.FormatExchangeCurrency(enabledPairs[i], assetType).String() + if _, ok := tick[curr]; !ok { + continue + } + tp.Pair = enabledPairs[i] tp.Ask = tick[curr].LowestAsk tp.Bid = tick[curr].HighestBid tp.High = tick[curr].High24Hr tp.Last = tick[curr].Last tp.Low = tick[curr].Low24Hr tp.Volume = tick[curr].BaseVolume + tp.QuoteVolume = tick[curr].QuoteVolume - err = ticker.ProcessTicker(p.GetName(), &tp, assetType) + err = ticker.ProcessTicker(p.Name, &tp, assetType) if err != nil { - return tickerPrice, err + log.Error(log.Ticker, err) } } return ticker.GetTicker(p.Name, currencyPair, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (p *Poloniex) GetTickerPrice(currencyPair currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(p.GetName(), currencyPair, assetType) +// FetchTicker returns the ticker for a currency pair +func (p *Poloniex) FetchTicker(currencyPair currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(p.Name, currencyPair, assetType) if err != nil { return p.UpdateTicker(currencyPair, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (p *Poloniex) GetOrderbookEx(currencyPair currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(p.GetName(), currencyPair, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (p *Poloniex) FetchOrderbook(currencyPair currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(p.Name, currencyPair, assetType) if err != nil { return p.UpdateOrderbook(currencyPair, assetType) } @@ -103,38 +281,35 @@ func (p *Poloniex) GetOrderbookEx(currencyPair currency.Pair, assetType string) } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (p *Poloniex) UpdateOrderbook(currencyPair currency.Pair, assetType string) (orderbook.Base, error) { +func (p *Poloniex) UpdateOrderbook(currencyPair currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base orderbookNew, err := p.GetOrderbook("", 1000) if err != nil { return orderBook, err } - for _, x := range p.GetEnabledCurrencies() { - currency := exchange.FormatExchangeCurrency(p.Name, x).String() - data, ok := orderbookNew.Data[currency] + enabledPairs := p.GetEnabledPairs(assetType) + for i := range enabledPairs { + data, ok := orderbookNew.Data[p.FormatExchangeCurrency(enabledPairs[i], assetType).String()] if !ok { continue } var obItems []orderbook.Item for y := range data.Bids { - obData := data.Bids[y] - obItems = append(obItems, - orderbook.Item{Amount: obData.Amount, Price: obData.Price}) + obItems = append(obItems, orderbook.Item{ + Amount: data.Bids[y].Amount, Price: data.Bids[y].Price}) } - orderBook.Bids = obItems + obItems = []orderbook.Item{} for y := range data.Asks { - obData := data.Asks[y] - obItems = append(obItems, - orderbook.Item{Amount: obData.Amount, Price: obData.Price}) + obItems = append(obItems, orderbook.Item{ + Amount: data.Asks[y].Amount, Price: data.Asks[y].Price}) } - - orderBook.Pair = x orderBook.Asks = obItems - orderBook.ExchangeName = p.GetName() + orderBook.Pair = enabledPairs[i] + orderBook.ExchangeName = p.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -149,7 +324,7 @@ func (p *Poloniex) UpdateOrderbook(currencyPair currency.Pair, assetType string) // Poloniex exchange func (p *Poloniex) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = p.GetName() + response.Exchange = p.Name accountBalance, err := p.GetBalances() if err != nil { return response, err @@ -173,44 +348,46 @@ func (p *Poloniex) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (p *Poloniex) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (p *Poloniex) GetExchangeHistory(currencyPair currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (p *Poloniex) GetExchangeHistory(currencyPair currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (p *Poloniex) SubmitOrder(currencyPair currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - fillOrKill := orderType == exchange.MarketOrderType - isBuyOrder := side == exchange.BuyOrderSide +func (p *Poloniex) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } - response, err := p.PlaceOrder(currencyPair.String(), - price, - amount, + fillOrKill := s.OrderType == order.Market + isBuyOrder := s.OrderSide == order.Buy + response, err := p.PlaceOrder(s.Pair.String(), + s.Price, + s.Amount, false, fillOrKill, isBuyOrder) - + if err != nil { + return submitOrderResponse, err + } if response.OrderNumber > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response.OrderNumber) + submitOrderResponse.OrderID = strconv.FormatInt(response.OrderNumber, 10) } - if err == nil { - submitOrderResponse.IsOrderPlaced = true + submitOrderResponse.IsOrderPlaced = true + if s.OrderType == order.Market { + submitOrderResponse.FullyMatched = true } - - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (p *Poloniex) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (p *Poloniex) ModifyOrder(action *order.Modify) (string, error) { oID, err := strconv.ParseInt(action.OrderID, 10, 64) if err != nil { return "", err @@ -229,7 +406,7 @@ func (p *Poloniex) ModifyOrder(action *exchange.ModifyOrder) (string, error) { } // CancelOrder cancels an order by its corresponding ID number -func (p *Poloniex) CancelOrder(order *exchange.OrderCancellation) error { +func (p *Poloniex) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) if err != nil { return err @@ -239,20 +416,21 @@ func (p *Poloniex) CancelOrder(order *exchange.OrderCancellation) error { } // CancelAllOrders cancels all orders associated with a currency pair -func (p *Poloniex) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (p *Poloniex) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } openOrders, err := p.GetOpenOrdersForAllCurrencies() if err != nil { return cancelAllOrdersResponse, err } - for _, openOrderPerCurrency := range openOrders.Data { - for _, openOrder := range openOrderPerCurrency { - err = p.CancelExistingOrder(openOrder.OrderNumber) + for key := range openOrders.Data { + for i := range openOrders.Data[key] { + err = p.CancelExistingOrder(openOrders.Data[key][i].OrderNumber) if err != nil { - cancelAllOrdersResponse.OrderStatus[strconv.FormatInt(openOrder.OrderNumber, 10)] = err.Error() + id := strconv.FormatInt(openOrders.Data[key][i].OrderNumber, 10) + cancelAllOrdersResponse.Status[id] = err.Error() } } } @@ -261,8 +439,8 @@ func (p *Poloniex) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Canc } // GetOrderInfo returns information on a current open order -func (p *Poloniex) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (p *Poloniex) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -284,20 +462,20 @@ func (p *Poloniex) GetDepositAddress(cryptocurrency currency.Code, _ string) (st // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (p *Poloniex) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (p *Poloniex) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { _, err := p.Withdraw(withdrawRequest.Currency.String(), withdrawRequest.Address, withdrawRequest.Amount) return "", err } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (p *Poloniex) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (p *Poloniex) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (p *Poloniex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (p *Poloniex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -308,7 +486,7 @@ func (p *Poloniex) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (p *Poloniex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (p.APIKey == "" || p.APISecret == "") && // Todo check connection status + if (!p.AllowAuthenticatedRequest() || p.SkipAuthCheck) && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -316,81 +494,90 @@ func (p *Poloniex) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error } // GetActiveOrders retrieves any orders that are active/open -func (p *Poloniex) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (p *Poloniex) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { resp, err := p.GetOpenOrdersForAllCurrencies() if err != nil { return nil, err } - var orders []exchange.OrderDetail - for currencyPair, openOrders := range resp.Data { - symbol := currency.NewPairDelimiter(currencyPair, - p.ConfigCurrencyPairFormat.Delimiter) + var orders []order.Detail + for key := range resp.Data { + symbol := currency.NewPairDelimiter(key, + p.GetPairFormat(asset.Spot, false).Delimiter) - for _, order := range openOrders { - orderSide := exchange.OrderSide(strings.ToUpper(order.Type)) - orderDate, err := time.Parse(poloniexDateLayout, order.Date) + for i := range resp.Data[key] { + orderSide := order.Side(strings.ToUpper(resp.Data[key][i].Type)) + orderDate, err := time.Parse(poloniexDateLayout, resp.Data[key][i].Date) if err != nil { - log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v", - p.Name, "GetActiveOrders", order.OrderNumber, order.Date) + log.Errorf(log.ExchangeSys, + "Exchange %v Func %v Order %v Could not parse date to unix with value of %v", + p.Name, + "GetActiveOrders", + resp.Data[key][i].OrderNumber, + resp.Data[key][i].Date) } - orders = append(orders, exchange.OrderDetail{ - ID: fmt.Sprintf("%v", order.OrderNumber), + orders = append(orders, order.Detail{ + ID: strconv.FormatInt(resp.Data[key][i].OrderNumber, 10), OrderSide: orderSide, - Amount: order.Amount, + Amount: resp.Data[key][i].Amount, OrderDate: orderDate, - Price: order.Rate, + Price: resp.Data[key][i].Rate, CurrencyPair: symbol, Exchange: p.Name, }) } } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (p *Poloniex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - resp, err := p.GetAuthenticatedTradeHistory(getOrdersRequest.StartTicks.Unix(), - getOrdersRequest.EndTicks.Unix(), +func (p *Poloniex) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + resp, err := p.GetAuthenticatedTradeHistory(req.StartTicks.Unix(), + req.EndTicks.Unix(), 10000) if err != nil { return nil, err } - var orders []exchange.OrderDetail - for currencyPair, historicOrders := range resp.Data { - symbol := currency.NewPairDelimiter(currencyPair, - p.ConfigCurrencyPairFormat.Delimiter) + var orders []order.Detail + for key := range resp.Data { + symbol := currency.NewPairDelimiter(key, + p.GetPairFormat(asset.Spot, false).Delimiter) - for _, order := range historicOrders { - orderSide := exchange.OrderSide(strings.ToUpper(order.Type)) - orderDate, err := time.Parse(poloniexDateLayout, order.Date) + for i := range resp.Data[key] { + orderSide := order.Side(strings.ToUpper(resp.Data[key][i].Type)) + orderDate, err := time.Parse(poloniexDateLayout, + resp.Data[key][i].Date) if err != nil { - log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v", - p.Name, "GetActiveOrders", order.OrderNumber, order.Date) + log.Errorf(log.ExchangeSys, + "Exchange %v Func %v Order %v Could not parse date to unix with value of %v", + p.Name, + "GetActiveOrders", + resp.Data[key][i].OrderNumber, + resp.Data[key][i].Date) } - orders = append(orders, exchange.OrderDetail{ - ID: fmt.Sprintf("%v", order.GlobalTradeID), + orders = append(orders, order.Detail{ + ID: strconv.FormatInt(resp.Data[key][i].GlobalTradeID, 10), OrderSide: orderSide, - Amount: order.Amount, + Amount: resp.Data[key][i].Amount, OrderDate: orderDate, - Price: order.Rate, + Price: resp.Data[key][i].Rate, CurrencyPair: symbol, Exchange: p.Name, }) } } - exchange.FilterOrdersByCurrencies(&orders, getOrdersRequest.Currencies) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) + order.FilterOrdersByCurrencies(&orders, req.Currencies) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } diff --git a/exchanges/protocol/features.go b/exchanges/protocol/features.go new file mode 100644 index 00000000..09e24396 --- /dev/null +++ b/exchanges/protocol/features.go @@ -0,0 +1,40 @@ +package protocol + +// Features holds all variables for the exchanges supported features +// for a protocol (e.g REST or Websocket) +type Features struct { + TickerBatching bool `json:"tickerBatching,omitempty"` + AutoPairUpdates bool `json:"autoPairUpdates,omitempty"` + AccountBalance bool `json:"accountBalance,omitempty"` + CryptoDeposit bool `json:"cryptoDeposit,omitempty"` + CryptoWithdrawal bool `json:"cryptoWithdrawal,omitempty"` + FiatWithdraw bool `json:"fiatWithdraw,omitempty"` + GetOrder bool `json:"getOrder,omitempty"` + GetOrders bool `json:"getOrders,omitempty"` + CancelOrders bool `json:"cancelOrders,omitempty"` + CancelOrder bool `json:"cancelOrder,omitempty"` + SubmitOrder bool `json:"submitOrder,omitempty"` + SubmitOrders bool `json:"submitOrders,omitempty"` + ModifyOrder bool `json:"modifyOrder,omitempty"` + DepositHistory bool `json:"depositHistory,omitempty"` + WithdrawalHistory bool `json:"withdrawalHistory,omitempty"` + TradeHistory bool `json:"tradeHistory,omitempty"` + UserTradeHistory bool `json:"userTradeHistory,omitempty"` + TradeFee bool `json:"tradeFee,omitempty"` + FiatDepositFee bool `json:"fiatDepositFee,omitempty"` + FiatWithdrawalFee bool `json:"fiatWithdrawalFee,omitempty"` + CryptoDepositFee bool `json:"cryptoDepositFee,omitempty"` + CryptoWithdrawalFee bool `json:"cryptoWithdrawalFee,omitempty"` + TickerFetching bool `json:"tickerFetching,omitempty"` + KlineFetching bool `json:"klineFetching,omitempty"` + TradeFetching bool `json:"tradeFetching,omitempty"` + OrderbookFetching bool `json:"orderbookFetching,omitempty"` + AccountInfo bool `json:"accountInfo,omitempty"` + FiatDeposit bool `json:"fiatDeposit,omitempty"` + DeadMansSwitch bool `json:"deadMansSwitch,omitempty"` + Subscribe bool `json:"subscribe,omitempty"` + Unsubscribe bool `json:"unsubscribe,omitempty"` + AuthenticatedEndpoints bool `json:"authenticatedEndpoints,omitempty"` + MessageCorrelation bool `json:"messageCorrelation,omitempty"` + MessageSequenceNumbers bool `json:"messageSequenceNumbers,omitempty"` +} diff --git a/exchanges/request/README.md b/exchanges/request/README.md index 37787934..fe20f726 100644 --- a/exchanges/request/README.md +++ b/exchanges/request/README.md @@ -43,4 +43,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/request/request.go b/exchanges/request/request.go index bc4ef032..ed2cfd9a 100644 --- a/exchanges/request/request.go +++ b/exchanges/request/request.go @@ -2,6 +2,7 @@ package request import ( "compress/gzip" + "encoding/json" "errors" "fmt" "io" @@ -10,77 +11,23 @@ import ( "net/http" "net/http/httputil" "net/url" - "sync" + "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/timedmutex" "github.com/thrasher-corp/gocryptotrader/exchanges/mock" "github.com/thrasher-corp/gocryptotrader/exchanges/nonce" log "github.com/thrasher-corp/gocryptotrader/logger" ) -var supportedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead, - http.MethodPut, http.MethodDelete, http.MethodOptions, http.MethodConnect} - -const ( - maxRequestJobs = 50 - proxyTLSTimeout = 15 * time.Second - defaultTimeoutRetryAttempts = 3 -) - -// Requester struct for the request client -type Requester struct { - HTTPClient *http.Client - UnauthLimit *RateLimit - AuthLimit *RateLimit - Name string - UserAgent string - Cycle time.Time - timeoutRetryAttempts int - m sync.Mutex - Jobs chan Job - disengage chan struct{} - WorkerStarted bool - Nonce nonce.Nonce - fifoLock sync.Mutex -} - -// RateLimit struct -type RateLimit struct { - Duration time.Duration - Rate int - Requests int - Mutex sync.Mutex -} - -// JobResult holds a request job result -type JobResult struct { - Error error - Result interface{} -} - -// Job holds a request job -type Job struct { - Request *http.Request - Method string - Path string - Headers map[string]string - Body io.Reader - Result interface{} - JobResult chan *JobResult - AuthRequest bool - Verbose bool - HTTPDebugging bool - Record bool -} - // NewRateLimit creates a new RateLimit func NewRateLimit(d time.Duration, rate int) *RateLimit { return &RateLimit{Duration: d, Rate: rate} } -// ToString returns the rate limiter in string notation -func (r *RateLimit) ToString() string { +// String returns the rate limiter in string notation +func (r *RateLimit) String() string { return fmt.Sprintf("Rate limiter set to %d requests per %v", r.Rate, r.Duration) } @@ -149,6 +96,10 @@ func (r *Requester) IsRateLimited(auth bool) bool { // RequiresRateLimiter returns whether or not the request Requester requires a rate limiter func (r *Requester) RequiresRateLimiter() bool { + if DisableRateLimiter { + return false + } + if r.AuthLimit.GetRate() != 0 || r.UnauthLimit.GetRate() != 0 { return true } @@ -221,9 +172,9 @@ func New(name string, authLimit, unauthLimit *RateLimit, httpRequester *http.Cli UnauthLimit: unauthLimit, AuthLimit: authLimit, Name: name, - Jobs: make(chan Job, maxRequestJobs), - disengage: make(chan struct{}, 1), - timeoutRetryAttempts: defaultTimeoutRetryAttempts, + Jobs: make(chan Job, MaxRequestJobs), + timeoutRetryAttempts: TimeoutRetryAttempts, + timedLock: timedmutex.NewTimedMutex(DefaultMutexLockTimeout), } } @@ -268,16 +219,19 @@ func (r *Requester) checkRequest(method, path string, body io.Reader, headers ma // DoRequest performs a HTTP/HTTPS request with the supplied params func (r *Requester) DoRequest(req *http.Request, path string, body io.Reader, result interface{}, authRequest, verbose, httpDebug, httpRecord bool) error { if verbose { - log.Debugf("%s exchange request path: %s requires rate limiter: %v", + log.Debugf(log.Global, + "%s exchange request path: %s requires rate limiter: %v", r.Name, path, r.RequiresRateLimiter()) for k, d := range req.Header { - log.Debugf("%s exchange request header [%s]: %s", r.Name, k, d) + log.Debugf(log.Global, "%s exchange request header [%s]: %s", r.Name, k, d) } - log.Debugf("%s exchange request type: %s", r.Name, req.Method) - log.Debugf("%s exchange request body: %v", r.Name, body) + log.Debugf(log.Global, + "%s exchange request type: %s", r.Name, req.Method) + log.Debugf(log.Global, + "%s exchange request body: %v", r.Name, body) } var timeoutError error @@ -286,7 +240,7 @@ func (r *Requester) DoRequest(req *http.Request, path string, body io.Reader, re if err != nil { if timeoutErr, ok := err.(net.Error); ok && timeoutErr.Timeout() { if verbose { - log.Errorf("%s request has timed-out retrying request, count %d", + log.Errorf(log.ExchangeSys, "%s request has timed-out retrying request, count %d", r.Name, i) } @@ -320,12 +274,17 @@ func (r *Requester) DoRequest(req *http.Request, path string, body io.Reader, re default: switch { - case common.StringContains(resp.Header.Get("Content-Type"), "application/json"): + case strings.Contains(resp.Header.Get("Content-Type"), "application/json"): reader = resp.Body default: - log.Warnf("%s request response content type differs from JSON; received %v [path: %s]", - r.Name, resp.Header.Get("Content-Type"), path) + if verbose { + log.Warnf(log.ExchangeSys, + "%s request response content type differs from JSON; received %v [path: %s]\n", + r.Name, + resp.Header.Get("Content-Type"), + path) + } reader = resp.Body } } @@ -356,22 +315,22 @@ func (r *Requester) DoRequest(req *http.Request, path string, body io.Reader, re if httpDebug { dump, err := httputil.DumpResponse(resp, false) if err != nil { - log.Errorf("DumpResponse invalid response: %v:", err) + log.Errorf(log.Global, "DumpResponse invalid response: %v:", err) } - log.Debugf("DumpResponse Headers (%v):\n%s", path, dump) - log.Debugf("DumpResponse Body (%v):\n %s", path, string(contents)) + log.Debugf(log.Global, "DumpResponse Headers (%v):\n%s", path, dump) + log.Debugf(log.Global, "DumpResponse Body (%v):\n %s", path, string(contents)) } resp.Body.Close() if verbose { - log.Debugf("HTTP status: %s, Code: %v", resp.Status, resp.StatusCode) + log.Debugf(log.ExchangeSys, "HTTP status: %s, Code: %v", resp.Status, resp.StatusCode) if !httpDebug { - log.Debugf("%s exchange raw response: %s", r.Name, string(contents)) + log.Debugf(log.ExchangeSys, "%s exchange raw response: %s", r.Name, string(contents)) } } if result != nil { - return common.JSONDecode(contents, result) + return json.Unmarshal(contents, result) } return nil @@ -395,7 +354,7 @@ func (r *Requester) worker() { limit := r.GetRateLimit(x.AuthRequest) diff := limit.GetDuration() - time.Since(r.Cycle) if x.Verbose { - log.Debugf("%s request. Rate limited! Sleeping for %v", r.Name, diff) + log.Debugf(log.ExchangeSys, "%s request. Rate limited! Sleeping for %v", r.Name, diff) } time.Sleep(diff) @@ -407,7 +366,7 @@ func (r *Requester) worker() { r.IncrementRequests(x.AuthRequest) if x.Verbose { - log.Debugf("%s request. No longer rate limited! Doing request", r.Name) + log.Debugf(log.ExchangeSys, "%s request. No longer rate limited! Doing request", r.Name) } err := r.DoRequest(x.Request, x.Path, x.Body, x.Result, x.AuthRequest, x.Verbose, x.HTTPDebugging, x.Record) @@ -425,45 +384,47 @@ func (r *Requester) worker() { // SendPayload handles sending HTTP/HTTPS requests func (r *Requester) SendPayload(method, path string, headers map[string]string, body io.Reader, result interface{}, authRequest, nonceEnabled, verbose, httpDebugging, record bool) error { if !nonceEnabled { - r.lock() + r.timedLock.LockForDuration() } if r == nil || r.Name == "" { - r.unlock() + r.timedLock.UnlockIfLocked() return errors.New("not initiliased, SetDefaults() called before making request?") } if !IsValidMethod(method) { - r.unlock() + r.timedLock.UnlockIfLocked() return fmt.Errorf("incorrect method supplied %s: supported %s", method, supportedMethods) } if path == "" { - r.unlock() + r.timedLock.UnlockIfLocked() return errors.New("invalid path") } req, err := r.checkRequest(method, path, body, headers) if err != nil { - r.unlock() + r.timedLock.UnlockIfLocked() return err } if httpDebugging { dump, err := httputil.DumpRequestOut(req, true) if err != nil { - log.Errorf("DumpRequest invalid response %v:", err) + log.Errorf(log.Global, + "DumpRequest invalid response %v:", err) } - log.Debugf("DumpRequest:\n%s", dump) + log.Debugf(log.Global, + "DumpRequest:\n%s", dump) } if !r.RequiresRateLimiter() { - r.unlock() + r.timedLock.UnlockIfLocked() return r.DoRequest(req, path, body, result, authRequest, verbose, httpDebugging, record) } - if len(r.Jobs) == maxRequestJobs { - r.unlock() + if len(r.Jobs) == MaxRequestJobs { + r.timedLock.UnlockIfLocked() return errors.New("max request jobs reached") } @@ -492,18 +453,18 @@ func (r *Requester) SendPayload(method, path string, headers map[string]string, } if verbose { - log.Debugf("%s request. Attaching new job.", r.Name) + log.Debugf(log.ExchangeSys, "%s request. Attaching new job.", r.Name) } r.Jobs <- newJob - r.unlock() + r.timedLock.UnlockIfLocked() if verbose { - log.Debugf("%s request. Waiting for job to complete.", r.Name) + log.Debugf(log.ExchangeSys, "%s request. Waiting for job to complete.", r.Name) } resp := <-newJob.JobResult if verbose { - log.Debugf("%s request. Job complete.", r.Name) + log.Debugf(log.ExchangeSys, "%s request. Job complete.", r.Name) } return resp.Error @@ -512,7 +473,7 @@ func (r *Requester) SendPayload(method, path string, headers map[string]string, // GetNonce returns a nonce for requests. This locks and enforces concurrent // nonce FIFO on the buffered job channel func (r *Requester) GetNonce(isNano bool) nonce.Value { - r.lock() + r.timedLock.LockForDuration() if r.Nonce.Get() == 0 { if isNano { r.Nonce.Set(time.Now().UnixNano()) @@ -528,7 +489,7 @@ func (r *Requester) GetNonce(isNano bool) nonce.Value { // GetNonceMilli returns a nonce for requests. This locks and enforces concurrent // nonce FIFO on the buffered job channel this is for millisecond func (r *Requester) GetNonceMilli() nonce.Value { - r.lock() + r.timedLock.LockForDuration() if r.Nonce.Get() == 0 { r.Nonce.Set(time.Now().UnixNano() / int64(time.Millisecond)) return r.Nonce.Get() @@ -549,33 +510,3 @@ func (r *Requester) SetProxy(p *url.URL) error { } return nil } - -// lock locks and sets up an issue timer, if something errors out of scope it -// automatically unlocks -func (r *Requester) lock() { - if r.disengage == nil { - r.disengage = make(chan struct{}, 1) - } - var wg sync.WaitGroup - r.fifoLock.Lock() - wg.Add(1) - go func() { - timer := time.NewTimer(50 * time.Millisecond) - wg.Done() - select { - case <-timer.C: - log.Errorf("Unlocking due to possible error for %s", r.Name) - r.fifoLock.Unlock() - - case <-r.disengage: - return - } - }() - wg.Wait() -} - -// unlock unlocks mtx and shuts down a timer -func (r *Requester) unlock() { - r.disengage <- struct{}{} - r.fifoLock.Unlock() -} diff --git a/exchanges/request/request_test.go b/exchanges/request/request_test.go index aa5c36df..37b94077 100644 --- a/exchanges/request/request_test.go +++ b/exchanges/request/request_test.go @@ -66,15 +66,15 @@ func TestIsRateLimited(t *testing.T) { r := New("bitfinex", NewRateLimit(time.Second*10, 5), NewRateLimit(time.Second*20, 100), new(http.Client)) r.StartCycle() - if r.AuthLimit.ToString() != "Rate limiter set to 5 requests per 10s" { + if r.AuthLimit.String() != "Rate limiter set to 5 requests per 10s" { t.Fatal("unexcpted values") } - if r.UnauthLimit.ToString() != "Rate limiter set to 100 requests per 20s" { + if r.UnauthLimit.String() != "Rate limiter set to 100 requests per 20s" { t.Fatal("unexpected values") } - if r.AuthLimit.ToString() != "Rate limiter set to 5 requests per 10s" { + if r.AuthLimit.String() != "Rate limiter set to 5 requests per 10s" { t.Fatal("unexcpted values") } @@ -199,35 +199,25 @@ func TestCheckRequest(t *testing.T) { } func TestDoRequest(t *testing.T) { - var test = new(Requester) - err := test.SendPayload(http.MethodGet, "https://www.google.com", nil, nil, nil, false, false, true, false, false) - if err == nil { - t.Fatal("not iniitalised") - } - r := New("", NewRateLimit(time.Second*10, 5), NewRateLimit(time.Second*20, 100), new(http.Client)) - if err == nil { - t.Fatal("unexpected values") - } - r.Name = "bitfinex" - err = r.SendPayload("BLAH", "https://www.google.com", nil, nil, nil, false, false, true, false, false) + err := r.SendPayload("BLAH", "https://www.google.com", nil, nil, nil, false, false, true, false, false) if err == nil { - t.Fatal("unexpected values") + t.Fatal("Expected error") } err = r.SendPayload(http.MethodGet, "", nil, nil, nil, false, false, true, false, false) if err == nil { - t.Fatal("unexpected values") + t.Fatal("Expected error") } err = r.SendPayload(http.MethodGet, "https://www.google.com", nil, nil, nil, false, false, true, false, false) if err != nil { - t.Fatal("unexpected values") + t.Fatal("unexpected values", err) } if !r.RequiresRateLimiter() { - t.Fatal("unexpcted values") + t.Fatal("unexpected values") } r.SetRateLimit(false, time.Second, 0) @@ -235,7 +225,7 @@ func TestDoRequest(t *testing.T) { err = r.SendPayload(http.MethodGet, "https://www.google.com", nil, nil, nil, false, false, true, false, false) if err != nil { - t.Fatal("unexpected values") + t.Fatal("unexpected values", err) } if r.RequiresRateLimiter() { @@ -247,7 +237,7 @@ func TestDoRequest(t *testing.T) { r.Cycle = time.Now().Add(time.Millisecond * -201) if r.IsValidCycle(false) { - t.Fatal("unexepcted values") + t.Fatal("unexpected values") } err = r.SendPayload(http.MethodGet, "https://www.google.com", nil, nil, nil, false, false, true, false, false) @@ -288,18 +278,18 @@ func TestDoRequest(t *testing.T) { err = r.SetTimeoutRetryAttempts(1) if err != nil { - t.Fatal("test failed - setting timeout retry attempts") + t.Fatal("setting timeout retry attempts") } err = r.SetTimeoutRetryAttempts(-1) if err == nil { - t.Fatal("test failed - setting timeout retry attempts with negative value") + t.Fatal("setting timeout retry attempts with negative value") } r.HTTPClient.Timeout = 1 * time.Second err = r.SendPayload(http.MethodPost, "https://httpstat.us/200?sleep=20000", nil, nil, nil, false, false, true, false, false) if err == nil { - t.Fatal(err) + t.Fatal("Expected error") } proxy, err := url.Parse("") @@ -309,7 +299,7 @@ func TestDoRequest(t *testing.T) { err = r.SetProxy(proxy) if err == nil { - t.Error("failed to set proxy") + t.Error("Expected error") } proxy, err = url.Parse("https://192.0.0.1") @@ -324,7 +314,7 @@ func TestDoRequest(t *testing.T) { } func BenchmarkRequestLockMech(b *testing.B) { - var r = new(Requester) + r := New("", NewRateLimit(time.Second*10, 5), NewRateLimit(time.Second*20, 100), new(http.Client)) var meep interface{} for n := 0; n < b.N; n++ { r.SendPayload(http.MethodGet, "127.0.0.1", nil, nil, &meep, false, false, false, false, false) diff --git a/exchanges/request/request_types.go b/exchanges/request/request_types.go new file mode 100644 index 00000000..f35fe3cb --- /dev/null +++ b/exchanges/request/request_types.go @@ -0,0 +1,75 @@ +package request + +import ( + "io" + "net/http" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common/timedmutex" + "github.com/thrasher-corp/gocryptotrader/exchanges/nonce" +) + +var supportedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead, + http.MethodPut, http.MethodDelete, http.MethodOptions, http.MethodConnect} + +// Const vars for rate limiter +const ( + DefaultMaxRequestJobs = 50 + DefaultTimeoutRetryAttempts = 3 + DefaultMutexLockTimeout = 50 * time.Millisecond + proxyTLSTimeout = 15 * time.Second +) + +// Vars for rate limiter +var ( + MaxRequestJobs = DefaultMaxRequestJobs + TimeoutRetryAttempts = DefaultTimeoutRetryAttempts + DisableRateLimiter bool +) + +// Requester struct for the request client +type Requester struct { + HTTPClient *http.Client + UnauthLimit *RateLimit + AuthLimit *RateLimit + Name string + UserAgent string + Cycle time.Time + timeoutRetryAttempts int + m sync.Mutex + Jobs chan Job + WorkerStarted bool + Nonce nonce.Nonce + DisableRateLimiter bool + timedLock *timedmutex.TimedMutex +} + +// RateLimit struct +type RateLimit struct { + Duration time.Duration + Rate int + Requests int + Mutex sync.Mutex +} + +// JobResult holds a request job result +type JobResult struct { + Error error + Result interface{} +} + +// Job holds a request job +type Job struct { + Request *http.Request + Method string + Path string + Headers map[string]string + Body io.Reader + Result interface{} + JobResult chan *JobResult + AuthRequest bool + Verbose bool + HTTPDebugging bool + Record bool +} diff --git a/exchanges/stats/README.md b/exchanges/stats/README.md index 890b0930..f90c39e1 100644 --- a/exchanges/stats/README.md +++ b/exchanges/stats/README.md @@ -46,4 +46,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/stats/stats.go b/exchanges/stats/stats.go index 252fcb49..f9805eed 100644 --- a/exchanges/stats/stats.go +++ b/exchanges/stats/stats.go @@ -4,13 +4,14 @@ import ( "sort" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) // Item holds various fields for storing currency pair stats type Item struct { Exchange string Pair currency.Pair - AssetType string + AssetType asset.Item Price float64 Volume float64 } @@ -49,7 +50,7 @@ func (b ByVolume) Swap(i, j int) { } // Add adds or updates the item stats -func Add(exchange string, p currency.Pair, assetType string, price, volume float64) { +func Add(exchange string, p currency.Pair, assetType asset.Item, price, volume float64) { if exchange == "" || assetType == "" || price == 0 || @@ -74,7 +75,7 @@ func Add(exchange string, p currency.Pair, assetType string, price, volume float // Append adds or updates the item stats for a specific // currency pair and asset type -func Append(exchange string, p currency.Pair, assetType string, price, volume float64) { +func Append(exchange string, p currency.Pair, assetType asset.Item, price, volume float64) { if AlreadyExists(exchange, p, assetType, price, volume) { return } @@ -92,7 +93,7 @@ func Append(exchange string, p currency.Pair, assetType string, price, volume fl // AlreadyExists checks to see if item info already exists // for a specific currency pair and asset type -func AlreadyExists(exchange string, p currency.Pair, assetType string, price, volume float64) bool { +func AlreadyExists(exchange string, p currency.Pair, assetType asset.Item, price, volume float64) bool { for i := range Items { if Items[i].Exchange == exchange && Items[i].Pair.EqualIncludeReciprocal(p) && @@ -107,7 +108,7 @@ func AlreadyExists(exchange string, p currency.Pair, assetType string, price, vo // SortExchangesByVolume sorts item info by volume for a specific // currency pair and asset type. Reverse will reverse the order from lowest to // highest -func SortExchangesByVolume(p currency.Pair, assetType string, reverse bool) []Item { +func SortExchangesByVolume(p currency.Pair, assetType asset.Item, reverse bool) []Item { var result []Item for x := range Items { if Items[x].Pair.EqualIncludeReciprocal(p) && @@ -127,7 +128,7 @@ func SortExchangesByVolume(p currency.Pair, assetType string, reverse bool) []It // SortExchangesByPrice sorts item info by volume for a specific // currency pair and asset type. Reverse will reverse the order from lowest to // highest -func SortExchangesByPrice(p currency.Pair, assetType string, reverse bool) []Item { +func SortExchangesByPrice(p currency.Pair, assetType asset.Item, reverse bool) []Item { var result []Item for x := range Items { if Items[x].Pair.EqualIncludeReciprocal(p) && diff --git a/exchanges/stats/stats_test.go b/exchanges/stats/stats_test.go index 28a97c09..0e22edd6 100644 --- a/exchanges/stats/stats_test.go +++ b/exchanges/stats/stats_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) func TestLenByPrice(t *testing.T) { @@ -12,14 +13,14 @@ func TestLenByPrice(t *testing.T) { { Exchange: "ANX", Pair: p, - AssetType: "SPOT", + AssetType: asset.Spot, Price: 1200, Volume: 5, }, } if ByPrice.Len(Items) < 1 { - t.Error("Test Failed - stats LenByPrice() length not correct.") + t.Error("stats LenByPrice() length not correct.") } } @@ -30,24 +31,24 @@ func TestLessByPrice(t *testing.T) { { Exchange: "alphapoint", Pair: p, - AssetType: "SPOT", + AssetType: asset.Spot, Price: 1200, Volume: 5, }, { Exchange: "bitfinex", Pair: p, - AssetType: "SPOT", + AssetType: asset.Spot, Price: 1198, Volume: 20, }, } if !ByPrice.Less(Items, 1, 0) { - t.Error("Test Failed - stats LessByPrice() incorrect return.") + t.Error("stats LessByPrice() incorrect return.") } if ByPrice.Less(Items, 0, 1) { - t.Error("Test Failed - stats LessByPrice() incorrect return.") + t.Error("stats LessByPrice() incorrect return.") } } @@ -58,14 +59,14 @@ func TestSwapByPrice(t *testing.T) { { Exchange: "bitstamp", Pair: p, - AssetType: "SPOT", + AssetType: asset.Spot, Price: 1324, Volume: 5, }, { Exchange: "bitfinex", Pair: p, - AssetType: "SPOT", + AssetType: asset.Spot, Price: 7863, Volume: 20, }, @@ -73,22 +74,22 @@ func TestSwapByPrice(t *testing.T) { ByPrice.Swap(Items, 0, 1) if Items[0].Exchange != "bitfinex" || Items[1].Exchange != "bitstamp" { - t.Error("Test Failed - stats SwapByPrice did not swap values.") + t.Error("stats SwapByPrice did not swap values.") } } func TestLenByVolume(t *testing.T) { if ByVolume.Len(Items) != 2 { - t.Error("Test Failed - stats lenByVolume did not swap values.") + t.Error("stats lenByVolume did not swap values.") } } func TestLessByVolume(t *testing.T) { if !ByVolume.Less(Items, 1, 0) { - t.Error("Test Failed - stats LessByVolume() incorrect return.") + t.Error("stats LessByVolume() incorrect return.") } if ByVolume.Less(Items, 0, 1) { - t.Error("Test Failed - stats LessByVolume() incorrect return.") + t.Error("stats LessByVolume() incorrect return.") } } @@ -96,86 +97,86 @@ func TestSwapByVolume(t *testing.T) { ByPrice.Swap(Items, 0, 1) if Items[1].Exchange != "bitfinex" || Items[0].Exchange != "bitstamp" { - t.Error("Test Failed - stats SwapByVolume did not swap values.") + t.Error("stats SwapByVolume did not swap values.") } } func TestAdd(t *testing.T) { Items = Items[:0] p := currency.NewPairFromStrings("BTC", "USD") - Add("ANX", p, "SPOT", 1200, 42) + Add("ANX", p, asset.Spot, 1200, 42) if len(Items) < 1 { - t.Error("Test Failed - stats Add did not add exchange info.") + t.Error("stats Add did not add exchange info.") } Add("", p, "", 0, 0) if len(Items) != 1 { - t.Error("Test Failed - stats Add did not add exchange info.") + t.Error("stats Add did not add exchange info.") } p.Base = currency.XBT - Add("ANX", p, "SPOT", 1201, 43) + Add("ANX", p, asset.Spot, 1201, 43) if Items[1].Pair.String() != "XBTUSD" { - t.Fatal("Test failed. stats Add did not add exchange info.") + t.Fatal("stats Add did not add exchange info.") } p = currency.NewPairFromStrings("ETH", "USDT") - Add("ANX", p, "SPOT", 300, 1000) + Add("ANX", p, asset.Spot, 300, 1000) if Items[2].Pair.String() != "ETHUSD" { - t.Fatal("Test failed. stats Add did not add exchange info.") + t.Fatal("stats Add did not add exchange info.") } } func TestAppend(t *testing.T) { p := currency.NewPairFromStrings("BTC", "USD") - Append("sillyexchange", p, "SPOT", 1234, 45) + Append("sillyexchange", p, asset.Spot, 1234, 45) if len(Items) < 2 { - t.Error("Test Failed - stats Append did not add exchange values.") + t.Error("stats Append did not add exchange values.") } - Append("sillyexchange", p, "SPOT", 1234, 45) + Append("sillyexchange", p, asset.Spot, 1234, 45) if len(Items) == 3 { - t.Error("Test Failed - stats Append added exchange values") + t.Error("stats Append added exchange values") } } func TestAlreadyExists(t *testing.T) { p := currency.NewPairFromStrings("BTC", "USD") - if !AlreadyExists("ANX", p, "SPOT", 1200, 42) { - t.Error("Test Failed - stats AlreadyExists exchange does not exist.") + if !AlreadyExists("ANX", p, asset.Spot, 1200, 42) { + t.Error("stats AlreadyExists exchange does not exist.") } p.Base = currency.NewCode("dii") - if AlreadyExists("bla", p, "SPOT", 1234, 123) { - t.Error("Test Failed - stats AlreadyExists found incorrect exchange.") + if AlreadyExists("bla", p, asset.Spot, 1234, 123) { + t.Error("stats AlreadyExists found incorrect exchange.") } } func TestSortExchangesByVolume(t *testing.T) { p := currency.NewPairFromStrings("BTC", "USD") - topVolume := SortExchangesByVolume(p, "SPOT", true) + topVolume := SortExchangesByVolume(p, asset.Spot, true) if topVolume[0].Exchange != "sillyexchange" { - t.Error("Test Failed - stats SortExchangesByVolume incorrectly sorted values.") + t.Error("stats SortExchangesByVolume incorrectly sorted values.") } - topVolume = SortExchangesByVolume(p, "SPOT", false) + topVolume = SortExchangesByVolume(p, asset.Spot, false) if topVolume[0].Exchange != "ANX" { - t.Error("Test Failed - stats SortExchangesByVolume incorrectly sorted values.") + t.Error("stats SortExchangesByVolume incorrectly sorted values.") } } func TestSortExchangesByPrice(t *testing.T) { p := currency.NewPairFromStrings("BTC", "USD") - topPrice := SortExchangesByPrice(p, "SPOT", true) + topPrice := SortExchangesByPrice(p, asset.Spot, true) if topPrice[0].Exchange != "sillyexchange" { - t.Error("Test Failed - stats SortExchangesByPrice incorrectly sorted values.") + t.Error("stats SortExchangesByPrice incorrectly sorted values.") } - topPrice = SortExchangesByPrice(p, "SPOT", false) + topPrice = SortExchangesByPrice(p, asset.Spot, false) if topPrice[0].Exchange != "ANX" { - t.Error("Test Failed - stats SortExchangesByPrice incorrectly sorted values.") + t.Error("stats SortExchangesByPrice incorrectly sorted values.") } } diff --git a/exchanges/support.go b/exchanges/support.go new file mode 100644 index 00000000..37e0ceed --- /dev/null +++ b/exchanges/support.go @@ -0,0 +1,45 @@ +package exchange + +import "strings" + +// IsSupported returns whether or not a specific exchange is supported +func IsSupported(exchangeName string) bool { + for x := range Exchanges { + if strings.EqualFold(exchangeName, Exchanges[x]) { + return true + } + } + return false +} + +// Exchanges stores a list of supported exchanges +var Exchanges = []string{ + "anx", + "binance", + "bitfinex", + "bitflyer", + "bithumb", + "bitmex", + "bitstamp", + "bittrex", + "btc markets", + "btse", + "coinbasepro", + "coinbene", + "coinut", + "exmo", + "gateio", + "gemini", + "hitbtc", + "huobi", + "itbit", + "kraken", + "lakebtc", + "lbank", + "localbitcoins", + "okcoin international", + "okex", + "poloniex", + "yobit", + "zb", +} diff --git a/exchanges/support_test.go b/exchanges/support_test.go new file mode 100644 index 00000000..978ab4e8 --- /dev/null +++ b/exchanges/support_test.go @@ -0,0 +1,13 @@ +package exchange + +import "testing" + +func TestIsSupported(t *testing.T) { + if ok := IsSupported("BiTStaMp"); !ok { + t.Error("supported exchange should be valid") + } + + if ok := IsSupported("meowexch"); ok { + t.Error("non-supported exchange should be in valid") + } +} diff --git a/exchanges/ticker/README.md b/exchanges/ticker/README.md index f6d5f406..f67a4c91 100644 --- a/exchanges/ticker/README.md +++ b/exchanges/ticker/README.md @@ -35,7 +35,7 @@ exchange interface system set by exchange wrapper orderbook functions in Examples below: ```go -tick, err := yobitExchange.GetTickerPrice() +tick, err := yobitExchange.FetchTicker() if err != nil { // Handle error } @@ -74,4 +74,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/ticker/ticker.go b/exchanges/ticker/ticker.go index fc7744d2..4ff779b6 100644 --- a/exchanges/ticker/ticker.go +++ b/exchanges/ticker/ticker.go @@ -2,188 +2,207 @@ package ticker import ( "errors" - "strconv" - "sync" + "fmt" + "strings" "time" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/gofrs/uuid" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/dispatch" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) -// const values for the ticker package -const ( - errExchangeTickerNotFound = "ticker for exchange does not exist" - errPairNotSet = "ticker currency pair not set" - errAssetTypeNotSet = "ticker asset type not set" - errBaseCurrencyNotFound = "ticker base currency not found" - errQuoteCurrencyNotFound = "ticker quote currency not found" - - Spot = "SPOT" -) - -// Vars for the ticker package -var ( - Tickers []Ticker - m sync.Mutex -) - -// Price struct stores the currency pair and pricing information -type Price struct { - Pair currency.Pair `json:"Pair"` - Last float64 `json:"Last"` - High float64 `json:"High"` - Low float64 `json:"Low"` - Bid float64 `json:"Bid"` - Ask float64 `json:"Ask"` - Volume float64 `json:"Volume"` - PriceATH float64 `json:"PriceATH"` - LastUpdated time.Time +func init() { + service = new(Service) + service.Tickers = make(map[string]map[*currency.Item]map[*currency.Item]map[asset.Item]*Ticker) + service.Exchange = make(map[string]uuid.UUID) + service.mux = dispatch.GetNewMux() } -// Ticker struct holds the ticker information for a currency pair and type -type Ticker struct { - Price map[string]map[string]map[string]Price - ExchangeName string -} +// SubscribeTicker subcribes to a ticker and returns a communication channel to +// stream new ticker updates +func SubscribeTicker(exchange string, p currency.Pair, a asset.Item) (dispatch.Pipe, error) { + exchange = strings.ToLower(exchange) + service.RLock() + defer service.RUnlock() -// PriceToString returns the string version of a stored price field -func (t *Ticker) PriceToString(p currency.Pair, priceType, tickerType string) string { - priceType = common.StringToLower(priceType) - - switch priceType { - case "last": - return strconv.FormatFloat(t.Price[p.Base.Upper().String()][p.Quote.Upper().String()][tickerType].Last, 'f', -1, 64) - case "high": - return strconv.FormatFloat(t.Price[p.Base.Upper().String()][p.Quote.Upper().String()][tickerType].High, 'f', -1, 64) - case "low": - return strconv.FormatFloat(t.Price[p.Base.Upper().String()][p.Quote.Upper().String()][tickerType].Low, 'f', -1, 64) - case "bid": - return strconv.FormatFloat(t.Price[p.Base.Upper().String()][p.Quote.Upper().String()][tickerType].Bid, 'f', -1, 64) - case "ask": - return strconv.FormatFloat(t.Price[p.Base.Upper().String()][p.Quote.Upper().String()][tickerType].Ask, 'f', -1, 64) - case "volume": - return strconv.FormatFloat(t.Price[p.Base.Upper().String()][p.Quote.Upper().String()][tickerType].Volume, 'f', -1, 64) - case "ath": - return strconv.FormatFloat(t.Price[p.Base.Upper().String()][p.Quote.Upper().String()][tickerType].PriceATH, 'f', -1, 64) - default: - return "" + tick, ok := service.Tickers[exchange][p.Base.Item][p.Quote.Item][a] + if !ok { + return dispatch.Pipe{}, fmt.Errorf("ticker item not found for %s %s %s", + exchange, + p, + a) } + + return service.mux.Subscribe(tick.Main) +} + +// SubscribeToExchangeTickers subcribes to all tickers on an exchange +func SubscribeToExchangeTickers(exchange string) (dispatch.Pipe, error) { + exchange = strings.ToLower(exchange) + service.RLock() + defer service.RUnlock() + id, ok := service.Exchange[exchange] + if !ok { + return dispatch.Pipe{}, fmt.Errorf("%s exchange tickers not found", + exchange) + } + + return service.mux.Subscribe(id) } // GetTicker checks and returns a requested ticker if it exists -func GetTicker(exchange string, p currency.Pair, tickerType string) (Price, error) { - ticker, err := GetTickerByExchange(exchange) - if err != nil { - return Price{}, err +func GetTicker(exchange string, p currency.Pair, tickerType asset.Item) (Price, error) { + exchange = strings.ToLower(exchange) + service.RLock() + defer service.RUnlock() + if service.Tickers[exchange] == nil { + return Price{}, fmt.Errorf("no tickers for %s exchange", exchange) } - if !BaseCurrencyExists(exchange, p.Base) { - return Price{}, errors.New(errBaseCurrencyNotFound) + if service.Tickers[exchange][p.Base.Item] == nil { + return Price{}, fmt.Errorf("no tickers associated with base currency %s", + p.Base) } - if !QuoteCurrencyExists(exchange, p) { - return Price{}, errors.New(errQuoteCurrencyNotFound) + if service.Tickers[exchange][p.Base.Item][p.Quote.Item] == nil { + return Price{}, fmt.Errorf("no tickers associated with quote currency %s", + p.Quote) } - return ticker.Price[p.Base.Upper().String()][p.Quote.Upper().String()][tickerType], nil -} - -// GetTickerByExchange returns an exchange Ticker -func GetTickerByExchange(exchange string) (*Ticker, error) { - m.Lock() - defer m.Unlock() - for x := range Tickers { - if Tickers[x].ExchangeName == exchange { - return &Tickers[x], nil - } + if service.Tickers[exchange][p.Base.Item][p.Quote.Item][tickerType] == nil { + return Price{}, fmt.Errorf("no tickers associated with asset type %s", + tickerType) } - return nil, errors.New(errExchangeTickerNotFound) -} -// BaseCurrencyExists checks to see if the base currency of the ticker map -// exists -func BaseCurrencyExists(exchange string, currency currency.Code) bool { - m.Lock() - defer m.Unlock() - for _, y := range Tickers { - if y.ExchangeName == exchange { - if _, ok := y.Price[currency.Upper().String()]; ok { - return true - } - } - } - return false -} - -// QuoteCurrencyExists checks to see if the quote currency of the ticker map -// exists -func QuoteCurrencyExists(exchange string, p currency.Pair) bool { - m.Lock() - defer m.Unlock() - for _, y := range Tickers { - if y.ExchangeName == exchange { - if _, ok := y.Price[p.Base.Upper().String()]; ok { - if _, ok := y.Price[p.Base.Upper().String()][p.Quote.Upper().String()]; ok { - return true - } - } - } - } - return false -} - -// CreateNewTicker creates a new Ticker -func CreateNewTicker(exchangeName string, tickerNew *Price, tickerType string) Ticker { - m.Lock() - defer m.Unlock() - ticker := Ticker{} - ticker.ExchangeName = exchangeName - ticker.Price = make(map[string]map[string]map[string]Price) - a := make(map[string]map[string]Price) - b := make(map[string]Price) - b[tickerType] = *tickerNew - a[tickerNew.Pair.Quote.Upper().String()] = b - ticker.Price[tickerNew.Pair.Base.Upper().String()] = a - Tickers = append(Tickers, ticker) - return ticker + return service.Tickers[exchange][p.Base.Item][p.Quote.Item][tickerType].Price, nil } // ProcessTicker processes incoming tickers, creating or updating the Tickers // list -func ProcessTicker(exchangeName string, tickerNew *Price, assetType string) error { +func ProcessTicker(exchangeName string, tickerNew *Price, assetType asset.Item) error { + if exchangeName == "" { + return fmt.Errorf(errExchangeNameUnset) + } + + tickerNew.ExchangeName = strings.ToLower(exchangeName) + if tickerNew.Pair.IsEmpty() { - return errors.New(errPairNotSet) + return fmt.Errorf("%s %s", exchangeName, errPairNotSet) } if assetType == "" { - return errors.New(errAssetTypeNotSet) + return fmt.Errorf("%s %s %s", exchangeName, + tickerNew.Pair, + errAssetTypeNotSet) } + tickerNew.AssetType = assetType + if tickerNew.LastUpdated.IsZero() { tickerNew.LastUpdated = time.Now() } - ticker, err := GetTickerByExchange(exchangeName) + return service.Update(tickerNew) +} + +// Update updates ticker price +func (s *Service) Update(p *Price) error { + var ids []uuid.UUID + + s.Lock() + switch { + case s.Tickers[p.ExchangeName] == nil: + s.Tickers[p.ExchangeName] = make(map[*currency.Item]map[*currency.Item]map[asset.Item]*Ticker) + s.Tickers[p.ExchangeName][p.Pair.Base.Item] = make(map[*currency.Item]map[asset.Item]*Ticker) + s.Tickers[p.ExchangeName][p.Pair.Base.Item][p.Pair.Quote.Item] = make(map[asset.Item]*Ticker) + err := s.SetItemID(p) + if err != nil { + s.Unlock() + return err + } + + case s.Tickers[p.ExchangeName][p.Pair.Base.Item] == nil: + s.Tickers[p.ExchangeName][p.Pair.Base.Item] = make(map[*currency.Item]map[asset.Item]*Ticker) + s.Tickers[p.ExchangeName][p.Pair.Base.Item][p.Pair.Quote.Item] = make(map[asset.Item]*Ticker) + err := s.SetItemID(p) + if err != nil { + s.Unlock() + return err + } + + case s.Tickers[p.ExchangeName][p.Pair.Base.Item][p.Pair.Quote.Item] == nil: + s.Tickers[p.ExchangeName][p.Pair.Base.Item][p.Pair.Quote.Item] = make(map[asset.Item]*Ticker) + err := s.SetItemID(p) + if err != nil { + s.Unlock() + return err + } + + case s.Tickers[p.ExchangeName][p.Pair.Base.Item][p.Pair.Quote.Item][p.AssetType] == nil: + err := s.SetItemID(p) + if err != nil { + s.Unlock() + return err + } + + default: + ticker := s.Tickers[p.ExchangeName][p.Pair.Base.Item][p.Pair.Quote.Item][p.AssetType] + ticker.Last = p.Last + ticker.High = p.High + ticker.Low = p.Low + ticker.Bid = p.Bid + ticker.Ask = p.Ask + ticker.Volume = p.Volume + ticker.QuoteVolume = p.QuoteVolume + ticker.PriceATH = p.PriceATH + ticker.Open = p.Open + ticker.Close = p.Close + ticker.LastUpdated = p.LastUpdated + ids = ticker.Assoc + ids = append(ids, ticker.Main) + } + s.Unlock() + return s.mux.Publish(ids, p) +} + +// SetItemID retrieves and sets dispatch mux publish IDs +func (s *Service) SetItemID(p *Price) error { + if p == nil { + return errors.New(errTickerPriceIsNil) + } + + ids, err := s.GetAssociations(p) if err != nil { - CreateNewTicker(exchangeName, tickerNew, assetType) - return nil + return err + } + singleID, err := s.mux.GetID() + if err != nil { + return err } - if BaseCurrencyExists(exchangeName, tickerNew.Pair.Base) { - m.Lock() - a := make(map[string]Price) - a[assetType] = *tickerNew - ticker.Price[tickerNew.Pair.Base.Upper().String()][tickerNew.Pair.Quote.Upper().String()] = a - m.Unlock() - return nil - } - - m.Lock() - a := make(map[string]map[string]Price) - b := make(map[string]Price) - b[assetType] = *tickerNew - a[tickerNew.Pair.Quote.Upper().String()] = b - ticker.Price[tickerNew.Pair.Base.Upper().String()] = a - m.Unlock() + s.Tickers[p.ExchangeName][p.Pair.Base.Item][p.Pair.Quote.Item][p.AssetType] = &Ticker{Price: *p, + Main: singleID, + Assoc: ids} return nil } + +// GetAssociations links a singular book with it's dispatch associations +func (s *Service) GetAssociations(p *Price) ([]uuid.UUID, error) { + if p == nil || *p == (Price{}) { + return nil, errors.New(errTickerPriceIsNil) + } + var ids []uuid.UUID + exchangeID, ok := s.Exchange[p.ExchangeName] + if !ok { + var err error + exchangeID, err = s.mux.GetID() + if err != nil { + return nil, err + } + s.Exchange[p.ExchangeName] = exchangeID + } + + ids = append(ids, exchangeID) + return ids, nil +} diff --git a/exchanges/ticker/ticker_test.go b/exchanges/ticker/ticker_test.go index 0781bcb2..328e13f1 100644 --- a/exchanges/ticker/ticker_test.go +++ b/exchanges/ticker/ticker_test.go @@ -1,55 +1,94 @@ package ticker import ( + "log" "math/rand" - "reflect" + "os" "strconv" "sync" "testing" "time" "github.com/thrasher-corp/gocryptotrader/currency" - log "github.com/thrasher-corp/gocryptotrader/logger" + "github.com/thrasher-corp/gocryptotrader/dispatch" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) -func TestPriceToString(t *testing.T) { - newPair := currency.NewPairFromStrings("BTC", "USD") - priceStruct := Price{ - Pair: newPair, - Last: 1200, - High: 1298, - Low: 1148, - Bid: 1195, - Ask: 1220, - Volume: 5, - PriceATH: 1337, +func TestMain(m *testing.M) { + err := dispatch.Start(1, dispatch.DefaultJobsLimit) + if err != nil { + log.Fatal(err) } - newTicker := CreateNewTicker("ANX", &priceStruct, Spot) + cpyMux = service.mux - if newTicker.PriceToString(newPair, "last", Spot) != "1200" { - t.Error("Test Failed - ticker PriceToString last value is incorrect") + os.Exit(m.Run()) +} + +var cpyMux *dispatch.Mux + +func TestSubscribeTicker(t *testing.T) { + _, err := SubscribeTicker("", currency.Pair{}, asset.Item("")) + if err == nil { + t.Error("error cannot be nil") } - if newTicker.PriceToString(newPair, "high", Spot) != "1298" { - t.Error("Test Failed - ticker PriceToString high value is incorrect") + + p := currency.NewPair(currency.BTC, currency.USD) + + // force error + service.mux = nil + err = ProcessTicker("subscribetest", &Price{Pair: p}, asset.Spot) + if err == nil { + t.Error("error cannot be nil") } - if newTicker.PriceToString(newPair, "low", Spot) != "1148" { - t.Error("Test Failed - ticker PriceToString low value is incorrect") + + sillyP := p + sillyP.Base = currency.GALA_NEO + err = ProcessTicker("subscribetest", &Price{Pair: sillyP}, asset.Spot) + if err == nil { + t.Error("error cannot be nil") } - if newTicker.PriceToString(newPair, "bid", Spot) != "1195" { - t.Error("Test Failed - ticker PriceToString bid value is incorrect") + + sillyP.Quote = currency.AAA + err = ProcessTicker("subscribetest", &Price{Pair: sillyP}, asset.Spot) + if err == nil { + t.Error("error cannot be nil") } - if newTicker.PriceToString(newPair, "ask", Spot) != "1220" { - t.Error("Test Failed - ticker PriceToString ask value is incorrect") + + err = ProcessTicker("subscribetest", &Price{Pair: sillyP}, "silly") + if err == nil { + t.Error("error cannot be nil") } - if newTicker.PriceToString(newPair, "volume", Spot) != "5" { - t.Error("Test Failed - ticker PriceToString volume value is incorrect") + // reinstate mux + service.mux = cpyMux + + err = ProcessTicker("subscribetest", &Price{Pair: p}, asset.Spot) + if err != nil { + t.Error("error cannot be nil") } - if newTicker.PriceToString(newPair, "ath", Spot) != "1337" { - t.Error("Test Failed - ticker PriceToString ath value is incorrect") + + _, err = SubscribeTicker("subscribetest", p, asset.Spot) + if err != nil { + t.Error("cannot subscribe to ticker", err) } - if newTicker.PriceToString(newPair, "obtuse", Spot) != "" { - t.Error("Test Failed - ticker PriceToString obtuse value is incorrect") +} + +func TestSubscribeToExchangeTickers(t *testing.T) { + _, err := SubscribeToExchangeTickers("") + if err == nil { + t.Error("error cannot be nil") + } + + p := currency.NewPair(currency.BTC, currency.USD) + + err = ProcessTicker("subscribeExchangeTest", &Price{Pair: p}, asset.Spot) + if err != nil { + t.Error(err) + } + + _, err = SubscribeToExchangeTickers("subscribeExchangeTest") + if err != nil { + t.Error("error cannot be nil", err) } } @@ -66,187 +105,70 @@ func TestGetTicker(t *testing.T) { PriceATH: 1337, } - err := ProcessTicker("bitfinex", &priceStruct, Spot) + err := ProcessTicker("bitfinex", &priceStruct, asset.Spot) if err != nil { - t.Fatal("Test failed. ProcessTicker error", err) + t.Fatal("ProcessTicker error", err) } - tickerPrice, err := GetTicker("bitfinex", newPair, Spot) + tickerPrice, err := GetTicker("bitfinex", newPair, asset.Spot) if err != nil { - t.Errorf("Test Failed - Ticker GetTicker init error: %s", err) + t.Errorf("Ticker GetTicker init error: %s", err) } if !tickerPrice.Pair.Equal(newPair) { - t.Error("Test Failed - ticker tickerPrice.CurrencyPair value is incorrect") + t.Error("ticker tickerPrice.CurrencyPair value is incorrect") } - _, err = GetTicker("blah", newPair, Spot) + _, err = GetTicker("blah", newPair, asset.Spot) if err == nil { - t.Fatal("Test Failed. TestGetTicker returned nil error on invalid exchange") + t.Fatal("TestGetTicker returned nil error on invalid exchange") } newPair.Base = currency.ETH - _, err = GetTicker("bitfinex", newPair, Spot) + _, err = GetTicker("bitfinex", newPair, asset.Spot) if err == nil { - t.Fatal("Test Failed. TestGetTicker returned ticker for invalid first currency") + t.Fatal("TestGetTicker returned ticker for invalid first currency") } btcltcPair := currency.NewPairFromStrings("BTC", "LTC") - _, err = GetTicker("bitfinex", btcltcPair, Spot) + _, err = GetTicker("bitfinex", btcltcPair, asset.Spot) if err == nil { - t.Fatal("Test Failed. TestGetTicker returned ticker for invalid second currency") + t.Fatal("TestGetTicker returned ticker for invalid second currency") } priceStruct.PriceATH = 9001 priceStruct.Pair.Base = currency.ETH err = ProcessTicker("bitfinex", &priceStruct, "futures_3m") if err != nil { - t.Fatal("Test failed. ProcessTicker error", err) + t.Fatal("ProcessTicker error", err) } tickerPrice, err = GetTicker("bitfinex", newPair, "futures_3m") if err != nil { - t.Errorf("Test Failed - Ticker GetTicker init error: %s", err) + t.Errorf("Ticker GetTicker init error: %s", err) } if tickerPrice.PriceATH != 9001 { - t.Error("Test Failed - ticker tickerPrice.PriceATH value is incorrect") - } -} - -func TestGetTickerByExchange(t *testing.T) { - newPair := currency.NewPairFromStrings("BTC", "USD") - priceStruct := Price{ - Pair: newPair, - Last: 1200, - High: 1298, - Low: 1148, - Bid: 1195, - Ask: 1220, - Volume: 5, - PriceATH: 1337, + t.Error("ticker tickerPrice.PriceATH value is incorrect") } - anxTicker := CreateNewTicker("ANX", &priceStruct, Spot) - Tickers = append(Tickers, anxTicker) + _, err = GetTicker("bitfinex", newPair, "meowCats") + if err == nil { + t.Error("Ticker GetTicker error cannot be nil") + } - tickerPtr, err := GetTickerByExchange("ANX") + err = ProcessTicker("bitfinex", &priceStruct, "meowCats") if err != nil { - t.Errorf("Test Failed - GetTickerByExchange init error: %s", err) - } - if tickerPtr.ExchangeName != "ANX" { - t.Error("Test Failed - GetTickerByExchange ExchangeName value is incorrect") - } -} - -func TestBaseCurrencyExists(t *testing.T) { - newPair := currency.NewPairFromStrings("BTC", "USD") - priceStruct := Price{ - Pair: newPair, - Last: 1200, - High: 1298, - Low: 1148, - Bid: 1195, - Ask: 1220, - Volume: 5, - PriceATH: 1337, + t.Fatal("ProcessTicker error", err) } - alphaTicker := CreateNewTicker("alphapoint", &priceStruct, Spot) - Tickers = append(Tickers, alphaTicker) - - if !BaseCurrencyExists("alphapoint", currency.BTC) { - t.Error("Test Failed - BaseCurrencyExists1 value return is incorrect") - } - if BaseCurrencyExists("alphapoint", currency.NewCode("CATS")) { - t.Error("Test Failed - BaseCurrencyExists2 value return is incorrect") - } -} - -func TestQuoteCurrencyExists(t *testing.T) { - t.Parallel() - - newPair := currency.NewPairFromStrings("BTC", "USD") - priceStruct := Price{ - Pair: newPair, - Last: 1200, - High: 1298, - Low: 1148, - Bid: 1195, - Ask: 1220, - Volume: 5, - PriceATH: 1337, - } - - bitstampTicker := CreateNewTicker("bitstamp", &priceStruct, "SPOT") - Tickers = append(Tickers, bitstampTicker) - - if !QuoteCurrencyExists("bitstamp", newPair) { - t.Error("Test Failed - QuoteCurrencyExists1 value return is incorrect") - } - - newPair.Quote = currency.NewCode("DOGS") - if QuoteCurrencyExists("bitstamp", newPair) { - t.Error("Test Failed - QuoteCurrencyExists2 value return is incorrect") - } -} - -func TestCreateNewTicker(t *testing.T) { - const float64Type = "float64" - newPair := currency.NewPairFromStrings("BTC", "USD") - priceStruct := Price{ - Pair: newPair, - Last: 1200, - High: 1298, - Low: 1148, - Bid: 1195, - Ask: 1220, - Volume: 5, - PriceATH: 1337, - } - - newTicker := CreateNewTicker("ANX", &priceStruct, Spot) - - if reflect.ValueOf(newTicker).NumField() != 2 { - t.Error("Test Failed - ticker CreateNewTicker struct change/or updated") - } - if reflect.TypeOf(newTicker.ExchangeName).String() != "string" { - t.Error("Test Failed - ticker CreateNewTicker.ExchangeName value is not a string") - } - if newTicker.ExchangeName != "ANX" { - t.Error("Test Failed - ticker CreateNewTicker.ExchangeName value is not ANX") - } - - if !newTicker.Price[currency.BTC.Upper().String()][currency.USD.Upper().String()][Spot].Pair.Equal(newPair) { - t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Pair.Pair().String() value is not expected 'BTCUSD'") - } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].Ask).String() != float64Type { - t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Ask value is not a float64") - } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].Bid).String() != float64Type { - t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Bid value is not a float64") - } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].Pair).String() != "currency.Pair" { - t.Error("Test Failed - ticker newTicker.Price[BTC][USD].CurrencyPair value is not a currency.Pair") - } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].High).String() != float64Type { - t.Error("Test Failed - ticker newTicker.Price[BTC][USD].High value is not a float64") - } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].Last).String() != float64Type { - t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Last value is not a float64") - } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].Low).String() != float64Type { - t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Low value is not a float64") - } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].PriceATH).String() != float64Type { - t.Error("Test Failed - ticker newTicker.Price[BTC][USD].PriceATH value is not a float64") - } - if reflect.TypeOf(newTicker.Price["BTC"]["USD"][Spot].Volume).String() != float64Type { - t.Error("Test Failed - ticker newTicker.Price[BTC][USD].Volume value is not a float64") + // process update again + err = ProcessTicker("bitfinex", &priceStruct, "meowCats") + if err != nil { + t.Fatal("ProcessTicker error", err) } } func TestProcessTicker(t *testing.T) { // non-appending function to tickers - Tickers = []Ticker{} exchName := "bitstamp" newPair := currency.NewPairFromStrings("BTC", "USD") priceStruct := Price{ @@ -259,8 +181,13 @@ func TestProcessTicker(t *testing.T) { // non-appending function to tickers PriceATH: 1337, } + err := ProcessTicker("", &priceStruct, asset.Spot) + if err == nil { + t.Fatal("empty exchange should throw an err") + } + // test for empty pair - err := ProcessTicker(exchName, &priceStruct, Spot) + err = ProcessTicker(exchName, &priceStruct, asset.Spot) if err == nil { t.Fatal("empty pair should throw an err") } @@ -269,52 +196,52 @@ func TestProcessTicker(t *testing.T) { // non-appending function to tickers priceStruct.Pair = newPair err = ProcessTicker(exchName, &priceStruct, "") if err == nil { - t.Fatal("Test failed. ProcessTicker error cannot be nil") + t.Fatal("ProcessTicker error cannot be nil") } // now process a valid ticker - err = ProcessTicker(exchName, &priceStruct, Spot) + err = ProcessTicker(exchName, &priceStruct, asset.Spot) if err != nil { - t.Fatal("Test failed. ProcessTicker error", err) + t.Fatal("ProcessTicker error", err) } - result, err := GetTicker(exchName, newPair, Spot) + result, err := GetTicker(exchName, newPair, asset.Spot) if err != nil { - t.Fatal("Test failed. TestProcessTicker failed to create and return a new ticker") + t.Fatal("TestProcessTicker failed to create and return a new ticker") } if !result.Pair.Equal(newPair) { - t.Fatal("Test failed. TestProcessTicker pair mismatch") + t.Fatal("TestProcessTicker pair mismatch") } // now test for processing a pair with a different quote currency newPair = currency.NewPairFromStrings("BTC", "AUD") priceStruct.Pair = newPair - err = ProcessTicker(exchName, &priceStruct, Spot) + err = ProcessTicker(exchName, &priceStruct, asset.Spot) if err != nil { - t.Fatal("Test failed. ProcessTicker error", err) + t.Fatal("ProcessTicker error", err) } - result, err = GetTicker(exchName, newPair, Spot) + result, err = GetTicker(exchName, newPair, asset.Spot) if err != nil { - t.Fatal("Test failed. TestProcessTicker failed to create and return a new ticker") + t.Fatal("TestProcessTicker failed to create and return a new ticker") } - result, err = GetTicker(exchName, newPair, Spot) + result, err = GetTicker(exchName, newPair, asset.Spot) if err != nil { - t.Fatal("Test failed. TestProcessTicker failed to return an existing ticker") + t.Fatal("TestProcessTicker failed to return an existing ticker") } // now test for processing a pair which has a different base currency newPair = currency.NewPairFromStrings("LTC", "AUD") priceStruct.Pair = newPair - err = ProcessTicker(exchName, &priceStruct, Spot) + err = ProcessTicker(exchName, &priceStruct, asset.Spot) if err != nil { - t.Fatal("Test failed. ProcessTicker error", err) + t.Fatal("ProcessTicker error", err) } - result, err = GetTicker(exchName, newPair, Spot) + result, err = GetTicker(exchName, newPair, asset.Spot) if err != nil { - t.Fatal("Test failed. TestProcessTicker failed to create and return a new ticker") + t.Fatal("TestProcessTicker failed to create and return a new ticker") } - result, err = GetTicker(exchName, newPair, Spot) + result, err = GetTicker(exchName, newPair, asset.Spot) if err != nil { - t.Fatal("Test failed. TestProcessTicker failed to return an existing ticker") + t.Fatal("TestProcessTicker failed to return an existing ticker") } type quick struct { @@ -348,9 +275,9 @@ func TestProcessTicker(t *testing.T) { // non-appending function to tickers } sm.Lock() - err = ProcessTicker(newName, &tp, Spot) + err = ProcessTicker(newName, &tp, asset.Spot) if err != nil { - log.Error(err) + t.Error(err) catastrophicFailure = true return } @@ -362,7 +289,7 @@ func TestProcessTicker(t *testing.T) { // non-appending function to tickers } if catastrophicFailure { - t.Fatal("Test failed. ProcessTicker error") + t.Fatal("ProcessTicker error") } wg.Wait() @@ -371,22 +298,62 @@ func TestProcessTicker(t *testing.T) { // non-appending function to tickers wg.Add(1) fatalErr := false go func(test quick) { - result, err := GetTicker(test.Name, test.P, Spot) + result, err := GetTicker(test.Name, test.P, asset.Spot) if err != nil { fatalErr = true return } if result.Last != test.TP.Last { - t.Error("Test failed. TestProcessTicker failed bad values") + t.Error("TestProcessTicker failed bad values") } wg.Done() }(test) if fatalErr { - t.Fatal("Test failed. TestProcessTicker failed to retrieve new ticker") + t.Fatal("TestProcessTicker failed to retrieve new ticker") } } wg.Wait() } + +func TestSetItemID(t *testing.T) { + err := service.SetItemID(nil) + if err == nil { + t.Error("error cannot be nil") + } + + err = service.SetItemID(&Price{}) + if err == nil { + t.Error("error cannot be nil") + } + + p := currency.NewPair(currency.CYC, currency.CYG) + + service.mux = nil + err = service.SetItemID(&Price{Pair: p, ExchangeName: "SetItemID"}) + if err == nil { + t.Error("error cannot be nil") + } + + service.mux = cpyMux +} + +func TestGetAssociation(t *testing.T) { + _, err := service.GetAssociations(nil) + if err == nil { + t.Error("error cannot be nil") + } + + p := currency.NewPair(currency.CYC, currency.CYG) + + service.mux = nil + + _, err = service.GetAssociations(&Price{Pair: p, ExchangeName: "GetAssociation"}) + if err == nil { + t.Error("error cannot be nil") + } + + service.mux = cpyMux +} diff --git a/exchanges/ticker/ticker_types.go b/exchanges/ticker/ticker_types.go new file mode 100644 index 00000000..ba37ed11 --- /dev/null +++ b/exchanges/ticker/ticker_types.go @@ -0,0 +1,57 @@ +package ticker + +import ( + "sync" + "time" + + "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/dispatch" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +// const values for the ticker package +const ( + errExchangeNameUnset = "ticker exchange name not set" + errPairNotSet = "ticker currency pair not set" + errAssetTypeNotSet = "ticker asset type not set" + errTickerPriceIsNil = "ticker price is nil" +) + +// Vars for the ticker package +var ( + service *Service +) + +// Service holds ticker information for each individual exchange +type Service struct { + Tickers map[string]map[*currency.Item]map[*currency.Item]map[asset.Item]*Ticker + Exchange map[string]uuid.UUID + mux *dispatch.Mux + sync.RWMutex +} + +// Price struct stores the currency pair and pricing information +type Price struct { + Last float64 `json:"Last"` + High float64 `json:"High"` + Low float64 `json:"Low"` + Bid float64 `json:"Bid"` + Ask float64 `json:"Ask"` + Volume float64 `json:"Volume"` + QuoteVolume float64 `json:"QuoteVolume"` + PriceATH float64 `json:"PriceATH"` + Open float64 `json:"Open"` + Close float64 `json:"Close"` + Pair currency.Pair `json:"Pair"` + ExchangeName string `json:"exchangeName"` + AssetType asset.Item `json:"assetType"` + LastUpdated time.Time +} + +// Ticker struct holds the ticker information for a currency pair and type +type Ticker struct { + Price + Main uuid.UUID + Assoc []uuid.UUID +} diff --git a/exchanges/websocket/wshandler/wshandler.go b/exchanges/websocket/wshandler/wshandler.go index d0a9166a..be0ee3c2 100644 --- a/exchanges/websocket/wshandler/wshandler.go +++ b/exchanges/websocket/wshandler/wshandler.go @@ -4,9 +4,11 @@ import ( "bytes" "compress/flate" "compress/gzip" + "encoding/json" "errors" "fmt" "io/ioutil" + "net" "net/http" "net/url" "strings" @@ -14,7 +16,6 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -31,41 +32,29 @@ func New() *Websocket { } // Setup sets main variables for websocket connection -func (w *Websocket) Setup(connector func() error, - subscriber func(channelToSubscribe WebsocketChannelSubscription) error, - unsubscriber func(channelToUnsubscribe WebsocketChannelSubscription) error, - exchangeName string, - wsEnabled, - verbose bool, - defaultURL, - runningURL string, - authenticatedWebsocketAPISupport bool) error { +func (w *Websocket) Setup(setupData *WebsocketSetup) error { w.DataHandler = make(chan interface{}, 1) - w.Connected = make(chan struct{}, 1) - w.Disconnected = make(chan struct{}, 1) w.TrafficAlert = make(chan struct{}, 1) - w.verbose = verbose - - w.SetChannelSubscriber(subscriber) - w.SetChannelUnsubscriber(unsubscriber) - err := w.SetWsStatusAndConnection(wsEnabled) + w.verbose = setupData.Verbose + w.SetChannelSubscriber(setupData.Subscriber) + w.SetChannelUnsubscriber(setupData.UnSubscriber) + w.enabled = setupData.Enabled + w.SetDefaultURL(setupData.DefaultURL) + w.SetConnector(setupData.Connector) + w.SetWebsocketURL(setupData.RunningURL) + w.SetExchangeName(setupData.ExchangeName) + w.SetCanUseAuthenticatedEndpoints(setupData.AuthenticatedWebsocketAPISupport) + w.trafficTimeout = setupData.WebsocketTimeout + w.features = setupData.Features + err := w.Initialise() if err != nil { return err } - w.SetDefaultURL(defaultURL) - w.SetConnector(connector) - w.SetWebsocketURL(runningURL) - w.SetExchangeName(exchangeName) - w.SetCanUseAuthenticatedEndpoints(authenticatedWebsocketAPISupport) - - w.init = false - w.noConnectionCheckLimit = 5 - w.reconnectionLimit = 10 return nil } -// Connect intiates a websocket connection by using a package defined connection +// Connect initiates a websocket connection by using a package defined connection // function func (w *Websocket) Connect() error { w.m.Lock() @@ -74,122 +63,121 @@ func (w *Websocket) Connect() error { if !w.IsEnabled() { return errors.New(WebsocketNotEnabled) } - - if w.connected { - w.connecting = false - return errors.New("exchange_websocket.go error - already connected, cannot connect again") + if w.IsConnecting() { + return fmt.Errorf("%v Websocket already attempting to connect", + w.exchangeName) } - - w.connecting = true + if w.IsConnected() { + return fmt.Errorf("%v Websocket already connected", + w.exchangeName) + } + w.setConnectingStatus(true) w.ShutdownC = make(chan struct{}, 1) + w.ReadMessageErrors = make(chan error, 1) err := w.connector() if err != nil { - w.connecting = false - return fmt.Errorf("exchange_websocket.go connection error %s", - err) + w.setConnectingStatus(false) + return fmt.Errorf("%v Error connecting %s", + w.exchangeName, err) } - if !w.connected { - w.Connected <- struct{}{} - w.connected = true - w.connecting = false - } + w.setConnectedStatus(true) + w.setConnectingStatus(false) + w.setInit(true) var anotherWG sync.WaitGroup anotherWG.Add(1) go w.trafficMonitor(&anotherWG) anotherWG.Wait() - if !w.connectionMonitorRunning { + if !w.IsConnectionMonitorRunning() { go w.connectionMonitor() } - go w.manageSubscriptions() + if w.features.Subscribe || w.features.Unsubscribe { + w.Wg.Add(1) + go w.manageSubscriptions() + } return nil } // connectionMonitor ensures that the WS keeps connecting func (w *Websocket) connectionMonitor() { - w.m.Lock() - w.connectionMonitorRunning = true - w.m.Unlock() + if w.IsConnectionMonitorRunning() { + return + } + w.setConnectionMonitorRunning(true) + timer := time.NewTimer(connectionMonitorDelay) + defer func() { - w.connectionMonitorRunning = false + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + w.setConnectionMonitorRunning(false) + if w.verbose { + log.Debugf(log.WebsocketMgr, "%v websocket connection monitor exiting", + w.exchangeName) + } }() for { - time.Sleep(connectionMonitorDelay) - w.m.Lock() - if !w.enabled { - w.m.Unlock() - w.DataHandler <- fmt.Errorf("%v connectionMonitor: websocket disabled, shutting down", w.exchangeName) - err := w.Shutdown() - if err != nil { - log.Error(err) + if w.verbose { + log.Debugf(log.WebsocketMgr, "%v running connection monitor cycle", + w.exchangeName) + } + if !w.IsEnabled() { + if w.verbose { + log.Debugf(log.WebsocketMgr, "%v connectionMonitor: websocket disabled, shutting down", w.exchangeName) + } + if w.IsConnected() { + err := w.Shutdown() + if err != nil { + log.Error(log.WebsocketMgr, err) + } } if w.verbose { - log.Debugf("%v connectionMonitor exiting", w.exchangeName) + log.Debugf(log.WebsocketMgr, "%v websocket connection monitor exiting", + w.exchangeName) } return } - w.m.Unlock() - err := w.checkConnection() - if err != nil { - log.Error(err) - } - } -} - -// checkConnection ensures the connection is maintained -// Will reconnect on disconnect -func (w *Websocket) checkConnection() error { - if w.verbose { - log.Debugf("%v checking connection", w.exchangeName) - } - switch { - case !w.IsConnected() && !w.IsConnecting(): - w.m.Lock() - defer w.m.Unlock() - if w.verbose { - log.Debugf("%v no connection. Attempt %v/%v", w.exchangeName, w.noConnectionChecks, w.noConnectionCheckLimit) - } - if w.noConnectionChecks >= w.noConnectionCheckLimit { - if w.verbose { - log.Debugf("%v resetting connection", w.exchangeName) + select { + case err := <-w.ReadMessageErrors: + // check if this error is a disconnection error + if isDisconnectionError(err) { + w.setConnectedStatus(false) + w.setConnectingStatus(false) + w.setInit(false) + if w.verbose { + log.Debugf(log.WebsocketMgr, "%v websocket has been disconnected. Reason: %v", + w.exchangeName, err) + } + err = w.Connect() + if err != nil { + log.Error(log.WebsocketMgr, err) + } + } else { + // pass off non disconnect errors to datahandler to manage + w.DataHandler <- err } - w.connecting = true - go w.WebsocketReset() - w.noConnectionChecks = 0 + case <-timer.C: + if !w.IsConnecting() && !w.IsConnected() { + err := w.Connect() + if err != nil { + log.Error(log.WebsocketMgr, err) + } + } + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer.Reset(connectionMonitorDelay) } - w.noConnectionChecks++ - case w.IsConnecting(): - if w.reconnectionChecks >= w.reconnectionLimit { - return fmt.Errorf("%v websocket failed to reconnect after %v seconds", - w.exchangeName, - w.reconnectionLimit*int(connectionMonitorDelay.Seconds())) - } - if w.verbose { - log.Debugf("%v Busy reconnecting", w.exchangeName) - } - w.reconnectionChecks++ - default: - w.noConnectionChecks = 0 - w.reconnectionChecks = 0 } - return nil -} - -// IsConnected exposes websocket connection status -func (w *Websocket) IsConnected() bool { - w.m.Lock() - defer w.m.Unlock() - return w.connected -} - -// IsConnecting checks whether websocket is busy connecting -func (w *Websocket) IsConnecting() bool { - w.m.Lock() - defer w.m.Unlock() - return w.connecting } // Shutdown attempts to shut down a websocket connection and associated routines @@ -200,124 +188,156 @@ func (w *Websocket) Shutdown() error { w.Orderbook.FlushCache() w.m.Unlock() }() - if !w.connected && w.ShutdownC == nil { + if !w.IsConnected() { return fmt.Errorf("%v cannot shutdown a disconnected websocket", w.exchangeName) } if w.verbose { - log.Debugf("%v shutting down websocket channels", w.exchangeName) + log.Debugf(log.WebsocketMgr, "%v shutting down websocket channels", w.exchangeName) } - timer := time.NewTimer(15 * time.Second) - c := make(chan struct{}, 1) - - go func(c chan struct{}) { - close(w.ShutdownC) - w.Wg.Wait() - if w.verbose { - log.Debugf("%v completed websocket channel shutdown", w.exchangeName) - } - c <- struct{}{} - }(c) - - select { - case <-c: - w.connected = false - return nil - case <-timer.C: - return fmt.Errorf("%s websocket routines failed to shutdown after 15 seconds", - w.GetName()) + close(w.ShutdownC) + w.Wg.Wait() + w.setConnectedStatus(false) + w.setConnectingStatus(false) + if w.verbose { + log.Debugf(log.WebsocketMgr, "%v completed websocket channel shutdown", w.exchangeName) } + return nil } -// WebsocketReset sends the shutdown command, waits for channel/func closure and then reconnects -func (w *Websocket) WebsocketReset() { - err := w.Shutdown() - if err != nil { - // does not return here to allow connection to be made if already shut down - w.DataHandler <- fmt.Errorf("%v shutdown error: %v", w.exchangeName, err) - } - log.Infof("%v reconnecting to websocket", w.exchangeName) - w.m.Lock() - w.init = true - w.m.Unlock() - err = w.Connect() - if err != nil { - w.DataHandler <- fmt.Errorf("%v connection error: %v", w.exchangeName, err) - } -} - -// trafficMonitor monitors traffic and switches connection modes for websocket +// trafficMonitor uses a timer of WebsocketTrafficLimitTime and once it expires +// Will reconnect if the TrafficAlert channel has not received any data +// The trafficTimer will reset on each traffic alert func (w *Websocket) trafficMonitor(wg *sync.WaitGroup) { w.Wg.Add(1) - wg.Done() // Makes sure we are unlocking after we add to waitgroup + wg.Done() + trafficTimer := time.NewTimer(w.trafficTimeout) defer func() { - if w.connected { - w.Disconnected <- struct{}{} + if !trafficTimer.Stop() { + select { + case <-trafficTimer.C: + default: + } } + w.setTrafficMonitorRunning(false) w.Wg.Done() }() - - // Define an initial traffic timer which will be a delay then fall over to - // WebsocketTrafficLimitTime after first response - trafficTimer := time.NewTimer(5 * time.Second) + if w.IsTrafficMonitorRunning() { + return + } + w.setTrafficMonitorRunning(true) for { select { - case <-w.ShutdownC: // Returns on shutdown channel close + case <-w.ShutdownC: if w.verbose { - log.Debugf("%v trafficMonitor shutdown message received", w.exchangeName) + log.Debugf(log.WebsocketMgr, "%v trafficMonitor shutdown message received", w.exchangeName) } return - case <-w.TrafficAlert: // Resets timer on traffic - w.m.Lock() - if !w.connected { - w.Connected <- struct{}{} - w.connected = true + case <-w.TrafficAlert: + if !trafficTimer.Stop() { + select { + case <-trafficTimer.C: + default: + } } - w.m.Unlock() - trafficTimer.Reset(WebsocketTrafficLimitTime) + trafficTimer.Reset(w.trafficTimeout) case <-trafficTimer.C: // Falls through when timer runs out - newtimer := time.NewTimer(10 * time.Second) // New secondary timer set if w.verbose { - log.Debugf("%v has not received a traffic alert in 5 seconds.", w.exchangeName) - } - w.m.Lock() - if w.connected { - // If connected divert traffic to rest - w.Disconnected <- struct{}{} - w.connected = false - } - w.m.Unlock() - - select { - case <-w.ShutdownC: // Returns on shutdown channel close - w.m.Lock() - w.connected = false - w.m.Unlock() - return - - case <-newtimer.C: // If secondary timer runs state timeout is sent to the data handler - if w.verbose { - log.Debugf("%v has not received a traffic alert in 15 seconds, exiting", w.exchangeName) - } - w.DataHandler <- fmt.Errorf("trafficMonitor %v", WebsocketStateTimeout) - return - - case <-w.TrafficAlert: // If in this time response traffic comes through - trafficTimer.Reset(WebsocketTrafficLimitTime) - w.m.Lock() - if !w.connected { - // If not connected dive rt traffic from REST to websocket - w.Connected <- struct{}{} - if w.verbose { - log.Debugf("%v has received a traffic alert. Setting status to connected", w.exchangeName) - } - w.connected = true - } - w.m.Unlock() + log.Warnf(log.WebsocketMgr, "%v has not received a traffic alert in %v. Reconnecting", w.exchangeName, w.trafficTimeout) } + go w.Shutdown() } } } +func (w *Websocket) setConnectedStatus(b bool) { + w.connectionMutex.Lock() + w.connected = b + w.connectionMutex.Unlock() +} + +// IsConnected returns status of connection +func (w *Websocket) IsConnected() bool { + w.connectionMutex.RLock() + defer w.connectionMutex.RUnlock() + return w.connected +} + +func (w *Websocket) setConnectingStatus(b bool) { + w.connectionMutex.Lock() + w.connecting = b + w.connectionMutex.Unlock() +} + +// IsConnecting returns status of connecting +func (w *Websocket) IsConnecting() bool { + w.connectionMutex.RLock() + defer w.connectionMutex.RUnlock() + return w.connecting +} + +func (w *Websocket) setEnabled(b bool) { + w.connectionMutex.Lock() + w.enabled = b + w.connectionMutex.Unlock() +} + +// IsEnabled returns status of enabled +func (w *Websocket) IsEnabled() bool { + w.connectionMutex.RLock() + defer w.connectionMutex.RUnlock() + return w.enabled +} + +func (w *Websocket) setInit(b bool) { + w.connectionMutex.Lock() + w.init = b + w.connectionMutex.Unlock() +} + +// IsInit returns status of init +func (w *Websocket) IsInit() bool { + w.connectionMutex.RLock() + defer w.connectionMutex.RUnlock() + return w.init +} + +func (w *Websocket) setTrafficMonitorRunning(b bool) { + w.connectionMutex.Lock() + w.trafficMonitorRunning = b + w.connectionMutex.Unlock() +} + +// IsTrafficMonitorRunning returns status of the traffic monitor +func (w *Websocket) IsTrafficMonitorRunning() bool { + w.connectionMutex.RLock() + defer w.connectionMutex.RUnlock() + return w.trafficMonitorRunning +} + +func (w *Websocket) setConnectionMonitorRunning(b bool) { + w.connectionMutex.Lock() + w.connectionMonitorRunning = b + w.connectionMutex.Unlock() +} + +// IsConnectionMonitorRunning returns status of connection monitor +func (w *Websocket) IsConnectionMonitorRunning() bool { + w.connectionMutex.RLock() + defer w.connectionMutex.RUnlock() + return w.connectionMonitorRunning +} + +// CanUseAuthenticatedWebsocketForWrapper Handles a common check to +// verify whether a wrapper can use an authenticated websocket endpoint +func (w *Websocket) CanUseAuthenticatedWebsocketForWrapper() bool { + if w.IsConnected() && w.CanUseAuthenticatedEndpoints() { + return true + } else if w.IsConnected() && !w.CanUseAuthenticatedEndpoints() { + log.Infof(log.WebsocketMgr, WebsocketNotAuthenticatedUsingRest, w.exchangeName) + } + return false +} + // SetWebsocketURL sets websocket URL func (w *Websocket) SetWebsocketURL(websocketURL string) { if websocketURL == "" || websocketURL == config.WebsocketURLNonDefaultMessage { @@ -332,55 +352,28 @@ func (w *Websocket) GetWebsocketURL() string { return w.runningURL } -// SetWsStatusAndConnection sets if websocket is enabled -// it will also connect/disconnect the websocket connection -func (w *Websocket) SetWsStatusAndConnection(enabled bool) error { - w.m.Lock() - if w.enabled == enabled { - if w.init { - w.m.Unlock() +// Initialise verifies status and connects +func (w *Websocket) Initialise() error { + if w.IsEnabled() { + if w.IsInit() { return nil } - w.m.Unlock() - return fmt.Errorf("exchange_websocket.go error - already set as %t", - enabled) + return fmt.Errorf("%v Websocket already initialised", + w.exchangeName) } - w.enabled = enabled - if !w.init { - if enabled { - if w.connected { - w.m.Unlock() - return nil - } - w.m.Unlock() - return w.Connect() - } - - if !w.connected { - w.m.Unlock() - return nil - } - w.m.Unlock() - return w.Shutdown() - } - w.m.Unlock() + w.setEnabled(w.enabled) return nil } -// IsEnabled returns bool -func (w *Websocket) IsEnabled() bool { - return w.enabled -} - // SetProxyAddress sets websocket proxy address func (w *Websocket) SetProxyAddress(proxyAddr string) error { if w.proxyAddr == proxyAddr { - return errors.New("exchange_websocket.go error - Setting proxy address - same address") + return fmt.Errorf("%v Cannot set proxy address to the same address '%v'", w.exchangeName, w.proxyAddr) } w.proxyAddr = proxyAddr - if !w.init && w.enabled { - if w.connected { + if !w.IsInit() && w.IsEnabled() { + if w.IsConnected() { err := w.Shutdown() if err != nil { return err @@ -421,87 +414,6 @@ func (w *Websocket) GetName() string { return w.exchangeName } -// GetFunctionality returns a functionality bitmask for the websocket -// connection -func (w *Websocket) GetFunctionality() uint32 { - return w.Functionality -} - -// SupportsFunctionality returns if the functionality is supported as a boolean -func (w *Websocket) SupportsFunctionality(f uint32) bool { - return w.GetFunctionality()&f == f -} - -// FormatFunctionality will return each of the websocket connection compatible -// stream methods as a string -func (w *Websocket) FormatFunctionality() string { - var functionality []string - for i := 0; i < 32; i++ { - var check uint32 = 1 << uint32(i) - if w.GetFunctionality()&check != 0 { - switch check { - case WebsocketTickerSupported: - functionality = append(functionality, WebsocketTickerSupportedText) - - case WebsocketOrderbookSupported: - functionality = append(functionality, WebsocketOrderbookSupportedText) - - case WebsocketKlineSupported: - functionality = append(functionality, WebsocketKlineSupportedText) - - case WebsocketTradeDataSupported: - functionality = append(functionality, WebsocketTradeDataSupportedText) - - case WebsocketAccountSupported: - functionality = append(functionality, WebsocketAccountSupportedText) - - case WebsocketAllowsRequests: - functionality = append(functionality, WebsocketAllowsRequestsText) - - case WebsocketSubscribeSupported: - functionality = append(functionality, WebsocketSubscribeSupportedText) - - case WebsocketUnsubscribeSupported: - functionality = append(functionality, WebsocketUnsubscribeSupportedText) - - case WebsocketAuthenticatedEndpointsSupported: - functionality = append(functionality, WebsocketAuthenticatedEndpointsSupportedText) - - case WebsocketAccountDataSupported: - functionality = append(functionality, WebsocketAccountDataSupportedText) - - case WebsocketSubmitOrderSupported: - functionality = append(functionality, WebsocketSubmitOrderSupportedText) - - case WebsocketCancelOrderSupported: - functionality = append(functionality, WebsocketCancelOrderSupportedText) - - case WebsocketWithdrawSupported: - functionality = append(functionality, WebsocketWithdrawSupportedText) - - case WebsocketMessageCorrelationSupported: - functionality = append(functionality, WebsocketMessageCorrelationSupportedText) - - case WebsocketSequenceNumberSupported: - functionality = append(functionality, WebsocketSequenceNumberSupportedText) - - case WebsocketDeadMansSwitchSupported: - functionality = append(functionality, WebsocketDeadMansSwitchSupportedText) - - default: - functionality = append(functionality, - fmt.Sprintf("%s[1<<%v]", UnknownWebsocketFunctionality, i)) - } - } - } - - if len(functionality) > 0 { - return strings.Join(functionality, " & ") - } - - return NoWebsocketSupportText -} - // SetChannelSubscriber sets the function to use the base subscribe func func (w *Websocket) SetChannelSubscriber(subscriber func(channelToSubscribe WebsocketChannelSubscription) error) { w.channelSubscriber = subscriber @@ -514,38 +426,46 @@ func (w *Websocket) SetChannelUnsubscriber(unsubscriber func(channelToUnsubscrib // ManageSubscriptions ensures the subscriptions specified continue to be subscribed to func (w *Websocket) manageSubscriptions() { - if !w.SupportsFunctionality(WebsocketSubscribeSupported) && !w.SupportsFunctionality(WebsocketUnsubscribeSupported) { + if !w.features.Subscribe && !w.features.Unsubscribe { w.DataHandler <- fmt.Errorf("%v does not support channel subscriptions, exiting ManageSubscriptions()", w.exchangeName) return } - w.Wg.Add(1) defer func() { if w.verbose { - log.Debugf("%v ManageSubscriptions exiting", w.exchangeName) + log.Debugf(log.WebsocketMgr, "%v ManageSubscriptions exiting", w.exchangeName) } w.Wg.Done() }() for { select { case <-w.ShutdownC: + w.subscriptionMutex.Lock() w.subscribedChannels = []WebsocketChannelSubscription{} + w.subscriptionMutex.Unlock() if w.verbose { - log.Debugf("%v shutdown manageSubscriptions", w.exchangeName) + log.Debugf(log.WebsocketMgr, "%v shutdown manageSubscriptions", w.exchangeName) } return default: time.Sleep(manageSubscriptionsDelay) + if !w.IsConnected() { + w.subscriptionMutex.Lock() + w.subscribedChannels = []WebsocketChannelSubscription{} + w.subscriptionMutex.Unlock() + + continue + } if w.verbose { - log.Debugf("%v checking subscriptions", w.exchangeName) + log.Debugf(log.WebsocketMgr, "%v checking subscriptions", w.exchangeName) } // Subscribe to channels Pending a subscription - if w.SupportsFunctionality(WebsocketSubscribeSupported) { - err := w.subscribeToChannels() + if w.features.Subscribe { + err := w.appendSubscribedChannels() if err != nil { w.DataHandler <- err } } - if w.SupportsFunctionality(WebsocketUnsubscribeSupported) { + if w.features.Unsubscribe { err := w.unsubscribeToChannels() if err != nil { w.DataHandler <- err @@ -555,11 +475,11 @@ func (w *Websocket) manageSubscriptions() { } } -// subscribeToChannels compares channelsToSubscribe to subscribedChannels +// appendSubscribedChannels compares channelsToSubscribe to subscribedChannels // and subscribes to any channels not present in subscribedChannels -func (w *Websocket) subscribeToChannels() error { - w.subscriptionLock.Lock() - defer w.subscriptionLock.Unlock() +func (w *Websocket) appendSubscribedChannels() error { + w.subscriptionMutex.Lock() + defer w.subscriptionMutex.Unlock() for i := 0; i < len(w.channelsToSubscribe); i++ { channelIsSubscribed := false for j := 0; j < len(w.subscribedChannels); j++ { @@ -570,7 +490,7 @@ func (w *Websocket) subscribeToChannels() error { } if !channelIsSubscribed { if w.verbose { - log.Debugf("%v Subscribing to %v %v", w.exchangeName, w.channelsToSubscribe[i].Channel, w.channelsToSubscribe[i].Currency.String()) + log.Debugf(log.WebsocketMgr, "%v Subscribing to %v %v", w.exchangeName, w.channelsToSubscribe[i].Channel, w.channelsToSubscribe[i].Currency.String()) } err := w.channelSubscriber(w.channelsToSubscribe[i]) if err != nil { @@ -585,8 +505,8 @@ func (w *Websocket) subscribeToChannels() error { // unsubscribeToChannels compares subscribedChannels to channelsToSubscribe // and unsubscribes to any channels not present in channelsToSubscribe func (w *Websocket) unsubscribeToChannels() error { - w.subscriptionLock.Lock() - defer w.subscriptionLock.Unlock() + w.subscriptionMutex.Lock() + defer w.subscriptionMutex.Unlock() for i := 0; i < len(w.subscribedChannels); i++ { subscriptionFound := false for j := 0; j < len(w.channelsToSubscribe); j++ { @@ -618,8 +538,8 @@ func (w *Websocket) RemoveSubscribedChannels(channels []WebsocketChannelSubscrip // removeChannelToSubscribe removes an entry from w.channelsToSubscribe // so an unsubscribe event can be triggered func (w *Websocket) removeChannelToSubscribe(subscribedChannel WebsocketChannelSubscription) { - w.subscriptionLock.Lock() - defer w.subscriptionLock.Unlock() + w.subscriptionMutex.Lock() + defer w.subscriptionMutex.Unlock() channelLength := len(w.channelsToSubscribe) i := 0 for j := 0; j < len(w.channelsToSubscribe); j++ { @@ -640,8 +560,8 @@ func (w *Websocket) removeChannelToSubscribe(subscribedChannel WebsocketChannelS // ResubscribeToChannel calls unsubscribe func and // removes it from subscribedChannels to trigger a subscribe event func (w *Websocket) ResubscribeToChannel(subscribedChannel WebsocketChannelSubscription) { - w.subscriptionLock.Lock() - defer w.subscriptionLock.Unlock() + w.subscriptionMutex.Lock() + defer w.subscriptionMutex.Unlock() err := w.channelUnsubscriber(subscribedChannel) if err != nil { w.DataHandler <- err @@ -671,7 +591,6 @@ func (w *Websocket) SubscribeToChannels(channels []WebsocketChannelSubscription) w.channelsToSubscribe = append(w.channelsToSubscribe, channels[i]) } } - w.noConnectionChecks = 0 } // Equal two WebsocketChannelSubscription to determine equality @@ -689,16 +608,16 @@ func (w *Websocket) GetSubscriptions() []WebsocketChannelSubscription { // SetCanUseAuthenticatedEndpoints sets canUseAuthenticatedEndpoints val in // a thread safe manner func (w *Websocket) SetCanUseAuthenticatedEndpoints(val bool) { - w.subscriptionLock.Lock() - defer w.subscriptionLock.Unlock() + w.subscriptionMutex.Lock() + defer w.subscriptionMutex.Unlock() w.canUseAuthenticatedEndpoints = val } // CanUseAuthenticatedEndpoints gets canUseAuthenticatedEndpoints val in // a thread safe manner func (w *Websocket) CanUseAuthenticatedEndpoints() bool { - w.subscriptionLock.Lock() - defer w.subscriptionLock.Unlock() + w.subscriptionMutex.Lock() + defer w.subscriptionMutex.Unlock() return w.canUseAuthenticatedEndpoints } @@ -721,7 +640,6 @@ func (w *WebsocketConnection) Dial(dialer *websocket.Dialer, headers http.Header } dialer.Proxy = http.ProxyURL(proxy) } - var err error var conStatus *http.Response w.Connection, conStatus, err = dialer.Dial(w.URL, headers) @@ -731,6 +649,10 @@ func (w *WebsocketConnection) Dial(dialer *websocket.Dialer, headers http.Header } return fmt.Errorf("%v Error: %v", w.URL, err) } + if w.Verbose { + log.Infof(log.WebsocketMgr, "%v Websocket connected to %s", w.ExchangeName, w.URL) + } + w.setConnectedStatus(true) return nil } @@ -738,12 +660,16 @@ func (w *WebsocketConnection) Dial(dialer *websocket.Dialer, headers http.Header func (w *WebsocketConnection) SendMessage(data interface{}) error { w.Lock() defer w.Unlock() - json, err := common.JSONEncode(data) + if !w.IsConnected() { + return fmt.Errorf("%v cannot send message to a disconnected websocket", w.ExchangeName) + } + json, err := json.Marshal(data) if err != nil { return err } if w.Verbose { - log.Debugf("%v sending message to websocket %v", w.ExchangeName, string(json)) + log.Debugf(log.WebsocketMgr, + "%v sending message to websocket %v", w.ExchangeName, string(json)) } if w.RateLimit > 0 { time.Sleep(time.Duration(w.RateLimit) * time.Millisecond) @@ -787,6 +713,12 @@ func (w *WebsocketConnection) WaitForResult(id int64, wg *sync.WaitGroup) { for k := range w.IDResponses { if k == id { w.Unlock() + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } return } } @@ -796,10 +728,26 @@ func (w *WebsocketConnection) WaitForResult(id int64, wg *sync.WaitGroup) { } } +func (w *WebsocketConnection) setConnectedStatus(b bool) { + w.connectionMutex.Lock() + w.connected = b + w.connectionMutex.Unlock() +} + +// IsConnected exposes websocket connection status +func (w *WebsocketConnection) IsConnected() bool { + w.connectionMutex.RLock() + defer w.connectionMutex.RUnlock() + return w.connected +} + // ReadMessage reads messages, can handle text, gzip and binary func (w *WebsocketConnection) ReadMessage() (WebsocketResponse, error) { mType, resp, err := w.Connection.ReadMessage() if err != nil { + if isDisconnectionError(err) { + w.setConnectedStatus(false) + } return WebsocketResponse{}, err } var standardMessage []byte @@ -813,7 +761,7 @@ func (w *WebsocketConnection) ReadMessage() (WebsocketResponse, error) { } } if w.Verbose { - log.Debugf("%v Websocket message received: %v", + log.Debugf(log.WebsocketMgr, "%v Websocket message received: %v", w.ExchangeName, string(standardMessage)) } @@ -861,3 +809,15 @@ func (w *WebsocketConnection) GenerateMessageID(useNano bool) int64 { } return time.Now().Unix() } + +// isDisconnectionError Determines if the error sent over chan ReadMessageErrors is a disconnection error +func isDisconnectionError(err error) bool { + if websocket.IsUnexpectedCloseError(err) { + return true + } + switch err.(type) { + case *websocket.CloseError, *net.OpError: + return true + } + return false +} diff --git a/exchanges/websocket/wshandler/wshandler_test.go b/exchanges/websocket/wshandler/wshandler_test.go index f3699a8b..bfb20816 100644 --- a/exchanges/websocket/wshandler/wshandler_test.go +++ b/exchanges/websocket/wshandler/wshandler_test.go @@ -1,149 +1,199 @@ package wshandler import ( - "fmt" + "bytes" + "compress/flate" + "compress/gzip" + "encoding/json" + "errors" + "net" + "net/http" + "os" "strings" + "sync" "testing" "time" + + "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" ) -var ws *Websocket +func TestTrafficMonitorTimeout(t *testing.T) { + ws := New() + err := ws.Setup( + &WebsocketSetup{ + Enabled: true, + AuthenticatedWebsocketAPISupport: true, + WebsocketTimeout: 10000, + DefaultURL: "testDefaultURL", + ExchangeName: "exchangeName", + RunningURL: "testRunningURL", + Connector: func() error { return nil }, + Subscriber: func(test WebsocketChannelSubscription) error { return nil }, + UnSubscriber: func(test WebsocketChannelSubscription) error { return nil }, + }) + if err != nil { + t.Error(err) + } + ws.setConnectedStatus(true) + ws.TrafficAlert = make(chan struct{}, 2) + ws.ShutdownC = make(chan struct{}) + var anotherWG sync.WaitGroup + anotherWG.Add(1) + go ws.trafficMonitor(&anotherWG) + anotherWG.Wait() + ws.TrafficAlert <- struct{}{} + trafficTimer := time.NewTimer(5 * time.Second) + select { + case <-trafficTimer.C: + t.Error("should be exiting") + default: + ws.Wg.Wait() + } +} -func TestWebsocketInit(t *testing.T) { - ws = New() - if ws == nil { - t.Error("test failed - Websocket New() error") +func TestIsDisconnectionError(t *testing.T) { + isADisconnectionError := isDisconnectionError(errors.New("errorText")) + if isADisconnectionError { + t.Error("Its not") + } + isADisconnectionError = isDisconnectionError(&websocket.CloseError{ + Code: 1006, + Text: "errorText", + }) + if !isADisconnectionError { + t.Error("It is") + } + + isADisconnectionError = isDisconnectionError(&net.OpError{ + Op: "", + Net: "", + Source: nil, + Addr: nil, + Err: errors.New("errorText"), + }) + if !isADisconnectionError { + t.Error("It is") + } +} + +func TestConnectionMessageErrors(t *testing.T) { + ws := New() + ws.connected = true + ws.enabled = true + ws.ReadMessageErrors = make(chan error) + ws.DataHandler = make(chan interface{}) + ws.ShutdownC = make(chan struct{}) + ws.connector = func() error { return nil } + ws.features = &protocol.Features{} + go ws.connectionMonitor() + timer := time.NewTimer(900 * time.Millisecond) + ws.ReadMessageErrors <- errors.New("errorText") + select { + case err := <-ws.DataHandler: + if err.(error).Error() != "errorText" { + t.Errorf("Expected 'errorText', received %v", err) + } + case <-timer.C: + t.Error("Timeout waiting for datahandler to receive error") + } + timer = time.NewTimer(900 * time.Millisecond) + ws.ReadMessageErrors <- &websocket.CloseError{ + Code: 1006, + Text: "errorText", + } +outer: + for { + select { + case <-ws.DataHandler: + t.Fatal("Error is a disconnection error") + case <-timer.C: + break outer + } } } func TestWebsocket(t *testing.T) { - if err := ws.SetProxyAddress("testProxy"); err != nil { - t.Error("test failed - SetProxyAddress", err) + ws := Websocket{} + ws.setInit(true) + err := ws.Setup(&WebsocketSetup{ + ExchangeName: "test", + Enabled: true, + }) + if err != nil && err.Error() != "test Websocket already initialised" { + t.Errorf("Expected 'test Websocket already initialised', received %v", err) } - ws.Setup(func() error { return nil }, - func(test WebsocketChannelSubscription) error { return nil }, - func(test WebsocketChannelSubscription) error { return nil }, - "testName", - true, - false, - "testDefaultURL", - "testRunningURL", - false) + ws = *New() + err = ws.SetProxyAddress("testProxy") + if err != nil { + t.Error("SetProxyAddress", err) + } - // Test variable setting and retreival - if ws.GetName() != "testName" { - t.Error("test failed - WebsocketSetup") + err = ws.Setup( + &WebsocketSetup{ + Enabled: true, + AuthenticatedWebsocketAPISupport: true, + WebsocketTimeout: 2, + DefaultURL: "testDefaultURL", + ExchangeName: "exchangeName", + RunningURL: "testRunningURL", + Connector: func() error { return nil }, + Subscriber: func(test WebsocketChannelSubscription) error { return nil }, + UnSubscriber: func(test WebsocketChannelSubscription) error { return nil }, + Features: &protocol.Features{}, + }) + if err != nil { + t.Error(err) + } + + if ws.GetName() != "exchangeName" { + t.Error("WebsocketSetup") } if !ws.IsEnabled() { - t.Error("test failed - WebsocketSetup") + t.Error("WebsocketSetup") } if ws.GetProxyAddress() != "testProxy" { - t.Error("test failed - WebsocketSetup") + t.Error("WebsocketSetup") } if ws.GetDefaultURL() != "testDefaultURL" { - t.Error("test failed - WebsocketSetup") + t.Error("WebsocketSetup") } if ws.GetWebsocketURL() != "testRunningURL" { - t.Error("test failed - WebsocketSetup") + t.Error("WebsocketSetup") } - // Test websocket connect and shutdown functions - comms := make(chan struct{}, 1) - go func() { - var count int - for { - if count == 4 { - close(comms) - return - } - select { - case <-ws.Connected: - count++ - case <-ws.Disconnected: - count++ - } - } - }() + if ws.trafficTimeout != time.Duration(2) { + t.Error("WebsocketSetup") + } // -- Not connected shutdown - err := ws.Shutdown() + err = ws.Shutdown() if err == nil { - t.Fatal("test failed - should not be connected to able to shut down") + t.Fatal("should not be connected to able to shut down") } ws.Wg.Wait() // -- Normal connect err = ws.Connect() if err != nil { - t.Fatal("test failed - WebsocketSetup", err) + t.Fatal("WebsocketSetup", err) } - + ws.SetWebsocketURL("ws://demos.kaazing.com/echo") // -- Already connected connect err = ws.Connect() if err == nil { - t.Fatal("test failed - should not connect, already connected") + t.Fatal("should not connect, already connected") } - - ws.SetWebsocketURL("") - - // -- Set true when already true - err = ws.SetWsStatusAndConnection(true) - if err == nil { - t.Fatal("test failed - setting enabled should not work") - } - - // -- Set false normal - err = ws.SetWsStatusAndConnection(false) - if err != nil { - t.Fatal("test failed - setting enabled should not work") - } - - // -- Set true normal - err = ws.SetWsStatusAndConnection(true) - if err != nil { - t.Fatal("test failed - setting enabled should not work") - } - // -- Normal shutdown err = ws.Shutdown() if err != nil { - t.Fatal("test failed - WebsocketSetup", err) - } - - timer := time.NewTimer(5 * time.Second) - select { - case <-comms: - case <-timer.C: - t.Fatal("test failed - WebsocketSetup - timeout") - } -} - -func TestFunctionality(t *testing.T) { - var w Websocket - - if w.FormatFunctionality() != NoWebsocketSupportText { - t.Fatalf("Test Failed - FormatFunctionality error expected %s but received %s", - NoWebsocketSupportText, w.FormatFunctionality()) - } - - w.Functionality = 1 << 31 - - if w.FormatFunctionality() != UnknownWebsocketFunctionality+"[1<<31]" { - t.Fatal("Test Failed - GetFunctionality error incorrect error returned") - } - - w.Functionality = WebsocketOrderbookSupported - - if w.GetFunctionality() != WebsocketOrderbookSupported { - t.Fatal("Test Failed - GetFunctionality error incorrect bitmask returned") - } - - if !w.SupportsFunctionality(WebsocketOrderbookSupported) { - t.Fatal("Test Failed - SupportsFunctionality error should be true") + t.Fatal("WebsocketSetup", err) } + ws.Wg.Wait() } // placeholderSubscriber basic function to test subscriptions @@ -162,12 +212,32 @@ func TestSubscribe(t *testing.T) { subscribedChannels: []WebsocketChannelSubscription{}, } w.SetChannelSubscriber(placeholderSubscriber) - w.subscribeToChannels() + err := w.appendSubscribedChannels() + if err != nil { + t.Error(err) + } if len(w.subscribedChannels) != 1 { t.Errorf("Subscription did not occur") } } +// TestSubscribe logic test +func TestSubscribeToChannels(t *testing.T) { + w := Websocket{ + channelsToSubscribe: []WebsocketChannelSubscription{ + { + Channel: "hello", + }, + }, + subscribedChannels: []WebsocketChannelSubscription{}, + } + w.SetChannelSubscriber(placeholderSubscriber) + w.SubscribeToChannels([]WebsocketChannelSubscription{{Channel: "hello"}, {Channel: "hello2"}}) + if len(w.channelsToSubscribe) != 2 { + t.Errorf("Subscription did not occur") + } +} + // TestUnsubscribe logic test func TestUnsubscribe(t *testing.T) { w := Websocket{ @@ -179,7 +249,10 @@ func TestUnsubscribe(t *testing.T) { }, } w.SetChannelUnsubscriber(placeholderSubscriber) - w.unsubscribeToChannels() + err := w.unsubscribeToChannels() + if err != nil { + t.Error(err) + } if len(w.subscribedChannels) != 0 { t.Errorf("Unsubscription did not occur") } @@ -200,7 +273,10 @@ func TestSubscriptionWithExistingEntry(t *testing.T) { }, } w.SetChannelSubscriber(placeholderSubscriber) - w.subscribeToChannels() + err := w.appendSubscribedChannels() + if err != nil { + t.Error(err) + } if len(w.subscribedChannels) != 1 { t.Errorf("Subscription should not have occurred") } @@ -221,7 +297,10 @@ func TestUnsubscriptionWithExistingEntry(t *testing.T) { }, } w.SetChannelUnsubscriber(placeholderSubscriber) - w.unsubscribeToChannels() + err := w.unsubscribeToChannels() + if err != nil { + t.Error(err) + } if len(w.subscribedChannels) != 1 { t.Errorf("Unsubscription should not have occurred") } @@ -230,67 +309,50 @@ func TestUnsubscriptionWithExistingEntry(t *testing.T) { // TestManageSubscriptionsStartStop logic test func TestManageSubscriptionsStartStop(t *testing.T) { w := Websocket{ - ShutdownC: make(chan struct{}, 1), - Functionality: WebsocketSubscribeSupported | WebsocketUnsubscribeSupported, + ShutdownC: make(chan struct{}), + features: &protocol.Features{Subscribe: true, Unsubscribe: true}, } + w.Wg.Add(1) go w.manageSubscriptions() - time.Sleep(time.Second) close(w.ShutdownC) + w.Wg.Wait() +} + +// TestManageSubscriptions logic test +func TestManageSubscriptions(t *testing.T) { + w := Websocket{ + ShutdownC: make(chan struct{}), + features: &protocol.Features{Subscribe: true, Unsubscribe: true}, + subscribedChannels: []WebsocketChannelSubscription{ + { + Channel: "hello", + }, + }, + } + w.SetChannelUnsubscriber(placeholderSubscriber) + w.SetChannelSubscriber(placeholderSubscriber) + w.setConnectedStatus(true) + go w.manageSubscriptions() + time.Sleep(8 * time.Second) + w.setConnectedStatus(false) + time.Sleep(manageSubscriptionsDelay) + w.subscriptionMutex.Lock() + if len(w.subscribedChannels) > 0 { + t.Error("Expected empty subscribed channels") + } + w.subscriptionMutex.Unlock() } // TestConnectionMonitorNoConnection logic test func TestConnectionMonitorNoConnection(t *testing.T) { - w := Websocket{} - w.DataHandler = make(chan interface{}, 1) - w.ShutdownC = make(chan struct{}, 1) - w.exchangeName = "hello" - go w.connectionMonitor() - err := <-w.DataHandler - if !strings.EqualFold(err.(error).Error(), - fmt.Sprintf("%v connectionMonitor: websocket disabled, shutting down", w.exchangeName)) { - t.Errorf("expecting error 'connectionMonitor: websocket disabled, shutting down', received '%v'", err) - } -} - -// TestWsNoConnectionTolerance logic test -func TestWsNoConnectionTolerance(t *testing.T) { - w := Websocket{} - w.DataHandler = make(chan interface{}, 1) - w.ShutdownC = make(chan struct{}, 1) - w.enabled = true - w.noConnectionCheckLimit = 500 - w.checkConnection() - if w.noConnectionChecks == 0 { - t.Errorf("Expected noConnectionTolerance to increment, received '%v'", w.noConnectionChecks) - } -} - -// TestConnecting logic test -func TestConnecting(t *testing.T) { - w := Websocket{} - w.DataHandler = make(chan interface{}, 1) - w.ShutdownC = make(chan struct{}, 1) - w.enabled = true - w.connecting = true - w.reconnectionLimit = 500 - w.checkConnection() - if w.reconnectionChecks != 1 { - t.Errorf("Expected reconnectionLimit to increment, received '%v'", w.reconnectionChecks) - } -} - -// TestReconnectionLimit logic test -func TestReconnectionLimit(t *testing.T) { - w := Websocket{} - w.DataHandler = make(chan interface{}, 1) - w.ShutdownC = make(chan struct{}, 1) - w.enabled = true - w.connecting = true - w.reconnectionChecks = 99 - w.reconnectionLimit = 1 - err := w.checkConnection() - if err == nil { - t.Error("Expected error") + ws := New() + ws.DataHandler = make(chan interface{}, 1) + ws.ShutdownC = make(chan struct{}, 1) + ws.exchangeName = "hello" + ws.trafficTimeout = 1 + go ws.connectionMonitor() + if ws.IsConnectionMonitorRunning() { + t.Fatal("Should have exited") } } @@ -360,26 +422,278 @@ func TestSliceCopyDoesntImpactBoth(t *testing.T) { }, } w.SetChannelUnsubscriber(placeholderSubscriber) - w.unsubscribeToChannels() + err := w.unsubscribeToChannels() + if err != nil { + t.Error(err) + } if len(w.subscribedChannels) != 2 { t.Errorf("Unsubscription did not occur") } w.subscribedChannels[0].Channel = "test" if strings.EqualFold(w.subscribedChannels[0].Channel, w.channelsToSubscribe[0].Channel) { - t.Errorf("Slice has not been copies appropriately") + t.Errorf("Slice has not been copied appropriately") + } +} + +// TestSliceCopyDoesntImpactBoth logic test +func TestGetSubscriptions(t *testing.T) { + w := Websocket{ + subscribedChannels: []WebsocketChannelSubscription{ + { + Channel: "hello3", + }, + }, + } + + subs := w.GetSubscriptions() + subs[0].Channel = "noHELLO" + if strings.EqualFold(w.subscribedChannels[0].Channel, subs[0].Channel) { + t.Error("Subscriptions was not copied properly") } } // TestSetCanUseAuthenticatedEndpoints logic test func TestSetCanUseAuthenticatedEndpoints(t *testing.T) { - w := Websocket{} - result := w.CanUseAuthenticatedEndpoints() + ws := New() + result := ws.CanUseAuthenticatedEndpoints() if result { t.Error("expected `canUseAuthenticatedEndpoints` to be false") } - w.SetCanUseAuthenticatedEndpoints(true) - result = w.CanUseAuthenticatedEndpoints() + ws.SetCanUseAuthenticatedEndpoints(true) + result = ws.CanUseAuthenticatedEndpoints() if !result { t.Error("expected `canUseAuthenticatedEndpoints` to be true") } } + +func TestRemoveSubscribedChannels(t *testing.T) { + w := Websocket{ + channelsToSubscribe: []WebsocketChannelSubscription{ + { + Channel: "hello3", + }, + }, + } + + w.RemoveSubscribedChannels([]WebsocketChannelSubscription{{Channel: "hello3"}}) + if len(w.channelsToSubscribe) == 1 { + t.Error("Did not remove subscription") + } +} + +const ( + websocketTestURL = "wss://www.bitmex.com/realtime" + returnResponseURL = "wss://ws.kraken.com" + useProxyTests = false // Disabled by default. Freely available proxy servers that work all the time are difficult to find + proxyURL = "http://212.186.171.4:80" // Replace with a usable proxy server +) + +var wc *WebsocketConnection +var dialer websocket.Dialer + +type testStruct struct { + Error error + WC WebsocketConnection +} + +type testRequest struct { + Event string `json:"event"` + RequestID int64 `json:"reqid,omitempty"` + Pairs []string `json:"pair"` + Subscription testRequestData `json:"subscription,omitempty"` +} + +// testRequestData contains details on WS channel +type testRequestData struct { + Name string `json:"name,omitempty"` + Interval int64 `json:"interval,omitempty"` + Depth int64 `json:"depth,omitempty"` +} + +type testResponse struct { + RequestID int64 `json:"reqid,omitempty"` +} + +// TestMain setup test +func TestMain(m *testing.M) { + wc = &WebsocketConnection{ + ExchangeName: "test", + URL: returnResponseURL, + ResponseMaxLimit: 7000000000, + ResponseCheckTimeout: 30000000, + } + os.Exit(m.Run()) +} + +// TestDial logic test +func TestDial(t *testing.T) { + var testCases = []testStruct{ + {Error: nil, WC: WebsocketConnection{ExchangeName: "test1", Verbose: true, URL: websocketTestURL, RateLimit: 10, ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + {Error: errors.New(" Error: malformed ws or wss URL"), WC: WebsocketConnection{ExchangeName: "test2", Verbose: true, URL: "", ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + {Error: nil, WC: WebsocketConnection{ExchangeName: "test3", Verbose: true, URL: websocketTestURL, ProxyURL: proxyURL, ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + } + for i := 0; i < len(testCases); i++ { + testData := &testCases[i] + t.Run(testData.WC.ExchangeName, func(t *testing.T) { + if testData.WC.ProxyURL != "" && !useProxyTests { + t.Skip("Proxy testing not enabled, skipping") + } + err := testData.WC.Dial(&dialer, http.Header{}) + if err != nil { + if testData.Error != nil && err.Error() == testData.Error.Error() { + return + } + t.Fatal(err) + } + }) + } +} + +// TestSendMessage logic test +func TestSendMessage(t *testing.T) { + var testCases = []testStruct{ + {Error: nil, WC: WebsocketConnection{ExchangeName: "test1", Verbose: true, URL: websocketTestURL, RateLimit: 10, ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + {Error: errors.New(" Error: malformed ws or wss URL"), WC: WebsocketConnection{ExchangeName: "test2", Verbose: true, URL: "", ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + {Error: nil, WC: WebsocketConnection{ExchangeName: "test3", Verbose: true, URL: websocketTestURL, ProxyURL: proxyURL, ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + } + for i := 0; i < len(testCases); i++ { + testData := &testCases[i] + t.Run(testData.WC.ExchangeName, func(t *testing.T) { + if testData.WC.ProxyURL != "" && !useProxyTests { + t.Skip("Proxy testing not enabled, skipping") + } + err := testData.WC.Dial(&dialer, http.Header{}) + if err != nil { + if testData.Error != nil && err.Error() == testData.Error.Error() { + return + } + t.Fatal(err) + } + err = testData.WC.SendMessage("ping") + if err != nil { + t.Error(err) + } + }) + } +} + +// TestSendMessageWithResponse logic test +func TestSendMessageWithResponse(t *testing.T) { + if wc.ProxyURL != "" && !useProxyTests { + t.Skip("Proxy testing not enabled, skipping") + } + err := wc.Dial(&dialer, http.Header{}) + if err != nil { + t.Fatal(err) + } + go readMessages(wc, t) + + request := testRequest{ + Event: "subscribe", + Pairs: []string{currency.NewPairWithDelimiter("XBT", "USD", "/").String()}, + Subscription: testRequestData{ + Name: "ticker", + }, + RequestID: wc.GenerateMessageID(false), + } + _, err = wc.SendMessageReturnResponse(request.RequestID, request) + if err != nil { + t.Error(err) + } +} + +// TestParseBinaryResponse logic test +func TestParseBinaryResponse(t *testing.T) { + var b bytes.Buffer + w := gzip.NewWriter(&b) + _, err := w.Write([]byte("hello")) + if err != nil { + t.Error(err) + } + err = w.Close() + if err != nil { + t.Error(err) + } + var resp []byte + resp, err = wc.parseBinaryResponse(b.Bytes()) + if err != nil { + t.Error(err) + } + if !strings.EqualFold(string(resp), "hello") { + t.Errorf("GZip conversion failed. Received: '%v', Expected: 'hello'", string(resp)) + } + + var b2 bytes.Buffer + w2, err2 := flate.NewWriter(&b2, 1) + if err2 != nil { + t.Error(err2) + } + _, err2 = w2.Write([]byte("hello")) + if err2 != nil { + t.Error(err) + } + err2 = w2.Close() + if err2 != nil { + t.Error(err) + } + resp2, err3 := wc.parseBinaryResponse(b2.Bytes()) + if err3 != nil { + t.Error(err3) + } + if !strings.EqualFold(string(resp2), "hello") { + t.Errorf("GZip conversion failed. Received: '%v', Expected: 'hello'", string(resp2)) + } +} + +// TestAddResponseWithID logic test +func TestAddResponseWithID(t *testing.T) { + wc.IDResponses = nil + wc.AddResponseWithID(0, []byte("hi")) + wc.AddResponseWithID(1, []byte("hi")) +} + +// readMessages helper func +func readMessages(wc *WebsocketConnection, t *testing.T) { + timer := time.NewTimer(20 * time.Second) + for { + select { + case <-timer.C: + return + default: + resp, err := wc.ReadMessage() + if err != nil { + t.Error(err) + return + } + var incoming testResponse + err = json.Unmarshal(resp.Raw, &incoming) + if err != nil { + t.Error(err) + return + } + if incoming.RequestID > 0 { + wc.AddResponseWithID(incoming.RequestID, resp.Raw) + return + } + } + } +} + +// TestCanUseAuthenticatedWebsocketForWrapper logic test +func TestCanUseAuthenticatedWebsocketForWrapper(t *testing.T) { + ws := &Websocket{} + resp := ws.CanUseAuthenticatedWebsocketForWrapper() + if resp { + t.Error("Expected false, `connected` is false") + } + ws.setConnectedStatus(true) + resp = ws.CanUseAuthenticatedWebsocketForWrapper() + if resp { + t.Error("Expected false, `connected` is true and `CanUseAuthenticatedEndpoints` is false") + } + ws.canUseAuthenticatedEndpoints = true + resp = ws.CanUseAuthenticatedWebsocketForWrapper() + if !resp { + t.Error("Expected true, `connected` and `CanUseAuthenticatedEndpoints` is true") + } +} diff --git a/exchanges/websocket/wshandler/wshandler_types.go b/exchanges/websocket/wshandler/wshandler_types.go index 98d288b4..22e47266 100644 --- a/exchanges/websocket/wshandler/wshandler_types.go +++ b/exchanges/websocket/wshandler/wshandler_types.go @@ -6,92 +6,46 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" ) // Websocket functionality list and state consts const ( - NoWebsocketSupport uint32 = 0 - WebsocketTickerSupported uint32 = 1 << (iota - 1) - WebsocketOrderbookSupported - WebsocketKlineSupported - WebsocketTradeDataSupported - WebsocketAccountSupported - WebsocketAllowsRequests - WebsocketSubscribeSupported - WebsocketUnsubscribeSupported - WebsocketAuthenticatedEndpointsSupported - WebsocketAccountDataSupported - WebsocketSubmitOrderSupported - WebsocketCancelOrderSupported - WebsocketWithdrawSupported - WebsocketMessageCorrelationSupported - WebsocketSequenceNumberSupported - WebsocketDeadMansSwitchSupported - - WebsocketTickerSupportedText = "TICKER STREAMING SUPPORTED" - WebsocketOrderbookSupportedText = "ORDERBOOK STREAMING SUPPORTED" - WebsocketKlineSupportedText = "KLINE STREAMING SUPPORTED" - WebsocketTradeDataSupportedText = "TRADE STREAMING SUPPORTED" - WebsocketAccountSupportedText = "ACCOUNT STREAMING SUPPORTED" - WebsocketAllowsRequestsText = "WEBSOCKET REQUESTS SUPPORTED" - NoWebsocketSupportText = "WEBSOCKET NOT SUPPORTED" - UnknownWebsocketFunctionality = "UNKNOWN FUNCTIONALITY BITMASK" - WebsocketSubscribeSupportedText = "WEBSOCKET SUBSCRIBE SUPPORTED" - WebsocketUnsubscribeSupportedText = "WEBSOCKET UNSUBSCRIBE SUPPORTED" - WebsocketAuthenticatedEndpointsSupportedText = "WEBSOCKET AUTHENTICATED ENDPOINTS SUPPORTED" - WebsocketAccountDataSupportedText = "WEBSOCKET ACCOUNT DATA SUPPORTED" - WebsocketSubmitOrderSupportedText = "WEBSOCKET SUBMIT ORDER SUPPORTED" - WebsocketCancelOrderSupportedText = "WEBSOCKET CANCEL ORDER SUPPORTED" - WebsocketWithdrawSupportedText = "WEBSOCKET WITHDRAW SUPPORTED" - WebsocketMessageCorrelationSupportedText = "WEBSOCKET MESSAGE CORRELATION SUPPORTED" - WebsocketSequenceNumberSupportedText = "WEBSOCKET SEQUENCE NUMBER SUPPORTED" - WebsocketDeadMansSwitchSupportedText = "WEBSOCKET DEAD MANS SWITCH SUPPORTED" - // WebsocketNotEnabled alerts of a disabled websocket - WebsocketNotEnabled = "exchange_websocket_not_enabled" - // WebsocketTrafficLimitTime defines a standard time for no traffic from the - // websocket connection - WebsocketTrafficLimitTime = 5 * time.Second - websocketRestablishConnection = time.Second - manageSubscriptionsDelay = 5 * time.Second + WebsocketNotEnabled = "exchange_websocket_not_enabled" + manageSubscriptionsDelay = 5 * time.Second // connection monitor time delays and limits - connectionMonitorDelay = 2 * time.Second - // WebsocketStateTimeout defines a const for when a websocket connection - // times out, will be handled by the routine management system - WebsocketStateTimeout = "TIMEOUT" + connectionMonitorDelay = 2 * time.Second + WebsocketNotAuthenticatedUsingRest = "%v - Websocket not authenticated, using REST" ) // Websocket defines a return type for websocket connections via the interface // wrapper for routine processing in routines.go type Websocket struct { - proxyAddr string - defaultURL string - runningURL string - exchangeName string - enabled bool - init bool - connected bool - connecting bool - verbose bool - connector func() error - m sync.Mutex - subscriptionLock sync.Mutex - connectionMonitorRunning bool - reconnectionLimit int - noConnectionChecks int - reconnectionChecks int - noConnectionCheckLimit int - subscribedChannels []WebsocketChannelSubscription - channelsToSubscribe []WebsocketChannelSubscription - channelSubscriber func(channelToSubscribe WebsocketChannelSubscription) error - channelUnsubscriber func(channelToUnsubscribe WebsocketChannelSubscription) error - // Connected denotes a channel switch for diversion of request flow - Connected chan struct{} - // Disconnected denotes a channel switch for diversion of request flow - Disconnected chan struct{} - // DataHandler pipes websocket data to an exchange websocket data handler - DataHandler chan interface{} + canUseAuthenticatedEndpoints bool + enabled bool + init bool + connected bool + connecting bool + trafficMonitorRunning bool + verbose bool + connectionMonitorRunning bool + trafficTimeout time.Duration + proxyAddr string + defaultURL string + runningURL string + exchangeName string + m sync.Mutex + subscriptionMutex sync.Mutex + connectionMutex sync.RWMutex + connector func() error + subscribedChannels []WebsocketChannelSubscription + channelsToSubscribe []WebsocketChannelSubscription + channelSubscriber func(channelToSubscribe WebsocketChannelSubscription) error + channelUnsubscriber func(channelToUnsubscribe WebsocketChannelSubscription) error + DataHandler chan interface{} // ShutdownC is the main shutdown channel which controls all websocket go funcs ShutdownC chan struct{} // Orderbook is a local cache of orderbooks @@ -101,9 +55,23 @@ type Websocket struct { Wg sync.WaitGroup // TrafficAlert monitors if there is a halt in traffic throughput TrafficAlert chan struct{} - // Functionality defines websocket stream capabilities - Functionality uint32 - canUseAuthenticatedEndpoints bool + // ReadMessageErrors will received all errors from ws.ReadMessage() and verify if its a disconnection + ReadMessageErrors chan error + features *protocol.Features +} + +type WebsocketSetup struct { + Enabled bool + Verbose bool + AuthenticatedWebsocketAPISupport bool + WebsocketTimeout time.Duration + DefaultURL string + ExchangeName string + RunningURL string + Connector func() error + Subscriber func(channelToSubscribe WebsocketChannelSubscription) error + UnSubscriber func(channelToUnsubscribe WebsocketChannelSubscription) error + Features *protocol.Features } // WebsocketChannelSubscription container for websocket subscriptions @@ -124,7 +92,7 @@ type WebsocketResponse struct { // has been updated in the orderbook package type WebsocketOrderbookUpdate struct { Pair currency.Pair - Asset string + Asset asset.Item Exchange string } @@ -132,7 +100,7 @@ type WebsocketOrderbookUpdate struct { type TradeData struct { Timestamp time.Time CurrencyPair currency.Pair - AssetType string + AssetType asset.Item Exchange string EventType string EventTime int64 @@ -143,22 +111,27 @@ type TradeData struct { // TickerData defines ticker feed type TickerData struct { - Timestamp time.Time - Pair currency.Pair - AssetType string - Exchange string - ClosePrice float64 - Quantity float64 - OpenPrice float64 - HighPrice float64 - LowPrice float64 + Exchange string + Open float64 + Close float64 + Volume float64 + QuoteVolume float64 + High float64 + Low float64 + Bid float64 + Ask float64 + Last float64 + PriceATH float64 + Timestamp time.Time + AssetType asset.Item + Pair currency.Pair } // KlineData defines kline feed type KlineData struct { Timestamp time.Time Pair currency.Pair - AssetType string + AssetType asset.Item Exchange string StartTime time.Time CloseTime time.Time @@ -174,23 +147,26 @@ type KlineData struct { type WebsocketPositionUpdated struct { Timestamp time.Time Pair currency.Pair - AssetType string + AssetType asset.Item Exchange string } // WebsocketConnection contains all the data needed to send a message to a WS type WebsocketConnection struct { sync.Mutex - Verbose bool - RateLimit float64 - ExchangeName string - URL string - ProxyURL string - Wg sync.WaitGroup - Connection *websocket.Conn - Shutdown chan struct{} + Verbose bool + connected bool + connectionMutex sync.RWMutex + RateLimit float64 + ExchangeName string + URL string + ProxyURL string + Wg sync.WaitGroup + Connection *websocket.Conn + Shutdown chan struct{} // These are the request IDs and the corresponding response JSON IDResponses map[int64][]byte ResponseCheckTimeout time.Duration ResponseMaxLimit time.Duration + TrafficTimeout time.Duration } diff --git a/exchanges/websocket/wsorderbook/wsorderbook.go b/exchanges/websocket/wsorderbook/wsorderbook.go index 80ed724e..26c4de55 100644 --- a/exchanges/websocket/wsorderbook/wsorderbook.go +++ b/exchanges/websocket/wsorderbook/wsorderbook.go @@ -1,11 +1,12 @@ package wsorderbook import ( + "errors" "fmt" "sort" - "sync" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" ) @@ -24,212 +25,219 @@ func (w *WebsocketOrderbookLocal) Setup(obBufferLimit int, bufferEnabled, sortBu // Volume == 0; deletion at price target // Price target not found; append of price target // Price target found; amend volume of price target -func (w *WebsocketOrderbookLocal) Update(orderbookUpdate *WebsocketOrderbookUpdate) error { - if (orderbookUpdate.Bids == nil && orderbookUpdate.Asks == nil) || - (len(orderbookUpdate.Bids) == 0 && len(orderbookUpdate.Asks) == 0) { - return fmt.Errorf("%v cannot have bids and ask targets both nil", w.exchangeName) +func (w *WebsocketOrderbookLocal) Update(u *WebsocketOrderbookUpdate) error { + if (u.Bids == nil && u.Asks == nil) || (len(u.Bids) == 0 && len(u.Asks) == 0) { + return fmt.Errorf("%v cannot have bids and ask targets both nil", + w.exchangeName) } w.m.Lock() defer w.m.Unlock() - if _, ok := w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType]; !ok { + obLookup, ok := w.ob[u.Pair][u.Asset] + if !ok { return fmt.Errorf("ob.Base could not be found for Exchange %s CurrencyPair: %s AssetType: %s", w.exchangeName, - orderbookUpdate.CurrencyPair.String(), - orderbookUpdate.AssetType) + u.Pair, + u.Asset) } + if w.bufferEnabled { - overBufferLimit := w.processBufferUpdate(orderbookUpdate) + overBufferLimit := w.processBufferUpdate(obLookup, u) if !overBufferLimit { return nil } } else { - w.processObUpdate(orderbookUpdate) + w.processObUpdate(obLookup, u) } - err := w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Process() + err := obLookup.Process() if err != nil { return err } if w.bufferEnabled { // Reset the buffer - w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType] = nil + w.buffer[u.Pair][u.Asset] = nil } return nil } -func (w *WebsocketOrderbookLocal) processBufferUpdate(orderbookUpdate *WebsocketOrderbookUpdate) bool { +func (w *WebsocketOrderbookLocal) processBufferUpdate(o *orderbook.Base, u *WebsocketOrderbookUpdate) bool { if w.buffer == nil { - w.buffer = make(map[currency.Pair]map[string][]WebsocketOrderbookUpdate) + w.buffer = make(map[currency.Pair]map[asset.Item][]*WebsocketOrderbookUpdate) } - if w.buffer[orderbookUpdate.CurrencyPair] == nil { - w.buffer[orderbookUpdate.CurrencyPair] = make(map[string][]WebsocketOrderbookUpdate) + if w.buffer[u.Pair] == nil { + w.buffer[u.Pair] = make(map[asset.Item][]*WebsocketOrderbookUpdate) } - if len(w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType]) <= w.obBufferLimit { - w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType] = append(w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType], *orderbookUpdate) - if len(w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType]) < w.obBufferLimit { + bufferLookup := w.buffer[u.Pair][u.Asset] + if len(bufferLookup) <= w.obBufferLimit { + bufferLookup = append(bufferLookup, u) + if len(bufferLookup) < w.obBufferLimit { + w.buffer[u.Pair][u.Asset] = bufferLookup return false } } if w.sortBuffer { // sort by last updated to ensure each update is in order if w.sortBufferByUpdateIDs { - sort.Slice(w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType], func(i, j int) bool { - return w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType][i].UpdateID < w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType][j].UpdateID + sort.Slice(bufferLookup, func(i, j int) bool { + return bufferLookup[i].UpdateID < bufferLookup[j].UpdateID }) } else { - sort.Slice(w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType], func(i, j int) bool { - return w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType][i].UpdateTime.Before(w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType][j].UpdateTime) + sort.Slice(bufferLookup, func(i, j int) bool { + return bufferLookup[i].UpdateTime.Before(bufferLookup[j].UpdateTime) }) } } - for i := 0; i < len(w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType]); i++ { - w.processObUpdate(&w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType][i]) + for i := 0; i < len(bufferLookup); i++ { + w.processObUpdate(o, bufferLookup[i]) } + w.buffer[u.Pair][u.Asset] = bufferLookup return true } -func (w *WebsocketOrderbookLocal) processObUpdate(orderbookUpdate *WebsocketOrderbookUpdate) { +func (w *WebsocketOrderbookLocal) processObUpdate(o *orderbook.Base, u *WebsocketOrderbookUpdate) { if w.updateEntriesByID { - w.updateByIDAndAction(orderbookUpdate) + w.updateByIDAndAction(o, u) } else { - var wg sync.WaitGroup - wg.Add(2) - go w.updateAsksByPrice(orderbookUpdate, &wg) - go w.updateBidsByPrice(orderbookUpdate, &wg) - wg.Wait() + w.updateAsksByPrice(o, u) + w.updateBidsByPrice(o, u) } } -func (w *WebsocketOrderbookLocal) updateAsksByPrice(base *WebsocketOrderbookUpdate, wg *sync.WaitGroup) { - for j := 0; j < len(base.Asks); j++ { - found := false - for k := 0; k < len(w.ob[base.CurrencyPair][base.AssetType].Asks); k++ { - if w.ob[base.CurrencyPair][base.AssetType].Asks[k].Price == base.Asks[j].Price { - found = true - if base.Asks[j].Amount == 0 { - w.ob[base.CurrencyPair][base.AssetType].Asks = append(w.ob[base.CurrencyPair][base.AssetType].Asks[:k], - w.ob[base.CurrencyPair][base.AssetType].Asks[k+1:]...) - break +func (w *WebsocketOrderbookLocal) updateAsksByPrice(o *orderbook.Base, u *WebsocketOrderbookUpdate) { +updates: + for j := range u.Asks { + for k := range o.Asks { + if o.Asks[k].Price == u.Asks[j].Price { + if u.Asks[j].Amount <= 0 { + o.Asks = append(o.Asks[:k], o.Asks[k+1:]...) + continue updates } - w.ob[base.CurrencyPair][base.AssetType].Asks[k].Amount = base.Asks[j].Amount - break + o.Asks[k].Amount = u.Asks[j].Amount + continue updates } } - if !found { - w.ob[base.CurrencyPair][base.AssetType].Asks = append(w.ob[base.CurrencyPair][base.AssetType].Asks, base.Asks[j]) + if u.Asks[j].Amount == 0 { + continue } + o.Asks = append(o.Asks, u.Asks[j]) } - sort.Slice(w.ob[base.CurrencyPair][base.AssetType].Asks, func(i, j int) bool { - return w.ob[base.CurrencyPair][base.AssetType].Asks[i].Price < w.ob[base.CurrencyPair][base.AssetType].Asks[j].Price + sort.Slice(o.Asks, func(i, j int) bool { + return o.Asks[i].Price < o.Asks[j].Price }) - wg.Done() } -func (w *WebsocketOrderbookLocal) updateBidsByPrice(base *WebsocketOrderbookUpdate, wg *sync.WaitGroup) { - for j := 0; j < len(base.Bids); j++ { - found := false - for k := 0; k < len(w.ob[base.CurrencyPair][base.AssetType].Bids); k++ { - if w.ob[base.CurrencyPair][base.AssetType].Bids[k].Price == base.Bids[j].Price { - found = true - if base.Bids[j].Amount == 0 { - w.ob[base.CurrencyPair][base.AssetType].Bids = append(w.ob[base.CurrencyPair][base.AssetType].Bids[:k], - w.ob[base.CurrencyPair][base.AssetType].Bids[k+1:]...) - break +func (w *WebsocketOrderbookLocal) updateBidsByPrice(o *orderbook.Base, u *WebsocketOrderbookUpdate) { +updates: + for j := range u.Bids { + for k := range o.Bids { + if o.Bids[k].Price == u.Bids[j].Price { + if u.Bids[j].Amount <= 0 { + o.Bids = append(o.Bids[:k], o.Bids[k+1:]...) + continue updates } - w.ob[base.CurrencyPair][base.AssetType].Bids[k].Amount = base.Bids[j].Amount - break + o.Bids[k].Amount = u.Bids[j].Amount + continue updates } } - if !found { - w.ob[base.CurrencyPair][base.AssetType].Bids = append(w.ob[base.CurrencyPair][base.AssetType].Bids, base.Bids[j]) + if u.Bids[j].Amount == 0 { + continue } + o.Bids = append(o.Bids, u.Bids[j]) } - sort.Slice(w.ob[base.CurrencyPair][base.AssetType].Bids, func(i, j int) bool { - return w.ob[base.CurrencyPair][base.AssetType].Bids[i].Price > w.ob[base.CurrencyPair][base.AssetType].Bids[j].Price + sort.Slice(o.Bids, func(i, j int) bool { + return o.Bids[i].Price > o.Bids[j].Price }) - wg.Done() } // updateByIDAndAction will receive an action to execute against the orderbook // it will then match by IDs instead of price to perform the action -func (w *WebsocketOrderbookLocal) updateByIDAndAction(orderbookUpdate *WebsocketOrderbookUpdate) { - switch orderbookUpdate.Action { +func (w *WebsocketOrderbookLocal) updateByIDAndAction(o *orderbook.Base, u *WebsocketOrderbookUpdate) { + switch u.Action { case "update": - for _, target := range orderbookUpdate.Bids { - for i := range w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Bids { - if w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Bids[i].ID == target.ID { - w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Bids[i].Amount = target.Amount + for x := range u.Bids { + for y := range o.Bids { + if o.Bids[y].ID == u.Bids[x].ID { + o.Bids[y].Amount = u.Bids[x].Amount break } } } - for _, target := range orderbookUpdate.Asks { - for i := range w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Asks { - if w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Asks[i].ID == target.ID { - w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Asks[i].Amount = target.Amount + for x := range u.Asks { + for y := range o.Asks { + if o.Asks[y].ID == u.Asks[x].ID { + o.Asks[y].Amount = u.Asks[x].Amount break } } } case "delete": - for _, target := range orderbookUpdate.Bids { - for i := 0; i < len(w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Bids); i++ { - if w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Bids[i].ID == target.ID { - w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Bids = append(w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Bids[:i], - w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Bids[i+1:]...) - i-- + for x := range u.Bids { + for y := 0; y < len(o.Bids); y++ { + if o.Bids[y].ID == u.Bids[x].ID { + o.Bids = append(o.Bids[:y], o.Bids[y+1:]...) break } } } - for _, target := range orderbookUpdate.Asks { - for i := 0; i < len(w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Asks); i++ { - if w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Asks[i].ID == target.ID { - w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Asks = append(w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Asks[:i], - w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Asks[i+1:]...) - i-- + for x := range u.Asks { + for y := 0; y < len(o.Asks); y++ { + if o.Asks[y].ID == u.Asks[x].ID { + o.Asks = append(o.Asks[:y], o.Asks[y+1:]...) break } } } case "insert": - w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Bids = append(w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Bids, orderbookUpdate.Bids...) - w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Asks = append(w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType].Asks, orderbookUpdate.Asks...) + o.Bids = append(o.Bids, u.Bids...) + sort.Slice(o.Bids, func(i, j int) bool { + return o.Bids[i].Price > o.Bids[j].Price + }) + + o.Asks = append(o.Asks, u.Asks...) + sort.Slice(o.Asks, func(i, j int) bool { + return o.Asks[i].Price < o.Asks[j].Price + }) } } // LoadSnapshot loads initial snapshot of ob data, overwrite allows full // ob to be completely rewritten because the exchange is a doing a full // update not an incremental one -func (w *WebsocketOrderbookLocal) LoadSnapshot(newOrderbook *orderbook.Base, overwrite bool) error { +func (w *WebsocketOrderbookLocal) LoadSnapshot(newOrderbook *orderbook.Base) error { if len(newOrderbook.Asks) == 0 || len(newOrderbook.Bids) == 0 { return fmt.Errorf("%v snapshot ask and bids are nil", w.exchangeName) } + + if newOrderbook.Pair.IsEmpty() { + return errors.New("websocket orderbook pair unset") + } + + if newOrderbook.AssetType.String() == "" { + return errors.New("websocket orderbook asset type unset") + } + + if newOrderbook.ExchangeName == "" { + return errors.New("websocket orderbook exchange name unset") + } + w.m.Lock() defer w.m.Unlock() if w.ob == nil { - w.ob = make(map[currency.Pair]map[string]*orderbook.Base) + w.ob = make(map[currency.Pair]map[asset.Item]*orderbook.Base) } if w.ob[newOrderbook.Pair] == nil { - w.ob[newOrderbook.Pair] = make(map[string]*orderbook.Base) - } - if w.ob[newOrderbook.Pair][newOrderbook.AssetType] != nil && - (len(w.ob[newOrderbook.Pair][newOrderbook.AssetType].Asks) > 0 || - len(w.ob[newOrderbook.Pair][newOrderbook.AssetType].Bids) > 0) { - if overwrite { - w.ob[newOrderbook.Pair][newOrderbook.AssetType] = newOrderbook - return newOrderbook.Process() - } - return fmt.Errorf("%v snapshot instance already found", w.exchangeName) + w.ob[newOrderbook.Pair] = make(map[asset.Item]*orderbook.Base) } + w.ob[newOrderbook.Pair][newOrderbook.AssetType] = newOrderbook return newOrderbook.Process() } -// GetOrderbook use sparingly. Modifying anything here will ruin hash calculation and cause problems -func (w *WebsocketOrderbookLocal) GetOrderbook(p currency.Pair, assetType string) *orderbook.Base { +// GetOrderbook use sparingly. Modifying anything here will ruin hash +// calculation and cause problems +func (w *WebsocketOrderbookLocal) GetOrderbook(p currency.Pair, a asset.Item) *orderbook.Base { w.m.Lock() defer w.m.Unlock() - return w.ob[p][assetType] + return w.ob[p][a] } // FlushCache flushes w.ob data to be garbage collected and refreshed when a diff --git a/exchanges/websocket/wsorderbook/wsorderbook_test.go b/exchanges/websocket/wsorderbook/wsorderbook_test.go index 2f5516f5..9c2cae51 100644 --- a/exchanges/websocket/wsorderbook/wsorderbook_test.go +++ b/exchanges/websocket/wsorderbook/wsorderbook_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" ) @@ -19,14 +20,15 @@ var itemArray = [][]orderbook.Item{ {{Price: 5000, Amount: 1, ID: 5}}, } +var cp = currency.NewPairFromString("BTCUSD") + const ( exchangeName = "exchangeTest" - spot = orderbook.Spot ) -func createSnapshot() (obl *WebsocketOrderbookLocal, curr currency.Pair, asks, bids []orderbook.Item, err error) { +func createSnapshot() (obl *WebsocketOrderbookLocal, asks, bids []orderbook.Item, err error) { var snapShot1 orderbook.Base - curr = currency.NewPairFromString("BTCUSD") + snapShot1.ExchangeName = exchangeName asks = []orderbook.Item{ {Price: 4000, Amount: 1, ID: 6}, } @@ -35,30 +37,94 @@ func createSnapshot() (obl *WebsocketOrderbookLocal, curr currency.Pair, asks, b } snapShot1.Asks = asks snapShot1.Bids = bids - snapShot1.AssetType = spot - snapShot1.Pair = curr - obl = &WebsocketOrderbookLocal{} - err = obl.LoadSnapshot(&snapShot1, false) + snapShot1.AssetType = asset.Spot + snapShot1.Pair = cp + obl = &WebsocketOrderbookLocal{exchangeName: exchangeName} + err = obl.LoadSnapshot(&snapShot1) return } -// BenchmarkBufferPerformance demonstrates buffer more performant than multi process calls +func bidAskGenerator() []orderbook.Item { + var response []orderbook.Item + randIterator := 100 + for i := 0; i < randIterator; i++ { + price := float64(rand.Intn(1000)) + if price == 0 { + price = 1 + } + response = append(response, orderbook.Item{ + Amount: float64(rand.Intn(10)), + Price: price, + ID: int64(i), + }) + } + return response +} + +func BenchmarkUpdateBidsByPrice(b *testing.B) { + ob, _, _, err := createSnapshot() + if err != nil { + b.Error(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + bidAsks := bidAskGenerator() + update := &WebsocketOrderbookUpdate{ + Bids: bidAsks, + Asks: bidAsks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + } + ob.updateBidsByPrice(ob.ob[cp][asset.Spot], update) + } +} + +func BenchmarkUpdateAsksByPrice(b *testing.B) { + ob, _, _, err := createSnapshot() + if err != nil { + b.Error(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + bidAsks := bidAskGenerator() + update := &WebsocketOrderbookUpdate{ + Bids: bidAsks, + Asks: bidAsks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + } + ob.updateAsksByPrice(ob.ob[cp][asset.Spot], update) + } +} + +// BenchmarkBufferPerformance demonstrates buffer more performant than multi +// process calls func BenchmarkBufferPerformance(b *testing.B) { - obl, curr, asks, bids, err := createSnapshot() + obl, asks, bids, err := createSnapshot() if err != nil { b.Fatal(err) } - obl.exchangeName = exchangeName - obl.sortBuffer = true - update := &WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: spot, + obl.bufferEnabled = true + // This is to ensure we do not send in zero orderbook info to our main book + // in orderbook.go, orderbooks should not be zero even after an update. + dummyItem := orderbook.Item{ + Amount: 1333337, + Price: 1337.1337, + ID: 1337, } + obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) + update := &WebsocketOrderbookUpdate{ + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + } + b.ResetTimer() for i := 0; i < b.N; i++ { - randomIndex := rand.Intn(5) + randomIndex := rand.Intn(4) update.Asks = itemArray[randomIndex] update.Bids = itemArray[randomIndex] err = obl.Update(update) @@ -70,23 +136,30 @@ func BenchmarkBufferPerformance(b *testing.B) { // BenchmarkBufferSortingPerformance benchmark func BenchmarkBufferSortingPerformance(b *testing.B) { - obl, curr, asks, bids, err := createSnapshot() + obl, asks, bids, err := createSnapshot() if err != nil { b.Fatal(err) } - obl.exchangeName = exchangeName - obl.sortBuffer = true obl.bufferEnabled = true - obl.obBufferLimit = 5 - update := &WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: spot, + obl.sortBuffer = true + // This is to ensure we do not send in zero orderbook info to our main book + // in orderbook.go, orderbooks should not be zero even after an update. + dummyItem := orderbook.Item{ + Amount: 1333337, + Price: 1337.1337, + ID: 1337, } + obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) + update := &WebsocketOrderbookUpdate{ + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + } + b.ResetTimer() for i := 0; i < b.N; i++ { - randomIndex := rand.Intn(5) + randomIndex := rand.Intn(4) update.Asks = itemArray[randomIndex] update.Bids = itemArray[randomIndex] err = obl.Update(update) @@ -96,22 +169,33 @@ func BenchmarkBufferSortingPerformance(b *testing.B) { } } -// BenchmarkNoBufferPerformance demonstrates orderbook process less performant than buffer -func BenchmarkNoBufferPerformance(b *testing.B) { - obl, curr, asks, bids, err := createSnapshot() +// BenchmarkBufferSortingPerformance benchmark +func BenchmarkBufferSortingByIDPerformance(b *testing.B) { + obl, asks, bids, err := createSnapshot() if err != nil { b.Fatal(err) } - obl.exchangeName = exchangeName - update := &WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: spot, + obl.bufferEnabled = true + obl.sortBuffer = true + obl.sortBufferByUpdateIDs = true + // This is to ensure we do not send in zero orderbook info to our main book + // in orderbook.go, orderbooks should not be zero even after an update. + dummyItem := orderbook.Item{ + Amount: 1333337, + Price: 1337.1337, + ID: 1337, } + obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) + update := &WebsocketOrderbookUpdate{ + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + } + b.ResetTimer() for i := 0; i < b.N; i++ { - randomIndex := rand.Intn(5) + randomIndex := rand.Intn(4) update.Asks = itemArray[randomIndex] update.Bids = itemArray[randomIndex] err = obl.Update(update) @@ -121,45 +205,112 @@ func BenchmarkNoBufferPerformance(b *testing.B) { } } +// BenchmarkNoBufferPerformance demonstrates orderbook process less performant +// than buffer +func BenchmarkNoBufferPerformance(b *testing.B) { + obl, asks, bids, err := createSnapshot() + if err != nil { + b.Fatal(err) + } + // This is to ensure we do not send in zero orderbook info to our main book + // in orderbook.go, orderbooks should not be zero even after an update. + dummyItem := orderbook.Item{ + Amount: 1333337, + Price: 1337.1337, + ID: 1337, + } + obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) + update := &WebsocketOrderbookUpdate{ + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + randomIndex := rand.Intn(4) + update.Asks = itemArray[randomIndex] + update.Bids = itemArray[randomIndex] + err = obl.Update(update) + if err != nil { + b.Fatal(err) + } + } +} + +func TestUpdates(t *testing.T) { + obl, _, _, err := createSnapshot() + if err != nil { + t.Error(err) + } + + obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &WebsocketOrderbookUpdate{ + Bids: itemArray[5], + Asks: itemArray[5], + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + }) + if err != nil { + t.Error(err) + } + + obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &WebsocketOrderbookUpdate{ + Bids: itemArray[0], + Asks: itemArray[0], + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + }) + if err != nil { + t.Error(err) + } + + if len(obl.ob[cp][asset.Spot].Asks) != 3 { + t.Error("Did not update") + } +} + // TestHittingTheBuffer logic test func TestHittingTheBuffer(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } - obl.exchangeName = exchangeName obl.bufferEnabled = true obl.obBufferLimit = 5 for i := 0; i < len(itemArray); i++ { asks := itemArray[i] bids := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: spot, + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, }) if err != nil { t.Fatal(err) } } - if len(obl.ob[curr][spot].Asks) != 3 { - t.Log(obl.ob[curr][spot]) - t.Errorf("expected 3 entries, received: %v", len(obl.ob[curr][spot].Asks)) + if len(obl.ob[cp][asset.Spot].Asks) != 3 { + t.Log(obl.ob[cp][asset.Spot]) + t.Errorf("expected 3 entries, received: %v", + len(obl.ob[cp][asset.Spot].Asks)) } - if len(obl.ob[curr][spot].Bids) != 3 { - t.Errorf("expected 3 entries, received: %v", len(obl.ob[curr][spot].Bids)) + if len(obl.ob[cp][asset.Spot].Bids) != 3 { + t.Errorf("expected 3 entries, received: %v", + len(obl.ob[cp][asset.Spot].Bids)) } } // TestInsertWithIDs logic test func TestInsertWithIDs(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } - obl.exchangeName = exchangeName obl.bufferEnabled = true obl.updateEntriesByID = true obl.obBufferLimit = 5 @@ -167,32 +318,33 @@ func TestInsertWithIDs(t *testing.T) { asks := itemArray[i] bids := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: spot, - Action: "insert", + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + Action: "insert", }) if err != nil { t.Fatal(err) } } - if len(obl.ob[curr][spot].Asks) != 6 { - t.Errorf("expected 6 entries, received: %v", len(obl.ob[curr][spot].Asks)) + if len(obl.ob[cp][asset.Spot].Asks) != 6 { + t.Errorf("expected 6 entries, received: %v", + len(obl.ob[cp][asset.Spot].Asks)) } - if len(obl.ob[curr][spot].Bids) != 6 { - t.Errorf("expected 6 entries, received: %v", len(obl.ob[curr][spot].Bids)) + if len(obl.ob[cp][asset.Spot].Bids) != 6 { + t.Errorf("expected 6 entries, received: %v", + len(obl.ob[cp][asset.Spot].Bids)) } } // TestSortIDs logic test func TestSortIDs(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } - obl.exchangeName = exchangeName obl.bufferEnabled = true obl.sortBufferByUpdateIDs = true obl.sortBuffer = true @@ -201,116 +353,136 @@ func TestSortIDs(t *testing.T) { asks := itemArray[i] bids := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateID: int64(i), - AssetType: spot, + Bids: bids, + Asks: asks, + Pair: cp, + UpdateID: int64(i), + Asset: asset.Spot, }) if err != nil { t.Fatal(err) } } - if len(obl.ob[curr][spot].Asks) != 3 { - t.Errorf("expected 6 entries, received: %v", len(obl.ob[curr][spot].Asks)) + if len(obl.ob[cp][asset.Spot].Asks) != 3 { + t.Errorf("expected 3 entries, received: %v", + len(obl.ob[cp][asset.Spot].Asks)) } - if len(obl.ob[curr][spot].Bids) != 3 { - t.Errorf("expected 6 entries, received: %v", len(obl.ob[curr][spot].Bids)) + if len(obl.ob[cp][asset.Spot].Bids) != 3 { + t.Errorf("expected 3 entries, received: %v", + len(obl.ob[cp][asset.Spot].Bids)) } } // TestDeleteWithIDs logic test func TestDeleteWithIDs(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } - obl.exchangeName = exchangeName + + // This is to ensure we do not send in zero orderbook info to our main book + // in orderbook.go, orderbooks should not be zero even after an update. + dummyItem := orderbook.Item{ + Amount: 1333337, + Price: 1337.1337, + ID: 1337, + } + obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem) + obl.ob[cp][asset.Spot].Asks = append(obl.ob[cp][asset.Spot].Asks, + itemArray[2][0]) + obl.ob[cp][asset.Spot].Asks = append(obl.ob[cp][asset.Spot].Asks, + itemArray[1][0]) + obl.updateEntriesByID = true for i := 0; i < len(itemArray); i++ { asks := itemArray[i] bids := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: spot, - Action: "delete", + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + Action: "delete", }) if err != nil { t.Fatal(err) } } - if len(obl.ob[curr][spot].Asks) != 0 { - t.Errorf("expected 0 entries, received: %v", len(obl.ob[curr][spot].Asks)) + + if len(obl.ob[cp][asset.Spot].Asks) != 0 { + t.Errorf("expected 0 entries, received: %v", + len(obl.ob[cp][asset.Spot].Asks)) } - if len(obl.ob[curr][spot].Bids) != 0 { - t.Errorf("expected 0 entries, received: %v", len(obl.ob[curr][spot].Bids)) + if len(obl.ob[cp][asset.Spot].Bids) != 1 { + t.Errorf("expected 1 entries, received: %v", + len(obl.ob[cp][asset.Spot].Bids)) } } // TestUpdateWithIDs logic test func TestUpdateWithIDs(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } - obl.exchangeName = exchangeName obl.updateEntriesByID = true for i := 0; i < len(itemArray); i++ { asks := itemArray[i] bids := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: spot, - Action: "update", + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + Action: "update", }) if err != nil { t.Fatal(err) } } - if len(obl.ob[curr][spot].Asks) != 1 { - t.Log(obl.ob[curr][spot]) - t.Errorf("expected 1 entries, received: %v", len(obl.ob[curr][spot].Asks)) + if len(obl.ob[cp][asset.Spot].Asks) != 1 { + t.Log(obl.ob[cp][asset.Spot]) + t.Errorf("expected 1 entries, received: %v", + len(obl.ob[cp][asset.Spot].Asks)) } - if len(obl.ob[curr][spot].Bids) != 1 { - t.Errorf("expected 1 entries, received: %v", len(obl.ob[curr][spot].Bids)) + if len(obl.ob[cp][asset.Spot].Bids) != 1 { + t.Errorf("expected 1 entries, received: %v", + len(obl.ob[cp][asset.Spot].Bids)) } } // TestOutOfOrderIDs logic test func TestOutOfOrderIDs(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } - outOFOrderIDs := []int64{2, 1, 5, 3, 4, 6} + outOFOrderIDs := []int64{2, 1, 5, 3, 4, 6, 7} if itemArray[0][0].Price != 1000 { - t.Errorf("expected sorted price to be 3000, received: %v", itemArray[1][0].Price) + t.Errorf("expected sorted price to be 3000, received: %v", + itemArray[1][0].Price) } - obl.exchangeName = exchangeName obl.bufferEnabled = true obl.sortBuffer = true obl.obBufferLimit = 5 for i := 0; i < len(itemArray); i++ { asks := itemArray[i] err = obl.Update(&WebsocketOrderbookUpdate{ - Asks: asks, - CurrencyPair: curr, - UpdateID: outOFOrderIDs[i], - AssetType: spot, + Asks: asks, + Pair: cp, + UpdateID: outOFOrderIDs[i], + Asset: asset.Spot, }) if err != nil { t.Fatal(err) } } // Index 1 since index 0 is price 7000 - if obl.ob[curr][spot].Asks[1].Price != 2000 { - t.Errorf("expected sorted price to be 3000, received: %v", obl.ob[curr][spot].Asks[1].Price) + if obl.ob[cp][asset.Spot].Asks[1].Price != 2000 { + t.Errorf("expected sorted price to be 3000, received: %v", + obl.ob[cp][asset.Spot].Asks[1].Price) } } @@ -318,7 +490,6 @@ func TestOutOfOrderIDs(t *testing.T) { func TestRunUpdateWithoutSnapshot(t *testing.T) { var obl WebsocketOrderbookLocal var snapShot1 orderbook.Base - curr := currency.NewPairFromString("BTCUSD") asks := []orderbook.Item{ {Price: 4000, Amount: 1, ID: 8}, } @@ -328,20 +499,20 @@ func TestRunUpdateWithoutSnapshot(t *testing.T) { } snapShot1.Asks = asks snapShot1.Bids = bids - snapShot1.AssetType = spot - snapShot1.Pair = curr + snapShot1.AssetType = asset.Spot + snapShot1.Pair = cp obl.exchangeName = exchangeName err := obl.Update(&WebsocketOrderbookUpdate{ - Bids: bids, - Asks: asks, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: spot, + Bids: bids, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, }) if err == nil { t.Fatal("expected an error running update with no snapshot loaded") } - if err.Error() != "ob.Base could not be found for Exchange exchangeTest CurrencyPair: BTCUSD AssetType: SPOT" { + if err.Error() != "ob.Base could not be found for Exchange exchangeTest CurrencyPair: BTCUSD AssetType: spot" { t.Fatal(err) } } @@ -350,23 +521,23 @@ func TestRunUpdateWithoutSnapshot(t *testing.T) { func TestRunUpdateWithoutAnyUpdates(t *testing.T) { var obl WebsocketOrderbookLocal var snapShot1 orderbook.Base - curr := currency.NewPairFromString("BTCUSD") snapShot1.Asks = []orderbook.Item{} snapShot1.Bids = []orderbook.Item{} - snapShot1.AssetType = spot - snapShot1.Pair = curr + snapShot1.AssetType = asset.Spot + snapShot1.Pair = cp obl.exchangeName = exchangeName err := obl.Update(&WebsocketOrderbookUpdate{ - Bids: snapShot1.Asks, - Asks: snapShot1.Bids, - CurrencyPair: curr, - UpdateTime: time.Now(), - AssetType: spot, + Bids: snapShot1.Asks, + Asks: snapShot1.Bids, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, }) if err == nil { t.Fatal("expected an error running update with no snapshot loaded") } - if err.Error() != fmt.Sprintf("%v cannot have bids and ask targets both nil", exchangeName) { + if err.Error() != fmt.Sprintf("%v cannot have bids and ask targets both nil", + exchangeName) { t.Fatal("expected nil asks and bids error") } } @@ -375,15 +546,13 @@ func TestRunUpdateWithoutAnyUpdates(t *testing.T) { func TestRunSnapshotWithNoData(t *testing.T) { var obl WebsocketOrderbookLocal var snapShot1 orderbook.Base - curr := currency.NewPairFromString("BTCUSD") snapShot1.Asks = []orderbook.Item{} snapShot1.Bids = []orderbook.Item{} - snapShot1.AssetType = spot - snapShot1.Pair = curr + snapShot1.AssetType = asset.Spot + snapShot1.Pair = cp snapShot1.ExchangeName = "test" obl.exchangeName = "test" - err := obl.LoadSnapshot(&snapShot1, - false) + err := obl.LoadSnapshot(&snapShot1) if err == nil { t.Fatal("expected an error loading a snapshot") } @@ -392,11 +561,11 @@ func TestRunSnapshotWithNoData(t *testing.T) { } } -// TestLoadSnapshotWithOverride logic test -func TestLoadSnapshotWithOverride(t *testing.T) { +// TestLoadSnapshot logic test +func TestLoadSnapshot(t *testing.T) { var obl WebsocketOrderbookLocal var snapShot1 orderbook.Base - curr := currency.NewPairFromString("BTCUSD") + snapShot1.ExchangeName = "SnapshotWithOverride" asks := []orderbook.Item{ {Price: 4000, Amount: 1, ID: 8}, } @@ -405,33 +574,25 @@ func TestLoadSnapshotWithOverride(t *testing.T) { } snapShot1.Asks = asks snapShot1.Bids = bids - snapShot1.AssetType = spot - snapShot1.Pair = curr - err := obl.LoadSnapshot(&snapShot1, false) - if err != nil { - t.Error(err) - } - err = obl.LoadSnapshot(&snapShot1, false) - if err == nil { - t.Error("expected error: 'snapshot instance already found'") - } - err = obl.LoadSnapshot(&snapShot1, true) + snapShot1.AssetType = asset.Spot + snapShot1.Pair = cp + err := obl.LoadSnapshot(&snapShot1) if err != nil { t.Error(err) } } -// TestInsertWithIDs logic test +// TestFlushCache logic test func TestFlushCache(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } - if obl.ob[curr][spot] == nil { + if obl.ob[cp][asset.Spot] == nil { t.Error("expected ob to have ask entries") } obl.FlushCache() - if obl.ob[curr][spot] != nil { + if obl.ob[cp][asset.Spot] != nil { t.Error("expected ob be flushed") } } @@ -440,6 +601,7 @@ func TestFlushCache(t *testing.T) { func TestInsertingSnapShots(t *testing.T) { var obl WebsocketOrderbookLocal var snapShot1 orderbook.Base + snapShot1.ExchangeName = "WSORDERBOOKTEST1" asks := []orderbook.Item{ {Price: 6000, Amount: 1, ID: 1}, {Price: 6001, Amount: 0.5, ID: 2}, @@ -470,13 +632,14 @@ func TestInsertingSnapShots(t *testing.T) { snapShot1.Asks = asks snapShot1.Bids = bids - snapShot1.AssetType = spot - snapShot1.Pair = currency.NewPairFromString("BTCUSD") - err := obl.LoadSnapshot(&snapShot1, false) + snapShot1.AssetType = asset.Spot + snapShot1.Pair = cp + err := obl.LoadSnapshot(&snapShot1) if err != nil { t.Fatal(err) } var snapShot2 orderbook.Base + snapShot2.ExchangeName = "WSORDERBOOKTEST2" asks = []orderbook.Item{ {Price: 51, Amount: 1, ID: 1}, {Price: 52, Amount: 0.5, ID: 2}, @@ -507,13 +670,14 @@ func TestInsertingSnapShots(t *testing.T) { snapShot2.Asks = asks snapShot2.Bids = bids - snapShot2.AssetType = spot + snapShot2.AssetType = asset.Spot snapShot2.Pair = currency.NewPairFromString("LTCUSD") - err = obl.LoadSnapshot(&snapShot2, false) + err = obl.LoadSnapshot(&snapShot2) if err != nil { t.Fatal(err) } var snapShot3 orderbook.Base + snapShot3.ExchangeName = "WSORDERBOOKTEST3" asks = []orderbook.Item{ {Price: 511, Amount: 1, ID: 1}, {Price: 52, Amount: 0.5, ID: 2}, @@ -546,28 +710,34 @@ func TestInsertingSnapShots(t *testing.T) { snapShot3.Bids = bids snapShot3.AssetType = "FUTURES" snapShot3.Pair = currency.NewPairFromString("LTCUSD") - err = obl.LoadSnapshot(&snapShot3, false) + err = obl.LoadSnapshot(&snapShot3) if err != nil { t.Fatal(err) } if obl.ob[snapShot1.Pair][snapShot1.AssetType].Asks[0] != snapShot1.Asks[0] { - t.Errorf("loaded data mismatch. Expected %v, received %v", snapShot1.Asks[0], obl.ob[snapShot1.Pair][snapShot1.AssetType].Asks[0]) + t.Errorf("loaded data mismatch. Expected %v, received %v", + snapShot1.Asks[0], + obl.ob[snapShot1.Pair][snapShot1.AssetType].Asks[0]) } if obl.ob[snapShot2.Pair][snapShot2.AssetType].Asks[0] != snapShot2.Asks[0] { - t.Errorf("loaded data mismatch. Expected %v, received %v", snapShot2.Asks[0], obl.ob[snapShot2.Pair][snapShot2.AssetType].Asks[0]) + t.Errorf("loaded data mismatch. Expected %v, received %v", + snapShot2.Asks[0], + obl.ob[snapShot2.Pair][snapShot2.AssetType].Asks[0]) } if obl.ob[snapShot3.Pair][snapShot3.AssetType].Asks[0] != snapShot3.Asks[0] { - t.Errorf("loaded data mismatch. Expected %v, received %v", snapShot3.Asks[0], obl.ob[snapShot3.Pair][snapShot3.AssetType].Asks[0]) + t.Errorf("loaded data mismatch. Expected %v, received %v", + snapShot3.Asks[0], + obl.ob[snapShot3.Pair][snapShot3.AssetType].Asks[0]) } } func TestGetOrderbook(t *testing.T) { - obl, curr, _, _, err := createSnapshot() + obl, _, _, err := createSnapshot() if err != nil { t.Fatal(err) } - ob := obl.GetOrderbook(curr, spot) - if obl.ob[curr][spot] != ob { + ob := obl.GetOrderbook(cp, asset.Spot) + if obl.ob[cp][asset.Spot] != ob { t.Error("Failed to get orderbook") } } @@ -575,7 +745,35 @@ func TestGetOrderbook(t *testing.T) { func TestSetup(t *testing.T) { w := WebsocketOrderbookLocal{} w.Setup(1, true, true, true, true, "hi") - if w.obBufferLimit != 1 || !w.bufferEnabled || !w.sortBuffer || !w.sortBufferByUpdateIDs || !w.updateEntriesByID || w.exchangeName != "hi" { + if w.obBufferLimit != 1 || + !w.bufferEnabled || + !w.sortBuffer || + !w.sortBufferByUpdateIDs || + !w.updateEntriesByID || + w.exchangeName != "hi" { t.Errorf("Setup incorrectly loaded %s", w.exchangeName) } } + +func TestEnsureMultipleUpdatesViaPrice(t *testing.T) { + obl, _, _, err := createSnapshot() + if err != nil { + t.Error(err) + } + + asks := bidAskGenerator() + obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &WebsocketOrderbookUpdate{ + Bids: asks, + Asks: asks, + Pair: cp, + UpdateTime: time.Now(), + Asset: asset.Spot, + }) + if err != nil { + t.Error(err) + } + + if len(obl.ob[cp][asset.Spot].Asks) <= 3 { + t.Errorf("Insufficient updates") + } +} diff --git a/exchanges/websocket/wsorderbook/wsorderbook_types.go b/exchanges/websocket/wsorderbook/wsorderbook_types.go index 07d7fc04..2ae77a29 100644 --- a/exchanges/websocket/wsorderbook/wsorderbook_types.go +++ b/exchanges/websocket/wsorderbook/wsorderbook_types.go @@ -5,14 +5,15 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" ) // WebsocketOrderbookLocal defines a local cache of orderbooks for amending, // appending and deleting changes and updates the main store in wsorderbook.go type WebsocketOrderbookLocal struct { - ob map[currency.Pair]map[string]*orderbook.Base - buffer map[currency.Pair]map[string][]WebsocketOrderbookUpdate + ob map[currency.Pair]map[asset.Item]*orderbook.Base + buffer map[currency.Pair]map[asset.Item][]*WebsocketOrderbookUpdate obBufferLimit int bufferEnabled bool sortBuffer bool @@ -24,11 +25,11 @@ type WebsocketOrderbookLocal struct { // WebsocketOrderbookUpdate stores orderbook updates and dictates what features to use when processing type WebsocketOrderbookUpdate struct { - UpdateID int64 // Used when no time is provided - UpdateTime time.Time - AssetType string - Action string // Used in conjunction with UpdateEntriesByID - Bids []orderbook.Item - Asks []orderbook.Item - CurrencyPair currency.Pair + UpdateID int64 // Used when no time is provided + UpdateTime time.Time + Asset asset.Item + Action string // Used in conjunction with UpdateEntriesByID + Bids []orderbook.Item + Asks []orderbook.Item + Pair currency.Pair } diff --git a/exchanges/yobit/README.md b/exchanges/yobit/README.md index cb3082b3..fc89aca6 100644 --- a/exchanges/yobit/README.md +++ b/exchanges/yobit/README.md @@ -47,22 +47,22 @@ main.go ```go var y exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "Yobit" { - y = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Yobit" { + y = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := y.GetTickerPrice() +tick, err := y.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := y.GetOrderbookEx() +ob, err := y.FetchOrderbook() if err != nil { // Handle error } @@ -130,4 +130,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/yobit/yobit.go b/exchanges/yobit/yobit.go index 0569b32c..da82d928 100644 --- a/exchanges/yobit/yobit.go +++ b/exchanges/yobit/yobit.go @@ -7,15 +7,10 @@ import ( "net/url" "strconv" "strings" - "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) @@ -45,83 +40,12 @@ const ( // Yobit is the overarching type across the Yobit package type Yobit struct { exchange.Base - Ticker map[string]Ticker -} - -// SetDefaults sets current default value for Yobit -func (y *Yobit) SetDefaults() { - y.Name = "Yobit" - y.Enabled = true - y.Fee = 0.2 - y.Verbose = false - y.RESTPollingDelay = 10 - y.AuthenticatedAPISupport = true - y.Ticker = make(map[string]Ticker) - y.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithAPIPermission | - exchange.WithdrawFiatViaWebsiteOnly - y.RequestCurrencyPairFormat.Delimiter = "_" - y.RequestCurrencyPairFormat.Uppercase = false - y.RequestCurrencyPairFormat.Separator = "-" - y.ConfigCurrencyPairFormat.Delimiter = "_" - y.ConfigCurrencyPairFormat.Uppercase = true - y.AssetTypes = []string{ticker.Spot} - y.SupportsAutoPairUpdating = false - y.SupportsRESTTickerBatching = true - y.Requester = request.New(y.Name, - request.NewRateLimit(time.Second, yobitAuthRate), - request.NewRateLimit(time.Second, yobitUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - y.APIUrlDefault = apiPublicURL - y.APIUrl = y.APIUrlDefault - y.APIUrlSecondaryDefault = apiPrivateURL - y.APIUrlSecondary = y.APIUrlSecondaryDefault - y.Websocket = wshandler.New() -} - -// Setup sets exchange configuration parameters for Yobit -func (y *Yobit) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - y.SetEnabled(false) - } else { - y.Enabled = true - y.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - y.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - y.RESTPollingDelay = exch.RESTPollingDelay - y.Verbose = exch.Verbose - y.HTTPDebugging = exch.HTTPDebugging - y.Websocket.SetWsStatusAndConnection(exch.Websocket) - y.BaseCurrencies = exch.BaseCurrencies - y.AvailablePairs = exch.AvailablePairs - y.EnabledPairs = exch.EnabledPairs - y.SetHTTPClientTimeout(exch.HTTPTimeout) - y.SetHTTPClientUserAgent(exch.HTTPUserAgent) - err := y.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = y.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = y.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = y.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = y.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - } } // GetInfo returns the Yobit info func (y *Yobit) GetInfo() (Info, error) { resp := Info{} - path := fmt.Sprintf("%s/%s/%s/", y.APIUrl, apiPublicVersion, publicInfo) + path := fmt.Sprintf("%s/%s/%s/", y.API.Endpoints.URL, apiPublicVersion, publicInfo) return resp, y.SendHTTPRequest(path, &resp) } @@ -133,7 +57,7 @@ func (y *Yobit) GetTicker(symbol string) (map[string]Ticker, error) { } response := Response{} - path := fmt.Sprintf("%s/%s/%s/%s", y.APIUrl, apiPublicVersion, publicTicker, symbol) + path := fmt.Sprintf("%s/%s/%s/%s", y.API.Endpoints.URL, apiPublicVersion, publicTicker, symbol) return response.Data, y.SendHTTPRequest(path, &response.Data) } @@ -145,7 +69,7 @@ func (y *Yobit) GetDepth(symbol string) (Orderbook, error) { } response := Response{} - path := fmt.Sprintf("%s/%s/%s/%s", y.APIUrl, apiPublicVersion, publicDepth, symbol) + path := fmt.Sprintf("%s/%s/%s/%s", y.API.Endpoints.URL, apiPublicVersion, publicDepth, symbol) return response.Data[symbol], y.SendHTTPRequest(path, &response.Data) @@ -158,7 +82,7 @@ func (y *Yobit) GetTrades(symbol string) ([]Trades, error) { } response := Response{} - path := fmt.Sprintf("%s/%s/%s/%s", y.APIUrl, apiPublicVersion, publicTrades, symbol) + path := fmt.Sprintf("%s/%s/%s/%s", y.API.Endpoints.URL, apiPublicVersion, publicTrades, symbol) return response.Data[symbol], y.SendHTTPRequest(path, &response.Data) } @@ -181,7 +105,7 @@ func (y *Yobit) GetAccountInformation() (AccountInfo, error) { func (y *Yobit) Trade(pair, orderType string, amount, price float64) (int64, error) { req := url.Values{} req.Add("pair", pair) - req.Add("type", orderType) + req.Add("type", strings.ToLower(orderType)) req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) req.Add("rate", strconv.FormatFloat(price, 'f', -1, 64)) @@ -218,7 +142,7 @@ func (y *Yobit) GetOrderInformation(orderID int64) (map[string]OrderInfo, error) } // CancelExistingOrder cancels an order for a specific order ID -func (y *Yobit) CancelExistingOrder(orderID int64) (bool, error) { +func (y *Yobit) CancelExistingOrder(orderID int64) error { req := url.Values{} req.Add("order_id", strconv.FormatInt(orderID, 10)) @@ -226,12 +150,12 @@ func (y *Yobit) CancelExistingOrder(orderID int64) (bool, error) { err := y.SendAuthenticatedHTTPRequest(privateCancelOrder, req, &result) if err != nil { - return false, err + return err } if result.Error != "" { - return false, errors.New(result.Error) + return errors.New(result.Error) } - return true, nil + return nil } // GetTradeHistory returns the trade history @@ -346,9 +270,8 @@ func (y *Yobit) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to Yobit func (y *Yobit) SendAuthenticatedHTTPRequest(path string, params url.Values, result interface{}) (err error) { - if !y.AuthenticatedAPISupport { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, - y.Name) + if !y.AllowAuthenticatedRequest() { + return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, y.Name) } if params == nil { @@ -361,20 +284,18 @@ func (y *Yobit) SendAuthenticatedHTTPRequest(path string, params url.Values, res params.Set("method", path) encoded := params.Encode() - hmac := common.GetHMAC(common.HashSHA512, - []byte(encoded), - []byte(y.APISecret)) + hmac := crypto.GetHMAC(crypto.HashSHA512, []byte(encoded), []byte(y.API.Credentials.Secret)) if y.Verbose { - log.Debugf("Sending POST request to %s calling path %s with params %s\n", + log.Debugf(log.ExchangeSys, "Sending POST request to %s calling path %s with params %s\n", apiPrivateURL, path, encoded) } headers := make(map[string]string) - headers["Key"] = y.APIKey - headers["Sign"] = common.HexEncodeToString(hmac) + headers["Key"] = y.API.Credentials.Key + headers["Sign"] = crypto.HexEncodeToString(hmac) headers["Content-Type"] = "application/x-www-form-urlencoded" return y.SendPayload(http.MethodPost, diff --git a/exchanges/yobit/yobit_test.go b/exchanges/yobit/yobit_test.go index d8bd220a..4a2dafab 100644 --- a/exchanges/yobit/yobit_test.go +++ b/exchanges/yobit/yobit_test.go @@ -1,7 +1,9 @@ package yobit import ( + "log" "math" + "os" "testing" "time" @@ -9,6 +11,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) var y Yobit @@ -20,29 +24,41 @@ const ( canManipulateRealOrders = false ) -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { y.SetDefaults() -} - -func TestSetup(t *testing.T) { yobitConfig := config.GetConfig() - yobitConfig.LoadConfig("../../testdata/configtest.json") + err := yobitConfig.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Yobit load config error", err) + } conf, err := yobitConfig.GetExchangeConfig("Yobit") if err != nil { - t.Error("Test Failed - Yobit init error") + log.Fatal("Yobit init error", err) } - conf.APIKey = apiKey - conf.APISecret = apiSecret - conf.AuthenticatedAPISupport = true + conf.API.Credentials.Key = apiKey + conf.API.Credentials.Secret = apiSecret + conf.API.AuthenticatedSupport = true - y.Setup(&conf) + err = y.Setup(conf) + if err != nil { + log.Fatal("Yobit setup error", err) + } + os.Exit(m.Run()) +} + +func TestFetchTradablePairs(t *testing.T) { + t.Parallel() + _, err := y.FetchTradablePairs(asset.Spot) + if err != nil { + t.Errorf("FetchTradablePairs err: %s", err) + } } func TestGetInfo(t *testing.T) { t.Parallel() _, err := y.GetInfo() if err != nil { - t.Error("Test Failed - GetInfo() error") + t.Error("GetInfo() error") } } @@ -50,7 +66,7 @@ func TestGetTicker(t *testing.T) { t.Parallel() _, err := y.GetTicker("btc_usd") if err != nil { - t.Error("Test Failed - GetTicker() error", err) + t.Error("GetTicker() error", err) } } @@ -58,7 +74,7 @@ func TestGetDepth(t *testing.T) { t.Parallel() _, err := y.GetDepth("btc_usd") if err != nil { - t.Error("Test Failed - GetDepth() error", err) + t.Error("GetDepth() error", err) } } @@ -66,7 +82,7 @@ func TestGetTrades(t *testing.T) { t.Parallel() _, err := y.GetTrades("btc_usd") if err != nil { - t.Error("Test Failed - GetTrades() error", err) + t.Error("GetTrades() error", err) } } @@ -74,7 +90,7 @@ func TestGetAccountInfo(t *testing.T) { t.Parallel() _, err := y.GetAccountInfo() if err == nil { - t.Error("Test Failed - GetAccountInfo() error", err) + t.Error("GetAccountInfo() Expected error") } } @@ -82,7 +98,7 @@ func TestGetOpenOrders(t *testing.T) { t.Parallel() _, err := y.GetOpenOrders("") if err == nil { - t.Error("Test Failed - GetOpenOrders() error", err) + t.Error("GetOpenOrders() Expected error") } } @@ -90,23 +106,23 @@ func TestGetOrderInfo(t *testing.T) { t.Parallel() _, err := y.GetOrderInfo("6196974") if err == nil { - t.Error("Test Failed - GetOrderInfo() error", err) + t.Error("GetOrderInfo() Expected error") } } func TestCancelOrder(t *testing.T) { t.Parallel() - _, err := y.CancelExistingOrder(1337) + err := y.CancelExistingOrder(1337) if err == nil { - t.Error("Test Failed - CancelOrder() error", err) + t.Error("CancelOrder() Expected error") } } func TestTrade(t *testing.T) { t.Parallel() - _, err := y.Trade("", "buy", 0, 0) + _, err := y.Trade("", order.Buy.String(), 0, 0) if err == nil { - t.Error("Test Failed - Trade() error", err) + t.Error("Trade() Expected error") } } @@ -114,7 +130,7 @@ func TestWithdrawCoinsToAddress(t *testing.T) { t.Parallel() _, err := y.WithdrawCoinsToAddress("", 0, "") if err == nil { - t.Error("Test Failed - WithdrawCoinsToAddress() error", err) + t.Error("WithdrawCoinsToAddress() Expected error") } } @@ -122,7 +138,7 @@ func TestCreateYobicode(t *testing.T) { t.Parallel() _, err := y.CreateCoupon("bla", 0) if err == nil { - t.Error("Test Failed - CreateYobicode() error", err) + t.Error("CreateYobicode() Expected error") } } @@ -130,7 +146,7 @@ func TestRedeemYobicode(t *testing.T) { t.Parallel() _, err := y.RedeemCoupon("bla2") if err == nil { - t.Error("Test Failed - RedeemYobicode() error", err) + t.Error("RedeemYobicode() Expected error") } } @@ -151,7 +167,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() y.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -163,14 +179,12 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - y.SetDefaults() - TestSetup(t) var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic if resp, err := y.GetFee(feeBuilder); resp != float64(0.002) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) } // CryptocurrencyTradeFee High quantity @@ -178,7 +192,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := y.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) t.Error(err) } @@ -186,7 +200,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := y.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) t.Error(err) } @@ -194,14 +208,14 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := y.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) t.Error(err) } @@ -210,7 +224,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -218,7 +232,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -226,7 +240,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -235,7 +249,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -245,7 +259,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FiatCurrency = currency.USD feeBuilder.BankTransactionType = exchange.Qiwi if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -255,7 +269,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FiatCurrency = currency.USD feeBuilder.BankTransactionType = exchange.WireTransfer if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -265,7 +279,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FiatCurrency = currency.USD feeBuilder.BankTransactionType = exchange.Payeer if resp, err := y.GetFee(feeBuilder); resp != float64(0.03) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.03), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.03), resp) t.Error(err) } @@ -275,7 +289,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FiatCurrency = currency.RUR feeBuilder.BankTransactionType = exchange.Capitalist if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -285,7 +299,7 @@ func TestGetFee(t *testing.T) { feeBuilder.FiatCurrency = currency.USD feeBuilder.BankTransactionType = exchange.AdvCash if resp, err := y.GetFee(feeBuilder); resp != float64(0.04) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.04), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.04), resp) t.Error(err) } @@ -295,28 +309,22 @@ func TestGetFee(t *testing.T) { feeBuilder.FiatCurrency = currency.RUR feeBuilder.BankTransactionType = exchange.PerfectMoney if resp, err := y.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - y.SetDefaults() expectedResult := exchange.AutoWithdrawCryptoWithAPIPermissionText + " & " + exchange.WithdrawFiatViaWebsiteOnlyText - withdrawPermissions := y.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - y.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, } @@ -330,11 +338,8 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - y.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, Currencies: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, StartTicks: time.Unix(0, 0), @@ -352,27 +357,27 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if y.APIKey != "" && y.APIKey != "Key" && - y.APISecret != "" && y.APISecret != "Secret" { - return true - } - return false + return y.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - y.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var pair = currency.Pair{ - Delimiter: "_", - Base: currency.BTC, - Quote: currency.USD, + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: "_", + Base: currency.BTC, + Quote: currency.USD, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - response, err := y.SubmitOrder(pair, exchange.BuyOrderSide, exchange.MarketOrderType, 1, 10, "hi") + response, err := y.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -381,16 +386,12 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - y.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -407,16 +408,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - y.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -432,26 +429,29 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestModifyOrder(t *testing.T) { - _, err := y.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := y.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - y.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.LTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -468,47 +468,43 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - y.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := y.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) + t.Errorf("Expected '%v', received: '%v'", + common.ErrFunctionNotSupported, + err) } } func TestWithdrawInternationalBank(t *testing.T) { - y.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := y.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) + t.Errorf("Expected '%v', received: '%v'", + common.ErrFunctionNotSupported, + err) } } func TestGetDepositAddress(t *testing.T) { - if apiKey != "" || apiSecret != "" { + if areTestAPIKeysSet() { _, err := y.GetDepositAddress(currency.BTC, "") if err != nil { - t.Error("Test Failed - GetDepositAddress() error", err) + t.Error("GetDepositAddress() Expected error") } } else { _, err := y.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error") + t.Error("GetDepositAddress() error") } } } diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index 010804aa..363f8208 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -2,7 +2,6 @@ package yobit import ( "errors" - "fmt" "math" "strconv" "strings" @@ -10,15 +9,118 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) -// Start starts the Yobit go routine +// GetDefaultConfig returns a default exchange config +func (y *Yobit) GetDefaultConfig() (*config.ExchangeConfig, error) { + y.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = y.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = y.BaseCurrencies + + err := y.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if y.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = y.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets current default value for Yobit +func (y *Yobit) SetDefaults() { + y.Name = "Yobit" + y.Enabled = true + y.Verbose = true + y.API.CredentialsValidator.RequiresKey = true + y.API.CredentialsValidator.RequiresSecret = true + + y.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "_", + Uppercase: false, + Separator: "-", + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "_", + Uppercase: true, + }, + } + + y.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: false, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + UserTradeHistory: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + FiatDepositFee: true, + FiatWithdrawalFee: true, + CryptoWithdrawalFee: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | + exchange.WithdrawFiatViaWebsiteOnly, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + y.Requester = request.New(y.Name, + request.NewRateLimit(time.Second, yobitAuthRate), + request.NewRateLimit(time.Second, yobitUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + y.API.Endpoints.URLDefault = apiPublicURL + y.API.Endpoints.URL = y.API.Endpoints.URLDefault + y.API.Endpoints.URLSecondaryDefault = apiPrivateURL + y.API.Endpoints.URLSecondary = y.API.Endpoints.URLSecondaryDefault +} + +// Setup sets exchange configuration parameters for Yobit +func (y *Yobit) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + y.SetEnabled(false) + return nil + } + + return y.SetupDefaults(exch) +} + +// Start starts the WEX go routine func (y *Yobit) Start(wg *sync.WaitGroup) { wg.Add(1) go func() { @@ -30,16 +132,52 @@ func (y *Yobit) Start(wg *sync.WaitGroup) { // Run implements the Yobit wrapper func (y *Yobit) Run() { if y.Verbose { - log.Debugf("%s Websocket: %s.", y.GetName(), common.IsEnabled(y.Websocket.IsEnabled())) - log.Debugf("%s polling delay: %ds.\n", y.GetName(), y.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", y.GetName(), len(y.EnabledPairs), y.EnabledPairs) + y.PrintEnabledPairs() + } + + if !y.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := y.UpdateTradablePairs(false) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + y.Name, + err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (y *Yobit) FetchTradablePairs(asset asset.Item) ([]string, error) { + info, err := y.GetInfo() + if err != nil { + return nil, err + } + + var currencies []string + for x := range info.Pairs { + currencies = append(currencies, strings.ToUpper(x)) + } + + return currencies, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (y *Yobit) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := y.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return y.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (y *Yobit) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (y *Yobit) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - pairsCollated, err := exchange.GetAndFormatExchangeCurrencies(y.Name, y.GetEnabledCurrencies()) + pairsCollated, err := y.FormatExchangeCurrencies(y.GetEnabledPairs(assetType), assetType) if err != nil { return tickerPrice, err } @@ -49,37 +187,43 @@ func (y *Yobit) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, e return tickerPrice, err } - for _, x := range y.GetEnabledCurrencies() { - currency := exchange.FormatExchangeCurrency(y.Name, x).Lower().String() + enabledPairs := y.GetEnabledPairs(assetType) + for i := range enabledPairs { + curr := y.FormatExchangeCurrency(enabledPairs[i], assetType).Lower().String() + if _, ok := result[curr]; !ok { + continue + } + resultCurr := result[curr] var tickerPrice ticker.Price - tickerPrice.Pair = x - tickerPrice.Last = result[currency].Last - tickerPrice.Ask = result[currency].Sell - tickerPrice.Bid = result[currency].Buy - tickerPrice.Last = result[currency].Last - tickerPrice.Low = result[currency].Low - tickerPrice.Volume = result[currency].VolumeCurrent + tickerPrice.Pair = enabledPairs[i] + tickerPrice.Last = resultCurr.Last + tickerPrice.Ask = resultCurr.Sell + tickerPrice.Bid = resultCurr.Buy + tickerPrice.Last = resultCurr.Last + tickerPrice.Low = resultCurr.Low + tickerPrice.QuoteVolume = resultCurr.VolumeCurrent + tickerPrice.Volume = resultCurr.Vol err = ticker.ProcessTicker(y.Name, &tickerPrice, assetType) if err != nil { - return tickerPrice, err + log.Error(log.Ticker, err) } } return ticker.GetTicker(y.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (y *Yobit) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tick, err := ticker.GetTicker(y.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (y *Yobit) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tick, err := ticker.GetTicker(y.Name, p, assetType) if err != nil { return y.UpdateTicker(p, assetType) } return tick, nil } -// GetOrderbookEx returns the orderbook for a currency pair -func (y *Yobit) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(y.GetName(), p, assetType) +// FetchOrderbook returns the orderbook for a currency pair +func (y *Yobit) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(y.Name, p, assetType) if err != nil { return y.UpdateOrderbook(p, assetType) } @@ -87,25 +231,31 @@ func (y *Yobit) GetOrderbookEx(p currency.Pair, assetType string) (orderbook.Bas } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (y *Yobit) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (y *Yobit) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := y.GetDepth(exchange.FormatExchangeCurrency(y.Name, p).String()) + orderbookNew, err := y.GetDepth(y.FormatExchangeCurrency(p, assetType).String()) if err != nil { return orderBook, err } - for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Price: data[0], Amount: data[1]}) + for i := range orderbookNew.Bids { + orderBook.Bids = append(orderBook.Bids, + orderbook.Item{ + Price: orderbookNew.Bids[i][0], + Amount: orderbookNew.Bids[i][1], + }) } - for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Price: data[0], Amount: data[1]}) + for i := range orderbookNew.Asks { + orderBook.Asks = append(orderBook.Asks, + orderbook.Item{ + Price: orderbookNew.Asks[i][0], + Amount: orderbookNew.Asks[i][1], + }) } orderBook.Pair = p - orderBook.ExchangeName = y.GetName() + orderBook.ExchangeName = y.Name orderBook.AssetType = assetType err = orderBook.Process() @@ -120,7 +270,7 @@ func (y *Yobit) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Ba // Yobit exchange func (y *Yobit) GetAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.Exchange = y.GetName() + response.Exchange = y.Name accountBalance, err := y.GetAccountInformation() if err != nil { return response, err @@ -151,64 +301,68 @@ func (y *Yobit) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (y *Yobit) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (y *Yobit) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (y *Yobit) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order // Yobit only supports limit orders -func (y *Yobit) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse +func (y *Yobit) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + if err := s.Validate(); err != nil { + return submitOrderResponse, err + } - if orderType != exchange.LimitOrderType { + if s.OrderType != order.Limit { return submitOrderResponse, errors.New("only limit orders are allowed") } - response, err := y.Trade(p.String(), side.ToString(), amount, price) + response, err := y.Trade(s.Pair.String(), + s.OrderSide.String(), + s.Amount, + s.Price) + if err != nil { + return submitOrderResponse, err + } if response > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response) + submitOrderResponse.OrderID = strconv.FormatInt(response, 10) } - if err == nil { - submitOrderResponse.IsOrderPlaced = true - } - - return submitOrderResponse, err + submitOrderResponse.IsOrderPlaced = true + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (y *Yobit) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (y *Yobit) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (y *Yobit) CancelOrder(order *exchange.OrderCancellation) error { +func (y *Yobit) CancelOrder(order *order.Cancel) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) if err != nil { return err } - _, err = y.CancelExistingOrder(orderIDInt) - return err + return y.CancelExistingOrder(orderIDInt) } // CancelAllOrders cancels all orders associated with a currency pair -func (y *Yobit) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (y *Yobit) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } - var allActiveOrders []map[string]ActiveOrders - for _, pair := range y.EnabledPairs { - activeOrdersForPair, err := y.GetOpenOrders(pair.String()) + var allActiveOrders []map[string]ActiveOrders + enabledPairs := y.GetEnabledPairs(asset.Spot) + for i := range enabledPairs { + fCurr := y.FormatExchangeCurrency(enabledPairs[i], asset.Spot).String() + activeOrdersForPair, err := y.GetOpenOrders(fCurr) if err != nil { return cancelAllOrdersResponse, err } @@ -216,17 +370,17 @@ func (y *Yobit) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelA allActiveOrders = append(allActiveOrders, activeOrdersForPair) } - for _, activeOrders := range allActiveOrders { - for key := range activeOrders { + for i := range allActiveOrders { + for key := range allActiveOrders[i] { orderIDInt, err := strconv.ParseInt(key, 10, 64) if err != nil { - cancelAllOrdersResponse.OrderStatus[key] = err.Error() + cancelAllOrdersResponse.Status[key] = err.Error() continue } - _, err = y.CancelExistingOrder(orderIDInt) + err = y.CancelExistingOrder(orderIDInt) if err != nil { - cancelAllOrdersResponse.OrderStatus[key] = err.Error() + cancelAllOrdersResponse.Status[key] = err.Error() } } } @@ -235,8 +389,8 @@ func (y *Yobit) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelA } // GetOrderInfo returns information on a current open order -func (y *Yobit) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (y *Yobit) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -252,7 +406,7 @@ func (y *Yobit) GetDepositAddress(cryptocurrency currency.Code, _ string) (strin // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (y *Yobit) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (y *Yobit) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { resp, err := y.WithdrawCoinsToAddress(withdrawRequest.Currency.String(), withdrawRequest.Amount, withdrawRequest.Address) if err != nil { return "", err @@ -265,13 +419,13 @@ func (y *Yobit) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRe // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (y *Yobit) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (y *Yobit) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (y *Yobit) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (y *Yobit) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -282,7 +436,7 @@ func (y *Yobit) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (y *Yobit) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (y.APIKey == "" || y.APISecret == "") && // Todo check connection status + if !y.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -290,24 +444,24 @@ func (y *Yobit) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { } // GetActiveOrders retrieves any orders that are active/open -func (y *Yobit) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - var orders []exchange.OrderDetail - for _, c := range getOrdersRequest.Currencies { - resp, err := y.GetOpenOrders(exchange.FormatExchangeCurrency(y.Name, - c).String()) +func (y *Yobit) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { + var orders []order.Detail + for x := range req.Currencies { + fCurr := y.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String() + resp, err := y.GetOpenOrders(fCurr) if err != nil { return nil, err } - for ID, order := range resp { - symbol := currency.NewPairDelimiter(order.Pair, - y.ConfigCurrencyPairFormat.Delimiter) - orderDate := time.Unix(int64(order.TimestampCreated), 0) - side := exchange.OrderSide(strings.ToUpper(order.Type)) - orders = append(orders, exchange.OrderDetail{ - ID: ID, - Amount: order.Amount, - Price: order.Rate, + for id := range resp { + symbol := currency.NewPairDelimiter(resp[id].Pair, + y.GetPairFormat(asset.Spot, false).Delimiter) + orderDate := time.Unix(int64(resp[id].TimestampCreated), 0) + side := order.Side(strings.ToUpper(resp[id].Type)) + orders = append(orders, order.Detail{ + ID: id, + Amount: resp[id].Amount, + Price: resp[id].Rate, OrderSide: side, OrderDate: orderDate, CurrencyPair: symbol, @@ -316,44 +470,42 @@ func (y *Yobit) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([] } } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status -func (y *Yobit) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (y *Yobit) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { var allOrders []TradeHistory - for _, currency := range getOrdersRequest.Currencies { + for x := range req.Currencies { resp, err := y.GetTradeHistory(0, 10000, math.MaxInt64, - getOrdersRequest.StartTicks.Unix(), - getOrdersRequest.EndTicks.Unix(), + req.StartTicks.Unix(), + req.EndTicks.Unix(), "DESC", - exchange.FormatExchangeCurrency(y.Name, currency).String()) + y.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String()) if err != nil { return nil, err } - for _, order := range resp { - allOrders = append(allOrders, order) + for key := range resp { + allOrders = append(allOrders, resp[key]) } } - var orders []exchange.OrderDetail - for _, order := range allOrders { - symbol := currency.NewPairDelimiter(order.Pair, - y.ConfigCurrencyPairFormat.Delimiter) - orderDate := time.Unix(int64(order.Timestamp), 0) - side := exchange.OrderSide(strings.ToUpper(order.Type)) - orders = append(orders, exchange.OrderDetail{ - ID: fmt.Sprintf("%v", order.OrderID), - Amount: order.Amount, - Price: order.Rate, + var orders []order.Detail + for i := range allOrders { + symbol := currency.NewPairDelimiter(allOrders[i].Pair, + y.GetPairFormat(asset.Spot, false).Delimiter) + orderDate := time.Unix(int64(allOrders[i].Timestamp), 0) + side := order.Side(strings.ToUpper(allOrders[i].Type)) + orders = append(orders, order.Detail{ + ID: strconv.FormatFloat(allOrders[i].OrderID, 'f', -1, 64), + Amount: allOrders[i].Amount, + Price: allOrders[i].Rate, OrderSide: side, OrderDate: orderDate, CurrencyPair: symbol, @@ -361,7 +513,7 @@ func (y *Yobit) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([] }) } - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } diff --git a/exchanges/zb/README.md b/exchanges/zb/README.md index 2a81723e..4d94cb06 100644 --- a/exchanges/zb/README.md +++ b/exchanges/zb/README.md @@ -47,22 +47,22 @@ main.go ```go var z exchange.IBotExchange -for i := range bot.exchanges { - if bot.exchanges[i].GetName() == "ZB" { - z = bot.exchanges[i] +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "ZB" { + z = bot.Exchanges[i] } } // Public calls - wrapper functions // Fetches current ticker information -tick, err := z.GetTickerPrice() +tick, err := z.FetchTicker() if err != nil { // Handle error } // Fetches current orderbook information -ob, err := z.GetOrderbookEx() +ob, err := z.FetchOrderbook() if err != nil { // Handle error } @@ -137,4 +137,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/exchanges/zb/zb.go b/exchanges/zb/zb.go index d9a3e553..dfb5d457 100644 --- a/exchanges/zb/zb.go +++ b/exchanges/zb/zb.go @@ -10,14 +10,11 @@ import ( "strings" "time" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( @@ -50,121 +47,12 @@ type ZB struct { exchange.Base } -// SetDefaults sets default values for the exchange -func (z *ZB) SetDefaults() { - z.Name = "ZB" - z.Enabled = false - z.Fee = 0 - z.Verbose = false - z.RESTPollingDelay = 10 - z.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | - exchange.NoFiatWithdrawals - z.RequestCurrencyPairFormat.Delimiter = "_" - z.RequestCurrencyPairFormat.Uppercase = false - z.ConfigCurrencyPairFormat.Delimiter = "_" - z.ConfigCurrencyPairFormat.Uppercase = true - z.AssetTypes = []string{ticker.Spot} - z.SupportsAutoPairUpdating = true - z.SupportsRESTTickerBatching = true - z.Requester = request.New(z.Name, - request.NewRateLimit(time.Second*10, zbAuthRate), - request.NewRateLimit(time.Second*10, zbUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - z.APIUrlDefault = zbTradeURL - z.APIUrl = z.APIUrlDefault - z.APIUrlSecondaryDefault = zbMarketURL - z.APIUrlSecondary = z.APIUrlSecondaryDefault - z.Websocket = wshandler.New() - z.Websocket.Functionality = wshandler.WebsocketTickerSupported | - wshandler.WebsocketOrderbookSupported | - wshandler.WebsocketTradeDataSupported | - wshandler.WebsocketSubscribeSupported | - wshandler.WebsocketAuthenticatedEndpointsSupported | - wshandler.WebsocketAccountDataSupported | - wshandler.WebsocketCancelOrderSupported | - wshandler.WebsocketSubmitOrderSupported | - wshandler.WebsocketMessageCorrelationSupported - z.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - z.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - z.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup sets user configuration -func (z *ZB) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - z.SetEnabled(false) - } else { - z.Enabled = true - z.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - z.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - z.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - z.APIAuthPEMKey = exch.APIAuthPEMKey - z.SetHTTPClientTimeout(exch.HTTPTimeout) - z.SetHTTPClientUserAgent(exch.HTTPUserAgent) - z.RESTPollingDelay = exch.RESTPollingDelay - z.Verbose = exch.Verbose - z.HTTPDebugging = exch.HTTPDebugging - z.Websocket.SetWsStatusAndConnection(exch.Websocket) - z.BaseCurrencies = exch.BaseCurrencies - z.AvailablePairs = exch.AvailablePairs - z.EnabledPairs = exch.EnabledPairs - err := z.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = z.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = z.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = z.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = z.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - err = z.Websocket.Setup(z.WsConnect, - z.Subscribe, - nil, - exch.Name, - exch.Websocket, - exch.Verbose, - zbWebsocketAPI, - exch.WebsocketURL, - exch.AuthenticatedWebsocketAPISupport) - if err != nil { - log.Fatal(err) - } - z.WebsocketConn = &wshandler.WebsocketConnection{ - ExchangeName: z.Name, - URL: z.Websocket.GetWebsocketURL(), - ProxyURL: z.Websocket.GetProxyAddress(), - Verbose: z.Verbose, - RateLimit: zbWebsocketRateLimit, - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - } - z.Websocket.Orderbook.Setup( - exch.WebsocketOrderbookBufferLimit, - false, - false, - false, - false, - exch.Name) - } -} - // SpotNewOrder submits an order to ZB func (z *ZB) SpotNewOrder(arg SpotNewOrderRequestParams) (int64, error) { var result SpotNewOrderResponse vals := url.Values{} - vals.Set("accesskey", z.APIKey) + vals.Set("accesskey", z.API.Credentials.Key) vals.Set("method", "order") vals.Set("amount", strconv.FormatFloat(arg.Amount, 'f', -1, 64)) vals.Set("currency", arg.Symbol) @@ -193,7 +81,7 @@ func (z *ZB) CancelExistingOrder(orderID int64, symbol string) error { } vals := url.Values{} - vals.Set("accesskey", z.APIKey) + vals.Set("accesskey", z.API.Credentials.Key) vals.Set("method", "cancelOrder") vals.Set("id", strconv.FormatInt(orderID, 10)) vals.Set("currency", symbol) @@ -216,7 +104,7 @@ func (z *ZB) GetAccountInformation() (AccountsResponse, error) { var result AccountsResponse vals := url.Values{} - vals.Set("accesskey", z.APIKey) + vals.Set("accesskey", z.API.Credentials.Key) vals.Set("method", "getAccountInfo") return result, z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result) @@ -226,7 +114,7 @@ func (z *ZB) GetAccountInformation() (AccountsResponse, error) { func (z *ZB) GetUnfinishedOrdersIgnoreTradeType(currency string, pageindex, pagesize int64) ([]Order, error) { var result []Order vals := url.Values{} - vals.Set("accesskey", z.APIKey) + vals.Set("accesskey", z.API.Credentials.Key) vals.Set("method", zbUnfinishedOrdersIgnoreTradeType) vals.Set("currency", currency) vals.Set("pageIndex", strconv.FormatInt(pageindex, 10)) @@ -240,7 +128,7 @@ func (z *ZB) GetUnfinishedOrdersIgnoreTradeType(currency string, pageindex, page func (z *ZB) GetOrders(currency string, pageindex, side int64) ([]Order, error) { var response []Order vals := url.Values{} - vals.Set("accesskey", z.APIKey) + vals.Set("accesskey", z.API.Credentials.Key) vals.Set("method", zbGetOrdersGet) vals.Set("currency", currency) vals.Set("pageIndex", strconv.FormatInt(pageindex, 10)) @@ -251,7 +139,7 @@ func (z *ZB) GetOrders(currency string, pageindex, side int64) ([]Order, error) // GetMarkets returns market information including pricing, symbols and // each symbols decimal precision func (z *ZB) GetMarkets() (map[string]MarketResponseItem, error) { - endpoint := fmt.Sprintf("%s/%s/%s", z.APIUrl, zbAPIVersion, zbMarkets) + endpoint := fmt.Sprintf("%s/%s/%s", z.API.Endpoints.URL, zbAPIVersion, zbMarkets) var res interface{} err := z.SendHTTPRequest(endpoint, &res) @@ -287,25 +175,23 @@ func (z *ZB) GetLatestSpotPrice(symbol string) (float64, error) { // GetTicker returns a ticker for a given symbol func (z *ZB) GetTicker(symbol string) (TickerResponse, error) { - urlPath := fmt.Sprintf("%s/%s/%s?market=%s", z.APIUrl, zbAPIVersion, zbTicker, symbol) + urlPath := fmt.Sprintf("%s/%s/%s?market=%s", z.API.Endpoints.URL, zbAPIVersion, zbTicker, symbol) var res TickerResponse - err := z.SendHTTPRequest(urlPath, &res) return res, err } // GetTickers returns ticker data for all supported symbols func (z *ZB) GetTickers() (map[string]TickerChildResponse, error) { - urlPath := fmt.Sprintf("%s/%s/%s", z.APIUrl, zbAPIVersion, zbTickers) + urlPath := fmt.Sprintf("%s/%s/%s", z.API.Endpoints.URL, zbAPIVersion, zbTickers) resp := make(map[string]TickerChildResponse) - err := z.SendHTTPRequest(urlPath, &resp) return resp, err } // GetOrderbook returns the orderbook for a given symbol func (z *ZB) GetOrderbook(symbol string) (OrderbookResponse, error) { - urlPath := fmt.Sprintf("%s/%s/%s?market=%s", z.APIUrl, zbAPIVersion, zbDepth, symbol) + urlPath := fmt.Sprintf("%s/%s/%s?market=%s", z.API.Endpoints.URL, zbAPIVersion, zbDepth, symbol) var res OrderbookResponse err := z.SendHTTPRequest(urlPath, &res) @@ -313,6 +199,14 @@ func (z *ZB) GetOrderbook(symbol string) (OrderbookResponse, error) { return res, err } + if len(res.Asks) == 0 { + return res, fmt.Errorf("ZB GetOrderbook asks is empty") + } + + if len(res.Bids) == 0 { + return res, fmt.Errorf("ZB GetOrderbook bids is empty") + } + // reverse asks data var data [][]float64 for x := len(res.Asks); x > 0; x-- { @@ -335,7 +229,7 @@ func (z *ZB) GetSpotKline(arg KlinesRequestParams) (KLineResponse, error) { vals.Set("size", fmt.Sprintf("%d", arg.Size)) } - urlPath := fmt.Sprintf("%s/%s/%s?%s", z.APIUrl, zbAPIVersion, zbKline, vals.Encode()) + urlPath := fmt.Sprintf("%s/%s/%s?%s", z.API.Endpoints.URL, zbAPIVersion, zbKline, vals.Encode()) var res KLineResponse var rawKlines map[string]interface{} @@ -356,7 +250,7 @@ func (z *ZB) GetSpotKline(arg KlinesRequestParams) (KLineResponse, error) { return res, errors.New("zb rawKlines unmarshal failed") } for _, k := range rawKlineDatas { - ot, err := common.TimeFromUnixTimestampFloat(k[0]) + ot, err := convert.TimeFromUnixTimestampFloat(k[0]) if err != nil { return res, errors.New("zb cannot parse Kline.OpenTime") } @@ -405,21 +299,21 @@ func (z *ZB) SendHTTPRequest(path string, result interface{}) error { // SendAuthenticatedHTTPRequest sends authenticated requests to the zb API func (z *ZB) SendAuthenticatedHTTPRequest(httpMethod string, params url.Values, result interface{}) error { - if !z.AuthenticatedAPISupport { + if !z.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, z.Name) } - params.Set("accesskey", z.APIKey) + params.Set("accesskey", z.API.Credentials.Key) - hmac := common.GetHMAC(common.HashMD5, + hmac := crypto.GetHMAC(crypto.HashMD5, []byte(params.Encode()), - []byte(common.Sha1ToHex(z.APISecret))) + []byte(crypto.Sha1ToHex(z.API.Credentials.Secret))) - params.Set("reqTime", fmt.Sprintf("%d", common.UnixMillis(time.Now()))) + params.Set("reqTime", fmt.Sprintf("%d", convert.UnixMillis(time.Now()))) params.Set("sign", fmt.Sprintf("%x", hmac)) urlPath := fmt.Sprintf("%s/%s?%s", - z.APIUrlSecondary, + z.API.Endpoints.URLSecondary, params.Get("method"), params.Encode()) @@ -444,7 +338,7 @@ func (z *ZB) SendAuthenticatedHTTPRequest(httpMethod string, params url.Values, return err } - err = common.JSONDecode(intermediary, &errCap) + err = json.Unmarshal(intermediary, &errCap) if err == nil { if errCap.Code > 1000 { return fmt.Errorf("sendAuthenticatedHTTPRequest error code: %d message %s", @@ -453,7 +347,7 @@ func (z *ZB) SendAuthenticatedHTTPRequest(httpMethod string, params url.Values, } } - return common.JSONDecode(intermediary, result) + return json.Unmarshal(intermediary, result) } // GetFee returns an estimate of fee based on type of transaction @@ -528,11 +422,11 @@ func (z *ZB) Withdraw(currency, address, safepassword string, amount, fees float } vals := url.Values{} - vals.Set("accesskey", z.APIKey) - vals.Set("amount", fmt.Sprintf("%v", amount)) + vals.Set("accesskey", z.API.Credentials.Key) + vals.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) vals.Set("currency", currency) - vals.Set("fees", fmt.Sprintf("%v", fees)) - vals.Set("itransfer", fmt.Sprintf("%v", itransfer)) + vals.Set("fees", strconv.FormatFloat(fees, 'f', -1, 64)) + vals.Set("itransfer", strconv.FormatBool(itransfer)) vals.Set("method", "withdraw") vals.Set("recieveAddr", address) vals.Set("safePwd", safepassword) diff --git a/exchanges/zb/zb_test.go b/exchanges/zb/zb_test.go index d38a2959..3182e16e 100644 --- a/exchanges/zb/zb_test.go +++ b/exchanges/zb/zb_test.go @@ -1,8 +1,12 @@ package zb import ( + "encoding/json" "fmt" + "log" "net/http" + "os" + "strconv" "testing" "github.com/gorilla/websocket" @@ -10,6 +14,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) @@ -23,32 +28,35 @@ const ( var z ZB var wsSetupRan bool -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { z.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("ZB load config error", err) + } zbConfig, err := cfg.GetExchangeConfig("ZB") if err != nil { - t.Error("Test Failed - ZB Setup() init error") + log.Fatal("ZB Setup() init error", err) } - zbConfig.AuthenticatedAPISupport = true - zbConfig.AuthenticatedWebsocketAPISupport = true - zbConfig.APIKey = apiKey - zbConfig.APISecret = apiSecret + zbConfig.API.AuthenticatedSupport = true + zbConfig.API.AuthenticatedWebsocketSupport = true + zbConfig.API.Credentials.Key = apiKey + zbConfig.API.Credentials.Secret = apiSecret - z.Setup(&zbConfig) + err = z.Setup(zbConfig) + if err != nil { + log.Fatal("ZB setup error", err) + } + + os.Exit(m.Run()) } func setupWsAuth(t *testing.T) { if wsSetupRan { return } - z.SetDefaults() - TestSetup(t) - if !z.Websocket.IsEnabled() && !z.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() || !canManipulateRealOrders { + if !z.Websocket.IsEnabled() && !z.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() || !canManipulateRealOrders { t.Skip(wshandler.WebsocketNotEnabled) } z.WebsocketConn = &wshandler.WebsocketConnection{ @@ -72,7 +80,7 @@ func setupWsAuth(t *testing.T) { func TestSpotNewOrder(t *testing.T) { t.Parallel() - if z.APIKey == "" || z.APISecret == "" { + if !z.ValidateAPICredentials() { t.Skip() } @@ -82,24 +90,22 @@ func TestSpotNewOrder(t *testing.T) { Amount: 0.01, Price: 10246.1, } - orderid, err := z.SpotNewOrder(arg) + _, err := z.SpotNewOrder(arg) if err != nil { - t.Errorf("Test failed - ZB SpotNewOrder: %s", err) - } else { - fmt.Println(orderid) + t.Errorf("ZB SpotNewOrder: %s", err) } } func TestCancelExistingOrder(t *testing.T) { t.Parallel() - if z.APIKey == "" || z.APISecret == "" { + if !z.ValidateAPICredentials() { t.Skip() } err := z.CancelExistingOrder(20180629145864850, "btc_usdt") if err != nil { - t.Errorf("Test failed - ZB CancelExistingOrder: %s", err) + t.Errorf("ZB CancelExistingOrder: %s", err) } } @@ -107,7 +113,7 @@ func TestGetLatestSpotPrice(t *testing.T) { t.Parallel() _, err := z.GetLatestSpotPrice("btc_usdt") if err != nil { - t.Errorf("Test failed - ZB GetLatestSpotPrice: %s", err) + t.Errorf("ZB GetLatestSpotPrice: %s", err) } } @@ -115,7 +121,7 @@ func TestGetTicker(t *testing.T) { t.Parallel() _, err := z.GetTicker("btc_usdt") if err != nil { - t.Errorf("Test failed - ZB GetTicker: %s", err) + t.Errorf("ZB GetTicker: %s", err) } } @@ -123,7 +129,7 @@ func TestGetTickers(t *testing.T) { t.Parallel() _, err := z.GetTickers() if err != nil { - t.Errorf("Test failed - ZB GetTicker: %s", err) + t.Errorf("ZB GetTicker: %s", err) } } @@ -131,7 +137,7 @@ func TestGetOrderbook(t *testing.T) { t.Parallel() _, err := z.GetOrderbook("btc_usdt") if err != nil { - t.Errorf("Test failed - ZB GetTicker: %s", err) + t.Errorf("ZB GetTicker: %s", err) } } @@ -139,7 +145,7 @@ func TestGetMarkets(t *testing.T) { t.Parallel() _, err := z.GetMarkets() if err != nil { - t.Errorf("Test failed - ZB GetMarkets: %s", err) + t.Errorf("ZB GetMarkets: %s", err) } } @@ -153,7 +159,7 @@ func TestGetSpotKline(t *testing.T) { } _, err := z.GetSpotKline(arg) if err != nil { - t.Errorf("Test failed - ZB GetSpotKline: %s", err) + t.Errorf("ZB GetSpotKline: %s", err) } } @@ -174,7 +180,7 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() z.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { + if !areTestAPIKeysSet() { if feeBuilder.FeeType != exchange.OfflineTradeFee { t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } @@ -186,14 +192,12 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { } func TestGetFee(t *testing.T) { - z.SetDefaults() - TestSetup(t) var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic if resp, err := z.GetFee(feeBuilder); resp != float64(0.002) || err != nil { t.Error(err) - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0015), resp) } // CryptocurrencyTradeFee High quantity @@ -201,7 +205,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 if resp, err := z.GetFee(feeBuilder); resp != float64(2000) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2000), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(2000), resp) t.Error(err) } @@ -209,7 +213,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true if resp, err := z.GetFee(feeBuilder); resp != float64(0.002) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) t.Error(err) } @@ -217,14 +221,14 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 if resp, err := z.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := z.GetFee(feeBuilder); resp != float64(0.005) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.005), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.005), resp) t.Error(err) } @@ -233,7 +237,7 @@ func TestGetFee(t *testing.T) { feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee if resp, err := z.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -241,7 +245,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CyptocurrencyDepositFee if resp, err := z.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -249,7 +253,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee if resp, err := z.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -258,30 +262,24 @@ func TestGetFee(t *testing.T) { feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD if resp, err := z.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) + t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } } func TestFormatWithdrawPermissions(t *testing.T) { - z.SetDefaults() expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := z.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) } } func TestGetActiveOrders(t *testing.T) { - z.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, - Currencies: []currency.Pair{currency.NewPair(currency.LTC, - currency.BTC)}, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, + Currencies: []currency.Pair{currency.NewPair(currency.XRP, + currency.USDT)}, } _, err := z.GetActiveOrders(&getOrdersRequest) @@ -293,12 +291,9 @@ func TestGetActiveOrders(t *testing.T) { } func TestGetOrderHistory(t *testing.T) { - z.SetDefaults() - TestSetup(t) - - var getOrdersRequest = exchange.GetOrdersRequest{ - OrderType: exchange.AnyOrderType, - OrderSide: exchange.BuyOrderSide, + var getOrdersRequest = order.GetOrdersRequest{ + OrderType: order.AnyType, + OrderSide: order.Buy, Currencies: []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)}, } @@ -314,35 +309,29 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- func areTestAPIKeysSet() bool { - if z.APIKey != "" && z.APIKey != "Key" && - z.APISecret != "" && z.APISecret != "Secret" { - return true - } - return false + return z.ValidateAPICredentials() } func TestSubmitOrder(t *testing.T) { - z.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip(fmt.Sprintf("ApiKey: %s. Can place orders: %v", - z.APIKey, + z.API.Credentials.Key, canManipulateRealOrders)) } - var pair = currency.Pair{ - Delimiter: "_", - Base: currency.QTUM, - Quote: currency.USDT, + + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Delimiter: "_", + Base: currency.XRP, + Quote: currency.USDT, + }, + OrderSide: order.Buy, + OrderType: order.Limit, + Price: 1, + Amount: 1, + ClientID: "meowOrder", } - - response, err := z.SubmitOrder(pair, - exchange.BuyOrderSide, - exchange.MarketOrderType, - 1, - 10, - "hi") - + response, err := z.SubmitOrder(orderSubmission) if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { t.Errorf("Order failed to be placed: %v", err) } else if !areTestAPIKeysSet() && err == nil { @@ -351,16 +340,12 @@ func TestSubmitOrder(t *testing.T) { } func TestCancelExchangeOrder(t *testing.T) { - z.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + currencyPair := currency.NewPair(currency.XRP, currency.USDT) + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -377,16 +362,12 @@ func TestCancelExchangeOrder(t *testing.T) { } func TestCancelAllExchangeOrders(t *testing.T) { - z.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &exchange.OrderCancellation{ + currencyPair := currency.NewPair(currency.XRP, currency.USDT) + var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", AccountID: "1", @@ -402,41 +383,44 @@ func TestCancelAllExchangeOrders(t *testing.T) { t.Errorf("Could not cancel orders: %v", err) } - if len(resp.OrderStatus) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.OrderStatus)) + if len(resp.Status) > 0 { + t.Errorf("%v orders failed to cancel", len(resp.Status)) } } func TestGetAccountInfo(t *testing.T) { - if apiKey != "" || apiSecret != "" { + if z.ValidateAPICredentials() { _, err := z.GetAccountInfo() if err != nil { - t.Error("Test Failed - GetAccountInfo() error", err) + t.Error("GetAccountInfo() error", err) } } else { _, err := z.GetAccountInfo() if err == nil { - t.Error("Test Failed - GetAccountInfo() error") + t.Error("GetAccountInfo() Expected error") } } } func TestModifyOrder(t *testing.T) { - _, err := z.ModifyOrder(&exchange.ModifyOrder{}) + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + _, err := z.ModifyOrder(&order.Modify{}) if err == nil { - t.Error("Test failed - ModifyOrder() error") + t.Error("ModifyOrder() Expected error") } } func TestWithdraw(t *testing.T) { - z.SetDefaults() - TestSetup(t) - var withdrawCryptoRequest = exchange.WithdrawRequest{ - Amount: 100, - Currency: currency.BTC, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - Description: "WITHDRAW IT ALL", - FeeAmount: 1, + withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ + GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + }, + Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", + FeeAmount: 1, } if areTestAPIKeysSet() && !canManipulateRealOrders { @@ -453,15 +437,11 @@ func TestWithdraw(t *testing.T) { } func TestWithdrawFiat(t *testing.T) { - z.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := z.WithdrawFiatFunds(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -469,15 +449,11 @@ func TestWithdrawFiat(t *testing.T) { } func TestWithdrawInternationalBank(t *testing.T) { - z.SetDefaults() - TestSetup(t) - if areTestAPIKeysSet() && !canManipulateRealOrders { t.Skip("API keys set, canManipulateRealOrders false, skipping test") } - var withdrawFiatRequest = exchange.WithdrawRequest{} - + var withdrawFiatRequest = exchange.FiatWithdrawRequest{} _, err := z.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) if err != common.ErrFunctionNotSupported { t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) @@ -485,16 +461,16 @@ func TestWithdrawInternationalBank(t *testing.T) { } func TestGetDepositAddress(t *testing.T) { - if apiKey != "" || apiSecret != "" { + if areTestAPIKeysSet() { _, err := z.GetDepositAddress(currency.BTC, "") if err != nil { - t.Error("Test Failed - GetDepositAddress() error PLEASE MAKE SURE YOU CREATE DEPOSIT ADDRESSES VIA ZB.COM", + t.Error("GetDepositAddress() error PLEASE MAKE SURE YOU CREATE DEPOSIT ADDRESSES VIA ZB.COM", err) } } else { _, err := z.GetDepositAddress(currency.BTC, "") if err == nil { - t.Error("Test Failed - GetDepositAddress() error") + t.Error("GetDepositAddress() Expected error") } } } @@ -502,10 +478,10 @@ func TestGetDepositAddress(t *testing.T) { // TestZBInvalidJSON ZB sends poorly formed JSON. this tests the JSON fixer // Then JSON decode it to test if successful func TestZBInvalidJSON(t *testing.T) { - json := `{"success":true,"code":1000,"channel":"getSubUserList","message":"[{"isOpenApi":false,"memo":"Memo","userName":"hello@imgoodthanksandyou.com@good","userId":1337,"isFreez":false}]","no":"0"}` - fixedJSON := z.wsFixInvalidJSON([]byte(json)) + data := `{"success":true,"code":1000,"channel":"getSubUserList","message":"[{"isOpenApi":false,"memo":"Memo","userName":"hello@imgoodthanksandyou.com@good","userId":1337,"isFreez":false}]","no":"0"}` + fixedJSON := z.wsFixInvalidJSON([]byte(data)) var response WsGetSubUserListResponse - err := common.JSONDecode(fixedJSON, &response) + err := json.Unmarshal(fixedJSON, &response) if err != nil { t.Fatal(err) } @@ -513,10 +489,10 @@ func TestZBInvalidJSON(t *testing.T) { t.Fatal("Expected extracted JSON USERID to equal 1337") } - json = `{"success":true,"code":1000,"channel":"createSubUserKey","message":"{"apiKey":"thisisnotareallykeyyousillybilly","apiSecret":"lol"}","no":"123"}` - fixedJSON = z.wsFixInvalidJSON([]byte(json)) + data = `{"success":true,"code":1000,"channel":"createSubUserKey","message":"{"apiKey":"thisisnotareallykeyyousillybilly","apiSecret":"lol"}","no":"123"}` + fixedJSON = z.wsFixInvalidJSON([]byte(data)) var response2 WsRequestResponse - err = common.JSONDecode(fixedJSON, &response2) + err = json.Unmarshal(fixedJSON, &response2) if err != nil { t.Error(err) } @@ -543,7 +519,7 @@ func TestWsCreateSuUserKey(t *testing.T) { t.Fatal(err) } userID := subUsers.Message[0].UserID - _, err = z.wsCreateSubUserKey(true, true, true, true, "subu", fmt.Sprintf("%v", userID)) + _, err = z.wsCreateSubUserKey(true, true, true, true, "subu", strconv.FormatInt(userID, 10)) if err != nil { t.Fatal(err) } diff --git a/exchanges/zb/zb_types.go b/exchanges/zb/zb_types.go index 0e062207..2dc9308a 100644 --- a/exchanges/zb/zb_types.go +++ b/exchanges/zb/zb_types.go @@ -4,7 +4,7 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) // OrderbookResponse holds the orderbook data for a symbol @@ -16,7 +16,7 @@ type OrderbookResponse struct { // AccountsResponseCoin holds the accounts coin details type AccountsResponseCoin struct { - Freez string `json:"freez"` // 冻结资产 + Freeze string `json:"freez"` // 冻结资产 EnName string `json:"enName"` // 币种英文名 UnitDecimal int `json:"unitDecimal"` // 保留小数位 UnName string `json:"cnName"` // 币种中文名 @@ -44,6 +44,9 @@ type Order struct { TradeDate int `json:"trade_date"` TradeMoney float64 `json:"trade_money"` Type int64 `json:"type"` + Fees float64 `json:"fees,omitempty"` + TradePrice float64 `json:"trade_price,omitempty"` + No int64 `json:"no,string,omitempty"` } // AccountsResponse 用户基本信息 @@ -72,12 +75,12 @@ type TickerResponse struct { // TickerChildResponse holds the ticker child response data type TickerChildResponse struct { - Vol float64 `json:"vol,string"` // 成交量(最近的24小时) - Last float64 `json:"last,string"` // 最新成交价 - Sell float64 `json:"sell,string"` // 卖一价 - Buy float64 `json:"buy,string"` // 买一价 - High float64 `json:"high,string"` // 最高价 - Low float64 `json:"low,string"` // 最低价 + Volume float64 `json:"vol,string"` // 成交量(最近的24小时) + Last float64 `json:"last,string"` // 最新成交价 + Sell float64 `json:"sell,string"` // 卖一价 + Buy float64 `json:"buy,string"` // 买一价 + High float64 `json:"high,string"` // 最高价 + Low float64 `json:"low,string"` // 最低价 } // SpotNewOrderRequestParamsType ZB 交易类型 @@ -241,7 +244,7 @@ var WithdrawalFees = map[currency.Code]float64{ } // orderSideMap holds order type info based on Alphapoint data -var orderSideMap = map[int64]exchange.OrderSide{ - 0: exchange.BuyOrderSide, - 1: exchange.SellOrderSide, +var orderSideMap = map[int64]order.Side{ + 0: order.Buy, + 1: order.Sell, } diff --git a/exchanges/zb/zb_websocket.go b/exchanges/zb/zb_websocket.go index 825ecac9..bc4466a9 100644 --- a/exchanges/zb/zb_websocket.go +++ b/exchanges/zb/zb_websocket.go @@ -1,16 +1,19 @@ package zb import ( + "encoding/json" "errors" "fmt" "net/http" "regexp" + "strings" "time" "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" @@ -55,13 +58,13 @@ func (z *ZB) WsHandleData() { default: resp, err := z.WebsocketConn.ReadMessage() if err != nil { - z.Websocket.DataHandler <- err + z.Websocket.ReadMessageErrors <- err return } z.Websocket.TrafficAlert <- struct{}{} fixedJSON := z.wsFixInvalidJSON(resp.Raw) var result Generic - err = common.JSONDecode(fixedJSON, &result) + err = json.Unmarshal(fixedJSON, &result) if err != nil { z.Websocket.DataHandler <- err continue @@ -75,36 +78,40 @@ func (z *ZB) WsHandleData() { continue } switch { - case common.StringContains(result.Channel, "markets"): + case strings.Contains(result.Channel, "markets"): var markets Markets - err := common.JSONDecode(result.Data, &markets) + err := json.Unmarshal(result.Data, &markets) if err != nil { z.Websocket.DataHandler <- err continue } - case common.StringContains(result.Channel, "ticker"): - cPair := common.SplitStrings(result.Channel, "_") + case strings.Contains(result.Channel, "ticker"): + cPair := strings.Split(result.Channel, "_") var ticker WsTicker - err := common.JSONDecode(fixedJSON, &ticker) + err := json.Unmarshal(fixedJSON, &ticker) if err != nil { z.Websocket.DataHandler <- err continue } z.Websocket.DataHandler <- wshandler.TickerData{ - Timestamp: time.Unix(0, ticker.Date), - Pair: currency.NewPairFromString(cPair[0]), - AssetType: orderbook.Spot, - Exchange: z.GetName(), - ClosePrice: ticker.Data.Last, - HighPrice: ticker.Data.High, - LowPrice: ticker.Data.Low, + Exchange: z.Name, + Close: ticker.Data.Last, + Volume: ticker.Data.Volume24Hr, + High: ticker.Data.High, + Low: ticker.Data.Low, + Last: ticker.Data.Last, + Bid: ticker.Data.Buy, + Ask: ticker.Data.Sell, + Timestamp: time.Unix(0, ticker.Date*int64(time.Millisecond)), + AssetType: asset.Spot, + Pair: currency.NewPairFromString(cPair[0]), } - case common.StringContains(result.Channel, "depth"): + case strings.Contains(result.Channel, "depth"): var depth WsDepth - err := common.JSONDecode(fixedJSON, &depth) + err := json.Unmarshal(fixedJSON, &depth) if err != nil { z.Websocket.DataHandler <- err continue @@ -112,32 +119,30 @@ func (z *ZB) WsHandleData() { var asks []orderbook.Item for i := range depth.Asks { - ask := depth.Asks[i].([]interface{}) asks = append(asks, orderbook.Item{ - Amount: ask[1].(float64), - Price: ask[0].(float64), + Amount: depth.Asks[i][1].(float64), + Price: depth.Asks[i][0].(float64), }) } var bids []orderbook.Item for i := range depth.Bids { - bid := depth.Bids[i].([]interface{}) bids = append(bids, orderbook.Item{ - Amount: bid[1].(float64), - Price: bid[0].(float64), + Amount: depth.Bids[i][1].(float64), + Price: depth.Bids[i][0].(float64), }) } - channelInfo := common.SplitStrings(result.Channel, "_") + channelInfo := strings.Split(result.Channel, "_") cPair := currency.NewPairFromString(channelInfo[0]) var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids - newOrderBook.AssetType = orderbook.Spot + newOrderBook.AssetType = asset.Spot newOrderBook.Pair = cPair + newOrderBook.ExchangeName = z.Name - err = z.Websocket.Orderbook.LoadSnapshot(&newOrderBook, - true) + err = z.Websocket.Orderbook.LoadSnapshot(&newOrderBook) if err != nil { z.Websocket.DataHandler <- err continue @@ -145,13 +150,13 @@ func (z *ZB) WsHandleData() { z.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: cPair, - Asset: orderbook.Spot, - Exchange: z.GetName(), + Asset: asset.Spot, + Exchange: z.Name, } - case common.StringContains(result.Channel, "trades"): + case strings.Contains(result.Channel, "trades"): var trades WsTrades - err := common.JSONDecode(fixedJSON, &trades) + err := json.Unmarshal(fixedJSON, &trades) if err != nil { z.Websocket.DataHandler <- err continue @@ -161,13 +166,14 @@ func (z *ZB) WsHandleData() { continue } t := trades.Data[len(trades.Data)-1] - channelInfo := common.SplitStrings(result.Channel, "_") + + channelInfo := strings.Split(result.Channel, "_") cPair := currency.NewPairFromString(channelInfo[0]) z.Websocket.DataHandler <- wshandler.TradeData{ - Timestamp: time.Unix(0, t.Date), + Timestamp: time.Unix(0, t.Date*int64(time.Millisecond)), CurrencyPair: cPair, - AssetType: orderbook.Spot, - Exchange: z.GetName(), + AssetType: asset.Spot, + Exchange: z.Name, EventTime: t.Date, Price: t.Price, Amount: t.Amount, @@ -181,40 +187,6 @@ func (z *ZB) WsHandleData() { } } -var wsErrCodes = map[int64]string{ - 1000: "Successful call", - 1001: "General error message", - 1002: "internal error", - 1003: "Verification failed", - 1004: "Financial security password lock", - 1005: "The fund security password is incorrect. Please confirm and re-enter.", - 1006: "Real-name certification is awaiting review or review", - 1007: "Channel is empty", - 1008: "Event is empty", - 1009: "This interface is being maintained", - 1011: "Not open yet", - 1012: "Insufficient permissions", - 1013: "Can not trade, if you have any questions, please contact online customer service", - 1014: "Cannot be sold during the pre-sale period", - 2002: "Insufficient balance in Bitcoin account", - 2003: "Insufficient balance of Litecoin account", - 2005: "Insufficient balance in Ethereum account", - 2006: "Insufficient balance in ETC currency account", - 2007: "Insufficient balance of BTS currency account", - 2008: "Insufficient balance in EOS currency account", - 2009: "Insufficient account balance", - 3001: "Pending order not found", - 3002: "Invalid amount", - 3003: "Invalid quantity", - 3004: "User does not exist", - 3005: "Invalid parameter", - 3006: "Invalid IP or inconsistent with the bound IP", - 3007: "Request time has expired", - 3008: "Transaction history not found", - 4001: "API interface is locked", - 4002: "Request too frequently", -} - // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (z *ZB) GenerateDefaultSubscriptions() { var subscriptions []wshandler.WebsocketChannelSubscription @@ -223,7 +195,7 @@ func (z *ZB) GenerateDefaultSubscriptions() { Channel: "markets", }) channels := []string{"%s_ticker", "%s_depth", "%s_trades"} - enabledCurrencies := z.GetEnabledCurrencies() + enabledCurrencies := z.GetEnabledPairs(asset.Spot) for i := range channels { for j := range enabledCurrencies { enabledCurrencies[j].Delimiter = "" @@ -246,14 +218,14 @@ func (z *ZB) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription } func (z *ZB) wsGenerateSignature(request interface{}) string { - jsonResponse, err := common.JSONEncode(request) + jsonResponse, err := json.Marshal(request) if err != nil { - log.Error(err) + log.Error(log.ExchangeSys, err) return "" } - hmac := common.GetHMAC(common.HashMD5, + hmac := crypto.GetHMAC(crypto.HashMD5, jsonResponse, - []byte(common.Sha1ToHex(z.APISecret))) + []byte(crypto.Sha1ToHex(z.API.Credentials.Secret))) return fmt.Sprintf("%x", hmac) } @@ -265,10 +237,10 @@ func (z *ZB) wsFixInvalidJSON(json []byte) []byte { return json } // Remove first quote character - capturedInvalidZBJSON := common.ReplaceString(string(matchingResults), "\"", "", 1) + capturedInvalidZBJSON := strings.Replace(string(matchingResults), "\"", "", 1) // Remove last quote character fixedJSON := capturedInvalidZBJSON[:len(capturedInvalidZBJSON)-1] - return []byte(common.ReplaceString(string(json), string(matchingResults), fixedJSON, 1)) + return []byte(strings.Replace(string(json), string(matchingResults), fixedJSON, 1)) } func (z *ZB) wsAddSubUser(username, password string) (*WsGetSubUserListResponse, error) { @@ -282,7 +254,7 @@ func (z *ZB) wsAddSubUser(username, password string) (*WsGetSubUserListResponse, } request.Channel = "addSubUser" request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey + request.Accesskey = z.API.Credentials.Key request.No = z.WebsocketConn.GenerateMessageID(true) request.Sign = z.wsGenerateSignature(request) resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) @@ -290,7 +262,7 @@ func (z *ZB) wsAddSubUser(username, password string) (*WsGetSubUserListResponse, return nil, err } var genericResponse Generic - err = common.JSONDecode(resp, &genericResponse) + err = json.Unmarshal(resp, &genericResponse) if err != nil { return nil, err } @@ -298,7 +270,7 @@ func (z *ZB) wsAddSubUser(username, password string) (*WsGetSubUserListResponse, return nil, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, genericResponse.Message, wsErrCodes[genericResponse.Code]) } var response WsGetSubUserListResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, err } @@ -312,7 +284,7 @@ func (z *ZB) wsGetSubUserList() (*WsGetSubUserListResponse, error) { request := WsAuthenticatedRequest{} request.Channel = "getSubUserList" request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey + request.Accesskey = z.API.Credentials.Key request.No = z.WebsocketConn.GenerateMessageID(true) request.Sign = z.wsGenerateSignature(request) @@ -321,7 +293,7 @@ func (z *ZB) wsGetSubUserList() (*WsGetSubUserListResponse, error) { return nil, err } var response WsGetSubUserListResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, err } @@ -344,7 +316,7 @@ func (z *ZB) wsDoTransferFunds(pair currency.Code, amount float64, fromUserName, } request.Channel = "doTransferFunds" request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey + request.Accesskey = z.API.Credentials.Key request.Sign = z.wsGenerateSignature(request) resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) @@ -352,7 +324,7 @@ func (z *ZB) wsDoTransferFunds(pair currency.Code, amount float64, fromUserName, return nil, err } var response WsRequestResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, err } @@ -377,7 +349,7 @@ func (z *ZB) wsCreateSubUserKey(assetPerm, entrustPerm, leverPerm, moneyPerm boo } request.Channel = "createSubUserKey" request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey + request.Accesskey = z.API.Credentials.Key request.Sign = z.wsGenerateSignature(request) resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) @@ -385,7 +357,7 @@ func (z *ZB) wsCreateSubUserKey(assetPerm, entrustPerm, leverPerm, moneyPerm boo return nil, err } var response WsRequestResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, err } @@ -405,9 +377,9 @@ func (z *ZB) wsSubmitOrder(pair currency.Pair, amount, price float64, tradeType TradeType: tradeType, No: z.WebsocketConn.GenerateMessageID(true), } - request.Channel = fmt.Sprintf("%v_order", pair.String()) + request.Channel = pair.String() + "_order" request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey + request.Accesskey = z.API.Credentials.Key request.Sign = z.wsGenerateSignature(request) resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) @@ -415,7 +387,7 @@ func (z *ZB) wsSubmitOrder(pair currency.Pair, amount, price float64, tradeType return nil, err } var response WsSubmitOrderResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, err } @@ -433,9 +405,9 @@ func (z *ZB) wsCancelOrder(pair currency.Pair, orderID int64) (*WsCancelOrderRes ID: orderID, No: z.WebsocketConn.GenerateMessageID(true), } - request.Channel = fmt.Sprintf("%v_cancelorder", pair.String()) + request.Channel = pair.String() + "_cancelorder" request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey + request.Accesskey = z.API.Credentials.Key request.Sign = z.wsGenerateSignature(request) resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) @@ -443,7 +415,7 @@ func (z *ZB) wsCancelOrder(pair currency.Pair, orderID int64) (*WsCancelOrderRes return nil, err } var response WsCancelOrderResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, err } @@ -461,9 +433,9 @@ func (z *ZB) wsGetOrder(pair currency.Pair, orderID int64) (*WsGetOrderResponse, ID: orderID, No: z.WebsocketConn.GenerateMessageID(true), } - request.Channel = fmt.Sprintf("%v_getorder", pair.String()) + request.Channel = pair.String() + "_getorder" request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey + request.Accesskey = z.API.Credentials.Key request.Sign = z.wsGenerateSignature(request) resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) @@ -471,7 +443,7 @@ func (z *ZB) wsGetOrder(pair currency.Pair, orderID int64) (*WsGetOrderResponse, return nil, err } var response WsGetOrderResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, err } @@ -490,16 +462,16 @@ func (z *ZB) wsGetOrders(pair currency.Pair, pageIndex, tradeType int64) (*WsGet TradeType: tradeType, No: z.WebsocketConn.GenerateMessageID(true), } - request.Channel = fmt.Sprintf("%v_getorders", pair.String()) + request.Channel = pair.String() + "_getorders" request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey + request.Accesskey = z.API.Credentials.Key request.Sign = z.wsGenerateSignature(request) resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) if err != nil { return nil, err } var response WsGetOrdersResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, err } @@ -518,9 +490,9 @@ func (z *ZB) wsGetOrdersIgnoreTradeType(pair currency.Pair, pageIndex, pageSize PageSize: pageSize, No: z.WebsocketConn.GenerateMessageID(true), } - request.Channel = fmt.Sprintf("%v_getordersignoretradetype", pair.String()) + request.Channel = pair.String() + "_getordersignoretradetype" request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey + request.Accesskey = z.API.Credentials.Key request.Sign = z.wsGenerateSignature(request) resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) @@ -528,7 +500,7 @@ func (z *ZB) wsGetOrdersIgnoreTradeType(pair currency.Pair, pageIndex, pageSize return nil, err } var response WsGetOrdersIgnoreTradeTypeResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, err } @@ -545,7 +517,7 @@ func (z *ZB) wsGetAccountInfoRequest() (*WsGetAccountInfoResponse, error) { request := WsAuthenticatedRequest{ Channel: "getaccountinfo", Event: zWebsocketAddChannel, - Accesskey: z.APIKey, + Accesskey: z.API.Credentials.Key, No: z.WebsocketConn.GenerateMessageID(true), } request.Sign = z.wsGenerateSignature(request) @@ -555,7 +527,7 @@ func (z *ZB) wsGetAccountInfoRequest() (*WsGetAccountInfoResponse, error) { return nil, err } var response WsGetAccountInfoResponse - err = common.JSONDecode(resp, &response) + err = json.Unmarshal(resp, &response) if err != nil { return nil, err } diff --git a/exchanges/zb/zb_websocket_types.go b/exchanges/zb/zb_websocket_types.go index 4faef0a0..29275f48 100644 --- a/exchanges/zb/zb_websocket_types.go +++ b/exchanges/zb/zb_websocket_types.go @@ -43,20 +43,20 @@ type WsTicker struct { // WsDepth defines websocket orderbook data type WsDepth struct { - Timestamp int64 `json:"timestamp"` - Asks []interface{} `json:"asks"` - Bids []interface{} `json:"bids"` + Timestamp int64 `json:"timestamp"` + Asks [][]interface{} `json:"asks"` + Bids [][]interface{} `json:"bids"` } // WsTrades defines websocket trade data type WsTrades struct { Data []struct { - Amount float64 `json:"amount,string"` - Price float64 `json:"price,string"` - TID int64 `json:"tid"` - Date int64 `json:"date"` - Type string `json:"type"` - TradeType string `json:"trade_type"` + Amount float64 `json:"amount,string"` + Price float64 `json:"price,string"` + TID interface{} `json:"tid"` + Date int64 `json:"date"` + Type string `json:"type"` + TradeType string `json:"trade_type"` } `json:"data"` } @@ -191,28 +191,12 @@ type WsGetOrderRequest struct { // WsGetOrderResponse contains order data type WsGetOrderResponse struct { - Message string `json:"message"` - No int64 `json:"no,string"` - Code int64 `json:"code"` - Channel string `json:"channel"` - Success bool `json:"success"` - Data WsGetOrderResponseData `json:"data"` -} - -// WsGetOrderResponseData Detailed order data -type WsGetOrderResponseData struct { - Currency string `json:"currency"` - Fees float64 `json:"fees"` - ID string `json:"id"` - Price float64 `json:"price"` - Status int64 `json:"status"` - TotalAmount float64 `json:"total_amount"` - TradeAmount float64 `json:"trade_amount"` - TradePrice float64 `json:"trade_price"` - TradeDate int64 `json:"trade_date"` - TradeMoney float64 `json:"trade_money"` - Type int64 `json:"type"` - No int64 `json:"no,string"` + Message string `json:"message"` + No int64 `json:"no,string"` + Code int64 `json:"code"` + Channel string `json:"channel"` + Success bool `json:"success"` + Data []Order `json:"data"` } // WsGetOrdersRequest get more orders, with no orderID filtering @@ -228,12 +212,12 @@ type WsGetOrdersRequest struct { // WsGetOrdersResponse contains orders data type WsGetOrdersResponse struct { - Message string `json:"message"` - No int64 `json:"no,string"` - Code int64 `json:"code"` - Channel string `json:"channel"` - Success bool `json:"success"` - Data []WsGetOrderResponseData `json:"data"` + Message string `json:"message"` + No int64 `json:"no,string"` + Code int64 `json:"code"` + Channel string `json:"channel"` + Success bool `json:"success"` + Data []Order `json:"data"` } // WsGetOrdersIgnoreTradeTypeRequest ws request @@ -249,12 +233,12 @@ type WsGetOrdersIgnoreTradeTypeRequest struct { // WsGetOrdersIgnoreTradeTypeResponse contains orders data type WsGetOrdersIgnoreTradeTypeResponse struct { - Message string `json:"message"` - No int64 `json:"no,string"` - Code int64 `json:"code"` - Channel string `json:"channel"` - Success bool `json:"success"` - Data []WsGetOrderResponseData `json:"data"` + Message string `json:"message"` + No int64 `json:"no,string"` + Code int64 `json:"code"` + Channel string `json:"channel"` + Success bool `json:"success"` + Data []Order `json:"data"` } // WsGetAccountInfoResponse contains account data @@ -262,23 +246,44 @@ type WsGetAccountInfoResponse struct { Message string `json:"message"` No int64 `json:"no,string"` Data struct { - Coins []struct { - Freez float64 `json:"freez,string"` - EnName string `json:"enName"` - UnitDecimal int64 `json:"unitDecimal"` - CnName string `json:"cnName"` - UnitTag string `json:"unitTag"` - Available float64 `json:"available,string"` - Key string `json:"key"` - } `json:"coins"` - Base struct { - Username string `json:"username"` - TradePasswordEnabled bool `json:"trade_password_enabled"` - AuthGoogleEnabled bool `json:"auth_google_enabled"` - AuthMobileEnabled bool `json:"auth_mobile_enabled"` - } `json:"base"` + Coins []AccountsResponseCoin `json:"coins"` + Base AccountsBaseResponse `json:"base"` } `json:"data"` Code int64 `json:"code"` Channel string `json:"channel"` Success bool `json:"success"` } + +var wsErrCodes = map[int64]string{ + 1000: "Successful call", + 1001: "General error message", + 1002: "internal error", + 1003: "Verification failed", + 1004: "Financial security password lock", + 1005: "The fund security password is incorrect. Please confirm and re-enter.", + 1006: "Real-name certification is awaiting review or review", + 1007: "Channel is empty", + 1008: "Event is empty", + 1009: "This interface is being maintained", + 1011: "Not open yet", + 1012: "Insufficient permissions", + 1013: "Can not trade, if you have any questions, please contact online customer service", + 1014: "Cannot be sold during the pre-sale period", + 2002: "Insufficient balance in Bitcoin account", + 2003: "Insufficient balance of Litecoin account", + 2005: "Insufficient balance in Ethereum account", + 2006: "Insufficient balance in ETC currency account", + 2007: "Insufficient balance of BTS currency account", + 2008: "Insufficient balance in EOS currency account", + 2009: "Insufficient account balance", + 3001: "Pending order not found", + 3002: "Invalid amount", + 3003: "Invalid quantity", + 3004: "User does not exist", + 3005: "Invalid parameter", + 3006: "Invalid IP or inconsistent with the bound IP", + 3007: "Request time has expired", + 3008: "Transaction history not found", + 4001: "API interface is locked", + 4002: "Request too frequently", +} diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index cfb6eb38..eac00ae5 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -4,18 +4,165 @@ import ( "errors" "fmt" "strconv" + "strings" "sync" "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) +// GetDefaultConfig returns a default exchange config +func (z *ZB) GetDefaultConfig() (*config.ExchangeConfig, error) { + z.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = z.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = z.BaseCurrencies + + err := z.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if z.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = z.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets default values for the exchange +func (z *ZB) SetDefaults() { + z.Name = "ZB" + z.Enabled = true + z.Verbose = true + z.API.CredentialsValidator.RequiresKey = true + z.API.CredentialsValidator.RequiresSecret = true + + z.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Delimiter: "_", + }, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: "_", + Uppercase: true, + }, + } + + z.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerBatching: true, + TickerFetching: true, + KlineFetching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + CryptoDeposit: true, + CryptoWithdrawal: true, + TradeFee: true, + CryptoDepositFee: true, + CryptoWithdrawalFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + Subscribe: true, + AuthenticatedEndpoints: true, + AccountInfo: true, + CancelOrder: true, + SubmitOrder: true, + MessageCorrelation: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.NoFiatWithdrawals, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + + z.Requester = request.New(z.Name, + request.NewRateLimit(time.Second*10, zbAuthRate), + request.NewRateLimit(time.Second*10, zbUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + z.API.Endpoints.URLDefault = zbTradeURL + z.API.Endpoints.URL = z.API.Endpoints.URLDefault + z.API.Endpoints.URLSecondaryDefault = zbMarketURL + z.API.Endpoints.URLSecondary = z.API.Endpoints.URLSecondaryDefault + z.API.Endpoints.WebsocketURL = zbWebsocketAPI + z.Websocket = wshandler.New() + z.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + z.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout +} + +// Setup sets user configuration +func (z *ZB) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + z.SetEnabled(false) + return nil + } + + err := z.SetupDefaults(exch) + if err != nil { + return err + } + + err = z.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: zbWebsocketAPI, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: z.WsConnect, + Subscriber: z.Subscribe, + Features: &z.Features.Supports.WebsocketCapabilities, + }) + if err != nil { + return err + } + + z.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: z.Name, + URL: z.Websocket.GetWebsocketURL(), + ProxyURL: z.Websocket.GetProxyAddress(), + Verbose: z.Verbose, + RateLimit: zbWebsocketRateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + return nil +} + // Start starts the OKEX go routine func (z *ZB) Start(wg *sync.WaitGroup) { wg.Add(1) @@ -28,35 +175,46 @@ func (z *ZB) Start(wg *sync.WaitGroup) { // Run implements the OKEX wrapper func (z *ZB) Run() { if z.Verbose { - log.Debugf("%s Websocket: %s. (url: %s).\n", z.GetName(), common.IsEnabled(z.Websocket.IsEnabled()), z.WebsocketURL) - log.Debugf("%s polling delay: %ds.\n", z.GetName(), z.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", z.GetName(), len(z.EnabledPairs), z.EnabledPairs) + z.PrintEnabledPairs() } - markets, err := z.GetMarkets() + if !z.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := z.UpdateTradablePairs(false) if err != nil { - log.Errorf("%s Unable to fetch symbols.\n", z.GetName()) - } else { - var currencies []string - for x := range markets { - currencies = append(currencies, x) - } - - var newCurrencies currency.Pairs - for _, p := range currencies { - newCurrencies = append(newCurrencies, - currency.NewPairFromString(p)) - } - - err = z.UpdateCurrencies(newCurrencies, false, false) - if err != nil { - log.Errorf("%s Failed to update available currencies.\n", z.GetName()) - } + log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", z.Name, err) } } +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (z *ZB) FetchTradablePairs(asset asset.Item) ([]string, error) { + markets, err := z.GetMarkets() + if err != nil { + return nil, err + } + + var currencies []string + for x := range markets { + currencies = append(currencies, x) + } + + return currencies, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (z *ZB) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := z.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + return z.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) +} + // UpdateTicker updates and returns the ticker for a currency pair -func (z *ZB) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { +func (z *ZB) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price result, err := z.GetTickers() @@ -64,69 +222,77 @@ func (z *ZB) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, erro return tickerPrice, err } - for _, x := range z.GetEnabledCurrencies() { - currencySplit := common.SplitStrings(exchange.FormatExchangeCurrency(z.Name, x).String(), "_") - currency := currencySplit[0] + currencySplit[1] + enabledPairs := z.GetEnabledPairs(assetType) + for x := range enabledPairs { + // We can't use either pair format here, so format it to lower- + // case and without any delimiter + curr := enabledPairs[x].Format("", false).String() + if _, ok := result[curr]; !ok { + continue + } var tp ticker.Price - tp.Pair = x - tp.High = result[currency].High - tp.Last = result[currency].Last - tp.Ask = result[currency].Sell - tp.Bid = result[currency].Buy - tp.Last = result[currency].Last - tp.Low = result[currency].Low - tp.Volume = result[currency].Vol + tp.Pair = enabledPairs[x] + tp.High = result[curr].High + tp.Last = result[curr].Last + tp.Ask = result[curr].Sell + tp.Bid = result[curr].Buy + tp.Low = result[curr].Low + tp.Volume = result[curr].Volume err = ticker.ProcessTicker(z.Name, &tp, assetType) if err != nil { - return tickerPrice, err + log.Error(log.Ticker, err) } } return ticker.GetTicker(z.Name, p, assetType) } -// GetTickerPrice returns the ticker for a currency pair -func (z *ZB) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(z.GetName(), p, assetType) +// FetchTicker returns the ticker for a currency pair +func (z *ZB) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(z.Name, p, assetType) if err != nil { return z.UpdateTicker(p, assetType) } return tickerNew, nil } -// GetOrderbookEx returns orderbook base on the currency pair -func (z *ZB) GetOrderbookEx(currency currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get(z.GetName(), currency, assetType) +// FetchOrderbook returns orderbook base on the currency pair +func (z *ZB) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(z.Name, p, assetType) if err != nil { - return z.UpdateOrderbook(currency, assetType) + return z.UpdateOrderbook(p, assetType) } return ob, nil } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (z *ZB) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { +func (z *ZB) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - currency := exchange.FormatExchangeCurrency(z.Name, p).String() + curr := z.FormatExchangeCurrency(p, assetType).String() - orderbookNew, err := z.GetOrderbook(currency) + orderbookNew, err := z.GetOrderbook(curr) if err != nil { return orderBook, err } for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data[1], Price: data[0]}) + orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + Amount: orderbookNew.Bids[x][1], + Price: orderbookNew.Bids[x][0], + }) } for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data[1], Price: data[0]}) + orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + Amount: orderbookNew.Asks[x][1], + Price: orderbookNew.Asks[x][0], + }) } orderBook.Pair = p orderBook.AssetType = assetType - orderBook.ExchangeName = z.GetName() + orderBook.ExchangeName = z.Name err = orderBook.Process() if err != nil { @@ -140,31 +306,41 @@ func (z *ZB) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, // ZB exchange func (z *ZB) GetAccountInfo() (exchange.AccountInfo, error) { var info exchange.AccountInfo - bal, err := z.GetAccountInformation() - if err != nil { - return info, err + var balances []exchange.AccountCurrencyInfo + var coins []AccountsResponseCoin + if z.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + resp, err := z.wsGetAccountInfoRequest() + if err != nil { + return info, err + } + coins = resp.Data.Coins + } else { + bal, err := z.GetAccountInformation() + if err != nil { + return info, err + } + coins = bal.Result.Coins } - var balances []exchange.AccountCurrencyInfo - for _, data := range bal.Result.Coins { - hold, err := strconv.ParseFloat(data.Freez, 64) + for i := range coins { + hold, err := strconv.ParseFloat(coins[i].Freeze, 64) if err != nil { return info, err } - avail, err := strconv.ParseFloat(data.Available, 64) + avail, err := strconv.ParseFloat(coins[i].Available, 64) if err != nil { return info, err } balances = append(balances, exchange.AccountCurrencyInfo{ - CurrencyName: currency.NewCode(data.EnName), + CurrencyName: currency.NewCode(coins[i].EnName), TotalValue: hold + avail, Hold: hold, }) } - info.Exchange = z.GetName() + info.Exchange = z.Name info.Accounts = append(info.Accounts, exchange.Account{ Currencies: balances, }) @@ -175,75 +351,107 @@ func (z *ZB) GetAccountInfo() (exchange.AccountInfo, error) { // GetFundingHistory returns funding history, deposits and // withdrawals func (z *ZB) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. -func (z *ZB) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory - - return resp, common.ErrNotYetImplemented +func (z *ZB) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrNotYetImplemented } // SubmitOrder submits a new order -func (z *ZB) SubmitOrder(p currency.Pair, side exchange.OrderSide, _ exchange.OrderType, amount, price float64, _ string) (exchange.SubmitOrderResponse, error) { - var submitOrderResponse exchange.SubmitOrderResponse - var oT SpotNewOrderRequestParamsType - - if side == exchange.BuyOrderSide { - oT = SpotNewOrderRequestParamsTypeBuy +func (z *ZB) SubmitOrder(o *order.Submit) (order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + err := o.Validate() + if err != nil { + return submitOrderResponse, err + } + if z.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + var isBuyOrder int64 + if o.OrderSide == order.Buy { + isBuyOrder = 1 + } else { + isBuyOrder = 0 + } + var response *WsSubmitOrderResponse + response, err = z.wsSubmitOrder(o.Pair, o.Amount, o.Price, isBuyOrder) + if err != nil { + return submitOrderResponse, err + } + submitOrderResponse.OrderID = strconv.FormatInt(response.Data.EntrustID, 10) } else { - oT = SpotNewOrderRequestParamsTypeSell - } + var oT SpotNewOrderRequestParamsType + if o.OrderSide == order.Buy { + oT = SpotNewOrderRequestParamsTypeBuy + } else { + oT = SpotNewOrderRequestParamsTypeSell + } - var params = SpotNewOrderRequestParams{ - Amount: amount, - Price: price, - Symbol: common.StringToLower(p.String()), - Type: oT, + var params = SpotNewOrderRequestParams{ + Amount: o.Amount, + Price: o.Price, + Symbol: o.Pair.Lower().String(), + Type: oT, + } + var response int64 + response, err = z.SpotNewOrder(params) + if err != nil { + return submitOrderResponse, err + } + if response > 0 { + submitOrderResponse.OrderID = strconv.FormatInt(response, 10) + } } - response, err := z.SpotNewOrder(params) - - if response > 0 { - submitOrderResponse.OrderID = fmt.Sprintf("%v", response) + submitOrderResponse.IsOrderPlaced = true + if o.OrderType == order.Market { + submitOrderResponse.FullyMatched = true } - - if err == nil { - submitOrderResponse.IsOrderPlaced = true - } - - return submitOrderResponse, err + return submitOrderResponse, nil } // ModifyOrder will allow of changing orderbook placement and limit to // market conversion -func (z *ZB) ModifyOrder(action *exchange.ModifyOrder) (string, error) { +func (z *ZB) ModifyOrder(action *order.Modify) (string, error) { return "", common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number -func (z *ZB) CancelOrder(order *exchange.OrderCancellation) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) - +func (z *ZB) CancelOrder(o *order.Cancel) error { + orderIDInt, err := strconv.ParseInt(o.OrderID, 10, 64) if err != nil { return err } - return z.CancelExistingOrder(orderIDInt, exchange.FormatExchangeCurrency(z.Name, order.CurrencyPair).String()) + if z.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + var response *WsCancelOrderResponse + response, err = z.wsCancelOrder(o.CurrencyPair, orderIDInt) + if err != nil { + return err + } + if !response.Success { + return fmt.Errorf("%v - Could not cancel order %v", z.Name, o.OrderID) + } + return nil + } + return z.CancelExistingOrder(orderIDInt, z.FormatExchangeCurrency(o.CurrencyPair, + o.AssetType).String()) } // CancelAllOrders cancels all orders associated with a currency pair -func (z *ZB) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ - OrderStatus: make(map[string]string), +func (z *ZB) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { + cancelAllOrdersResponse := order.CancelAllResponse{ + Status: make(map[string]string), } var allOpenOrders []Order - for _, currency := range z.GetEnabledCurrencies() { - // Limiting to 10 pages - for i := 0; i < 10; i++ { - openOrders, err := z.GetUnfinishedOrdersIgnoreTradeType(exchange.FormatExchangeCurrency(z.Name, currency).String(), 1, 10) + enabledPairs := z.GetEnabledPairs(asset.Spot) + for x := range enabledPairs { + fPair := z.FormatExchangeCurrency(enabledPairs[x], asset.Spot).String() + for y := int64(1); ; y++ { + openOrders, err := z.GetUnfinishedOrdersIgnoreTradeType(fPair, y, 10) if err != nil { + if strings.Contains(err.Error(), "3001") { + break + } return cancelAllOrdersResponse, err } @@ -252,13 +460,20 @@ func (z *ZB) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllO } allOpenOrders = append(allOpenOrders, openOrders...) + + if len(openOrders) != 10 { + break + } } } - for _, openOrder := range allOpenOrders { - err := z.CancelExistingOrder(openOrder.ID, openOrder.Currency) + for i := range allOpenOrders { + err := z.CancelOrder(&order.Cancel{ + OrderID: strconv.FormatInt(allOpenOrders[i].ID, 10), + CurrencyPair: currency.NewPairFromString(allOpenOrders[i].Currency), + }) if err != nil { - cancelAllOrdersResponse.OrderStatus[strconv.FormatInt(openOrder.ID, 10)] = err.Error() + cancelAllOrdersResponse.Status[strconv.FormatInt(allOpenOrders[i].ID, 10)] = err.Error() } } @@ -266,8 +481,8 @@ func (z *ZB) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.CancelAllO } // GetOrderInfo returns information on a current open order -func (z *ZB) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail +func (z *ZB) GetOrderInfo(orderID string) (order.Detail, error) { + var orderDetail order.Detail return orderDetail, common.ErrNotYetImplemented } @@ -283,19 +498,19 @@ func (z *ZB) GetDepositAddress(cryptocurrency currency.Code, _ string) (string, // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted -func (z *ZB) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (z *ZB) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { return z.Withdraw(withdrawRequest.Currency.Lower().String(), withdrawRequest.Address, withdrawRequest.TradePassword, withdrawRequest.Amount, withdrawRequest.FeeAmount, false) } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted -func (z *ZB) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (z *ZB) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (z *ZB) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { +func (z *ZB) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { return "", common.ErrFunctionNotSupported } @@ -306,7 +521,7 @@ func (z *ZB) GetWebsocket() (*wshandler.Websocket, error) { // GetFeeByType returns an estimate of fee based on type of transaction func (z *ZB) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - if (z.APIKey == "" || z.APISecret == "") && // Todo check connection status + if !z.AllowAuthenticatedRequest() && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } @@ -315,101 +530,116 @@ func (z *ZB) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { // GetActiveOrders retrieves any orders that are active/open // This function is not concurrency safe due to orderSide/orderType maps -func (z *ZB) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { +func (z *ZB) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { var allOrders []Order - for _, currency := range getOrdersRequest.Currencies { - var pageNumber int64 - // Limiting to 10 pages - for i := 0; i < 10; i++ { - resp, err := z.GetUnfinishedOrdersIgnoreTradeType(exchange.FormatExchangeCurrency(z.Name, currency).String(), pageNumber, 10) + for x := range req.Currencies { + for i := int64(1); ; i++ { + fPair := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String() + resp, err := z.GetUnfinishedOrdersIgnoreTradeType(fPair, i, 10) if err != nil { + if strings.Contains(err.Error(), "3001") { + break + } return nil, err } + if len(resp) == 0 { break } allOrders = append(allOrders, resp...) - pageNumber++ + + if len(resp) != 10 { + break + } } } - var orders []exchange.OrderDetail - for _, order := range allOrders { - symbol := currency.NewPairDelimiter(order.Currency, - z.ConfigCurrencyPairFormat.Delimiter) - orderDate := time.Unix(int64(order.TradeDate), 0) - orderSide := orderSideMap[order.Type] - orders = append(orders, exchange.OrderDetail{ - ID: fmt.Sprintf("%v", order.ID), - Amount: order.TotalAmount, + var orders []order.Detail + for i := range allOrders { + symbol := currency.NewPairDelimiter(allOrders[i].Currency, + z.GetPairFormat(asset.Spot, false).Delimiter) + orderDate := time.Unix(int64(allOrders[i].TradeDate), 0) + orderSide := orderSideMap[allOrders[i].Type] + orders = append(orders, order.Detail{ + ID: strconv.FormatInt(allOrders[i].ID, 10), + Amount: allOrders[i].TotalAmount, Exchange: z.Name, OrderDate: orderDate, - Price: order.Price, + Price: allOrders[i].Price, OrderSide: orderSide, CurrencyPair: symbol, }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, - getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&orders, req.OrderSide) return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status // This function is not concurrency safe due to orderSide/orderType maps -func (z *ZB) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - if getOrdersRequest.OrderSide == exchange.AnyOrderSide || getOrdersRequest.OrderSide == "" { +func (z *ZB) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + if req.OrderSide == order.AnySide || req.OrderSide == "" { return nil, errors.New("specific order side is required") } - var allOrders []Order - + var orders []order.Detail var side int64 - if getOrdersRequest.OrderSide == exchange.BuyOrderSide { - side = 1 - } - for _, currency := range getOrdersRequest.Currencies { - var pageNumber int64 - // Limiting to 10 pages - for i := 0; i < 10; i++ { - resp, err := z.GetOrders(exchange.FormatExchangeCurrency(z.Name, currency).String(), pageNumber, side) - if err != nil { - return nil, err + if z.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + for x := range req.Currencies { + for y := int64(1); ; y++ { + resp, err := z.wsGetOrdersIgnoreTradeType(req.Currencies[x], y, 10) + if err != nil { + return nil, err + } + allOrders = append(allOrders, resp.Data...) + if len(resp.Data) != 10 { + break + } } - - if len(resp) == 0 { - break + } + } else { + if req.OrderSide == order.Buy { + side = 1 + } + for x := range req.Currencies { + for y := int64(1); ; y++ { + fPair := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String() + resp, err := z.GetOrders(fPair, y, side) + if err != nil { + return nil, err + } + if len(resp) == 0 { + break + } + allOrders = append(allOrders, resp...) + if len(resp) != 10 { + break + } } - - allOrders = append(allOrders, resp...) - pageNumber++ } } - var orders []exchange.OrderDetail - for _, order := range allOrders { - symbol := currency.NewPairDelimiter(order.Currency, - z.ConfigCurrencyPairFormat.Delimiter) - orderDate := time.Unix(int64(order.TradeDate), 0) - orderSide := orderSideMap[order.Type] - orders = append(orders, exchange.OrderDetail{ - ID: fmt.Sprintf("%v", order.ID), - Amount: order.TotalAmount, + for i := range allOrders { + symbol := currency.NewPairDelimiter(allOrders[i].Currency, + z.GetPairFormat(asset.Spot, false).Delimiter) + orderDate := time.Unix(int64(allOrders[i].TradeDate), 0) + orderSide := orderSideMap[allOrders[i].Type] + orders = append(orders, order.Detail{ + ID: strconv.FormatInt(allOrders[i].ID, 10), + Amount: allOrders[i].TotalAmount, Exchange: z.Name, OrderDate: orderDate, - Price: order.Price, + Price: allOrders[i].Price, OrderSide: orderSide, CurrencyPair: symbol, }) } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - + order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) return orders, nil } diff --git a/gctrpc/README.md b/gctrpc/README.md new file mode 100644 index 00000000..b7f0ee9d --- /dev/null +++ b/gctrpc/README.md @@ -0,0 +1,63 @@ +# GoCryptoTrader gRPC Service + + + +[![Build Status](https://travis-ci.com/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.com/thrasher-corp/gocryptotrader) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + +A cryptocurrency trading bot supporting multiple exchanges written in Golang. + +**Please note that this bot is under development and is not ready for production!** + +## Community + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Background + +GoCryptoTrader utilises gRPC for client/server interaction. Authentication is done +by a self signed TLS cert, which only supports connections from localhost and also +through basic authorisation specified by the users config file. + +GoCryptoTrader also supports a gRPC JSON proxy service for applications which can +be toggled on or off depending on the users preference. + +## Installation + +GoCryptoTrader requires a local installation of the Google protocol buffers +compiler `protoc` v3.0.0 or above. Please install this via your local package +manager or by downloading one of the releases from the official repository: + +[protoc releases](https://github.com/protocolbuffers/protobuf/releases) + +Then use `go get -u` to download the following packages: + +```bash +go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway +go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger +go get -u github.com/golang/protobuf/protoc-gen-go +``` + +This will place three binaries in your `$GOBIN`; + +* `protoc-gen-grpc-gateway` +* `protoc-gen-swagger` +* `protoc-gen-go` + +Make sure that your `$GOBIN` is in your `$PATH`. + +## Usage + +After the above dependencies are required, make necessary changes to the `rpc.proto` +spec file and run the generation scripts: + +### Windows + +Run `gen_pb_win.bat` + +### Linux and macOS + +Run `./gen_pb_linux.sh` diff --git a/gctrpc/auth/auth.go b/gctrpc/auth/auth.go new file mode 100644 index 00000000..e8076222 --- /dev/null +++ b/gctrpc/auth/auth.go @@ -0,0 +1,26 @@ +package auth + +import ( + "context" + "encoding/base64" +) + +// BasicAuth stores a basic auth username/password +type BasicAuth struct { + Username string + Password string +} + +// GetRequestMetadata is a implementation of the GetRequestMetadata function +func (b BasicAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) { + auth := b.Username + ":" + b.Password + enc := base64.StdEncoding.EncodeToString([]byte(auth)) + return map[string]string{ + "authorization": "Basic " + enc, + }, nil +} + +// RequireTransportSecurity is required for basic auth +func (BasicAuth) RequireTransportSecurity() bool { + return true +} diff --git a/gctrpc/gen_pb_linux.sh b/gctrpc/gen_pb_linux.sh new file mode 100755 index 00000000..8240e5ee --- /dev/null +++ b/gctrpc/gen_pb_linux.sh @@ -0,0 +1,4 @@ +echo "GoCryptoTrader: Generating gRPC, proxy and swagger files." +protoc -I=. -I=$GOPATH/src -I=$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --go_out=plugins=grpc:. rpc.proto +protoc -I=. -I=$GOPATH/src -I=$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --grpc-gateway_out=logtostderr=true:. rpc.proto +protoc -I=. -I=$GOPATH/src -I=$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --swagger_out=logtostderr=true:. rpc.proto \ No newline at end of file diff --git a/gctrpc/gen_pb_win.bat b/gctrpc/gen_pb_win.bat new file mode 100644 index 00000000..246b7dfe --- /dev/null +++ b/gctrpc/gen_pb_win.bat @@ -0,0 +1,5 @@ +@echo off +echo GoCryptoTrader: Generating gRPC, proxy and swagger files. +protoc -I=. -I=%GOPATH%\src -I=%GOPATH%\src\github.com\grpc-ecosystem\grpc-gateway\third_party\googleapis --go_out=plugins=grpc:. rpc.proto +protoc -I=. -I=%GOPATH%\src -I=%GOPATH%\src\github.com\grpc-ecosystem\grpc-gateway\third_party\googleapis --grpc-gateway_out=logtostderr=true:. rpc.proto +protoc -I=. -I=%GOPATH%\src -I=%GOPATH%\src\github.com\grpc-ecosystem\grpc-gateway\third_party\googleapis --swagger_out=logtostderr=true:. rpc.proto \ No newline at end of file diff --git a/gctrpc/rpc.pb.go b/gctrpc/rpc.pb.go new file mode 100644 index 00000000..a5796e2b --- /dev/null +++ b/gctrpc/rpc.pb.go @@ -0,0 +1,7384 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: rpc.proto + +package gctrpc + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + _ "google.golang.org/genproto/googleapis/api/annotations" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type GetInfoRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetInfoRequest) Reset() { *m = GetInfoRequest{} } +func (m *GetInfoRequest) String() string { return proto.CompactTextString(m) } +func (*GetInfoRequest) ProtoMessage() {} +func (*GetInfoRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{0} +} + +func (m *GetInfoRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetInfoRequest.Unmarshal(m, b) +} +func (m *GetInfoRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetInfoRequest.Marshal(b, m, deterministic) +} +func (m *GetInfoRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetInfoRequest.Merge(m, src) +} +func (m *GetInfoRequest) XXX_Size() int { + return xxx_messageInfo_GetInfoRequest.Size(m) +} +func (m *GetInfoRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetInfoRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetInfoRequest proto.InternalMessageInfo + +type GetInfoResponse struct { + Uptime string `protobuf:"bytes,1,opt,name=uptime,proto3" json:"uptime,omitempty"` + AvailableExchanges int64 `protobuf:"varint,2,opt,name=available_exchanges,json=availableExchanges,proto3" json:"available_exchanges,omitempty"` + EnabledExchanges int64 `protobuf:"varint,3,opt,name=enabled_exchanges,json=enabledExchanges,proto3" json:"enabled_exchanges,omitempty"` + DefaultForexProvider string `protobuf:"bytes,4,opt,name=default_forex_provider,json=defaultForexProvider,proto3" json:"default_forex_provider,omitempty"` + DefaultFiatCurrency string `protobuf:"bytes,5,opt,name=default_fiat_currency,json=defaultFiatCurrency,proto3" json:"default_fiat_currency,omitempty"` + SubsystemStatus map[string]bool `protobuf:"bytes,6,rep,name=subsystem_status,json=subsystemStatus,proto3" json:"subsystem_status,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` + RpcEndpoints map[string]*RPCEndpoint `protobuf:"bytes,7,rep,name=rpc_endpoints,json=rpcEndpoints,proto3" json:"rpc_endpoints,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetInfoResponse) Reset() { *m = GetInfoResponse{} } +func (m *GetInfoResponse) String() string { return proto.CompactTextString(m) } +func (*GetInfoResponse) ProtoMessage() {} +func (*GetInfoResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{1} +} + +func (m *GetInfoResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetInfoResponse.Unmarshal(m, b) +} +func (m *GetInfoResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetInfoResponse.Marshal(b, m, deterministic) +} +func (m *GetInfoResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetInfoResponse.Merge(m, src) +} +func (m *GetInfoResponse) XXX_Size() int { + return xxx_messageInfo_GetInfoResponse.Size(m) +} +func (m *GetInfoResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetInfoResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetInfoResponse proto.InternalMessageInfo + +func (m *GetInfoResponse) GetUptime() string { + if m != nil { + return m.Uptime + } + return "" +} + +func (m *GetInfoResponse) GetAvailableExchanges() int64 { + if m != nil { + return m.AvailableExchanges + } + return 0 +} + +func (m *GetInfoResponse) GetEnabledExchanges() int64 { + if m != nil { + return m.EnabledExchanges + } + return 0 +} + +func (m *GetInfoResponse) GetDefaultForexProvider() string { + if m != nil { + return m.DefaultForexProvider + } + return "" +} + +func (m *GetInfoResponse) GetDefaultFiatCurrency() string { + if m != nil { + return m.DefaultFiatCurrency + } + return "" +} + +func (m *GetInfoResponse) GetSubsystemStatus() map[string]bool { + if m != nil { + return m.SubsystemStatus + } + return nil +} + +func (m *GetInfoResponse) GetRpcEndpoints() map[string]*RPCEndpoint { + if m != nil { + return m.RpcEndpoints + } + return nil +} + +type GetCommunicationRelayersRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetCommunicationRelayersRequest) Reset() { *m = GetCommunicationRelayersRequest{} } +func (m *GetCommunicationRelayersRequest) String() string { return proto.CompactTextString(m) } +func (*GetCommunicationRelayersRequest) ProtoMessage() {} +func (*GetCommunicationRelayersRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{2} +} + +func (m *GetCommunicationRelayersRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetCommunicationRelayersRequest.Unmarshal(m, b) +} +func (m *GetCommunicationRelayersRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetCommunicationRelayersRequest.Marshal(b, m, deterministic) +} +func (m *GetCommunicationRelayersRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetCommunicationRelayersRequest.Merge(m, src) +} +func (m *GetCommunicationRelayersRequest) XXX_Size() int { + return xxx_messageInfo_GetCommunicationRelayersRequest.Size(m) +} +func (m *GetCommunicationRelayersRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetCommunicationRelayersRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetCommunicationRelayersRequest proto.InternalMessageInfo + +type CommunicationRelayer struct { + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CommunicationRelayer) Reset() { *m = CommunicationRelayer{} } +func (m *CommunicationRelayer) String() string { return proto.CompactTextString(m) } +func (*CommunicationRelayer) ProtoMessage() {} +func (*CommunicationRelayer) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{3} +} + +func (m *CommunicationRelayer) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CommunicationRelayer.Unmarshal(m, b) +} +func (m *CommunicationRelayer) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CommunicationRelayer.Marshal(b, m, deterministic) +} +func (m *CommunicationRelayer) XXX_Merge(src proto.Message) { + xxx_messageInfo_CommunicationRelayer.Merge(m, src) +} +func (m *CommunicationRelayer) XXX_Size() int { + return xxx_messageInfo_CommunicationRelayer.Size(m) +} +func (m *CommunicationRelayer) XXX_DiscardUnknown() { + xxx_messageInfo_CommunicationRelayer.DiscardUnknown(m) +} + +var xxx_messageInfo_CommunicationRelayer proto.InternalMessageInfo + +func (m *CommunicationRelayer) GetEnabled() bool { + if m != nil { + return m.Enabled + } + return false +} + +func (m *CommunicationRelayer) GetConnected() bool { + if m != nil { + return m.Connected + } + return false +} + +type GetCommunicationRelayersResponse struct { + CommunicationRelayers map[string]*CommunicationRelayer `protobuf:"bytes,1,rep,name=communication_relayers,json=communicationRelayers,proto3" json:"communication_relayers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetCommunicationRelayersResponse) Reset() { *m = GetCommunicationRelayersResponse{} } +func (m *GetCommunicationRelayersResponse) String() string { return proto.CompactTextString(m) } +func (*GetCommunicationRelayersResponse) ProtoMessage() {} +func (*GetCommunicationRelayersResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{4} +} + +func (m *GetCommunicationRelayersResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetCommunicationRelayersResponse.Unmarshal(m, b) +} +func (m *GetCommunicationRelayersResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetCommunicationRelayersResponse.Marshal(b, m, deterministic) +} +func (m *GetCommunicationRelayersResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetCommunicationRelayersResponse.Merge(m, src) +} +func (m *GetCommunicationRelayersResponse) XXX_Size() int { + return xxx_messageInfo_GetCommunicationRelayersResponse.Size(m) +} +func (m *GetCommunicationRelayersResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetCommunicationRelayersResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetCommunicationRelayersResponse proto.InternalMessageInfo + +func (m *GetCommunicationRelayersResponse) GetCommunicationRelayers() map[string]*CommunicationRelayer { + if m != nil { + return m.CommunicationRelayers + } + return nil +} + +type GenericSubsystemRequest struct { + Subsystem string `protobuf:"bytes,1,opt,name=subsystem,proto3" json:"subsystem,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GenericSubsystemRequest) Reset() { *m = GenericSubsystemRequest{} } +func (m *GenericSubsystemRequest) String() string { return proto.CompactTextString(m) } +func (*GenericSubsystemRequest) ProtoMessage() {} +func (*GenericSubsystemRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{5} +} + +func (m *GenericSubsystemRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GenericSubsystemRequest.Unmarshal(m, b) +} +func (m *GenericSubsystemRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GenericSubsystemRequest.Marshal(b, m, deterministic) +} +func (m *GenericSubsystemRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenericSubsystemRequest.Merge(m, src) +} +func (m *GenericSubsystemRequest) XXX_Size() int { + return xxx_messageInfo_GenericSubsystemRequest.Size(m) +} +func (m *GenericSubsystemRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GenericSubsystemRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GenericSubsystemRequest proto.InternalMessageInfo + +func (m *GenericSubsystemRequest) GetSubsystem() string { + if m != nil { + return m.Subsystem + } + return "" +} + +type GenericSubsystemResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GenericSubsystemResponse) Reset() { *m = GenericSubsystemResponse{} } +func (m *GenericSubsystemResponse) String() string { return proto.CompactTextString(m) } +func (*GenericSubsystemResponse) ProtoMessage() {} +func (*GenericSubsystemResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{6} +} + +func (m *GenericSubsystemResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GenericSubsystemResponse.Unmarshal(m, b) +} +func (m *GenericSubsystemResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GenericSubsystemResponse.Marshal(b, m, deterministic) +} +func (m *GenericSubsystemResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenericSubsystemResponse.Merge(m, src) +} +func (m *GenericSubsystemResponse) XXX_Size() int { + return xxx_messageInfo_GenericSubsystemResponse.Size(m) +} +func (m *GenericSubsystemResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GenericSubsystemResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GenericSubsystemResponse proto.InternalMessageInfo + +type GetSubsystemsRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetSubsystemsRequest) Reset() { *m = GetSubsystemsRequest{} } +func (m *GetSubsystemsRequest) String() string { return proto.CompactTextString(m) } +func (*GetSubsystemsRequest) ProtoMessage() {} +func (*GetSubsystemsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{7} +} + +func (m *GetSubsystemsRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetSubsystemsRequest.Unmarshal(m, b) +} +func (m *GetSubsystemsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetSubsystemsRequest.Marshal(b, m, deterministic) +} +func (m *GetSubsystemsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetSubsystemsRequest.Merge(m, src) +} +func (m *GetSubsystemsRequest) XXX_Size() int { + return xxx_messageInfo_GetSubsystemsRequest.Size(m) +} +func (m *GetSubsystemsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetSubsystemsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetSubsystemsRequest proto.InternalMessageInfo + +type GetSusbsytemsResponse struct { + SubsystemsStatus map[string]bool `protobuf:"bytes,1,rep,name=subsystems_status,json=subsystemsStatus,proto3" json:"subsystems_status,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetSusbsytemsResponse) Reset() { *m = GetSusbsytemsResponse{} } +func (m *GetSusbsytemsResponse) String() string { return proto.CompactTextString(m) } +func (*GetSusbsytemsResponse) ProtoMessage() {} +func (*GetSusbsytemsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{8} +} + +func (m *GetSusbsytemsResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetSusbsytemsResponse.Unmarshal(m, b) +} +func (m *GetSusbsytemsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetSusbsytemsResponse.Marshal(b, m, deterministic) +} +func (m *GetSusbsytemsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetSusbsytemsResponse.Merge(m, src) +} +func (m *GetSusbsytemsResponse) XXX_Size() int { + return xxx_messageInfo_GetSusbsytemsResponse.Size(m) +} +func (m *GetSusbsytemsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetSusbsytemsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetSusbsytemsResponse proto.InternalMessageInfo + +func (m *GetSusbsytemsResponse) GetSubsystemsStatus() map[string]bool { + if m != nil { + return m.SubsystemsStatus + } + return nil +} + +type GetRPCEndpointsRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetRPCEndpointsRequest) Reset() { *m = GetRPCEndpointsRequest{} } +func (m *GetRPCEndpointsRequest) String() string { return proto.CompactTextString(m) } +func (*GetRPCEndpointsRequest) ProtoMessage() {} +func (*GetRPCEndpointsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{9} +} + +func (m *GetRPCEndpointsRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetRPCEndpointsRequest.Unmarshal(m, b) +} +func (m *GetRPCEndpointsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetRPCEndpointsRequest.Marshal(b, m, deterministic) +} +func (m *GetRPCEndpointsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetRPCEndpointsRequest.Merge(m, src) +} +func (m *GetRPCEndpointsRequest) XXX_Size() int { + return xxx_messageInfo_GetRPCEndpointsRequest.Size(m) +} +func (m *GetRPCEndpointsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetRPCEndpointsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetRPCEndpointsRequest proto.InternalMessageInfo + +type RPCEndpoint struct { + Started bool `protobuf:"varint,1,opt,name=started,proto3" json:"started,omitempty"` + ListenAddress string `protobuf:"bytes,2,opt,name=listen_address,json=listenAddress,proto3" json:"listen_address,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RPCEndpoint) Reset() { *m = RPCEndpoint{} } +func (m *RPCEndpoint) String() string { return proto.CompactTextString(m) } +func (*RPCEndpoint) ProtoMessage() {} +func (*RPCEndpoint) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{10} +} + +func (m *RPCEndpoint) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RPCEndpoint.Unmarshal(m, b) +} +func (m *RPCEndpoint) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RPCEndpoint.Marshal(b, m, deterministic) +} +func (m *RPCEndpoint) XXX_Merge(src proto.Message) { + xxx_messageInfo_RPCEndpoint.Merge(m, src) +} +func (m *RPCEndpoint) XXX_Size() int { + return xxx_messageInfo_RPCEndpoint.Size(m) +} +func (m *RPCEndpoint) XXX_DiscardUnknown() { + xxx_messageInfo_RPCEndpoint.DiscardUnknown(m) +} + +var xxx_messageInfo_RPCEndpoint proto.InternalMessageInfo + +func (m *RPCEndpoint) GetStarted() bool { + if m != nil { + return m.Started + } + return false +} + +func (m *RPCEndpoint) GetListenAddress() string { + if m != nil { + return m.ListenAddress + } + return "" +} + +type GetRPCEndpointsResponse struct { + Endpoints map[string]*RPCEndpoint `protobuf:"bytes,1,rep,name=endpoints,proto3" json:"endpoints,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetRPCEndpointsResponse) Reset() { *m = GetRPCEndpointsResponse{} } +func (m *GetRPCEndpointsResponse) String() string { return proto.CompactTextString(m) } +func (*GetRPCEndpointsResponse) ProtoMessage() {} +func (*GetRPCEndpointsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{11} +} + +func (m *GetRPCEndpointsResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetRPCEndpointsResponse.Unmarshal(m, b) +} +func (m *GetRPCEndpointsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetRPCEndpointsResponse.Marshal(b, m, deterministic) +} +func (m *GetRPCEndpointsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetRPCEndpointsResponse.Merge(m, src) +} +func (m *GetRPCEndpointsResponse) XXX_Size() int { + return xxx_messageInfo_GetRPCEndpointsResponse.Size(m) +} +func (m *GetRPCEndpointsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetRPCEndpointsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetRPCEndpointsResponse proto.InternalMessageInfo + +func (m *GetRPCEndpointsResponse) GetEndpoints() map[string]*RPCEndpoint { + if m != nil { + return m.Endpoints + } + return nil +} + +type GenericExchangeNameRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GenericExchangeNameRequest) Reset() { *m = GenericExchangeNameRequest{} } +func (m *GenericExchangeNameRequest) String() string { return proto.CompactTextString(m) } +func (*GenericExchangeNameRequest) ProtoMessage() {} +func (*GenericExchangeNameRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{12} +} + +func (m *GenericExchangeNameRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GenericExchangeNameRequest.Unmarshal(m, b) +} +func (m *GenericExchangeNameRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GenericExchangeNameRequest.Marshal(b, m, deterministic) +} +func (m *GenericExchangeNameRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenericExchangeNameRequest.Merge(m, src) +} +func (m *GenericExchangeNameRequest) XXX_Size() int { + return xxx_messageInfo_GenericExchangeNameRequest.Size(m) +} +func (m *GenericExchangeNameRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GenericExchangeNameRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GenericExchangeNameRequest proto.InternalMessageInfo + +func (m *GenericExchangeNameRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +type GenericExchangeNameResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GenericExchangeNameResponse) Reset() { *m = GenericExchangeNameResponse{} } +func (m *GenericExchangeNameResponse) String() string { return proto.CompactTextString(m) } +func (*GenericExchangeNameResponse) ProtoMessage() {} +func (*GenericExchangeNameResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{13} +} + +func (m *GenericExchangeNameResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GenericExchangeNameResponse.Unmarshal(m, b) +} +func (m *GenericExchangeNameResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GenericExchangeNameResponse.Marshal(b, m, deterministic) +} +func (m *GenericExchangeNameResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenericExchangeNameResponse.Merge(m, src) +} +func (m *GenericExchangeNameResponse) XXX_Size() int { + return xxx_messageInfo_GenericExchangeNameResponse.Size(m) +} +func (m *GenericExchangeNameResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GenericExchangeNameResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GenericExchangeNameResponse proto.InternalMessageInfo + +type GetExchangesRequest struct { + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetExchangesRequest) Reset() { *m = GetExchangesRequest{} } +func (m *GetExchangesRequest) String() string { return proto.CompactTextString(m) } +func (*GetExchangesRequest) ProtoMessage() {} +func (*GetExchangesRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{14} +} + +func (m *GetExchangesRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetExchangesRequest.Unmarshal(m, b) +} +func (m *GetExchangesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetExchangesRequest.Marshal(b, m, deterministic) +} +func (m *GetExchangesRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetExchangesRequest.Merge(m, src) +} +func (m *GetExchangesRequest) XXX_Size() int { + return xxx_messageInfo_GetExchangesRequest.Size(m) +} +func (m *GetExchangesRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetExchangesRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetExchangesRequest proto.InternalMessageInfo + +func (m *GetExchangesRequest) GetEnabled() bool { + if m != nil { + return m.Enabled + } + return false +} + +type GetExchangesResponse struct { + Exchanges string `protobuf:"bytes,1,opt,name=exchanges,proto3" json:"exchanges,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetExchangesResponse) Reset() { *m = GetExchangesResponse{} } +func (m *GetExchangesResponse) String() string { return proto.CompactTextString(m) } +func (*GetExchangesResponse) ProtoMessage() {} +func (*GetExchangesResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{15} +} + +func (m *GetExchangesResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetExchangesResponse.Unmarshal(m, b) +} +func (m *GetExchangesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetExchangesResponse.Marshal(b, m, deterministic) +} +func (m *GetExchangesResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetExchangesResponse.Merge(m, src) +} +func (m *GetExchangesResponse) XXX_Size() int { + return xxx_messageInfo_GetExchangesResponse.Size(m) +} +func (m *GetExchangesResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetExchangesResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetExchangesResponse proto.InternalMessageInfo + +func (m *GetExchangesResponse) GetExchanges() string { + if m != nil { + return m.Exchanges + } + return "" +} + +type GetExchangeOTPReponse struct { + OtpCode string `protobuf:"bytes,1,opt,name=otp_code,json=otpCode,proto3" json:"otp_code,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetExchangeOTPReponse) Reset() { *m = GetExchangeOTPReponse{} } +func (m *GetExchangeOTPReponse) String() string { return proto.CompactTextString(m) } +func (*GetExchangeOTPReponse) ProtoMessage() {} +func (*GetExchangeOTPReponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{16} +} + +func (m *GetExchangeOTPReponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetExchangeOTPReponse.Unmarshal(m, b) +} +func (m *GetExchangeOTPReponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetExchangeOTPReponse.Marshal(b, m, deterministic) +} +func (m *GetExchangeOTPReponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetExchangeOTPReponse.Merge(m, src) +} +func (m *GetExchangeOTPReponse) XXX_Size() int { + return xxx_messageInfo_GetExchangeOTPReponse.Size(m) +} +func (m *GetExchangeOTPReponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetExchangeOTPReponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetExchangeOTPReponse proto.InternalMessageInfo + +func (m *GetExchangeOTPReponse) GetOtpCode() string { + if m != nil { + return m.OtpCode + } + return "" +} + +type GetExchangeOTPsRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetExchangeOTPsRequest) Reset() { *m = GetExchangeOTPsRequest{} } +func (m *GetExchangeOTPsRequest) String() string { return proto.CompactTextString(m) } +func (*GetExchangeOTPsRequest) ProtoMessage() {} +func (*GetExchangeOTPsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{17} +} + +func (m *GetExchangeOTPsRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetExchangeOTPsRequest.Unmarshal(m, b) +} +func (m *GetExchangeOTPsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetExchangeOTPsRequest.Marshal(b, m, deterministic) +} +func (m *GetExchangeOTPsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetExchangeOTPsRequest.Merge(m, src) +} +func (m *GetExchangeOTPsRequest) XXX_Size() int { + return xxx_messageInfo_GetExchangeOTPsRequest.Size(m) +} +func (m *GetExchangeOTPsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetExchangeOTPsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetExchangeOTPsRequest proto.InternalMessageInfo + +type GetExchangeOTPsResponse struct { + OtpCodes map[string]string `protobuf:"bytes,1,rep,name=otp_codes,json=otpCodes,proto3" json:"otp_codes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetExchangeOTPsResponse) Reset() { *m = GetExchangeOTPsResponse{} } +func (m *GetExchangeOTPsResponse) String() string { return proto.CompactTextString(m) } +func (*GetExchangeOTPsResponse) ProtoMessage() {} +func (*GetExchangeOTPsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{18} +} + +func (m *GetExchangeOTPsResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetExchangeOTPsResponse.Unmarshal(m, b) +} +func (m *GetExchangeOTPsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetExchangeOTPsResponse.Marshal(b, m, deterministic) +} +func (m *GetExchangeOTPsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetExchangeOTPsResponse.Merge(m, src) +} +func (m *GetExchangeOTPsResponse) XXX_Size() int { + return xxx_messageInfo_GetExchangeOTPsResponse.Size(m) +} +func (m *GetExchangeOTPsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetExchangeOTPsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetExchangeOTPsResponse proto.InternalMessageInfo + +func (m *GetExchangeOTPsResponse) GetOtpCodes() map[string]string { + if m != nil { + return m.OtpCodes + } + return nil +} + +type DisableExchangeRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *DisableExchangeRequest) Reset() { *m = DisableExchangeRequest{} } +func (m *DisableExchangeRequest) String() string { return proto.CompactTextString(m) } +func (*DisableExchangeRequest) ProtoMessage() {} +func (*DisableExchangeRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{19} +} + +func (m *DisableExchangeRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_DisableExchangeRequest.Unmarshal(m, b) +} +func (m *DisableExchangeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_DisableExchangeRequest.Marshal(b, m, deterministic) +} +func (m *DisableExchangeRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_DisableExchangeRequest.Merge(m, src) +} +func (m *DisableExchangeRequest) XXX_Size() int { + return xxx_messageInfo_DisableExchangeRequest.Size(m) +} +func (m *DisableExchangeRequest) XXX_DiscardUnknown() { + xxx_messageInfo_DisableExchangeRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_DisableExchangeRequest proto.InternalMessageInfo + +func (m *DisableExchangeRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +type PairsSupported struct { + AvailablePairs string `protobuf:"bytes,1,opt,name=available_pairs,json=availablePairs,proto3" json:"available_pairs,omitempty"` + EnabledPairs string `protobuf:"bytes,2,opt,name=enabled_pairs,json=enabledPairs,proto3" json:"enabled_pairs,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PairsSupported) Reset() { *m = PairsSupported{} } +func (m *PairsSupported) String() string { return proto.CompactTextString(m) } +func (*PairsSupported) ProtoMessage() {} +func (*PairsSupported) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{20} +} + +func (m *PairsSupported) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PairsSupported.Unmarshal(m, b) +} +func (m *PairsSupported) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PairsSupported.Marshal(b, m, deterministic) +} +func (m *PairsSupported) XXX_Merge(src proto.Message) { + xxx_messageInfo_PairsSupported.Merge(m, src) +} +func (m *PairsSupported) XXX_Size() int { + return xxx_messageInfo_PairsSupported.Size(m) +} +func (m *PairsSupported) XXX_DiscardUnknown() { + xxx_messageInfo_PairsSupported.DiscardUnknown(m) +} + +var xxx_messageInfo_PairsSupported proto.InternalMessageInfo + +func (m *PairsSupported) GetAvailablePairs() string { + if m != nil { + return m.AvailablePairs + } + return "" +} + +func (m *PairsSupported) GetEnabledPairs() string { + if m != nil { + return m.EnabledPairs + } + return "" +} + +type GetExchangeInfoResponse struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` + Verbose bool `protobuf:"varint,3,opt,name=verbose,proto3" json:"verbose,omitempty"` + UsingSandbox bool `protobuf:"varint,4,opt,name=using_sandbox,json=usingSandbox,proto3" json:"using_sandbox,omitempty"` + HttpTimeout string `protobuf:"bytes,5,opt,name=http_timeout,json=httpTimeout,proto3" json:"http_timeout,omitempty"` + HttpUseragent string `protobuf:"bytes,6,opt,name=http_useragent,json=httpUseragent,proto3" json:"http_useragent,omitempty"` + HttpProxy string `protobuf:"bytes,7,opt,name=http_proxy,json=httpProxy,proto3" json:"http_proxy,omitempty"` + BaseCurrencies string `protobuf:"bytes,8,opt,name=base_currencies,json=baseCurrencies,proto3" json:"base_currencies,omitempty"` + SupportedAssets map[string]*PairsSupported `protobuf:"bytes,9,rep,name=supported_assets,json=supportedAssets,proto3" json:"supported_assets,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + AuthenticatedApi bool `protobuf:"varint,10,opt,name=authenticated_api,json=authenticatedApi,proto3" json:"authenticated_api,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetExchangeInfoResponse) Reset() { *m = GetExchangeInfoResponse{} } +func (m *GetExchangeInfoResponse) String() string { return proto.CompactTextString(m) } +func (*GetExchangeInfoResponse) ProtoMessage() {} +func (*GetExchangeInfoResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{21} +} + +func (m *GetExchangeInfoResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetExchangeInfoResponse.Unmarshal(m, b) +} +func (m *GetExchangeInfoResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetExchangeInfoResponse.Marshal(b, m, deterministic) +} +func (m *GetExchangeInfoResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetExchangeInfoResponse.Merge(m, src) +} +func (m *GetExchangeInfoResponse) XXX_Size() int { + return xxx_messageInfo_GetExchangeInfoResponse.Size(m) +} +func (m *GetExchangeInfoResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetExchangeInfoResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetExchangeInfoResponse proto.InternalMessageInfo + +func (m *GetExchangeInfoResponse) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *GetExchangeInfoResponse) GetEnabled() bool { + if m != nil { + return m.Enabled + } + return false +} + +func (m *GetExchangeInfoResponse) GetVerbose() bool { + if m != nil { + return m.Verbose + } + return false +} + +func (m *GetExchangeInfoResponse) GetUsingSandbox() bool { + if m != nil { + return m.UsingSandbox + } + return false +} + +func (m *GetExchangeInfoResponse) GetHttpTimeout() string { + if m != nil { + return m.HttpTimeout + } + return "" +} + +func (m *GetExchangeInfoResponse) GetHttpUseragent() string { + if m != nil { + return m.HttpUseragent + } + return "" +} + +func (m *GetExchangeInfoResponse) GetHttpProxy() string { + if m != nil { + return m.HttpProxy + } + return "" +} + +func (m *GetExchangeInfoResponse) GetBaseCurrencies() string { + if m != nil { + return m.BaseCurrencies + } + return "" +} + +func (m *GetExchangeInfoResponse) GetSupportedAssets() map[string]*PairsSupported { + if m != nil { + return m.SupportedAssets + } + return nil +} + +func (m *GetExchangeInfoResponse) GetAuthenticatedApi() bool { + if m != nil { + return m.AuthenticatedApi + } + return false +} + +type GetTickerRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,2,opt,name=pair,proto3" json:"pair,omitempty"` + AssetType string `protobuf:"bytes,3,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetTickerRequest) Reset() { *m = GetTickerRequest{} } +func (m *GetTickerRequest) String() string { return proto.CompactTextString(m) } +func (*GetTickerRequest) ProtoMessage() {} +func (*GetTickerRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{22} +} + +func (m *GetTickerRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetTickerRequest.Unmarshal(m, b) +} +func (m *GetTickerRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetTickerRequest.Marshal(b, m, deterministic) +} +func (m *GetTickerRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetTickerRequest.Merge(m, src) +} +func (m *GetTickerRequest) XXX_Size() int { + return xxx_messageInfo_GetTickerRequest.Size(m) +} +func (m *GetTickerRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetTickerRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetTickerRequest proto.InternalMessageInfo + +func (m *GetTickerRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *GetTickerRequest) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *GetTickerRequest) GetAssetType() string { + if m != nil { + return m.AssetType + } + return "" +} + +type CurrencyPair struct { + Delimiter string `protobuf:"bytes,1,opt,name=delimiter,proto3" json:"delimiter,omitempty"` + Base string `protobuf:"bytes,2,opt,name=base,proto3" json:"base,omitempty"` + Quote string `protobuf:"bytes,3,opt,name=quote,proto3" json:"quote,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CurrencyPair) Reset() { *m = CurrencyPair{} } +func (m *CurrencyPair) String() string { return proto.CompactTextString(m) } +func (*CurrencyPair) ProtoMessage() {} +func (*CurrencyPair) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{23} +} + +func (m *CurrencyPair) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CurrencyPair.Unmarshal(m, b) +} +func (m *CurrencyPair) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CurrencyPair.Marshal(b, m, deterministic) +} +func (m *CurrencyPair) XXX_Merge(src proto.Message) { + xxx_messageInfo_CurrencyPair.Merge(m, src) +} +func (m *CurrencyPair) XXX_Size() int { + return xxx_messageInfo_CurrencyPair.Size(m) +} +func (m *CurrencyPair) XXX_DiscardUnknown() { + xxx_messageInfo_CurrencyPair.DiscardUnknown(m) +} + +var xxx_messageInfo_CurrencyPair proto.InternalMessageInfo + +func (m *CurrencyPair) GetDelimiter() string { + if m != nil { + return m.Delimiter + } + return "" +} + +func (m *CurrencyPair) GetBase() string { + if m != nil { + return m.Base + } + return "" +} + +func (m *CurrencyPair) GetQuote() string { + if m != nil { + return m.Quote + } + return "" +} + +type TickerResponse struct { + Pair *CurrencyPair `protobuf:"bytes,1,opt,name=pair,proto3" json:"pair,omitempty"` + LastUpdated int64 `protobuf:"varint,2,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"` + CurrencyPair string `protobuf:"bytes,3,opt,name=currency_pair,json=currencyPair,proto3" json:"currency_pair,omitempty"` + Last float64 `protobuf:"fixed64,4,opt,name=last,proto3" json:"last,omitempty"` + High float64 `protobuf:"fixed64,5,opt,name=high,proto3" json:"high,omitempty"` + Low float64 `protobuf:"fixed64,6,opt,name=low,proto3" json:"low,omitempty"` + Bid float64 `protobuf:"fixed64,7,opt,name=bid,proto3" json:"bid,omitempty"` + Ask float64 `protobuf:"fixed64,8,opt,name=ask,proto3" json:"ask,omitempty"` + Volume float64 `protobuf:"fixed64,9,opt,name=volume,proto3" json:"volume,omitempty"` + PriceAth float64 `protobuf:"fixed64,10,opt,name=price_ath,json=priceAth,proto3" json:"price_ath,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *TickerResponse) Reset() { *m = TickerResponse{} } +func (m *TickerResponse) String() string { return proto.CompactTextString(m) } +func (*TickerResponse) ProtoMessage() {} +func (*TickerResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{24} +} + +func (m *TickerResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_TickerResponse.Unmarshal(m, b) +} +func (m *TickerResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_TickerResponse.Marshal(b, m, deterministic) +} +func (m *TickerResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_TickerResponse.Merge(m, src) +} +func (m *TickerResponse) XXX_Size() int { + return xxx_messageInfo_TickerResponse.Size(m) +} +func (m *TickerResponse) XXX_DiscardUnknown() { + xxx_messageInfo_TickerResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_TickerResponse proto.InternalMessageInfo + +func (m *TickerResponse) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *TickerResponse) GetLastUpdated() int64 { + if m != nil { + return m.LastUpdated + } + return 0 +} + +func (m *TickerResponse) GetCurrencyPair() string { + if m != nil { + return m.CurrencyPair + } + return "" +} + +func (m *TickerResponse) GetLast() float64 { + if m != nil { + return m.Last + } + return 0 +} + +func (m *TickerResponse) GetHigh() float64 { + if m != nil { + return m.High + } + return 0 +} + +func (m *TickerResponse) GetLow() float64 { + if m != nil { + return m.Low + } + return 0 +} + +func (m *TickerResponse) GetBid() float64 { + if m != nil { + return m.Bid + } + return 0 +} + +func (m *TickerResponse) GetAsk() float64 { + if m != nil { + return m.Ask + } + return 0 +} + +func (m *TickerResponse) GetVolume() float64 { + if m != nil { + return m.Volume + } + return 0 +} + +func (m *TickerResponse) GetPriceAth() float64 { + if m != nil { + return m.PriceAth + } + return 0 +} + +type GetTickersRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetTickersRequest) Reset() { *m = GetTickersRequest{} } +func (m *GetTickersRequest) String() string { return proto.CompactTextString(m) } +func (*GetTickersRequest) ProtoMessage() {} +func (*GetTickersRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{25} +} + +func (m *GetTickersRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetTickersRequest.Unmarshal(m, b) +} +func (m *GetTickersRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetTickersRequest.Marshal(b, m, deterministic) +} +func (m *GetTickersRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetTickersRequest.Merge(m, src) +} +func (m *GetTickersRequest) XXX_Size() int { + return xxx_messageInfo_GetTickersRequest.Size(m) +} +func (m *GetTickersRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetTickersRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetTickersRequest proto.InternalMessageInfo + +type Tickers struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Tickers []*TickerResponse `protobuf:"bytes,2,rep,name=tickers,proto3" json:"tickers,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Tickers) Reset() { *m = Tickers{} } +func (m *Tickers) String() string { return proto.CompactTextString(m) } +func (*Tickers) ProtoMessage() {} +func (*Tickers) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{26} +} + +func (m *Tickers) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Tickers.Unmarshal(m, b) +} +func (m *Tickers) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Tickers.Marshal(b, m, deterministic) +} +func (m *Tickers) XXX_Merge(src proto.Message) { + xxx_messageInfo_Tickers.Merge(m, src) +} +func (m *Tickers) XXX_Size() int { + return xxx_messageInfo_Tickers.Size(m) +} +func (m *Tickers) XXX_DiscardUnknown() { + xxx_messageInfo_Tickers.DiscardUnknown(m) +} + +var xxx_messageInfo_Tickers proto.InternalMessageInfo + +func (m *Tickers) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *Tickers) GetTickers() []*TickerResponse { + if m != nil { + return m.Tickers + } + return nil +} + +type GetTickersResponse struct { + Tickers []*Tickers `protobuf:"bytes,1,rep,name=tickers,proto3" json:"tickers,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetTickersResponse) Reset() { *m = GetTickersResponse{} } +func (m *GetTickersResponse) String() string { return proto.CompactTextString(m) } +func (*GetTickersResponse) ProtoMessage() {} +func (*GetTickersResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{27} +} + +func (m *GetTickersResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetTickersResponse.Unmarshal(m, b) +} +func (m *GetTickersResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetTickersResponse.Marshal(b, m, deterministic) +} +func (m *GetTickersResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetTickersResponse.Merge(m, src) +} +func (m *GetTickersResponse) XXX_Size() int { + return xxx_messageInfo_GetTickersResponse.Size(m) +} +func (m *GetTickersResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetTickersResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetTickersResponse proto.InternalMessageInfo + +func (m *GetTickersResponse) GetTickers() []*Tickers { + if m != nil { + return m.Tickers + } + return nil +} + +type GetOrderbookRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,2,opt,name=pair,proto3" json:"pair,omitempty"` + AssetType string `protobuf:"bytes,3,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetOrderbookRequest) Reset() { *m = GetOrderbookRequest{} } +func (m *GetOrderbookRequest) String() string { return proto.CompactTextString(m) } +func (*GetOrderbookRequest) ProtoMessage() {} +func (*GetOrderbookRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{28} +} + +func (m *GetOrderbookRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetOrderbookRequest.Unmarshal(m, b) +} +func (m *GetOrderbookRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetOrderbookRequest.Marshal(b, m, deterministic) +} +func (m *GetOrderbookRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetOrderbookRequest.Merge(m, src) +} +func (m *GetOrderbookRequest) XXX_Size() int { + return xxx_messageInfo_GetOrderbookRequest.Size(m) +} +func (m *GetOrderbookRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetOrderbookRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetOrderbookRequest proto.InternalMessageInfo + +func (m *GetOrderbookRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *GetOrderbookRequest) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *GetOrderbookRequest) GetAssetType() string { + if m != nil { + return m.AssetType + } + return "" +} + +type OrderbookItem struct { + Amount float64 `protobuf:"fixed64,1,opt,name=amount,proto3" json:"amount,omitempty"` + Price float64 `protobuf:"fixed64,2,opt,name=price,proto3" json:"price,omitempty"` + Id int64 `protobuf:"varint,3,opt,name=id,proto3" json:"id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *OrderbookItem) Reset() { *m = OrderbookItem{} } +func (m *OrderbookItem) String() string { return proto.CompactTextString(m) } +func (*OrderbookItem) ProtoMessage() {} +func (*OrderbookItem) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{29} +} + +func (m *OrderbookItem) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_OrderbookItem.Unmarshal(m, b) +} +func (m *OrderbookItem) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_OrderbookItem.Marshal(b, m, deterministic) +} +func (m *OrderbookItem) XXX_Merge(src proto.Message) { + xxx_messageInfo_OrderbookItem.Merge(m, src) +} +func (m *OrderbookItem) XXX_Size() int { + return xxx_messageInfo_OrderbookItem.Size(m) +} +func (m *OrderbookItem) XXX_DiscardUnknown() { + xxx_messageInfo_OrderbookItem.DiscardUnknown(m) +} + +var xxx_messageInfo_OrderbookItem proto.InternalMessageInfo + +func (m *OrderbookItem) GetAmount() float64 { + if m != nil { + return m.Amount + } + return 0 +} + +func (m *OrderbookItem) GetPrice() float64 { + if m != nil { + return m.Price + } + return 0 +} + +func (m *OrderbookItem) GetId() int64 { + if m != nil { + return m.Id + } + return 0 +} + +type OrderbookResponse struct { + Pair *CurrencyPair `protobuf:"bytes,1,opt,name=pair,proto3" json:"pair,omitempty"` + CurrencyPair string `protobuf:"bytes,2,opt,name=currency_pair,json=currencyPair,proto3" json:"currency_pair,omitempty"` + Bids []*OrderbookItem `protobuf:"bytes,3,rep,name=bids,proto3" json:"bids,omitempty"` + Asks []*OrderbookItem `protobuf:"bytes,4,rep,name=asks,proto3" json:"asks,omitempty"` + LastUpdated int64 `protobuf:"varint,5,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"` + AssetType string `protobuf:"bytes,6,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *OrderbookResponse) Reset() { *m = OrderbookResponse{} } +func (m *OrderbookResponse) String() string { return proto.CompactTextString(m) } +func (*OrderbookResponse) ProtoMessage() {} +func (*OrderbookResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{30} +} + +func (m *OrderbookResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_OrderbookResponse.Unmarshal(m, b) +} +func (m *OrderbookResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_OrderbookResponse.Marshal(b, m, deterministic) +} +func (m *OrderbookResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_OrderbookResponse.Merge(m, src) +} +func (m *OrderbookResponse) XXX_Size() int { + return xxx_messageInfo_OrderbookResponse.Size(m) +} +func (m *OrderbookResponse) XXX_DiscardUnknown() { + xxx_messageInfo_OrderbookResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_OrderbookResponse proto.InternalMessageInfo + +func (m *OrderbookResponse) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *OrderbookResponse) GetCurrencyPair() string { + if m != nil { + return m.CurrencyPair + } + return "" +} + +func (m *OrderbookResponse) GetBids() []*OrderbookItem { + if m != nil { + return m.Bids + } + return nil +} + +func (m *OrderbookResponse) GetAsks() []*OrderbookItem { + if m != nil { + return m.Asks + } + return nil +} + +func (m *OrderbookResponse) GetLastUpdated() int64 { + if m != nil { + return m.LastUpdated + } + return 0 +} + +func (m *OrderbookResponse) GetAssetType() string { + if m != nil { + return m.AssetType + } + return "" +} + +type GetOrderbooksRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetOrderbooksRequest) Reset() { *m = GetOrderbooksRequest{} } +func (m *GetOrderbooksRequest) String() string { return proto.CompactTextString(m) } +func (*GetOrderbooksRequest) ProtoMessage() {} +func (*GetOrderbooksRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{31} +} + +func (m *GetOrderbooksRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetOrderbooksRequest.Unmarshal(m, b) +} +func (m *GetOrderbooksRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetOrderbooksRequest.Marshal(b, m, deterministic) +} +func (m *GetOrderbooksRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetOrderbooksRequest.Merge(m, src) +} +func (m *GetOrderbooksRequest) XXX_Size() int { + return xxx_messageInfo_GetOrderbooksRequest.Size(m) +} +func (m *GetOrderbooksRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetOrderbooksRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetOrderbooksRequest proto.InternalMessageInfo + +type Orderbooks struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Orderbooks []*OrderbookResponse `protobuf:"bytes,2,rep,name=orderbooks,proto3" json:"orderbooks,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Orderbooks) Reset() { *m = Orderbooks{} } +func (m *Orderbooks) String() string { return proto.CompactTextString(m) } +func (*Orderbooks) ProtoMessage() {} +func (*Orderbooks) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{32} +} + +func (m *Orderbooks) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Orderbooks.Unmarshal(m, b) +} +func (m *Orderbooks) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Orderbooks.Marshal(b, m, deterministic) +} +func (m *Orderbooks) XXX_Merge(src proto.Message) { + xxx_messageInfo_Orderbooks.Merge(m, src) +} +func (m *Orderbooks) XXX_Size() int { + return xxx_messageInfo_Orderbooks.Size(m) +} +func (m *Orderbooks) XXX_DiscardUnknown() { + xxx_messageInfo_Orderbooks.DiscardUnknown(m) +} + +var xxx_messageInfo_Orderbooks proto.InternalMessageInfo + +func (m *Orderbooks) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *Orderbooks) GetOrderbooks() []*OrderbookResponse { + if m != nil { + return m.Orderbooks + } + return nil +} + +type GetOrderbooksResponse struct { + Orderbooks []*Orderbooks `protobuf:"bytes,1,rep,name=orderbooks,proto3" json:"orderbooks,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetOrderbooksResponse) Reset() { *m = GetOrderbooksResponse{} } +func (m *GetOrderbooksResponse) String() string { return proto.CompactTextString(m) } +func (*GetOrderbooksResponse) ProtoMessage() {} +func (*GetOrderbooksResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{33} +} + +func (m *GetOrderbooksResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetOrderbooksResponse.Unmarshal(m, b) +} +func (m *GetOrderbooksResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetOrderbooksResponse.Marshal(b, m, deterministic) +} +func (m *GetOrderbooksResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetOrderbooksResponse.Merge(m, src) +} +func (m *GetOrderbooksResponse) XXX_Size() int { + return xxx_messageInfo_GetOrderbooksResponse.Size(m) +} +func (m *GetOrderbooksResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetOrderbooksResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetOrderbooksResponse proto.InternalMessageInfo + +func (m *GetOrderbooksResponse) GetOrderbooks() []*Orderbooks { + if m != nil { + return m.Orderbooks + } + return nil +} + +type GetAccountInfoRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetAccountInfoRequest) Reset() { *m = GetAccountInfoRequest{} } +func (m *GetAccountInfoRequest) String() string { return proto.CompactTextString(m) } +func (*GetAccountInfoRequest) ProtoMessage() {} +func (*GetAccountInfoRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{34} +} + +func (m *GetAccountInfoRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetAccountInfoRequest.Unmarshal(m, b) +} +func (m *GetAccountInfoRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetAccountInfoRequest.Marshal(b, m, deterministic) +} +func (m *GetAccountInfoRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetAccountInfoRequest.Merge(m, src) +} +func (m *GetAccountInfoRequest) XXX_Size() int { + return xxx_messageInfo_GetAccountInfoRequest.Size(m) +} +func (m *GetAccountInfoRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetAccountInfoRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetAccountInfoRequest proto.InternalMessageInfo + +func (m *GetAccountInfoRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +type Account struct { + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Currencies []*AccountCurrencyInfo `protobuf:"bytes,2,rep,name=currencies,proto3" json:"currencies,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Account) Reset() { *m = Account{} } +func (m *Account) String() string { return proto.CompactTextString(m) } +func (*Account) ProtoMessage() {} +func (*Account) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{35} +} + +func (m *Account) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Account.Unmarshal(m, b) +} +func (m *Account) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Account.Marshal(b, m, deterministic) +} +func (m *Account) XXX_Merge(src proto.Message) { + xxx_messageInfo_Account.Merge(m, src) +} +func (m *Account) XXX_Size() int { + return xxx_messageInfo_Account.Size(m) +} +func (m *Account) XXX_DiscardUnknown() { + xxx_messageInfo_Account.DiscardUnknown(m) +} + +var xxx_messageInfo_Account proto.InternalMessageInfo + +func (m *Account) GetId() string { + if m != nil { + return m.Id + } + return "" +} + +func (m *Account) GetCurrencies() []*AccountCurrencyInfo { + if m != nil { + return m.Currencies + } + return nil +} + +type AccountCurrencyInfo struct { + Currency string `protobuf:"bytes,1,opt,name=currency,proto3" json:"currency,omitempty"` + TotalValue float64 `protobuf:"fixed64,2,opt,name=total_value,json=totalValue,proto3" json:"total_value,omitempty"` + Hold float64 `protobuf:"fixed64,3,opt,name=hold,proto3" json:"hold,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AccountCurrencyInfo) Reset() { *m = AccountCurrencyInfo{} } +func (m *AccountCurrencyInfo) String() string { return proto.CompactTextString(m) } +func (*AccountCurrencyInfo) ProtoMessage() {} +func (*AccountCurrencyInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{36} +} + +func (m *AccountCurrencyInfo) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AccountCurrencyInfo.Unmarshal(m, b) +} +func (m *AccountCurrencyInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AccountCurrencyInfo.Marshal(b, m, deterministic) +} +func (m *AccountCurrencyInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_AccountCurrencyInfo.Merge(m, src) +} +func (m *AccountCurrencyInfo) XXX_Size() int { + return xxx_messageInfo_AccountCurrencyInfo.Size(m) +} +func (m *AccountCurrencyInfo) XXX_DiscardUnknown() { + xxx_messageInfo_AccountCurrencyInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_AccountCurrencyInfo proto.InternalMessageInfo + +func (m *AccountCurrencyInfo) GetCurrency() string { + if m != nil { + return m.Currency + } + return "" +} + +func (m *AccountCurrencyInfo) GetTotalValue() float64 { + if m != nil { + return m.TotalValue + } + return 0 +} + +func (m *AccountCurrencyInfo) GetHold() float64 { + if m != nil { + return m.Hold + } + return 0 +} + +type GetAccountInfoResponse struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Accounts []*Account `protobuf:"bytes,2,rep,name=accounts,proto3" json:"accounts,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetAccountInfoResponse) Reset() { *m = GetAccountInfoResponse{} } +func (m *GetAccountInfoResponse) String() string { return proto.CompactTextString(m) } +func (*GetAccountInfoResponse) ProtoMessage() {} +func (*GetAccountInfoResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{37} +} + +func (m *GetAccountInfoResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetAccountInfoResponse.Unmarshal(m, b) +} +func (m *GetAccountInfoResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetAccountInfoResponse.Marshal(b, m, deterministic) +} +func (m *GetAccountInfoResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetAccountInfoResponse.Merge(m, src) +} +func (m *GetAccountInfoResponse) XXX_Size() int { + return xxx_messageInfo_GetAccountInfoResponse.Size(m) +} +func (m *GetAccountInfoResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetAccountInfoResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetAccountInfoResponse proto.InternalMessageInfo + +func (m *GetAccountInfoResponse) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *GetAccountInfoResponse) GetAccounts() []*Account { + if m != nil { + return m.Accounts + } + return nil +} + +type GetConfigRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetConfigRequest) Reset() { *m = GetConfigRequest{} } +func (m *GetConfigRequest) String() string { return proto.CompactTextString(m) } +func (*GetConfigRequest) ProtoMessage() {} +func (*GetConfigRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{38} +} + +func (m *GetConfigRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetConfigRequest.Unmarshal(m, b) +} +func (m *GetConfigRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetConfigRequest.Marshal(b, m, deterministic) +} +func (m *GetConfigRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetConfigRequest.Merge(m, src) +} +func (m *GetConfigRequest) XXX_Size() int { + return xxx_messageInfo_GetConfigRequest.Size(m) +} +func (m *GetConfigRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetConfigRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetConfigRequest proto.InternalMessageInfo + +type GetConfigResponse struct { + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetConfigResponse) Reset() { *m = GetConfigResponse{} } +func (m *GetConfigResponse) String() string { return proto.CompactTextString(m) } +func (*GetConfigResponse) ProtoMessage() {} +func (*GetConfigResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{39} +} + +func (m *GetConfigResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetConfigResponse.Unmarshal(m, b) +} +func (m *GetConfigResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetConfigResponse.Marshal(b, m, deterministic) +} +func (m *GetConfigResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetConfigResponse.Merge(m, src) +} +func (m *GetConfigResponse) XXX_Size() int { + return xxx_messageInfo_GetConfigResponse.Size(m) +} +func (m *GetConfigResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetConfigResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetConfigResponse proto.InternalMessageInfo + +func (m *GetConfigResponse) GetData() []byte { + if m != nil { + return m.Data + } + return nil +} + +type PortfolioAddress struct { + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + CoinType string `protobuf:"bytes,2,opt,name=coin_type,json=coinType,proto3" json:"coin_type,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Balance float64 `protobuf:"fixed64,4,opt,name=balance,proto3" json:"balance,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PortfolioAddress) Reset() { *m = PortfolioAddress{} } +func (m *PortfolioAddress) String() string { return proto.CompactTextString(m) } +func (*PortfolioAddress) ProtoMessage() {} +func (*PortfolioAddress) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{40} +} + +func (m *PortfolioAddress) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PortfolioAddress.Unmarshal(m, b) +} +func (m *PortfolioAddress) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PortfolioAddress.Marshal(b, m, deterministic) +} +func (m *PortfolioAddress) XXX_Merge(src proto.Message) { + xxx_messageInfo_PortfolioAddress.Merge(m, src) +} +func (m *PortfolioAddress) XXX_Size() int { + return xxx_messageInfo_PortfolioAddress.Size(m) +} +func (m *PortfolioAddress) XXX_DiscardUnknown() { + xxx_messageInfo_PortfolioAddress.DiscardUnknown(m) +} + +var xxx_messageInfo_PortfolioAddress proto.InternalMessageInfo + +func (m *PortfolioAddress) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *PortfolioAddress) GetCoinType() string { + if m != nil { + return m.CoinType + } + return "" +} + +func (m *PortfolioAddress) GetDescription() string { + if m != nil { + return m.Description + } + return "" +} + +func (m *PortfolioAddress) GetBalance() float64 { + if m != nil { + return m.Balance + } + return 0 +} + +type GetPortfolioRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetPortfolioRequest) Reset() { *m = GetPortfolioRequest{} } +func (m *GetPortfolioRequest) String() string { return proto.CompactTextString(m) } +func (*GetPortfolioRequest) ProtoMessage() {} +func (*GetPortfolioRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{41} +} + +func (m *GetPortfolioRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetPortfolioRequest.Unmarshal(m, b) +} +func (m *GetPortfolioRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetPortfolioRequest.Marshal(b, m, deterministic) +} +func (m *GetPortfolioRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetPortfolioRequest.Merge(m, src) +} +func (m *GetPortfolioRequest) XXX_Size() int { + return xxx_messageInfo_GetPortfolioRequest.Size(m) +} +func (m *GetPortfolioRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetPortfolioRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetPortfolioRequest proto.InternalMessageInfo + +type GetPortfolioResponse struct { + Portfolio []*PortfolioAddress `protobuf:"bytes,1,rep,name=portfolio,proto3" json:"portfolio,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetPortfolioResponse) Reset() { *m = GetPortfolioResponse{} } +func (m *GetPortfolioResponse) String() string { return proto.CompactTextString(m) } +func (*GetPortfolioResponse) ProtoMessage() {} +func (*GetPortfolioResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{42} +} + +func (m *GetPortfolioResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetPortfolioResponse.Unmarshal(m, b) +} +func (m *GetPortfolioResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetPortfolioResponse.Marshal(b, m, deterministic) +} +func (m *GetPortfolioResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetPortfolioResponse.Merge(m, src) +} +func (m *GetPortfolioResponse) XXX_Size() int { + return xxx_messageInfo_GetPortfolioResponse.Size(m) +} +func (m *GetPortfolioResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetPortfolioResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetPortfolioResponse proto.InternalMessageInfo + +func (m *GetPortfolioResponse) GetPortfolio() []*PortfolioAddress { + if m != nil { + return m.Portfolio + } + return nil +} + +type GetPortfolioSummaryRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetPortfolioSummaryRequest) Reset() { *m = GetPortfolioSummaryRequest{} } +func (m *GetPortfolioSummaryRequest) String() string { return proto.CompactTextString(m) } +func (*GetPortfolioSummaryRequest) ProtoMessage() {} +func (*GetPortfolioSummaryRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{43} +} + +func (m *GetPortfolioSummaryRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetPortfolioSummaryRequest.Unmarshal(m, b) +} +func (m *GetPortfolioSummaryRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetPortfolioSummaryRequest.Marshal(b, m, deterministic) +} +func (m *GetPortfolioSummaryRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetPortfolioSummaryRequest.Merge(m, src) +} +func (m *GetPortfolioSummaryRequest) XXX_Size() int { + return xxx_messageInfo_GetPortfolioSummaryRequest.Size(m) +} +func (m *GetPortfolioSummaryRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetPortfolioSummaryRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetPortfolioSummaryRequest proto.InternalMessageInfo + +type Coin struct { + Coin string `protobuf:"bytes,1,opt,name=coin,proto3" json:"coin,omitempty"` + Balance float64 `protobuf:"fixed64,2,opt,name=balance,proto3" json:"balance,omitempty"` + Address string `protobuf:"bytes,3,opt,name=address,proto3" json:"address,omitempty"` + Percentage float64 `protobuf:"fixed64,4,opt,name=percentage,proto3" json:"percentage,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Coin) Reset() { *m = Coin{} } +func (m *Coin) String() string { return proto.CompactTextString(m) } +func (*Coin) ProtoMessage() {} +func (*Coin) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{44} +} + +func (m *Coin) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Coin.Unmarshal(m, b) +} +func (m *Coin) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Coin.Marshal(b, m, deterministic) +} +func (m *Coin) XXX_Merge(src proto.Message) { + xxx_messageInfo_Coin.Merge(m, src) +} +func (m *Coin) XXX_Size() int { + return xxx_messageInfo_Coin.Size(m) +} +func (m *Coin) XXX_DiscardUnknown() { + xxx_messageInfo_Coin.DiscardUnknown(m) +} + +var xxx_messageInfo_Coin proto.InternalMessageInfo + +func (m *Coin) GetCoin() string { + if m != nil { + return m.Coin + } + return "" +} + +func (m *Coin) GetBalance() float64 { + if m != nil { + return m.Balance + } + return 0 +} + +func (m *Coin) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *Coin) GetPercentage() float64 { + if m != nil { + return m.Percentage + } + return 0 +} + +type OfflineCoinSummary struct { + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Balance float64 `protobuf:"fixed64,2,opt,name=balance,proto3" json:"balance,omitempty"` + Percentage float64 `protobuf:"fixed64,3,opt,name=percentage,proto3" json:"percentage,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *OfflineCoinSummary) Reset() { *m = OfflineCoinSummary{} } +func (m *OfflineCoinSummary) String() string { return proto.CompactTextString(m) } +func (*OfflineCoinSummary) ProtoMessage() {} +func (*OfflineCoinSummary) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{45} +} + +func (m *OfflineCoinSummary) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_OfflineCoinSummary.Unmarshal(m, b) +} +func (m *OfflineCoinSummary) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_OfflineCoinSummary.Marshal(b, m, deterministic) +} +func (m *OfflineCoinSummary) XXX_Merge(src proto.Message) { + xxx_messageInfo_OfflineCoinSummary.Merge(m, src) +} +func (m *OfflineCoinSummary) XXX_Size() int { + return xxx_messageInfo_OfflineCoinSummary.Size(m) +} +func (m *OfflineCoinSummary) XXX_DiscardUnknown() { + xxx_messageInfo_OfflineCoinSummary.DiscardUnknown(m) +} + +var xxx_messageInfo_OfflineCoinSummary proto.InternalMessageInfo + +func (m *OfflineCoinSummary) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *OfflineCoinSummary) GetBalance() float64 { + if m != nil { + return m.Balance + } + return 0 +} + +func (m *OfflineCoinSummary) GetPercentage() float64 { + if m != nil { + return m.Percentage + } + return 0 +} + +type OnlineCoinSummary struct { + Balance float64 `protobuf:"fixed64,1,opt,name=balance,proto3" json:"balance,omitempty"` + Percentage float64 `protobuf:"fixed64,2,opt,name=percentage,proto3" json:"percentage,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *OnlineCoinSummary) Reset() { *m = OnlineCoinSummary{} } +func (m *OnlineCoinSummary) String() string { return proto.CompactTextString(m) } +func (*OnlineCoinSummary) ProtoMessage() {} +func (*OnlineCoinSummary) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{46} +} + +func (m *OnlineCoinSummary) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_OnlineCoinSummary.Unmarshal(m, b) +} +func (m *OnlineCoinSummary) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_OnlineCoinSummary.Marshal(b, m, deterministic) +} +func (m *OnlineCoinSummary) XXX_Merge(src proto.Message) { + xxx_messageInfo_OnlineCoinSummary.Merge(m, src) +} +func (m *OnlineCoinSummary) XXX_Size() int { + return xxx_messageInfo_OnlineCoinSummary.Size(m) +} +func (m *OnlineCoinSummary) XXX_DiscardUnknown() { + xxx_messageInfo_OnlineCoinSummary.DiscardUnknown(m) +} + +var xxx_messageInfo_OnlineCoinSummary proto.InternalMessageInfo + +func (m *OnlineCoinSummary) GetBalance() float64 { + if m != nil { + return m.Balance + } + return 0 +} + +func (m *OnlineCoinSummary) GetPercentage() float64 { + if m != nil { + return m.Percentage + } + return 0 +} + +type OfflineCoins struct { + Addresses []*OfflineCoinSummary `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *OfflineCoins) Reset() { *m = OfflineCoins{} } +func (m *OfflineCoins) String() string { return proto.CompactTextString(m) } +func (*OfflineCoins) ProtoMessage() {} +func (*OfflineCoins) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{47} +} + +func (m *OfflineCoins) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_OfflineCoins.Unmarshal(m, b) +} +func (m *OfflineCoins) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_OfflineCoins.Marshal(b, m, deterministic) +} +func (m *OfflineCoins) XXX_Merge(src proto.Message) { + xxx_messageInfo_OfflineCoins.Merge(m, src) +} +func (m *OfflineCoins) XXX_Size() int { + return xxx_messageInfo_OfflineCoins.Size(m) +} +func (m *OfflineCoins) XXX_DiscardUnknown() { + xxx_messageInfo_OfflineCoins.DiscardUnknown(m) +} + +var xxx_messageInfo_OfflineCoins proto.InternalMessageInfo + +func (m *OfflineCoins) GetAddresses() []*OfflineCoinSummary { + if m != nil { + return m.Addresses + } + return nil +} + +type OnlineCoins struct { + Coins map[string]*OnlineCoinSummary `protobuf:"bytes,1,rep,name=coins,proto3" json:"coins,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *OnlineCoins) Reset() { *m = OnlineCoins{} } +func (m *OnlineCoins) String() string { return proto.CompactTextString(m) } +func (*OnlineCoins) ProtoMessage() {} +func (*OnlineCoins) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{48} +} + +func (m *OnlineCoins) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_OnlineCoins.Unmarshal(m, b) +} +func (m *OnlineCoins) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_OnlineCoins.Marshal(b, m, deterministic) +} +func (m *OnlineCoins) XXX_Merge(src proto.Message) { + xxx_messageInfo_OnlineCoins.Merge(m, src) +} +func (m *OnlineCoins) XXX_Size() int { + return xxx_messageInfo_OnlineCoins.Size(m) +} +func (m *OnlineCoins) XXX_DiscardUnknown() { + xxx_messageInfo_OnlineCoins.DiscardUnknown(m) +} + +var xxx_messageInfo_OnlineCoins proto.InternalMessageInfo + +func (m *OnlineCoins) GetCoins() map[string]*OnlineCoinSummary { + if m != nil { + return m.Coins + } + return nil +} + +type GetPortfolioSummaryResponse struct { + CoinTotals []*Coin `protobuf:"bytes,1,rep,name=coin_totals,json=coinTotals,proto3" json:"coin_totals,omitempty"` + CoinsOffline []*Coin `protobuf:"bytes,2,rep,name=coins_offline,json=coinsOffline,proto3" json:"coins_offline,omitempty"` + CoinsOfflineSummary map[string]*OfflineCoins `protobuf:"bytes,3,rep,name=coins_offline_summary,json=coinsOfflineSummary,proto3" json:"coins_offline_summary,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + CoinsOnline []*Coin `protobuf:"bytes,4,rep,name=coins_online,json=coinsOnline,proto3" json:"coins_online,omitempty"` + CoinsOnlineSummary map[string]*OnlineCoins `protobuf:"bytes,5,rep,name=coins_online_summary,json=coinsOnlineSummary,proto3" json:"coins_online_summary,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetPortfolioSummaryResponse) Reset() { *m = GetPortfolioSummaryResponse{} } +func (m *GetPortfolioSummaryResponse) String() string { return proto.CompactTextString(m) } +func (*GetPortfolioSummaryResponse) ProtoMessage() {} +func (*GetPortfolioSummaryResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{49} +} + +func (m *GetPortfolioSummaryResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetPortfolioSummaryResponse.Unmarshal(m, b) +} +func (m *GetPortfolioSummaryResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetPortfolioSummaryResponse.Marshal(b, m, deterministic) +} +func (m *GetPortfolioSummaryResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetPortfolioSummaryResponse.Merge(m, src) +} +func (m *GetPortfolioSummaryResponse) XXX_Size() int { + return xxx_messageInfo_GetPortfolioSummaryResponse.Size(m) +} +func (m *GetPortfolioSummaryResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetPortfolioSummaryResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetPortfolioSummaryResponse proto.InternalMessageInfo + +func (m *GetPortfolioSummaryResponse) GetCoinTotals() []*Coin { + if m != nil { + return m.CoinTotals + } + return nil +} + +func (m *GetPortfolioSummaryResponse) GetCoinsOffline() []*Coin { + if m != nil { + return m.CoinsOffline + } + return nil +} + +func (m *GetPortfolioSummaryResponse) GetCoinsOfflineSummary() map[string]*OfflineCoins { + if m != nil { + return m.CoinsOfflineSummary + } + return nil +} + +func (m *GetPortfolioSummaryResponse) GetCoinsOnline() []*Coin { + if m != nil { + return m.CoinsOnline + } + return nil +} + +func (m *GetPortfolioSummaryResponse) GetCoinsOnlineSummary() map[string]*OnlineCoins { + if m != nil { + return m.CoinsOnlineSummary + } + return nil +} + +type AddPortfolioAddressRequest struct { + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + CoinType string `protobuf:"bytes,2,opt,name=coin_type,json=coinType,proto3" json:"coin_type,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Balance float64 `protobuf:"fixed64,4,opt,name=balance,proto3" json:"balance,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AddPortfolioAddressRequest) Reset() { *m = AddPortfolioAddressRequest{} } +func (m *AddPortfolioAddressRequest) String() string { return proto.CompactTextString(m) } +func (*AddPortfolioAddressRequest) ProtoMessage() {} +func (*AddPortfolioAddressRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{50} +} + +func (m *AddPortfolioAddressRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AddPortfolioAddressRequest.Unmarshal(m, b) +} +func (m *AddPortfolioAddressRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AddPortfolioAddressRequest.Marshal(b, m, deterministic) +} +func (m *AddPortfolioAddressRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_AddPortfolioAddressRequest.Merge(m, src) +} +func (m *AddPortfolioAddressRequest) XXX_Size() int { + return xxx_messageInfo_AddPortfolioAddressRequest.Size(m) +} +func (m *AddPortfolioAddressRequest) XXX_DiscardUnknown() { + xxx_messageInfo_AddPortfolioAddressRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_AddPortfolioAddressRequest proto.InternalMessageInfo + +func (m *AddPortfolioAddressRequest) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *AddPortfolioAddressRequest) GetCoinType() string { + if m != nil { + return m.CoinType + } + return "" +} + +func (m *AddPortfolioAddressRequest) GetDescription() string { + if m != nil { + return m.Description + } + return "" +} + +func (m *AddPortfolioAddressRequest) GetBalance() float64 { + if m != nil { + return m.Balance + } + return 0 +} + +type AddPortfolioAddressResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AddPortfolioAddressResponse) Reset() { *m = AddPortfolioAddressResponse{} } +func (m *AddPortfolioAddressResponse) String() string { return proto.CompactTextString(m) } +func (*AddPortfolioAddressResponse) ProtoMessage() {} +func (*AddPortfolioAddressResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{51} +} + +func (m *AddPortfolioAddressResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AddPortfolioAddressResponse.Unmarshal(m, b) +} +func (m *AddPortfolioAddressResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AddPortfolioAddressResponse.Marshal(b, m, deterministic) +} +func (m *AddPortfolioAddressResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_AddPortfolioAddressResponse.Merge(m, src) +} +func (m *AddPortfolioAddressResponse) XXX_Size() int { + return xxx_messageInfo_AddPortfolioAddressResponse.Size(m) +} +func (m *AddPortfolioAddressResponse) XXX_DiscardUnknown() { + xxx_messageInfo_AddPortfolioAddressResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_AddPortfolioAddressResponse proto.InternalMessageInfo + +type RemovePortfolioAddressRequest struct { + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + CoinType string `protobuf:"bytes,2,opt,name=coin_type,json=coinType,proto3" json:"coin_type,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RemovePortfolioAddressRequest) Reset() { *m = RemovePortfolioAddressRequest{} } +func (m *RemovePortfolioAddressRequest) String() string { return proto.CompactTextString(m) } +func (*RemovePortfolioAddressRequest) ProtoMessage() {} +func (*RemovePortfolioAddressRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{52} +} + +func (m *RemovePortfolioAddressRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RemovePortfolioAddressRequest.Unmarshal(m, b) +} +func (m *RemovePortfolioAddressRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RemovePortfolioAddressRequest.Marshal(b, m, deterministic) +} +func (m *RemovePortfolioAddressRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_RemovePortfolioAddressRequest.Merge(m, src) +} +func (m *RemovePortfolioAddressRequest) XXX_Size() int { + return xxx_messageInfo_RemovePortfolioAddressRequest.Size(m) +} +func (m *RemovePortfolioAddressRequest) XXX_DiscardUnknown() { + xxx_messageInfo_RemovePortfolioAddressRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_RemovePortfolioAddressRequest proto.InternalMessageInfo + +func (m *RemovePortfolioAddressRequest) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *RemovePortfolioAddressRequest) GetCoinType() string { + if m != nil { + return m.CoinType + } + return "" +} + +func (m *RemovePortfolioAddressRequest) GetDescription() string { + if m != nil { + return m.Description + } + return "" +} + +type RemovePortfolioAddressResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RemovePortfolioAddressResponse) Reset() { *m = RemovePortfolioAddressResponse{} } +func (m *RemovePortfolioAddressResponse) String() string { return proto.CompactTextString(m) } +func (*RemovePortfolioAddressResponse) ProtoMessage() {} +func (*RemovePortfolioAddressResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{53} +} + +func (m *RemovePortfolioAddressResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RemovePortfolioAddressResponse.Unmarshal(m, b) +} +func (m *RemovePortfolioAddressResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RemovePortfolioAddressResponse.Marshal(b, m, deterministic) +} +func (m *RemovePortfolioAddressResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_RemovePortfolioAddressResponse.Merge(m, src) +} +func (m *RemovePortfolioAddressResponse) XXX_Size() int { + return xxx_messageInfo_RemovePortfolioAddressResponse.Size(m) +} +func (m *RemovePortfolioAddressResponse) XXX_DiscardUnknown() { + xxx_messageInfo_RemovePortfolioAddressResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_RemovePortfolioAddressResponse proto.InternalMessageInfo + +type GetForexProvidersRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetForexProvidersRequest) Reset() { *m = GetForexProvidersRequest{} } +func (m *GetForexProvidersRequest) String() string { return proto.CompactTextString(m) } +func (*GetForexProvidersRequest) ProtoMessage() {} +func (*GetForexProvidersRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{54} +} + +func (m *GetForexProvidersRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetForexProvidersRequest.Unmarshal(m, b) +} +func (m *GetForexProvidersRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetForexProvidersRequest.Marshal(b, m, deterministic) +} +func (m *GetForexProvidersRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetForexProvidersRequest.Merge(m, src) +} +func (m *GetForexProvidersRequest) XXX_Size() int { + return xxx_messageInfo_GetForexProvidersRequest.Size(m) +} +func (m *GetForexProvidersRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetForexProvidersRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetForexProvidersRequest proto.InternalMessageInfo + +type ForexProvider struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` + Verbose bool `protobuf:"varint,3,opt,name=verbose,proto3" json:"verbose,omitempty"` + RestPollingDelay string `protobuf:"bytes,4,opt,name=rest_polling_delay,json=restPollingDelay,proto3" json:"rest_polling_delay,omitempty"` + ApiKey string `protobuf:"bytes,5,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` + ApiKeyLevel int64 `protobuf:"varint,6,opt,name=api_key_level,json=apiKeyLevel,proto3" json:"api_key_level,omitempty"` + PrimaryProvider bool `protobuf:"varint,7,opt,name=primary_provider,json=primaryProvider,proto3" json:"primary_provider,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ForexProvider) Reset() { *m = ForexProvider{} } +func (m *ForexProvider) String() string { return proto.CompactTextString(m) } +func (*ForexProvider) ProtoMessage() {} +func (*ForexProvider) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{55} +} + +func (m *ForexProvider) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ForexProvider.Unmarshal(m, b) +} +func (m *ForexProvider) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ForexProvider.Marshal(b, m, deterministic) +} +func (m *ForexProvider) XXX_Merge(src proto.Message) { + xxx_messageInfo_ForexProvider.Merge(m, src) +} +func (m *ForexProvider) XXX_Size() int { + return xxx_messageInfo_ForexProvider.Size(m) +} +func (m *ForexProvider) XXX_DiscardUnknown() { + xxx_messageInfo_ForexProvider.DiscardUnknown(m) +} + +var xxx_messageInfo_ForexProvider proto.InternalMessageInfo + +func (m *ForexProvider) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *ForexProvider) GetEnabled() bool { + if m != nil { + return m.Enabled + } + return false +} + +func (m *ForexProvider) GetVerbose() bool { + if m != nil { + return m.Verbose + } + return false +} + +func (m *ForexProvider) GetRestPollingDelay() string { + if m != nil { + return m.RestPollingDelay + } + return "" +} + +func (m *ForexProvider) GetApiKey() string { + if m != nil { + return m.ApiKey + } + return "" +} + +func (m *ForexProvider) GetApiKeyLevel() int64 { + if m != nil { + return m.ApiKeyLevel + } + return 0 +} + +func (m *ForexProvider) GetPrimaryProvider() bool { + if m != nil { + return m.PrimaryProvider + } + return false +} + +type GetForexProvidersResponse struct { + ForexProviders []*ForexProvider `protobuf:"bytes,1,rep,name=forex_providers,json=forexProviders,proto3" json:"forex_providers,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetForexProvidersResponse) Reset() { *m = GetForexProvidersResponse{} } +func (m *GetForexProvidersResponse) String() string { return proto.CompactTextString(m) } +func (*GetForexProvidersResponse) ProtoMessage() {} +func (*GetForexProvidersResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{56} +} + +func (m *GetForexProvidersResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetForexProvidersResponse.Unmarshal(m, b) +} +func (m *GetForexProvidersResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetForexProvidersResponse.Marshal(b, m, deterministic) +} +func (m *GetForexProvidersResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetForexProvidersResponse.Merge(m, src) +} +func (m *GetForexProvidersResponse) XXX_Size() int { + return xxx_messageInfo_GetForexProvidersResponse.Size(m) +} +func (m *GetForexProvidersResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetForexProvidersResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetForexProvidersResponse proto.InternalMessageInfo + +func (m *GetForexProvidersResponse) GetForexProviders() []*ForexProvider { + if m != nil { + return m.ForexProviders + } + return nil +} + +type GetForexRatesRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetForexRatesRequest) Reset() { *m = GetForexRatesRequest{} } +func (m *GetForexRatesRequest) String() string { return proto.CompactTextString(m) } +func (*GetForexRatesRequest) ProtoMessage() {} +func (*GetForexRatesRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{57} +} + +func (m *GetForexRatesRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetForexRatesRequest.Unmarshal(m, b) +} +func (m *GetForexRatesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetForexRatesRequest.Marshal(b, m, deterministic) +} +func (m *GetForexRatesRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetForexRatesRequest.Merge(m, src) +} +func (m *GetForexRatesRequest) XXX_Size() int { + return xxx_messageInfo_GetForexRatesRequest.Size(m) +} +func (m *GetForexRatesRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetForexRatesRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetForexRatesRequest proto.InternalMessageInfo + +type ForexRatesConversion struct { + From string `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` + To string `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` + Rate float64 `protobuf:"fixed64,3,opt,name=rate,proto3" json:"rate,omitempty"` + InverseRate float64 `protobuf:"fixed64,4,opt,name=inverse_rate,json=inverseRate,proto3" json:"inverse_rate,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ForexRatesConversion) Reset() { *m = ForexRatesConversion{} } +func (m *ForexRatesConversion) String() string { return proto.CompactTextString(m) } +func (*ForexRatesConversion) ProtoMessage() {} +func (*ForexRatesConversion) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{58} +} + +func (m *ForexRatesConversion) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ForexRatesConversion.Unmarshal(m, b) +} +func (m *ForexRatesConversion) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ForexRatesConversion.Marshal(b, m, deterministic) +} +func (m *ForexRatesConversion) XXX_Merge(src proto.Message) { + xxx_messageInfo_ForexRatesConversion.Merge(m, src) +} +func (m *ForexRatesConversion) XXX_Size() int { + return xxx_messageInfo_ForexRatesConversion.Size(m) +} +func (m *ForexRatesConversion) XXX_DiscardUnknown() { + xxx_messageInfo_ForexRatesConversion.DiscardUnknown(m) +} + +var xxx_messageInfo_ForexRatesConversion proto.InternalMessageInfo + +func (m *ForexRatesConversion) GetFrom() string { + if m != nil { + return m.From + } + return "" +} + +func (m *ForexRatesConversion) GetTo() string { + if m != nil { + return m.To + } + return "" +} + +func (m *ForexRatesConversion) GetRate() float64 { + if m != nil { + return m.Rate + } + return 0 +} + +func (m *ForexRatesConversion) GetInverseRate() float64 { + if m != nil { + return m.InverseRate + } + return 0 +} + +type GetForexRatesResponse struct { + ForexRates []*ForexRatesConversion `protobuf:"bytes,1,rep,name=forex_rates,json=forexRates,proto3" json:"forex_rates,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetForexRatesResponse) Reset() { *m = GetForexRatesResponse{} } +func (m *GetForexRatesResponse) String() string { return proto.CompactTextString(m) } +func (*GetForexRatesResponse) ProtoMessage() {} +func (*GetForexRatesResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{59} +} + +func (m *GetForexRatesResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetForexRatesResponse.Unmarshal(m, b) +} +func (m *GetForexRatesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetForexRatesResponse.Marshal(b, m, deterministic) +} +func (m *GetForexRatesResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetForexRatesResponse.Merge(m, src) +} +func (m *GetForexRatesResponse) XXX_Size() int { + return xxx_messageInfo_GetForexRatesResponse.Size(m) +} +func (m *GetForexRatesResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetForexRatesResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetForexRatesResponse proto.InternalMessageInfo + +func (m *GetForexRatesResponse) GetForexRates() []*ForexRatesConversion { + if m != nil { + return m.ForexRates + } + return nil +} + +type OrderDetails struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + BaseCurrency string `protobuf:"bytes,3,opt,name=base_currency,json=baseCurrency,proto3" json:"base_currency,omitempty"` + QuoteCurrency string `protobuf:"bytes,4,opt,name=quote_currency,json=quoteCurrency,proto3" json:"quote_currency,omitempty"` + AssetType string `protobuf:"bytes,5,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"` + OrderSide string `protobuf:"bytes,6,opt,name=order_side,json=orderSide,proto3" json:"order_side,omitempty"` + OrderType string `protobuf:"bytes,7,opt,name=order_type,json=orderType,proto3" json:"order_type,omitempty"` + CreationTime int64 `protobuf:"varint,8,opt,name=creation_time,json=creationTime,proto3" json:"creation_time,omitempty"` + Status string `protobuf:"bytes,9,opt,name=status,proto3" json:"status,omitempty"` + Price float64 `protobuf:"fixed64,10,opt,name=price,proto3" json:"price,omitempty"` + Amount float64 `protobuf:"fixed64,11,opt,name=amount,proto3" json:"amount,omitempty"` + OpenVolume float64 `protobuf:"fixed64,12,opt,name=open_volume,json=openVolume,proto3" json:"open_volume,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *OrderDetails) Reset() { *m = OrderDetails{} } +func (m *OrderDetails) String() string { return proto.CompactTextString(m) } +func (*OrderDetails) ProtoMessage() {} +func (*OrderDetails) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{60} +} + +func (m *OrderDetails) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_OrderDetails.Unmarshal(m, b) +} +func (m *OrderDetails) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_OrderDetails.Marshal(b, m, deterministic) +} +func (m *OrderDetails) XXX_Merge(src proto.Message) { + xxx_messageInfo_OrderDetails.Merge(m, src) +} +func (m *OrderDetails) XXX_Size() int { + return xxx_messageInfo_OrderDetails.Size(m) +} +func (m *OrderDetails) XXX_DiscardUnknown() { + xxx_messageInfo_OrderDetails.DiscardUnknown(m) +} + +var xxx_messageInfo_OrderDetails proto.InternalMessageInfo + +func (m *OrderDetails) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *OrderDetails) GetId() string { + if m != nil { + return m.Id + } + return "" +} + +func (m *OrderDetails) GetBaseCurrency() string { + if m != nil { + return m.BaseCurrency + } + return "" +} + +func (m *OrderDetails) GetQuoteCurrency() string { + if m != nil { + return m.QuoteCurrency + } + return "" +} + +func (m *OrderDetails) GetAssetType() string { + if m != nil { + return m.AssetType + } + return "" +} + +func (m *OrderDetails) GetOrderSide() string { + if m != nil { + return m.OrderSide + } + return "" +} + +func (m *OrderDetails) GetOrderType() string { + if m != nil { + return m.OrderType + } + return "" +} + +func (m *OrderDetails) GetCreationTime() int64 { + if m != nil { + return m.CreationTime + } + return 0 +} + +func (m *OrderDetails) GetStatus() string { + if m != nil { + return m.Status + } + return "" +} + +func (m *OrderDetails) GetPrice() float64 { + if m != nil { + return m.Price + } + return 0 +} + +func (m *OrderDetails) GetAmount() float64 { + if m != nil { + return m.Amount + } + return 0 +} + +func (m *OrderDetails) GetOpenVolume() float64 { + if m != nil { + return m.OpenVolume + } + return 0 +} + +type GetOrdersRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + AssetType string `protobuf:"bytes,2,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,3,opt,name=pair,proto3" json:"pair,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetOrdersRequest) Reset() { *m = GetOrdersRequest{} } +func (m *GetOrdersRequest) String() string { return proto.CompactTextString(m) } +func (*GetOrdersRequest) ProtoMessage() {} +func (*GetOrdersRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{61} +} + +func (m *GetOrdersRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetOrdersRequest.Unmarshal(m, b) +} +func (m *GetOrdersRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetOrdersRequest.Marshal(b, m, deterministic) +} +func (m *GetOrdersRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetOrdersRequest.Merge(m, src) +} +func (m *GetOrdersRequest) XXX_Size() int { + return xxx_messageInfo_GetOrdersRequest.Size(m) +} +func (m *GetOrdersRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetOrdersRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetOrdersRequest proto.InternalMessageInfo + +func (m *GetOrdersRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *GetOrdersRequest) GetAssetType() string { + if m != nil { + return m.AssetType + } + return "" +} + +func (m *GetOrdersRequest) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +type GetOrdersResponse struct { + Orders []*OrderDetails `protobuf:"bytes,1,rep,name=orders,proto3" json:"orders,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetOrdersResponse) Reset() { *m = GetOrdersResponse{} } +func (m *GetOrdersResponse) String() string { return proto.CompactTextString(m) } +func (*GetOrdersResponse) ProtoMessage() {} +func (*GetOrdersResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{62} +} + +func (m *GetOrdersResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetOrdersResponse.Unmarshal(m, b) +} +func (m *GetOrdersResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetOrdersResponse.Marshal(b, m, deterministic) +} +func (m *GetOrdersResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetOrdersResponse.Merge(m, src) +} +func (m *GetOrdersResponse) XXX_Size() int { + return xxx_messageInfo_GetOrdersResponse.Size(m) +} +func (m *GetOrdersResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetOrdersResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetOrdersResponse proto.InternalMessageInfo + +func (m *GetOrdersResponse) GetOrders() []*OrderDetails { + if m != nil { + return m.Orders + } + return nil +} + +type GetOrderRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + OrderId string `protobuf:"bytes,2,opt,name=order_id,json=orderId,proto3" json:"order_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetOrderRequest) Reset() { *m = GetOrderRequest{} } +func (m *GetOrderRequest) String() string { return proto.CompactTextString(m) } +func (*GetOrderRequest) ProtoMessage() {} +func (*GetOrderRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{63} +} + +func (m *GetOrderRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetOrderRequest.Unmarshal(m, b) +} +func (m *GetOrderRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetOrderRequest.Marshal(b, m, deterministic) +} +func (m *GetOrderRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetOrderRequest.Merge(m, src) +} +func (m *GetOrderRequest) XXX_Size() int { + return xxx_messageInfo_GetOrderRequest.Size(m) +} +func (m *GetOrderRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetOrderRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetOrderRequest proto.InternalMessageInfo + +func (m *GetOrderRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *GetOrderRequest) GetOrderId() string { + if m != nil { + return m.OrderId + } + return "" +} + +type SubmitOrderRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,2,opt,name=pair,proto3" json:"pair,omitempty"` + Side string `protobuf:"bytes,3,opt,name=side,proto3" json:"side,omitempty"` + OrderType string `protobuf:"bytes,4,opt,name=order_type,json=orderType,proto3" json:"order_type,omitempty"` + Amount float64 `protobuf:"fixed64,5,opt,name=amount,proto3" json:"amount,omitempty"` + Price float64 `protobuf:"fixed64,6,opt,name=price,proto3" json:"price,omitempty"` + ClientId string `protobuf:"bytes,7,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SubmitOrderRequest) Reset() { *m = SubmitOrderRequest{} } +func (m *SubmitOrderRequest) String() string { return proto.CompactTextString(m) } +func (*SubmitOrderRequest) ProtoMessage() {} +func (*SubmitOrderRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{64} +} + +func (m *SubmitOrderRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SubmitOrderRequest.Unmarshal(m, b) +} +func (m *SubmitOrderRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SubmitOrderRequest.Marshal(b, m, deterministic) +} +func (m *SubmitOrderRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_SubmitOrderRequest.Merge(m, src) +} +func (m *SubmitOrderRequest) XXX_Size() int { + return xxx_messageInfo_SubmitOrderRequest.Size(m) +} +func (m *SubmitOrderRequest) XXX_DiscardUnknown() { + xxx_messageInfo_SubmitOrderRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_SubmitOrderRequest proto.InternalMessageInfo + +func (m *SubmitOrderRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *SubmitOrderRequest) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *SubmitOrderRequest) GetSide() string { + if m != nil { + return m.Side + } + return "" +} + +func (m *SubmitOrderRequest) GetOrderType() string { + if m != nil { + return m.OrderType + } + return "" +} + +func (m *SubmitOrderRequest) GetAmount() float64 { + if m != nil { + return m.Amount + } + return 0 +} + +func (m *SubmitOrderRequest) GetPrice() float64 { + if m != nil { + return m.Price + } + return 0 +} + +func (m *SubmitOrderRequest) GetClientId() string { + if m != nil { + return m.ClientId + } + return "" +} + +type SubmitOrderResponse struct { + OrderPlaced bool `protobuf:"varint,1,opt,name=order_placed,json=orderPlaced,proto3" json:"order_placed,omitempty"` + OrderId string `protobuf:"bytes,2,opt,name=order_id,json=orderId,proto3" json:"order_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SubmitOrderResponse) Reset() { *m = SubmitOrderResponse{} } +func (m *SubmitOrderResponse) String() string { return proto.CompactTextString(m) } +func (*SubmitOrderResponse) ProtoMessage() {} +func (*SubmitOrderResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{65} +} + +func (m *SubmitOrderResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SubmitOrderResponse.Unmarshal(m, b) +} +func (m *SubmitOrderResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SubmitOrderResponse.Marshal(b, m, deterministic) +} +func (m *SubmitOrderResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_SubmitOrderResponse.Merge(m, src) +} +func (m *SubmitOrderResponse) XXX_Size() int { + return xxx_messageInfo_SubmitOrderResponse.Size(m) +} +func (m *SubmitOrderResponse) XXX_DiscardUnknown() { + xxx_messageInfo_SubmitOrderResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_SubmitOrderResponse proto.InternalMessageInfo + +func (m *SubmitOrderResponse) GetOrderPlaced() bool { + if m != nil { + return m.OrderPlaced + } + return false +} + +func (m *SubmitOrderResponse) GetOrderId() string { + if m != nil { + return m.OrderId + } + return "" +} + +type SimulateOrderRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,2,opt,name=pair,proto3" json:"pair,omitempty"` + Amount float64 `protobuf:"fixed64,3,opt,name=amount,proto3" json:"amount,omitempty"` + Side string `protobuf:"bytes,4,opt,name=side,proto3" json:"side,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SimulateOrderRequest) Reset() { *m = SimulateOrderRequest{} } +func (m *SimulateOrderRequest) String() string { return proto.CompactTextString(m) } +func (*SimulateOrderRequest) ProtoMessage() {} +func (*SimulateOrderRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{66} +} + +func (m *SimulateOrderRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SimulateOrderRequest.Unmarshal(m, b) +} +func (m *SimulateOrderRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SimulateOrderRequest.Marshal(b, m, deterministic) +} +func (m *SimulateOrderRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_SimulateOrderRequest.Merge(m, src) +} +func (m *SimulateOrderRequest) XXX_Size() int { + return xxx_messageInfo_SimulateOrderRequest.Size(m) +} +func (m *SimulateOrderRequest) XXX_DiscardUnknown() { + xxx_messageInfo_SimulateOrderRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_SimulateOrderRequest proto.InternalMessageInfo + +func (m *SimulateOrderRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *SimulateOrderRequest) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *SimulateOrderRequest) GetAmount() float64 { + if m != nil { + return m.Amount + } + return 0 +} + +func (m *SimulateOrderRequest) GetSide() string { + if m != nil { + return m.Side + } + return "" +} + +type SimulateOrderResponse struct { + Orders []*OrderbookItem `protobuf:"bytes,1,rep,name=orders,proto3" json:"orders,omitempty"` + Amount float64 `protobuf:"fixed64,2,opt,name=amount,proto3" json:"amount,omitempty"` + MinimumPrice float64 `protobuf:"fixed64,3,opt,name=minimum_price,json=minimumPrice,proto3" json:"minimum_price,omitempty"` + MaximumPrice float64 `protobuf:"fixed64,4,opt,name=maximum_price,json=maximumPrice,proto3" json:"maximum_price,omitempty"` + PercentageGainLoss float64 `protobuf:"fixed64,5,opt,name=percentage_gain_loss,json=percentageGainLoss,proto3" json:"percentage_gain_loss,omitempty"` + Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SimulateOrderResponse) Reset() { *m = SimulateOrderResponse{} } +func (m *SimulateOrderResponse) String() string { return proto.CompactTextString(m) } +func (*SimulateOrderResponse) ProtoMessage() {} +func (*SimulateOrderResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{67} +} + +func (m *SimulateOrderResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SimulateOrderResponse.Unmarshal(m, b) +} +func (m *SimulateOrderResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SimulateOrderResponse.Marshal(b, m, deterministic) +} +func (m *SimulateOrderResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_SimulateOrderResponse.Merge(m, src) +} +func (m *SimulateOrderResponse) XXX_Size() int { + return xxx_messageInfo_SimulateOrderResponse.Size(m) +} +func (m *SimulateOrderResponse) XXX_DiscardUnknown() { + xxx_messageInfo_SimulateOrderResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_SimulateOrderResponse proto.InternalMessageInfo + +func (m *SimulateOrderResponse) GetOrders() []*OrderbookItem { + if m != nil { + return m.Orders + } + return nil +} + +func (m *SimulateOrderResponse) GetAmount() float64 { + if m != nil { + return m.Amount + } + return 0 +} + +func (m *SimulateOrderResponse) GetMinimumPrice() float64 { + if m != nil { + return m.MinimumPrice + } + return 0 +} + +func (m *SimulateOrderResponse) GetMaximumPrice() float64 { + if m != nil { + return m.MaximumPrice + } + return 0 +} + +func (m *SimulateOrderResponse) GetPercentageGainLoss() float64 { + if m != nil { + return m.PercentageGainLoss + } + return 0 +} + +func (m *SimulateOrderResponse) GetStatus() string { + if m != nil { + return m.Status + } + return "" +} + +type WhaleBombRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,2,opt,name=pair,proto3" json:"pair,omitempty"` + PriceTarget float64 `protobuf:"fixed64,3,opt,name=price_target,json=priceTarget,proto3" json:"price_target,omitempty"` + Side string `protobuf:"bytes,4,opt,name=side,proto3" json:"side,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *WhaleBombRequest) Reset() { *m = WhaleBombRequest{} } +func (m *WhaleBombRequest) String() string { return proto.CompactTextString(m) } +func (*WhaleBombRequest) ProtoMessage() {} +func (*WhaleBombRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{68} +} + +func (m *WhaleBombRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_WhaleBombRequest.Unmarshal(m, b) +} +func (m *WhaleBombRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_WhaleBombRequest.Marshal(b, m, deterministic) +} +func (m *WhaleBombRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_WhaleBombRequest.Merge(m, src) +} +func (m *WhaleBombRequest) XXX_Size() int { + return xxx_messageInfo_WhaleBombRequest.Size(m) +} +func (m *WhaleBombRequest) XXX_DiscardUnknown() { + xxx_messageInfo_WhaleBombRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_WhaleBombRequest proto.InternalMessageInfo + +func (m *WhaleBombRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *WhaleBombRequest) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *WhaleBombRequest) GetPriceTarget() float64 { + if m != nil { + return m.PriceTarget + } + return 0 +} + +func (m *WhaleBombRequest) GetSide() string { + if m != nil { + return m.Side + } + return "" +} + +type CancelOrderRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + AccountId string `protobuf:"bytes,2,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` + OrderId string `protobuf:"bytes,3,opt,name=order_id,json=orderId,proto3" json:"order_id,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,4,opt,name=pair,proto3" json:"pair,omitempty"` + AssetType string `protobuf:"bytes,5,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"` + WalletAddress string `protobuf:"bytes,6,opt,name=wallet_address,json=walletAddress,proto3" json:"wallet_address,omitempty"` + Side string `protobuf:"bytes,7,opt,name=side,proto3" json:"side,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CancelOrderRequest) Reset() { *m = CancelOrderRequest{} } +func (m *CancelOrderRequest) String() string { return proto.CompactTextString(m) } +func (*CancelOrderRequest) ProtoMessage() {} +func (*CancelOrderRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{69} +} + +func (m *CancelOrderRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CancelOrderRequest.Unmarshal(m, b) +} +func (m *CancelOrderRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CancelOrderRequest.Marshal(b, m, deterministic) +} +func (m *CancelOrderRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_CancelOrderRequest.Merge(m, src) +} +func (m *CancelOrderRequest) XXX_Size() int { + return xxx_messageInfo_CancelOrderRequest.Size(m) +} +func (m *CancelOrderRequest) XXX_DiscardUnknown() { + xxx_messageInfo_CancelOrderRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_CancelOrderRequest proto.InternalMessageInfo + +func (m *CancelOrderRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *CancelOrderRequest) GetAccountId() string { + if m != nil { + return m.AccountId + } + return "" +} + +func (m *CancelOrderRequest) GetOrderId() string { + if m != nil { + return m.OrderId + } + return "" +} + +func (m *CancelOrderRequest) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *CancelOrderRequest) GetAssetType() string { + if m != nil { + return m.AssetType + } + return "" +} + +func (m *CancelOrderRequest) GetWalletAddress() string { + if m != nil { + return m.WalletAddress + } + return "" +} + +func (m *CancelOrderRequest) GetSide() string { + if m != nil { + return m.Side + } + return "" +} + +type CancelOrderResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CancelOrderResponse) Reset() { *m = CancelOrderResponse{} } +func (m *CancelOrderResponse) String() string { return proto.CompactTextString(m) } +func (*CancelOrderResponse) ProtoMessage() {} +func (*CancelOrderResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{70} +} + +func (m *CancelOrderResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CancelOrderResponse.Unmarshal(m, b) +} +func (m *CancelOrderResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CancelOrderResponse.Marshal(b, m, deterministic) +} +func (m *CancelOrderResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_CancelOrderResponse.Merge(m, src) +} +func (m *CancelOrderResponse) XXX_Size() int { + return xxx_messageInfo_CancelOrderResponse.Size(m) +} +func (m *CancelOrderResponse) XXX_DiscardUnknown() { + xxx_messageInfo_CancelOrderResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_CancelOrderResponse proto.InternalMessageInfo + +type CancelAllOrdersRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CancelAllOrdersRequest) Reset() { *m = CancelAllOrdersRequest{} } +func (m *CancelAllOrdersRequest) String() string { return proto.CompactTextString(m) } +func (*CancelAllOrdersRequest) ProtoMessage() {} +func (*CancelAllOrdersRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{71} +} + +func (m *CancelAllOrdersRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CancelAllOrdersRequest.Unmarshal(m, b) +} +func (m *CancelAllOrdersRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CancelAllOrdersRequest.Marshal(b, m, deterministic) +} +func (m *CancelAllOrdersRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_CancelAllOrdersRequest.Merge(m, src) +} +func (m *CancelAllOrdersRequest) XXX_Size() int { + return xxx_messageInfo_CancelAllOrdersRequest.Size(m) +} +func (m *CancelAllOrdersRequest) XXX_DiscardUnknown() { + xxx_messageInfo_CancelAllOrdersRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_CancelAllOrdersRequest proto.InternalMessageInfo + +func (m *CancelAllOrdersRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +type CancelAllOrdersResponse struct { + Orders []*CancelAllOrdersResponse_Orders `protobuf:"bytes,1,rep,name=orders,proto3" json:"orders,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CancelAllOrdersResponse) Reset() { *m = CancelAllOrdersResponse{} } +func (m *CancelAllOrdersResponse) String() string { return proto.CompactTextString(m) } +func (*CancelAllOrdersResponse) ProtoMessage() {} +func (*CancelAllOrdersResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{72} +} + +func (m *CancelAllOrdersResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CancelAllOrdersResponse.Unmarshal(m, b) +} +func (m *CancelAllOrdersResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CancelAllOrdersResponse.Marshal(b, m, deterministic) +} +func (m *CancelAllOrdersResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_CancelAllOrdersResponse.Merge(m, src) +} +func (m *CancelAllOrdersResponse) XXX_Size() int { + return xxx_messageInfo_CancelAllOrdersResponse.Size(m) +} +func (m *CancelAllOrdersResponse) XXX_DiscardUnknown() { + xxx_messageInfo_CancelAllOrdersResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_CancelAllOrdersResponse proto.InternalMessageInfo + +func (m *CancelAllOrdersResponse) GetOrders() []*CancelAllOrdersResponse_Orders { + if m != nil { + return m.Orders + } + return nil +} + +type CancelAllOrdersResponse_Orders struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + OrderStatus map[string]string `protobuf:"bytes,2,rep,name=order_status,json=orderStatus,proto3" json:"order_status,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CancelAllOrdersResponse_Orders) Reset() { *m = CancelAllOrdersResponse_Orders{} } +func (m *CancelAllOrdersResponse_Orders) String() string { return proto.CompactTextString(m) } +func (*CancelAllOrdersResponse_Orders) ProtoMessage() {} +func (*CancelAllOrdersResponse_Orders) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{72, 0} +} + +func (m *CancelAllOrdersResponse_Orders) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CancelAllOrdersResponse_Orders.Unmarshal(m, b) +} +func (m *CancelAllOrdersResponse_Orders) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CancelAllOrdersResponse_Orders.Marshal(b, m, deterministic) +} +func (m *CancelAllOrdersResponse_Orders) XXX_Merge(src proto.Message) { + xxx_messageInfo_CancelAllOrdersResponse_Orders.Merge(m, src) +} +func (m *CancelAllOrdersResponse_Orders) XXX_Size() int { + return xxx_messageInfo_CancelAllOrdersResponse_Orders.Size(m) +} +func (m *CancelAllOrdersResponse_Orders) XXX_DiscardUnknown() { + xxx_messageInfo_CancelAllOrdersResponse_Orders.DiscardUnknown(m) +} + +var xxx_messageInfo_CancelAllOrdersResponse_Orders proto.InternalMessageInfo + +func (m *CancelAllOrdersResponse_Orders) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *CancelAllOrdersResponse_Orders) GetOrderStatus() map[string]string { + if m != nil { + return m.OrderStatus + } + return nil +} + +type GetEventsRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetEventsRequest) Reset() { *m = GetEventsRequest{} } +func (m *GetEventsRequest) String() string { return proto.CompactTextString(m) } +func (*GetEventsRequest) ProtoMessage() {} +func (*GetEventsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{73} +} + +func (m *GetEventsRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetEventsRequest.Unmarshal(m, b) +} +func (m *GetEventsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetEventsRequest.Marshal(b, m, deterministic) +} +func (m *GetEventsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetEventsRequest.Merge(m, src) +} +func (m *GetEventsRequest) XXX_Size() int { + return xxx_messageInfo_GetEventsRequest.Size(m) +} +func (m *GetEventsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetEventsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetEventsRequest proto.InternalMessageInfo + +type ConditionParams struct { + Condition string `protobuf:"bytes,1,opt,name=condition,proto3" json:"condition,omitempty"` + Price float64 `protobuf:"fixed64,2,opt,name=price,proto3" json:"price,omitempty"` + CheckBids bool `protobuf:"varint,3,opt,name=check_bids,json=checkBids,proto3" json:"check_bids,omitempty"` + CheckBidsAndAsks bool `protobuf:"varint,4,opt,name=check_bids_and_asks,json=checkBidsAndAsks,proto3" json:"check_bids_and_asks,omitempty"` + OrderbookAmount float64 `protobuf:"fixed64,5,opt,name=orderbook_amount,json=orderbookAmount,proto3" json:"orderbook_amount,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ConditionParams) Reset() { *m = ConditionParams{} } +func (m *ConditionParams) String() string { return proto.CompactTextString(m) } +func (*ConditionParams) ProtoMessage() {} +func (*ConditionParams) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{74} +} + +func (m *ConditionParams) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ConditionParams.Unmarshal(m, b) +} +func (m *ConditionParams) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ConditionParams.Marshal(b, m, deterministic) +} +func (m *ConditionParams) XXX_Merge(src proto.Message) { + xxx_messageInfo_ConditionParams.Merge(m, src) +} +func (m *ConditionParams) XXX_Size() int { + return xxx_messageInfo_ConditionParams.Size(m) +} +func (m *ConditionParams) XXX_DiscardUnknown() { + xxx_messageInfo_ConditionParams.DiscardUnknown(m) +} + +var xxx_messageInfo_ConditionParams proto.InternalMessageInfo + +func (m *ConditionParams) GetCondition() string { + if m != nil { + return m.Condition + } + return "" +} + +func (m *ConditionParams) GetPrice() float64 { + if m != nil { + return m.Price + } + return 0 +} + +func (m *ConditionParams) GetCheckBids() bool { + if m != nil { + return m.CheckBids + } + return false +} + +func (m *ConditionParams) GetCheckBidsAndAsks() bool { + if m != nil { + return m.CheckBidsAndAsks + } + return false +} + +func (m *ConditionParams) GetOrderbookAmount() float64 { + if m != nil { + return m.OrderbookAmount + } + return 0 +} + +type GetEventsResponse struct { + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Exchange string `protobuf:"bytes,2,opt,name=exchange,proto3" json:"exchange,omitempty"` + Item string `protobuf:"bytes,3,opt,name=item,proto3" json:"item,omitempty"` + ConditionParams *ConditionParams `protobuf:"bytes,4,opt,name=condition_params,json=conditionParams,proto3" json:"condition_params,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,5,opt,name=pair,proto3" json:"pair,omitempty"` + Action string `protobuf:"bytes,6,opt,name=action,proto3" json:"action,omitempty"` + Executed bool `protobuf:"varint,7,opt,name=executed,proto3" json:"executed,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetEventsResponse) Reset() { *m = GetEventsResponse{} } +func (m *GetEventsResponse) String() string { return proto.CompactTextString(m) } +func (*GetEventsResponse) ProtoMessage() {} +func (*GetEventsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{75} +} + +func (m *GetEventsResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetEventsResponse.Unmarshal(m, b) +} +func (m *GetEventsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetEventsResponse.Marshal(b, m, deterministic) +} +func (m *GetEventsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetEventsResponse.Merge(m, src) +} +func (m *GetEventsResponse) XXX_Size() int { + return xxx_messageInfo_GetEventsResponse.Size(m) +} +func (m *GetEventsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetEventsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetEventsResponse proto.InternalMessageInfo + +func (m *GetEventsResponse) GetId() int64 { + if m != nil { + return m.Id + } + return 0 +} + +func (m *GetEventsResponse) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *GetEventsResponse) GetItem() string { + if m != nil { + return m.Item + } + return "" +} + +func (m *GetEventsResponse) GetConditionParams() *ConditionParams { + if m != nil { + return m.ConditionParams + } + return nil +} + +func (m *GetEventsResponse) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *GetEventsResponse) GetAction() string { + if m != nil { + return m.Action + } + return "" +} + +func (m *GetEventsResponse) GetExecuted() bool { + if m != nil { + return m.Executed + } + return false +} + +type AddEventRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Item string `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` + ConditionParams *ConditionParams `protobuf:"bytes,3,opt,name=condition_params,json=conditionParams,proto3" json:"condition_params,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,4,opt,name=pair,proto3" json:"pair,omitempty"` + AssetType string `protobuf:"bytes,5,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"` + Action string `protobuf:"bytes,6,opt,name=action,proto3" json:"action,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AddEventRequest) Reset() { *m = AddEventRequest{} } +func (m *AddEventRequest) String() string { return proto.CompactTextString(m) } +func (*AddEventRequest) ProtoMessage() {} +func (*AddEventRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{76} +} + +func (m *AddEventRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AddEventRequest.Unmarshal(m, b) +} +func (m *AddEventRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AddEventRequest.Marshal(b, m, deterministic) +} +func (m *AddEventRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_AddEventRequest.Merge(m, src) +} +func (m *AddEventRequest) XXX_Size() int { + return xxx_messageInfo_AddEventRequest.Size(m) +} +func (m *AddEventRequest) XXX_DiscardUnknown() { + xxx_messageInfo_AddEventRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_AddEventRequest proto.InternalMessageInfo + +func (m *AddEventRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *AddEventRequest) GetItem() string { + if m != nil { + return m.Item + } + return "" +} + +func (m *AddEventRequest) GetConditionParams() *ConditionParams { + if m != nil { + return m.ConditionParams + } + return nil +} + +func (m *AddEventRequest) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *AddEventRequest) GetAssetType() string { + if m != nil { + return m.AssetType + } + return "" +} + +func (m *AddEventRequest) GetAction() string { + if m != nil { + return m.Action + } + return "" +} + +type AddEventResponse struct { + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AddEventResponse) Reset() { *m = AddEventResponse{} } +func (m *AddEventResponse) String() string { return proto.CompactTextString(m) } +func (*AddEventResponse) ProtoMessage() {} +func (*AddEventResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{77} +} + +func (m *AddEventResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AddEventResponse.Unmarshal(m, b) +} +func (m *AddEventResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AddEventResponse.Marshal(b, m, deterministic) +} +func (m *AddEventResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_AddEventResponse.Merge(m, src) +} +func (m *AddEventResponse) XXX_Size() int { + return xxx_messageInfo_AddEventResponse.Size(m) +} +func (m *AddEventResponse) XXX_DiscardUnknown() { + xxx_messageInfo_AddEventResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_AddEventResponse proto.InternalMessageInfo + +func (m *AddEventResponse) GetId() int64 { + if m != nil { + return m.Id + } + return 0 +} + +type RemoveEventRequest struct { + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RemoveEventRequest) Reset() { *m = RemoveEventRequest{} } +func (m *RemoveEventRequest) String() string { return proto.CompactTextString(m) } +func (*RemoveEventRequest) ProtoMessage() {} +func (*RemoveEventRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{78} +} + +func (m *RemoveEventRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RemoveEventRequest.Unmarshal(m, b) +} +func (m *RemoveEventRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RemoveEventRequest.Marshal(b, m, deterministic) +} +func (m *RemoveEventRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_RemoveEventRequest.Merge(m, src) +} +func (m *RemoveEventRequest) XXX_Size() int { + return xxx_messageInfo_RemoveEventRequest.Size(m) +} +func (m *RemoveEventRequest) XXX_DiscardUnknown() { + xxx_messageInfo_RemoveEventRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_RemoveEventRequest proto.InternalMessageInfo + +func (m *RemoveEventRequest) GetId() int64 { + if m != nil { + return m.Id + } + return 0 +} + +type RemoveEventResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RemoveEventResponse) Reset() { *m = RemoveEventResponse{} } +func (m *RemoveEventResponse) String() string { return proto.CompactTextString(m) } +func (*RemoveEventResponse) ProtoMessage() {} +func (*RemoveEventResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{79} +} + +func (m *RemoveEventResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RemoveEventResponse.Unmarshal(m, b) +} +func (m *RemoveEventResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RemoveEventResponse.Marshal(b, m, deterministic) +} +func (m *RemoveEventResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_RemoveEventResponse.Merge(m, src) +} +func (m *RemoveEventResponse) XXX_Size() int { + return xxx_messageInfo_RemoveEventResponse.Size(m) +} +func (m *RemoveEventResponse) XXX_DiscardUnknown() { + xxx_messageInfo_RemoveEventResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_RemoveEventResponse proto.InternalMessageInfo + +type GetCryptocurrencyDepositAddressesRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetCryptocurrencyDepositAddressesRequest) Reset() { + *m = GetCryptocurrencyDepositAddressesRequest{} +} +func (m *GetCryptocurrencyDepositAddressesRequest) String() string { return proto.CompactTextString(m) } +func (*GetCryptocurrencyDepositAddressesRequest) ProtoMessage() {} +func (*GetCryptocurrencyDepositAddressesRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{80} +} + +func (m *GetCryptocurrencyDepositAddressesRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetCryptocurrencyDepositAddressesRequest.Unmarshal(m, b) +} +func (m *GetCryptocurrencyDepositAddressesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetCryptocurrencyDepositAddressesRequest.Marshal(b, m, deterministic) +} +func (m *GetCryptocurrencyDepositAddressesRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetCryptocurrencyDepositAddressesRequest.Merge(m, src) +} +func (m *GetCryptocurrencyDepositAddressesRequest) XXX_Size() int { + return xxx_messageInfo_GetCryptocurrencyDepositAddressesRequest.Size(m) +} +func (m *GetCryptocurrencyDepositAddressesRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetCryptocurrencyDepositAddressesRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetCryptocurrencyDepositAddressesRequest proto.InternalMessageInfo + +func (m *GetCryptocurrencyDepositAddressesRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +type GetCryptocurrencyDepositAddressesResponse struct { + Addresses map[string]string `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetCryptocurrencyDepositAddressesResponse) Reset() { + *m = GetCryptocurrencyDepositAddressesResponse{} +} +func (m *GetCryptocurrencyDepositAddressesResponse) String() string { return proto.CompactTextString(m) } +func (*GetCryptocurrencyDepositAddressesResponse) ProtoMessage() {} +func (*GetCryptocurrencyDepositAddressesResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{81} +} + +func (m *GetCryptocurrencyDepositAddressesResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetCryptocurrencyDepositAddressesResponse.Unmarshal(m, b) +} +func (m *GetCryptocurrencyDepositAddressesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetCryptocurrencyDepositAddressesResponse.Marshal(b, m, deterministic) +} +func (m *GetCryptocurrencyDepositAddressesResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetCryptocurrencyDepositAddressesResponse.Merge(m, src) +} +func (m *GetCryptocurrencyDepositAddressesResponse) XXX_Size() int { + return xxx_messageInfo_GetCryptocurrencyDepositAddressesResponse.Size(m) +} +func (m *GetCryptocurrencyDepositAddressesResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetCryptocurrencyDepositAddressesResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetCryptocurrencyDepositAddressesResponse proto.InternalMessageInfo + +func (m *GetCryptocurrencyDepositAddressesResponse) GetAddresses() map[string]string { + if m != nil { + return m.Addresses + } + return nil +} + +type GetCryptocurrencyDepositAddressRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Cryptocurrency string `protobuf:"bytes,2,opt,name=cryptocurrency,proto3" json:"cryptocurrency,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetCryptocurrencyDepositAddressRequest) Reset() { + *m = GetCryptocurrencyDepositAddressRequest{} +} +func (m *GetCryptocurrencyDepositAddressRequest) String() string { return proto.CompactTextString(m) } +func (*GetCryptocurrencyDepositAddressRequest) ProtoMessage() {} +func (*GetCryptocurrencyDepositAddressRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{82} +} + +func (m *GetCryptocurrencyDepositAddressRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetCryptocurrencyDepositAddressRequest.Unmarshal(m, b) +} +func (m *GetCryptocurrencyDepositAddressRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetCryptocurrencyDepositAddressRequest.Marshal(b, m, deterministic) +} +func (m *GetCryptocurrencyDepositAddressRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetCryptocurrencyDepositAddressRequest.Merge(m, src) +} +func (m *GetCryptocurrencyDepositAddressRequest) XXX_Size() int { + return xxx_messageInfo_GetCryptocurrencyDepositAddressRequest.Size(m) +} +func (m *GetCryptocurrencyDepositAddressRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetCryptocurrencyDepositAddressRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetCryptocurrencyDepositAddressRequest proto.InternalMessageInfo + +func (m *GetCryptocurrencyDepositAddressRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *GetCryptocurrencyDepositAddressRequest) GetCryptocurrency() string { + if m != nil { + return m.Cryptocurrency + } + return "" +} + +type GetCryptocurrencyDepositAddressResponse struct { + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetCryptocurrencyDepositAddressResponse) Reset() { + *m = GetCryptocurrencyDepositAddressResponse{} +} +func (m *GetCryptocurrencyDepositAddressResponse) String() string { return proto.CompactTextString(m) } +func (*GetCryptocurrencyDepositAddressResponse) ProtoMessage() {} +func (*GetCryptocurrencyDepositAddressResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{83} +} + +func (m *GetCryptocurrencyDepositAddressResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetCryptocurrencyDepositAddressResponse.Unmarshal(m, b) +} +func (m *GetCryptocurrencyDepositAddressResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetCryptocurrencyDepositAddressResponse.Marshal(b, m, deterministic) +} +func (m *GetCryptocurrencyDepositAddressResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetCryptocurrencyDepositAddressResponse.Merge(m, src) +} +func (m *GetCryptocurrencyDepositAddressResponse) XXX_Size() int { + return xxx_messageInfo_GetCryptocurrencyDepositAddressResponse.Size(m) +} +func (m *GetCryptocurrencyDepositAddressResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetCryptocurrencyDepositAddressResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetCryptocurrencyDepositAddressResponse proto.InternalMessageInfo + +func (m *GetCryptocurrencyDepositAddressResponse) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +type WithdrawCurrencyRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + OneTimePassword string `protobuf:"bytes,3,opt,name=one_time_password,json=oneTimePassword,proto3" json:"one_time_password,omitempty"` + AccountId string `protobuf:"bytes,4,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` + Pin int64 `protobuf:"varint,5,opt,name=pin,proto3" json:"pin,omitempty"` + TradePassword string `protobuf:"bytes,6,opt,name=trade_password,json=tradePassword,proto3" json:"trade_password,omitempty"` + Currency string `protobuf:"bytes,7,opt,name=currency,proto3" json:"currency,omitempty"` + Address string `protobuf:"bytes,8,opt,name=address,proto3" json:"address,omitempty"` + AddressTag string `protobuf:"bytes,9,opt,name=address_tag,json=addressTag,proto3" json:"address_tag,omitempty"` + Amount float64 `protobuf:"fixed64,10,opt,name=amount,proto3" json:"amount,omitempty"` + FeeAmount float64 `protobuf:"fixed64,11,opt,name=fee_amount,json=feeAmount,proto3" json:"fee_amount,omitempty"` + BankName string `protobuf:"bytes,12,opt,name=bank_name,json=bankName,proto3" json:"bank_name,omitempty"` + BankAddress string `protobuf:"bytes,13,opt,name=bank_address,json=bankAddress,proto3" json:"bank_address,omitempty"` + BankCity string `protobuf:"bytes,14,opt,name=bank_city,json=bankCity,proto3" json:"bank_city,omitempty"` + BankCountry string `protobuf:"bytes,15,opt,name=bank_country,json=bankCountry,proto3" json:"bank_country,omitempty"` + SwifeCode string `protobuf:"bytes,16,opt,name=swife_code,json=swifeCode,proto3" json:"swife_code,omitempty"` + WireCurrency string `protobuf:"bytes,17,opt,name=wire_currency,json=wireCurrency,proto3" json:"wire_currency,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *WithdrawCurrencyRequest) Reset() { *m = WithdrawCurrencyRequest{} } +func (m *WithdrawCurrencyRequest) String() string { return proto.CompactTextString(m) } +func (*WithdrawCurrencyRequest) ProtoMessage() {} +func (*WithdrawCurrencyRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{84} +} + +func (m *WithdrawCurrencyRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_WithdrawCurrencyRequest.Unmarshal(m, b) +} +func (m *WithdrawCurrencyRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_WithdrawCurrencyRequest.Marshal(b, m, deterministic) +} +func (m *WithdrawCurrencyRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_WithdrawCurrencyRequest.Merge(m, src) +} +func (m *WithdrawCurrencyRequest) XXX_Size() int { + return xxx_messageInfo_WithdrawCurrencyRequest.Size(m) +} +func (m *WithdrawCurrencyRequest) XXX_DiscardUnknown() { + xxx_messageInfo_WithdrawCurrencyRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_WithdrawCurrencyRequest proto.InternalMessageInfo + +func (m *WithdrawCurrencyRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetDescription() string { + if m != nil { + return m.Description + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetOneTimePassword() string { + if m != nil { + return m.OneTimePassword + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetAccountId() string { + if m != nil { + return m.AccountId + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetPin() int64 { + if m != nil { + return m.Pin + } + return 0 +} + +func (m *WithdrawCurrencyRequest) GetTradePassword() string { + if m != nil { + return m.TradePassword + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetCurrency() string { + if m != nil { + return m.Currency + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetAddressTag() string { + if m != nil { + return m.AddressTag + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetAmount() float64 { + if m != nil { + return m.Amount + } + return 0 +} + +func (m *WithdrawCurrencyRequest) GetFeeAmount() float64 { + if m != nil { + return m.FeeAmount + } + return 0 +} + +func (m *WithdrawCurrencyRequest) GetBankName() string { + if m != nil { + return m.BankName + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetBankAddress() string { + if m != nil { + return m.BankAddress + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetBankCity() string { + if m != nil { + return m.BankCity + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetBankCountry() string { + if m != nil { + return m.BankCountry + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetSwifeCode() string { + if m != nil { + return m.SwifeCode + } + return "" +} + +func (m *WithdrawCurrencyRequest) GetWireCurrency() string { + if m != nil { + return m.WireCurrency + } + return "" +} + +type WithdrawResponse struct { + Result string `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *WithdrawResponse) Reset() { *m = WithdrawResponse{} } +func (m *WithdrawResponse) String() string { return proto.CompactTextString(m) } +func (*WithdrawResponse) ProtoMessage() {} +func (*WithdrawResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{85} +} + +func (m *WithdrawResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_WithdrawResponse.Unmarshal(m, b) +} +func (m *WithdrawResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_WithdrawResponse.Marshal(b, m, deterministic) +} +func (m *WithdrawResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_WithdrawResponse.Merge(m, src) +} +func (m *WithdrawResponse) XXX_Size() int { + return xxx_messageInfo_WithdrawResponse.Size(m) +} +func (m *WithdrawResponse) XXX_DiscardUnknown() { + xxx_messageInfo_WithdrawResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_WithdrawResponse proto.InternalMessageInfo + +func (m *WithdrawResponse) GetResult() string { + if m != nil { + return m.Result + } + return "" +} + +type GetLoggerDetailsRequest struct { + Logger string `protobuf:"bytes,1,opt,name=logger,proto3" json:"logger,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetLoggerDetailsRequest) Reset() { *m = GetLoggerDetailsRequest{} } +func (m *GetLoggerDetailsRequest) String() string { return proto.CompactTextString(m) } +func (*GetLoggerDetailsRequest) ProtoMessage() {} +func (*GetLoggerDetailsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{86} +} + +func (m *GetLoggerDetailsRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetLoggerDetailsRequest.Unmarshal(m, b) +} +func (m *GetLoggerDetailsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetLoggerDetailsRequest.Marshal(b, m, deterministic) +} +func (m *GetLoggerDetailsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetLoggerDetailsRequest.Merge(m, src) +} +func (m *GetLoggerDetailsRequest) XXX_Size() int { + return xxx_messageInfo_GetLoggerDetailsRequest.Size(m) +} +func (m *GetLoggerDetailsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetLoggerDetailsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetLoggerDetailsRequest proto.InternalMessageInfo + +func (m *GetLoggerDetailsRequest) GetLogger() string { + if m != nil { + return m.Logger + } + return "" +} + +type GetLoggerDetailsResponse struct { + Info bool `protobuf:"varint,1,opt,name=info,proto3" json:"info,omitempty"` + Debug bool `protobuf:"varint,2,opt,name=debug,proto3" json:"debug,omitempty"` + Warn bool `protobuf:"varint,3,opt,name=warn,proto3" json:"warn,omitempty"` + Error bool `protobuf:"varint,4,opt,name=error,proto3" json:"error,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetLoggerDetailsResponse) Reset() { *m = GetLoggerDetailsResponse{} } +func (m *GetLoggerDetailsResponse) String() string { return proto.CompactTextString(m) } +func (*GetLoggerDetailsResponse) ProtoMessage() {} +func (*GetLoggerDetailsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{87} +} + +func (m *GetLoggerDetailsResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetLoggerDetailsResponse.Unmarshal(m, b) +} +func (m *GetLoggerDetailsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetLoggerDetailsResponse.Marshal(b, m, deterministic) +} +func (m *GetLoggerDetailsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetLoggerDetailsResponse.Merge(m, src) +} +func (m *GetLoggerDetailsResponse) XXX_Size() int { + return xxx_messageInfo_GetLoggerDetailsResponse.Size(m) +} +func (m *GetLoggerDetailsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetLoggerDetailsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetLoggerDetailsResponse proto.InternalMessageInfo + +func (m *GetLoggerDetailsResponse) GetInfo() bool { + if m != nil { + return m.Info + } + return false +} + +func (m *GetLoggerDetailsResponse) GetDebug() bool { + if m != nil { + return m.Debug + } + return false +} + +func (m *GetLoggerDetailsResponse) GetWarn() bool { + if m != nil { + return m.Warn + } + return false +} + +func (m *GetLoggerDetailsResponse) GetError() bool { + if m != nil { + return m.Error + } + return false +} + +type SetLoggerDetailsRequest struct { + Logger string `protobuf:"bytes,1,opt,name=logger,proto3" json:"logger,omitempty"` + Level string `protobuf:"bytes,2,opt,name=level,proto3" json:"level,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SetLoggerDetailsRequest) Reset() { *m = SetLoggerDetailsRequest{} } +func (m *SetLoggerDetailsRequest) String() string { return proto.CompactTextString(m) } +func (*SetLoggerDetailsRequest) ProtoMessage() {} +func (*SetLoggerDetailsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{88} +} + +func (m *SetLoggerDetailsRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SetLoggerDetailsRequest.Unmarshal(m, b) +} +func (m *SetLoggerDetailsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SetLoggerDetailsRequest.Marshal(b, m, deterministic) +} +func (m *SetLoggerDetailsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_SetLoggerDetailsRequest.Merge(m, src) +} +func (m *SetLoggerDetailsRequest) XXX_Size() int { + return xxx_messageInfo_SetLoggerDetailsRequest.Size(m) +} +func (m *SetLoggerDetailsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_SetLoggerDetailsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_SetLoggerDetailsRequest proto.InternalMessageInfo + +func (m *SetLoggerDetailsRequest) GetLogger() string { + if m != nil { + return m.Logger + } + return "" +} + +func (m *SetLoggerDetailsRequest) GetLevel() string { + if m != nil { + return m.Level + } + return "" +} + +type GetExchangePairsRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Asset string `protobuf:"bytes,2,opt,name=asset,proto3" json:"asset,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetExchangePairsRequest) Reset() { *m = GetExchangePairsRequest{} } +func (m *GetExchangePairsRequest) String() string { return proto.CompactTextString(m) } +func (*GetExchangePairsRequest) ProtoMessage() {} +func (*GetExchangePairsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{89} +} + +func (m *GetExchangePairsRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetExchangePairsRequest.Unmarshal(m, b) +} +func (m *GetExchangePairsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetExchangePairsRequest.Marshal(b, m, deterministic) +} +func (m *GetExchangePairsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetExchangePairsRequest.Merge(m, src) +} +func (m *GetExchangePairsRequest) XXX_Size() int { + return xxx_messageInfo_GetExchangePairsRequest.Size(m) +} +func (m *GetExchangePairsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetExchangePairsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetExchangePairsRequest proto.InternalMessageInfo + +func (m *GetExchangePairsRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *GetExchangePairsRequest) GetAsset() string { + if m != nil { + return m.Asset + } + return "" +} + +type GetExchangePairsResponse struct { + SupportedAssets map[string]*PairsSupported `protobuf:"bytes,1,rep,name=supported_assets,json=supportedAssets,proto3" json:"supported_assets,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetExchangePairsResponse) Reset() { *m = GetExchangePairsResponse{} } +func (m *GetExchangePairsResponse) String() string { return proto.CompactTextString(m) } +func (*GetExchangePairsResponse) ProtoMessage() {} +func (*GetExchangePairsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{90} +} + +func (m *GetExchangePairsResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetExchangePairsResponse.Unmarshal(m, b) +} +func (m *GetExchangePairsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetExchangePairsResponse.Marshal(b, m, deterministic) +} +func (m *GetExchangePairsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetExchangePairsResponse.Merge(m, src) +} +func (m *GetExchangePairsResponse) XXX_Size() int { + return xxx_messageInfo_GetExchangePairsResponse.Size(m) +} +func (m *GetExchangePairsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetExchangePairsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetExchangePairsResponse proto.InternalMessageInfo + +func (m *GetExchangePairsResponse) GetSupportedAssets() map[string]*PairsSupported { + if m != nil { + return m.SupportedAssets + } + return nil +} + +type ExchangePairRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + AssetType string `protobuf:"bytes,2,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,3,opt,name=pair,proto3" json:"pair,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ExchangePairRequest) Reset() { *m = ExchangePairRequest{} } +func (m *ExchangePairRequest) String() string { return proto.CompactTextString(m) } +func (*ExchangePairRequest) ProtoMessage() {} +func (*ExchangePairRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{91} +} + +func (m *ExchangePairRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ExchangePairRequest.Unmarshal(m, b) +} +func (m *ExchangePairRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ExchangePairRequest.Marshal(b, m, deterministic) +} +func (m *ExchangePairRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ExchangePairRequest.Merge(m, src) +} +func (m *ExchangePairRequest) XXX_Size() int { + return xxx_messageInfo_ExchangePairRequest.Size(m) +} +func (m *ExchangePairRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ExchangePairRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ExchangePairRequest proto.InternalMessageInfo + +func (m *ExchangePairRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *ExchangePairRequest) GetAssetType() string { + if m != nil { + return m.AssetType + } + return "" +} + +func (m *ExchangePairRequest) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +type GetOrderbookStreamRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,2,opt,name=pair,proto3" json:"pair,omitempty"` + AssetType string `protobuf:"bytes,3,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetOrderbookStreamRequest) Reset() { *m = GetOrderbookStreamRequest{} } +func (m *GetOrderbookStreamRequest) String() string { return proto.CompactTextString(m) } +func (*GetOrderbookStreamRequest) ProtoMessage() {} +func (*GetOrderbookStreamRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{92} +} + +func (m *GetOrderbookStreamRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetOrderbookStreamRequest.Unmarshal(m, b) +} +func (m *GetOrderbookStreamRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetOrderbookStreamRequest.Marshal(b, m, deterministic) +} +func (m *GetOrderbookStreamRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetOrderbookStreamRequest.Merge(m, src) +} +func (m *GetOrderbookStreamRequest) XXX_Size() int { + return xxx_messageInfo_GetOrderbookStreamRequest.Size(m) +} +func (m *GetOrderbookStreamRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetOrderbookStreamRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetOrderbookStreamRequest proto.InternalMessageInfo + +func (m *GetOrderbookStreamRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *GetOrderbookStreamRequest) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *GetOrderbookStreamRequest) GetAssetType() string { + if m != nil { + return m.AssetType + } + return "" +} + +type GetExchangeOrderbookStreamRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetExchangeOrderbookStreamRequest) Reset() { *m = GetExchangeOrderbookStreamRequest{} } +func (m *GetExchangeOrderbookStreamRequest) String() string { return proto.CompactTextString(m) } +func (*GetExchangeOrderbookStreamRequest) ProtoMessage() {} +func (*GetExchangeOrderbookStreamRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{93} +} + +func (m *GetExchangeOrderbookStreamRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetExchangeOrderbookStreamRequest.Unmarshal(m, b) +} +func (m *GetExchangeOrderbookStreamRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetExchangeOrderbookStreamRequest.Marshal(b, m, deterministic) +} +func (m *GetExchangeOrderbookStreamRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetExchangeOrderbookStreamRequest.Merge(m, src) +} +func (m *GetExchangeOrderbookStreamRequest) XXX_Size() int { + return xxx_messageInfo_GetExchangeOrderbookStreamRequest.Size(m) +} +func (m *GetExchangeOrderbookStreamRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetExchangeOrderbookStreamRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetExchangeOrderbookStreamRequest proto.InternalMessageInfo + +func (m *GetExchangeOrderbookStreamRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +type GetTickerStreamRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + Pair *CurrencyPair `protobuf:"bytes,2,opt,name=pair,proto3" json:"pair,omitempty"` + AssetType string `protobuf:"bytes,3,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetTickerStreamRequest) Reset() { *m = GetTickerStreamRequest{} } +func (m *GetTickerStreamRequest) String() string { return proto.CompactTextString(m) } +func (*GetTickerStreamRequest) ProtoMessage() {} +func (*GetTickerStreamRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{94} +} + +func (m *GetTickerStreamRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetTickerStreamRequest.Unmarshal(m, b) +} +func (m *GetTickerStreamRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetTickerStreamRequest.Marshal(b, m, deterministic) +} +func (m *GetTickerStreamRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetTickerStreamRequest.Merge(m, src) +} +func (m *GetTickerStreamRequest) XXX_Size() int { + return xxx_messageInfo_GetTickerStreamRequest.Size(m) +} +func (m *GetTickerStreamRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetTickerStreamRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetTickerStreamRequest proto.InternalMessageInfo + +func (m *GetTickerStreamRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +func (m *GetTickerStreamRequest) GetPair() *CurrencyPair { + if m != nil { + return m.Pair + } + return nil +} + +func (m *GetTickerStreamRequest) GetAssetType() string { + if m != nil { + return m.AssetType + } + return "" +} + +type GetExchangeTickerStreamRequest struct { + Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetExchangeTickerStreamRequest) Reset() { *m = GetExchangeTickerStreamRequest{} } +func (m *GetExchangeTickerStreamRequest) String() string { return proto.CompactTextString(m) } +func (*GetExchangeTickerStreamRequest) ProtoMessage() {} +func (*GetExchangeTickerStreamRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{95} +} + +func (m *GetExchangeTickerStreamRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetExchangeTickerStreamRequest.Unmarshal(m, b) +} +func (m *GetExchangeTickerStreamRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetExchangeTickerStreamRequest.Marshal(b, m, deterministic) +} +func (m *GetExchangeTickerStreamRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetExchangeTickerStreamRequest.Merge(m, src) +} +func (m *GetExchangeTickerStreamRequest) XXX_Size() int { + return xxx_messageInfo_GetExchangeTickerStreamRequest.Size(m) +} +func (m *GetExchangeTickerStreamRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetExchangeTickerStreamRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetExchangeTickerStreamRequest proto.InternalMessageInfo + +func (m *GetExchangeTickerStreamRequest) GetExchange() string { + if m != nil { + return m.Exchange + } + return "" +} + +type GetAuditEventRequest struct { + StartDate string `protobuf:"bytes,1,opt,name=start_date,json=startDate,proto3" json:"start_date,omitempty"` + EndDate string `protobuf:"bytes,2,opt,name=end_date,json=endDate,proto3" json:"end_date,omitempty"` + OrderBy string `protobuf:"bytes,3,opt,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"` + Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` + Offset int32 `protobuf:"varint,5,opt,name=offset,proto3" json:"offset,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetAuditEventRequest) Reset() { *m = GetAuditEventRequest{} } +func (m *GetAuditEventRequest) String() string { return proto.CompactTextString(m) } +func (*GetAuditEventRequest) ProtoMessage() {} +func (*GetAuditEventRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{96} +} + +func (m *GetAuditEventRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetAuditEventRequest.Unmarshal(m, b) +} +func (m *GetAuditEventRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetAuditEventRequest.Marshal(b, m, deterministic) +} +func (m *GetAuditEventRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetAuditEventRequest.Merge(m, src) +} +func (m *GetAuditEventRequest) XXX_Size() int { + return xxx_messageInfo_GetAuditEventRequest.Size(m) +} +func (m *GetAuditEventRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetAuditEventRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetAuditEventRequest proto.InternalMessageInfo + +func (m *GetAuditEventRequest) GetStartDate() string { + if m != nil { + return m.StartDate + } + return "" +} + +func (m *GetAuditEventRequest) GetEndDate() string { + if m != nil { + return m.EndDate + } + return "" +} + +func (m *GetAuditEventRequest) GetOrderBy() string { + if m != nil { + return m.OrderBy + } + return "" +} + +func (m *GetAuditEventRequest) GetLimit() int32 { + if m != nil { + return m.Limit + } + return 0 +} + +func (m *GetAuditEventRequest) GetOffset() int32 { + if m != nil { + return m.Offset + } + return 0 +} + +type GetAuditEventResponse struct { + Events []*AuditEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetAuditEventResponse) Reset() { *m = GetAuditEventResponse{} } +func (m *GetAuditEventResponse) String() string { return proto.CompactTextString(m) } +func (*GetAuditEventResponse) ProtoMessage() {} +func (*GetAuditEventResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{97} +} + +func (m *GetAuditEventResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetAuditEventResponse.Unmarshal(m, b) +} +func (m *GetAuditEventResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetAuditEventResponse.Marshal(b, m, deterministic) +} +func (m *GetAuditEventResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetAuditEventResponse.Merge(m, src) +} +func (m *GetAuditEventResponse) XXX_Size() int { + return xxx_messageInfo_GetAuditEventResponse.Size(m) +} +func (m *GetAuditEventResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetAuditEventResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetAuditEventResponse proto.InternalMessageInfo + +func (m *GetAuditEventResponse) GetEvents() []*AuditEvent { + if m != nil { + return m.Events + } + return nil +} + +type AuditEvent struct { + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Identifier string `protobuf:"bytes,2,opt,name=identifier,proto3" json:"identifier,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + Timestamp string `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AuditEvent) Reset() { *m = AuditEvent{} } +func (m *AuditEvent) String() string { return proto.CompactTextString(m) } +func (*AuditEvent) ProtoMessage() {} +func (*AuditEvent) Descriptor() ([]byte, []int) { + return fileDescriptor_77a6da22d6a3feb1, []int{98} +} + +func (m *AuditEvent) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AuditEvent.Unmarshal(m, b) +} +func (m *AuditEvent) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AuditEvent.Marshal(b, m, deterministic) +} +func (m *AuditEvent) XXX_Merge(src proto.Message) { + xxx_messageInfo_AuditEvent.Merge(m, src) +} +func (m *AuditEvent) XXX_Size() int { + return xxx_messageInfo_AuditEvent.Size(m) +} +func (m *AuditEvent) XXX_DiscardUnknown() { + xxx_messageInfo_AuditEvent.DiscardUnknown(m) +} + +var xxx_messageInfo_AuditEvent proto.InternalMessageInfo + +func (m *AuditEvent) GetType() string { + if m != nil { + return m.Type + } + return "" +} + +func (m *AuditEvent) GetIdentifier() string { + if m != nil { + return m.Identifier + } + return "" +} + +func (m *AuditEvent) GetMessage() string { + if m != nil { + return m.Message + } + return "" +} + +func (m *AuditEvent) GetTimestamp() string { + if m != nil { + return m.Timestamp + } + return "" +} + +func init() { + proto.RegisterType((*GetInfoRequest)(nil), "gctrpc.GetInfoRequest") + proto.RegisterType((*GetInfoResponse)(nil), "gctrpc.GetInfoResponse") + proto.RegisterMapType((map[string]*RPCEndpoint)(nil), "gctrpc.GetInfoResponse.RpcEndpointsEntry") + proto.RegisterMapType((map[string]bool)(nil), "gctrpc.GetInfoResponse.SubsystemStatusEntry") + proto.RegisterType((*GetCommunicationRelayersRequest)(nil), "gctrpc.GetCommunicationRelayersRequest") + proto.RegisterType((*CommunicationRelayer)(nil), "gctrpc.CommunicationRelayer") + proto.RegisterType((*GetCommunicationRelayersResponse)(nil), "gctrpc.GetCommunicationRelayersResponse") + proto.RegisterMapType((map[string]*CommunicationRelayer)(nil), "gctrpc.GetCommunicationRelayersResponse.CommunicationRelayersEntry") + proto.RegisterType((*GenericSubsystemRequest)(nil), "gctrpc.GenericSubsystemRequest") + proto.RegisterType((*GenericSubsystemResponse)(nil), "gctrpc.GenericSubsystemResponse") + proto.RegisterType((*GetSubsystemsRequest)(nil), "gctrpc.GetSubsystemsRequest") + proto.RegisterType((*GetSusbsytemsResponse)(nil), "gctrpc.GetSusbsytemsResponse") + proto.RegisterMapType((map[string]bool)(nil), "gctrpc.GetSusbsytemsResponse.SubsystemsStatusEntry") + proto.RegisterType((*GetRPCEndpointsRequest)(nil), "gctrpc.GetRPCEndpointsRequest") + proto.RegisterType((*RPCEndpoint)(nil), "gctrpc.RPCEndpoint") + proto.RegisterType((*GetRPCEndpointsResponse)(nil), "gctrpc.GetRPCEndpointsResponse") + proto.RegisterMapType((map[string]*RPCEndpoint)(nil), "gctrpc.GetRPCEndpointsResponse.EndpointsEntry") + proto.RegisterType((*GenericExchangeNameRequest)(nil), "gctrpc.GenericExchangeNameRequest") + proto.RegisterType((*GenericExchangeNameResponse)(nil), "gctrpc.GenericExchangeNameResponse") + proto.RegisterType((*GetExchangesRequest)(nil), "gctrpc.GetExchangesRequest") + proto.RegisterType((*GetExchangesResponse)(nil), "gctrpc.GetExchangesResponse") + proto.RegisterType((*GetExchangeOTPReponse)(nil), "gctrpc.GetExchangeOTPReponse") + proto.RegisterType((*GetExchangeOTPsRequest)(nil), "gctrpc.GetExchangeOTPsRequest") + proto.RegisterType((*GetExchangeOTPsResponse)(nil), "gctrpc.GetExchangeOTPsResponse") + proto.RegisterMapType((map[string]string)(nil), "gctrpc.GetExchangeOTPsResponse.OtpCodesEntry") + proto.RegisterType((*DisableExchangeRequest)(nil), "gctrpc.DisableExchangeRequest") + proto.RegisterType((*PairsSupported)(nil), "gctrpc.PairsSupported") + proto.RegisterType((*GetExchangeInfoResponse)(nil), "gctrpc.GetExchangeInfoResponse") + proto.RegisterMapType((map[string]*PairsSupported)(nil), "gctrpc.GetExchangeInfoResponse.SupportedAssetsEntry") + proto.RegisterType((*GetTickerRequest)(nil), "gctrpc.GetTickerRequest") + proto.RegisterType((*CurrencyPair)(nil), "gctrpc.CurrencyPair") + proto.RegisterType((*TickerResponse)(nil), "gctrpc.TickerResponse") + proto.RegisterType((*GetTickersRequest)(nil), "gctrpc.GetTickersRequest") + proto.RegisterType((*Tickers)(nil), "gctrpc.Tickers") + proto.RegisterType((*GetTickersResponse)(nil), "gctrpc.GetTickersResponse") + proto.RegisterType((*GetOrderbookRequest)(nil), "gctrpc.GetOrderbookRequest") + proto.RegisterType((*OrderbookItem)(nil), "gctrpc.OrderbookItem") + proto.RegisterType((*OrderbookResponse)(nil), "gctrpc.OrderbookResponse") + proto.RegisterType((*GetOrderbooksRequest)(nil), "gctrpc.GetOrderbooksRequest") + proto.RegisterType((*Orderbooks)(nil), "gctrpc.Orderbooks") + proto.RegisterType((*GetOrderbooksResponse)(nil), "gctrpc.GetOrderbooksResponse") + proto.RegisterType((*GetAccountInfoRequest)(nil), "gctrpc.GetAccountInfoRequest") + proto.RegisterType((*Account)(nil), "gctrpc.Account") + proto.RegisterType((*AccountCurrencyInfo)(nil), "gctrpc.AccountCurrencyInfo") + proto.RegisterType((*GetAccountInfoResponse)(nil), "gctrpc.GetAccountInfoResponse") + proto.RegisterType((*GetConfigRequest)(nil), "gctrpc.GetConfigRequest") + proto.RegisterType((*GetConfigResponse)(nil), "gctrpc.GetConfigResponse") + proto.RegisterType((*PortfolioAddress)(nil), "gctrpc.PortfolioAddress") + proto.RegisterType((*GetPortfolioRequest)(nil), "gctrpc.GetPortfolioRequest") + proto.RegisterType((*GetPortfolioResponse)(nil), "gctrpc.GetPortfolioResponse") + proto.RegisterType((*GetPortfolioSummaryRequest)(nil), "gctrpc.GetPortfolioSummaryRequest") + proto.RegisterType((*Coin)(nil), "gctrpc.Coin") + proto.RegisterType((*OfflineCoinSummary)(nil), "gctrpc.OfflineCoinSummary") + proto.RegisterType((*OnlineCoinSummary)(nil), "gctrpc.OnlineCoinSummary") + proto.RegisterType((*OfflineCoins)(nil), "gctrpc.OfflineCoins") + proto.RegisterType((*OnlineCoins)(nil), "gctrpc.OnlineCoins") + proto.RegisterMapType((map[string]*OnlineCoinSummary)(nil), "gctrpc.OnlineCoins.CoinsEntry") + proto.RegisterType((*GetPortfolioSummaryResponse)(nil), "gctrpc.GetPortfolioSummaryResponse") + proto.RegisterMapType((map[string]*OfflineCoins)(nil), "gctrpc.GetPortfolioSummaryResponse.CoinsOfflineSummaryEntry") + proto.RegisterMapType((map[string]*OnlineCoins)(nil), "gctrpc.GetPortfolioSummaryResponse.CoinsOnlineSummaryEntry") + proto.RegisterType((*AddPortfolioAddressRequest)(nil), "gctrpc.AddPortfolioAddressRequest") + proto.RegisterType((*AddPortfolioAddressResponse)(nil), "gctrpc.AddPortfolioAddressResponse") + proto.RegisterType((*RemovePortfolioAddressRequest)(nil), "gctrpc.RemovePortfolioAddressRequest") + proto.RegisterType((*RemovePortfolioAddressResponse)(nil), "gctrpc.RemovePortfolioAddressResponse") + proto.RegisterType((*GetForexProvidersRequest)(nil), "gctrpc.GetForexProvidersRequest") + proto.RegisterType((*ForexProvider)(nil), "gctrpc.ForexProvider") + proto.RegisterType((*GetForexProvidersResponse)(nil), "gctrpc.GetForexProvidersResponse") + proto.RegisterType((*GetForexRatesRequest)(nil), "gctrpc.GetForexRatesRequest") + proto.RegisterType((*ForexRatesConversion)(nil), "gctrpc.ForexRatesConversion") + proto.RegisterType((*GetForexRatesResponse)(nil), "gctrpc.GetForexRatesResponse") + proto.RegisterType((*OrderDetails)(nil), "gctrpc.OrderDetails") + proto.RegisterType((*GetOrdersRequest)(nil), "gctrpc.GetOrdersRequest") + proto.RegisterType((*GetOrdersResponse)(nil), "gctrpc.GetOrdersResponse") + proto.RegisterType((*GetOrderRequest)(nil), "gctrpc.GetOrderRequest") + proto.RegisterType((*SubmitOrderRequest)(nil), "gctrpc.SubmitOrderRequest") + proto.RegisterType((*SubmitOrderResponse)(nil), "gctrpc.SubmitOrderResponse") + proto.RegisterType((*SimulateOrderRequest)(nil), "gctrpc.SimulateOrderRequest") + proto.RegisterType((*SimulateOrderResponse)(nil), "gctrpc.SimulateOrderResponse") + proto.RegisterType((*WhaleBombRequest)(nil), "gctrpc.WhaleBombRequest") + proto.RegisterType((*CancelOrderRequest)(nil), "gctrpc.CancelOrderRequest") + proto.RegisterType((*CancelOrderResponse)(nil), "gctrpc.CancelOrderResponse") + proto.RegisterType((*CancelAllOrdersRequest)(nil), "gctrpc.CancelAllOrdersRequest") + proto.RegisterType((*CancelAllOrdersResponse)(nil), "gctrpc.CancelAllOrdersResponse") + proto.RegisterType((*CancelAllOrdersResponse_Orders)(nil), "gctrpc.CancelAllOrdersResponse.Orders") + proto.RegisterMapType((map[string]string)(nil), "gctrpc.CancelAllOrdersResponse.Orders.OrderStatusEntry") + proto.RegisterType((*GetEventsRequest)(nil), "gctrpc.GetEventsRequest") + proto.RegisterType((*ConditionParams)(nil), "gctrpc.ConditionParams") + proto.RegisterType((*GetEventsResponse)(nil), "gctrpc.GetEventsResponse") + proto.RegisterType((*AddEventRequest)(nil), "gctrpc.AddEventRequest") + proto.RegisterType((*AddEventResponse)(nil), "gctrpc.AddEventResponse") + proto.RegisterType((*RemoveEventRequest)(nil), "gctrpc.RemoveEventRequest") + proto.RegisterType((*RemoveEventResponse)(nil), "gctrpc.RemoveEventResponse") + proto.RegisterType((*GetCryptocurrencyDepositAddressesRequest)(nil), "gctrpc.GetCryptocurrencyDepositAddressesRequest") + proto.RegisterType((*GetCryptocurrencyDepositAddressesResponse)(nil), "gctrpc.GetCryptocurrencyDepositAddressesResponse") + proto.RegisterMapType((map[string]string)(nil), "gctrpc.GetCryptocurrencyDepositAddressesResponse.AddressesEntry") + proto.RegisterType((*GetCryptocurrencyDepositAddressRequest)(nil), "gctrpc.GetCryptocurrencyDepositAddressRequest") + proto.RegisterType((*GetCryptocurrencyDepositAddressResponse)(nil), "gctrpc.GetCryptocurrencyDepositAddressResponse") + proto.RegisterType((*WithdrawCurrencyRequest)(nil), "gctrpc.WithdrawCurrencyRequest") + proto.RegisterType((*WithdrawResponse)(nil), "gctrpc.WithdrawResponse") + proto.RegisterType((*GetLoggerDetailsRequest)(nil), "gctrpc.GetLoggerDetailsRequest") + proto.RegisterType((*GetLoggerDetailsResponse)(nil), "gctrpc.GetLoggerDetailsResponse") + proto.RegisterType((*SetLoggerDetailsRequest)(nil), "gctrpc.SetLoggerDetailsRequest") + proto.RegisterType((*GetExchangePairsRequest)(nil), "gctrpc.GetExchangePairsRequest") + proto.RegisterType((*GetExchangePairsResponse)(nil), "gctrpc.GetExchangePairsResponse") + proto.RegisterMapType((map[string]*PairsSupported)(nil), "gctrpc.GetExchangePairsResponse.SupportedAssetsEntry") + proto.RegisterType((*ExchangePairRequest)(nil), "gctrpc.ExchangePairRequest") + proto.RegisterType((*GetOrderbookStreamRequest)(nil), "gctrpc.GetOrderbookStreamRequest") + proto.RegisterType((*GetExchangeOrderbookStreamRequest)(nil), "gctrpc.GetExchangeOrderbookStreamRequest") + proto.RegisterType((*GetTickerStreamRequest)(nil), "gctrpc.GetTickerStreamRequest") + proto.RegisterType((*GetExchangeTickerStreamRequest)(nil), "gctrpc.GetExchangeTickerStreamRequest") + proto.RegisterType((*GetAuditEventRequest)(nil), "gctrpc.GetAuditEventRequest") + proto.RegisterType((*GetAuditEventResponse)(nil), "gctrpc.GetAuditEventResponse") + proto.RegisterType((*AuditEvent)(nil), "gctrpc.AuditEvent") +} + +func init() { proto.RegisterFile("rpc.proto", fileDescriptor_77a6da22d6a3feb1) } + +var fileDescriptor_77a6da22d6a3feb1 = []byte{ + // 4838 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x3c, 0x4d, 0x8f, 0x24, 0x47, + 0x56, 0xca, 0xea, 0xcf, 0x7a, 0x5d, 0xfd, 0x15, 0xfd, 0x55, 0x53, 0xdd, 0x3d, 0x3d, 0x93, 0x5e, + 0x8f, 0x67, 0x66, 0xbd, 0x3d, 0xf6, 0x78, 0x60, 0xcd, 0xda, 0xec, 0xd2, 0x6e, 0xdb, 0xbd, 0xc6, + 0x5e, 0x4f, 0x93, 0x3d, 0x3b, 0x96, 0xbc, 0xc8, 0x45, 0x76, 0x65, 0x54, 0x77, 0x32, 0x55, 0x99, + 0xe9, 0xcc, 0xa8, 0xee, 0x29, 0x03, 0x62, 0x65, 0x09, 0xc4, 0x01, 0xc1, 0x61, 0x85, 0x04, 0x12, + 0x27, 0x8e, 0x48, 0x5c, 0x10, 0x27, 0x0e, 0x2b, 0xae, 0x88, 0x23, 0x17, 0x7e, 0x00, 0xe2, 0x06, + 0x48, 0x2b, 0x71, 0xe1, 0x84, 0xe2, 0xc5, 0x47, 0x46, 0x64, 0x66, 0x55, 0x57, 0xef, 0xce, 0x0e, + 0x17, 0xbb, 0xf2, 0xc5, 0x8b, 0xf7, 0x5e, 0xbc, 0x78, 0xf1, 0xe2, 0xbd, 0x17, 0xaf, 0x07, 0xea, + 0x69, 0xd2, 0xd9, 0x4f, 0xd2, 0x98, 0xc5, 0x64, 0xf6, 0xac, 0xc3, 0xd2, 0xa4, 0xd3, 0xda, 0x39, + 0x8b, 0xe3, 0xb3, 0x1e, 0x7d, 0xe0, 0x27, 0xe1, 0x03, 0x3f, 0x8a, 0x62, 0xe6, 0xb3, 0x30, 0x8e, + 0x32, 0x81, 0xe5, 0xae, 0xc0, 0xd2, 0x11, 0x65, 0x1f, 0x45, 0xdd, 0xd8, 0xa3, 0x5f, 0x0e, 0x68, + 0xc6, 0xdc, 0x7f, 0x98, 0x86, 0x65, 0x0d, 0xca, 0x92, 0x38, 0xca, 0x28, 0xd9, 0x84, 0xd9, 0x41, + 0xc2, 0xc2, 0x3e, 0x6d, 0x3a, 0xb7, 0x9c, 0xbb, 0x75, 0x4f, 0x7e, 0x91, 0x07, 0xb0, 0xe6, 0x5f, + 0xf8, 0x61, 0xcf, 0x3f, 0xed, 0xd1, 0x36, 0x7d, 0xde, 0x39, 0xf7, 0xa3, 0x33, 0x9a, 0x35, 0x6b, + 0xb7, 0x9c, 0xbb, 0x53, 0x1e, 0xd1, 0x43, 0x1f, 0xa8, 0x11, 0xf2, 0x4d, 0x58, 0xa5, 0x11, 0x07, + 0x05, 0x06, 0xfa, 0x14, 0xa2, 0xaf, 0xc8, 0x81, 0x1c, 0xf9, 0x11, 0x6c, 0x06, 0xb4, 0xeb, 0x0f, + 0x7a, 0xac, 0xdd, 0x8d, 0x53, 0xfa, 0xbc, 0x9d, 0xa4, 0xf1, 0x45, 0x18, 0xd0, 0xb4, 0x39, 0x8d, + 0x52, 0xac, 0xcb, 0xd1, 0x0f, 0xf9, 0xe0, 0xb1, 0x1c, 0x23, 0x0f, 0x61, 0x43, 0xcf, 0x0a, 0x7d, + 0xd6, 0xee, 0x0c, 0xd2, 0x94, 0x46, 0x9d, 0x61, 0x73, 0x06, 0x27, 0xad, 0xa9, 0x49, 0xa1, 0xcf, + 0x0e, 0xe5, 0x10, 0xf9, 0x0c, 0x56, 0xb2, 0xc1, 0x69, 0x36, 0xcc, 0x18, 0xed, 0xb7, 0x33, 0xe6, + 0xb3, 0x41, 0xd6, 0x9c, 0xbd, 0x35, 0x75, 0x77, 0xe1, 0xe1, 0xeb, 0xfb, 0x42, 0x8d, 0xfb, 0x05, + 0x95, 0xec, 0x9f, 0x28, 0xfc, 0x13, 0x44, 0xff, 0x20, 0x62, 0xe9, 0xd0, 0x5b, 0xce, 0x6c, 0x28, + 0xf9, 0x14, 0x16, 0xd3, 0xa4, 0xd3, 0xa6, 0x51, 0x90, 0xc4, 0x61, 0xc4, 0xb2, 0xe6, 0x1c, 0x52, + 0xbd, 0x37, 0x8a, 0xaa, 0x97, 0x74, 0x3e, 0x50, 0xb8, 0x82, 0x64, 0x23, 0x35, 0x40, 0xad, 0xf7, + 0x60, 0xbd, 0x8a, 0x31, 0x59, 0x81, 0xa9, 0x67, 0x74, 0x28, 0x77, 0x87, 0xff, 0x24, 0xeb, 0x30, + 0x73, 0xe1, 0xf7, 0x06, 0x14, 0x37, 0x63, 0xde, 0x13, 0x1f, 0xdf, 0xa9, 0xbd, 0xed, 0xb4, 0x9e, + 0xc0, 0x6a, 0x89, 0x4d, 0x05, 0x81, 0x7b, 0x26, 0x81, 0x85, 0x87, 0x6b, 0x4a, 0x64, 0xef, 0xf8, + 0x50, 0xcd, 0x35, 0xa8, 0xba, 0xb7, 0x61, 0xef, 0x88, 0xb2, 0xc3, 0xb8, 0xdf, 0x1f, 0x44, 0x61, + 0x07, 0x6d, 0xcc, 0xa3, 0x3d, 0x7f, 0x48, 0xd3, 0x4c, 0x59, 0xd6, 0xa7, 0xb0, 0x5e, 0x35, 0x4e, + 0x9a, 0x30, 0x27, 0xf7, 0x1e, 0xf9, 0xcf, 0x7b, 0xea, 0x93, 0xec, 0x40, 0xbd, 0x13, 0x47, 0x11, + 0xed, 0x30, 0x1a, 0xc8, 0x85, 0xe4, 0x00, 0xf7, 0x8f, 0x6b, 0x70, 0x6b, 0x34, 0x4f, 0x69, 0xba, + 0x5f, 0xc1, 0x66, 0xc7, 0x44, 0x68, 0xa7, 0x12, 0xa3, 0xe9, 0xe0, 0x56, 0x1c, 0x1a, 0x5b, 0x31, + 0x96, 0xd2, 0x7e, 0xe5, 0xa8, 0xd8, 0xa4, 0x8d, 0x4e, 0xd5, 0x58, 0xab, 0x0b, 0xad, 0xd1, 0x93, + 0x2a, 0x54, 0xfe, 0xd0, 0x56, 0xf9, 0x8e, 0x12, 0xad, 0x8a, 0x88, 0xa9, 0xfb, 0x6f, 0xc3, 0xd6, + 0x11, 0x8d, 0x68, 0x1a, 0x76, 0xb4, 0x71, 0x48, 0x9d, 0x73, 0x0d, 0x6a, 0x9b, 0x94, 0xac, 0x72, + 0x80, 0xdb, 0x82, 0x66, 0x79, 0xa2, 0x58, 0xae, 0xbb, 0x09, 0xeb, 0x47, 0x94, 0x69, 0xb8, 0xde, + 0xc5, 0x9f, 0x3a, 0xb0, 0x81, 0x03, 0xd9, 0x69, 0x36, 0x14, 0x03, 0x52, 0xd5, 0xbf, 0x03, 0xab, + 0x9a, 0x74, 0xa6, 0x8e, 0x91, 0xd0, 0xf2, 0x5b, 0x86, 0x96, 0xcb, 0x33, 0xf3, 0xc3, 0x94, 0x99, + 0xa7, 0x29, 0x3f, 0x93, 0x12, 0xdc, 0x3a, 0x84, 0x8d, 0x4a, 0xd4, 0xeb, 0xd8, 0xbf, 0xdb, 0x84, + 0xcd, 0x23, 0xca, 0x0c, 0x33, 0x36, 0x0c, 0x74, 0xc1, 0x00, 0x73, 0xbb, 0xcc, 0x98, 0x9f, 0xb2, + 0xdc, 0x2e, 0xe5, 0x27, 0x79, 0x15, 0x96, 0x7a, 0x61, 0xc6, 0x68, 0xd4, 0xf6, 0x83, 0x20, 0xa5, + 0x99, 0x70, 0x79, 0x75, 0x6f, 0x51, 0x40, 0x0f, 0x04, 0xd0, 0xfd, 0x47, 0x87, 0x6f, 0x4c, 0x81, + 0x95, 0x54, 0xd6, 0x27, 0x50, 0xcf, 0xbd, 0x82, 0x50, 0xd2, 0xbe, 0xa1, 0xa4, 0xaa, 0x39, 0xfb, + 0x05, 0xd7, 0x90, 0x13, 0x68, 0xfd, 0x16, 0x2c, 0xbd, 0xe8, 0x03, 0xfd, 0x36, 0xb4, 0xa4, 0x6d, + 0x28, 0x8f, 0xfc, 0xa9, 0xdf, 0xa7, 0xca, 0xae, 0x5a, 0x30, 0xaf, 0x1c, 0xb8, 0xe4, 0xa1, 0xbf, + 0xdd, 0x5d, 0xd8, 0xae, 0x9c, 0x29, 0x0d, 0xeb, 0x01, 0xac, 0x1d, 0x51, 0xa6, 0xdd, 0xbc, 0xa2, + 0x38, 0xd2, 0x0b, 0xb8, 0x8f, 0xd0, 0x12, 0x8d, 0x09, 0x52, 0x85, 0x3b, 0x50, 0xcf, 0x2f, 0x11, + 0x69, 0xdb, 0x1a, 0xe0, 0x3e, 0x44, 0x33, 0x55, 0xb3, 0x1e, 0x3f, 0x39, 0xf6, 0xa8, 0x98, 0x76, + 0x03, 0xe6, 0x63, 0x96, 0xb4, 0x3b, 0x71, 0xa0, 0x44, 0x9f, 0x8b, 0x59, 0x72, 0x18, 0x07, 0x54, + 0x9a, 0x86, 0x31, 0x47, 0x9b, 0xc6, 0xdf, 0x88, 0xad, 0xb4, 0x87, 0xa4, 0x1c, 0xbf, 0x09, 0x75, + 0x45, 0x50, 0x6d, 0xe5, 0xb7, 0x8c, 0xad, 0xac, 0x9a, 0xb3, 0xff, 0x58, 0x70, 0x94, 0x3b, 0x39, + 0x2f, 0x05, 0xc8, 0x5a, 0xef, 0xc0, 0xa2, 0x35, 0x74, 0x95, 0x65, 0xd7, 0xcd, 0x2d, 0x7b, 0x04, + 0x9b, 0xef, 0x87, 0x99, 0x79, 0xe3, 0x4e, 0xb2, 0x5d, 0x5f, 0xc0, 0xd2, 0xb1, 0x1f, 0xa6, 0xd9, + 0xc9, 0x20, 0x49, 0x62, 0x34, 0xef, 0xd7, 0x60, 0x39, 0xbf, 0xd6, 0x13, 0x3e, 0x26, 0x27, 0x2d, + 0x69, 0x30, 0xce, 0x20, 0xaf, 0xc0, 0xa2, 0xba, 0xce, 0x05, 0x9a, 0x10, 0xa9, 0x21, 0x81, 0x88, + 0xe4, 0x7e, 0x3d, 0x6d, 0xa9, 0xce, 0x0a, 0x2c, 0x08, 0x4c, 0x47, 0xbe, 0x0e, 0x2b, 0xf0, 0xb7, + 0x69, 0x08, 0x35, 0xfb, 0x3a, 0x68, 0xc2, 0xdc, 0x05, 0x4d, 0x4f, 0xe3, 0x8c, 0x62, 0xcc, 0x30, + 0xef, 0xa9, 0x4f, 0x2e, 0xc8, 0x20, 0x0b, 0xa3, 0xb3, 0x76, 0xe6, 0x47, 0xc1, 0x69, 0xfc, 0x1c, + 0x23, 0x84, 0x79, 0xaf, 0x81, 0xc0, 0x13, 0x01, 0x23, 0xb7, 0xa1, 0x71, 0xce, 0x58, 0xd2, 0xe6, + 0xa1, 0x4b, 0x3c, 0x60, 0x32, 0x20, 0x58, 0xe0, 0xb0, 0x27, 0x02, 0xc4, 0x0f, 0x36, 0xa2, 0x0c, + 0x32, 0x9a, 0xfa, 0x67, 0x34, 0x62, 0xcd, 0x59, 0x71, 0xb0, 0x39, 0xf4, 0x87, 0x0a, 0x48, 0x76, + 0x01, 0x10, 0x2d, 0x49, 0xe3, 0xe7, 0xc3, 0xe6, 0x9c, 0x30, 0x3d, 0x0e, 0x39, 0xe6, 0x00, 0xae, + 0xbf, 0x53, 0x3f, 0xa3, 0x2a, 0xf4, 0x08, 0x69, 0xd6, 0x9c, 0x17, 0xfa, 0xe3, 0xe0, 0x43, 0x0d, + 0x25, 0x6d, 0x1e, 0x77, 0x48, 0xad, 0xb7, 0xfd, 0x2c, 0xa3, 0x2c, 0x6b, 0xd6, 0xd1, 0x80, 0x1e, + 0x55, 0x18, 0x50, 0x21, 0xfe, 0x90, 0xf3, 0x0e, 0x70, 0x9a, 0x8e, 0x3f, 0x2c, 0x28, 0x8f, 0xb7, + 0xfc, 0x01, 0x3b, 0xa7, 0x11, 0xe3, 0xb7, 0x07, 0x67, 0x92, 0x84, 0x4d, 0x40, 0xdd, 0xac, 0x58, + 0x03, 0x07, 0x49, 0xd8, 0xfa, 0x9c, 0x07, 0x17, 0x65, 0xaa, 0x15, 0x26, 0xf8, 0xba, 0xed, 0x4a, + 0x36, 0x95, 0xb0, 0xb6, 0x1d, 0x99, 0xa6, 0x79, 0x09, 0x2b, 0x47, 0x94, 0x3d, 0x09, 0x3b, 0xcf, + 0x68, 0x3a, 0x81, 0x51, 0x92, 0xbb, 0x30, 0xcd, 0x2d, 0x4a, 0x32, 0x58, 0xd7, 0x37, 0xa1, 0x8c, + 0xd8, 0x38, 0x23, 0x0f, 0x31, 0xf8, 0x5e, 0xa0, 0xe6, 0xda, 0x6c, 0x98, 0x08, 0xbb, 0xa8, 0x7b, + 0x75, 0x84, 0x3c, 0x19, 0x26, 0xd4, 0x7d, 0x0a, 0x0d, 0x73, 0x12, 0x77, 0x1a, 0x01, 0xed, 0x85, + 0xfd, 0x90, 0xd1, 0x54, 0x39, 0x0d, 0x0d, 0xe0, 0xf6, 0xc8, 0xb7, 0x48, 0xda, 0x31, 0xfe, 0xe6, + 0xe7, 0xed, 0xcb, 0x41, 0xcc, 0x14, 0x6d, 0xf1, 0xe1, 0xfe, 0x45, 0x0d, 0x96, 0xd4, 0x72, 0xa4, + 0x31, 0x2b, 0x99, 0x9d, 0x2b, 0x65, 0xbe, 0x0d, 0x8d, 0x9e, 0x9f, 0xb1, 0xf6, 0x20, 0x09, 0x7c, + 0x15, 0xda, 0x4c, 0x79, 0x0b, 0x1c, 0xf6, 0x43, 0x01, 0xe2, 0x16, 0xad, 0x22, 0x57, 0x3c, 0x5b, + 0x92, 0x7b, 0xa3, 0x63, 0x2e, 0x86, 0xc0, 0x34, 0x9f, 0x83, 0xd6, 0xee, 0x78, 0xf8, 0x9b, 0xc3, + 0xce, 0xc3, 0xb3, 0x73, 0xb4, 0x6e, 0xc7, 0xc3, 0xdf, 0x7c, 0x07, 0x7b, 0xf1, 0x25, 0xda, 0xb2, + 0xe3, 0xf1, 0x9f, 0x1c, 0x72, 0x1a, 0x06, 0x68, 0xba, 0x8e, 0xc7, 0x7f, 0x72, 0x88, 0x9f, 0x3d, + 0x43, 0x43, 0x75, 0x3c, 0xfe, 0x93, 0x47, 0xfd, 0x17, 0x71, 0x6f, 0xd0, 0xa7, 0xcd, 0x3a, 0x02, + 0xe5, 0x17, 0xd9, 0x86, 0x7a, 0x92, 0x86, 0x1d, 0xda, 0xf6, 0xd9, 0x39, 0x1a, 0x93, 0xe3, 0xcd, + 0x23, 0xe0, 0x80, 0x9d, 0xbb, 0x6b, 0xb0, 0xaa, 0x37, 0x5a, 0x7b, 0xcf, 0xcf, 0x60, 0x4e, 0x42, + 0xc6, 0x6e, 0xfa, 0x1b, 0x30, 0xc7, 0x04, 0x5a, 0xb3, 0x86, 0xa7, 0x40, 0x1b, 0x96, 0xad, 0x69, + 0x4f, 0xa1, 0xb9, 0xdf, 0x03, 0x62, 0x72, 0x93, 0x1b, 0x71, 0x2f, 0xa7, 0x23, 0xdc, 0xf1, 0xb2, + 0x4d, 0x27, 0xcb, 0x09, 0x7c, 0x85, 0x97, 0xd1, 0xe3, 0x34, 0xe0, 0x8e, 0x24, 0x7e, 0xf6, 0x52, + 0x4d, 0xf3, 0x07, 0xb0, 0xa8, 0x19, 0x7f, 0xc4, 0x68, 0x9f, 0x2b, 0xdc, 0xef, 0xc7, 0x83, 0x88, + 0x21, 0x4f, 0xc7, 0x93, 0x5f, 0xdc, 0x02, 0x51, 0xbf, 0xc8, 0xd2, 0xf1, 0xc4, 0x07, 0x59, 0x82, + 0x5a, 0x18, 0xc8, 0xe4, 0xa9, 0x16, 0x06, 0xee, 0xff, 0x3a, 0xb0, 0x6a, 0x2c, 0xe4, 0xda, 0x46, + 0x59, 0xb2, 0xb8, 0x5a, 0x85, 0xc5, 0xdd, 0x83, 0xe9, 0xd3, 0x30, 0xe0, 0x39, 0x1b, 0xd7, 0xeb, + 0x86, 0x22, 0x67, 0xad, 0xc3, 0x43, 0x14, 0x8e, 0xea, 0x67, 0xcf, 0xb2, 0xe6, 0xf4, 0x58, 0x54, + 0x8e, 0x52, 0x3a, 0x0f, 0x33, 0xe5, 0xf3, 0x60, 0xeb, 0x72, 0xb6, 0xa8, 0x4b, 0x11, 0xad, 0x6a, + 0xda, 0xda, 0xf2, 0x3a, 0x00, 0x39, 0x70, 0xec, 0xb6, 0xfe, 0x1a, 0x40, 0xac, 0x31, 0xa5, 0xfd, + 0xdd, 0x28, 0x09, 0xad, 0x4d, 0xd0, 0x40, 0x76, 0x3f, 0xc6, 0x50, 0xc3, 0x64, 0x2e, 0x95, 0xff, + 0xd0, 0xa2, 0x29, 0x6c, 0x91, 0x94, 0x68, 0x66, 0x16, 0xb1, 0xb7, 0x90, 0xd8, 0x41, 0xa7, 0xc3, + 0xb7, 0xde, 0x48, 0xcc, 0xc7, 0xde, 0xe1, 0x4f, 0x61, 0x4e, 0xce, 0x90, 0x66, 0x21, 0x10, 0x6a, + 0x61, 0x40, 0xde, 0x01, 0x30, 0xee, 0x21, 0xb1, 0xae, 0x6d, 0x25, 0x83, 0x9c, 0xa4, 0xac, 0x01, + 0xd9, 0x19, 0xe8, 0x6e, 0x17, 0xd6, 0x2a, 0x50, 0xb8, 0x28, 0x3a, 0xad, 0x96, 0xa2, 0xa8, 0x6f, + 0xb2, 0x07, 0x0b, 0x2c, 0x66, 0x7e, 0xaf, 0x9d, 0xdf, 0x10, 0x8e, 0x07, 0x08, 0x7a, 0xca, 0x21, + 0xe8, 0xa0, 0xe2, 0x9e, 0xb0, 0x5c, 0xee, 0xa0, 0xe2, 0x5e, 0xe0, 0xfa, 0x18, 0x78, 0x59, 0x8b, + 0x96, 0x2a, 0x1c, 0xb7, 0x65, 0xdf, 0x84, 0x79, 0x5f, 0x4c, 0x51, 0x0b, 0x5b, 0x2e, 0x2c, 0xcc, + 0xd3, 0x08, 0x2e, 0xc1, 0x1b, 0xe8, 0x30, 0x8e, 0xba, 0xe1, 0x99, 0xb2, 0x8e, 0xd7, 0xd0, 0x59, + 0x29, 0x58, 0x1e, 0x93, 0x04, 0x3e, 0xf3, 0x91, 0x5b, 0xc3, 0xc3, 0xdf, 0xee, 0x1f, 0x39, 0xb0, + 0x72, 0x1c, 0xa7, 0xac, 0x1b, 0xf7, 0xc2, 0x58, 0x86, 0xf7, 0x3c, 0x1c, 0x51, 0xe1, 0xbf, 0x8c, + 0x23, 0xe5, 0x27, 0xf7, 0x90, 0x9d, 0x38, 0x8c, 0x84, 0xad, 0xd6, 0xa4, 0x82, 0xe2, 0x30, 0xe2, + 0xa6, 0x4a, 0x6e, 0xc1, 0x42, 0x40, 0xb3, 0x4e, 0x1a, 0x26, 0x3c, 0x9d, 0x93, 0x6e, 0xc1, 0x04, + 0x71, 0xc2, 0xa7, 0x7e, 0xcf, 0x8f, 0x3a, 0x54, 0x7a, 0x76, 0xf5, 0xe9, 0x6e, 0xa0, 0xbb, 0xd2, + 0x92, 0x18, 0x99, 0xb5, 0x0d, 0x96, 0x4b, 0xf9, 0x55, 0xa8, 0x27, 0x0a, 0x28, 0xcd, 0xaf, 0xa9, + 0xef, 0xea, 0xc2, 0x72, 0xbc, 0x1c, 0xd5, 0xdd, 0xe1, 0xb1, 0x7f, 0x4e, 0xef, 0x64, 0xd0, 0xef, + 0xfb, 0xe9, 0x50, 0x71, 0x8b, 0x60, 0xfa, 0x30, 0x0e, 0x23, 0xae, 0x28, 0xbe, 0x28, 0x15, 0xbc, + 0xf1, 0xdf, 0xa6, 0xe8, 0x35, 0x4b, 0x74, 0x53, 0x5b, 0x53, 0xb6, 0xb6, 0x6e, 0x02, 0x24, 0x34, + 0xed, 0xd0, 0x88, 0xf9, 0x67, 0x6a, 0xc5, 0x06, 0xc4, 0x3d, 0x07, 0xf2, 0xb8, 0xdb, 0xed, 0x85, + 0x11, 0xe5, 0x6c, 0xa5, 0x30, 0x63, 0xb4, 0x3f, 0x5a, 0x06, 0x9b, 0xd3, 0x54, 0x89, 0xd3, 0x0f, + 0x60, 0xf5, 0x71, 0x54, 0xc1, 0x48, 0x91, 0x73, 0xc6, 0x91, 0xab, 0x95, 0xc8, 0x7d, 0x1f, 0x1a, + 0x86, 0xe0, 0x19, 0x79, 0x1b, 0xea, 0x52, 0x46, 0x9d, 0x28, 0xb4, 0xb4, 0x37, 0x28, 0xad, 0xd0, + 0xcb, 0x91, 0xdd, 0xbf, 0x74, 0x60, 0x21, 0x97, 0x2c, 0x23, 0x8f, 0x60, 0x86, 0xab, 0x5b, 0x51, + 0xb9, 0xa9, 0xa9, 0xe4, 0x38, 0xfb, 0xf8, 0x5f, 0x11, 0x17, 0x0a, 0xe4, 0xd6, 0x09, 0x40, 0x0e, + 0xac, 0x08, 0xeb, 0x1e, 0xd8, 0x61, 0xdd, 0x8d, 0x32, 0x55, 0x25, 0x9a, 0x11, 0xd9, 0xfd, 0xcb, + 0x34, 0x4f, 0xf7, 0x2a, 0x8c, 0x45, 0xda, 0xe0, 0xb7, 0x60, 0x41, 0x9c, 0x05, 0xee, 0x01, 0x94, + 0xc0, 0x8d, 0xbc, 0xb4, 0x11, 0x46, 0x1e, 0xe0, 0xd9, 0xc0, 0x71, 0xf2, 0x26, 0x2c, 0xa2, 0xb0, + 0xed, 0x58, 0x28, 0x44, 0x1e, 0x6c, 0x7b, 0x42, 0x03, 0x51, 0xa4, 0xca, 0x48, 0x02, 0x1b, 0xd6, + 0x94, 0x76, 0x26, 0x44, 0x90, 0x97, 0xd4, 0xbb, 0x46, 0x28, 0x3d, 0x4a, 0x4a, 0xa1, 0x2c, 0x49, + 0x50, 0x8e, 0x09, 0xd5, 0xad, 0x75, 0xca, 0x23, 0xe4, 0x01, 0x34, 0x24, 0x47, 0xd4, 0x8c, 0xbc, + 0xe2, 0x6c, 0x19, 0x17, 0xc4, 0x44, 0x44, 0x20, 0x7d, 0x58, 0x37, 0x27, 0x68, 0x09, 0x67, 0x70, + 0xe2, 0x3b, 0x93, 0x4b, 0x18, 0x95, 0x04, 0x24, 0x9d, 0xd2, 0x40, 0xeb, 0xb7, 0xa1, 0x39, 0x6a, + 0x41, 0x15, 0xdb, 0x7e, 0xdf, 0xde, 0xf6, 0xf5, 0x0a, 0x93, 0xcc, 0xcc, 0x02, 0xe2, 0xe7, 0xb0, + 0x35, 0x42, 0x98, 0x6b, 0x54, 0x1d, 0x0c, 0x4b, 0x35, 0xad, 0xe9, 0xcf, 0x1d, 0x68, 0x1d, 0x04, + 0x41, 0xc9, 0x39, 0xe5, 0x45, 0x82, 0x97, 0xed, 0x72, 0x77, 0x61, 0xbb, 0x52, 0x20, 0x59, 0xcd, + 0x78, 0x0e, 0xbb, 0x1e, 0xed, 0xc7, 0x17, 0xf4, 0x65, 0x8b, 0xec, 0xde, 0x82, 0x9b, 0xa3, 0x38, + 0x4b, 0xd9, 0xb0, 0xbc, 0x67, 0x97, 0xc7, 0x75, 0x60, 0xf4, 0x9f, 0x0e, 0x2c, 0xda, 0x85, 0xf3, + 0x17, 0x95, 0x8b, 0xbf, 0x0e, 0x24, 0xa5, 0x19, 0x6b, 0x27, 0x71, 0xaf, 0xc7, 0x53, 0xf2, 0x80, + 0xf6, 0xfc, 0xa1, 0x2c, 0xd9, 0xaf, 0xf0, 0x91, 0x63, 0x31, 0xf0, 0x3e, 0x87, 0x93, 0x2d, 0x98, + 0xf3, 0x93, 0xb0, 0xcd, 0xad, 0x46, 0xe4, 0xe3, 0xb3, 0x7e, 0x12, 0x7e, 0x4c, 0x87, 0xc4, 0x85, + 0x45, 0x39, 0xd0, 0xee, 0xd1, 0x0b, 0xda, 0xc3, 0x98, 0x6f, 0xca, 0x5b, 0x10, 0xc3, 0x9f, 0x70, + 0x10, 0xb9, 0x07, 0x2b, 0x49, 0x1a, 0x72, 0xf3, 0xcb, 0xdf, 0x06, 0xe6, 0x50, 0x9a, 0x65, 0x09, + 0x57, 0xab, 0x73, 0x7f, 0x04, 0x37, 0x2a, 0x74, 0x21, 0x7d, 0xd4, 0x77, 0x61, 0xd9, 0x7e, 0x61, + 0x50, 0x7e, 0x4a, 0x47, 0xad, 0xd6, 0x44, 0x6f, 0xa9, 0x6b, 0xd1, 0x91, 0xd1, 0x27, 0xe2, 0x78, + 0x3e, 0xd3, 0x35, 0x2d, 0xf7, 0x4b, 0x58, 0xcf, 0x81, 0x87, 0x71, 0x74, 0x41, 0xd3, 0x8c, 0x5b, + 0x1b, 0x81, 0xe9, 0x6e, 0x1a, 0xab, 0x82, 0x2c, 0xfe, 0xe6, 0x71, 0x1b, 0x8b, 0xa5, 0x19, 0xd4, + 0x58, 0xcc, 0x71, 0x52, 0x9f, 0xa9, 0x5b, 0x0a, 0x7f, 0xf3, 0x38, 0x39, 0x44, 0x22, 0xb4, 0x8d, + 0x63, 0xc2, 0x54, 0x17, 0x24, 0x8c, 0x73, 0x71, 0x9f, 0x62, 0xf8, 0x68, 0x8a, 0x22, 0xd7, 0xf8, + 0xeb, 0xb0, 0x20, 0xd6, 0xc8, 0x67, 0xaa, 0xf5, 0xed, 0x58, 0xeb, 0x2b, 0x88, 0xe9, 0x41, 0x57, + 0x43, 0xdd, 0xff, 0xae, 0x41, 0x03, 0x23, 0xd6, 0xf7, 0x29, 0xf3, 0xc3, 0xde, 0xf8, 0x58, 0x5a, + 0xc4, 0xa0, 0x35, 0x1d, 0x83, 0xbe, 0x02, 0x8b, 0x66, 0x41, 0x64, 0xa8, 0x92, 0x59, 0xa3, 0x1c, + 0x32, 0x24, 0xaf, 0xc2, 0x12, 0xa6, 0xd6, 0x39, 0x96, 0xb0, 0x99, 0x45, 0x84, 0x6a, 0x34, 0x3b, + 0x11, 0x98, 0x29, 0x24, 0x02, 0x7c, 0x18, 0x83, 0xe9, 0x76, 0x16, 0x06, 0x3a, 0x4f, 0x40, 0xc8, + 0x49, 0x18, 0x18, 0xc3, 0x38, 0x7b, 0xce, 0x18, 0xc6, 0xd9, 0x3c, 0x07, 0x4a, 0xa9, 0x78, 0x28, + 0xc0, 0xf7, 0xae, 0x79, 0x34, 0xba, 0x86, 0x02, 0x3e, 0x09, 0xfb, 0xf8, 0x1a, 0x26, 0x8b, 0xdb, + 0x75, 0x61, 0xb1, 0xe2, 0x2b, 0x4f, 0xd3, 0xc0, 0x4c, 0xd3, 0xf2, 0xa4, 0x6e, 0xc1, 0x4a, 0xea, + 0xf6, 0x60, 0x21, 0x4e, 0x68, 0xd4, 0x96, 0x29, 0x76, 0x43, 0x44, 0x0f, 0x1c, 0xf4, 0x14, 0x21, + 0xb2, 0x64, 0x82, 0x3a, 0xcf, 0x26, 0xc9, 0x4b, 0x6d, 0xc5, 0xd4, 0x8a, 0x8a, 0x51, 0x89, 0xe0, + 0xd4, 0x55, 0x89, 0xa0, 0x7b, 0x80, 0x51, 0xb1, 0x62, 0x2c, 0xcd, 0xe7, 0x75, 0x98, 0x45, 0x35, + 0x29, 0xcb, 0x59, 0xb7, 0xd2, 0x18, 0x69, 0x14, 0x9e, 0xc4, 0x71, 0xbf, 0x8f, 0x6f, 0x88, 0x38, + 0x34, 0x89, 0xe8, 0x37, 0x60, 0x5e, 0xec, 0x8a, 0xb6, 0x9a, 0x39, 0xfc, 0xfe, 0x28, 0x70, 0xff, + 0xcd, 0x01, 0x72, 0x32, 0x38, 0xed, 0x87, 0x93, 0x53, 0x9b, 0x3c, 0x41, 0x27, 0x30, 0x8d, 0x66, + 0x22, 0xcc, 0x11, 0x7f, 0x17, 0x2c, 0x64, 0xba, 0x68, 0x21, 0xf9, 0x76, 0xce, 0x54, 0xe7, 0xe8, + 0xb3, 0xe6, 0xe6, 0x73, 0x17, 0xdf, 0x0b, 0x69, 0xc4, 0xda, 0xb2, 0xd8, 0xc2, 0x5d, 0x3c, 0x02, + 0x3e, 0x0a, 0xdc, 0x13, 0x58, 0xb3, 0x56, 0x26, 0x35, 0x7d, 0x1b, 0x1a, 0x42, 0x80, 0xa4, 0xe7, + 0x77, 0x74, 0x35, 0x7c, 0x01, 0x61, 0xc7, 0x08, 0x1a, 0xa7, 0xaf, 0x3f, 0x71, 0x60, 0xfd, 0x24, + 0xec, 0x0f, 0x7a, 0x3e, 0xa3, 0xbf, 0x04, 0x8d, 0xe5, 0xcb, 0x9f, 0xb2, 0x96, 0xaf, 0x34, 0x39, + 0x9d, 0x6b, 0xd2, 0xfd, 0x99, 0x03, 0x1b, 0x05, 0x51, 0x74, 0x4c, 0x68, 0x1b, 0xd3, 0x88, 0xe2, + 0x80, 0x44, 0x32, 0x98, 0xd6, 0x2c, 0xa6, 0xaf, 0xc0, 0x62, 0x3f, 0x8c, 0xc2, 0xfe, 0xa0, 0xdf, + 0x16, 0xba, 0x17, 0x32, 0x35, 0x24, 0xf0, 0x18, 0xb7, 0x80, 0x23, 0xf9, 0xcf, 0x0d, 0xa4, 0x69, + 0x89, 0x24, 0x80, 0x02, 0xe9, 0x0d, 0x58, 0xcf, 0xe3, 0xf6, 0xf6, 0x99, 0x1f, 0x46, 0xed, 0x5e, + 0x9c, 0x65, 0x72, 0x8f, 0x49, 0x3e, 0x76, 0xe4, 0x87, 0xd1, 0x27, 0x71, 0x96, 0x19, 0x4e, 0x60, + 0xd6, 0x74, 0x02, 0x3c, 0x80, 0x59, 0xf9, 0xec, 0xdc, 0xef, 0xd1, 0xf7, 0xe2, 0xfe, 0xe9, 0x8b, + 0xd5, 0xfd, 0x6d, 0x68, 0x88, 0xba, 0x1b, 0xf3, 0xd3, 0x33, 0xaa, 0x76, 0x60, 0x01, 0x61, 0x4f, + 0x10, 0x54, 0xb9, 0x0d, 0xff, 0xe5, 0x00, 0x39, 0xe4, 0xa1, 0x4c, 0x6f, 0x62, 0x7b, 0xe0, 0xae, + 0x44, 0xe4, 0xcd, 0xb9, 0x85, 0xd5, 0x25, 0xe4, 0x23, 0xdb, 0xfc, 0xa6, 0x2c, 0xf3, 0xd3, 0xab, + 0x99, 0xbe, 0x66, 0x71, 0xac, 0xe4, 0xc7, 0x5f, 0x85, 0xa5, 0x4b, 0xbf, 0xd7, 0xa3, 0x4c, 0x3f, + 0xb1, 0xc9, 0x4a, 0xbc, 0x80, 0xaa, 0x1c, 0x5c, 0x2d, 0x78, 0xce, 0x58, 0xf0, 0x06, 0xac, 0x59, + 0xeb, 0x95, 0xd1, 0xd0, 0x23, 0xd8, 0x14, 0xe0, 0x83, 0x5e, 0x6f, 0x62, 0xaf, 0xea, 0xfe, 0x75, + 0x0d, 0xb6, 0x4a, 0xd3, 0x74, 0xd8, 0x60, 0x9b, 0xf1, 0x1d, 0xbd, 0xdc, 0xea, 0x09, 0xfb, 0xf2, + 0x53, 0xce, 0x6a, 0xfd, 0x93, 0x03, 0xb3, 0x02, 0x34, 0x76, 0x37, 0x3e, 0x57, 0x0e, 0x41, 0x1a, + 0x9c, 0xc8, 0x88, 0xbe, 0x3d, 0x19, 0x33, 0xf1, 0x3f, 0xf3, 0x59, 0x55, 0x78, 0x12, 0xf9, 0xa2, + 0xfa, 0x5d, 0x58, 0x29, 0x22, 0x5c, 0xeb, 0xc9, 0x49, 0x54, 0x55, 0x3e, 0xb8, 0xa0, 0xc6, 0x33, + 0xea, 0x4f, 0x1d, 0x58, 0x3e, 0x8c, 0xa3, 0x20, 0xe4, 0x37, 0xe6, 0xb1, 0x9f, 0xfa, 0xfd, 0x4c, + 0xbe, 0xe4, 0x0b, 0x90, 0x2a, 0xbb, 0x6b, 0xc0, 0x88, 0x02, 0xe7, 0x2e, 0x40, 0xe7, 0x9c, 0x76, + 0x9e, 0xb5, 0x65, 0xc5, 0x51, 0x3c, 0xff, 0x73, 0xc8, 0x7b, 0x61, 0x90, 0x91, 0x6f, 0xc1, 0x5a, + 0x3e, 0xdc, 0xf6, 0xa3, 0xa0, 0x2d, 0xcb, 0x8d, 0xf8, 0xba, 0xa1, 0xf1, 0x0e, 0xa2, 0xe0, 0x20, + 0x7b, 0x96, 0xf1, 0x58, 0x51, 0x57, 0xd9, 0xda, 0x96, 0x0b, 0x5f, 0xd6, 0xf0, 0x03, 0x04, 0xbb, + 0xff, 0xe3, 0xe0, 0x0d, 0xa8, 0x56, 0x25, 0x77, 0x3b, 0x2f, 0xac, 0x61, 0xbd, 0xd5, 0xda, 0xb2, + 0x5a, 0x61, 0xcb, 0x08, 0x4c, 0x87, 0x8c, 0xf6, 0xd5, 0xc5, 0xc2, 0x7f, 0x93, 0xf7, 0x60, 0x45, + 0xaf, 0xb8, 0x9d, 0xa0, 0x5a, 0xe4, 0x31, 0xd9, 0xca, 0x13, 0x47, 0x4b, 0x6b, 0xde, 0x72, 0xa7, + 0xa0, 0x46, 0x75, 0xbc, 0x66, 0x26, 0x72, 0xd4, 0x1d, 0xd4, 0xb6, 0xf4, 0x4f, 0xe2, 0x4b, 0x48, + 0x4d, 0x3b, 0x03, 0x46, 0x03, 0x19, 0x2a, 0xeb, 0x6f, 0xf7, 0x3f, 0x1c, 0x58, 0x3e, 0x08, 0x02, + 0x5c, 0xf7, 0x24, 0x6e, 0x42, 0xad, 0xb2, 0x76, 0xc5, 0x2a, 0xa7, 0x7e, 0xce, 0x55, 0xfe, 0xc2, + 0x4e, 0x64, 0x84, 0x12, 0x5c, 0x17, 0x56, 0xf2, 0x75, 0x56, 0x6f, 0xaf, 0xfb, 0x0d, 0x20, 0x22, + 0xbd, 0xb2, 0xd4, 0x51, 0xc4, 0xda, 0x80, 0x35, 0x0b, 0x4b, 0xfa, 0x9a, 0x0f, 0xe1, 0xee, 0x11, + 0x65, 0x87, 0xe9, 0x30, 0x61, 0xb1, 0x0a, 0x67, 0xdf, 0xa7, 0x49, 0x9c, 0x85, 0xca, 0x73, 0xd1, + 0x89, 0xbc, 0xcf, 0x3f, 0x3b, 0x70, 0x6f, 0x02, 0x42, 0x72, 0x09, 0x5f, 0x94, 0xeb, 0x4b, 0xbf, + 0x61, 0xb6, 0xb7, 0x4c, 0x44, 0x65, 0x5f, 0x43, 0x64, 0x97, 0x81, 0x26, 0xd9, 0x7a, 0x17, 0x96, + 0xec, 0xc1, 0x6b, 0xb9, 0x8a, 0x1e, 0xdc, 0xb9, 0x42, 0x88, 0x49, 0x6c, 0xee, 0x0e, 0x2c, 0x75, + 0x2c, 0x12, 0x92, 0x51, 0x01, 0xea, 0x1e, 0xc2, 0x6b, 0x57, 0x72, 0x93, 0x6a, 0x1b, 0x99, 0xa1, + 0xbb, 0x7f, 0x37, 0x0d, 0x5b, 0x9f, 0x85, 0xec, 0x3c, 0x48, 0xfd, 0x4b, 0x65, 0x7d, 0x93, 0x08, + 0x59, 0x48, 0xde, 0x6b, 0xe5, 0x7a, 0xc3, 0x7d, 0x58, 0x8d, 0x23, 0x8a, 0x39, 0x46, 0x3b, 0xf1, + 0xb3, 0xec, 0x32, 0x4e, 0xd5, 0x5d, 0xba, 0x1c, 0x47, 0x94, 0xe7, 0x19, 0xc7, 0x12, 0x5c, 0xb8, + 0x8d, 0xa7, 0x8b, 0xb7, 0xf1, 0x0a, 0x4c, 0x25, 0x61, 0x24, 0xdf, 0x4c, 0xf8, 0x4f, 0x7e, 0x77, + 0xb2, 0xd4, 0x0f, 0x0c, 0xca, 0xf2, 0xee, 0x44, 0xa8, 0xa6, 0x6b, 0x56, 0xf1, 0xe7, 0x0a, 0x55, + 0x7c, 0x43, 0x27, 0xf3, 0x76, 0xd5, 0x62, 0x0f, 0x16, 0xe4, 0xcf, 0x36, 0xf3, 0xcf, 0x64, 0x0a, + 0x04, 0x12, 0xf4, 0xc4, 0x3f, 0x33, 0xa2, 0x35, 0xb0, 0xa2, 0xb5, 0x5d, 0x80, 0x2e, 0xa5, 0x6d, + 0x2b, 0x19, 0xaa, 0x77, 0x29, 0x15, 0x4e, 0x97, 0x87, 0xca, 0xa7, 0x7e, 0xf4, 0xac, 0x8d, 0x35, + 0x88, 0x86, 0x10, 0x87, 0x03, 0x3e, 0xf5, 0xfb, 0x18, 0x13, 0xe3, 0xa0, 0x92, 0x69, 0x51, 0x68, + 0x94, 0xc3, 0x0e, 0xf2, 0x6a, 0x0a, 0xa2, 0x74, 0x42, 0x36, 0x6c, 0x2e, 0xe5, 0xf3, 0x0f, 0x43, + 0x36, 0xd4, 0xf3, 0x51, 0x67, 0xe9, 0xb0, 0xb9, 0x9c, 0xcf, 0x3f, 0x14, 0x20, 0x2e, 0x5e, 0x76, + 0x19, 0x76, 0xa9, 0x68, 0x0c, 0x59, 0x91, 0xad, 0x52, 0x1c, 0x72, 0x18, 0x07, 0x18, 0x46, 0x5e, + 0x86, 0xa9, 0x91, 0x9c, 0xae, 0x8a, 0x14, 0x96, 0x03, 0x95, 0x69, 0xb8, 0xf7, 0x61, 0x45, 0x99, + 0x8b, 0xd9, 0x3b, 0x99, 0xd2, 0x6c, 0xd0, 0x63, 0xaa, 0x77, 0x52, 0x7c, 0xb9, 0x6f, 0x62, 0x57, + 0xc4, 0x27, 0xf1, 0xd9, 0x59, 0x9e, 0x3e, 0x49, 0xd3, 0xda, 0x84, 0xd9, 0x1e, 0xc2, 0xd5, 0x14, + 0xf1, 0xe5, 0x46, 0x58, 0xcf, 0x29, 0x4c, 0xc9, 0x5f, 0x2d, 0xc2, 0xa8, 0x1b, 0xcb, 0x6c, 0x01, + 0x7f, 0xf3, 0xb3, 0x18, 0xd0, 0xd3, 0xc1, 0x99, 0xea, 0x81, 0xc2, 0x0f, 0x8e, 0x79, 0xe9, 0xa7, + 0x91, 0xbc, 0x50, 0xf1, 0x37, 0xc7, 0xa4, 0x69, 0x1a, 0xa7, 0xf2, 0xf6, 0x14, 0x1f, 0xee, 0x11, + 0x6c, 0x9d, 0x5c, 0x4f, 0x44, 0x4e, 0x48, 0x54, 0x6b, 0xe4, 0xf1, 0xc7, 0x0f, 0xf7, 0x63, 0xab, + 0x03, 0x04, 0xbb, 0x04, 0x26, 0x39, 0x46, 0xeb, 0x30, 0x83, 0xbe, 0x5c, 0x11, 0xc3, 0x0f, 0x9e, + 0x11, 0x36, 0xcb, 0xd4, 0x74, 0x0f, 0x5a, 0xb9, 0xa3, 0x42, 0x78, 0xc2, 0x5f, 0xa9, 0xe8, 0xa8, + 0xb0, 0xe6, 0x4e, 0xd6, 0x52, 0xf1, 0x4b, 0xed, 0x92, 0xf8, 0x0a, 0xd6, 0x4c, 0xd1, 0x5e, 0x6a, + 0xd6, 0xff, 0x63, 0x07, 0x2b, 0x64, 0x3a, 0x03, 0x3b, 0x61, 0x29, 0xf5, 0xfb, 0x2f, 0xf5, 0x41, + 0xfc, 0x7b, 0x70, 0xdb, 0xec, 0x97, 0xba, 0xb6, 0x24, 0xee, 0x1f, 0xe0, 0x33, 0xa2, 0x78, 0xe4, + 0xff, 0x7f, 0x90, 0xff, 0x5d, 0xb8, 0x69, 0xc8, 0x7f, 0x4d, 0x31, 0xdc, 0xbf, 0x72, 0xb0, 0x8a, + 0x78, 0x30, 0x08, 0x42, 0x66, 0xc5, 0x1c, 0xdc, 0x33, 0x31, 0x3f, 0x65, 0xed, 0xc0, 0x67, 0x54, + 0x37, 0x71, 0x72, 0xc8, 0xfb, 0x3e, 0xc3, 0xe2, 0x09, 0x8d, 0x02, 0x31, 0x28, 0x8b, 0x01, 0x34, + 0x0a, 0xd4, 0x90, 0xc8, 0x1c, 0x4e, 0x87, 0x56, 0xa2, 0xf6, 0x1e, 0xde, 0xd3, 0xd8, 0xf4, 0x82, + 0x27, 0x7e, 0xc6, 0x13, 0x1f, 0xfc, 0x58, 0xc7, 0xdd, 0x2e, 0x3f, 0x72, 0x33, 0x08, 0x96, 0x5f, + 0xee, 0xa1, 0x78, 0x94, 0x36, 0x44, 0x93, 0xe7, 0xed, 0x3e, 0xcc, 0x52, 0x0c, 0x93, 0x8b, 0xaf, + 0xdb, 0x06, 0xae, 0xc4, 0x70, 0x9f, 0x03, 0xe4, 0x50, 0xee, 0x86, 0x50, 0x8b, 0xb2, 0x06, 0xca, + 0x7f, 0x93, 0x9b, 0x00, 0x61, 0x40, 0x23, 0x16, 0x76, 0x43, 0xaa, 0xfa, 0x0f, 0x0c, 0x08, 0xbf, + 0x95, 0xfa, 0x34, 0xcb, 0xd4, 0xe3, 0x5d, 0xdd, 0x53, 0x9f, 0x3c, 0xbf, 0xe0, 0x77, 0x69, 0xc6, + 0xfc, 0x7e, 0xa2, 0xae, 0x48, 0x0d, 0x78, 0xf8, 0xb3, 0xfb, 0xb0, 0x74, 0x14, 0x8b, 0x60, 0xe0, + 0x09, 0xbf, 0x03, 0x53, 0xf2, 0x18, 0xe6, 0x64, 0xf3, 0x35, 0xd9, 0x2c, 0x75, 0x63, 0xa3, 0xde, + 0x5b, 0x5b, 0x23, 0xba, 0xb4, 0xdd, 0xb5, 0xaf, 0xff, 0xf5, 0xdf, 0x7f, 0x52, 0x5b, 0x24, 0x0b, + 0x0f, 0x2e, 0xde, 0x7c, 0x70, 0x46, 0x19, 0x3a, 0xdb, 0x33, 0x58, 0xb4, 0xfa, 0x65, 0xc9, 0x8e, + 0xd5, 0xf3, 0x5a, 0x68, 0xa3, 0x6d, 0xed, 0x8e, 0xed, 0x88, 0x75, 0x6f, 0x20, 0x8b, 0x35, 0xb2, + 0x2a, 0x59, 0xe4, 0xad, 0xb0, 0xe4, 0x4b, 0x58, 0xfe, 0x00, 0x8b, 0xf0, 0x9a, 0x28, 0xd9, 0xcb, + 0x89, 0x55, 0xb6, 0x01, 0xb7, 0x6e, 0x8d, 0x46, 0x90, 0x0c, 0xb7, 0x91, 0xe1, 0x06, 0x59, 0xe3, + 0x0c, 0x45, 0x91, 0x5f, 0xf3, 0x24, 0x19, 0xac, 0xc8, 0xc6, 0xc2, 0x17, 0xca, 0x73, 0x07, 0x79, + 0x6e, 0x92, 0x75, 0xce, 0x33, 0x10, 0x0c, 0x72, 0xa6, 0x31, 0xd6, 0x10, 0xcd, 0x46, 0x58, 0x72, + 0x73, 0x64, 0x87, 0xac, 0x60, 0xb9, 0x77, 0x45, 0x07, 0xad, 0xbd, 0xca, 0x33, 0xca, 0x71, 0x75, + 0x13, 0x2d, 0xf9, 0x89, 0xb8, 0x58, 0x2a, 0x5b, 0xb6, 0xc9, 0x6b, 0x57, 0xf7, 0x89, 0x0b, 0x19, + 0xee, 0x4e, 0xda, 0x50, 0xee, 0x7e, 0x03, 0x85, 0xb9, 0x49, 0x76, 0xa4, 0x30, 0x56, 0x13, 0xb9, + 0x6a, 0x53, 0x27, 0x1d, 0x68, 0x98, 0xdd, 0xaf, 0x64, 0xbb, 0xe2, 0x1e, 0xd3, 0xcc, 0x77, 0xaa, + 0x07, 0x25, 0xc3, 0x26, 0x32, 0x24, 0x64, 0x45, 0x32, 0xd4, 0xcd, 0xb2, 0xe4, 0x2b, 0x58, 0x2e, + 0x74, 0x8e, 0x12, 0xb7, 0xb0, 0x7d, 0x15, 0x5d, 0xc0, 0xad, 0x57, 0xc6, 0xe2, 0x48, 0xae, 0x37, + 0x91, 0x6b, 0xd3, 0x5d, 0x33, 0x76, 0x59, 0x71, 0xfe, 0x8e, 0x73, 0x9f, 0x64, 0xb8, 0xcf, 0x66, + 0x93, 0xe3, 0x44, 0xbc, 0xf7, 0xae, 0xe8, 0x90, 0x2c, 0xed, 0xb5, 0xe2, 0x89, 0xa7, 0x35, 0xc3, + 0xc6, 0x31, 0xa3, 0x35, 0x17, 0x83, 0xbc, 0x49, 0xf8, 0xee, 0x56, 0xb7, 0xf6, 0xca, 0xee, 0x62, + 0xb7, 0x85, 0x5c, 0xd7, 0x09, 0x29, 0x70, 0x8d, 0x59, 0x42, 0x32, 0xab, 0xf3, 0x59, 0x32, 0xb5, + 0xad, 0xba, 0xa2, 0xf7, 0xb8, 0x72, 0xa5, 0x66, 0x33, 0xf1, 0xc8, 0x95, 0xc6, 0x2c, 0xc9, 0xc8, + 0x73, 0x58, 0x12, 0xee, 0xe2, 0xc5, 0xef, 0xec, 0x2e, 0xf2, 0xdd, 0x72, 0x49, 0xee, 0x33, 0xcc, + 0x8d, 0xfd, 0x0c, 0xea, 0xfa, 0x36, 0x26, 0x4d, 0x63, 0x11, 0x56, 0x1b, 0x68, 0x6b, 0x44, 0x93, + 0x9f, 0xb2, 0x56, 0x77, 0x51, 0xae, 0x4a, 0xb4, 0xec, 0x71, 0xc2, 0x3f, 0x02, 0xc8, 0xbb, 0xfe, + 0xc8, 0x8d, 0x12, 0x65, 0xad, 0xb9, 0x56, 0xd5, 0x90, 0xfa, 0xfb, 0x06, 0x24, 0xbf, 0x42, 0x96, + 0x2c, 0xf2, 0xea, 0xbc, 0xe9, 0xe0, 0xc3, 0x3a, 0x6f, 0xc5, 0x3e, 0xc1, 0xd6, 0xe8, 0x06, 0x31, + 0xb5, 0x29, 0xae, 0x3a, 0x6c, 0xba, 0xc8, 0xc4, 0x57, 0x20, 0x2e, 0x0b, 0xa3, 0x33, 0x6d, 0xa7, + 0x8a, 0x4b, 0xe5, 0x65, 0x51, 0x6e, 0x33, 0x2b, 0x5d, 0x16, 0x79, 0x37, 0x19, 0x79, 0x86, 0x7f, + 0xdf, 0x65, 0x34, 0x56, 0x11, 0x93, 0x56, 0xb9, 0xcb, 0xac, 0x75, 0x73, 0xd4, 0x70, 0x56, 0x6d, + 0xdf, 0x32, 0x0f, 0xc5, 0x43, 0x25, 0x36, 0x5c, 0xb4, 0x53, 0x59, 0x1b, 0x6e, 0x75, 0x5d, 0xb5, + 0x6e, 0x54, 0x8c, 0x48, 0xea, 0x1b, 0x48, 0x7d, 0x99, 0x2c, 0x6a, 0x97, 0x88, 0xb4, 0xc4, 0x9e, + 0xe8, 0x77, 0x6e, 0x6b, 0x4f, 0x8a, 0xcd, 0x50, 0x96, 0x0f, 0x2c, 0xb5, 0x44, 0x95, 0x7c, 0xa0, + 0x6e, 0x7a, 0x22, 0x7f, 0x68, 0xf7, 0x56, 0xa9, 0x5e, 0x0f, 0x77, 0x6c, 0x73, 0x46, 0xe9, 0xb4, + 0x8c, 0x6c, 0xe0, 0x70, 0xf7, 0x90, 0xf3, 0x0d, 0xb2, 0x55, 0xe4, 0x2c, 0x9b, 0x41, 0xc8, 0xd7, + 0x0e, 0xac, 0x55, 0xb4, 0x1a, 0xe4, 0x12, 0x8c, 0x6e, 0x8c, 0xc8, 0x25, 0x18, 0xd7, 0xab, 0xe0, + 0xa2, 0x04, 0x3b, 0x2e, 0x4a, 0xe0, 0x07, 0x81, 0x96, 0x40, 0xa6, 0xd5, 0xdc, 0x32, 0xff, 0xcc, + 0x81, 0xcd, 0xea, 0xb6, 0x02, 0xf2, 0xaa, 0xfe, 0x8b, 0x91, 0x71, 0x0d, 0x0f, 0xad, 0x3b, 0x57, + 0xa1, 0x49, 0x69, 0x5e, 0x45, 0x69, 0xf6, 0xdc, 0x16, 0x97, 0x26, 0x45, 0xdc, 0x2a, 0x81, 0x2e, + 0xb1, 0x16, 0x6b, 0x3f, 0xdc, 0x13, 0x23, 0xb6, 0xa8, 0xee, 0x6f, 0x68, 0xdd, 0x1e, 0x83, 0x61, + 0xbb, 0x2f, 0xb2, 0x21, 0x37, 0x04, 0x5f, 0xbb, 0x75, 0x07, 0x80, 0x3c, 0xa3, 0xf9, 0xc3, 0xb8, + 0x75, 0x46, 0x4b, 0x6f, 0xfd, 0xd6, 0x19, 0x2d, 0x3f, 0xbf, 0x97, 0xce, 0x28, 0x32, 0xc3, 0xa7, + 0x78, 0xf2, 0x39, 0x1e, 0x1b, 0xf9, 0x10, 0xd0, 0x2c, 0x1e, 0xf5, 0xac, 0xea, 0xd8, 0xd8, 0xa5, + 0xfe, 0x92, 0xab, 0x14, 0xef, 0x0b, 0x5c, 0x7b, 0x1e, 0xcc, 0x2b, 0x74, 0xb2, 0x55, 0x24, 0xa0, + 0x28, 0x57, 0xbe, 0xe5, 0xba, 0x5b, 0x48, 0x74, 0xd5, 0x6d, 0x98, 0x44, 0x39, 0xcd, 0x53, 0x58, + 0x30, 0xde, 0x2d, 0x89, 0x76, 0xb2, 0xe5, 0x67, 0xda, 0xd6, 0x76, 0xe5, 0x98, 0xed, 0x4a, 0xdc, + 0x65, 0xce, 0x20, 0x43, 0x04, 0xcd, 0xe3, 0x77, 0x61, 0xd1, 0x7a, 0x3a, 0xcc, 0x95, 0x5f, 0xf5, + 0xb8, 0x99, 0x2b, 0xbf, 0xf2, 0xbd, 0x51, 0x05, 0x9a, 0x2e, 0x2a, 0x3f, 0x93, 0x28, 0x9a, 0xd7, + 0x17, 0x50, 0xd7, 0x2f, 0x76, 0xb9, 0xfe, 0x8b, 0x8f, 0x78, 0x57, 0xf1, 0xb0, 0xf6, 0xe0, 0x92, + 0x4f, 0x3e, 0x8d, 0xfb, 0xa7, 0x52, 0x5f, 0xc6, 0x7b, 0x54, 0xae, 0xaf, 0xf2, 0xa3, 0x5c, 0xae, + 0xaf, 0xaa, 0x07, 0x2c, 0x4b, 0x5f, 0x1d, 0x44, 0xd0, 0x6b, 0x48, 0x61, 0xb9, 0xf0, 0x0e, 0x94, + 0x87, 0x15, 0xd5, 0xaf, 0x5e, 0x79, 0x58, 0x31, 0xe2, 0x01, 0xc9, 0x0e, 0xdc, 0x04, 0x3f, 0xbf, + 0xd7, 0xcb, 0x6d, 0x4b, 0xb8, 0x7b, 0xf1, 0x4a, 0x62, 0xd9, 0xad, 0xf5, 0x1c, 0x64, 0xd9, 0xad, + 0xfd, 0xa4, 0x52, 0x72, 0xf7, 0x22, 0x51, 0x24, 0x4f, 0x61, 0x5e, 0x95, 0xe7, 0x73, 0xa3, 0x2d, + 0x3c, 0x4c, 0xb4, 0x9a, 0xe5, 0x01, 0x49, 0xd5, 0x32, 0x5c, 0x3f, 0x08, 0x90, 0xaa, 0xdc, 0x08, + 0xa3, 0x58, 0x9f, 0x6f, 0x44, 0xb9, 0xce, 0x9f, 0x6f, 0x44, 0x55, 0x75, 0xdf, 0xda, 0x08, 0xe1, + 0xb9, 0x34, 0x8f, 0xbf, 0x77, 0xb0, 0x88, 0x31, 0xbe, 0xd6, 0x4e, 0xde, 0xb8, 0x46, 0x59, 0x5e, + 0x08, 0xf4, 0xe6, 0xb5, 0x0b, 0xf9, 0xee, 0x5d, 0x14, 0xd3, 0x75, 0x77, 0xd5, 0x65, 0x8a, 0xd3, + 0x02, 0x81, 0xae, 0xab, 0xfa, 0x5c, 0xe8, 0xbf, 0x75, 0xc4, 0x5f, 0xef, 0x8e, 0xa1, 0x4b, 0xf6, + 0x27, 0x14, 0x40, 0x09, 0xfc, 0x60, 0x62, 0x7c, 0x29, 0xee, 0x1d, 0x14, 0xf7, 0x96, 0xbb, 0x3d, + 0x46, 0x5c, 0x2e, 0xec, 0xef, 0xc3, 0xb6, 0xae, 0xc9, 0x5b, 0x74, 0x3f, 0x1c, 0x44, 0x41, 0x96, + 0xe7, 0xa5, 0x23, 0x0a, 0xf7, 0xb9, 0xe1, 0x14, 0x4b, 0xb5, 0xf6, 0xfd, 0x78, 0x29, 0x47, 0x85, + 0x18, 0x5d, 0x4e, 0x9b, 0x73, 0x4f, 0x60, 0x55, 0xcd, 0xfb, 0x30, 0xf4, 0xd9, 0x2f, 0xcc, 0xf3, + 0x16, 0xf2, 0x6c, 0xb9, 0x1b, 0x26, 0xcf, 0x6e, 0xe8, 0x33, 0xcd, 0x31, 0xc3, 0x27, 0x56, 0xab, + 0x0a, 0x6b, 0x26, 0xdf, 0x95, 0xf5, 0x59, 0x33, 0xf9, 0xae, 0x2e, 0x18, 0xdb, 0xc9, 0xf7, 0x19, + 0x65, 0xa2, 0x80, 0x1b, 0x48, 0x06, 0x17, 0xb0, 0x72, 0x32, 0x92, 0xe9, 0xc9, 0xcf, 0xcd, 0x54, + 0xc6, 0x40, 0x2e, 0x32, 0xcd, 0x0a, 0x4c, 0xf9, 0x62, 0x2f, 0xc4, 0x7b, 0xb2, 0x59, 0x9f, 0x25, + 0x7b, 0xa3, 0x2b, 0xb7, 0x65, 0xbe, 0x95, 0xa5, 0x5d, 0x9b, 0xaf, 0x91, 0x21, 0xe1, 0x5f, 0x2d, + 0x72, 0xbe, 0x43, 0x20, 0x76, 0x96, 0x84, 0x7f, 0xed, 0xa2, 0xbd, 0x40, 0x45, 0x55, 0x76, 0xb2, + 0x14, 0xe9, 0x36, 0x32, 0xde, 0x76, 0x37, 0xcb, 0x29, 0x12, 0xe7, 0xcd, 0x59, 0xff, 0x1e, 0xac, + 0x15, 0x72, 0xef, 0x17, 0xc4, 0xdb, 0x32, 0xe7, 0x42, 0xe2, 0xad, 0x98, 0x33, 0xcc, 0x83, 0x0b, + 0xa5, 0x56, 0x72, 0xbb, 0x2a, 0xdf, 0xb0, 0x2a, 0x99, 0xe3, 0x32, 0x1f, 0x79, 0x6f, 0x90, 0xcd, + 0x52, 0x3a, 0x82, 0x14, 0xde, 0x70, 0xc8, 0x9f, 0x3a, 0xf8, 0x07, 0x06, 0x23, 0x2a, 0xbd, 0xe4, + 0x5e, 0x55, 0xc2, 0x7b, 0x6d, 0x31, 0xa4, 0x3f, 0x21, 0x37, 0x8b, 0x59, 0x71, 0x49, 0x9c, 0x73, + 0xac, 0x40, 0x98, 0xf5, 0x5a, 0x2b, 0x27, 0xaf, 0x28, 0xe4, 0x8e, 0x4c, 0x5a, 0x8b, 0xa9, 0xb8, + 0xcc, 0x2a, 0x15, 0xa7, 0x1f, 0xdb, 0x7f, 0x46, 0x6c, 0xb1, 0xbc, 0x53, 0xb1, 0xea, 0xeb, 0xb0, + 0x7e, 0x05, 0x59, 0xef, 0x92, 0xed, 0xc2, 0x7a, 0x0b, 0x22, 0x88, 0xb0, 0xd6, 0x28, 0xc4, 0x9a, + 0x61, 0x6d, 0xa9, 0xf8, 0x6c, 0x85, 0xb5, 0xe5, 0xfa, 0x6f, 0x29, 0xac, 0xf5, 0x39, 0x0a, 0x5e, + 0x86, 0xa7, 0xb3, 0xf8, 0x2f, 0x8c, 0xbc, 0xf5, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x02, 0x27, + 0x05, 0x6d, 0x94, 0x44, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// GoCryptoTraderClient is the client API for GoCryptoTrader service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type GoCryptoTraderClient interface { + GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error) + GetSubsystems(ctx context.Context, in *GetSubsystemsRequest, opts ...grpc.CallOption) (*GetSusbsytemsResponse, error) + EnableSubsystem(ctx context.Context, in *GenericSubsystemRequest, opts ...grpc.CallOption) (*GenericSubsystemResponse, error) + DisableSubsystem(ctx context.Context, in *GenericSubsystemRequest, opts ...grpc.CallOption) (*GenericSubsystemResponse, error) + GetRPCEndpoints(ctx context.Context, in *GetRPCEndpointsRequest, opts ...grpc.CallOption) (*GetRPCEndpointsResponse, error) + GetCommunicationRelayers(ctx context.Context, in *GetCommunicationRelayersRequest, opts ...grpc.CallOption) (*GetCommunicationRelayersResponse, error) + GetExchanges(ctx context.Context, in *GetExchangesRequest, opts ...grpc.CallOption) (*GetExchangesResponse, error) + DisableExchange(ctx context.Context, in *GenericExchangeNameRequest, opts ...grpc.CallOption) (*GenericExchangeNameResponse, error) + GetExchangeInfo(ctx context.Context, in *GenericExchangeNameRequest, opts ...grpc.CallOption) (*GetExchangeInfoResponse, error) + GetExchangeOTPCode(ctx context.Context, in *GenericExchangeNameRequest, opts ...grpc.CallOption) (*GetExchangeOTPReponse, error) + GetExchangeOTPCodes(ctx context.Context, in *GetExchangeOTPsRequest, opts ...grpc.CallOption) (*GetExchangeOTPsResponse, error) + EnableExchange(ctx context.Context, in *GenericExchangeNameRequest, opts ...grpc.CallOption) (*GenericExchangeNameResponse, error) + GetTicker(ctx context.Context, in *GetTickerRequest, opts ...grpc.CallOption) (*TickerResponse, error) + GetTickers(ctx context.Context, in *GetTickersRequest, opts ...grpc.CallOption) (*GetTickersResponse, error) + GetOrderbook(ctx context.Context, in *GetOrderbookRequest, opts ...grpc.CallOption) (*OrderbookResponse, error) + GetOrderbooks(ctx context.Context, in *GetOrderbooksRequest, opts ...grpc.CallOption) (*GetOrderbooksResponse, error) + GetAccountInfo(ctx context.Context, in *GetAccountInfoRequest, opts ...grpc.CallOption) (*GetAccountInfoResponse, error) + GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) + GetPortfolio(ctx context.Context, in *GetPortfolioRequest, opts ...grpc.CallOption) (*GetPortfolioResponse, error) + GetPortfolioSummary(ctx context.Context, in *GetPortfolioSummaryRequest, opts ...grpc.CallOption) (*GetPortfolioSummaryResponse, error) + AddPortfolioAddress(ctx context.Context, in *AddPortfolioAddressRequest, opts ...grpc.CallOption) (*AddPortfolioAddressResponse, error) + RemovePortfolioAddress(ctx context.Context, in *RemovePortfolioAddressRequest, opts ...grpc.CallOption) (*RemovePortfolioAddressResponse, error) + GetForexProviders(ctx context.Context, in *GetForexProvidersRequest, opts ...grpc.CallOption) (*GetForexProvidersResponse, error) + GetForexRates(ctx context.Context, in *GetForexRatesRequest, opts ...grpc.CallOption) (*GetForexRatesResponse, error) + GetOrders(ctx context.Context, in *GetOrdersRequest, opts ...grpc.CallOption) (*GetOrdersResponse, error) + GetOrder(ctx context.Context, in *GetOrderRequest, opts ...grpc.CallOption) (*OrderDetails, error) + SubmitOrder(ctx context.Context, in *SubmitOrderRequest, opts ...grpc.CallOption) (*SubmitOrderResponse, error) + SimulateOrder(ctx context.Context, in *SimulateOrderRequest, opts ...grpc.CallOption) (*SimulateOrderResponse, error) + WhaleBomb(ctx context.Context, in *WhaleBombRequest, opts ...grpc.CallOption) (*SimulateOrderResponse, error) + CancelOrder(ctx context.Context, in *CancelOrderRequest, opts ...grpc.CallOption) (*CancelOrderResponse, error) + CancelAllOrders(ctx context.Context, in *CancelAllOrdersRequest, opts ...grpc.CallOption) (*CancelAllOrdersResponse, error) + GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) + AddEvent(ctx context.Context, in *AddEventRequest, opts ...grpc.CallOption) (*AddEventResponse, error) + RemoveEvent(ctx context.Context, in *RemoveEventRequest, opts ...grpc.CallOption) (*RemoveEventResponse, error) + GetCryptocurrencyDepositAddresses(ctx context.Context, in *GetCryptocurrencyDepositAddressesRequest, opts ...grpc.CallOption) (*GetCryptocurrencyDepositAddressesResponse, error) + GetCryptocurrencyDepositAddress(ctx context.Context, in *GetCryptocurrencyDepositAddressRequest, opts ...grpc.CallOption) (*GetCryptocurrencyDepositAddressResponse, error) + WithdrawCryptocurrencyFunds(ctx context.Context, in *WithdrawCurrencyRequest, opts ...grpc.CallOption) (*WithdrawResponse, error) + WithdrawFiatFunds(ctx context.Context, in *WithdrawCurrencyRequest, opts ...grpc.CallOption) (*WithdrawResponse, error) + GetLoggerDetails(ctx context.Context, in *GetLoggerDetailsRequest, opts ...grpc.CallOption) (*GetLoggerDetailsResponse, error) + SetLoggerDetails(ctx context.Context, in *SetLoggerDetailsRequest, opts ...grpc.CallOption) (*GetLoggerDetailsResponse, error) + GetExchangePairs(ctx context.Context, in *GetExchangePairsRequest, opts ...grpc.CallOption) (*GetExchangePairsResponse, error) + EnableExchangePair(ctx context.Context, in *ExchangePairRequest, opts ...grpc.CallOption) (*GenericExchangeNameResponse, error) + DisableExchangePair(ctx context.Context, in *ExchangePairRequest, opts ...grpc.CallOption) (*GenericExchangeNameResponse, error) + GetOrderbookStream(ctx context.Context, in *GetOrderbookStreamRequest, opts ...grpc.CallOption) (GoCryptoTrader_GetOrderbookStreamClient, error) + GetExchangeOrderbookStream(ctx context.Context, in *GetExchangeOrderbookStreamRequest, opts ...grpc.CallOption) (GoCryptoTrader_GetExchangeOrderbookStreamClient, error) + GetTickerStream(ctx context.Context, in *GetTickerStreamRequest, opts ...grpc.CallOption) (GoCryptoTrader_GetTickerStreamClient, error) + GetExchangeTickerStream(ctx context.Context, in *GetExchangeTickerStreamRequest, opts ...grpc.CallOption) (GoCryptoTrader_GetExchangeTickerStreamClient, error) + GetAuditEvent(ctx context.Context, in *GetAuditEventRequest, opts ...grpc.CallOption) (*GetAuditEventResponse, error) +} + +type goCryptoTraderClient struct { + cc *grpc.ClientConn +} + +func NewGoCryptoTraderClient(cc *grpc.ClientConn) GoCryptoTraderClient { + return &goCryptoTraderClient{cc} +} + +func (c *goCryptoTraderClient) GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error) { + out := new(GetInfoResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetInfo", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetSubsystems(ctx context.Context, in *GetSubsystemsRequest, opts ...grpc.CallOption) (*GetSusbsytemsResponse, error) { + out := new(GetSusbsytemsResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetSubsystems", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) EnableSubsystem(ctx context.Context, in *GenericSubsystemRequest, opts ...grpc.CallOption) (*GenericSubsystemResponse, error) { + out := new(GenericSubsystemResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/EnableSubsystem", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) DisableSubsystem(ctx context.Context, in *GenericSubsystemRequest, opts ...grpc.CallOption) (*GenericSubsystemResponse, error) { + out := new(GenericSubsystemResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/DisableSubsystem", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetRPCEndpoints(ctx context.Context, in *GetRPCEndpointsRequest, opts ...grpc.CallOption) (*GetRPCEndpointsResponse, error) { + out := new(GetRPCEndpointsResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetRPCEndpoints", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetCommunicationRelayers(ctx context.Context, in *GetCommunicationRelayersRequest, opts ...grpc.CallOption) (*GetCommunicationRelayersResponse, error) { + out := new(GetCommunicationRelayersResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetCommunicationRelayers", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetExchanges(ctx context.Context, in *GetExchangesRequest, opts ...grpc.CallOption) (*GetExchangesResponse, error) { + out := new(GetExchangesResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetExchanges", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) DisableExchange(ctx context.Context, in *GenericExchangeNameRequest, opts ...grpc.CallOption) (*GenericExchangeNameResponse, error) { + out := new(GenericExchangeNameResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/DisableExchange", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetExchangeInfo(ctx context.Context, in *GenericExchangeNameRequest, opts ...grpc.CallOption) (*GetExchangeInfoResponse, error) { + out := new(GetExchangeInfoResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetExchangeInfo", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetExchangeOTPCode(ctx context.Context, in *GenericExchangeNameRequest, opts ...grpc.CallOption) (*GetExchangeOTPReponse, error) { + out := new(GetExchangeOTPReponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetExchangeOTPCode", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetExchangeOTPCodes(ctx context.Context, in *GetExchangeOTPsRequest, opts ...grpc.CallOption) (*GetExchangeOTPsResponse, error) { + out := new(GetExchangeOTPsResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetExchangeOTPCodes", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) EnableExchange(ctx context.Context, in *GenericExchangeNameRequest, opts ...grpc.CallOption) (*GenericExchangeNameResponse, error) { + out := new(GenericExchangeNameResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/EnableExchange", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetTicker(ctx context.Context, in *GetTickerRequest, opts ...grpc.CallOption) (*TickerResponse, error) { + out := new(TickerResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetTicker", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetTickers(ctx context.Context, in *GetTickersRequest, opts ...grpc.CallOption) (*GetTickersResponse, error) { + out := new(GetTickersResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetTickers", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetOrderbook(ctx context.Context, in *GetOrderbookRequest, opts ...grpc.CallOption) (*OrderbookResponse, error) { + out := new(OrderbookResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetOrderbook", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetOrderbooks(ctx context.Context, in *GetOrderbooksRequest, opts ...grpc.CallOption) (*GetOrderbooksResponse, error) { + out := new(GetOrderbooksResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetOrderbooks", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetAccountInfo(ctx context.Context, in *GetAccountInfoRequest, opts ...grpc.CallOption) (*GetAccountInfoResponse, error) { + out := new(GetAccountInfoResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetAccountInfo", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) { + out := new(GetConfigResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetConfig", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetPortfolio(ctx context.Context, in *GetPortfolioRequest, opts ...grpc.CallOption) (*GetPortfolioResponse, error) { + out := new(GetPortfolioResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetPortfolio", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetPortfolioSummary(ctx context.Context, in *GetPortfolioSummaryRequest, opts ...grpc.CallOption) (*GetPortfolioSummaryResponse, error) { + out := new(GetPortfolioSummaryResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetPortfolioSummary", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) AddPortfolioAddress(ctx context.Context, in *AddPortfolioAddressRequest, opts ...grpc.CallOption) (*AddPortfolioAddressResponse, error) { + out := new(AddPortfolioAddressResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/AddPortfolioAddress", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) RemovePortfolioAddress(ctx context.Context, in *RemovePortfolioAddressRequest, opts ...grpc.CallOption) (*RemovePortfolioAddressResponse, error) { + out := new(RemovePortfolioAddressResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/RemovePortfolioAddress", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetForexProviders(ctx context.Context, in *GetForexProvidersRequest, opts ...grpc.CallOption) (*GetForexProvidersResponse, error) { + out := new(GetForexProvidersResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetForexProviders", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetForexRates(ctx context.Context, in *GetForexRatesRequest, opts ...grpc.CallOption) (*GetForexRatesResponse, error) { + out := new(GetForexRatesResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetForexRates", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetOrders(ctx context.Context, in *GetOrdersRequest, opts ...grpc.CallOption) (*GetOrdersResponse, error) { + out := new(GetOrdersResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetOrders", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetOrder(ctx context.Context, in *GetOrderRequest, opts ...grpc.CallOption) (*OrderDetails, error) { + out := new(OrderDetails) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetOrder", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) SubmitOrder(ctx context.Context, in *SubmitOrderRequest, opts ...grpc.CallOption) (*SubmitOrderResponse, error) { + out := new(SubmitOrderResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/SubmitOrder", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) SimulateOrder(ctx context.Context, in *SimulateOrderRequest, opts ...grpc.CallOption) (*SimulateOrderResponse, error) { + out := new(SimulateOrderResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/SimulateOrder", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) WhaleBomb(ctx context.Context, in *WhaleBombRequest, opts ...grpc.CallOption) (*SimulateOrderResponse, error) { + out := new(SimulateOrderResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/WhaleBomb", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) CancelOrder(ctx context.Context, in *CancelOrderRequest, opts ...grpc.CallOption) (*CancelOrderResponse, error) { + out := new(CancelOrderResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/CancelOrder", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) CancelAllOrders(ctx context.Context, in *CancelAllOrdersRequest, opts ...grpc.CallOption) (*CancelAllOrdersResponse, error) { + out := new(CancelAllOrdersResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/CancelAllOrders", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) { + out := new(GetEventsResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetEvents", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) AddEvent(ctx context.Context, in *AddEventRequest, opts ...grpc.CallOption) (*AddEventResponse, error) { + out := new(AddEventResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/AddEvent", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) RemoveEvent(ctx context.Context, in *RemoveEventRequest, opts ...grpc.CallOption) (*RemoveEventResponse, error) { + out := new(RemoveEventResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/RemoveEvent", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetCryptocurrencyDepositAddresses(ctx context.Context, in *GetCryptocurrencyDepositAddressesRequest, opts ...grpc.CallOption) (*GetCryptocurrencyDepositAddressesResponse, error) { + out := new(GetCryptocurrencyDepositAddressesResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetCryptocurrencyDepositAddresses", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetCryptocurrencyDepositAddress(ctx context.Context, in *GetCryptocurrencyDepositAddressRequest, opts ...grpc.CallOption) (*GetCryptocurrencyDepositAddressResponse, error) { + out := new(GetCryptocurrencyDepositAddressResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetCryptocurrencyDepositAddress", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) WithdrawCryptocurrencyFunds(ctx context.Context, in *WithdrawCurrencyRequest, opts ...grpc.CallOption) (*WithdrawResponse, error) { + out := new(WithdrawResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/WithdrawCryptocurrencyFunds", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) WithdrawFiatFunds(ctx context.Context, in *WithdrawCurrencyRequest, opts ...grpc.CallOption) (*WithdrawResponse, error) { + out := new(WithdrawResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/WithdrawFiatFunds", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetLoggerDetails(ctx context.Context, in *GetLoggerDetailsRequest, opts ...grpc.CallOption) (*GetLoggerDetailsResponse, error) { + out := new(GetLoggerDetailsResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetLoggerDetails", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) SetLoggerDetails(ctx context.Context, in *SetLoggerDetailsRequest, opts ...grpc.CallOption) (*GetLoggerDetailsResponse, error) { + out := new(GetLoggerDetailsResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/SetLoggerDetails", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetExchangePairs(ctx context.Context, in *GetExchangePairsRequest, opts ...grpc.CallOption) (*GetExchangePairsResponse, error) { + out := new(GetExchangePairsResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetExchangePairs", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) EnableExchangePair(ctx context.Context, in *ExchangePairRequest, opts ...grpc.CallOption) (*GenericExchangeNameResponse, error) { + out := new(GenericExchangeNameResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/EnableExchangePair", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) DisableExchangePair(ctx context.Context, in *ExchangePairRequest, opts ...grpc.CallOption) (*GenericExchangeNameResponse, error) { + out := new(GenericExchangeNameResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/DisableExchangePair", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *goCryptoTraderClient) GetOrderbookStream(ctx context.Context, in *GetOrderbookStreamRequest, opts ...grpc.CallOption) (GoCryptoTrader_GetOrderbookStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &_GoCryptoTrader_serviceDesc.Streams[0], "/gctrpc.GoCryptoTrader/GetOrderbookStream", opts...) + if err != nil { + return nil, err + } + x := &goCryptoTraderGetOrderbookStreamClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type GoCryptoTrader_GetOrderbookStreamClient interface { + Recv() (*OrderbookResponse, error) + grpc.ClientStream +} + +type goCryptoTraderGetOrderbookStreamClient struct { + grpc.ClientStream +} + +func (x *goCryptoTraderGetOrderbookStreamClient) Recv() (*OrderbookResponse, error) { + m := new(OrderbookResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *goCryptoTraderClient) GetExchangeOrderbookStream(ctx context.Context, in *GetExchangeOrderbookStreamRequest, opts ...grpc.CallOption) (GoCryptoTrader_GetExchangeOrderbookStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &_GoCryptoTrader_serviceDesc.Streams[1], "/gctrpc.GoCryptoTrader/GetExchangeOrderbookStream", opts...) + if err != nil { + return nil, err + } + x := &goCryptoTraderGetExchangeOrderbookStreamClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type GoCryptoTrader_GetExchangeOrderbookStreamClient interface { + Recv() (*OrderbookResponse, error) + grpc.ClientStream +} + +type goCryptoTraderGetExchangeOrderbookStreamClient struct { + grpc.ClientStream +} + +func (x *goCryptoTraderGetExchangeOrderbookStreamClient) Recv() (*OrderbookResponse, error) { + m := new(OrderbookResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *goCryptoTraderClient) GetTickerStream(ctx context.Context, in *GetTickerStreamRequest, opts ...grpc.CallOption) (GoCryptoTrader_GetTickerStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &_GoCryptoTrader_serviceDesc.Streams[2], "/gctrpc.GoCryptoTrader/GetTickerStream", opts...) + if err != nil { + return nil, err + } + x := &goCryptoTraderGetTickerStreamClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type GoCryptoTrader_GetTickerStreamClient interface { + Recv() (*TickerResponse, error) + grpc.ClientStream +} + +type goCryptoTraderGetTickerStreamClient struct { + grpc.ClientStream +} + +func (x *goCryptoTraderGetTickerStreamClient) Recv() (*TickerResponse, error) { + m := new(TickerResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *goCryptoTraderClient) GetExchangeTickerStream(ctx context.Context, in *GetExchangeTickerStreamRequest, opts ...grpc.CallOption) (GoCryptoTrader_GetExchangeTickerStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &_GoCryptoTrader_serviceDesc.Streams[3], "/gctrpc.GoCryptoTrader/GetExchangeTickerStream", opts...) + if err != nil { + return nil, err + } + x := &goCryptoTraderGetExchangeTickerStreamClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type GoCryptoTrader_GetExchangeTickerStreamClient interface { + Recv() (*TickerResponse, error) + grpc.ClientStream +} + +type goCryptoTraderGetExchangeTickerStreamClient struct { + grpc.ClientStream +} + +func (x *goCryptoTraderGetExchangeTickerStreamClient) Recv() (*TickerResponse, error) { + m := new(TickerResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *goCryptoTraderClient) GetAuditEvent(ctx context.Context, in *GetAuditEventRequest, opts ...grpc.CallOption) (*GetAuditEventResponse, error) { + out := new(GetAuditEventResponse) + err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTrader/GetAuditEvent", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GoCryptoTraderServer is the server API for GoCryptoTrader service. +type GoCryptoTraderServer interface { + GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) + GetSubsystems(context.Context, *GetSubsystemsRequest) (*GetSusbsytemsResponse, error) + EnableSubsystem(context.Context, *GenericSubsystemRequest) (*GenericSubsystemResponse, error) + DisableSubsystem(context.Context, *GenericSubsystemRequest) (*GenericSubsystemResponse, error) + GetRPCEndpoints(context.Context, *GetRPCEndpointsRequest) (*GetRPCEndpointsResponse, error) + GetCommunicationRelayers(context.Context, *GetCommunicationRelayersRequest) (*GetCommunicationRelayersResponse, error) + GetExchanges(context.Context, *GetExchangesRequest) (*GetExchangesResponse, error) + DisableExchange(context.Context, *GenericExchangeNameRequest) (*GenericExchangeNameResponse, error) + GetExchangeInfo(context.Context, *GenericExchangeNameRequest) (*GetExchangeInfoResponse, error) + GetExchangeOTPCode(context.Context, *GenericExchangeNameRequest) (*GetExchangeOTPReponse, error) + GetExchangeOTPCodes(context.Context, *GetExchangeOTPsRequest) (*GetExchangeOTPsResponse, error) + EnableExchange(context.Context, *GenericExchangeNameRequest) (*GenericExchangeNameResponse, error) + GetTicker(context.Context, *GetTickerRequest) (*TickerResponse, error) + GetTickers(context.Context, *GetTickersRequest) (*GetTickersResponse, error) + GetOrderbook(context.Context, *GetOrderbookRequest) (*OrderbookResponse, error) + GetOrderbooks(context.Context, *GetOrderbooksRequest) (*GetOrderbooksResponse, error) + GetAccountInfo(context.Context, *GetAccountInfoRequest) (*GetAccountInfoResponse, error) + GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) + GetPortfolio(context.Context, *GetPortfolioRequest) (*GetPortfolioResponse, error) + GetPortfolioSummary(context.Context, *GetPortfolioSummaryRequest) (*GetPortfolioSummaryResponse, error) + AddPortfolioAddress(context.Context, *AddPortfolioAddressRequest) (*AddPortfolioAddressResponse, error) + RemovePortfolioAddress(context.Context, *RemovePortfolioAddressRequest) (*RemovePortfolioAddressResponse, error) + GetForexProviders(context.Context, *GetForexProvidersRequest) (*GetForexProvidersResponse, error) + GetForexRates(context.Context, *GetForexRatesRequest) (*GetForexRatesResponse, error) + GetOrders(context.Context, *GetOrdersRequest) (*GetOrdersResponse, error) + GetOrder(context.Context, *GetOrderRequest) (*OrderDetails, error) + SubmitOrder(context.Context, *SubmitOrderRequest) (*SubmitOrderResponse, error) + SimulateOrder(context.Context, *SimulateOrderRequest) (*SimulateOrderResponse, error) + WhaleBomb(context.Context, *WhaleBombRequest) (*SimulateOrderResponse, error) + CancelOrder(context.Context, *CancelOrderRequest) (*CancelOrderResponse, error) + CancelAllOrders(context.Context, *CancelAllOrdersRequest) (*CancelAllOrdersResponse, error) + GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) + AddEvent(context.Context, *AddEventRequest) (*AddEventResponse, error) + RemoveEvent(context.Context, *RemoveEventRequest) (*RemoveEventResponse, error) + GetCryptocurrencyDepositAddresses(context.Context, *GetCryptocurrencyDepositAddressesRequest) (*GetCryptocurrencyDepositAddressesResponse, error) + GetCryptocurrencyDepositAddress(context.Context, *GetCryptocurrencyDepositAddressRequest) (*GetCryptocurrencyDepositAddressResponse, error) + WithdrawCryptocurrencyFunds(context.Context, *WithdrawCurrencyRequest) (*WithdrawResponse, error) + WithdrawFiatFunds(context.Context, *WithdrawCurrencyRequest) (*WithdrawResponse, error) + GetLoggerDetails(context.Context, *GetLoggerDetailsRequest) (*GetLoggerDetailsResponse, error) + SetLoggerDetails(context.Context, *SetLoggerDetailsRequest) (*GetLoggerDetailsResponse, error) + GetExchangePairs(context.Context, *GetExchangePairsRequest) (*GetExchangePairsResponse, error) + EnableExchangePair(context.Context, *ExchangePairRequest) (*GenericExchangeNameResponse, error) + DisableExchangePair(context.Context, *ExchangePairRequest) (*GenericExchangeNameResponse, error) + GetOrderbookStream(*GetOrderbookStreamRequest, GoCryptoTrader_GetOrderbookStreamServer) error + GetExchangeOrderbookStream(*GetExchangeOrderbookStreamRequest, GoCryptoTrader_GetExchangeOrderbookStreamServer) error + GetTickerStream(*GetTickerStreamRequest, GoCryptoTrader_GetTickerStreamServer) error + GetExchangeTickerStream(*GetExchangeTickerStreamRequest, GoCryptoTrader_GetExchangeTickerStreamServer) error + GetAuditEvent(context.Context, *GetAuditEventRequest) (*GetAuditEventResponse, error) +} + +// UnimplementedGoCryptoTraderServer can be embedded to have forward compatible implementations. +type UnimplementedGoCryptoTraderServer struct { +} + +func (*UnimplementedGoCryptoTraderServer) GetInfo(ctx context.Context, req *GetInfoRequest) (*GetInfoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetSubsystems(ctx context.Context, req *GetSubsystemsRequest) (*GetSusbsytemsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSubsystems not implemented") +} +func (*UnimplementedGoCryptoTraderServer) EnableSubsystem(ctx context.Context, req *GenericSubsystemRequest) (*GenericSubsystemResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method EnableSubsystem not implemented") +} +func (*UnimplementedGoCryptoTraderServer) DisableSubsystem(ctx context.Context, req *GenericSubsystemRequest) (*GenericSubsystemResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DisableSubsystem not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetRPCEndpoints(ctx context.Context, req *GetRPCEndpointsRequest) (*GetRPCEndpointsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetRPCEndpoints not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetCommunicationRelayers(ctx context.Context, req *GetCommunicationRelayersRequest) (*GetCommunicationRelayersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCommunicationRelayers not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetExchanges(ctx context.Context, req *GetExchangesRequest) (*GetExchangesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetExchanges not implemented") +} +func (*UnimplementedGoCryptoTraderServer) DisableExchange(ctx context.Context, req *GenericExchangeNameRequest) (*GenericExchangeNameResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DisableExchange not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetExchangeInfo(ctx context.Context, req *GenericExchangeNameRequest) (*GetExchangeInfoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetExchangeInfo not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetExchangeOTPCode(ctx context.Context, req *GenericExchangeNameRequest) (*GetExchangeOTPReponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetExchangeOTPCode not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetExchangeOTPCodes(ctx context.Context, req *GetExchangeOTPsRequest) (*GetExchangeOTPsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetExchangeOTPCodes not implemented") +} +func (*UnimplementedGoCryptoTraderServer) EnableExchange(ctx context.Context, req *GenericExchangeNameRequest) (*GenericExchangeNameResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method EnableExchange not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetTicker(ctx context.Context, req *GetTickerRequest) (*TickerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetTicker not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetTickers(ctx context.Context, req *GetTickersRequest) (*GetTickersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetTickers not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetOrderbook(ctx context.Context, req *GetOrderbookRequest) (*OrderbookResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetOrderbook not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetOrderbooks(ctx context.Context, req *GetOrderbooksRequest) (*GetOrderbooksResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetOrderbooks not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetAccountInfo(ctx context.Context, req *GetAccountInfoRequest) (*GetAccountInfoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAccountInfo not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetConfig(ctx context.Context, req *GetConfigRequest) (*GetConfigResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetPortfolio(ctx context.Context, req *GetPortfolioRequest) (*GetPortfolioResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPortfolio not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetPortfolioSummary(ctx context.Context, req *GetPortfolioSummaryRequest) (*GetPortfolioSummaryResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPortfolioSummary not implemented") +} +func (*UnimplementedGoCryptoTraderServer) AddPortfolioAddress(ctx context.Context, req *AddPortfolioAddressRequest) (*AddPortfolioAddressResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddPortfolioAddress not implemented") +} +func (*UnimplementedGoCryptoTraderServer) RemovePortfolioAddress(ctx context.Context, req *RemovePortfolioAddressRequest) (*RemovePortfolioAddressResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemovePortfolioAddress not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetForexProviders(ctx context.Context, req *GetForexProvidersRequest) (*GetForexProvidersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetForexProviders not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetForexRates(ctx context.Context, req *GetForexRatesRequest) (*GetForexRatesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetForexRates not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetOrders(ctx context.Context, req *GetOrdersRequest) (*GetOrdersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetOrders not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetOrder(ctx context.Context, req *GetOrderRequest) (*OrderDetails, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetOrder not implemented") +} +func (*UnimplementedGoCryptoTraderServer) SubmitOrder(ctx context.Context, req *SubmitOrderRequest) (*SubmitOrderResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SubmitOrder not implemented") +} +func (*UnimplementedGoCryptoTraderServer) SimulateOrder(ctx context.Context, req *SimulateOrderRequest) (*SimulateOrderResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SimulateOrder not implemented") +} +func (*UnimplementedGoCryptoTraderServer) WhaleBomb(ctx context.Context, req *WhaleBombRequest) (*SimulateOrderResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method WhaleBomb not implemented") +} +func (*UnimplementedGoCryptoTraderServer) CancelOrder(ctx context.Context, req *CancelOrderRequest) (*CancelOrderResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CancelOrder not implemented") +} +func (*UnimplementedGoCryptoTraderServer) CancelAllOrders(ctx context.Context, req *CancelAllOrdersRequest) (*CancelAllOrdersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CancelAllOrders not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetEvents(ctx context.Context, req *GetEventsRequest) (*GetEventsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetEvents not implemented") +} +func (*UnimplementedGoCryptoTraderServer) AddEvent(ctx context.Context, req *AddEventRequest) (*AddEventResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddEvent not implemented") +} +func (*UnimplementedGoCryptoTraderServer) RemoveEvent(ctx context.Context, req *RemoveEventRequest) (*RemoveEventResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveEvent not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetCryptocurrencyDepositAddresses(ctx context.Context, req *GetCryptocurrencyDepositAddressesRequest) (*GetCryptocurrencyDepositAddressesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCryptocurrencyDepositAddresses not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetCryptocurrencyDepositAddress(ctx context.Context, req *GetCryptocurrencyDepositAddressRequest) (*GetCryptocurrencyDepositAddressResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCryptocurrencyDepositAddress not implemented") +} +func (*UnimplementedGoCryptoTraderServer) WithdrawCryptocurrencyFunds(ctx context.Context, req *WithdrawCurrencyRequest) (*WithdrawResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method WithdrawCryptocurrencyFunds not implemented") +} +func (*UnimplementedGoCryptoTraderServer) WithdrawFiatFunds(ctx context.Context, req *WithdrawCurrencyRequest) (*WithdrawResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method WithdrawFiatFunds not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetLoggerDetails(ctx context.Context, req *GetLoggerDetailsRequest) (*GetLoggerDetailsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetLoggerDetails not implemented") +} +func (*UnimplementedGoCryptoTraderServer) SetLoggerDetails(ctx context.Context, req *SetLoggerDetailsRequest) (*GetLoggerDetailsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetLoggerDetails not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetExchangePairs(ctx context.Context, req *GetExchangePairsRequest) (*GetExchangePairsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetExchangePairs not implemented") +} +func (*UnimplementedGoCryptoTraderServer) EnableExchangePair(ctx context.Context, req *ExchangePairRequest) (*GenericExchangeNameResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method EnableExchangePair not implemented") +} +func (*UnimplementedGoCryptoTraderServer) DisableExchangePair(ctx context.Context, req *ExchangePairRequest) (*GenericExchangeNameResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DisableExchangePair not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetOrderbookStream(req *GetOrderbookStreamRequest, srv GoCryptoTrader_GetOrderbookStreamServer) error { + return status.Errorf(codes.Unimplemented, "method GetOrderbookStream not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetExchangeOrderbookStream(req *GetExchangeOrderbookStreamRequest, srv GoCryptoTrader_GetExchangeOrderbookStreamServer) error { + return status.Errorf(codes.Unimplemented, "method GetExchangeOrderbookStream not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetTickerStream(req *GetTickerStreamRequest, srv GoCryptoTrader_GetTickerStreamServer) error { + return status.Errorf(codes.Unimplemented, "method GetTickerStream not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetExchangeTickerStream(req *GetExchangeTickerStreamRequest, srv GoCryptoTrader_GetExchangeTickerStreamServer) error { + return status.Errorf(codes.Unimplemented, "method GetExchangeTickerStream not implemented") +} +func (*UnimplementedGoCryptoTraderServer) GetAuditEvent(ctx context.Context, req *GetAuditEventRequest) (*GetAuditEventResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAuditEvent not implemented") +} + +func RegisterGoCryptoTraderServer(s *grpc.Server, srv GoCryptoTraderServer) { + s.RegisterService(&_GoCryptoTrader_serviceDesc, srv) +} + +func _GoCryptoTrader_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetInfoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetInfo", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetInfo(ctx, req.(*GetInfoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetSubsystems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSubsystemsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetSubsystems(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetSubsystems", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetSubsystems(ctx, req.(*GetSubsystemsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_EnableSubsystem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GenericSubsystemRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).EnableSubsystem(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/EnableSubsystem", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).EnableSubsystem(ctx, req.(*GenericSubsystemRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_DisableSubsystem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GenericSubsystemRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).DisableSubsystem(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/DisableSubsystem", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).DisableSubsystem(ctx, req.(*GenericSubsystemRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetRPCEndpoints_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRPCEndpointsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetRPCEndpoints(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetRPCEndpoints", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetRPCEndpoints(ctx, req.(*GetRPCEndpointsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetCommunicationRelayers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetCommunicationRelayersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetCommunicationRelayers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetCommunicationRelayers", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetCommunicationRelayers(ctx, req.(*GetCommunicationRelayersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetExchanges_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetExchangesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetExchanges(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetExchanges", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetExchanges(ctx, req.(*GetExchangesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_DisableExchange_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GenericExchangeNameRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).DisableExchange(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/DisableExchange", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).DisableExchange(ctx, req.(*GenericExchangeNameRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetExchangeInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GenericExchangeNameRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetExchangeInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetExchangeInfo", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetExchangeInfo(ctx, req.(*GenericExchangeNameRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetExchangeOTPCode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GenericExchangeNameRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetExchangeOTPCode(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetExchangeOTPCode", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetExchangeOTPCode(ctx, req.(*GenericExchangeNameRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetExchangeOTPCodes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetExchangeOTPsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetExchangeOTPCodes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetExchangeOTPCodes", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetExchangeOTPCodes(ctx, req.(*GetExchangeOTPsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_EnableExchange_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GenericExchangeNameRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).EnableExchange(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/EnableExchange", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).EnableExchange(ctx, req.(*GenericExchangeNameRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetTicker_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTickerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetTicker(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetTicker", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetTicker(ctx, req.(*GetTickerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetTickers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTickersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetTickers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetTickers", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetTickers(ctx, req.(*GetTickersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetOrderbook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetOrderbookRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetOrderbook(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetOrderbook", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetOrderbook(ctx, req.(*GetOrderbookRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetOrderbooks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetOrderbooksRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetOrderbooks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetOrderbooks", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetOrderbooks(ctx, req.(*GetOrderbooksRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetAccountInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAccountInfoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetAccountInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetAccountInfo", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetAccountInfo(ctx, req.(*GetAccountInfoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetConfig", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetConfig(ctx, req.(*GetConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetPortfolio_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPortfolioRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetPortfolio(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetPortfolio", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetPortfolio(ctx, req.(*GetPortfolioRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetPortfolioSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPortfolioSummaryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetPortfolioSummary(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetPortfolioSummary", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetPortfolioSummary(ctx, req.(*GetPortfolioSummaryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_AddPortfolioAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddPortfolioAddressRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).AddPortfolioAddress(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/AddPortfolioAddress", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).AddPortfolioAddress(ctx, req.(*AddPortfolioAddressRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_RemovePortfolioAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemovePortfolioAddressRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).RemovePortfolioAddress(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/RemovePortfolioAddress", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).RemovePortfolioAddress(ctx, req.(*RemovePortfolioAddressRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetForexProviders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetForexProvidersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetForexProviders(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetForexProviders", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetForexProviders(ctx, req.(*GetForexProvidersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetForexRates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetForexRatesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetForexRates(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetForexRates", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetForexRates(ctx, req.(*GetForexRatesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetOrders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetOrdersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetOrders(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetOrders", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetOrders(ctx, req.(*GetOrdersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetOrderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetOrder(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetOrder", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetOrder(ctx, req.(*GetOrderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_SubmitOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SubmitOrderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).SubmitOrder(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/SubmitOrder", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).SubmitOrder(ctx, req.(*SubmitOrderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_SimulateOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SimulateOrderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).SimulateOrder(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/SimulateOrder", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).SimulateOrder(ctx, req.(*SimulateOrderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_WhaleBomb_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WhaleBombRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).WhaleBomb(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/WhaleBomb", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).WhaleBomb(ctx, req.(*WhaleBombRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_CancelOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CancelOrderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).CancelOrder(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/CancelOrder", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).CancelOrder(ctx, req.(*CancelOrderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_CancelAllOrders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CancelAllOrdersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).CancelAllOrders(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/CancelAllOrders", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).CancelAllOrders(ctx, req.(*CancelAllOrdersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetEventsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetEvents(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetEvents", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetEvents(ctx, req.(*GetEventsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_AddEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddEventRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).AddEvent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/AddEvent", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).AddEvent(ctx, req.(*AddEventRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_RemoveEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveEventRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).RemoveEvent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/RemoveEvent", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).RemoveEvent(ctx, req.(*RemoveEventRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetCryptocurrencyDepositAddresses_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetCryptocurrencyDepositAddressesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetCryptocurrencyDepositAddresses(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetCryptocurrencyDepositAddresses", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetCryptocurrencyDepositAddresses(ctx, req.(*GetCryptocurrencyDepositAddressesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetCryptocurrencyDepositAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetCryptocurrencyDepositAddressRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetCryptocurrencyDepositAddress(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetCryptocurrencyDepositAddress", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetCryptocurrencyDepositAddress(ctx, req.(*GetCryptocurrencyDepositAddressRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_WithdrawCryptocurrencyFunds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WithdrawCurrencyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).WithdrawCryptocurrencyFunds(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/WithdrawCryptocurrencyFunds", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).WithdrawCryptocurrencyFunds(ctx, req.(*WithdrawCurrencyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_WithdrawFiatFunds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WithdrawCurrencyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).WithdrawFiatFunds(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/WithdrawFiatFunds", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).WithdrawFiatFunds(ctx, req.(*WithdrawCurrencyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetLoggerDetails_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetLoggerDetailsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetLoggerDetails(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetLoggerDetails", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetLoggerDetails(ctx, req.(*GetLoggerDetailsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_SetLoggerDetails_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetLoggerDetailsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).SetLoggerDetails(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/SetLoggerDetails", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).SetLoggerDetails(ctx, req.(*SetLoggerDetailsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetExchangePairs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetExchangePairsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetExchangePairs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetExchangePairs", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetExchangePairs(ctx, req.(*GetExchangePairsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_EnableExchangePair_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExchangePairRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).EnableExchangePair(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/EnableExchangePair", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).EnableExchangePair(ctx, req.(*ExchangePairRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_DisableExchangePair_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExchangePairRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).DisableExchangePair(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/DisableExchangePair", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).DisableExchangePair(ctx, req.(*ExchangePairRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GoCryptoTrader_GetOrderbookStream_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetOrderbookStreamRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(GoCryptoTraderServer).GetOrderbookStream(m, &goCryptoTraderGetOrderbookStreamServer{stream}) +} + +type GoCryptoTrader_GetOrderbookStreamServer interface { + Send(*OrderbookResponse) error + grpc.ServerStream +} + +type goCryptoTraderGetOrderbookStreamServer struct { + grpc.ServerStream +} + +func (x *goCryptoTraderGetOrderbookStreamServer) Send(m *OrderbookResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _GoCryptoTrader_GetExchangeOrderbookStream_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetExchangeOrderbookStreamRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(GoCryptoTraderServer).GetExchangeOrderbookStream(m, &goCryptoTraderGetExchangeOrderbookStreamServer{stream}) +} + +type GoCryptoTrader_GetExchangeOrderbookStreamServer interface { + Send(*OrderbookResponse) error + grpc.ServerStream +} + +type goCryptoTraderGetExchangeOrderbookStreamServer struct { + grpc.ServerStream +} + +func (x *goCryptoTraderGetExchangeOrderbookStreamServer) Send(m *OrderbookResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _GoCryptoTrader_GetTickerStream_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetTickerStreamRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(GoCryptoTraderServer).GetTickerStream(m, &goCryptoTraderGetTickerStreamServer{stream}) +} + +type GoCryptoTrader_GetTickerStreamServer interface { + Send(*TickerResponse) error + grpc.ServerStream +} + +type goCryptoTraderGetTickerStreamServer struct { + grpc.ServerStream +} + +func (x *goCryptoTraderGetTickerStreamServer) Send(m *TickerResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _GoCryptoTrader_GetExchangeTickerStream_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetExchangeTickerStreamRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(GoCryptoTraderServer).GetExchangeTickerStream(m, &goCryptoTraderGetExchangeTickerStreamServer{stream}) +} + +type GoCryptoTrader_GetExchangeTickerStreamServer interface { + Send(*TickerResponse) error + grpc.ServerStream +} + +type goCryptoTraderGetExchangeTickerStreamServer struct { + grpc.ServerStream +} + +func (x *goCryptoTraderGetExchangeTickerStreamServer) Send(m *TickerResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _GoCryptoTrader_GetAuditEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAuditEventRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GoCryptoTraderServer).GetAuditEvent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gctrpc.GoCryptoTrader/GetAuditEvent", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GoCryptoTraderServer).GetAuditEvent(ctx, req.(*GetAuditEventRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _GoCryptoTrader_serviceDesc = grpc.ServiceDesc{ + ServiceName: "gctrpc.GoCryptoTrader", + HandlerType: (*GoCryptoTraderServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetInfo", + Handler: _GoCryptoTrader_GetInfo_Handler, + }, + { + MethodName: "GetSubsystems", + Handler: _GoCryptoTrader_GetSubsystems_Handler, + }, + { + MethodName: "EnableSubsystem", + Handler: _GoCryptoTrader_EnableSubsystem_Handler, + }, + { + MethodName: "DisableSubsystem", + Handler: _GoCryptoTrader_DisableSubsystem_Handler, + }, + { + MethodName: "GetRPCEndpoints", + Handler: _GoCryptoTrader_GetRPCEndpoints_Handler, + }, + { + MethodName: "GetCommunicationRelayers", + Handler: _GoCryptoTrader_GetCommunicationRelayers_Handler, + }, + { + MethodName: "GetExchanges", + Handler: _GoCryptoTrader_GetExchanges_Handler, + }, + { + MethodName: "DisableExchange", + Handler: _GoCryptoTrader_DisableExchange_Handler, + }, + { + MethodName: "GetExchangeInfo", + Handler: _GoCryptoTrader_GetExchangeInfo_Handler, + }, + { + MethodName: "GetExchangeOTPCode", + Handler: _GoCryptoTrader_GetExchangeOTPCode_Handler, + }, + { + MethodName: "GetExchangeOTPCodes", + Handler: _GoCryptoTrader_GetExchangeOTPCodes_Handler, + }, + { + MethodName: "EnableExchange", + Handler: _GoCryptoTrader_EnableExchange_Handler, + }, + { + MethodName: "GetTicker", + Handler: _GoCryptoTrader_GetTicker_Handler, + }, + { + MethodName: "GetTickers", + Handler: _GoCryptoTrader_GetTickers_Handler, + }, + { + MethodName: "GetOrderbook", + Handler: _GoCryptoTrader_GetOrderbook_Handler, + }, + { + MethodName: "GetOrderbooks", + Handler: _GoCryptoTrader_GetOrderbooks_Handler, + }, + { + MethodName: "GetAccountInfo", + Handler: _GoCryptoTrader_GetAccountInfo_Handler, + }, + { + MethodName: "GetConfig", + Handler: _GoCryptoTrader_GetConfig_Handler, + }, + { + MethodName: "GetPortfolio", + Handler: _GoCryptoTrader_GetPortfolio_Handler, + }, + { + MethodName: "GetPortfolioSummary", + Handler: _GoCryptoTrader_GetPortfolioSummary_Handler, + }, + { + MethodName: "AddPortfolioAddress", + Handler: _GoCryptoTrader_AddPortfolioAddress_Handler, + }, + { + MethodName: "RemovePortfolioAddress", + Handler: _GoCryptoTrader_RemovePortfolioAddress_Handler, + }, + { + MethodName: "GetForexProviders", + Handler: _GoCryptoTrader_GetForexProviders_Handler, + }, + { + MethodName: "GetForexRates", + Handler: _GoCryptoTrader_GetForexRates_Handler, + }, + { + MethodName: "GetOrders", + Handler: _GoCryptoTrader_GetOrders_Handler, + }, + { + MethodName: "GetOrder", + Handler: _GoCryptoTrader_GetOrder_Handler, + }, + { + MethodName: "SubmitOrder", + Handler: _GoCryptoTrader_SubmitOrder_Handler, + }, + { + MethodName: "SimulateOrder", + Handler: _GoCryptoTrader_SimulateOrder_Handler, + }, + { + MethodName: "WhaleBomb", + Handler: _GoCryptoTrader_WhaleBomb_Handler, + }, + { + MethodName: "CancelOrder", + Handler: _GoCryptoTrader_CancelOrder_Handler, + }, + { + MethodName: "CancelAllOrders", + Handler: _GoCryptoTrader_CancelAllOrders_Handler, + }, + { + MethodName: "GetEvents", + Handler: _GoCryptoTrader_GetEvents_Handler, + }, + { + MethodName: "AddEvent", + Handler: _GoCryptoTrader_AddEvent_Handler, + }, + { + MethodName: "RemoveEvent", + Handler: _GoCryptoTrader_RemoveEvent_Handler, + }, + { + MethodName: "GetCryptocurrencyDepositAddresses", + Handler: _GoCryptoTrader_GetCryptocurrencyDepositAddresses_Handler, + }, + { + MethodName: "GetCryptocurrencyDepositAddress", + Handler: _GoCryptoTrader_GetCryptocurrencyDepositAddress_Handler, + }, + { + MethodName: "WithdrawCryptocurrencyFunds", + Handler: _GoCryptoTrader_WithdrawCryptocurrencyFunds_Handler, + }, + { + MethodName: "WithdrawFiatFunds", + Handler: _GoCryptoTrader_WithdrawFiatFunds_Handler, + }, + { + MethodName: "GetLoggerDetails", + Handler: _GoCryptoTrader_GetLoggerDetails_Handler, + }, + { + MethodName: "SetLoggerDetails", + Handler: _GoCryptoTrader_SetLoggerDetails_Handler, + }, + { + MethodName: "GetExchangePairs", + Handler: _GoCryptoTrader_GetExchangePairs_Handler, + }, + { + MethodName: "EnableExchangePair", + Handler: _GoCryptoTrader_EnableExchangePair_Handler, + }, + { + MethodName: "DisableExchangePair", + Handler: _GoCryptoTrader_DisableExchangePair_Handler, + }, + { + MethodName: "GetAuditEvent", + Handler: _GoCryptoTrader_GetAuditEvent_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "GetOrderbookStream", + Handler: _GoCryptoTrader_GetOrderbookStream_Handler, + ServerStreams: true, + }, + { + StreamName: "GetExchangeOrderbookStream", + Handler: _GoCryptoTrader_GetExchangeOrderbookStream_Handler, + ServerStreams: true, + }, + { + StreamName: "GetTickerStream", + Handler: _GoCryptoTrader_GetTickerStream_Handler, + ServerStreams: true, + }, + { + StreamName: "GetExchangeTickerStream", + Handler: _GoCryptoTrader_GetExchangeTickerStream_Handler, + ServerStreams: true, + }, + }, + Metadata: "rpc.proto", +} diff --git a/gctrpc/rpc.pb.gw.go b/gctrpc/rpc.pb.gw.go new file mode 100644 index 00000000..6788ff42 --- /dev/null +++ b/gctrpc/rpc.pb.gw.go @@ -0,0 +1,3537 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: rpc.proto + +/* +Package gctrpc is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package gctrpc + +import ( + "context" + "io" + "net/http" + + "github.com/golang/protobuf/descriptor" + "github.com/golang/protobuf/proto" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/grpc-ecosystem/grpc-gateway/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/status" +) + +// Suppress "imported and not used" errors +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray +var _ = descriptor.ForMessage + +func request_GoCryptoTrader_GetInfo_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetInfoRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetInfo_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetInfoRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetInfo(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetSubsystems_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetSubsystemsRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetSubsystems(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetSubsystems_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetSubsystemsRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetSubsystems(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_GoCryptoTrader_EnableSubsystem_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_EnableSubsystem_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericSubsystemRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_EnableSubsystem_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.EnableSubsystem(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_EnableSubsystem_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericSubsystemRequest + var metadata runtime.ServerMetadata + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GoCryptoTrader_EnableSubsystem_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.EnableSubsystem(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_GoCryptoTrader_DisableSubsystem_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_DisableSubsystem_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericSubsystemRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_DisableSubsystem_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.DisableSubsystem(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_DisableSubsystem_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericSubsystemRequest + var metadata runtime.ServerMetadata + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GoCryptoTrader_DisableSubsystem_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.DisableSubsystem(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetRPCEndpoints_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetRPCEndpointsRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetRPCEndpoints(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetRPCEndpoints_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetRPCEndpointsRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetRPCEndpoints(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetCommunicationRelayers_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetCommunicationRelayersRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetCommunicationRelayers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetCommunicationRelayers_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetCommunicationRelayersRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetCommunicationRelayers(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_GoCryptoTrader_GetExchanges_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_GetExchanges_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetExchangesRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_GetExchanges_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetExchanges(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetExchanges_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetExchangesRequest + var metadata runtime.ServerMetadata + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GoCryptoTrader_GetExchanges_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetExchanges(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_DisableExchange_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericExchangeNameRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.DisableExchange(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_DisableExchange_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericExchangeNameRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.DisableExchange(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_GoCryptoTrader_GetExchangeInfo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_GetExchangeInfo_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericExchangeNameRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_GetExchangeInfo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetExchangeInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetExchangeInfo_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericExchangeNameRequest + var metadata runtime.ServerMetadata + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GoCryptoTrader_GetExchangeInfo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetExchangeInfo(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_GoCryptoTrader_GetExchangeOTPCode_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_GetExchangeOTPCode_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericExchangeNameRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_GetExchangeOTPCode_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetExchangeOTPCode(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetExchangeOTPCode_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericExchangeNameRequest + var metadata runtime.ServerMetadata + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GoCryptoTrader_GetExchangeOTPCode_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetExchangeOTPCode(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetExchangeOTPCodes_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetExchangeOTPsRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetExchangeOTPCodes(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetExchangeOTPCodes_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetExchangeOTPsRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetExchangeOTPCodes(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_EnableExchange_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericExchangeNameRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.EnableExchange(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_EnableExchange_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GenericExchangeNameRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.EnableExchange(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetTicker_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetTickerRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetTicker(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetTicker_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetTickerRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetTicker(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetTickers_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetTickersRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetTickers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetTickers_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetTickersRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetTickers(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetOrderbook_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetOrderbookRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetOrderbook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetOrderbook_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetOrderbookRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetOrderbook(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetOrderbooks_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetOrderbooksRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetOrderbooks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetOrderbooks_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetOrderbooksRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetOrderbooks(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_GoCryptoTrader_GetAccountInfo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_GetAccountInfo_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAccountInfoRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_GetAccountInfo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetAccountInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetAccountInfo_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAccountInfoRequest + var metadata runtime.ServerMetadata + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GoCryptoTrader_GetAccountInfo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetAccountInfo(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetConfig_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetConfigRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetConfig(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetConfig_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetConfigRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetConfig(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetPortfolio_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetPortfolioRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetPortfolio(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetPortfolio_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetPortfolioRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetPortfolio(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetPortfolioSummary_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetPortfolioSummaryRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetPortfolioSummary(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetPortfolioSummary_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetPortfolioSummaryRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetPortfolioSummary(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_AddPortfolioAddress_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddPortfolioAddressRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.AddPortfolioAddress(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_AddPortfolioAddress_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddPortfolioAddressRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.AddPortfolioAddress(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_RemovePortfolioAddress_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq RemovePortfolioAddressRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.RemovePortfolioAddress(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_RemovePortfolioAddress_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq RemovePortfolioAddressRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.RemovePortfolioAddress(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetForexProviders_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetForexProvidersRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetForexProviders(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetForexProviders_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetForexProvidersRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetForexProviders(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetForexRates_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetForexRatesRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetForexRates(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetForexRates_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetForexRatesRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetForexRates(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetOrders_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetOrdersRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetOrders(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetOrders_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetOrdersRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetOrders(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetOrder_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetOrder(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetOrder_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetOrder(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_SubmitOrder_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SubmitOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.SubmitOrder(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_SubmitOrder_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SubmitOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.SubmitOrder(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_SimulateOrder_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SimulateOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.SimulateOrder(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_SimulateOrder_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SimulateOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.SimulateOrder(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_WhaleBomb_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq WhaleBombRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.WhaleBomb(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_WhaleBomb_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq WhaleBombRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.WhaleBomb(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_CancelOrder_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CancelOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.CancelOrder(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_CancelOrder_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CancelOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.CancelOrder(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_CancelAllOrders_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CancelAllOrdersRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.CancelAllOrders(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_CancelAllOrders_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CancelAllOrdersRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.CancelAllOrders(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetEvents_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetEventsRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetEvents(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetEvents_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetEventsRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetEvents(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_AddEvent_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddEventRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.AddEvent(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_AddEvent_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddEventRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.AddEvent(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_RemoveEvent_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq RemoveEventRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.RemoveEvent(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_RemoveEvent_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq RemoveEventRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.RemoveEvent(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetCryptocurrencyDepositAddresses_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetCryptocurrencyDepositAddressesRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetCryptocurrencyDepositAddresses(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetCryptocurrencyDepositAddresses_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetCryptocurrencyDepositAddressesRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetCryptocurrencyDepositAddresses(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetCryptocurrencyDepositAddress_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetCryptocurrencyDepositAddressRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetCryptocurrencyDepositAddress(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetCryptocurrencyDepositAddress_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetCryptocurrencyDepositAddressRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetCryptocurrencyDepositAddress(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_WithdrawCryptocurrencyFunds_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq WithdrawCurrencyRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.WithdrawCryptocurrencyFunds(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_WithdrawCryptocurrencyFunds_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq WithdrawCurrencyRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.WithdrawCryptocurrencyFunds(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_WithdrawFiatFunds_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq WithdrawCurrencyRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.WithdrawFiatFunds(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_WithdrawFiatFunds_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq WithdrawCurrencyRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.WithdrawFiatFunds(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_GoCryptoTrader_GetLoggerDetails_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_GetLoggerDetails_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetLoggerDetailsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_GetLoggerDetails_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetLoggerDetails(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetLoggerDetails_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetLoggerDetailsRequest + var metadata runtime.ServerMetadata + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GoCryptoTrader_GetLoggerDetails_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetLoggerDetails(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_SetLoggerDetails_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SetLoggerDetailsRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.SetLoggerDetails(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_SetLoggerDetails_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SetLoggerDetailsRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.SetLoggerDetails(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_GetExchangePairs_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetExchangePairsRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetExchangePairs(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetExchangePairs_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetExchangePairsRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetExchangePairs(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_EnableExchangePair_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ExchangePairRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.EnableExchangePair(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_EnableExchangePair_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ExchangePairRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.EnableExchangePair(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GoCryptoTrader_DisableExchangePair_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ExchangePairRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.DisableExchangePair(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_DisableExchangePair_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ExchangePairRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.DisableExchangePair(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_GoCryptoTrader_GetOrderbookStream_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_GetOrderbookStream_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (GoCryptoTrader_GetOrderbookStreamClient, runtime.ServerMetadata, error) { + var protoReq GetOrderbookStreamRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_GetOrderbookStream_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.GetOrderbookStream(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +var ( + filter_GoCryptoTrader_GetExchangeOrderbookStream_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_GetExchangeOrderbookStream_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (GoCryptoTrader_GetExchangeOrderbookStreamClient, runtime.ServerMetadata, error) { + var protoReq GetExchangeOrderbookStreamRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_GetExchangeOrderbookStream_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.GetExchangeOrderbookStream(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +var ( + filter_GoCryptoTrader_GetTickerStream_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_GetTickerStream_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (GoCryptoTrader_GetTickerStreamClient, runtime.ServerMetadata, error) { + var protoReq GetTickerStreamRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_GetTickerStream_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.GetTickerStream(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +var ( + filter_GoCryptoTrader_GetExchangeTickerStream_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_GetExchangeTickerStream_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (GoCryptoTrader_GetExchangeTickerStreamClient, runtime.ServerMetadata, error) { + var protoReq GetExchangeTickerStreamRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_GetExchangeTickerStream_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.GetExchangeTickerStream(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +var ( + filter_GoCryptoTrader_GetAuditEvent_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_GoCryptoTrader_GetAuditEvent_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAuditEventRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTrader_GetAuditEvent_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetAuditEvent(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GoCryptoTrader_GetAuditEvent_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAuditEventRequest + var metadata runtime.ServerMetadata + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GoCryptoTrader_GetAuditEvent_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetAuditEvent(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterGoCryptoTraderHandlerServer registers the http handlers for service GoCryptoTrader to "mux". +// UnaryRPC :call GoCryptoTraderServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +func RegisterGoCryptoTraderHandlerServer(ctx context.Context, mux *runtime.ServeMux, server GoCryptoTraderServer) error { + + mux.Handle("GET", pattern_GoCryptoTrader_GetInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetInfo_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetSubsystems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetSubsystems_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetSubsystems_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_EnableSubsystem_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_EnableSubsystem_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_EnableSubsystem_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_DisableSubsystem_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_DisableSubsystem_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_DisableSubsystem_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetRPCEndpoints_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetRPCEndpoints_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetRPCEndpoints_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetCommunicationRelayers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetCommunicationRelayers_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetCommunicationRelayers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchanges_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetExchanges_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchanges_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_DisableExchange_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_DisableExchange_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_DisableExchange_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchangeInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetExchangeInfo_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchangeInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchangeOTPCode_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetExchangeOTPCode_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchangeOTPCode_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchangeOTPCodes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetExchangeOTPCodes_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchangeOTPCodes_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_EnableExchange_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_EnableExchange_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_EnableExchange_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetTicker_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetTicker_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetTicker_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetTickers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetTickers_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetTickers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetOrderbook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetOrderbook_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetOrderbook_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetOrderbooks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetOrderbooks_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetOrderbooks_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetAccountInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetAccountInfo_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetAccountInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetConfig_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetConfig_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetPortfolio_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetPortfolio_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetPortfolio_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetPortfolioSummary_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetPortfolioSummary_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetPortfolioSummary_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_AddPortfolioAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_AddPortfolioAddress_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_AddPortfolioAddress_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_RemovePortfolioAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_RemovePortfolioAddress_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_RemovePortfolioAddress_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetForexProviders_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetForexProviders_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetForexProviders_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetForexRates_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetForexRates_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetForexRates_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetOrders_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetOrders_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetOrders_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetOrder_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetOrder_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetOrder_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_SubmitOrder_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_SubmitOrder_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_SubmitOrder_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_SimulateOrder_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_SimulateOrder_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_SimulateOrder_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_WhaleBomb_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_WhaleBomb_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_WhaleBomb_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_CancelOrder_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_CancelOrder_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_CancelOrder_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_CancelAllOrders_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_CancelAllOrders_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_CancelAllOrders_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetEvents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetEvents_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetEvents_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_AddEvent_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_AddEvent_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_AddEvent_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_RemoveEvent_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_RemoveEvent_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_RemoveEvent_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetCryptocurrencyDepositAddresses_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetCryptocurrencyDepositAddresses_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetCryptocurrencyDepositAddresses_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetCryptocurrencyDepositAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetCryptocurrencyDepositAddress_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetCryptocurrencyDepositAddress_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_WithdrawCryptocurrencyFunds_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_WithdrawCryptocurrencyFunds_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_WithdrawCryptocurrencyFunds_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_WithdrawFiatFunds_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_WithdrawFiatFunds_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_WithdrawFiatFunds_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetLoggerDetails_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetLoggerDetails_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetLoggerDetails_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_SetLoggerDetails_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_SetLoggerDetails_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_SetLoggerDetails_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetExchangePairs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetExchangePairs_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchangePairs_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_EnableExchangePair_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_EnableExchangePair_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_EnableExchangePair_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_DisableExchangePair_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_DisableExchangePair_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_DisableExchangePair_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetOrderbookStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchangeOrderbookStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetTickerStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchangeTickerStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetAuditEvent_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GoCryptoTrader_GetAuditEvent_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetAuditEvent_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +// RegisterGoCryptoTraderHandlerFromEndpoint is same as RegisterGoCryptoTraderHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterGoCryptoTraderHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.Dial(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterGoCryptoTraderHandler(ctx, mux, conn) +} + +// RegisterGoCryptoTraderHandler registers the http handlers for service GoCryptoTrader to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterGoCryptoTraderHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterGoCryptoTraderHandlerClient(ctx, mux, NewGoCryptoTraderClient(conn)) +} + +// RegisterGoCryptoTraderHandlerClient registers the http handlers for service GoCryptoTrader +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "GoCryptoTraderClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "GoCryptoTraderClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "GoCryptoTraderClient" to call the correct interceptors. +func RegisterGoCryptoTraderHandlerClient(ctx context.Context, mux *runtime.ServeMux, client GoCryptoTraderClient) error { + + mux.Handle("GET", pattern_GoCryptoTrader_GetInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetInfo_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetSubsystems_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetSubsystems_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetSubsystems_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_EnableSubsystem_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_EnableSubsystem_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_EnableSubsystem_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_DisableSubsystem_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_DisableSubsystem_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_DisableSubsystem_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetRPCEndpoints_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetRPCEndpoints_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetRPCEndpoints_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetCommunicationRelayers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetCommunicationRelayers_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetCommunicationRelayers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchanges_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetExchanges_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchanges_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_DisableExchange_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_DisableExchange_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_DisableExchange_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchangeInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetExchangeInfo_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchangeInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchangeOTPCode_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetExchangeOTPCode_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchangeOTPCode_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchangeOTPCodes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetExchangeOTPCodes_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchangeOTPCodes_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_EnableExchange_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_EnableExchange_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_EnableExchange_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetTicker_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetTicker_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetTicker_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetTickers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetTickers_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetTickers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetOrderbook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetOrderbook_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetOrderbook_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetOrderbooks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetOrderbooks_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetOrderbooks_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetAccountInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetAccountInfo_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetAccountInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetConfig_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetConfig_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetPortfolio_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetPortfolio_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetPortfolio_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetPortfolioSummary_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetPortfolioSummary_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetPortfolioSummary_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_AddPortfolioAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_AddPortfolioAddress_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_AddPortfolioAddress_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_RemovePortfolioAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_RemovePortfolioAddress_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_RemovePortfolioAddress_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetForexProviders_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetForexProviders_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetForexProviders_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetForexRates_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetForexRates_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetForexRates_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetOrders_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetOrders_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetOrders_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetOrder_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetOrder_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetOrder_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_SubmitOrder_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_SubmitOrder_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_SubmitOrder_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_SimulateOrder_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_SimulateOrder_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_SimulateOrder_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_WhaleBomb_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_WhaleBomb_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_WhaleBomb_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_CancelOrder_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_CancelOrder_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_CancelOrder_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_CancelAllOrders_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_CancelAllOrders_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_CancelAllOrders_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetEvents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetEvents_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetEvents_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_AddEvent_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_AddEvent_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_AddEvent_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_RemoveEvent_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_RemoveEvent_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_RemoveEvent_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetCryptocurrencyDepositAddresses_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetCryptocurrencyDepositAddresses_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetCryptocurrencyDepositAddresses_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetCryptocurrencyDepositAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetCryptocurrencyDepositAddress_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetCryptocurrencyDepositAddress_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_WithdrawCryptocurrencyFunds_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_WithdrawCryptocurrencyFunds_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_WithdrawCryptocurrencyFunds_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_WithdrawFiatFunds_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_WithdrawFiatFunds_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_WithdrawFiatFunds_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetLoggerDetails_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetLoggerDetails_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetLoggerDetails_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_SetLoggerDetails_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_SetLoggerDetails_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_SetLoggerDetails_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_GetExchangePairs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetExchangePairs_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchangePairs_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_EnableExchangePair_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_EnableExchangePair_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_EnableExchangePair_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GoCryptoTrader_DisableExchangePair_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_DisableExchangePair_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_DisableExchangePair_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetOrderbookStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetOrderbookStream_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetOrderbookStream_0(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchangeOrderbookStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetExchangeOrderbookStream_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchangeOrderbookStream_0(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetTickerStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetTickerStream_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetTickerStream_0(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetExchangeTickerStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetExchangeTickerStream_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetExchangeTickerStream_0(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_GoCryptoTrader_GetAuditEvent_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GoCryptoTrader_GetAuditEvent_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_GoCryptoTrader_GetAuditEvent_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_GoCryptoTrader_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getinfo"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetSubsystems_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getsubsystems"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_EnableSubsystem_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "enablesubsystem"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_DisableSubsystem_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "disablesubsystem"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetRPCEndpoints_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getrpcendpoints"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetCommunicationRelayers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getcommunicationrelayers"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetExchanges_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getexchanges"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_DisableExchange_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "disableexchange"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetExchangeInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getexchangeinfo"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetExchangeOTPCode_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getexchangeotp"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetExchangeOTPCodes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getexchangeotps"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_EnableExchange_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "enableexchange"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetTicker_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getticker"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetTickers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "gettickers"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetOrderbook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getorderbook"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetOrderbooks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getorderbooks"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetAccountInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getaccountinfo"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetConfig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getconfig"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetPortfolio_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getportfolio"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetPortfolioSummary_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getportfoliosummary"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_AddPortfolioAddress_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "addportfolioaddress"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_RemovePortfolioAddress_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "removeportfolioaddress"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetForexProviders_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getforexproviders"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetForexRates_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getforexrates"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetOrders_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getorders"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetOrder_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getorder"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_SubmitOrder_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "submitorder"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_SimulateOrder_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "simulateorder"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_WhaleBomb_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "whalebomb"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_CancelOrder_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "cancelorder"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_CancelAllOrders_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "cancelallorders"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetEvents_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getevents"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_AddEvent_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "addevent"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_RemoveEvent_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "removeevent"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetCryptocurrencyDepositAddresses_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getcryptodepositaddresses"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetCryptocurrencyDepositAddress_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getcryptodepositaddress"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_WithdrawCryptocurrencyFunds_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "withdrawcryptofunds"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_WithdrawFiatFunds_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "withdrawfiatfunds"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetLoggerDetails_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getloggerdetails"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_SetLoggerDetails_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "setloggerdetails"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetExchangePairs_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getexchangepairs"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_EnableExchangePair_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "enableexchangepair"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_DisableExchangePair_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "disableexchangepair"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetOrderbookStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getorderbookstream"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetExchangeOrderbookStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getexchangeorderbookstream"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetTickerStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "gettickerstream"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetExchangeTickerStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getexchangetickerstream"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_GoCryptoTrader_GetAuditEvent_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getauditevent"}, "", runtime.AssumeColonVerbOpt(true))) +) + +var ( + forward_GoCryptoTrader_GetInfo_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetSubsystems_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_EnableSubsystem_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_DisableSubsystem_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetRPCEndpoints_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetCommunicationRelayers_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetExchanges_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_DisableExchange_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetExchangeInfo_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetExchangeOTPCode_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetExchangeOTPCodes_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_EnableExchange_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetTicker_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetTickers_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetOrderbook_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetOrderbooks_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetAccountInfo_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetConfig_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetPortfolio_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetPortfolioSummary_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_AddPortfolioAddress_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_RemovePortfolioAddress_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetForexProviders_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetForexRates_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetOrders_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetOrder_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_SubmitOrder_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_SimulateOrder_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_WhaleBomb_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_CancelOrder_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_CancelAllOrders_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetEvents_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_AddEvent_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_RemoveEvent_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetCryptocurrencyDepositAddresses_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetCryptocurrencyDepositAddress_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_WithdrawCryptocurrencyFunds_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_WithdrawFiatFunds_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetLoggerDetails_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_SetLoggerDetails_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetExchangePairs_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_EnableExchangePair_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_DisableExchangePair_0 = runtime.ForwardResponseMessage + + forward_GoCryptoTrader_GetOrderbookStream_0 = runtime.ForwardResponseStream + + forward_GoCryptoTrader_GetExchangeOrderbookStream_0 = runtime.ForwardResponseStream + + forward_GoCryptoTrader_GetTickerStream_0 = runtime.ForwardResponseStream + + forward_GoCryptoTrader_GetExchangeTickerStream_0 = runtime.ForwardResponseStream + + forward_GoCryptoTrader_GetAuditEvent_0 = runtime.ForwardResponseMessage +) diff --git a/gctrpc/rpc.proto b/gctrpc/rpc.proto new file mode 100644 index 00000000..079d2747 --- /dev/null +++ b/gctrpc/rpc.proto @@ -0,0 +1,843 @@ +syntax = "proto3"; + +import "google/api/annotations.proto"; + +package gctrpc; + +message GetInfoRequest {} + +message GetInfoResponse { + string uptime = 1; + int64 available_exchanges = 2; + int64 enabled_exchanges = 3; + string default_forex_provider = 4; + string default_fiat_currency = 5; + map subsystem_status = 6; + map rpc_endpoints = 7; +} + +message GetCommunicationRelayersRequest {} + +message CommunicationRelayer { + bool enabled = 1; + bool connected = 2; +} + +message GetCommunicationRelayersResponse { + map communication_relayers = 1; +} + +message GenericSubsystemRequest { + string subsystem = 1; +} + +message GenericSubsystemResponse {} + +message GetSubsystemsRequest {} + +message GetSusbsytemsResponse { + map subsystems_status = 1; +} + +message GetRPCEndpointsRequest{} + +message RPCEndpoint { + bool started = 1; + string listen_address = 2; +} + +message GetRPCEndpointsResponse { + map endpoints = 1; +} + +message GenericExchangeNameRequest { + string exchange = 1; +} + +message GenericExchangeNameResponse {} + +message GetExchangesRequest { + bool enabled = 1; +} + +message GetExchangesResponse { + string exchanges = 1; +} + +message GetExchangeOTPReponse { + string otp_code = 1; +} + +message GetExchangeOTPsRequest {} + +message GetExchangeOTPsResponse { + map otp_codes = 1; +} + +message DisableExchangeRequest { + string exchange = 1; +} + +message PairsSupported { + string available_pairs = 1; + string enabled_pairs = 2; +} + +message GetExchangeInfoResponse { + string name = 1; + bool enabled = 2; + bool verbose = 3; + bool using_sandbox = 4; + string http_timeout = 5; + string http_useragent = 6; + string http_proxy = 7; + string base_currencies = 8; + map supported_assets = 9; + bool authenticated_api = 10; +} + +message GetTickerRequest { + string exchange = 1; + CurrencyPair pair = 2; + string asset_type = 3; +} + +message CurrencyPair { + string delimiter = 1; + string base = 2; + string quote = 3; +} + +message TickerResponse { + CurrencyPair pair = 1; + int64 last_updated = 2; + string currency_pair = 3; + double last = 4; + double high = 5; + double low = 6; + double bid = 7; + double ask = 8; + double volume = 9; + double price_ath = 10; +} + +message GetTickersRequest {} + +message Tickers { + string exchange = 1; + repeated TickerResponse tickers = 2; +} + +message GetTickersResponse { + repeated Tickers tickers = 1; +} + +message GetOrderbookRequest { + string exchange = 1; + CurrencyPair pair = 2; + string asset_type = 3; +} + +message OrderbookItem { + double amount = 1; + double price = 2; + int64 id = 3; +} + +message OrderbookResponse { + CurrencyPair pair = 1; + string currency_pair = 2; + repeated OrderbookItem bids = 3; + repeated OrderbookItem asks = 4; + int64 last_updated = 5; + string asset_type = 6; +} + +message GetOrderbooksRequest {} + +message Orderbooks { + string exchange = 1; + repeated OrderbookResponse orderbooks = 2; +} + +message GetOrderbooksResponse { + repeated Orderbooks orderbooks = 1; +} + +message GetAccountInfoRequest { + string exchange = 1; +} + +message Account { + string id = 1; + repeated AccountCurrencyInfo currencies = 2; +} + +message AccountCurrencyInfo { + string currency = 1; + double total_value = 2; + double hold = 3; +} + +message GetAccountInfoResponse { + string exchange = 1; + repeated Account accounts = 2; +} + +message GetConfigRequest {} + +message GetConfigResponse { + bytes data = 1; +} + +message PortfolioAddress { + string address = 1; + string coin_type = 2; + string description = 3; + double balance = 4; +} + +message GetPortfolioRequest {} + +message GetPortfolioResponse { + repeated PortfolioAddress portfolio = 1; +} + +message GetPortfolioSummaryRequest {} + +message Coin { + string coin = 1; + double balance = 2; + string address = 3; + double percentage = 4; +} + +message OfflineCoinSummary { + string address = 1; + double balance = 2; + double percentage = 3; +} + +message OnlineCoinSummary { + double balance = 1; + double percentage = 2; +} + +message OfflineCoins { + repeated OfflineCoinSummary addresses = 1; +} + +message OnlineCoins { + map coins = 1; +} + +message GetPortfolioSummaryResponse { + repeated Coin coin_totals = 1; + repeated Coin coins_offline = 2; + map coins_offline_summary = 3; + repeated Coin coins_online = 4; + map coins_online_summary = 5; +} + +message AddPortfolioAddressRequest { + string address = 1; + string coin_type = 2; + string description = 3; + double balance = 4; +} + +message AddPortfolioAddressResponse {} + +message RemovePortfolioAddressRequest { + string address = 1; + string coin_type = 2; + string description = 3; +} + +message RemovePortfolioAddressResponse {} + +message GetForexProvidersRequest {} + +message ForexProvider { + string name = 1; + bool enabled = 2; + bool verbose = 3; + string rest_polling_delay = 4; + string api_key = 5; + int64 api_key_level =6; + bool primary_provider = 7; +} + +message GetForexProvidersResponse { + repeated ForexProvider forex_providers = 1; +} + +message GetForexRatesRequest {} + +message ForexRatesConversion { + string from = 1; + string to = 2; + double rate = 3; + double inverse_rate = 4; + +} +message GetForexRatesResponse { + repeated ForexRatesConversion forex_rates = 1; +} + +message OrderDetails { + string exchange = 1; + string id = 2; + string base_currency = 3; + string quote_currency = 4; + string asset_type = 5; + string order_side = 6; + string order_type = 7; + int64 creation_time = 8; + string status = 9; + double price = 10; + double amount = 11; + double open_volume = 12; +} + +message GetOrdersRequest { + string exchange = 1; + string asset_type = 2; + CurrencyPair pair = 3; +} + +message GetOrdersResponse { + repeated OrderDetails orders = 1; +} + +message GetOrderRequest { + string exchange = 1; + string order_id = 2; +} + +message SubmitOrderRequest { + string exchange = 1; + CurrencyPair pair = 2; + string side = 3; + string order_type = 4; + double amount = 5; + double price = 6; + string client_id = 7; +} + +message SubmitOrderResponse { + bool order_placed = 1; + string order_id = 2; +} + +message SimulateOrderRequest { + string exchange = 1; + CurrencyPair pair = 2; + double amount = 3; + string side = 4; +} + +message SimulateOrderResponse { + repeated OrderbookItem orders = 1; + double amount = 2; + double minimum_price = 3; + double maximum_price = 4; + double percentage_gain_loss = 5; + string status = 6; +} + +message WhaleBombRequest { + string exchange = 1; + CurrencyPair pair = 2; + double price_target = 3; + string side = 4; +} + +message CancelOrderRequest { + string exchange = 1; + string account_id = 2; + string order_id = 3; + CurrencyPair pair = 4; + string asset_type = 5; + string wallet_address = 6; + string side = 7; +} + +message CancelOrderResponse {} + +message CancelAllOrdersRequest { + string exchange = 1; +} + +message CancelAllOrdersResponse { + message Orders { + string exchange = 1; + map order_status = 2; + } + repeated Orders orders = 1; +} + +message GetEventsRequest {} + + +message ConditionParams { + string condition = 1; + double price = 2; + bool check_bids = 3; + bool check_bids_and_asks = 4; + double orderbook_amount = 5; +} + +message GetEventsResponse { + int64 id = 1; + string exchange = 2; + string item = 3; + ConditionParams condition_params = 4; + CurrencyPair pair = 5; + string action = 6; + bool executed = 7; +} + +message AddEventRequest { + string exchange = 1; + string item = 2; + ConditionParams condition_params = 3; + CurrencyPair pair = 4; + string asset_type = 5; + string action = 6; +} + +message AddEventResponse { + int64 id = 1; +} + +message RemoveEventRequest { + int64 id = 1; +} + +message RemoveEventResponse {} + +message GetCryptocurrencyDepositAddressesRequest { + string exchange = 1; +} + +message GetCryptocurrencyDepositAddressesResponse { + map addresses = 1; +} + +message GetCryptocurrencyDepositAddressRequest { + string exchange = 1; + string cryptocurrency = 2; +} + +message GetCryptocurrencyDepositAddressResponse { + string address = 1; +} + +message WithdrawCurrencyRequest { + string exchange = 1; + string description = 2; + string one_time_password = 3; + string account_id = 4; + int64 pin = 5; + string trade_password = 6; + string currency = 7; + string address = 8; + string address_tag = 9; + double amount = 10; + double fee_amount = 11; + string bank_name = 12; + string bank_address = 13; + string bank_city = 14; + string bank_country = 15; + string swife_code = 16; + string wire_currency = 17; +} + +message WithdrawResponse { + string result = 1; +} + +message GetLoggerDetailsRequest { + string logger = 1; +} + +message GetLoggerDetailsResponse{ + bool info = 1; + bool debug = 2; + bool warn = 3; + bool error = 4; +} + +message SetLoggerDetailsRequest { + string logger = 1; + string level = 2; +} + +message GetExchangePairsRequest { + string exchange = 1; + string asset = 2; +} + +message GetExchangePairsResponse { + map supported_assets = 1; +} + +message ExchangePairRequest { + string exchange = 1; + string asset_type = 2; + CurrencyPair pair = 3; +} + +message GetOrderbookStreamRequest { + string exchange = 1; + CurrencyPair pair = 2; + string asset_type = 3; +} + +message GetExchangeOrderbookStreamRequest { + string exchange = 1; +} + +message GetTickerStreamRequest { + string exchange = 1; + CurrencyPair pair = 2; + string asset_type = 3; +} + +message GetExchangeTickerStreamRequest { + string exchange = 1; +} + +message GetAuditEventRequest { + string start_date = 1; + string end_date = 2; + string order_by = 3; + int32 limit = 4; + int32 offset = 5; +} + +message GetAuditEventResponse { + repeated AuditEvent events = 1; +} + +message AuditEvent { + string type = 1 ; + string identifier = 2; + string message = 3; + string timestamp = 4; +} + +service GoCryptoTrader { + rpc GetInfo (GetInfoRequest) returns (GetInfoResponse) { + option (google.api.http) = { + get :"/v1/getinfo" + }; + } + + rpc GetSubsystems (GetSubsystemsRequest) returns (GetSusbsytemsResponse) { + option (google.api.http) = { + get: "/v1/getsubsystems" + }; + } + + rpc EnableSubsystem (GenericSubsystemRequest) returns (GenericSubsystemResponse) { + option (google.api.http) = { + get: "/v1/enablesubsystem" + }; + } + + rpc DisableSubsystem (GenericSubsystemRequest) returns (GenericSubsystemResponse) { + option (google.api.http) = { + get: "/v1/disablesubsystem" + }; + } + + rpc GetRPCEndpoints (GetRPCEndpointsRequest) returns (GetRPCEndpointsResponse) { + option (google.api.http) = { + get: "/v1/getrpcendpoints" + }; + } + + rpc GetCommunicationRelayers (GetCommunicationRelayersRequest) returns (GetCommunicationRelayersResponse) { + option (google.api.http) = { + get: "/v1/getcommunicationrelayers" + }; + } + + rpc GetExchanges (GetExchangesRequest) returns (GetExchangesResponse) { + option (google.api.http) = { + get: "/v1/getexchanges" + }; + } + + rpc DisableExchange (GenericExchangeNameRequest) returns (GenericExchangeNameResponse) { + option (google.api.http) = { + post: "/v1/disableexchange" + body: "*" + }; + } + + rpc GetExchangeInfo (GenericExchangeNameRequest) returns (GetExchangeInfoResponse) { + option (google.api.http) = { + get: "/v1/getexchangeinfo" + }; + } + + rpc GetExchangeOTPCode (GenericExchangeNameRequest) returns (GetExchangeOTPReponse) { + option (google.api.http) = { + get: "/v1/getexchangeotp" + }; + } + + rpc GetExchangeOTPCodes (GetExchangeOTPsRequest) returns (GetExchangeOTPsResponse) { + option (google.api.http) = { + get: "/v1/getexchangeotps" + }; + } + + rpc EnableExchange (GenericExchangeNameRequest) returns (GenericExchangeNameResponse) { + option (google.api.http) = { + post: "/v1/enableexchange" + body: "*" + }; + } + + rpc GetTicker (GetTickerRequest) returns (TickerResponse) { + option (google.api.http) = { + post: "/v1/getticker" + body: "*" + }; + } + + rpc GetTickers (GetTickersRequest) returns (GetTickersResponse) { + option (google.api.http) = { + get: "/v1/gettickers" + }; + } + + rpc GetOrderbook (GetOrderbookRequest) returns (OrderbookResponse) { + option (google.api.http) = { + post: "/v1/getorderbook" + body: "*" + }; + } + + rpc GetOrderbooks (GetOrderbooksRequest) returns (GetOrderbooksResponse) { + option (google.api.http) = { + get: "/v1/getorderbooks" + }; + } + + rpc GetAccountInfo (GetAccountInfoRequest) returns (GetAccountInfoResponse) { + option (google.api.http) = { + get: "/v1/getaccountinfo" + }; + } + + rpc GetConfig (GetConfigRequest) returns (GetConfigResponse) { + option (google.api.http) = { + get: "/v1/getconfig" + }; + } + + rpc GetPortfolio (GetPortfolioRequest) returns (GetPortfolioResponse) { + option (google.api.http) = { + get: "/v1/getportfolio" + }; + } + + rpc GetPortfolioSummary (GetPortfolioSummaryRequest) returns (GetPortfolioSummaryResponse) { + option (google.api.http) = { + get: "/v1/getportfoliosummary" + }; + } + + + rpc AddPortfolioAddress (AddPortfolioAddressRequest) returns (AddPortfolioAddressResponse) { + option (google.api.http) = { + post: "/v1/addportfolioaddress" + body: "*" + }; + } + + rpc RemovePortfolioAddress (RemovePortfolioAddressRequest) returns (RemovePortfolioAddressResponse) { + option (google.api.http) = { + post: "/v1/removeportfolioaddress" + body: "*" + }; + } + + rpc GetForexProviders (GetForexProvidersRequest) returns (GetForexProvidersResponse) { + option (google.api.http) = { + get: "/v1/getforexproviders" + }; + } + + rpc GetForexRates (GetForexRatesRequest) returns (GetForexRatesResponse) { + option (google.api.http) = { + get: "/v1/getforexrates" + }; + } + + rpc GetOrders (GetOrdersRequest) returns (GetOrdersResponse) { + option (google.api.http) = { + post: "/v1/getorders" + body: "*" + }; + } + + rpc GetOrder (GetOrderRequest) returns (OrderDetails) { + option (google.api.http) = { + post: "/v1/getorder" + body: "*" + }; + } + + rpc SubmitOrder (SubmitOrderRequest) returns (SubmitOrderResponse) { + option (google.api.http) = { + post: "/v1/submitorder" + body: "*" + }; + } + + rpc SimulateOrder (SimulateOrderRequest) returns (SimulateOrderResponse) { + option (google.api.http) = { + post: "/v1/simulateorder" + body: "*" + }; + } + + rpc WhaleBomb (WhaleBombRequest) returns (SimulateOrderResponse) { + option (google.api.http) = { + post: "/v1/whalebomb" + body: "*" + }; + } + + rpc CancelOrder (CancelOrderRequest) returns (CancelOrderResponse) { + option (google.api.http) = { + post: "/v1/cancelorder" + body: "*" + }; + } + + rpc CancelAllOrders (CancelAllOrdersRequest) returns (CancelAllOrdersResponse) { + option (google.api.http) = { + post: "/v1/cancelallorders" + body: "*" + }; + } + + rpc GetEvents(GetEventsRequest) returns (GetEventsResponse) { + option (google.api.http) = { + get: "/v1/getevents" + }; + } + + rpc AddEvent(AddEventRequest) returns (AddEventResponse) { + option (google.api.http) = { + post: "/v1/addevent" + body: "*" + }; + } + + rpc RemoveEvent(RemoveEventRequest) returns (RemoveEventResponse) { + option (google.api.http) = { + post: "/v1/removeevent" + body: "*" + }; + } + + rpc GetCryptocurrencyDepositAddresses(GetCryptocurrencyDepositAddressesRequest) returns (GetCryptocurrencyDepositAddressesResponse) { + option (google.api.http) = { + post: "/v1/getcryptodepositaddresses" + body: "*" + }; + } + + rpc GetCryptocurrencyDepositAddress(GetCryptocurrencyDepositAddressRequest) returns (GetCryptocurrencyDepositAddressResponse) { + option (google.api.http) = { + post: "/v1/getcryptodepositaddress" + body: "*" + }; + } + + rpc WithdrawCryptocurrencyFunds(WithdrawCurrencyRequest) returns (WithdrawResponse) { + option (google.api.http) = { + post: "/v1/withdrawcryptofunds" + body: "*" + }; + } + + rpc WithdrawFiatFunds(WithdrawCurrencyRequest) returns (WithdrawResponse) { + option (google.api.http) = { + post: "/v1/withdrawfiatfunds" + body: "*" + }; + } + + rpc GetLoggerDetails(GetLoggerDetailsRequest) returns (GetLoggerDetailsResponse) { + option (google.api.http) = { + get: "/v1/getloggerdetails" + }; + } + + rpc SetLoggerDetails(SetLoggerDetailsRequest) returns (GetLoggerDetailsResponse) { + option (google.api.http) = { + post: "/v1/setloggerdetails", + body: "*" + }; + } + + rpc GetExchangePairs(GetExchangePairsRequest) returns (GetExchangePairsResponse) { + option (google.api.http) = { + post: "/v1/getexchangepairs", + body: "*" + }; + } + + rpc EnableExchangePair(ExchangePairRequest) returns (GenericExchangeNameResponse) { + option (google.api.http) = { + post: "/v1/enableexchangepair", + body: "*" + }; + } + + rpc DisableExchangePair(ExchangePairRequest) returns (GenericExchangeNameResponse) { + option (google.api.http) = { + post: "/v1/disableexchangepair", + body: "*" + }; + } + + rpc GetOrderbookStream(GetOrderbookStreamRequest) returns (stream OrderbookResponse) { + option (google.api.http) = { + get: "/v1/getorderbookstream" + }; + } + + rpc GetExchangeOrderbookStream(GetExchangeOrderbookStreamRequest) returns (stream OrderbookResponse) { + option (google.api.http) = { + get: "/v1/getexchangeorderbookstream" + }; + } + + rpc GetTickerStream(GetTickerStreamRequest) returns (stream TickerResponse) { + option (google.api.http) = { + get: "/v1/gettickerstream" + }; + } + + rpc GetExchangeTickerStream(GetExchangeTickerStreamRequest) returns (stream TickerResponse) { + option (google.api.http) = { + get: "/v1/getexchangetickerstream" + }; + } + + rpc GetAuditEvent(GetAuditEventRequest) returns (GetAuditEventResponse) { + option (google.api.http) = { + get: "/v1/getauditevent" + }; + } +} diff --git a/gctrpc/rpc.swagger.json b/gctrpc/rpc.swagger.json new file mode 100644 index 00000000..6f754eb1 --- /dev/null +++ b/gctrpc/rpc.swagger.json @@ -0,0 +1,2465 @@ +{ + "swagger": "2.0", + "info": { + "title": "rpc.proto", + "version": "version not set" + }, + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/v1/addevent": { + "post": { + "operationId": "AddEvent", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcAddEventResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcAddEventRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/addportfolioaddress": { + "post": { + "operationId": "AddPortfolioAddress", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcAddPortfolioAddressResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcAddPortfolioAddressRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/cancelallorders": { + "post": { + "operationId": "CancelAllOrders", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcCancelAllOrdersResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcCancelAllOrdersRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/cancelorder": { + "post": { + "operationId": "CancelOrder", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcCancelOrderResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcCancelOrderRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/disableexchange": { + "post": { + "operationId": "DisableExchange", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGenericExchangeNameResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcGenericExchangeNameRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/disableexchangepair": { + "post": { + "operationId": "DisableExchangePair", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGenericExchangeNameResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcExchangePairRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/disablesubsystem": { + "get": { + "operationId": "DisableSubsystem", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGenericSubsystemResponse" + } + } + }, + "parameters": [ + { + "name": "subsystem", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/enableexchange": { + "post": { + "operationId": "EnableExchange", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGenericExchangeNameResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcGenericExchangeNameRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/enableexchangepair": { + "post": { + "operationId": "EnableExchangePair", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGenericExchangeNameResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcExchangePairRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/enablesubsystem": { + "get": { + "operationId": "EnableSubsystem", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGenericSubsystemResponse" + } + } + }, + "parameters": [ + { + "name": "subsystem", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getaccountinfo": { + "get": { + "operationId": "GetAccountInfo", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetAccountInfoResponse" + } + } + }, + "parameters": [ + { + "name": "exchange", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getauditevent": { + "get": { + "operationId": "GetAuditEvent", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetAuditEventResponse" + } + } + }, + "parameters": [ + { + "name": "start_date", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "end_date", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "limit", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "offset", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getcommunicationrelayers": { + "get": { + "operationId": "GetCommunicationRelayers", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetCommunicationRelayersResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getconfig": { + "get": { + "operationId": "GetConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetConfigResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getcryptodepositaddress": { + "post": { + "operationId": "GetCryptocurrencyDepositAddress", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetCryptocurrencyDepositAddressResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcGetCryptocurrencyDepositAddressRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getcryptodepositaddresses": { + "post": { + "operationId": "GetCryptocurrencyDepositAddresses", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetCryptocurrencyDepositAddressesResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcGetCryptocurrencyDepositAddressesRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getevents": { + "get": { + "operationId": "GetEvents", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetEventsResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getexchangeinfo": { + "get": { + "operationId": "GetExchangeInfo", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetExchangeInfoResponse" + } + } + }, + "parameters": [ + { + "name": "exchange", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getexchangeorderbookstream": { + "get": { + "operationId": "GetExchangeOrderbookStream", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "$ref": "#/x-stream-definitions/gctrpcOrderbookResponse" + } + } + }, + "parameters": [ + { + "name": "exchange", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getexchangeotp": { + "get": { + "operationId": "GetExchangeOTPCode", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetExchangeOTPReponse" + } + } + }, + "parameters": [ + { + "name": "exchange", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getexchangeotps": { + "get": { + "operationId": "GetExchangeOTPCodes", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetExchangeOTPsResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getexchangepairs": { + "post": { + "operationId": "GetExchangePairs", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetExchangePairsResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcGetExchangePairsRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getexchanges": { + "get": { + "operationId": "GetExchanges", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetExchangesResponse" + } + } + }, + "parameters": [ + { + "name": "enabled", + "in": "query", + "required": false, + "type": "boolean", + "format": "boolean" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getexchangetickerstream": { + "get": { + "operationId": "GetExchangeTickerStream", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "$ref": "#/x-stream-definitions/gctrpcTickerResponse" + } + } + }, + "parameters": [ + { + "name": "exchange", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getforexproviders": { + "get": { + "operationId": "GetForexProviders", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetForexProvidersResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getforexrates": { + "get": { + "operationId": "GetForexRates", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetForexRatesResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getinfo": { + "get": { + "operationId": "GetInfo", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetInfoResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getloggerdetails": { + "get": { + "operationId": "GetLoggerDetails", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetLoggerDetailsResponse" + } + } + }, + "parameters": [ + { + "name": "logger", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getorder": { + "post": { + "operationId": "GetOrder", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcOrderDetails" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcGetOrderRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getorderbook": { + "post": { + "operationId": "GetOrderbook", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcOrderbookResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcGetOrderbookRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getorderbooks": { + "get": { + "operationId": "GetOrderbooks", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetOrderbooksResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getorderbookstream": { + "get": { + "operationId": "GetOrderbookStream", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "$ref": "#/x-stream-definitions/gctrpcOrderbookResponse" + } + } + }, + "parameters": [ + { + "name": "exchange", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "pair.delimiter", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "pair.base", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "pair.quote", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "asset_type", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getorders": { + "post": { + "operationId": "GetOrders", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetOrdersResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcGetOrdersRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getportfolio": { + "get": { + "operationId": "GetPortfolio", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetPortfolioResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getportfoliosummary": { + "get": { + "operationId": "GetPortfolioSummary", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetPortfolioSummaryResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getrpcendpoints": { + "get": { + "operationId": "GetRPCEndpoints", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetRPCEndpointsResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getsubsystems": { + "get": { + "operationId": "GetSubsystems", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetSusbsytemsResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/getticker": { + "post": { + "operationId": "GetTicker", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcTickerResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcGetTickerRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/gettickers": { + "get": { + "operationId": "GetTickers", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetTickersResponse" + } + } + }, + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/gettickerstream": { + "get": { + "operationId": "GetTickerStream", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "$ref": "#/x-stream-definitions/gctrpcTickerResponse" + } + } + }, + "parameters": [ + { + "name": "exchange", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "pair.delimiter", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "pair.base", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "pair.quote", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "asset_type", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/removeevent": { + "post": { + "operationId": "RemoveEvent", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcRemoveEventResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcRemoveEventRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/removeportfolioaddress": { + "post": { + "operationId": "RemovePortfolioAddress", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcRemovePortfolioAddressResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcRemovePortfolioAddressRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/setloggerdetails": { + "post": { + "operationId": "SetLoggerDetails", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcGetLoggerDetailsResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcSetLoggerDetailsRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/simulateorder": { + "post": { + "operationId": "SimulateOrder", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcSimulateOrderResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcSimulateOrderRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/submitorder": { + "post": { + "operationId": "SubmitOrder", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcSubmitOrderResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcSubmitOrderRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/whalebomb": { + "post": { + "operationId": "WhaleBomb", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcSimulateOrderResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcWhaleBombRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/withdrawcryptofunds": { + "post": { + "operationId": "WithdrawCryptocurrencyFunds", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcWithdrawResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcWithdrawCurrencyRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + }, + "/v1/withdrawfiatfunds": { + "post": { + "operationId": "WithdrawFiatFunds", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/gctrpcWithdrawResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gctrpcWithdrawCurrencyRequest" + } + } + ], + "tags": [ + "GoCryptoTrader" + ] + } + } + }, + "definitions": { + "CancelAllOrdersResponseOrders": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "order_status": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "gctrpcAccount": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "currencies": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcAccountCurrencyInfo" + } + } + } + }, + "gctrpcAccountCurrencyInfo": { + "type": "object", + "properties": { + "currency": { + "type": "string" + }, + "total_value": { + "type": "number", + "format": "double" + }, + "hold": { + "type": "number", + "format": "double" + } + } + }, + "gctrpcAddEventRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "item": { + "type": "string" + }, + "condition_params": { + "$ref": "#/definitions/gctrpcConditionParams" + }, + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + }, + "asset_type": { + "type": "string" + }, + "action": { + "type": "string" + } + } + }, + "gctrpcAddEventResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "int64" + } + } + }, + "gctrpcAddPortfolioAddressRequest": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "coin_type": { + "type": "string" + }, + "description": { + "type": "string" + }, + "balance": { + "type": "number", + "format": "double" + } + } + }, + "gctrpcAddPortfolioAddressResponse": { + "type": "object" + }, + "gctrpcAuditEvent": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "message": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "gctrpcCancelAllOrdersRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + } + } + }, + "gctrpcCancelAllOrdersResponse": { + "type": "object", + "properties": { + "orders": { + "type": "array", + "items": { + "$ref": "#/definitions/CancelAllOrdersResponseOrders" + } + } + } + }, + "gctrpcCancelOrderRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "account_id": { + "type": "string" + }, + "order_id": { + "type": "string" + }, + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + }, + "asset_type": { + "type": "string" + }, + "wallet_address": { + "type": "string" + }, + "side": { + "type": "string" + } + } + }, + "gctrpcCancelOrderResponse": { + "type": "object" + }, + "gctrpcCoin": { + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "balance": { + "type": "number", + "format": "double" + }, + "address": { + "type": "string" + }, + "percentage": { + "type": "number", + "format": "double" + } + } + }, + "gctrpcCommunicationRelayer": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "format": "boolean" + }, + "connected": { + "type": "boolean", + "format": "boolean" + } + } + }, + "gctrpcConditionParams": { + "type": "object", + "properties": { + "condition": { + "type": "string" + }, + "price": { + "type": "number", + "format": "double" + }, + "check_bids": { + "type": "boolean", + "format": "boolean" + }, + "check_bids_and_asks": { + "type": "boolean", + "format": "boolean" + }, + "orderbook_amount": { + "type": "number", + "format": "double" + } + } + }, + "gctrpcCurrencyPair": { + "type": "object", + "properties": { + "delimiter": { + "type": "string" + }, + "base": { + "type": "string" + }, + "quote": { + "type": "string" + } + } + }, + "gctrpcExchangePairRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "asset_type": { + "type": "string" + }, + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + } + } + }, + "gctrpcForexProvider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "enabled": { + "type": "boolean", + "format": "boolean" + }, + "verbose": { + "type": "boolean", + "format": "boolean" + }, + "rest_polling_delay": { + "type": "string" + }, + "api_key": { + "type": "string" + }, + "api_key_level": { + "type": "string", + "format": "int64" + }, + "primary_provider": { + "type": "boolean", + "format": "boolean" + } + } + }, + "gctrpcForexRatesConversion": { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "to": { + "type": "string" + }, + "rate": { + "type": "number", + "format": "double" + }, + "inverse_rate": { + "type": "number", + "format": "double" + } + } + }, + "gctrpcGenericExchangeNameRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + } + } + }, + "gctrpcGenericExchangeNameResponse": { + "type": "object" + }, + "gctrpcGenericSubsystemResponse": { + "type": "object" + }, + "gctrpcGetAccountInfoResponse": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcAccount" + } + } + } + }, + "gctrpcGetAuditEventResponse": { + "type": "object", + "properties": { + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcAuditEvent" + } + } + } + }, + "gctrpcGetCommunicationRelayersResponse": { + "type": "object", + "properties": { + "communication_relayers": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/gctrpcCommunicationRelayer" + } + } + } + }, + "gctrpcGetConfigResponse": { + "type": "object", + "properties": { + "data": { + "type": "string", + "format": "byte" + } + } + }, + "gctrpcGetCryptocurrencyDepositAddressRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "cryptocurrency": { + "type": "string" + } + } + }, + "gctrpcGetCryptocurrencyDepositAddressResponse": { + "type": "object", + "properties": { + "address": { + "type": "string" + } + } + }, + "gctrpcGetCryptocurrencyDepositAddressesRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + } + } + }, + "gctrpcGetCryptocurrencyDepositAddressesResponse": { + "type": "object", + "properties": { + "addresses": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "gctrpcGetEventsResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "int64" + }, + "exchange": { + "type": "string" + }, + "item": { + "type": "string" + }, + "condition_params": { + "$ref": "#/definitions/gctrpcConditionParams" + }, + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + }, + "action": { + "type": "string" + }, + "executed": { + "type": "boolean", + "format": "boolean" + } + } + }, + "gctrpcGetExchangeInfoResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "enabled": { + "type": "boolean", + "format": "boolean" + }, + "verbose": { + "type": "boolean", + "format": "boolean" + }, + "using_sandbox": { + "type": "boolean", + "format": "boolean" + }, + "http_timeout": { + "type": "string" + }, + "http_useragent": { + "type": "string" + }, + "http_proxy": { + "type": "string" + }, + "base_currencies": { + "type": "string" + }, + "supported_assets": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/gctrpcPairsSupported" + } + }, + "authenticated_api": { + "type": "boolean", + "format": "boolean" + } + } + }, + "gctrpcGetExchangeOTPReponse": { + "type": "object", + "properties": { + "otp_code": { + "type": "string" + } + } + }, + "gctrpcGetExchangeOTPsResponse": { + "type": "object", + "properties": { + "otp_codes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "gctrpcGetExchangePairsRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "asset": { + "type": "string" + } + } + }, + "gctrpcGetExchangePairsResponse": { + "type": "object", + "properties": { + "supported_assets": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/gctrpcPairsSupported" + } + } + } + }, + "gctrpcGetExchangesResponse": { + "type": "object", + "properties": { + "exchanges": { + "type": "string" + } + } + }, + "gctrpcGetForexProvidersResponse": { + "type": "object", + "properties": { + "forex_providers": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcForexProvider" + } + } + } + }, + "gctrpcGetForexRatesResponse": { + "type": "object", + "properties": { + "forex_rates": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcForexRatesConversion" + } + } + } + }, + "gctrpcGetInfoResponse": { + "type": "object", + "properties": { + "uptime": { + "type": "string" + }, + "available_exchanges": { + "type": "string", + "format": "int64" + }, + "enabled_exchanges": { + "type": "string", + "format": "int64" + }, + "default_forex_provider": { + "type": "string" + }, + "default_fiat_currency": { + "type": "string" + }, + "subsystem_status": { + "type": "object", + "additionalProperties": { + "type": "boolean", + "format": "boolean" + } + }, + "rpc_endpoints": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/gctrpcRPCEndpoint" + } + } + } + }, + "gctrpcGetLoggerDetailsResponse": { + "type": "object", + "properties": { + "info": { + "type": "boolean", + "format": "boolean" + }, + "debug": { + "type": "boolean", + "format": "boolean" + }, + "warn": { + "type": "boolean", + "format": "boolean" + }, + "error": { + "type": "boolean", + "format": "boolean" + } + } + }, + "gctrpcGetOrderRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "order_id": { + "type": "string" + } + } + }, + "gctrpcGetOrderbookRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + }, + "asset_type": { + "type": "string" + } + } + }, + "gctrpcGetOrderbooksResponse": { + "type": "object", + "properties": { + "orderbooks": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcOrderbooks" + } + } + } + }, + "gctrpcGetOrdersRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "asset_type": { + "type": "string" + }, + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + } + } + }, + "gctrpcGetOrdersResponse": { + "type": "object", + "properties": { + "orders": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcOrderDetails" + } + } + } + }, + "gctrpcGetPortfolioResponse": { + "type": "object", + "properties": { + "portfolio": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcPortfolioAddress" + } + } + } + }, + "gctrpcGetPortfolioSummaryResponse": { + "type": "object", + "properties": { + "coin_totals": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcCoin" + } + }, + "coins_offline": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcCoin" + } + }, + "coins_offline_summary": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/gctrpcOfflineCoins" + } + }, + "coins_online": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcCoin" + } + }, + "coins_online_summary": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/gctrpcOnlineCoins" + } + } + } + }, + "gctrpcGetRPCEndpointsResponse": { + "type": "object", + "properties": { + "endpoints": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/gctrpcRPCEndpoint" + } + } + } + }, + "gctrpcGetSusbsytemsResponse": { + "type": "object", + "properties": { + "subsystems_status": { + "type": "object", + "additionalProperties": { + "type": "boolean", + "format": "boolean" + } + } + } + }, + "gctrpcGetTickerRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + }, + "asset_type": { + "type": "string" + } + } + }, + "gctrpcGetTickersResponse": { + "type": "object", + "properties": { + "tickers": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcTickers" + } + } + } + }, + "gctrpcOfflineCoinSummary": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "balance": { + "type": "number", + "format": "double" + }, + "percentage": { + "type": "number", + "format": "double" + } + } + }, + "gctrpcOfflineCoins": { + "type": "object", + "properties": { + "addresses": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcOfflineCoinSummary" + } + } + } + }, + "gctrpcOnlineCoinSummary": { + "type": "object", + "properties": { + "balance": { + "type": "number", + "format": "double" + }, + "percentage": { + "type": "number", + "format": "double" + } + } + }, + "gctrpcOnlineCoins": { + "type": "object", + "properties": { + "coins": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/gctrpcOnlineCoinSummary" + } + } + } + }, + "gctrpcOrderDetails": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "id": { + "type": "string" + }, + "base_currency": { + "type": "string" + }, + "quote_currency": { + "type": "string" + }, + "asset_type": { + "type": "string" + }, + "order_side": { + "type": "string" + }, + "order_type": { + "type": "string" + }, + "creation_time": { + "type": "string", + "format": "int64" + }, + "status": { + "type": "string" + }, + "price": { + "type": "number", + "format": "double" + }, + "amount": { + "type": "number", + "format": "double" + }, + "open_volume": { + "type": "number", + "format": "double" + } + } + }, + "gctrpcOrderbookItem": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "double" + }, + "price": { + "type": "number", + "format": "double" + }, + "id": { + "type": "string", + "format": "int64" + } + } + }, + "gctrpcOrderbookResponse": { + "type": "object", + "properties": { + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + }, + "currency_pair": { + "type": "string" + }, + "bids": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcOrderbookItem" + } + }, + "asks": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcOrderbookItem" + } + }, + "last_updated": { + "type": "string", + "format": "int64" + }, + "asset_type": { + "type": "string" + } + } + }, + "gctrpcOrderbooks": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "orderbooks": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcOrderbookResponse" + } + } + } + }, + "gctrpcPairsSupported": { + "type": "object", + "properties": { + "available_pairs": { + "type": "string" + }, + "enabled_pairs": { + "type": "string" + } + } + }, + "gctrpcPortfolioAddress": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "coin_type": { + "type": "string" + }, + "description": { + "type": "string" + }, + "balance": { + "type": "number", + "format": "double" + } + } + }, + "gctrpcRPCEndpoint": { + "type": "object", + "properties": { + "started": { + "type": "boolean", + "format": "boolean" + }, + "listen_address": { + "type": "string" + } + } + }, + "gctrpcRemoveEventRequest": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "int64" + } + } + }, + "gctrpcRemoveEventResponse": { + "type": "object" + }, + "gctrpcRemovePortfolioAddressRequest": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "coin_type": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "gctrpcRemovePortfolioAddressResponse": { + "type": "object" + }, + "gctrpcSetLoggerDetailsRequest": { + "type": "object", + "properties": { + "logger": { + "type": "string" + }, + "level": { + "type": "string" + } + } + }, + "gctrpcSimulateOrderRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + }, + "amount": { + "type": "number", + "format": "double" + }, + "side": { + "type": "string" + } + } + }, + "gctrpcSimulateOrderResponse": { + "type": "object", + "properties": { + "orders": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcOrderbookItem" + } + }, + "amount": { + "type": "number", + "format": "double" + }, + "minimum_price": { + "type": "number", + "format": "double" + }, + "maximum_price": { + "type": "number", + "format": "double" + }, + "percentage_gain_loss": { + "type": "number", + "format": "double" + }, + "status": { + "type": "string" + } + } + }, + "gctrpcSubmitOrderRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + }, + "side": { + "type": "string" + }, + "order_type": { + "type": "string" + }, + "amount": { + "type": "number", + "format": "double" + }, + "price": { + "type": "number", + "format": "double" + }, + "client_id": { + "type": "string" + } + } + }, + "gctrpcSubmitOrderResponse": { + "type": "object", + "properties": { + "order_placed": { + "type": "boolean", + "format": "boolean" + }, + "order_id": { + "type": "string" + } + } + }, + "gctrpcTickerResponse": { + "type": "object", + "properties": { + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + }, + "last_updated": { + "type": "string", + "format": "int64" + }, + "currency_pair": { + "type": "string" + }, + "last": { + "type": "number", + "format": "double" + }, + "high": { + "type": "number", + "format": "double" + }, + "low": { + "type": "number", + "format": "double" + }, + "bid": { + "type": "number", + "format": "double" + }, + "ask": { + "type": "number", + "format": "double" + }, + "volume": { + "type": "number", + "format": "double" + }, + "price_ath": { + "type": "number", + "format": "double" + } + } + }, + "gctrpcTickers": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "tickers": { + "type": "array", + "items": { + "$ref": "#/definitions/gctrpcTickerResponse" + } + } + } + }, + "gctrpcWhaleBombRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "pair": { + "$ref": "#/definitions/gctrpcCurrencyPair" + }, + "price_target": { + "type": "number", + "format": "double" + }, + "side": { + "type": "string" + } + } + }, + "gctrpcWithdrawCurrencyRequest": { + "type": "object", + "properties": { + "exchange": { + "type": "string" + }, + "description": { + "type": "string" + }, + "one_time_password": { + "type": "string" + }, + "account_id": { + "type": "string" + }, + "pin": { + "type": "string", + "format": "int64" + }, + "trade_password": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "address": { + "type": "string" + }, + "address_tag": { + "type": "string" + }, + "amount": { + "type": "number", + "format": "double" + }, + "fee_amount": { + "type": "number", + "format": "double" + }, + "bank_name": { + "type": "string" + }, + "bank_address": { + "type": "string" + }, + "bank_city": { + "type": "string" + }, + "bank_country": { + "type": "string" + }, + "swife_code": { + "type": "string" + }, + "wire_currency": { + "type": "string" + } + } + }, + "gctrpcWithdrawResponse": { + "type": "object", + "properties": { + "result": { + "type": "string" + } + } + }, + "protobufAny": { + "type": "object", + "properties": { + "type_url": { + "type": "string" + }, + "value": { + "type": "string", + "format": "byte" + } + } + }, + "runtimeStreamError": { + "type": "object", + "properties": { + "grpc_code": { + "type": "integer", + "format": "int32" + }, + "http_code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "http_status": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + } + }, + "x-stream-definitions": { + "gctrpcOrderbookResponse": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/gctrpcOrderbookResponse" + }, + "error": { + "$ref": "#/definitions/runtimeStreamError" + } + }, + "title": "Stream result of gctrpcOrderbookResponse" + }, + "gctrpcTickerResponse": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/gctrpcTickerResponse" + }, + "error": { + "$ref": "#/definitions/runtimeStreamError" + } + }, + "title": "Stream result of gctrpcTickerResponse" + } + } +} diff --git a/go.mod b/go.mod index fb714ed2..a412732a 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,27 @@ module github.com/thrasher-corp/gocryptotrader go 1.12 require ( + github.com/gofrs/uuid v3.2.0+incompatible + github.com/golang/protobuf v1.3.2 github.com/google/go-querystring v1.0.0 github.com/gorilla/mux v1.7.3 github.com/gorilla/websocket v1.4.0 + github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 + github.com/grpc-ecosystem/grpc-gateway v1.11.3 + github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 + github.com/lib/pq v1.2.0 + github.com/mattn/go-sqlite3 v1.11.0 + github.com/pkg/errors v0.8.1 + github.com/pquerna/otp v1.2.0 + github.com/spf13/viper v1.4.0 + github.com/thrasher-corp/goose v2.7.0-rc4.0.20191002032028-0f2c2a27abdb+incompatible + github.com/thrasher-corp/sqlboiler v1.0.1-0.20191001234224-71e17f37a85e github.com/toorop/go-pusher v0.0.0-20180521062818-4521e2eb39fb - golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f + github.com/urfave/cli v1.20.0 + golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 + golang.org/x/net v0.0.0-20190606173856-1492cefac77f // indirect + golang.org/x/sys v0.0.0-20191003212358-c178f38b412c // indirect + google.golang.org/genproto v0.0.0-20191002211648-c459b9ce5143 + google.golang.org/grpc v1.21.1 + gopkg.in/yaml.v2 v2.2.4 // indirect ) diff --git a/go.sum b/go.sum index cf95f9f8..9d1076b0 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,244 @@ +cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/apmckinlay/gsuneido v0.0.0-20180907175622-1f10244968e3/go.mod h1:hJnaqxrCRgMCTWtpNz9XUFkBCREiQdlcyK6YNmOfroM= +github.com/apmckinlay/gsuneido v0.0.0-20190404155041-0b6cd442a18f/go.mod h1:JU2DOj5Fc6rol0yaT79Csr47QR0vONGwJtBNGRD7jmc= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20190924004331-208c0a498538/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/ericlagergren/decimal v0.0.0-20180907214518-0bb163153a5d/go.mod h1:1yj25TwtUlJ+pfOu9apAVaM1RWfZGg+aFpd4hPQZekQ= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.11.3 h1:h8+NsYENhxNTuq+dobk3+ODoJtwY4Fu0WQXsxJfL8aM= +github.com/grpc-ecosystem/grpc-gateway v1.11.3/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 h1:DQVOxR9qdYEybJUr/c7ku34r3PfajaMYXZwgDM7KuSk= +github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12/go.mod h1:u9MdXq/QageOOSGp7qG4XAQsYUMP+V5zEel/Vrl6OOc= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok= +github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/thrasher-corp/goose v2.7.0-rc4.0.20191002032028-0f2c2a27abdb+incompatible h1:SPqQlzFu3g4P9wK2iwJaWVLJWcQ5rYc43rvXBJ8RSCY= +github.com/thrasher-corp/goose v2.7.0-rc4.0.20191002032028-0f2c2a27abdb+incompatible/go.mod h1:2Bb/y0SpnUWOlPU5kDz+ctvb3w/mzuAVqxy7JPfBzgw= +github.com/thrasher-corp/sqlboiler v1.0.1-0.20191001234224-71e17f37a85e h1:4kYBo2YhqqFY7aZPPEhrtPTMoAq4iCsoDITd3jseRbY= +github.com/thrasher-corp/sqlboiler v1.0.1-0.20191001234224-71e17f37a85e/go.mod h1:JfJE+3gijF30ZJbUCzxGkU0+ymQxBfBOVp4XDObmJBE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/toorop/go-pusher v0.0.0-20180521062818-4521e2eb39fb h1:9kcmLvQdiIecpgVEL3/+J5QIP/ElRBJDljOay0SvqnA= github.com/toorop/go-pusher v0.0.0-20180521062818-4521e2eb39fb/go.mod h1:VTLqNCX1tXrur6pdIRCl8Q90FR7nw/mEBdyMkWMcsb0= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/volatiletech/inflect v0.0.0-20170731032912-e7201282ae8d h1:gI4/tqP6lCY5k6Sg+4k9qSoBXmPwG+xXgMpK7jivD4M= +github.com/volatiletech/inflect v0.0.0-20170731032912-e7201282ae8d/go.mod h1:jspfvgf53t5NLUT4o9L1IX0kIBNKamGq1tWc/MgWK9Q= +github.com/volatiletech/null v8.0.0+incompatible h1:7wP8m5d/gZ6kW/9GnrLtMCRre2dlEnaQ9Km5OXlK4zg= +github.com/volatiletech/null v8.0.0+incompatible/go.mod h1:0wD98JzdqB+rLyZ70fN05VDbXbafIb0KU0MdVhCzmOQ= +github.com/volatiletech/sqlboiler v3.5.0+incompatible h1:n160O7UQLpZVRnJY6VH5eRNkt7sQdQBZGCCZ3CUy1+g= +github.com/volatiletech/sqlboiler v3.5.0+incompatible/go.mod h1:jLfDkkHWPbS2cWRLkyC20vQWaIQsASEY7gM7zSo11Yw= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo= -golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190606173856-1492cefac77f h1:IWHgpgFqnL5AhBUBZSgBdjl2vkQUEzcY+JNKWfcgAU0= +golang.org/x/net v0.0.0-20190606173856-1492cefac77f/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA= +golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191003212358-c178f38b412c h1:6Zx7DRlKXf79yfxuQ/7GqV3w2y7aDsk6bGg0MzF5RVU= +golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20191002211648-c459b9ce5143 h1:tikhlQEJeezbnu0Zcblj7g5vm/L7xt6g1vnfq8mRCS4= +google.golang.org/genproto v0.0.0-20191002211648-c459b9ce5143/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7 h1:+t9dhfO+GNOIGJof6kPOAenx7YgrZMTdRPV+EsnPabk= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/helpers.go b/helpers.go deleted file mode 100644 index 3e0f490a..00000000 --- a/helpers.go +++ /dev/null @@ -1,420 +0,0 @@ -package main - -import ( - "errors" - "fmt" - - "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/stats" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - log "github.com/thrasher-corp/gocryptotrader/logger" - "github.com/thrasher-corp/gocryptotrader/portfolio" -) - -// GetAllAvailablePairs returns a list of all available pairs on either enabled -// or disabled exchanges -func GetAllAvailablePairs(enabledExchangesOnly bool) currency.Pairs { - var pairList currency.Pairs - for x := range bot.config.Exchanges { - if enabledExchangesOnly && !bot.config.Exchanges[x].Enabled { - continue - } - - exchName := bot.config.Exchanges[x].Name - pairs, err := bot.config.GetAvailablePairs(exchName) - if err != nil { - continue - } - - for y := range pairs { - if pairList.Contains(pairs[y], false) { - continue - } - pairList = append(pairList, pairs[y]) - } - } - return pairList -} - -// GetSpecificAvailablePairs returns a list of supported pairs based on specific -// parameters -func GetSpecificAvailablePairs(enabledExchangesOnly, fiatPairs, includeUSDT, cryptoPairs bool) currency.Pairs { - var pairList currency.Pairs - supportedPairs := GetAllAvailablePairs(enabledExchangesOnly) - - for x := range supportedPairs { - if fiatPairs { - if supportedPairs[x].IsCryptoFiatPair() && - !supportedPairs[x].ContainsCurrency(currency.USDT) || - (includeUSDT && - supportedPairs[x].ContainsCurrency(currency.USDT) && - supportedPairs[x].IsCryptoPair()) { - if pairList.Contains(supportedPairs[x], false) { - continue - } - pairList = append(pairList, supportedPairs[x]) - } - } - if cryptoPairs { - if supportedPairs[x].IsCryptoPair() { - if pairList.Contains(supportedPairs[x], false) { - continue - } - pairList = append(pairList, supportedPairs[x]) - } - } - } - return pairList -} - -// IsRelatablePairs checks to see if the two pairs are relatable -func IsRelatablePairs(p1, p2 currency.Pair, includeUSDT bool) bool { - if p1.EqualIncludeReciprocal(p2) { - return true - } - - var relatablePairs = GetRelatableCurrencies(p1, true, includeUSDT) - if p1.IsCryptoFiatPair() { - for x := range relatablePairs { - relatablePairs = append(relatablePairs, - GetRelatableFiatCurrencies(relatablePairs[x])...) - } - } - return relatablePairs.Contains(p2, false) -} - -// MapCurrenciesByExchange returns a list of currency pairs mapped to an -// exchange -func MapCurrenciesByExchange(p []currency.Pair, enabledExchangesOnly bool) map[string]currency.Pairs { - currencyExchange := make(map[string]currency.Pairs) - for x := range p { - for y := range bot.config.Exchanges { - if enabledExchangesOnly && !bot.config.Exchanges[y].Enabled { - continue - } - exchName := bot.config.Exchanges[y].Name - success, err := bot.config.SupportsPair(exchName, p[x]) - if err != nil || !success { - continue - } - - result, ok := currencyExchange[exchName] - if !ok { - var pairs []currency.Pair - pairs = append(pairs, p[x]) - currencyExchange[exchName] = pairs - } else { - if result.Contains(p[x], false) { - continue - } - result = append(result, p[x]) - currencyExchange[exchName] = result - } - } - } - return currencyExchange -} - -// GetExchangeNamesByCurrency returns a list of exchanges supporting -// a currency pair based on whether the exchange is enabled or not -func GetExchangeNamesByCurrency(p currency.Pair, enabled bool) []string { - var exchanges []string - for x := range bot.config.Exchanges { - if enabled != bot.config.Exchanges[x].Enabled { - continue - } - - exchName := bot.config.Exchanges[x].Name - success, err := bot.config.SupportsPair(exchName, p) - if err != nil { - continue - } - - if success { - exchanges = append(exchanges, exchName) - } - } - return exchanges -} - -// GetRelatableCryptocurrencies returns a list of currency pairs if it can find -// any relatable currencies (e.g ETHBTC -> ETHLTC -> ETHUSDT -> ETHREP) -func GetRelatableCryptocurrencies(p currency.Pair) currency.Pairs { - var pairs currency.Pairs - cryptocurrencies := currency.GetCryptocurrencies() - - for x := range cryptocurrencies { - newPair := currency.NewPair(p.Base, cryptocurrencies[x]) - if newPair.IsInvalid() { - continue - } - - if newPair.Base.Upper() == p.Base.Upper() && - newPair.Quote.Upper() == p.Quote.Upper() { - continue - } - - if pairs.Contains(newPair, false) { - continue - } - pairs = append(pairs, newPair) - } - return pairs -} - -// GetRelatableFiatCurrencies returns a list of currency pairs if it can find -// any relatable currencies (e.g ETHUSD -> ETHAUD -> ETHGBP -> ETHJPY) -func GetRelatableFiatCurrencies(p currency.Pair) currency.Pairs { - var pairs currency.Pairs - fiatCurrencies := currency.GetFiatCurrencies() - - for x := range fiatCurrencies { - newPair := currency.NewPair(p.Base, fiatCurrencies[x]) - if newPair.Base.Upper() == newPair.Quote.Upper() { - continue - } - - if newPair.Base.Upper() == p.Base.Upper() && - newPair.Quote.Upper() == p.Quote.Upper() { - continue - } - - if pairs.Contains(newPair, false) { - continue - } - pairs = append(pairs, newPair) - } - return pairs -} - -// GetRelatableCurrencies returns a list of currency pairs if it can find -// any relatable currencies (e.g BTCUSD -> BTC USDT -> XBT USDT -> XBT USD) -// incOrig includes the supplied pair if desired -func GetRelatableCurrencies(p currency.Pair, incOrig, incUSDT bool) currency.Pairs { - var pairs currency.Pairs - - addPair := func(p currency.Pair) { - if pairs.Contains(p, true) { - return - } - pairs = append(pairs, p) - } - - buildPairs := func(p currency.Pair, incOrig bool) { - if incOrig { - addPair(p) - } - - first, ok := currency.GetTranslation(p.Base) - if ok { - addPair(currency.NewPair(first, p.Quote)) - - var second currency.Code - second, ok = currency.GetTranslation(p.Quote) - if ok { - addPair(currency.NewPair(first, second)) - } - } - - second, ok := currency.GetTranslation(p.Quote) - if ok { - addPair(currency.NewPair(p.Base, second)) - } - } - - buildPairs(p, incOrig) - buildPairs(p.Swap(), incOrig) - - if !incUSDT { - pairs = pairs.RemovePairsByFilter(currency.USDT) - } - - return pairs -} - -// GetSpecificOrderbook returns a specific orderbook given the currency, -// exchangeName and assetType -func GetSpecificOrderbook(currencyPair, exchangeName, assetType string) (orderbook.Base, error) { - var specificOrderbook orderbook.Base - var err error - for x := range bot.exchanges { - if bot.exchanges[x] != nil { - if bot.exchanges[x].GetName() == exchangeName { - specificOrderbook, err = bot.exchanges[x].GetOrderbookEx( - currency.NewPairFromString(currencyPair), - assetType, - ) - break - } - } - } - return specificOrderbook, err -} - -// GetSpecificTicker returns a specific ticker given the currency, -// exchangeName and assetType -func GetSpecificTicker(currencyPair, exchangeName, assetType string) (ticker.Price, error) { - var specificTicker ticker.Price - var err error - for x := range bot.exchanges { - if bot.exchanges[x] != nil { - if bot.exchanges[x].GetName() == exchangeName { - specificTicker, err = bot.exchanges[x].GetTickerPrice( - currency.NewPairFromString(currencyPair), - assetType, - ) - break - } - } - } - return specificTicker, err -} - -// GetCollatedExchangeAccountInfoByCoin collates individual exchange account -// information and turns into into a map string of -// exchange.AccountCurrencyInfo -func GetCollatedExchangeAccountInfoByCoin(exchAccounts []exchange.AccountInfo) map[currency.Code]exchange.AccountCurrencyInfo { - result := make(map[currency.Code]exchange.AccountCurrencyInfo) - for _, accounts := range exchAccounts { - for _, account := range accounts.Accounts { - for _, accountCurrencyInfo := range account.Currencies { - currencyName := accountCurrencyInfo.CurrencyName - avail := accountCurrencyInfo.TotalValue - onHold := accountCurrencyInfo.Hold - - info, ok := result[currencyName] - if !ok { - accountInfo := exchange.AccountCurrencyInfo{CurrencyName: currencyName, Hold: onHold, TotalValue: avail} - result[currencyName] = accountInfo - } else { - info.Hold += onHold - info.TotalValue += avail - result[currencyName] = info - } - } - } - } - return result -} - -// GetAccountCurrencyInfoByExchangeName returns info for an exchange -func GetAccountCurrencyInfoByExchangeName(accounts []exchange.AccountInfo, exchangeName string) (exchange.AccountInfo, error) { - for i := 0; i < len(accounts); i++ { - if accounts[i].Exchange == exchangeName { - return accounts[i], nil - } - } - return exchange.AccountInfo{}, errors.New(exchange.ErrExchangeNotFound) -} - -// GetExchangeHighestPriceByCurrencyPair returns the exchange with the highest -// price for a given currency pair and asset type -func GetExchangeHighestPriceByCurrencyPair(p currency.Pair, assetType string) (string, error) { - result := stats.SortExchangesByPrice(p, assetType, true) - if len(result) == 0 { - return "", fmt.Errorf("no stats for supplied currency pair and asset type") - } - - return result[0].Exchange, nil -} - -// GetExchangeLowestPriceByCurrencyPair returns the exchange with the lowest -// price for a given currency pair and asset type -func GetExchangeLowestPriceByCurrencyPair(p currency.Pair, assetType string) (string, error) { - result := stats.SortExchangesByPrice(p, assetType, false) - if len(result) == 0 { - return "", fmt.Errorf("no stats for supplied currency pair and asset type") - } - - return result[0].Exchange, nil -} - -// SeedExchangeAccountInfo seeds account info -func SeedExchangeAccountInfo(data []exchange.AccountInfo) { - if len(data) == 0 { - return - } - - port := portfolio.GetPortfolio() - - for _, exchangeData := range data { - exchangeName := exchangeData.Exchange - - var currencies []exchange.AccountCurrencyInfo - for _, account := range exchangeData.Accounts { - for _, info := range account.Currencies { - var update bool - for i := range currencies { - if info.CurrencyName == currencies[i].CurrencyName { - currencies[i].Hold += info.Hold - currencies[i].TotalValue += info.TotalValue - update = true - } - } - - if update { - continue - } - - currencies = append(currencies, exchange.AccountCurrencyInfo{ - CurrencyName: info.CurrencyName, - TotalValue: info.TotalValue, - Hold: info.Hold, - }) - } - } - - for _, total := range currencies { - currencyName := total.CurrencyName - total := total.TotalValue - - if !port.ExchangeAddressExists(exchangeName, currencyName) { - if total <= 0 { - continue - } - - log.Debugf("Portfolio: Adding new exchange address: %s, %s, %f, %s\n", - exchangeName, - currencyName, - total, - portfolio.PortfolioAddressExchange) - - port.Addresses = append( - port.Addresses, - portfolio.Address{Address: exchangeName, - CoinType: currencyName, - Balance: total, - Description: portfolio.PortfolioAddressExchange}) - } else { - if total <= 0 { - log.Debugf("Portfolio: Removing %s %s entry.\n", - exchangeName, - currencyName) - - port.RemoveExchangeAddress(exchangeName, currencyName) - } else { - balance, ok := port.GetAddressBalance(exchangeName, - portfolio.PortfolioAddressExchange, - currencyName) - - if !ok { - continue - } - - if balance != total { - log.Debugf("Portfolio: Updating %s %s entry with balance %f.\n", - exchangeName, - currencyName, - total) - - port.UpdateExchangeAddressBalance(exchangeName, - currencyName, - total) - } - } - } - } - } -} diff --git a/logger/logger.go b/logger/logger.go index 84572942..da624b54 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -1,120 +1,81 @@ package logger import ( + "errors" "fmt" "io" - "io/ioutil" - "log" - "os" - "path/filepath" - "runtime" "time" ) -func init() { - setDefaultOutputs() -} - -// SetupLogger configure logger instance with user provided settings -func SetupLogger() (err error) { - if *Logger.Enabled { - err = setupOutputs() - if err != nil { - return - } - logLevel() - if Logger.ColourOutput { - colourOutput() - } - } else { - clearAllLoggers() - } - return -} - -// setDefaultOutputs() this setups defaults used by the logger -// This allows it to be used without any user configuration -func setDefaultOutputs() { - debugLogger = log.New(os.Stdout, - "[DEBUG]: ", - log.Ldate|log.Ltime) - - infoLogger = log.New(os.Stdout, - "[INFO]: ", - log.Ldate|log.Ltime) - - warnLogger = log.New(os.Stdout, - "[WARN]: ", - log.Ldate|log.Ltime) - - errorLogger = log.New(os.Stdout, - "[ERROR]: ", - log.Ldate|log.Ltime) - - fatalLogger = log.New(os.Stdout, - "[FATAL]: ", - log.Ldate|log.Ltime) -} - -// colorOutput() sets the prefix of each log type to matching colour -// TODO: add windows support -func colourOutput() { - if runtime.GOOS != "windows" || Logger.ColourOutputOverride { - debugLogger.SetPrefix("\033[34m[DEBUG]\033[0m: ") - infoLogger.SetPrefix("\033[32m[INFO]\033[0m: ") - warnLogger.SetPrefix("\033[33m[WARN]\033[0m: ") - errorLogger.SetPrefix("\033[31m[ERROR]\033[0m: ") - fatalLogger.SetPrefix("\033[31m[FATAL]\033[0m: ") +func newLogger(c *Config) *Logger { + return &Logger{ + Timestamp: c.AdvancedSettings.TimeStampFormat, + Spacer: c.AdvancedSettings.Spacer, + ErrorHeader: c.AdvancedSettings.Headers.Error, + InfoHeader: c.AdvancedSettings.Headers.Info, + WarnHeader: c.AdvancedSettings.Headers.Warn, + DebugHeader: c.AdvancedSettings.Headers.Debug, } } -// clearAllLoggers() sets all logger flags to 0 and outputs to Discard -func clearAllLoggers() { - debugLogger.SetFlags(0) - infoLogger.SetFlags(0) - warnLogger.SetFlags(0) - errorLogger.SetFlags(0) - fatalLogger.SetFlags(0) - - debugLogger.SetOutput(ioutil.Discard) - infoLogger.SetOutput(ioutil.Discard) - warnLogger.SetOutput(ioutil.Discard) - errorLogger.SetOutput(ioutil.Discard) - fatalLogger.SetOutput(ioutil.Discard) -} - -// setupOutputs() sets up the io.writer to use for logging -// TODO: Fix up rotating at the moment its a quick job -func setupOutputs() (err error) { - if len(Logger.File) > 0 { - logFile := filepath.Join(LogPath, Logger.File) - if Logger.Rotate { - if _, err = os.Stat(logFile); !os.IsNotExist(err) { - currentTime := time.Now() - newName := currentTime.Format("2006-01-02 15-04-05") - newFile := newName + " " + Logger.File - err = os.Rename(logFile, filepath.Join(LogPath, newFile)) - if err != nil { - err = fmt.Errorf("failed to rename old log file %s", err) - return - } - } - } - logFileHandle, err = os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) - if err != nil { - return - } - logOutput = io.MultiWriter(os.Stdout, logFileHandle) - } else { - logOutput = os.Stdout +func (l *Logger) newLogEvent(data, header string, w io.Writer) error { + if w == nil { + return errors.New("io.Writer not set") } - return + + e := eventPool.Get().(*LogEvent) + e.output = w + e.data = append(e.data, []byte(header)...) + e.data = append(e.data, l.Spacer...) + if l.Timestamp != "" { + e.data = time.Now().AppendFormat(e.data, l.Timestamp) + } + e.data = append(e.data, l.Spacer...) + e.data = append(e.data, []byte(data)...) + if data == "" || data[len(data)-1] != '\n' { + e.data = append(e.data, '\n') + } + _, err := e.output.Write(e.data) + + e.data = e.data[:0] + eventPool.Put(e) + + return err } -// CloseLogFile close the handler for any open log files -func CloseLogFile() (err error) { - if logFileHandle != nil { - err = logFileHandle.Close() +// CloseLogger is called on shutdown of application +func CloseLogger() error { + err := GlobalLogFile.Close() + if err != nil { + return err } - return + return nil +} + +func validSubLogger(s string) (bool, *subLogger) { + if v, found := subLoggers[s]; found { + return true, v + } + return false, nil +} + +// Level retries the current sublogger levels +func Level(s string) (*Levels, error) { + found, logger := validSubLogger(s) + if !found { + return nil, fmt.Errorf("logger %v not found", s) + } + + return &logger.Levels, nil +} + +// SetLevel sets sublogger levels +func SetLevel(s, level string) (*Levels, error) { + found, logger := validSubLogger(s) + if !found { + return nil, fmt.Errorf("logger %v not found", s) + } + logger.Levels = splitLevel(level) + + return &logger.Levels, nil } diff --git a/logger/logger_levels.go b/logger/logger_levels.go deleted file mode 100644 index ce7f2c8c..00000000 --- a/logger/logger_levels.go +++ /dev/null @@ -1,33 +0,0 @@ -package logger - -import ( - "log" - "strings" -) - -func logLevel() { - clearAllLoggers() - enabledLevels := strings.Split(Logger.Level, "|") - - for x := range enabledLevels { - switch level := enabledLevels[x]; level { - case "DEBUG": - debugLogger.SetOutput(logOutput) - debugLogger.SetFlags(log.Ldate | log.Ltime) - case "INFO": - infoLogger.SetOutput(logOutput) - infoLogger.SetFlags(log.Ldate | log.Ltime) - case "WARN": - warnLogger.SetOutput(logOutput) - warnLogger.SetFlags(log.Ldate | log.Ltime) - case "ERROR": - errorLogger.SetOutput(logOutput) - errorLogger.SetFlags(log.Ldate | log.Ltime) - case "FATAL": - fatalLogger.SetOutput(logOutput) - fatalLogger.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) - default: - continue - } - } -} diff --git a/logger/logger_multiwriter.go b/logger/logger_multiwriter.go new file mode 100644 index 00000000..2d59fe62 --- /dev/null +++ b/logger/logger_multiwriter.go @@ -0,0 +1,74 @@ +package logger + +import ( + "io" +) + +// Add appends a new writer to the multiwriter slice +func (mw *multiWriter) Add(writer io.Writer) { + mw.mu.Lock() + mw.writers = append(mw.writers, writer) + mw.mu.Unlock() +} + +// Remove removes existing writer from multiwriter slice +func (mw *multiWriter) Remove(writer io.Writer) { + mw.mu.Lock() + + var removeIDs []int + for i := range mw.writers { + if mw.writers[i] == writer { + removeIDs = append(removeIDs, i) + } + } + + for x := range removeIDs { + mw.writers[x] = mw.writers[len(mw.writers)-1] + mw.writers[len(mw.writers)-1] = nil + mw.writers = mw.writers[:len(mw.writers)-1] + } + + mw.mu.Unlock() +} + +// Write concurrent safe Write for each writer +func (mw *multiWriter) Write(p []byte) (n int, err error) { + type data struct { + n int + err error + } + mw.mu.RLock() + defer mw.mu.RUnlock() + + results := make(chan data, len(mw.writers)) + + for _, wr := range mw.writers { + go func(w io.Writer, p []byte, ch chan data) { + n, err = w.Write(p) + if err != nil { + ch <- data{n, err} + return + } + if n != len(p) { + ch <- data{n, io.ErrShortWrite} + return + } + ch <- data{n, nil} + }(wr, p, results) + } + + for range mw.writers { + d := <-results + if d.err != nil { + return d.n, d.err + } + } + return len(p), nil +} + +// MultiWriter make and return a new copy of multiWriter +func MultiWriter(writers ...io.Writer) io.Writer { + w := make([]io.Writer, len(writers)) + copy(w, writers) + return &multiWriter{writers: w} +} diff --git a/logger/logger_rotate.go b/logger/logger_rotate.go new file mode 100644 index 00000000..bf42abc1 --- /dev/null +++ b/logger/logger_rotate.go @@ -0,0 +1,134 @@ +package logger + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/thrasher-corp/gocryptotrader/common/file" +) + +// Write implementation to satisfy io.Writer handles length check and rotation +func (r *Rotate) Write(output []byte) (n int, err error) { + r.mu.Lock() + defer r.mu.Unlock() + + outputLen := int64(len(output)) + + if outputLen > r.maxSize() { + return 0, fmt.Errorf( + "write length %v exceeds max file size %v", outputLen, r.maxSize(), + ) + } + + if r.output == nil { + err = r.openOrCreateFile(outputLen) + if err != nil { + return 0, err + } + } + + if *r.Rotate { + if r.size+outputLen > r.maxSize() { + err = r.rotateFile() + if err != nil { + return 0, err + } + } + } + + n, err = r.output.Write(output) + r.size += int64(n) + + return n, err +} + +func (r *Rotate) openOrCreateFile(n int64) error { + logFile := filepath.Join(LogPath, r.FileName) + + info, err := os.Stat(logFile) + if err != nil { + if os.IsNotExist(err) { + return r.openNew() + } + return fmt.Errorf("error opening log file info: %s", err) + } + + if *r.Rotate { + if info.Size()+n >= r.maxSize() { + return r.rotateFile() + } + } + + file, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return r.openNew() + } + + r.output = file + r.size = info.Size() + + return nil +} + +func (r *Rotate) openNew() error { + name := filepath.Join(LogPath, r.FileName) + _, err := os.Stat(name) + + if err == nil { + timestamp := time.Now().Format("2006-01-02T15-04-05") + newName := filepath.Join(LogPath, timestamp+"-"+r.FileName) + + err = file.Move(name, newName) + if err != nil { + return fmt.Errorf("can't rename log file: %s", err) + } + } + + file, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("can't open new logfile: %s", err) + } + + r.output = file + r.size = 0 + + return nil +} + +func (r *Rotate) close() (err error) { + if r.output == nil { + return nil + } + err = r.output.Close() + r.output = nil + return err +} + +// Close handler for open file +func (r *Rotate) Close() error { + r.mu.Lock() + defer r.mu.Unlock() + return r.close() +} + +func (r *Rotate) rotateFile() (err error) { + err = r.close() + if err != nil { + return + } + + err = r.openNew() + if err != nil { + return + } + return nil +} + +func (r *Rotate) maxSize() int64 { + if r.MaxSize == 0 { + return int64(defaultMaxSize * megabyte) + } + return r.MaxSize * int64(megabyte) +} diff --git a/logger/logger_rotate_types.go b/logger/logger_rotate_types.go new file mode 100644 index 00000000..8b298ce8 --- /dev/null +++ b/logger/logger_rotate_types.go @@ -0,0 +1,22 @@ +package logger + +import ( + "os" + "sync" +) + +const ( + defaultMaxSize = 250 + megabyte = 1024 * 1024 +) + +// Rotate struct for each instance of Rotate +type Rotate struct { + FileName string + Rotate *bool + MaxSize int64 + + size int64 + output *os.File + mu sync.Mutex +} diff --git a/logger/logger_setup.go b/logger/logger_setup.go new file mode 100644 index 00000000..05887001 --- /dev/null +++ b/logger/logger_setup.go @@ -0,0 +1,157 @@ +package logger + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "strings" +) + +func getWriters(s *SubLoggerConfig) io.Writer { + mw := MultiWriter() + m := mw.(*multiWriter) + + outputWriters := strings.Split(s.Output, "|") + for x := range outputWriters { + switch outputWriters[x] { + case "stdout", "console": + m.Add(os.Stdout) + case "stderr": + m.Add(os.Stderr) + case "file": + if FileLoggingConfiguredCorrectly { + m.Add(GlobalLogFile) + } + default: + m.Add(ioutil.Discard) + } + } + return m +} + +// GenDefaultSettings return struct with known sane/working logger settings +func GenDefaultSettings() (log Config) { + t := func(t bool) *bool { return &t }(true) + f := func(f bool) *bool { return &f }(false) + + log = Config{ + Enabled: t, + SubLoggerConfig: SubLoggerConfig{ + Level: "INFO|DEBUG|WARN|ERROR", + Output: "console", + }, + LoggerFileConfig: &loggerFileConfig{ + FileName: "log.txt", + Rotate: f, + MaxSize: 0, + }, + AdvancedSettings: advancedSettings{ + Spacer: " | ", + TimeStampFormat: timestampFormat, + Headers: headers{ + Info: "[INFO]", + Warn: "[WARN]", + Debug: "[DEBUG]", + Error: "[ERROR]", + }, + }, + } + return +} + +func configureSubLogger(logger, levels string, output io.Writer) error { + found, logPtr := validSubLogger(logger) + if !found { + return fmt.Errorf("logger %v not found", logger) + } + + logPtr.output = output + + logPtr.Levels = splitLevel(levels) + subLoggers[logger] = logPtr + + return nil +} + +// SetupSubLoggers configure all sub loggers with provided configuration values +func SetupSubLoggers(s []SubLoggerConfig) { + for x := range s { + output := getWriters(&s[x]) + err := configureSubLogger(s[x].Name, s[x].Level, output) + if err != nil { + continue + } + } +} + +// SetupGlobalLogger setup the global loggers with the default global config values +func SetupGlobalLogger() { + if FileLoggingConfiguredCorrectly { + GlobalLogFile = &Rotate{ + FileName: GlobalLogConfig.LoggerFileConfig.FileName, + MaxSize: GlobalLogConfig.LoggerFileConfig.MaxSize, + Rotate: GlobalLogConfig.LoggerFileConfig.Rotate, + } + } + + for x := range subLoggers { + subLoggers[x].Levels = splitLevel(GlobalLogConfig.Level) + subLoggers[x].output = getWriters(&GlobalLogConfig.SubLoggerConfig) + } + + logger = newLogger(GlobalLogConfig) +} + +func splitLevel(level string) (l Levels) { + enabledLevels := strings.Split(level, "|") + for x := range enabledLevels { + switch level := enabledLevels[x]; level { + case "DEBUG": + l.Debug = true + case "INFO": + l.Info = true + case "WARN": + l.Warn = true + case "ERROR": + l.Error = true + } + } + return +} + +func registerNewSubLogger(logger string) *subLogger { + temp := subLogger{ + name: logger, + output: os.Stdout, + } + + temp.Levels = splitLevel("INFO|WARN|DEBUG|ERROR") + subLoggers[logger] = &temp + + return &temp +} + +// register all loggers at package init() +func init() { + Global = registerNewSubLogger("log") + + ConnectionMgr = registerNewSubLogger("connection") + CommunicationMgr = registerNewSubLogger("comms") + ConfigMgr = registerNewSubLogger("config") + DatabaseMgr = registerNewSubLogger("database") + OrderMgr = registerNewSubLogger("order") + PortfolioMgr = registerNewSubLogger("portfolio") + SyncMgr = registerNewSubLogger("sync") + TimeMgr = registerNewSubLogger("timekeeper") + WebsocketMgr = registerNewSubLogger("websocket") + EventMgr = registerNewSubLogger("event") + DispatchMgr = registerNewSubLogger("dispatch") + + ExchangeSys = registerNewSubLogger("exchange") + GRPCSys = registerNewSubLogger("grpc") + RESTSys = registerNewSubLogger("rest") + + Ticker = registerNewSubLogger("ticker") + OrderBook = registerNewSubLogger("orderbook") +} diff --git a/logger/logger_test.go b/logger/logger_test.go index 35d0cd65..29fb2ce2 100644 --- a/logger/logger_test.go +++ b/logger/logger_test.go @@ -1,8 +1,9 @@ package logger import ( + "bytes" + "io/ioutil" "os" - "path/filepath" "testing" ) @@ -11,77 +12,237 @@ var ( falseptr = func(b bool) *bool { return &b }(false) ) -func TestCloseLogFile(t *testing.T) { - Logger = &Logging{ - Enabled: trueptr, - Level: "DEBUG", - ColourOutput: false, - File: "", - Rotate: false, +func SetupTest() { + logTest := Config{ + Enabled: trueptr, + SubLoggerConfig: SubLoggerConfig{ + Output: "console", + Level: "INFO|WARN|DEBUG|ERROR", + }, + AdvancedSettings: advancedSettings{ + Spacer: " | ", + TimeStampFormat: timestampFormat, + Headers: headers{ + Info: "[INFO]", + Warn: "[WARN]", + Debug: "[DEBUG]", + Error: "[ERROR]", + }, + }, + SubLoggers: []SubLoggerConfig{ + { + Name: "test", + Level: "INFO|DEBUG|WARN|ERROR", + Output: "stdout", + }}, } - SetupLogger() - err := CloseLogFile() - if err != nil { - t.Fatalf("CloseLogFile failed with %v", err) - } - os.Remove(filepath.Join(LogPath, Logger.File)) + + GlobalLogConfig = &logTest + SetupGlobalLogger() + SetupSubLoggers(logTest.SubLoggers) } -func TestSetupOutputsValidPath(t *testing.T) { - Logger.Enabled = trueptr - Logger.File = "debug.txt" - LogPath = "../testdata/" - err := setupOutputs() - if err != nil { - t.Fatalf("SetupOutputs failed expected nil got %v", err) +func SetupDisabled() { + logTest := Config{ + Enabled: falseptr, } - err = CloseLogFile() - if err != nil { - t.Fatalf("CloseLogFile failed with %v", err) - } + GlobalLogConfig = &logTest + SetupGlobalLogger() + SetupSubLoggers(logTest.SubLoggers) +} - err = os.Remove(filepath.Join(LogPath, Logger.File)) - if err != nil { - t.Fatal("Test Failed - SetupOutputsValidPath() error could not remove test file", err) +func BenchmarkInfo(b *testing.B) { + SetupTest() + + b.ResetTimer() + for n := 0; n < b.N; n++ { + Info(Global, "Hello this is an info benchmark") } } -func TestSetupOutputsInValidPath(t *testing.T) { - Logger.Enabled = trueptr - Logger.File = "debug.txt" - LogPath = "../testdataa/" - err := setupOutputs() - if err != nil { - if !os.IsNotExist(err) { - t.Fatalf("SetupOutputs failed expected %v got %v", os.ErrNotExist, err) - } +func SetupTestDisabled(t *testing.T) { + SetupDisabled() +} + +func TestAddWriter(t *testing.T) { + mw := MultiWriter() + m := mw.(*multiWriter) + + m.Add(ioutil.Discard) + m.Add(os.Stdin) + m.Add(os.Stdout) + + total := len(m.writers) + + if total != 3 { + t.Errorf("expected m.Writers to be 3 %v", total) } - err = os.Remove(filepath.Join(LogPath, Logger.File)) +} + +func TestRemoveWriter(t *testing.T) { + mw := MultiWriter() + m := mw.(*multiWriter) + + m.Add(ioutil.Discard) + m.Add(os.Stdin) + m.Add(os.Stdout) + + total := len(m.writers) + + m.Remove(os.Stdin) + m.Remove(os.Stdout) + + if len(m.writers) != total-2 { + t.Errorf("expected m.Writers to be %v got %v", total-2, len(m.writers)) + } +} + +func TestLevel(t *testing.T) { + SetupTest() + + _, err := Level("log") + if err != nil { + t.Errorf("Failed to get log %s levels skippin", err) + } + + _, err = Level("totallyinvalidlogger") if err == nil { - t.Fatal("Test Failed - SetupOutputsInValidPath() error cannot be nil") + t.Error("Expected error on invalid logger") } } -func BenchmarkDebugf(b *testing.B) { - Logger = &Logging{ - Enabled: trueptr, - Level: "DEBUG", - ColourOutput: false, - File: "", - Rotate: false, +func TestSetLevel(t *testing.T) { + SetupTest() + + newLevel, err := SetLevel("log", "ERROR") + if err != nil { + t.Skipf("Failed to get log %s levels skipping", err) } - SetupLogger() - b.ResetTimer() - for n := 0; n < b.N; n++ { - Debugf("This is a debug benchmark %d", n) + + if newLevel.Info || newLevel.Debug || newLevel.Warn { + t.Error("failed to set level correctly") + } + + if !newLevel.Error { + t.Error("failed to set level correctly") + } + + _, err = SetLevel("abc12345556665", "ERROR") + if err == nil { + t.Error("SetLevel() Should return error on invalid logger") } } -func BenchmarkDebugfLoggerDisabled(b *testing.B) { - clearAllLoggers() - b.ResetTimer() - for n := 0; n < b.N; n++ { - Debugf("this is a debug benchmark") +func TestValidSubLogger(t *testing.T) { + b, logPtr := validSubLogger("log") + + if !b { + t.Skip("validSubLogger() should return found, pointer if valid logger found") + } + if logPtr == nil { + t.Error("validSubLogger() should return a pointer and not nil") + } +} + +func TestCloseLogger(t *testing.T) { + err := CloseLogger() + if err != nil { + t.Errorf("CloseLogger() failed %v", err) + } +} + +func TestConfigureSubLogger(t *testing.T) { + err := configureSubLogger("log", "INFO", os.Stdin) + if err != nil { + t.Skipf("configureSubLogger() returned unexpected error %v", err) + } + if (Global.Levels != Levels{ + Info: true, + Debug: false, + }) { + t.Error("configureSubLogger() incorrectly configure subLogger") + } +} + +func TestSplitLevel(t *testing.T) { + levelsInfoDebug := splitLevel("INFO|DEBUG") + + expected := Levels{ + Info: true, + Debug: true, + Warn: false, + Error: false, + } + + if levelsInfoDebug != expected { + t.Errorf("splitLevel() returned invalid data expected: %+v got: %+v", expected, levelsInfoDebug) + } +} + +func BenchmarkInfoDisabled(b *testing.B) { + SetupDisabled() + + b.ResetTimer() + for n := 0; n < b.N; n++ { + Info(Global, "Hello this is an info benchmark") + } +} + +func BenchmarkInfof(b *testing.B) { + SetupTest() + + b.ResetTimer() + for n := 0; n < b.N; n++ { + Infof(Global, "Hello this is an infof benchmark %v %v %v\n", n, 1, 2) + } +} + +func BenchmarkInfoln(b *testing.B) { + SetupTest() + + b.ResetTimer() + for n := 0; n < b.N; n++ { + Infoln(Global, "Hello this is an infoln benchmark") + } +} + +func TestNewLogEvent(t *testing.T) { + w := &bytes.Buffer{} + logger.newLogEvent("out", "header", w) + + if w.String() == "" { + t.Error("newLogEvent() failed expected output got empty string") + } + + err := logger.newLogEvent("out", "header", nil) + if err == nil { + t.Error("Error expected with output is set to nil") + } +} + +func TestInfo(t *testing.T) { + w := &bytes.Buffer{} + + tempSL := subLogger{ + "testymctestalot", + splitLevel("INFO|WARN|DEBUG|ERROR"), + w, + } + + Info(&tempSL, "Hello") + + if w.String() == "" { + t.Error("expected Info() to write output to buffer") + } + + tempSL.output = nil + w.Reset() + + SetLevel("testymctestalot", "INFO") + Debug(&tempSL, "HelloHello") + + if w.String() != "" { + t.Error("Expected output buffer to be empty but Debug wrote to output") } } diff --git a/logger/logger_types.go b/logger/logger_types.go index 970d8900..19a8aabf 100644 --- a/logger/logger_types.go +++ b/logger/logger_types.go @@ -2,34 +2,95 @@ package logger import ( "io" - "log" - "os" + "sync" ) -// Logging struct that holds all user configurable options for the logger -type Logging struct { - Enabled *bool `json:"enabled,omitempty"` - File string `json:"file"` - ColourOutput bool `json:"colour"` - ColourOutputOverride bool `json:"colourOverride,omitempty"` - Level string `json:"level"` - Rotate bool `json:"rotate"` -} +const ( + timestampFormat = " 02/01/2006 15:04:05 " + spacer = "|" +) var ( - debugLogger *log.Logger - infoLogger *log.Logger - warnLogger *log.Logger - errorLogger *log.Logger - fatalLogger *log.Logger + logger = &Logger{} + // FileLoggingConfiguredCorrectly flag set during config check if file logging meets requirements + FileLoggingConfiguredCorrectly bool + // GlobalLogConfig holds global configuration options for logger + GlobalLogConfig = &Config{} + // GlobalLogFile hold global configuration options for file logger + GlobalLogFile = &Rotate{} - logFileHandle *os.File + eventPool = &sync.Pool{ + New: func() interface{} { + return &LogEvent{ + data: make([]byte, 0, 80), + } + }, + } - logOutput io.Writer - - // LogPath location to store logs in + // LogPath system path to store log files in LogPath string - - // Logger create a pointer to Logging struct for holding data - Logger = &Logging{} ) + +// Config holds configuration settings loaded from bot config +type Config struct { + Enabled *bool `json:"enabled"` + SubLoggerConfig + LoggerFileConfig *loggerFileConfig `json:"fileSettings,omitempty"` + AdvancedSettings advancedSettings `json:"advancedSettings"` + SubLoggers []SubLoggerConfig `json:"subloggers,omitempty"` +} + +type advancedSettings struct { + Spacer string `json:"spacer"` + TimeStampFormat string `json:"timeStampFormat"` + Headers headers `json:"headers"` +} + +type headers struct { + Info string `json:"info"` + Warn string `json:"warn"` + Debug string `json:"debug"` + Error string `json:"error"` +} + +// SubLoggerConfig holds sub logger configuration settings loaded from bot config +type SubLoggerConfig struct { + Name string `json:"name,omitempty"` + Level string `json:"level"` + Output string `json:"output"` +} + +type loggerFileConfig struct { + FileName string `json:"filename,omitempty"` + Rotate *bool `json:"rotate,omitempty"` + MaxSize int64 `json:"maxsize,omitempty"` +} + +// Logger each instance of logger settings +type Logger struct { + Timestamp string + InfoHeader, ErrorHeader, DebugHeader, WarnHeader string + Spacer string +} + +// Levels flags for each sub logger type +type Levels struct { + Info, Debug, Warn, Error bool +} + +type subLogger struct { + name string + Levels + output io.Writer +} + +// LogEvent holds the data sent to the log and which multiwriter to send to +type LogEvent struct { + data []byte + output io.Writer +} + +type multiWriter struct { + writers []io.Writer + mu sync.RWMutex +} diff --git a/logger/loggers.go b/logger/loggers.go index 7e6c1f4f..4173b437 100644 --- a/logger/loggers.go +++ b/logger/loggers.go @@ -3,78 +3,166 @@ package logger import ( "fmt" "log" - "os" ) -// Info handler takes any input returns unformatted output to infoLogger writer -func Info(v ...interface{}) { - infoLogger.Print(v...) +// Info takes a pointer subLogger struct and string sends to newLogEvent +func Info(sl *subLogger, data string) { + if sl == nil { + return + } + + if !sl.Info { + return + } + + displayError(logger.newLogEvent(data, logger.InfoHeader, sl.output)) } -// Infof handler takes any input infoLogger returns formatted output to infoLogger writer -func Infof(data string, v ...interface{}) { - infoLogger.Printf(data, v...) +// Infoln takes a pointer subLogger struct and interface sends to newLogEvent +func Infoln(sl *subLogger, v ...interface{}) { + if sl == nil { + return + } + + if !sl.Info { + return + } + + displayError(logger.newLogEvent(fmt.Sprintln(v...), logger.InfoHeader, sl.output)) } -// Infoln handler takes any input infoLogger returns formatted output to infoLogger writer -func Infoln(v ...interface{}) { - infoLogger.Println(v...) +// Infof takes a pointer subLogger struct, string & interface formats and sends to Info() +func Infof(sl *subLogger, data string, v ...interface{}) { + if sl == nil { + return + } + + if !sl.Info { + return + } + + Info(sl, fmt.Sprintf(data, v...)) } -// Print aliased to Standard log.Print -var Print = log.Print +// Debug takes a pointer subLogger struct and string sends to multiwriter +func Debug(sl *subLogger, data string) { + if sl == nil { + return + } -// Printf aliased to Standard log.Printf -var Printf = log.Printf + if !sl.Debug { + return + } -// Println aliased to Standard log.Println -var Println = log.Println - -// Debug handler takes any input returns unformatted output to infoLogger writer -func Debug(v ...interface{}) { - debugLogger.Print(v...) + displayError(logger.newLogEvent(data, logger.DebugHeader, sl.output)) } -// Debugf handler takes any input infoLogger returns formatted output to infoLogger writer -func Debugf(data string, v ...interface{}) { - debugLogger.Printf(data, v...) +// Debugln takes a pointer subLogger struct, string and interface sends to newLogEvent +func Debugln(sl *subLogger, v ...interface{}) { + if sl == nil { + return + } + + if !sl.Debug { + return + } + + displayError(logger.newLogEvent(fmt.Sprintln(v...), logger.DebugHeader, sl.output)) } -// Debugln handler takes any input infoLogger returns formatted output to infoLogger writer -func Debugln(v ...interface{}) { - debugLogger.Println(v...) +// Debugf takes a pointer subLogger struct, string & interface formats and sends to Info() +func Debugf(sl *subLogger, data string, v ...interface{}) { + if sl == nil { + return + } + + if !sl.Debug { + return + } + + Debug(sl, fmt.Sprintf(data, v...)) } -// Warn handler takes any input returns unformatted output to warnLogger writer -func Warn(v ...interface{}) { - warnLogger.Print(v...) +// Warn takes a pointer subLogger struct & string and sends to newLogEvent() +func Warn(sl *subLogger, data string) { + if sl == nil { + return + } + + if !sl.Warn { + return + } + + displayError(logger.newLogEvent(data, logger.WarnHeader, sl.output)) } -// Warnf handler takes any input returns unformatted output to warnLogger writer -func Warnf(data string, v ...interface{}) { - warnLogger.Printf(data, v...) +// Warnln takes a pointer subLogger struct & interface formats and sends to newLogEvent() +func Warnln(sl *subLogger, v ...interface{}) { + if sl == nil { + return + } + + if !sl.Warn { + return + } + + displayError(logger.newLogEvent(fmt.Sprintln(v...), logger.WarnHeader, sl.output)) } -// Error handler takes any input returns unformatted output to errorLogger writer -func Error(v ...interface{}) { - errorLogger.Print(v...) +// Warnf takes a pointer subLogger struct, string & interface formats and sends to Warn() +func Warnf(sl *subLogger, data string, v ...interface{}) { + if sl == nil { + return + } + + if !sl.Warn { + return + } + + Warn(sl, fmt.Sprintf(data, v...)) } -// Errorf handler takes any input returns unformatted output to errorLogger writer -func Errorf(data string, v ...interface{}) { - errorLogger.Printf(data, v...) +// Error takes a pointer subLogger struct & interface formats and sends to newLogEvent() +func Error(sl *subLogger, data ...interface{}) { + if sl == nil { + return + } + + if !sl.Error { + return + } + + displayError(logger.newLogEvent(fmt.Sprint(data...), logger.ErrorHeader, sl.output)) } -// Fatal handler takes any input returns unformatted output to fatalLogger writer -func Fatal(v ...interface{}) { - // Send to Output instead of Fatal to allow us to increase the output depth by 1 to make sure the correct file is displayed - fatalLogger.Output(2, fmt.Sprint(v...)) - os.Exit(1) +// Errorln takes a pointer subLogger struct, string & interface formats and sends to newLogEvent() +func Errorln(sl *subLogger, v ...interface{}) { + if sl == nil { + return + } + + if !sl.Error { + return + } + + displayError(logger.newLogEvent(fmt.Sprintln(v...), logger.ErrorHeader, sl.output)) } -// Fatalf handler takes any input returns unformatted output to fatalLogger writer -func Fatalf(data string, v ...interface{}) { - // Send to Output instead of Fatal to allow us to increase the output depth by 1 to make sure the correct file is displayed - fatalLogger.Output(2, fmt.Sprintf(data, v...)) - os.Exit(1) +// Errorf takes a pointer subLogger struct, string & interface formats and sends to Debug() +func Errorf(sl *subLogger, data string, v ...interface{}) { + if sl == nil { + return + } + + if !sl.Error { + return + } + + Error(sl, fmt.Sprintf(data, v...)) +} + +func displayError(err error) { + if err != nil { + log.Printf("Logger write error: %v\n", err) + } } diff --git a/logger/sublogger_types.go b/logger/sublogger_types.go new file mode 100644 index 00000000..7bbb027f --- /dev/null +++ b/logger/sublogger_types.go @@ -0,0 +1,26 @@ +package logger + +//nolint +var ( + subLoggers = map[string]*subLogger{} + + Global *subLogger + ConnectionMgr *subLogger + CommunicationMgr *subLogger + ConfigMgr *subLogger + DatabaseMgr *subLogger + OrderMgr *subLogger + PortfolioMgr *subLogger + SyncMgr *subLogger + TimeMgr *subLogger + WebsocketMgr *subLogger + EventMgr *subLogger + DispatchMgr *subLogger + + ExchangeSys *subLogger + GRPCSys *subLogger + RESTSys *subLogger + + Ticker *subLogger + OrderBook *subLogger +) diff --git a/main.go b/main.go index 725b5730..fdb02f25 100644 --- a/main.go +++ b/main.go @@ -3,295 +3,113 @@ package main import ( "flag" "fmt" - "net/http" "os" - "os/signal" "runtime" - "strconv" - "sync" - "syscall" "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/communications" - "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/connchecker" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/currency/coinmarketcap" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/core" + "github.com/thrasher-corp/gocryptotrader/dispatch" + "github.com/thrasher-corp/gocryptotrader/engine" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" log "github.com/thrasher-corp/gocryptotrader/logger" - "github.com/thrasher-corp/gocryptotrader/ntpclient" - "github.com/thrasher-corp/gocryptotrader/portfolio" + "github.com/thrasher-corp/gocryptotrader/signaler" ) -// Bot contains configuration, portfolio, exchange & ticker data and is the -// overarching type across this code base. -type Bot struct { - config *config.Config - portfolio *portfolio.Base - exchanges []exchange.IBotExchange - comms *communications.Communications - shutdown chan bool - dryRun bool - configFile string - dataDir string - connectivity *connchecker.Checker - sync.Mutex -} - -const banner = ` - ______ ______ __ ______ __ - / ____/____ / ____/_____ __ __ ____ / /_ ____ /_ __/_____ ______ ____/ /___ _____ - / / __ / __ \ / / / ___// / / // __ \ / __// __ \ / / / ___// __ // __ // _ \ / ___/ -/ /_/ // /_/ // /___ / / / /_/ // /_/ // /_ / /_/ // / / / / /_/ // /_/ // __// / -\____/ \____/ \____//_/ \__, // .___/ \__/ \____//_/ /_/ \__,_/ \__,_/ \___//_/ - /____//_/ -` - -var bot Bot - func main() { - bot.shutdown = make(chan bool) - HandleInterrupt() - - defaultPath, err := config.GetFilePath("") - if err != nil { - log.Fatal(err) - } - // Handle flags - flag.StringVar(&bot.configFile, "config", defaultPath, "config file to load") - flag.StringVar(&bot.dataDir, "datadir", common.GetDefaultDataDir(runtime.GOOS), "default data directory for GoCryptoTrader files") - dryrun := flag.Bool("dryrun", false, "dry runs bot, doesn't save config file") - version := flag.Bool("version", false, "retrieves current GoCryptoTrader version") - verbosity := flag.Bool("verbose", false, "increases logging verbosity for GoCryptoTrader") + var settings engine.Settings + versionFlag := flag.Bool("version", false, "retrieves current GoCryptoTrader version") - Coinmarketcap := flag.Bool("c", false, "overrides config and runs currency analaysis") - FxCurrencyConverter := flag.Bool("fxa", false, "overrides config and sets up foreign exchange Currency Converter") - FxCurrencyLayer := flag.Bool("fxb", false, "overrides config and sets up foreign exchange Currency Layer") - FxFixer := flag.Bool("fxc", false, "overrides config and sets up foreign exchange Fixer.io") - FxOpenExchangeRates := flag.Bool("fxd", false, "overrides config and sets up foreign exchange Open Exchange Rates") + // Core settings + flag.StringVar(&settings.ConfigFile, "config", "", "config file to load") + flag.StringVar(&settings.DataDir, "datadir", common.GetDefaultDataDir(runtime.GOOS), "default data directory for GoCryptoTrader files") + flag.IntVar(&settings.GoMaxProcs, "gomaxprocs", runtime.GOMAXPROCS(-1), "sets the runtime GOMAXPROCS value") + flag.BoolVar(&settings.EnableDryRun, "dryrun", false, "dry runs bot, doesn't save config file") + flag.BoolVar(&settings.EnableAllExchanges, "enableallexchanges", false, "enables all exchanges") + flag.BoolVar(&settings.EnableAllPairs, "enableallpairs", false, "enables all pairs for enabled exchanges") + flag.BoolVar(&settings.EnablePortfolioManager, "portfoliomanager", true, "enables the portfolio manager") + flag.BoolVar(&settings.EnableGRPC, "grpc", true, "enables the grpc server") + flag.BoolVar(&settings.EnableGRPCProxy, "grpcproxy", false, "enables the grpc proxy server") + flag.BoolVar(&settings.EnableWebsocketRPC, "websocketrpc", true, "enables the websocket RPC server") + flag.BoolVar(&settings.EnableDeprecatedRPC, "deprecatedrpc", true, "enables the deprecated RPC server") + flag.BoolVar(&settings.EnableCommsRelayer, "enablecommsrelayer", true, "enables available communications relayer") + flag.BoolVar(&settings.Verbose, "verbose", false, "increases logging verbosity for GoCryptoTrader") + flag.BoolVar(&settings.EnableExchangeSyncManager, "syncmanager", true, "enables to exchange sync manager") + flag.BoolVar(&settings.EnableWebsocketRoutine, "websocketroutine", true, "enables the websocket routine for all loaded exchanges") + flag.BoolVar(&settings.EnableCoinmarketcapAnalysis, "coinmarketcap", false, "overrides config and runs currency analysis") + flag.BoolVar(&settings.EnableEventManager, "eventmanager", true, "enables the event manager") + flag.BoolVar(&settings.EnableOrderManager, "ordermanager", true, "enables the order manager") + flag.BoolVar(&settings.EnableDepositAddressManager, "depositaddressmanager", true, "enables the deposit address manager") + flag.BoolVar(&settings.EnableConnectivityMonitor, "connectivitymonitor", true, "enables the connectivity monitor") + flag.BoolVar(&settings.EnableDatabaseManager, "databasemanager", true, "enables database manager") + flag.DurationVar(&settings.EventManagerDelay, "eventmanagerdelay", time.Duration(0), "sets the event managers sleep delay between event checking") + flag.BoolVar(&settings.EnableNTPClient, "ntpclient", true, "enables the NTP client to check system clock drift") + flag.BoolVar(&settings.EnableDispatcher, "dispatch", true, "enables the dispatch system") + flag.IntVar(&settings.DispatchMaxWorkerAmount, "dispatchworkers", dispatch.DefaultMaxWorkers, "sets the dispatch package max worker generation limit") + flag.IntVar(&settings.DispatchJobsLimit, "dispatchjobslimit", dispatch.DefaultJobsLimit, "sets the dispatch package max jobs limit") + + // Exchange syncer settings + flag.BoolVar(&settings.EnableTickerSyncing, "tickersync", true, "enables ticker syncing for all enabled exchanges") + flag.BoolVar(&settings.EnableOrderbookSyncing, "orderbooksync", true, "enables orderbook syncing for all enabled exchanges") + flag.BoolVar(&settings.EnableTradeSyncing, "tradesync", false, "enables trade syncing for all enabled exchanges") + flag.IntVar(&settings.SyncWorkers, "syncworkers", engine.DefaultSyncerWorkers, "the amount of workers (goroutines) to use for syncing exchange data") + flag.BoolVar(&settings.SyncContinuously, "synccontinuously", true, "whether to sync exchange data continuously (ticker, orderbook and trade history info") + flag.DurationVar(&settings.SyncTimeout, "synctimeout", engine.DefaultSyncerTimeout, + "the amount of time before the syncer will switch from one protocol to the other (e.g. from REST to websocket)") + + // Forex provider settings + flag.BoolVar(&settings.EnableCurrencyConverter, "currencyconverter", false, "overrides config and sets up foreign exchange Currency Converter") + flag.BoolVar(&settings.EnableCurrencyLayer, "currencylayer", false, "overrides config and sets up foreign exchange Currency Layer") + flag.BoolVar(&settings.EnableFixer, "fixer", false, "overrides config and sets up foreign exchange Fixer.io") + flag.BoolVar(&settings.EnableOpenExchangeRates, "openexchangerates", false, "overrides config and sets up foreign exchange Open Exchange Rates") + + // Exchange tuning settings + flag.BoolVar(&settings.EnableExchangeAutoPairUpdates, "exchangeautopairupdates", false, "enables automatic available currency pair updates for supported exchanges") + flag.BoolVar(&settings.DisableExchangeAutoPairUpdates, "exchangedisableautopairupdates", false, "disables exchange auto pair updates") + flag.BoolVar(&settings.EnableExchangeWebsocketSupport, "exchangewebsocketsupport", false, "enables Websocket support for exchanges") + flag.BoolVar(&settings.EnableExchangeRESTSupport, "exchangerestsupport", true, "enables REST support for exchanges") + flag.BoolVar(&settings.EnableExchangeVerbose, "exchangeverbose", false, "increases exchange logging verbosity") + flag.BoolVar(&settings.ExchangePurgeCredentials, "exchangepurgecredentials", false, "purges the stored exchange API credentials") + flag.BoolVar(&settings.EnableExchangeHTTPRateLimiter, "ratelimiter", true, "enables the rate limiter for HTTP requests") + flag.IntVar(&settings.MaxHTTPRequestJobsLimit, "requestjobslimit", request.DefaultMaxRequestJobs, "sets the max amount of jobs the HTTP request package stores") + flag.IntVar(&settings.RequestTimeoutRetryAttempts, "exchangehttptimeoutretryattempts", request.DefaultTimeoutRetryAttempts, "sets the amount of retry attempts after a HTTP request times out") + flag.DurationVar(&settings.ExchangeHTTPTimeout, "exchangehttptimeout", time.Duration(0), "sets the exchangs HTTP timeout value for HTTP requests") + flag.StringVar(&settings.ExchangeHTTPUserAgent, "exchangehttpuseragent", "", "sets the exchanges HTTP user agent") + flag.StringVar(&settings.ExchangeHTTPProxy, "exchangehttpproxy", "", "sets the exchanges HTTP proxy server") + flag.BoolVar(&settings.EnableExchangeHTTPDebugging, "exchangehttpdebugging", false, "sets the exchanges HTTP debugging") + + // Common tuning settings + flag.DurationVar(&settings.GlobalHTTPTimeout, "globalhttptimeout", time.Duration(0), "sets common HTTP timeout value for HTTP requests") + flag.StringVar(&settings.GlobalHTTPUserAgent, "globalhttpuseragent", "", "sets the common HTTP client's user agent") + flag.StringVar(&settings.GlobalHTTPProxy, "globalhttpproxy", "", "sets the common HTTP client's proxy server") flag.Parse() - if *version { - fmt.Print(BuildVersion(true)) + if *versionFlag { + fmt.Print(core.Version(true)) os.Exit(0) } - if *dryrun { - bot.dryRun = true - } + fmt.Println(core.Banner) + fmt.Println(core.Version(false)) - fmt.Println(banner) - fmt.Println(BuildVersion(false)) - - bot.config = &config.Cfg - log.Debugf("Loading config file %s..\n", bot.configFile) - err = bot.config.LoadConfig(bot.configFile) - if err != nil { - log.Fatalf("Failed to load config. Err: %s", err) - } - - err = common.CreateDir(bot.dataDir) - if err != nil { - log.Fatalf("Failed to open/create data directory: %s. Err: %s", bot.dataDir, err) - } - log.Debugf("Using data directory: %s.\n", bot.dataDir) - - err = bot.config.CheckLoggerConfig() - if err != nil { - log.Errorf("Failed to configure logger reason: %s", err) - } - - err = log.SetupLogger() - if err != nil { - log.Errorf("Failed to setup logger reason: %s", err) - } - - ActivateNTP() - ActivateConnectivityMonitor() - AdjustGoMaxProcs() - - log.Debugf("Bot '%s' started.\n", bot.config.Name) - log.Debugf("Bot dry run mode: %v.\n", common.IsEnabled(bot.dryRun)) - - log.Debugf("Available Exchanges: %d. Enabled Exchanges: %d.\n", - len(bot.config.Exchanges), - bot.config.CountEnabledExchanges()) - - common.HTTPClient = common.NewHTTPClientWithTimeout(bot.config.GlobalHTTPTimeout) - log.Debugf("Global HTTP request timeout: %v.\n", common.HTTPClient.Timeout) - - SetupExchanges() - if len(bot.exchanges) == 0 { - log.Fatal("No exchanges were able to be loaded. Exiting") - } - - log.Debugf("Starting communication mediums..") - cfg := bot.config.GetCommunicationsConfig() - bot.comms = communications.NewComm(&cfg) - bot.comms.GetEnabledCommunicationMediums() - - var newFxSettings []currency.FXSettings - for _, d := range bot.config.Currency.ForexProviders { - newFxSettings = append(newFxSettings, currency.FXSettings(d)) - } - - err = currency.RunStorageUpdater(currency.BotOverrides{ - Coinmarketcap: *Coinmarketcap, - FxCurrencyConverter: *FxCurrencyConverter, - FxCurrencyLayer: *FxCurrencyLayer, - FxFixer: *FxFixer, - FxOpenExchangeRates: *FxOpenExchangeRates, - }, - ¤cy.MainConfiguration{ - ForexProviders: newFxSettings, - CryptocurrencyProvider: coinmarketcap.Settings(bot.config.Currency.CryptocurrencyProvider), - Cryptocurrencies: bot.config.Currency.Cryptocurrencies, - FiatDisplayCurrency: bot.config.Currency.FiatDisplayCurrency, - CurrencyDelay: bot.config.Currency.CurrencyFileUpdateDuration, - FxRateDelay: bot.config.Currency.ForeignExchangeUpdateDuration, - }, - bot.dataDir, - *verbosity) - if err != nil { - log.Fatalf("currency updater system failed to start %v", err) - } - - bot.portfolio = &portfolio.Portfolio - bot.portfolio.SeedPortfolio(bot.config.Portfolio) - SeedExchangeAccountInfo(GetAllEnabledExchangeAccountInfo().Data) - - ActivateWebServer() - - go portfolio.StartPortfolioWatcher() - - go TickerUpdaterRoutine() - go OrderbookUpdaterRoutine() - go WebsocketRoutine(*verbosity) - - <-bot.shutdown - Shutdown() -} - -// ActivateWebServer Sets up a local web server -func ActivateWebServer() { - if bot.config.Webserver.Enabled { - listenAddr := bot.config.Webserver.ListenAddress - log.Debugf( - "HTTP Webserver support enabled. Listen URL: http://%s:%d/\n", - common.ExtractHost(listenAddr), common.ExtractPort(listenAddr), - ) - - router := NewRouter() - go func() { - err := http.ListenAndServe(listenAddr, router) - if err != nil { - log.Fatal(err) - } - }() - - log.Debugln("HTTP Webserver started successfully.") - log.Debugln("Starting websocket handler.") - StartWebsocketHandler() - } else { - log.Debugln("HTTP RESTful Webserver support disabled.") - } -} - -// ActivateConnectivityMonitor Sets up internet connectivity monitor -func ActivateConnectivityMonitor() { var err error - bot.connectivity, err = connchecker.New(bot.config.ConnectionMonitor.DNSList, - bot.config.ConnectionMonitor.PublicDomainList, - bot.config.ConnectionMonitor.CheckInterval) - if err != nil { - log.Fatalf("Connectivity checker failure: %s", err) + settings.CheckParamInteraction = true + engine.Bot, err = engine.NewFromSettings(&settings) + if engine.Bot == nil || err != nil { + log.Errorf(log.Global, "Unable to initialise bot engine. Error: %s\n", err) + os.Exit(1) } -} - -// ActivateNTP Sets up NTP client -func ActivateNTP() { - if bot.config.NTPClient.Level != -1 { - bot.config.CheckNTPConfig() - NTPTime, errNTP := ntpclient.NTPClient(bot.config.NTPClient.Pool) - currentTime := time.Now() - if errNTP != nil { - log.Warnf("NTPClient failed to create: %v", errNTP) - } else { - NTPcurrentTimeDifference := NTPTime.Sub(currentTime) - configNTPTime := *bot.config.NTPClient.AllowedDifference - configNTPNegativeTime := (*bot.config.NTPClient.AllowedNegativeDifference - (*bot.config.NTPClient.AllowedNegativeDifference * 2)) - if NTPcurrentTimeDifference > configNTPTime || NTPcurrentTimeDifference < configNTPNegativeTime { - log.Warnf("Time out of sync (NTP): %v | (time.Now()): %v | (Difference): %v | (Allowed): +%v / %v", NTPTime, currentTime, NTPcurrentTimeDifference, configNTPTime, configNTPNegativeTime) - if *bot.config.Logging.Enabled && bot.config.NTPClient.Level == 0 { - disable, errNTP := bot.config.DisableNTPCheck(os.Stdin) - if errNTP != nil { - log.Errorf("failed to disable ntp time check reason: %v", errNTP) - } else { - log.Info(disable) - } - } - } - } - } -} - -// AdjustGoMaxProcs adjusts the maximum processes that the CPU can handle. -func AdjustGoMaxProcs() { - log.Debugln("Adjusting bot runtime performance..") - maxProcsEnv := os.Getenv("GOMAXPROCS") - maxProcs := runtime.NumCPU() - log.Debugln("Number of CPU's detected:", maxProcs) - - if maxProcsEnv != "" { - log.Debugln("GOMAXPROCS env =", maxProcsEnv) - env, err := strconv.Atoi(maxProcsEnv) - if err != nil { - log.Debugf("Unable to convert GOMAXPROCS to int, using %d", maxProcs) - } else { - maxProcs = env - } - } - if i := runtime.GOMAXPROCS(maxProcs); i != maxProcs { - log.Error("Go Max Procs were not set correctly.") - } - log.Debugln("Set GOMAXPROCS to:", maxProcs) -} - -// HandleInterrupt monitors and captures the SIGTERM in a new goroutine then -// shuts down bot -func HandleInterrupt() { - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - go func() { - sig := <-c - log.Debugf("Captured %v, shutdown requested.", sig) - close(bot.shutdown) - }() -} - -// Shutdown correctly shuts down bot saving configuration files -func Shutdown() { - log.Debugln("Bot shutting down..") - - if len(portfolio.Portfolio.Addresses) != 0 { - bot.config.Portfolio = portfolio.Portfolio - } - - if !bot.dryRun { - err := bot.config.SaveConfig(bot.configFile) - - if err != nil { - log.Warn("Unable to save config.") - } else { - log.Debugln("Config file saved successfully.") - } - } - - log.Debugln("Exiting.") - - log.CloseLogFile() - os.Exit(0) + + engine.PrintSettings(&engine.Bot.Settings) + if err = engine.Bot.Start(); err != nil { + log.Errorf(log.Global, "Unable to start bot engine. Error: %s\n", err) + os.Exit(1) + } + + interrupt := signaler.WaitForInterrupt() + log.Infof(log.Global, "Captured %v, shutdown requested.\n", interrupt) + engine.Bot.Stop() + log.Infoln(log.Global, "Exiting.") } diff --git a/ntpclient/ntpclient.go b/ntpclient/ntpclient.go index 0683e72e..2f239f7a 100644 --- a/ntpclient/ntpclient.go +++ b/ntpclient/ntpclient.go @@ -32,7 +32,7 @@ func NTPClient(pool []string) (time.Time, error) { for i := range pool { con, err := net.Dial("udp", pool[i]) if err != nil { - log.Warnf("Unable to connect to hosts %v attempting next", pool[i]) + log.Warnf(log.TimeMgr, "Unable to connect to hosts %v attempting next", pool[i]) continue } diff --git a/ntpclient/ntpclient_test.go b/ntpclient/ntpclient_test.go index 0668409e..6fe5ee5e 100644 --- a/ntpclient/ntpclient_test.go +++ b/ntpclient/ntpclient_test.go @@ -14,7 +14,7 @@ func TestNTPClient(t *testing.T) { invalidpool := []string{"pool.thisisinvalid.org"} _, err = NTPClient(invalidpool) if err == nil { - t.Errorf("failed to get time %v", err) + t.Errorf("Expected error") } firstInvalid := []string{"pool.thisisinvalid.org", "pool.ntp.org:123", "0.pool.ntp.org:123"} diff --git a/portfolio/README.md b/portfolio/README.md index e9f77b9d..cf5b3579 100644 --- a/portfolio/README.md +++ b/portfolio/README.md @@ -42,4 +42,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/portfolio/portfolio.go b/portfolio/portfolio.go index e645e6b3..5f4cd676 100644 --- a/portfolio/portfolio.go +++ b/portfolio/portfolio.go @@ -3,6 +3,7 @@ package portfolio import ( "errors" "fmt" + "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" @@ -148,10 +149,17 @@ func (p *Base) UpdateExchangeAddressBalance(exchangeName string, coinType curren } // AddAddress adds an address to the portfolio base -func (p *Base) AddAddress(address, description string, coinType currency.Code, balance float64) { +func (p *Base) AddAddress(address, description string, coinType currency.Code, balance float64) error { + if address == "" { + return errors.New("address is empty") + } + + if coinType.String() == "" { + return errors.New("coin type is empty") + } + if description == PortfolioAddressExchange { p.AddExchangeAddress(address, coinType, balance) - return } if !p.AddressExists(address) { p.Addresses = append( @@ -165,25 +173,36 @@ func (p *Base) AddAddress(address, description string, coinType currency.Code, b p.UpdateAddressBalance(address, balance) } } + return nil } // RemoveAddress removes an address when checked against the correct address and // coinType -func (p *Base) RemoveAddress(address, description string, coinType currency.Code) { +func (p *Base) RemoveAddress(address, description string, coinType currency.Code) error { + if address == "" { + return errors.New("address is empty") + } + + if coinType.String() == "" { + return errors.New("coin type is empty") + } + for x := range p.Addresses { if p.Addresses[x].Address == address && p.Addresses[x].CoinType == coinType && p.Addresses[x].Description == description { p.Addresses = append(p.Addresses[:x], p.Addresses[x+1:]...) - return + return nil } } + + return errors.New("portfolio item does not exist") } // UpdatePortfolio adds to the portfolio addresses by coin type func (p *Base) UpdatePortfolio(addresses []string, coinType currency.Code) bool { - if common.StringContains(common.JoinStrings(addresses, ","), PortfolioAddressExchange) || - common.StringContains(common.JoinStrings(addresses, ","), PortfolioAddressPersonal) { + if strings.Contains(strings.Join(addresses, ","), PortfolioAddressExchange) || + strings.Contains(strings.Join(addresses, ","), PortfolioAddressPersonal) { return true } @@ -224,7 +243,7 @@ func (p *Base) UpdatePortfolio(addresses []string, coinType currency.Code) bool func (p *Base) GetPortfolioByExchange(exchangeName string) map[currency.Code]float64 { result := make(map[currency.Code]float64) for x := range p.Addresses { - if common.StringContains(p.Addresses[x].Address, exchangeName) { + if strings.Contains(p.Addresses[x].Address, exchangeName) { result[p.Addresses[x].CoinType] = p.Addresses[x].Balance } } @@ -379,7 +398,7 @@ func (p *Base) GetPortfolioSummary() Summary { func (p *Base) GetPortfolioGroupedCoin() map[currency.Code][]string { result := make(map[currency.Code][]string) for _, x := range p.Addresses { - if common.StringContains(x.Description, PortfolioAddressExchange) { + if strings.Contains(x.Description, PortfolioAddressExchange) { continue } result[x.CoinType] = append(result[x.CoinType], x.Address) @@ -387,16 +406,16 @@ func (p *Base) GetPortfolioGroupedCoin() map[currency.Code][]string { return result } -// SeedPortfolio appends a portfolio base object with another base portfolio +// Seed appends a portfolio base object with another base portfolio // addresses -func (p *Base) SeedPortfolio(port Base) { +func (p *Base) Seed(port Base) { p.Addresses = port.Addresses } // StartPortfolioWatcher observes the portfolio object func StartPortfolioWatcher() { addrCount := len(Portfolio.Addresses) - log.Debugf( + log.Debugf(log.PortfolioMgr, "PortfolioWatcher started: Have %d entries in portfolio.\n", addrCount, ) for { @@ -404,7 +423,7 @@ func StartPortfolioWatcher() { for key, value := range data { success := Portfolio.UpdatePortfolio(value, key) if success { - log.Debugf( + log.Debugf(log.PortfolioMgr, "PortfolioWatcher: Successfully updated address balance for %s address(es) %s\n", key, value, ) diff --git a/portfolio/portfolio_test.go b/portfolio/portfolio_test.go index 2bc79317..2b8029de 100644 --- a/portfolio/portfolio_test.go +++ b/portfolio/portfolio_test.go @@ -14,16 +14,16 @@ func TestGetEthereumBalance(t *testing.T) { response, err := GetEthereumBalance(address) if err != nil { - t.Errorf("Test Failed - Portfolio GetEthereumBalance() Error: %s", err) + t.Errorf("Portfolio GetEthereumBalance() Error: %s", err) } if response.Address != "0xb794f5ea0ba39494ce839613fffba74279579268" { - t.Error("Test Failed - Portfolio GetEthereumBalance() address invalid") + t.Error("Portfolio GetEthereumBalance() address invalid") } response, err = GetEthereumBalance(nonsenseAddress) if response.Error.Message != "" || err == nil { - t.Errorf("Test Failed - Portfolio GetEthereumBalance() Error: %s", + t.Errorf("Portfolio GetEthereumBalance() Error: %s", response.Error.Message) } } @@ -32,7 +32,7 @@ func TestGetCryptoIDBalance(t *testing.T) { ltcAddress := "LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1" _, err := GetCryptoIDAddress(ltcAddress, currency.LTC) if err != nil { - t.Fatalf("Test failed. TestGetCryptoIDBalance error: %s", err) + t.Fatalf("TestGetCryptoIDBalance error: %s", err) } } @@ -50,7 +50,7 @@ func TestGetAddressBalance(t *testing.T) { ltc) if addBalance != balance { - t.Error("Test Failed - Portfolio GetAddressBalance() Error: Incorrect value") + t.Error("Portfolio GetAddressBalance() Error: Incorrect value") } addBalance, found := portfolio.GetAddressBalance("WigWham", @@ -58,10 +58,10 @@ func TestGetAddressBalance(t *testing.T) { ltc) if addBalance != 0 { - t.Error("Test Failed - Portfolio GetAddressBalance() Error: Incorrect value") + t.Error("Portfolio GetAddressBalance() Error: Incorrect value") } if found { - t.Error("Test Failed - Portfolio GetAddressBalance() Error: Incorrect value") + t.Error("Portfolio GetAddressBalance() Error: Incorrect value") } } @@ -73,10 +73,10 @@ func TestExchangeExists(t *testing.T) { 0.02) if !newBase.ExchangeExists("someaddress") { - t.Error("Test Failed - portfolio_test.go - AddressExists error") + t.Error("portfolio_test.go - AddressExists error") } if newBase.ExchangeExists("bla") { - t.Error("Test Failed - portfolio_test.go - AddressExists error") + t.Error("portfolio_test.go - AddressExists error") } } @@ -88,10 +88,10 @@ func TestAddressExists(t *testing.T) { 0.02) if !newbase.AddressExists("someaddress") { - t.Error("Test Failed - portfolio_test.go - AddressExists error") + t.Error("portfolio_test.go - AddressExists error") } if newbase.AddressExists("bla") { - t.Error("Test Failed - portfolio_test.go - AddressExists error") + t.Error("portfolio_test.go - AddressExists error") } } @@ -103,10 +103,10 @@ func TestExchangeAddressExists(t *testing.T) { 0.02) if !newbase.ExchangeAddressExists("someaddress", currency.LTC) { - t.Error("Test Failed - portfolio_test.go - ExchangeAddressExists error") + t.Error("portfolio_test.go - ExchangeAddressExists error") } if newbase.ExchangeAddressExists("TEST", currency.LTC) { - t.Error("Test Failed - portfolio_test.go - ExchangeAddressExists error") + t.Error("portfolio_test.go - ExchangeAddressExists error") } } @@ -116,7 +116,7 @@ func TestAddExchangeAddress(t *testing.T) { newbase.AddExchangeAddress("ANX", currency.BTC, 200) if !newbase.ExchangeAddressExists("ANX", currency.BTC) { - t.Error("Test Failed - TestExchangeAddressExists address doesn't exist") + t.Error("TestExchangeAddressExists address doesn't exist") } } @@ -132,26 +132,38 @@ func TestUpdateAddressBalance(t *testing.T) { value := newbase.GetPortfolioSummary() if value.Totals[0].Coin != currency.LTC && value.Totals[0].Balance != 0.03 { - t.Error("Test Failed - portfolio_test.go - UpdateUpdateAddressBalance error") + t.Error("portfolio_test.go - UpdateUpdateAddressBalance error") } } func TestRemoveAddress(t *testing.T) { - newbase := Base{} + var newbase Base + if err := newbase.RemoveAddress("", "MEOW", currency.LTC); err == nil { + t.Error("invalid address should throw an error") + } + + if err := newbase.RemoveAddress("Gibson", "", currency.NewCode("")); err == nil { + t.Error("invalid coin type should throw an error") + } + + if err := newbase.RemoveAddress("HIDDENERINO", "MEOW", currency.LTC); err == nil { + t.Error("non-existent address should throw an error") + } + newbase.AddAddress("someaddr", currency.LTC.String(), currency.NewCode("LTCWALLETTEST"), 420) if !newbase.AddressExists("someaddr") { - t.Error("Test failed - portfolio_test.go - TestRemoveAddress") + t.Error("portfolio_test.go - TestRemoveAddress") } newbase.RemoveAddress("someaddr", currency.LTC.String(), currency.NewCode("LTCWALLETTEST")) if newbase.AddressExists("someaddr") { - t.Error("Test failed - portfolio_test.go - TestRemoveAddress") + t.Error("portfolio_test.go - TestRemoveAddress") } } @@ -163,12 +175,12 @@ func TestRemoveExchangeAddress(t *testing.T) { newbase.AddExchangeAddress(exchangeName, coinType, 420) if !newbase.ExchangeAddressExists(exchangeName, coinType) { - t.Error("Test failed - portfolio_test.go - TestRemoveAddress") + t.Error("portfolio_test.go - TestRemoveAddress") } newbase.RemoveExchangeAddress(exchangeName, coinType) if newbase.ExchangeAddressExists(exchangeName, coinType) { - t.Error("Test failed - portfolio_test.go - TestRemoveAddress") + t.Error("portfolio_test.go - TestRemoveAddress") } } @@ -176,26 +188,50 @@ func TestUpdateExchangeAddressBalance(t *testing.T) { newbase := Base{} newbase.AddExchangeAddress("someaddress", currency.LTC, 0.02) portfolio := GetPortfolio() - portfolio.SeedPortfolio(newbase) + portfolio.Seed(newbase) portfolio.UpdateExchangeAddressBalance("someaddress", currency.LTC, 0.04) value := portfolio.GetPortfolioSummary() if value.Totals[0].Coin != currency.LTC && value.Totals[0].Balance != 0.04 { - t.Error("Test Failed - portfolio_test.go - UpdateExchangeAddressBalance error") + t.Error("portfolio_test.go - UpdateExchangeAddressBalance error") } } func TestAddAddress(t *testing.T) { - newbase := Base{} + var newbase Base + if err := newbase.AddAddress("", "MEOW", currency.LTC, 1); err == nil { + t.Error("invalid address should throw an error") + } + + if err := newbase.AddAddress("Gibson", "", currency.NewCode(""), 1); err == nil { + t.Error("invalid coin type should throw an error") + } + + // test adding an exchange address + err := newbase.AddAddress("COINUT", PortfolioAddressExchange, currency.LTC, 0) + if err != nil { + t.Errorf("failed to add address: %v", err) + } + + // add a test portfolio address and amount newbase.AddAddress("Gibson", currency.LTC.String(), currency.NewCode("LTCWALLETTEST"), 0.02) + // test updating the balance and make sure it's reflected + newbase.AddAddress("Gibson", currency.LTC.String(), + currency.NewCode("LTCWALLETTEST"), 0.05) + b, _ := newbase.GetAddressBalance("Gibson", "LTC", + currency.NewCode("LTCWALLETTEST")) + if b != 0.05 { + t.Error("invalid portfolio amount") + } + portfolio := GetPortfolio() - portfolio.SeedPortfolio(newbase) + portfolio.Seed(newbase) if !portfolio.AddressExists("Gibson") { - t.Error("Test Failed - portfolio_test.go - AddAddress error") + t.Error("portfolio_test.go - AddAddress error") } // Test updating balance to <= 0, expected result is to remove the address. @@ -206,7 +242,7 @@ func TestAddAddress(t *testing.T) { -1) if newbase.AddressExists("Gibson") { - t.Error("Test Failed - portfolio_test.go - AddAddress error") + t.Error("portfolio_test.go - AddAddress error") } } @@ -218,30 +254,30 @@ func TestUpdatePortfolio(t *testing.T) { 0.02) portfolio := GetPortfolio() - portfolio.SeedPortfolio(newbase) + portfolio.Seed(newbase) value := portfolio.UpdatePortfolio( []string{"LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL"}, currency.LTC, ) if !value { - t.Error("Test Failed - portfolio_test.go - UpdatePortfolio error") + t.Error("portfolio_test.go - UpdatePortfolio error") } value = portfolio.UpdatePortfolio([]string{"Testy"}, currency.LTC) if value { - t.Error("Test Failed - portfolio_test.go - UpdatePortfolio error") + t.Error("portfolio_test.go - UpdatePortfolio error") } value = portfolio.UpdatePortfolio( []string{"LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL", "LVa8wZ983PvWtdwXZ8viK6SocMENLCXkEy"}, currency.LTC, ) if !value { - t.Error("Test Failed - portfolio_test.go - UpdatePortfolio error") + t.Error("portfolio_test.go - UpdatePortfolio error") } value = portfolio.UpdatePortfolio( []string{"LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL", "Testy"}, currency.LTC, ) if value { - t.Error("Test Failed - portfolio_test.go - UpdatePortfolio error") + t.Error("portfolio_test.go - UpdatePortfolio error") } time.Sleep(time.Second * 5) @@ -250,20 +286,20 @@ func TestUpdatePortfolio(t *testing.T) { "0xe853c56864a2ebe4576a807d26fdc4a0ada51919"}, currency.ETH, ) if !value { - t.Error("Test Failed - portfolio_test.go - UpdatePortfolio error") + t.Error("portfolio_test.go - UpdatePortfolio error") } value = portfolio.UpdatePortfolio( []string{"0xb794f5ea0ba39494ce839613fffba74279579268", "TESTY"}, currency.ETH, ) if value { - t.Error("Test Failed - portfolio_test.go - UpdatePortfolio error") + t.Error("portfolio_test.go - UpdatePortfolio error") } value = portfolio.UpdatePortfolio( []string{PortfolioAddressExchange, PortfolioAddressPersonal}, currency.LTC) if !value { - t.Error("Test Failed - portfolio_test.go - UpdatePortfolio error") + t.Error("portfolio_test.go - UpdatePortfolio error") } } @@ -273,25 +309,25 @@ func TestGetPortfolioByExchange(t *testing.T) { newbase.AddExchangeAddress("Bitfinex", currency.LTC, 0.05) newbase.AddAddress("someaddress", "LTC", currency.NewCode(PortfolioAddressPersonal), 0.03) portfolio := GetPortfolio() - portfolio.SeedPortfolio(newbase) + portfolio.Seed(newbase) value := portfolio.GetPortfolioByExchange("ANX") result, ok := value[currency.LTC] if !ok { - t.Error("Test Failed - portfolio_test.go - GetPortfolioByExchange error") + t.Error("portfolio_test.go - GetPortfolioByExchange error") } if result != 0.07 { - t.Error("Test Failed - portfolio_test.go - GetPortfolioByExchange result != 0.10") + t.Error("portfolio_test.go - GetPortfolioByExchange result != 0.10") } value = portfolio.GetPortfolioByExchange("Bitfinex") result, ok = value[currency.LTC] if !ok { - t.Error("Test Failed - portfolio_test.go - GetPortfolioByExchange error") + t.Error("portfolio_test.go - GetPortfolioByExchange error") } if result != 0.05 { - t.Error("Test Failed - portfolio_test.go - GetPortfolioByExchange result != 0.05") + t.Error("portfolio_test.go - GetPortfolioByExchange result != 0.05") } } @@ -301,16 +337,16 @@ func TestGetExchangePortfolio(t *testing.T) { newbase.AddAddress("Bitfinex", PortfolioAddressExchange, currency.LTC, 0.05) newbase.AddAddress("someaddress", PortfolioAddressPersonal, currency.LTC, 0.03) portfolio := GetPortfolio() - portfolio.SeedPortfolio(newbase) + portfolio.Seed(newbase) value := portfolio.GetExchangePortfolio() result, ok := value[currency.LTC] if !ok { - t.Error("Test Failed - portfolio_test.go - GetExchangePortfolio error") + t.Error("portfolio_test.go - GetExchangePortfolio error") } if result != 0.08 { - t.Error("Test Failed - portfolio_test.go - GetExchangePortfolio result != 0.08") + t.Error("portfolio_test.go - GetExchangePortfolio result != 0.08") } } @@ -320,15 +356,15 @@ func TestGetPersonalPortfolio(t *testing.T) { newbase.AddAddress("anotheraddress", PortfolioAddressPersonal, currency.N2O, 0.03) newbase.AddAddress("Exchange", PortfolioAddressExchange, currency.N2O, 0.01) portfolio := GetPortfolio() - portfolio.SeedPortfolio(newbase) + portfolio.Seed(newbase) value := portfolio.GetPersonalPortfolio() result, ok := value[currency.N2O] if !ok { - t.Error("Test Failed - portfolio_test.go - GetPersonalPortfolio error") + t.Error("portfolio_test.go - GetPersonalPortfolio error") } if result != 0.05 { - t.Error("Test Failed - portfolio_test.go - GetPersonalPortfolio result != 0.05") + t.Error("portfolio_test.go - GetPersonalPortfolio result != 0.05") } } @@ -349,7 +385,7 @@ func TestGetPortfolioSummary(t *testing.T) { newbase.AddExchangeAddress("ANX", currency.ETH, 42) portfolio := GetPortfolio() - portfolio.SeedPortfolio(newbase) + portfolio.Seed(newbase) value := portfolio.GetPortfolioSummary() getTotalsVal := func(c currency.Code) Coin { @@ -362,19 +398,19 @@ func TestGetPortfolioSummary(t *testing.T) { } if getTotalsVal(currency.LTC).Coin != currency.LTC { - t.Error("Test Failed - portfolio_test.go - TestGetPortfolioSummary error") + t.Error("portfolio_test.go - TestGetPortfolioSummary error") } if getTotalsVal(currency.ETH).Coin == currency.LTC { - t.Error("Test Failed - portfolio_test.go - TestGetPortfolioSummary error") + t.Error("portfolio_test.go - TestGetPortfolioSummary error") } if getTotalsVal(currency.LTC).Balance != 23 { - t.Error("Test Failed - portfolio_test.go - TestGetPortfolioSummary error") + t.Error("portfolio_test.go - TestGetPortfolioSummary error") } if getTotalsVal(currency.BTC).Balance != 200 { - t.Error("Test Failed - portfolio_test.go - TestGetPortfolioSummary error") + t.Error("portfolio_test.go - TestGetPortfolioSummary error") } } @@ -383,21 +419,21 @@ func TestGetPortfolioGroupedCoin(t *testing.T) { newbase.AddAddress("someaddress", currency.LTC.String(), currency.LTC, 0.02) newbase.AddAddress("Exchange", PortfolioAddressExchange, currency.LTC, 0.05) portfolio := GetPortfolio() - portfolio.SeedPortfolio(newbase) + portfolio.Seed(newbase) value := portfolio.GetPortfolioGroupedCoin() if value[currency.LTC][0] != "someaddress" && len(value[currency.LTC][0]) != 1 { - t.Error("Test Failed - portfolio_test.go - GetPortfolioGroupedCoin error") + t.Error("portfolio_test.go - GetPortfolioGroupedCoin error") } } -func TestSeedPortfolio(t *testing.T) { +func TestSeed(t *testing.T) { newbase := Base{} newbase.AddAddress("someaddress", currency.LTC.String(), currency.LTC, 0.02) portfolio := GetPortfolio() - portfolio.SeedPortfolio(newbase) + portfolio.Seed(newbase) if !portfolio.AddressExists("someaddress") { - t.Error("Test Failed - portfolio_test.go - SeedPortfolio error") + t.Error("portfolio_test.go - Seed error") } } @@ -414,10 +450,10 @@ func TestStartPortfolioWatcher(t *testing.T) { 0.02) portfolio := GetPortfolio() - portfolio.SeedPortfolio(newBase) + portfolio.Seed(newBase) if !portfolio.AddressExists("LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1") { - t.Error("Test Failed - portfolio_test.go - TestStartPortfolioWatcher") + t.Error("portfolio_test.go - TestStartPortfolioWatcher") } go StartPortfolioWatcher() @@ -426,6 +462,6 @@ func TestStartPortfolioWatcher(t *testing.T) { func TestGetPortfolio(t *testing.T) { ptrBASE := GetPortfolio() if reflect.TypeOf(ptrBASE).String() != "*portfolio.Base" { - t.Error("Test Failed - portfolio_test.go - GetoPortfolio error") + t.Error("portfolio_test.go - GetoPortfolio error") } } diff --git a/portfolio/portfolio_types.go b/portfolio/portfolio_types.go index 7ddf02b2..eae8b29e 100644 --- a/portfolio/portfolio_types.go +++ b/portfolio/portfolio_types.go @@ -4,7 +4,7 @@ import "github.com/thrasher-corp/gocryptotrader/currency" // Base holds the portfolio base addresses type Base struct { - Addresses []Address + Addresses []Address `json:"addresses"` } // Address sub type holding address information for portfolio diff --git a/restful_router.go b/restful_router.go deleted file mode 100644 index 08ee7f65..00000000 --- a/restful_router.go +++ /dev/null @@ -1,141 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "strconv" - "time" - - "github.com/gorilla/mux" - "github.com/thrasher-corp/gocryptotrader/common" - log "github.com/thrasher-corp/gocryptotrader/logger" - - _ "net/http/pprof" // nolint: gosec -) - -// RESTLogger logs the requests internally -func RESTLogger(inner http.Handler, name string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - inner.ServeHTTP(w, r) - - log.Debugf( - "%s\t%s\t%s\t%s", - r.Method, - r.RequestURI, - name, - time.Since(start), - ) - }) -} - -// Route is a sub type that holds the request routes -type Route struct { - Name string - Method string - Pattern string - HandlerFunc http.HandlerFunc -} - -// Routes is an array of all the registered routes -type Routes []Route - -var routes = Routes{} - -// NewRouter takes in the exchange interfaces and returns a new multiplexor -// router -func NewRouter() *mux.Router { - router := mux.NewRouter().StrictSlash(true) - var listenAddr string - - if common.ExtractPort(bot.config.Webserver.ListenAddress) == 80 { - listenAddr = common.ExtractHost(bot.config.Webserver.ListenAddress) - } else { - listenAddr = common.JoinStrings([]string{common.ExtractHost(bot.config.Webserver.ListenAddress), - strconv.Itoa(common.ExtractPort(bot.config.Webserver.ListenAddress))}, ":") - } - - routes = Routes{ - Route{ - "", - http.MethodGet, - "/", - getIndex, - }, - Route{ - "GetAllSettings", - http.MethodGet, - "/config/all", - RESTGetAllSettings, - }, - Route{ - "SaveAllSettings", - http.MethodPost, - "/config/all/save", - RESTSaveAllSettings, - }, - Route{ - "AllEnabledAccountInfo", - http.MethodGet, - "/exchanges/enabled/accounts/all", - RESTGetAllEnabledAccountInfo, - }, - Route{ - "AllActiveExchangesAndCurrencies", - http.MethodGet, - "/exchanges/enabled/latest/all", - RESTGetAllActiveTickers, - }, - Route{ - "IndividualExchangeAndCurrency", - http.MethodGet, - "/exchanges/{exchangeName}/latest/{currency}", - RESTGetTicker, - }, - Route{ - "GetPortfolio", - http.MethodGet, - "/portfolio/all", - RESTGetPortfolio, - }, - Route{ - "AllActiveExchangesAndOrderbooks", - http.MethodGet, - "/exchanges/orderbook/latest/all", - RESTGetAllActiveOrderbooks, - }, - Route{ - "IndividualExchangeOrderbook", - http.MethodGet, - "/exchanges/{exchangeName}/orderbook/latest/{currency}", - RESTGetOrderbook, - }, - Route{ - "ws", - http.MethodGet, - "/ws", - WebsocketClientHandler, - }, - } - - for _, route := range routes { - router. - Methods(route.Method). - Path(route.Pattern). - Name(route.Name). - Handler(RESTLogger(route.HandlerFunc, route.Name)). - Host(listenAddr) - } - - if bot.config.Profiler.Enabled { - log.Debugln("Profiler enabled") - router.PathPrefix("/debug").Handler(http.DefaultServeMux) - } - - return router -} - -func getIndex(w http.ResponseWriter, _ *http.Request) { - fmt.Fprint(w, "GoCryptoTrader RESTful interface. For the web GUI, please visit the web GUI readme.") -} diff --git a/restful_server.go b/restful_server.go deleted file mode 100644 index 3b7d3466..00000000 --- a/restful_server.go +++ /dev/null @@ -1,298 +0,0 @@ -package main - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - "github.com/thrasher-corp/gocryptotrader/config" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - log "github.com/thrasher-corp/gocryptotrader/logger" -) - -// AllEnabledExchangeOrderbooks holds the enabled exchange orderbooks -type AllEnabledExchangeOrderbooks struct { - Data []EnabledExchangeOrderbooks `json:"data"` -} - -// EnabledExchangeOrderbooks is a sub type for singular exchanges and respective -// orderbooks -type EnabledExchangeOrderbooks struct { - ExchangeName string `json:"exchangeName"` - ExchangeValues []orderbook.Base `json:"exchangeValues"` -} - -// AllEnabledExchangeCurrencies holds the enabled exchange currencies -type AllEnabledExchangeCurrencies struct { - Data []EnabledExchangeCurrencies `json:"data"` -} - -// EnabledExchangeCurrencies is a sub type for singular exchanges and respective -// currencies -type EnabledExchangeCurrencies struct { - ExchangeName string `json:"exchangeName"` - ExchangeValues []ticker.Price `json:"exchangeValues"` -} - -// AllEnabledExchangeAccounts holds all enabled accounts info -type AllEnabledExchangeAccounts struct { - Data []exchange.AccountInfo `json:"data"` -} - -// RESTfulJSONResponse outputs a JSON response of the response interface -func RESTfulJSONResponse(w http.ResponseWriter, response interface{}) error { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - return json.NewEncoder(w).Encode(response) -} - -// RESTfulError prints the REST method and error -func RESTfulError(method string, err error) { - log.Errorf("RESTful %s: server failed to send JSON response. Error %s", - method, err) -} - -// RESTGetAllSettings replies to a request with an encoded JSON response about the -// trading bots configuration. -func RESTGetAllSettings(w http.ResponseWriter, r *http.Request) { - err := RESTfulJSONResponse(w, bot.config) - if err != nil { - RESTfulError(r.Method, err) - } -} - -// RESTSaveAllSettings saves all current settings from request body as a JSON -// document then reloads state and returns the settings -func RESTSaveAllSettings(w http.ResponseWriter, r *http.Request) { - // Get the data from the request - decoder := json.NewDecoder(r.Body) - var responseData config.Post - err := decoder.Decode(&responseData) - if err != nil { - RESTfulError(r.Method, err) - } - - // Save change the settings - err = bot.config.UpdateConfig(bot.configFile, &responseData.Data) - if err != nil { - RESTfulError(r.Method, err) - } - - err = RESTfulJSONResponse(w, bot.config) - if err != nil { - RESTfulError(r.Method, err) - } - - SetupExchanges() -} - -// RESTGetOrderbook returns orderbook info for a given currency, exchange and -// asset type -func RESTGetOrderbook(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - currency := vars["currency"] - exchangeName := vars["exchangeName"] - assetType := vars["assetType"] - - if assetType == "" { - assetType = orderbook.Spot - } - - response, err := GetSpecificOrderbook(currency, exchangeName, assetType) - if err != nil { - log.Errorf("Failed to fetch orderbook for %s currency: %s\n", exchangeName, - currency) - return - } - - err = RESTfulJSONResponse(w, response) - if err != nil { - RESTfulError(r.Method, err) - } -} - -// GetAllActiveOrderbooks returns all enabled exchanges orderbooks -func GetAllActiveOrderbooks() []EnabledExchangeOrderbooks { - var orderbookData []EnabledExchangeOrderbooks - - for _, individualBot := range bot.exchanges { - if individualBot == nil || !individualBot.IsEnabled() { - continue - } - - var individualExchange EnabledExchangeOrderbooks - exchangeName := individualBot.GetName() - individualExchange.ExchangeName = exchangeName - currencies := individualBot.GetEnabledCurrencies() - assetTypes, err := exchange.GetExchangeAssetTypes(exchangeName) - if err != nil { - log.Errorf("failed to get %s exchange asset types. Error: %s", - exchangeName, err) - continue - } - for _, x := range currencies { - pair := x - - var ob orderbook.Base - if len(assetTypes) > 1 { - for y := range assetTypes { - ob, err = individualBot.GetOrderbookEx(pair, - assetTypes[y]) - } - } else { - ob, err = individualBot.GetOrderbookEx(pair, - assetTypes[0]) - } - - if err != nil { - log.Errorf("failed to get %s %s orderbook. Error: %s", - pair, - exchangeName, - err) - continue - } - - individualExchange.ExchangeValues = append( - individualExchange.ExchangeValues, ob, - ) - } - orderbookData = append(orderbookData, individualExchange) - } - return orderbookData -} - -// RESTGetAllActiveOrderbooks returns all enabled exchange orderbooks -func RESTGetAllActiveOrderbooks(w http.ResponseWriter, r *http.Request) { - var response AllEnabledExchangeOrderbooks - response.Data = GetAllActiveOrderbooks() - - err := RESTfulJSONResponse(w, response) - if err != nil { - RESTfulError(r.Method, err) - } -} - -// RESTGetPortfolio returns the bot portfolio -func RESTGetPortfolio(w http.ResponseWriter, r *http.Request) { - result := bot.portfolio.GetPortfolioSummary() - err := RESTfulJSONResponse(w, result) - if err != nil { - RESTfulError(r.Method, err) - } -} - -// RESTGetTicker returns ticker info for a given currency, exchange and -// asset type -func RESTGetTicker(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - currency := vars["currency"] - exchangeName := vars["exchangeName"] - assetType := vars["assetType"] - - if assetType == "" { - assetType = ticker.Spot - } - response, err := GetSpecificTicker(currency, exchangeName, assetType) - if err != nil { - log.Errorf("Failed to fetch ticker for %s currency: %s\n", exchangeName, - currency) - return - } - err = RESTfulJSONResponse(w, response) - if err != nil { - RESTfulError(r.Method, err) - } -} - -// GetAllActiveTickers returns all enabled exchange tickers -func GetAllActiveTickers() []EnabledExchangeCurrencies { - var tickerData []EnabledExchangeCurrencies - - for _, individualBot := range bot.exchanges { - if individualBot == nil || !individualBot.IsEnabled() { - continue - } - - var individualExchange EnabledExchangeCurrencies - exchangeName := individualBot.GetName() - individualExchange.ExchangeName = exchangeName - currencies := individualBot.GetEnabledCurrencies() - for _, x := range currencies { - pair := x - assetTypes, err := exchange.GetExchangeAssetTypes(exchangeName) - if err != nil { - log.Errorf("failed to get %s exchange asset types. Error: %s", - exchangeName, err) - continue - } - var tickerPrice ticker.Price - if len(assetTypes) > 1 { - for y := range assetTypes { - tickerPrice, err = individualBot.GetTickerPrice(pair, - assetTypes[y]) - } - } else { - tickerPrice, err = individualBot.GetTickerPrice(pair, - assetTypes[0]) - } - - if err != nil { - log.Errorf("failed to get %s %s ticker. Error: %s", - pair, - exchangeName, - err) - continue - } - - individualExchange.ExchangeValues = append( - individualExchange.ExchangeValues, tickerPrice, - ) - } - tickerData = append(tickerData, individualExchange) - } - return tickerData -} - -// RESTGetAllActiveTickers returns all active tickers -func RESTGetAllActiveTickers(w http.ResponseWriter, r *http.Request) { - var response AllEnabledExchangeCurrencies - response.Data = GetAllActiveTickers() - - err := RESTfulJSONResponse(w, response) - if err != nil { - RESTfulError(r.Method, err) - } -} - -// GetAllEnabledExchangeAccountInfo returns all the current enabled exchanges -func GetAllEnabledExchangeAccountInfo() AllEnabledExchangeAccounts { - var response AllEnabledExchangeAccounts - for _, individualBot := range bot.exchanges { - if individualBot != nil && individualBot.IsEnabled() { - if !individualBot.GetAuthenticatedAPISupport(exchange.RestAuthentication) { - log.Warnf("GetAllEnabledExchangeAccountInfo: Skippping %s due to disabled authenticated API support.", individualBot.GetName()) - continue - } - individualExchange, err := individualBot.GetAccountInfo() - if err != nil { - log.Errorf("Error encountered retrieving exchange account info for %s. Error %s", - individualBot.GetName(), err) - continue - } - response.Data = append(response.Data, individualExchange) - } - } - return response -} - -// RESTGetAllEnabledAccountInfo via get request returns JSON response of account -// info -func RESTGetAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) { - response := GetAllEnabledExchangeAccountInfo() - err := RESTfulJSONResponse(w, response) - if err != nil { - RESTfulError(r.Method, err) - } -} diff --git a/routines.go b/routines.go deleted file mode 100644 index 8577f0c5..00000000 --- a/routines.go +++ /dev/null @@ -1,443 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "sync" - "time" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/stats" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" -) - -func printCurrencyFormat(price float64) string { - displaySymbol, err := currency.GetSymbolByCurrencyName(bot.config.Currency.FiatDisplayCurrency) - if err != nil { - log.Errorf("Failed to get display symbol: %s", err) - } - - return fmt.Sprintf("%s%.8f", displaySymbol, price) -} - -func printConvertCurrencyFormat(origCurrency currency.Code, origPrice float64) string { - displayCurrency := bot.config.Currency.FiatDisplayCurrency - conv, err := currency.ConvertCurrency(origPrice, - origCurrency, - displayCurrency) - if err != nil { - log.Errorf("Failed to convert currency: %s", err) - } - - displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency) - if err != nil { - log.Errorf("Failed to get display symbol: %s", err) - } - - origSymbol, err := currency.GetSymbolByCurrencyName(origCurrency) - if err != nil { - log.Errorf("Failed to get original currency symbol for %s: %s", - origCurrency, - err) - } - - return fmt.Sprintf("%s%.2f %s (%s%.2f %s)", - displaySymbol, - conv, - displayCurrency, - origSymbol, - origPrice, - origCurrency, - ) -} - -func printTickerSummary(result *ticker.Price, p currency.Pair, assetType, exchangeName string, err error) { - if err != nil { - log.Errorf("Failed to get %s %s ticker. Error: %s", - p.String(), - exchangeName, - err) - return - } - - stats.Add(exchangeName, p, assetType, result.Last, result.Volume) - if p.Quote.IsFiatCurrency() && - p.Quote != bot.config.Currency.FiatDisplayCurrency { - origCurrency := p.Quote.Upper() - log.Infof("%s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f", - exchangeName, - exchange.FormatCurrency(p).String(), - assetType, - printConvertCurrencyFormat(origCurrency, result.Last), - printConvertCurrencyFormat(origCurrency, result.Ask), - printConvertCurrencyFormat(origCurrency, result.Bid), - printConvertCurrencyFormat(origCurrency, result.High), - printConvertCurrencyFormat(origCurrency, result.Low), - result.Volume) - } else { - if p.Quote.IsFiatCurrency() && - p.Quote == bot.config.Currency.FiatDisplayCurrency { - log.Infof("%s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f", - exchangeName, - exchange.FormatCurrency(p).String(), - assetType, - printCurrencyFormat(result.Last), - printCurrencyFormat(result.Ask), - printCurrencyFormat(result.Bid), - printCurrencyFormat(result.High), - printCurrencyFormat(result.Low), - result.Volume) - } else { - log.Infof("%s %s %s: TICKER: Last %.8f Ask %.8f Bid %.8f High %.8f Low %.8f Volume %.8f", - exchangeName, - exchange.FormatCurrency(p).String(), - assetType, - result.Last, - result.Ask, - result.Bid, - result.High, - result.Low, - result.Volume) - } - } -} - -func printOrderbookSummary(result *orderbook.Base, p currency.Pair, assetType, exchangeName string, err error) { - if err != nil { - log.Errorf("Failed to get %s %s orderbook of type %s. Error: %s", - p, - exchangeName, - assetType, - err) - return - } - - bidsAmount, bidsValue := result.TotalBidsAmount() - asksAmount, asksValue := result.TotalAsksAmount() - - if p.Quote.IsFiatCurrency() && - p.Quote != bot.config.Currency.FiatDisplayCurrency { - origCurrency := p.Quote.Upper() - log.Infof("%s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s", - exchangeName, - exchange.FormatCurrency(p).String(), - assetType, - len(result.Bids), - bidsAmount, - p.Base.String(), - printConvertCurrencyFormat(origCurrency, bidsValue), - len(result.Asks), - asksAmount, - p.Base.String(), - printConvertCurrencyFormat(origCurrency, asksValue), - ) - } else { - if p.Quote.IsFiatCurrency() && - p.Quote == bot.config.Currency.FiatDisplayCurrency { - log.Infof("%s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s", - exchangeName, - exchange.FormatCurrency(p).String(), - assetType, - len(result.Bids), - bidsAmount, - p.Base.String(), - printCurrencyFormat(bidsValue), - len(result.Asks), - asksAmount, - p.Base.String(), - printCurrencyFormat(asksValue), - ) - } else { - log.Infof("%s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %f Asks len: %d Amount: %f %s. Total value: %f", - exchangeName, - exchange.FormatCurrency(p).String(), - assetType, - len(result.Bids), - bidsAmount, - p.Base.String(), - bidsValue, - len(result.Asks), - asksAmount, - p.Base.String(), - asksValue, - ) - } - } -} - -func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string) { - evt := WebsocketEvent{ - Data: result, - Event: event, - AssetType: assetType, - Exchange: exchangeName, - } - err := BroadcastWebsocketMessage(evt) - if err != nil { - log.Errorf("Failed to broadcast websocket event %v. Error: %s", - event, err) - } -} - -// TickerUpdaterRoutine fetches and updates the ticker for all enabled -// currency pairs and exchanges -func TickerUpdaterRoutine() { - log.Debugf("Starting ticker updater routine.") - var wg sync.WaitGroup - for { - wg.Add(len(bot.exchanges)) - for x := range bot.exchanges { - go func(x int, wg *sync.WaitGroup) { - defer wg.Done() - if bot.exchanges[x] == nil { - return - } - exchangeName := bot.exchanges[x].GetName() - enabledCurrencies := bot.exchanges[x].GetEnabledCurrencies() - supportsBatching := bot.exchanges[x].SupportsRESTTickerBatchUpdates() - assetTypes, err := exchange.GetExchangeAssetTypes(exchangeName) - if err != nil { - log.Debugf("failed to get %s exchange asset types. Error: %s", - exchangeName, err) - return - } - - processTicker := func(exch exchange.IBotExchange, update bool, c currency.Pair, assetType string) { - var result ticker.Price - var err error - if update { - result, err = exch.UpdateTicker(c, assetType) - } else { - result, err = exch.GetTickerPrice(c, assetType) - } - printTickerSummary(&result, c, assetType, exchangeName, err) - if err == nil { - bot.comms.StageTickerData(exchangeName, assetType, &result) - if bot.config.Webserver.Enabled { - relayWebsocketEvent(result, "ticker_update", assetType, exchangeName) - } - } - } - - for y := range assetTypes { - for z := range enabledCurrencies { - if supportsBatching && z > 0 { - processTicker(bot.exchanges[x], false, enabledCurrencies[z], assetTypes[y]) - continue - } - processTicker(bot.exchanges[x], true, enabledCurrencies[z], assetTypes[y]) - } - } - }(x, &wg) - } - wg.Wait() - log.Debugln("All enabled currency tickers fetched.") - time.Sleep(time.Second * 10) - } -} - -// OrderbookUpdaterRoutine fetches and updates the orderbooks for all enabled -// currency pairs and exchanges -func OrderbookUpdaterRoutine() { - log.Debugln("Starting orderbook updater routine.") - var wg sync.WaitGroup - for { - wg.Add(len(bot.exchanges)) - for x := range bot.exchanges { - go func(x int, wg *sync.WaitGroup) { - defer wg.Done() - - if bot.exchanges[x] == nil { - return - } - exchangeName := bot.exchanges[x].GetName() - enabledCurrencies := bot.exchanges[x].GetEnabledCurrencies() - assetTypes, err := exchange.GetExchangeAssetTypes(exchangeName) - if err != nil { - log.Errorf("failed to get %s exchange asset types. Error: %s", - exchangeName, err) - return - } - - processOrderbook := func(exch exchange.IBotExchange, c currency.Pair, assetType string) { - result, err := exch.UpdateOrderbook(c, assetType) - printOrderbookSummary(&result, c, assetType, exchangeName, err) - if err == nil { - bot.comms.StageOrderbookData(exchangeName, assetType, &result) - if bot.config.Webserver.Enabled { - relayWebsocketEvent(result, "orderbook_update", assetType, exchangeName) - } - } - } - - for y := range assetTypes { - for z := range enabledCurrencies { - processOrderbook(bot.exchanges[x], enabledCurrencies[z], assetTypes[y]) - } - } - }(x, &wg) - } - wg.Wait() - log.Debugln("All enabled currency orderbooks fetched.") - time.Sleep(time.Second * 10) - } -} - -// WebsocketRoutine Initial routine management system for websocket -func WebsocketRoutine(verbose bool) { - log.Debugln("Connecting exchange websocket services...") - - for i := range bot.exchanges { - go func(i int) { - if verbose { - log.Debugf("Establishing websocket connection for %s", - bot.exchanges[i].GetName()) - } - - ws, err := bot.exchanges[i].GetWebsocket() - if err != nil { - log.Debugf("Websocket not enabled for %s", - bot.exchanges[i].GetName()) - return - } - - // Data handler routine - go WebsocketDataHandler(ws, verbose) - - err = ws.Connect() - if err != nil { - switch err.Error() { - case wshandler.WebsocketNotEnabled: - log.Warnf("%s - websocket disabled", bot.exchanges[i].GetName()) - default: - log.Error(err) - } - } - }(i) - } -} - -var shutdowner = make(chan struct{}, 1) -var wg sync.WaitGroup - -// Websocketshutdown shuts down the exchange routines and then shuts down -// governing routines -func Websocketshutdown(ws *wshandler.Websocket) error { - err := ws.Shutdown() // shutdown routines on the exchange - if err != nil { - log.Errorf("routines.go error - failed to shutodwn %s", err) - } - - timer := time.NewTimer(5 * time.Second) - c := make(chan struct{}, 1) - - go func(c chan struct{}) { - close(shutdowner) - wg.Wait() - c <- struct{}{} - }(c) - - select { - case <-timer.C: - return errors.New("routines.go error - failed to shutdown routines") - - case <-c: - return nil - } -} - -// streamDiversion is a diversion switch from websocket to REST or other -// alternative feed -func streamDiversion(ws *wshandler.Websocket, verbose bool) { - wg.Add(1) - defer wg.Done() - - for { - select { - case <-shutdowner: - return - - case <-ws.Connected: - if verbose { - log.Debugf("exchange %s websocket feed connected", ws.GetName()) - } - - case <-ws.Disconnected: - if verbose { - log.Debugf("exchange %s websocket feed disconnected, switching to REST functionality", - ws.GetName()) - } - } - } -} - -// WebsocketDataHandler handles websocket data coming from a websocket feed -// associated with an exchange -func WebsocketDataHandler(ws *wshandler.Websocket, verbose bool) { - wg.Add(1) - defer wg.Done() - - go streamDiversion(ws, verbose) - - for { - select { - case <-shutdowner: - return - - case data := <-ws.DataHandler: - switch d := data.(type) { - case string: - switch d { - case wshandler.WebsocketNotEnabled: - if verbose { - log.Warnf("routines.go warning - exchange %s weboscket not enabled", - ws.GetName()) - } - - default: - log.Infof(d) - } - - case error: - switch { - case common.StringContains(d.Error(), "close 1006"): - go ws.WebsocketReset() - continue - default: - log.Errorf("routines.go exchange %s websocket error - %s", ws.GetName(), data) - } - - case wshandler.TradeData: - // Trade Data - if verbose { - log.Infoln("Websocket trades Updated: ", d) - } - - case wshandler.TickerData: - // Ticker data - if verbose { - log.Infoln("Websocket Ticker Updated: ", d) - } - case wshandler.KlineData: - // Kline data - if verbose { - log.Infoln("Websocket Kline Updated: ", d) - } - case wshandler.WebsocketOrderbookUpdate: - // Orderbook data - if verbose { - log.Infoln("Websocket Orderbook Updated:", d) - } - default: - if verbose { - log.Warnf("Websocket Unknown type: %s", d) - } - } - } - } -} diff --git a/signaler/signaler.go b/signaler/signaler.go new file mode 100644 index 00000000..036efde8 --- /dev/null +++ b/signaler/signaler.go @@ -0,0 +1,27 @@ +package signaler + +import ( + "os" + "os/signal" + "syscall" +) + +var ( + s = make(chan os.Signal, 1) +) + +func init() { + sigs := []os.Signal{ + os.Interrupt, + os.Kill, + syscall.SIGTERM, + syscall.SIGABRT, + } + signal.Notify(s, sigs...) +} + +// WaitForInterrupt waits until a os.Signal is +// received and returns the result +func WaitForInterrupt() os.Signal { + return <-s +} diff --git a/sqlboiler_example.json b/sqlboiler_example.json new file mode 100644 index 00000000..e6415e62 --- /dev/null +++ b/sqlboiler_example.json @@ -0,0 +1,34 @@ +{ + "psql": { + "dbname": "", + "host": "", + "port": 5432, + "user": "", + "pass": "", + "schema": "public", + "sslmode": "disable", + "blacklist": [ + "goose_db_version" + ] + }, + "mysql": { + "dbname": "", + "host": "", + "port": 3306, + "user": "", + "pass": "", + "sslmode": "false" + }, + "mssql": { + "dbname": "", + "host": "", + "port": 1433, + "user": "", + "pass": "", + "sslmode": "disable", + "schema": "" + }, + "sqlite": { + "dbname": "/.gocryptotrader/database/gocryptotrader.db" + } +} \ No newline at end of file diff --git a/testdata/README.md b/testdata/README.md index 2f0aea4b..f3b7a360 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -41,5 +41,4 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: -***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - +***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** \ No newline at end of file diff --git a/testdata/configtest.json b/testdata/configtest.json index 6e33885b..b831e9b8 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -2,12 +2,51 @@ "name": "Skynet", "encryptConfig": -1, "globalHTTPTimeout": 15000000000, + "database": { + "enabled": false, + "verbose": false, + "driver": "sqlite3", + "connectionDetails": { + "host": "", + "port": 0, + "username": "", + "password": "", + "database": "gocryptotrader.db", + "sslmode": "" + } + }, "logging": { "enabled": true, - "file": "debug.txt", - "colour": false, - "level": "DEBUG|WARN|INFO|ERROR|FATAL", - "rotate": true + "level": "INFO|DEBUG|WARN|ERROR", + "output": "console", + "fileSettings": { + "filename": "log.txt", + "rotate": false + }, + "advancedSettings": { + "spacer": " | ", + "timeStampFormat": " 02/01/2006 15:04:05 ", + "headers": { + "info": "[INFO]", + "warn": "[WARN]", + "debug": "[DEBUG]", + "error": "[ERROR]" + } + } + }, + "connectionMonitor": { + "preferredDNSList": [ + "8.8.8.8", + "8.8.4.4", + "1.1.1.1", + "1.0.0.1" + ], + "preferredDomainList": [ + "www.google.com", + "www.cloudflare.com", + "www.facebook.com" + ], + "checkInterval": 1000000000 }, "profiler": { "enabled": false @@ -95,6 +134,7 @@ }, "smsGlobal": { "name": "SMSGlobal", + "from": "Skynet", "enabled": true, "verbose": false, "username": "1234", @@ -115,6 +155,7 @@ "port": "537", "accountName": "some", "accountPassword": "password", + "from": "", "recipientList": "lol123@gmail.com" }, "telegram": { @@ -124,8 +165,29 @@ "verificationToken": "testest" } }, + "remoteControl": { + "username": "admin", + "password": "Password", + "gRPC": { + "enabled": true, + "listenAddress": "localhost:9052", + "grpcProxyEnabled": true, + "grpcProxyListenAddress": "localhost:9053" + }, + "deprecatedRPC": { + "enabled": true, + "listenAddress": "localhost:9050" + }, + "websocketRPC": { + "enabled": true, + "listenAddress": "localhost:9051", + "connectionLimit": 1, + "maxAuthFailures": 3, + "allowInsecureOrigin": true + } + }, "portfolioAddresses": { - "Addresses": [ + "addresses": [ { "Address": "1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy", "CoinType": "BTC", @@ -152,53 +214,76 @@ } ] }, - "webserver": { - "enabled": false, - "adminUsername": "admin", - "adminPassword": "Password", - "listenAddress": ":9050", - "websocketConnectionLimit": 1, - "websocketMaxAuthFailures": 3, - "websocketAllowInsecureOrigin": false - }, "exchanges": [ { "name": "ANX", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "ETH_HKD,START_GBP,BTC_CAD,OAX_ETH,START_SGD,LTC_BTC,STR_BTC,ATENC_NZD,BTC_AUD,BTC_SGD,ETH_BTC,XRP_BTC,START_JPY,ATENC_CAD,BTC_GBP,ETH_USD,GNT_ETH,START_AUD,START_HKD,ATENC_GBP,BTC_USD,START_BTC,START_CAD,START_EUR,BTC_JPY,BTC_NZD,DOGE_BTC,ATENC_EUR,ATENC_JPY,ATENC_USD,BTC_EUR,BTC_HKD,START_NZD,START_USD,ATENC_AUD,ATENC_HKD,ATENC_SGD", - "enabledPairs": "BTC_USD,BTC_HKD,BTC_EUR,BTC_CAD,BTC_AUD,BTC_SGD,BTC_JPY,BTC_GBP,BTC_NZD,LTC_BTC,STR_BTC,XRP_BTC", "baseCurrencies": "USD,HKD,EUR,CAD,AUD,SGD,JPY,GBP,NZD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_USD,BTC_HKD,BTC_EUR,BTC_CAD,BTC_AUD,BTC_SGD,BTC_JPY,BTC_GBP,BTC_NZD,LTC_BTC,STR_BTC,XRP_BTC", + "available": "ETH_HKD,START_GBP,BTC_CAD,OAX_ETH,START_SGD,LTC_BTC,STR_BTC,ATENC_NZD,BTC_AUD,BTC_SGD,ETH_BTC,XRP_BTC,START_JPY,ATENC_CAD,BTC_GBP,ETH_USD,GNT_ETH,START_AUD,START_HKD,ATENC_GBP,BTC_USD,START_BTC,START_CAD,START_EUR,BTC_JPY,BTC_NZD,DOGE_BTC,ATENC_EUR,ATENC_JPY,ATENC_USD,BTC_EUR,BTC_HKD,START_NZD,START_USD,ATENC_AUD,ATENC_HKD,ATENC_SGD" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresBase64DecodeSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -211,39 +296,71 @@ "name": "Binance", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "ETH-BTC,LTC-BTC,BNB-BTC,NEO-BTC,QTUM-ETH,EOS-ETH,SNT-ETH,BNT-ETH,GAS-BTC,BNB-ETH,BTC-USDT,ETH-USDT,OAX-ETH,DNT-ETH,MCO-ETH,MCO-BTC,WTC-BTC,WTC-ETH,LRC-BTC,LRC-ETH,QTUM-BTC,YOYO-BTC,OMG-BTC,OMG-ETH,ZRX-BTC,ZRX-ETH,STRAT-BTC,STRAT-ETH,SNGLS-BTC,SNGLS-ETH,BQX-BTC,BQX-ETH,KNC-BTC,KNC-ETH,FUN-BTC,FUN-ETH,SNM-BTC,SNM-ETH,NEO-ETH,IOTA-BTC,IOTA-ETH,LINK-BTC,LINK-ETH,XVG-BTC,XVG-ETH,MDA-BTC,MDA-ETH,MTL-BTC,MTL-ETH,EOS-BTC,SNT-BTC,ETC-ETH,ETC-BTC,MTH-BTC,MTH-ETH,ENG-BTC,ENG-ETH,DNT-BTC,ZEC-BTC,ZEC-ETH,BNT-BTC,AST-BTC,AST-ETH,DASH-BTC,DASH-ETH,OAX-BTC,BTG-BTC,BTG-ETH,EVX-BTC,EVX-ETH,REQ-BTC,REQ-ETH,VIB-BTC,VIB-ETH,TRX-BTC,TRX-ETH,POWR-BTC,POWR-ETH,ARK-BTC,ARK-ETH,YOYO-ETH,XRP-BTC,XRP-ETH,ENJ-BTC,ENJ-ETH,STORJ-BTC,STORJ-ETH,BNB-USDT,YOYO-BNB,POWR-BNB,KMD-BTC,KMD-ETH,NULS-BNB,RCN-BTC,RCN-ETH,RCN-BNB,NULS-BTC,NULS-ETH,RDN-BTC,RDN-ETH,RDN-BNB,XMR-BTC,XMR-ETH,DLT-BNB,WTC-BNB,DLT-BTC,DLT-ETH,AMB-BTC,AMB-ETH,AMB-BNB,BAT-BTC,BAT-ETH,BAT-BNB,BCPT-BTC,BCPT-ETH,BCPT-BNB,ARN-BTC,ARN-ETH,GVT-BTC,GVT-ETH,CDT-BTC,CDT-ETH,GXS-BTC,GXS-ETH,NEO-USDT,NEO-BNB,POE-BTC,POE-ETH,QSP-BTC,QSP-ETH,QSP-BNB,BTS-BTC,BTS-ETH,BTS-BNB,XZC-BTC,XZC-ETH,XZC-BNB,LSK-BTC,LSK-ETH,LSK-BNB,TNT-BTC,TNT-ETH,FUEL-BTC,FUEL-ETH,MANA-BTC,MANA-ETH,BCD-BTC,BCD-ETH,DGD-BTC,DGD-ETH,IOTA-BNB,ADX-BTC,ADX-ETH,ADX-BNB,ADA-BTC,ADA-ETH,PPT-BTC,PPT-ETH,CMT-BTC,CMT-ETH,CMT-BNB,XLM-BTC,XLM-ETH,XLM-BNB,CND-BTC,CND-ETH,CND-BNB,LEND-BTC,LEND-ETH,WABI-BTC,WABI-ETH,WABI-BNB,LTC-ETH,LTC-USDT,LTC-BNB,TNB-BTC,TNB-ETH,WAVES-BTC,WAVES-ETH,WAVES-BNB,GTO-BTC,GTO-ETH,GTO-BNB,ICX-BTC,ICX-ETH,ICX-BNB,OST-BTC,OST-ETH,OST-BNB,ELF-BTC,ELF-ETH,AION-BTC,AION-ETH,AION-BNB,NEBL-BTC,NEBL-ETH,NEBL-BNB,BRD-BTC,BRD-ETH,BRD-BNB,MCO-BNB,EDO-BTC,EDO-ETH,NAV-BTC,NAV-ETH,NAV-BNB,LUN-BTC,LUN-ETH,APPC-BTC,APPC-ETH,APPC-BNB,VIBE-BTC,VIBE-ETH,RLC-BTC,RLC-ETH,RLC-BNB,INS-BTC,INS-ETH,PIVX-BTC,PIVX-ETH,PIVX-BNB,IOST-BTC,IOST-ETH,STEEM-BTC,STEEM-ETH,STEEM-BNB,NANO-BTC,NANO-ETH,NANO-BNB,VIA-BTC,VIA-ETH,VIA-BNB,BLZ-BTC,BLZ-ETH,BLZ-BNB,AE-BTC,AE-ETH,AE-BNB,NCASH-BTC,NCASH-ETH,NCASH-BNB,POA-BTC,POA-ETH,POA-BNB,ZIL-BTC,ZIL-ETH,ZIL-BNB,ONT-BTC,ONT-ETH,ONT-BNB,STORM-BTC,STORM-ETH,STORM-BNB,QTUM-BNB,QTUM-USDT,XEM-BTC,XEM-ETH,XEM-BNB,WAN-BTC,WAN-ETH,WAN-BNB,WPR-BTC,WPR-ETH,QLC-BTC,QLC-ETH,SYS-BTC,SYS-ETH,SYS-BNB,QLC-BNB,GRS-BTC,GRS-ETH,ADA-USDT,ADA-BNB,GNT-BTC,GNT-ETH,GNT-BNB,LOOM-BTC,LOOM-ETH,LOOM-BNB,XRP-USDT,REP-BTC,REP-ETH,REP-BNB,BTC-TUSD,ETH-TUSD,ZEN-BTC,ZEN-ETH,ZEN-BNB,SKY-BTC,SKY-ETH,SKY-BNB,EOS-USDT,EOS-BNB,CVC-BTC,CVC-ETH,CVC-BNB,THETA-BTC,THETA-ETH,THETA-BNB,XRP-BNB,TUSD-USDT,IOTA-USDT,XLM-USDT,IOTX-BTC,IOTX-ETH,QKC-BTC,QKC-ETH,AGI-BTC,AGI-ETH,AGI-BNB,NXS-BTC,NXS-ETH,NXS-BNB,ENJ-BNB,DATA-BTC,DATA-ETH,ONT-USDT,TRX-BNB,TRX-USDT,ETC-USDT,ETC-BNB,ICX-USDT,SC-BTC,SC-ETH,SC-BNB,NPXS-BTC,NPXS-ETH,KEY-BTC,KEY-ETH,NAS-BTC,NAS-ETH,NAS-BNB,MFT-BTC,MFT-ETH,MFT-BNB,DENT-BTC,DENT-ETH,ARDR-BTC,ARDR-ETH,ARDR-BNB,NULS-USDT,HOT-BTC,HOT-ETH,VET-BTC,VET-ETH,VET-USDT,VET-BNB,DOCK-BTC,DOCK-ETH,POLY-BTC,POLY-BNB,HC-BTC,HC-ETH,GO-BTC,GO-BNB,PAX-USDT,RVN-BTC,RVN-BNB,DCR-BTC,DCR-BNB,MITH-BTC,MITH-BNB,BCHABC-BTC,BCHABC-USDT,BNB-PAX,BTC-PAX,ETH-PAX,XRP-PAX,EOS-PAX,XLM-PAX,REN-BTC,REN-BNB,BNB-TUSD,XRP-TUSD,EOS-TUSD,XLM-TUSD,BNB-USDC,BTC-USDC,ETH-USDC,XRP-USDC,EOS-USDC,XLM-USDC,USDC-USDT,ADA-TUSD,TRX-TUSD,NEO-TUSD,TRX-XRP,XZC-XRP,PAX-TUSD,USDC-TUSD,USDC-PAX,LINK-USDT,LINK-TUSD,LINK-PAX,LINK-USDC,WAVES-USDT,WAVES-TUSD,WAVES-PAX,WAVES-USDC,BCHABC-TUSD,BCHABC-PAX,BCHABC-USDC,LTC-TUSD,LTC-PAX,LTC-USDC,TRX-PAX,TRX-USDC,BTT-BTC,BTT-BNB,BTT-USDT,BNB-USDS,BTC-USDS,USDS-USDT,USDS-PAX,USDS-TUSD,USDS-USDC,BTT-PAX,BTT-TUSD,BTT-USDC,ONG-BNB,ONG-BTC,ONG-USDT,HOT-BNB,HOT-USDT,ZIL-USDT,ZRX-BNB,ZRX-USDT,FET-BNB,FET-BTC,FET-USDT,BAT-USDT,XMR-BNB,XMR-USDT,ZEC-BNB,ZEC-USDT,ZEC-PAX,ZEC-TUSD,ZEC-USDC,IOST-BNB,IOST-USDT,CELR-BNB,CELR-BTC,CELR-USDT,ADA-PAX,ADA-USDC,NEO-PAX,NEO-USDC,DASH-BNB,DASH-USDT,NANO-USDT,OMG-BNB,OMG-USDT,THETA-USDT,ENJ-USDT,MITH-USDT,MATIC-BNB,MATIC-BTC,MATIC-USDT,ATOM-BNB,ATOM-BTC,ATOM-USDT,ATOM-USDC,ATOM-PAX,ATOM-TUSD,ETC-USDC,ETC-PAX,ETC-TUSD,BAT-USDC,BAT-PAX,BAT-TUSD,PHB-BNB,PHB-BTC,PHB-USDC,PHB-TUSD,PHB-PAX,TFUEL-BNB,TFUEL-BTC,TFUEL-USDT,TFUEL-USDC,TFUEL-TUSD,TFUEL-PAX,ONE-BNB,ONE-BTC,ONE-USDT,ONE-TUSD,ONE-PAX,ONE-USDC,FTM-BNB,FTM-BTC,FTM-USDT,FTM-TUSD,FTM-PAX,FTM-USDC,BTCB-BTC,BCPT-TUSD,BCPT-PAX,BCPT-USDC,ALGO-BNB,ALGO-BTC,ALGO-USDT,ALGO-TUSD,ALGO-PAX,ALGO-USDC,USDSB-USDT,USDSB-USDS,GTO-USDT,GTO-PAX,GTO-TUSD,GTO-USDC,ERD-BNB,ERD-BTC,ERD-USDT,ERD-PAX,ERD-USDC,DOGE-BNB,DOGE-BTC,DOGE-USDT,DOGE-PAX,DOGE-USDC,DUSK-BNB,DUSK-BTC,DUSK-USDT,DUSK-USDC,DUSK-PAX,BGBP-USDC,ANKR-BNB,ANKR-BTC,ANKR-USDT,ANKR-TUSD,ANKR-PAX,ANKR-USDC,ONT-PAX,ONT-USDC,WIN-BNB,WIN-BTC,WIN-USDT,WIN-USDC,COS-BNB,COS-BTC,COS-USDT,TUSDB-TUSD,NPXS-USDT,NPXS-USDC,COCOS-BNB,COCOS-BTC,COCOS-USDT,MTL-USDT,TOMO-BNB,TOMO-BTC,TOMO-USDT,TOMO-USDC", - "enabledPairs": "BTC-USDT", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USDT", + "available": "ETH-BTC,LTC-BTC,BNB-BTC,NEO-BTC,QTUM-ETH,EOS-ETH,SNT-ETH,BNT-ETH,GAS-BTC,BNB-ETH,BTC-USDT,ETH-USDT,OAX-ETH,DNT-ETH,MCO-ETH,MCO-BTC,WTC-BTC,WTC-ETH,LRC-BTC,LRC-ETH,QTUM-BTC,YOYO-BTC,OMG-BTC,OMG-ETH,ZRX-BTC,ZRX-ETH,STRAT-BTC,STRAT-ETH,SNGLS-BTC,BQX-BTC,BQX-ETH,KNC-BTC,KNC-ETH,FUN-BTC,FUN-ETH,SNM-BTC,SNM-ETH,NEO-ETH,IOTA-BTC,IOTA-ETH,LINK-BTC,LINK-ETH,XVG-BTC,XVG-ETH,MDA-BTC,MDA-ETH,MTL-BTC,MTL-ETH,EOS-BTC,SNT-BTC,ETC-ETH,ETC-BTC,MTH-BTC,MTH-ETH,ENG-BTC,ENG-ETH,DNT-BTC,ZEC-BTC,ZEC-ETH,BNT-BTC,AST-BTC,AST-ETH,DASH-BTC,DASH-ETH,OAX-BTC,BTG-BTC,BTG-ETH,EVX-BTC,EVX-ETH,REQ-BTC,REQ-ETH,VIB-BTC,VIB-ETH,TRX-BTC,TRX-ETH,POWR-BTC,POWR-ETH,ARK-BTC,ARK-ETH,YOYO-ETH,XRP-BTC,XRP-ETH,ENJ-BTC,ENJ-ETH,STORJ-BTC,STORJ-ETH,BNB-USDT,YOYO-BNB,POWR-BNB,KMD-BTC,KMD-ETH,NULS-BNB,RCN-BTC,RCN-ETH,RCN-BNB,NULS-BTC,NULS-ETH,RDN-BTC,RDN-ETH,RDN-BNB,XMR-BTC,XMR-ETH,DLT-BNB,WTC-BNB,DLT-BTC,DLT-ETH,AMB-BTC,AMB-ETH,AMB-BNB,BAT-BTC,BAT-ETH,BAT-BNB,BCPT-BTC,BCPT-ETH,BCPT-BNB,ARN-BTC,ARN-ETH,GVT-BTC,GVT-ETH,CDT-BTC,CDT-ETH,GXS-BTC,GXS-ETH,NEO-USDT,NEO-BNB,POE-BTC,POE-ETH,QSP-BTC,QSP-ETH,QSP-BNB,BTS-BTC,BTS-ETH,XZC-BTC,XZC-ETH,XZC-BNB,LSK-BTC,LSK-ETH,LSK-BNB,TNT-BTC,TNT-ETH,FUEL-BTC,MANA-BTC,MANA-ETH,BCD-BTC,BCD-ETH,DGD-BTC,DGD-ETH,IOTA-BNB,ADX-BTC,ADX-ETH,ADX-BNB,ADA-BTC,ADA-ETH,PPT-BTC,PPT-ETH,CMT-BTC,CMT-ETH,CMT-BNB,XLM-BTC,XLM-ETH,XLM-BNB,CND-BTC,CND-ETH,CND-BNB,LEND-BTC,LEND-ETH,WABI-BTC,WABI-ETH,WABI-BNB,LTC-ETH,LTC-USDT,LTC-BNB,TNB-BTC,TNB-ETH,WAVES-BTC,WAVES-ETH,WAVES-BNB,GTO-BTC,GTO-ETH,GTO-BNB,ICX-BTC,ICX-ETH,ICX-BNB,OST-BTC,OST-ETH,OST-BNB,ELF-BTC,ELF-ETH,AION-BTC,AION-ETH,AION-BNB,NEBL-BTC,NEBL-ETH,NEBL-BNB,BRD-BTC,BRD-ETH,BRD-BNB,MCO-BNB,EDO-BTC,EDO-ETH,NAV-BTC,LUN-BTC,APPC-BTC,APPC-ETH,APPC-BNB,VIBE-BTC,VIBE-ETH,RLC-BTC,RLC-ETH,RLC-BNB,INS-BTC,INS-ETH,PIVX-BTC,PIVX-ETH,PIVX-BNB,IOST-BTC,IOST-ETH,STEEM-BTC,STEEM-ETH,STEEM-BNB,NANO-BTC,NANO-ETH,NANO-BNB,VIA-BTC,VIA-ETH,VIA-BNB,BLZ-BTC,BLZ-ETH,BLZ-BNB,AE-BTC,AE-ETH,AE-BNB,NCASH-BTC,NCASH-ETH,POA-BTC,POA-ETH,POA-BNB,ZIL-BTC,ZIL-ETH,ZIL-BNB,ONT-BTC,ONT-ETH,ONT-BNB,STORM-BTC,STORM-ETH,STORM-BNB,QTUM-BNB,QTUM-USDT,XEM-BTC,XEM-ETH,XEM-BNB,WAN-BTC,WAN-ETH,WAN-BNB,WPR-BTC,WPR-ETH,QLC-BTC,QLC-ETH,SYS-BTC,SYS-ETH,SYS-BNB,QLC-BNB,GRS-BTC,GRS-ETH,ADA-USDT,ADA-BNB,GNT-BTC,GNT-ETH,GNT-BNB,LOOM-BTC,LOOM-ETH,LOOM-BNB,XRP-USDT,REP-BTC,REP-ETH,BTC-TUSD,ETH-TUSD,ZEN-BTC,ZEN-ETH,ZEN-BNB,SKY-BTC,SKY-ETH,SKY-BNB,EOS-USDT,EOS-BNB,CVC-BTC,CVC-ETH,THETA-BTC,THETA-ETH,THETA-BNB,XRP-BNB,TUSD-USDT,IOTA-USDT,XLM-USDT,IOTX-BTC,IOTX-ETH,QKC-BTC,QKC-ETH,AGI-BTC,AGI-ETH,AGI-BNB,NXS-BTC,NXS-ETH,NXS-BNB,ENJ-BNB,DATA-BTC,DATA-ETH,ONT-USDT,TRX-BNB,TRX-USDT,ETC-USDT,ETC-BNB,ICX-USDT,SC-BTC,SC-ETH,SC-BNB,NPXS-ETH,KEY-BTC,KEY-ETH,NAS-BTC,NAS-ETH,NAS-BNB,MFT-BTC,MFT-ETH,MFT-BNB,DENT-ETH,ARDR-BTC,ARDR-ETH,ARDR-BNB,NULS-USDT,HOT-BTC,HOT-ETH,VET-BTC,VET-ETH,VET-USDT,VET-BNB,DOCK-BTC,DOCK-ETH,POLY-BTC,POLY-BNB,HC-BTC,HC-ETH,GO-BTC,GO-BNB,PAX-USDT,RVN-BTC,RVN-BNB,DCR-BTC,DCR-BNB,MITH-BTC,MITH-BNB,BNB-PAX,BTC-PAX,ETH-PAX,XRP-PAX,EOS-PAX,XLM-PAX,REN-BTC,REN-BNB,BNB-TUSD,XRP-TUSD,EOS-TUSD,XLM-TUSD,BNB-USDC,BTC-USDC,ETH-USDC,XRP-USDC,EOS-USDC,XLM-USDC,USDC-USDT,ADA-TUSD,TRX-TUSD,NEO-TUSD,TRX-XRP,XZC-XRP,PAX-TUSD,USDC-TUSD,USDC-PAX,LINK-USDT,LINK-TUSD,LINK-PAX,LINK-USDC,WAVES-USDT,WAVES-TUSD,WAVES-USDC,LTC-TUSD,LTC-PAX,LTC-USDC,TRX-PAX,TRX-USDC,BTT-BNB,BTT-USDT,BNB-USDS,BTC-USDS,USDS-USDT,USDS-PAX,USDS-TUSD,USDS-USDC,BTT-PAX,BTT-TUSD,BTT-USDC,ONG-BNB,ONG-BTC,ONG-USDT,HOT-BNB,HOT-USDT,ZIL-USDT,ZRX-BNB,ZRX-USDT,FET-BNB,FET-BTC,FET-USDT,BAT-USDT,XMR-BNB,XMR-USDT,ZEC-BNB,ZEC-USDT,ZEC-PAX,ZEC-TUSD,ZEC-USDC,IOST-BNB,IOST-USDT,CELR-BNB,CELR-BTC,CELR-USDT,ADA-PAX,ADA-USDC,NEO-PAX,NEO-USDC,DASH-BNB,DASH-USDT,NANO-USDT,OMG-BNB,OMG-USDT,THETA-USDT,ENJ-USDT,MITH-USDT,MATIC-BNB,MATIC-BTC,MATIC-USDT,ATOM-BNB,ATOM-BTC,ATOM-USDT,ATOM-USDC,ATOM-TUSD,ETC-TUSD,BAT-USDC,BAT-PAX,BAT-TUSD,PHB-BNB,PHB-BTC,PHB-TUSD,TFUEL-BNB,TFUEL-BTC,TFUEL-USDT,ONE-BNB,ONE-BTC,ONE-USDT,ONE-USDC,FTM-BNB,FTM-BTC,FTM-USDT,FTM-USDC,ALGO-BNB,ALGO-BTC,ALGO-USDT,ALGO-TUSD,ALGO-PAX,ALGO-USDC,GTO-USDT,ERD-BNB,ERD-BTC,ERD-USDT,DOGE-BNB,DOGE-BTC,DOGE-USDT,DUSK-BNB,DUSK-BTC,DUSK-USDT,DUSK-USDC,DUSK-PAX,BGBP-USDC,ANKR-BNB,ANKR-BTC,ANKR-USDT,ONT-PAX,ONT-USDC,WIN-BNB,WIN-USDT,WIN-USDC,COS-BNB,COS-BTC,COS-USDT,NPXS-USDT,COCOS-BNB,COCOS-BTC,COCOS-USDT,MTL-USDT,TOMO-BNB,TOMO-BTC,TOMO-USDT,TOMO-USDC,PERL-BNB,PERL-BTC,PERL-USDC,PERL-USDT,DENT-USDT,MFT-USDT,KEY-USDT,STORM-USDT,DOCK-USDT,WAN-USDT,FUN-USDT,CVC-USDT,BTT-TRX,WIN-TRX,CHZ-BNB,CHZ-BTC,CHZ-USDT,BAND-BNB,BAND-BTC,BAND-USDT,BNB-BUSD,BTC-BUSD,BUSD-USDT,BEAM-BNB,BEAM-BTC,BEAM-USDT,XTZ-BNB,XTZ-BTC,XTZ-USDT,REN-USDT,RVN-USDT,HC-USDT,HBAR-BNB,HBAR-BTC,HBAR-USDT,NKN-BNB,NKN-BTC,NKN-USDT,XRP-BUSD,ETH-BUSD,LTC-BUSD,LINK-BUSD,ETC-BUSD,STX-BNB,STX-BTC,STX-USDT,KAVA-BNB,KAVA-BTC,KAVA-USDT,BUSD-NGN,BNB-NGN,BTC-NGN,ARPA-BNB,ARPA-BTC,ARPA-USDT,TRX-BUSD,EOS-BUSD,IOTX-USDT,RLC-USDT,MCO-USDT,XLM-BUSD,ADA-BUSD,CTXC-BNB,CTXC-BTC,CTXC-USDT,BCH-BNB,BCH-BTC,BCH-USDT,BCH-USDC,BCH-TUSD,BCH-PAX,BCH-BUSD,BTC-RUB,ETH-RUB,XRP-RUB,BNB-RUB" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -256,38 +373,70 @@ "name": "Bitfinex", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC,BTCEUR,BTCJPY,XRPUSD,XRPBTC,IOTUSD,IOTBTC,IOTETH,EOSUSD,EOSBTC,EOSETH,SANUSD,SANBTC,SANETH,OMGUSD,OMGBTC,OMGETH,NEOUSD,NEOBTC,NEOETH,ETPUSD,ETPBTC,ETPETH,QTMUSD,QTMBTC,QTMETH,AVTUSD,AVTBTC,AVTETH,EDOUSD,EDOBTC,EDOETH,BTGUSD,BTGBTC,DATUSD,DATBTC,DATETH,QSHUSD,QSHBTC,QSHETH,YYWUSD,YYWBTC,YYWETH,GNTUSD,GNTBTC,GNTETH,SNTUSD,SNTBTC,SNTETH,IOTEUR,BATUSD,BATBTC,BATETH,MNAUSD,MNABTC,MNAETH,FUNUSD,FUNBTC,FUNETH,ZRXUSD,ZRXBTC,ZRXETH,TNBUSD,TNBBTC,TNBETH,SPKUSD,SPKBTC,SPKETH,TRXUSD,TRXBTC,TRXETH,RCNUSD,RCNBTC,RCNETH,RLCUSD,RLCBTC,RLCETH,AIDUSD,AIDBTC,AIDETH,SNGUSD,SNGBTC,SNGETH,REPUSD,REPBTC,REPETH,ELFUSD,ELFBTC,ELFETH,NECUSD,NECBTC,NECETH,BTCGBP,ETHEUR,ETHJPY,ETHGBP,NEOEUR,NEOJPY,NEOGBP,EOSEUR,EOSJPY,EOSGBP,IOTJPY,IOTGBP,IOSUSD,IOSBTC,IOSETH,AIOUSD,AIOBTC,AIOETH,REQUSD,REQBTC,REQETH,RDNUSD,RDNBTC,RDNETH,LRCUSD,LRCBTC,LRCETH,WAXUSD,WAXBTC,WAXETH,DAIUSD,DAIBTC,DAIETH,AGIUSD,AGIBTC,AGIETH,BFTUSD,BFTBTC,BFTETH,MTNUSD,MTNBTC,MTNETH,ODEUSD,ODEBTC,ODEETH,ANTUSD,ANTBTC,ANTETH,DTHUSD,DTHBTC,DTHETH,MITUSD,MITBTC,MITETH,STJUSD,STJBTC,STJETH,XLMUSD,XLMEUR,XLMJPY,XLMGBP,XLMBTC,XLMETH,XVGUSD,XVGEUR,XVGJPY,XVGGBP,XVGBTC,XVGETH,BCIUSD,BCIBTC,MKRUSD,MKRBTC,MKRETH,KNCUSD,KNCBTC,KNCETH,POAUSD,POABTC,POAETH,EVTUSD,LYMUSD,LYMBTC,LYMETH,UTKUSD,UTKBTC,UTKETH,VEEUSD,VEEBTC,VEEETH,DADUSD,DADBTC,DADETH,ORSUSD,ORSBTC,ORSETH,AUCUSD,AUCBTC,AUCETH,POYUSD,POYBTC,POYETH,FSNUSD,FSNBTC,FSNETH,CBTUSD,CBTBTC,CBTETH,ZCNUSD,ZCNBTC,ZCNETH,SENUSD,SENBTC,SENETH,NCAUSD,NCABTC,NCAETH,CNDUSD,CNDBTC,CNDETH,CTXUSD,CTXBTC,CTXETH,PAIUSD,PAIBTC,SEEUSD,SEEBTC,SEEETH,ESSUSD,ESSBTC,ESSETH,ATMUSD,ATMBTC,ATMETH,HOTUSD,HOTBTC,HOTETH,DTAUSD,DTABTC,DTAETH,IQXUSD,IQXBTC,IQXEOS,WPRUSD,WPRBTC,WPRETH,ZILUSD,ZILBTC,ZILETH,BNTUSD,BNTBTC,BNTETH,ABSUSD,ABSETH,XRAUSD,XRAETH,MANUSD,MANETH,BBNUSD,BBNETH,NIOUSD,NIOETH,DGXUSD,DGXETH,VETUSD,VETBTC,VETETH,UTNUSD,UTNETH,TKNUSD,TKNETH,GOTUSD,GOTEUR,GOTETH,XTZUSD,XTZBTC,CNNUSD,CNNETH,BOXUSD,BOXETH,TRXEUR,TRXGBP,TRXJPY,MGOUSD,MGOETH,RTEUSD,RTEETH,YGGUSD,YGGETH,MLNUSD,MLNETH,WTCUSD,WTCETH,CSXUSD,CSXETH,OMNUSD,OMNBTC,INTUSD,INTETH,DRNUSD,DRNETH,PNKUSD,PNKETH,DGBUSD,DGBBTC,BSVUSD,BSVBTC,BABUSD,BABBTC,WLOUSD,WLOXLM,VLDUSD,VLDETH,ENJUSD,ENJETH,ONLUSD,ONLETH,RBTUSD,RBTBTC,USTUSD,EUTEUR,EUTUSD,GSDUSD,UDCUSD,TSDUSD,PAXUSD,RIFUSD,RIFBTC,PASUSD,PASETH,VSYUSD,VSYBTC,ZRXDAI,MKRDAI,OMGDAI,BTTUSD,BTTBTC,BTCUST,ETHUST,CLOUSD,CLOBTC,IMPUSD,IMPETH,LTCUST,EOSUST,BABUST,SCRUSD,SCRETH,GNOUSD,GNOETH,GENUSD,GENETH,ATOUSD,ATOBTC,ATOETH,WBTUSD,XCHUSD,EUSUSD,WBTETH,XCHETH,EUSETH,LEOUSD,LEOBTC,LEOUST,LEOEOS,LEOETH,ASTUSD,ASTETH,FOAUSD,FOAETH,UFRUSD,UFRETH,ZBTUSD,ZBTUST,OKBUSD,USKUSD,GTXUSD,KANUSD,OKBUST,OKBETH,OKBBTC,USKUST,USKETH,USKBTC,USKEOS,GTXUST,KANUST,AMPUSD,ALGUSD,ALGBTC,ALGUST,BTCXCH,SWMUSD,SWMETH,TRIUSD,TRIETH,LOOUSD,LOOETH,AMPUST,DUSK:USD,DUSK:BTC,UOSUSD,UOSBTC,RRBUSD,RRBUST,DTXUSD,DTXUST,AMPBTC,FTTUSD,FTTUST,BTCF0:USTF0,ETHF0:USTF0", - "enabledPairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC", + "available": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC,BTCEUR,BTCJPY,XRPUSD,XRPBTC,IOTUSD,IOTBTC,IOTETH,EOSUSD,EOSBTC,EOSETH,SANUSD,SANBTC,SANETH,OMGUSD,OMGBTC,OMGETH,NEOUSD,NEOBTC,NEOETH,ETPUSD,ETPBTC,ETPETH,QTMUSD,QTMBTC,QTMETH,AVTUSD,AVTBTC,AVTETH,EDOUSD,EDOBTC,EDOETH,BTGUSD,BTGBTC,DATUSD,DATBTC,DATETH,QSHUSD,QSHBTC,QSHETH,YYWUSD,YYWBTC,YYWETH,GNTUSD,GNTBTC,GNTETH,SNTUSD,SNTBTC,SNTETH,IOTEUR,BATUSD,BATBTC,BATETH,MNAUSD,MNABTC,MNAETH,FUNUSD,FUNBTC,FUNETH,ZRXUSD,ZRXBTC,ZRXETH,TNBUSD,TNBBTC,TNBETH,SPKUSD,SPKBTC,SPKETH,TRXUSD,TRXBTC,TRXETH,RCNUSD,RCNBTC,RCNETH,RLCUSD,RLCBTC,RLCETH,AIDUSD,AIDBTC,AIDETH,SNGUSD,SNGBTC,SNGETH,REPUSD,REPBTC,REPETH,ELFUSD,ELFBTC,ELFETH,NECUSD,NECBTC,NECETH,BTCGBP,ETHEUR,ETHJPY,ETHGBP,NEOEUR,NEOJPY,NEOGBP,EOSEUR,EOSJPY,EOSGBP,IOTJPY,IOTGBP,IOSUSD,IOSBTC,IOSETH,AIOUSD,AIOBTC,AIOETH,REQUSD,REQBTC,REQETH,RDNUSD,RDNBTC,RDNETH,LRCUSD,LRCBTC,LRCETH,WAXUSD,WAXBTC,WAXETH,DAIUSD,DAIBTC,DAIETH,AGIUSD,AGIBTC,AGIETH,BFTUSD,BFTBTC,BFTETH,MTNUSD,MTNBTC,MTNETH,ODEUSD,ODEBTC,ODEETH,ANTUSD,ANTBTC,ANTETH,DTHUSD,DTHBTC,DTHETH,MITUSD,MITBTC,MITETH,STJUSD,STJBTC,STJETH,XLMUSD,XLMEUR,XLMJPY,XLMGBP,XLMBTC,XLMETH,XVGUSD,XVGEUR,XVGJPY,XVGGBP,XVGBTC,XVGETH,BCIUSD,BCIBTC,MKRUSD,MKRBTC,MKRETH,KNCUSD,KNCBTC,KNCETH,POAUSD,POABTC,POAETH,EVTUSD,LYMUSD,LYMBTC,LYMETH,UTKUSD,UTKBTC,UTKETH,VEEUSD,VEEBTC,VEEETH,DADUSD,DADBTC,DADETH,ORSUSD,ORSBTC,ORSETH,AUCUSD,AUCBTC,AUCETH,POYUSD,POYBTC,POYETH,FSNUSD,FSNBTC,FSNETH,CBTUSD,CBTBTC,CBTETH,ZCNUSD,ZCNBTC,ZCNETH,SENUSD,SENBTC,SENETH,NCAUSD,NCABTC,NCAETH,CNDUSD,CNDBTC,CNDETH,CTXUSD,CTXBTC,CTXETH,PAIUSD,PAIBTC,SEEUSD,SEEBTC,SEEETH,ESSUSD,ESSBTC,ESSETH,ATMUSD,ATMBTC,ATMETH,HOTUSD,HOTBTC,HOTETH,DTAUSD,DTABTC,DTAETH,IQXUSD,IQXBTC,IQXEOS,WPRUSD,WPRBTC,WPRETH,ZILUSD,ZILBTC,ZILETH,BNTUSD,BNTBTC,BNTETH,ABSUSD,ABSETH,XRAUSD,XRAETH,MANUSD,MANETH,BBNUSD,BBNETH,NIOUSD,NIOETH,DGXUSD,DGXETH,VETUSD,VETBTC,VETETH,UTNUSD,UTNETH,TKNUSD,TKNETH,GOTUSD,GOTEUR,GOTETH,XTZUSD,XTZBTC,CNNUSD,CNNETH,BOXUSD,BOXETH,TRXEUR,TRXGBP,TRXJPY,MGOUSD,MGOETH,RTEUSD,RTEETH,YGGUSD,YGGETH,MLNUSD,MLNETH,WTCUSD,WTCETH,CSXUSD,CSXETH,OMNUSD,OMNBTC,INTUSD,INTETH,DRNUSD,DRNETH,PNKUSD,PNKETH,DGBUSD,DGBBTC,BSVUSD,BSVBTC,BABUSD,BABBTC,WLOUSD,WLOXLM,VLDUSD,VLDETH,ENJUSD,ENJETH,ONLUSD,ONLETH,RBTUSD,RBTBTC,USTUSD,EUTEUR,EUTUSD,GSDUSD,UDCUSD,TSDUSD,PAXUSD,RIFUSD,RIFBTC,PASUSD,PASETH,VSYUSD,VSYBTC,ZRXDAI,MKRDAI,OMGDAI,BTTUSD,BTTBTC,BTCUST,ETHUST,CLOUSD,CLOBTC,IMPUSD,IMPETH,LTCUST,EOSUST,BABUST,SCRUSD,SCRETH,GNOUSD,GNOETH,GENUSD,GENETH,ATOUSD,ATOBTC,ATOETH,WBTUSD,XCHUSD,EUSUSD,WBTETH,XCHETH,EUSETH,LEOUSD,LEOBTC,LEOUST,LEOEOS,LEOETH,ASTUSD,ASTETH,FOAUSD,FOAETH,UFRUSD,UFRETH,ZBTUSD,ZBTUST,OKBUSD,USKUSD,GTXUSD,KANUSD,OKBUST,OKBETH,OKBBTC,USKUST,USKETH,USKBTC,USKEOS,GTXUST,KANUST,AMPUSD,ALGUSD,ALGBTC,ALGUST,BTCXCH,SWMUSD,SWMETH,TRIUSD,TRIETH,LOOUSD,LOOETH,AMPUST,DUSK:USD,DUSK:BTC,UOSUSD,UOSBTC,RRBUSD,RRBUST,DTXUSD,DTXUST,AMPBTC,FTTUSD,FTTUST,PAXUST,UDCUST,TSDUST,BTC:CNHT,UST:CNHT,CNH:CNHT,CHZUSD,CHZUST,BTCF0:USTF0,ETHF0:USTF0" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "Deutsche Bank Privat Und Geschaeftskunden AG", "bankAddress": "Karlsruhe, 76125, GERMANY", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "GLOBAL TRADE SOLUTIONS GmbH", "accountNumber": "DE51660700240057016802", "swiftCode": "DEUTDEDB660", @@ -295,8 +444,12 @@ "supportedCurrencies": "EUR,USD" }, { + "enabled": false, "bankName": "Deutsche Bank Privat Und Geschaeftskunden AG", "bankAddress": "Karlsruhe, 76125, GERMANY", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "GLOBAL TRADE SOLUTIONS GmbH", "accountNumber": "DE78660700240057016801", "swiftCode": "DEUTDEDB660", @@ -309,41 +462,73 @@ "name": "Bitflyer", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC_JPY,FXBTC_JPY,ETH_BTC,BCH_BTC", - "enabledPairs": "BTC_JPY,ETH_BTC,BCH_BTC", "baseCurrencies": "JPY", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": false, - "pairsLastUpdated": 1566798411, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "lastUpdated": 1566798411, + "assetTypes": [ + "spot", + "futures" + ], + "pairs": { + "spot": { + "enabled": "BTC_JPY,ETH_BTC,BCH_BTC", + "available": "BTC_JPY,FXBTC_JPY,ETH_BTC,BCH_BTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": false, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -356,40 +541,72 @@ "name": "Bithumb", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "clientId": "ClientID", - "availablePairs": "STRATKRW,PLYKRW,ETCKRW,LOOMKRW,BZNTKRW,WTCKRW,ENJKRW,INSKRW,ELFKRW,ZRXKRW,CMTKRW,CROKRW,APISKRW,LINKKRW,MCOKRW,MIXKRW,BHPKRW,ZECKRW,BSVKRW,BATKRW,WAXKRW,SNTKRW,ORBSKRW,XLMKRW,LAMBKRW,EOSKRW,HYCKRW,OCNKRW,MITHKRW,OMGKRW,XRPKRW,BCHKRW,VALORKRW,AEKRW,BTTKRW,THETAKRW,IOSTKRW,RNTKRW,AMOKRW,XVGKRW,ABTKRW,SALTKRW,MXCKRW,KNCKRW,REPKRW,POLYKRW,LRCKRW,ADAKRW,DACCKRW,MTLKRW,HDACKRW,ITCKRW,LBAKRW,RDNKRW,TMTGKRW,TRUEKRW,ARNKRW,VETKRW,DASHKRW,PSTKRW,WETKRW,ICXKRW,STEEMKRW,DACKRW,ROMKRW,AUTOKRW,CONKRW,XEMKRW,QKCKRW,WAVESKRW,TRXKRW,XMRKRW,BTGKRW,NPXSKRW,ANKRKRW,QTUMKRW,POWRKRW,HCKRW,ETZKRW,ETHKRW,CTXCKRW,GTOKRW,BCDKRW,ETHOSKRW,PIVXKRW,LTCKRW,GNTKRW,PAYKRW,BTCKRW,ZILKRW,PPTKRW,GXCKRW", - "enabledPairs": "BTCKRW,ETHKRW,DASHKRW,LTCKRW,ETCKRW,XRPKRW,BCHKRW,XMRKRW,ZECKRW,QTUMKRW,BTGKRW,EOSKRW", "baseCurrencies": "KRW", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "index": "KRW" + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "index": "KRW" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCKRW,ETHKRW,DASHKRW,LTCKRW,ETCKRW,XRPKRW,BCHKRW,XMRKRW,ZECKRW,QTUMKRW,BTGKRW,EOSKRW", + "available": "ICXKRW,CMTKRW,BCHKRW,MCOKRW,XVGKRW,KNCKRW,XMRKRW,EOSKRW,IOSTKRW,HDACKRW,TMTGKRW,QKCKRW,TRXKRW,ARNKRW,WAVESKRW,DASHKRW,BTCKRW,RNTKRW,DADKRW,HYCKRW,DACKRW,XSRKRW,LINKKRW,BTTKRW,FABKRW,DACCKRW,STEEMKRW,PSTKRW,GTOKRW,ZILKRW,APISKRW,VALORKRW,PAYKRW,BHPKRW,THETAKRW,XLMKRW,ETCKRW,FNBKRW,MTLKRW,LTCKRW,ETZKRW,ROMKRW,PPTKRW,AUTOKRW,FCTKRW,VETKRW,NPXSKRW,BSVKRW,ZRXKRW,LOOMKRW,CROKRW,AOAKRW,WETKRW,GXCKRW,CONKRW,SALTKRW,CHRKRW,POWRKRW,ZECKRW,SNTKRW,INSKRW,ETHKRW,XEMKRW,DVPKRW,ETHOSKRW,PLYKRW,BZNTKRW,OGOKRW,LBAKRW,ADAKRW,WOMKRW,GNTKRW,LRCKRW,HCKRW,ABTKRW,FZZKRW,PIVXKRW,ENJKRW,RDNKRW,TRUEKRW,LAMBKRW,MITHKRW,ORBSKRW,OCNKRW,WICCKRW,PCMKRW,WTCKRW,OMGKRW,MXCKRW,MIXKRW,BTGKRW,AEKRW,WAXPKRW,TRVKRW,FXKRW,AMOKRW,XRPKRW,ELFKRW,ITCKRW,REPKRW,ANKRKRW,QTUMKRW,POLYKRW,BATKRW,STRATKRW,BCDKRW,CTXCKRW" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -402,39 +619,71 @@ "name": "Bitstamp", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "clientId": "ClientID", - "availablePairs": "LTCUSD,ETHUSD,XRPEUR,BCHUSD,BCHEUR,BTCEUR,XRPBTC,EURUSD,BCHBTC,LTCEUR,BTCUSD,LTCBTC,XRPUSD,ETHBTC,ETHEUR", - "enabledPairs": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", "baseCurrencies": "USD,EUR", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", + "available": "LTCUSD,ETHUSD,XRPEUR,BCHUSD,BCHEUR,BTCEUR,XRPBTC,EURUSD,BCHBTC,LTCEUR,BTCUSD,LTCBTC,XRPUSD,ETHBTC,ETHEUR" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -447,40 +696,72 @@ "name": "LBank", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "FBC_USDT,HDS_USDT,GALT_USDT,IOG_USDT,IOEX_USDT,VOLLAR_USDT,OATH_USDT,BLOC_USDT,BTC_USDT,ETH_USDT,ETH_BTC,ABBC_BTC,BZKY_ETH,ONOT_ETH,KISC_ETH,BXA_USDT,ATP_USDT,MAT_USDT,SKY_BTC,RNT_USDT,VENA_USDT,GRIN_USDT,IDA_USDT,PNT_USDT,BSV_USDT,OPX_USDT,TENA_ETH,VTHO_BTC,VNX_BTC,AMO_ETH,UBEX_BTC,EOS_BTC,UBEX_USDT,TNS_BTC,ALI_ETH,SDC_ETH,SAIT_ETH,ARTCN_USDT,DAX_BTC,DAX_ETH,DALI_USDT,VET_USDT,BCH_BTC,BCH_USDT,NEO_USDT,QTUM_USDT,ZEC_USDT,VET_BTC,PAI_BTC,PNT_BTC,NEO_BTC,DASH_BTC,LTC_BTC,ETC_BTC,QTUM_BTC,ZEC_BTC,SC_BTC,BTS_BTC,CPX_BTC,XWC_BTC,FIL6_BTC,FIL12_BTC,FIL36_BTC,EOS_USDT,UT_ETH,ELA_ETH,VET_ETH,VTHO_ETH,PAI_ETH,BFDT_ETH,HER_ETH,PTT_ETH,TAC_ETH,IDHUB_ETH,SSC_ETH,SKM_ETH,IIC_ETH,PLY_ETH,EXT_ETH,EOS_ETH,YOYOW_ETH,TRX_ETH,QTUM_ETH,ZEC_ETH,BTS_ETH,BTM_ETH,MITH_ETH,NAS_ETH,MAN_ETH,DBC_ETH,BTO_ETH,DDD_ETH,CPX_ETH,CS_ETH,IHT_ETH,TKY_ETH,OCN_ETH,DCT_ETH,ZPT_ETH,EKO_ETH,MDA_ETH,PST_ETH,XWC_ETH,PUT_ETH,PNT_ETH,AAC_ETH,FIL6_ETH,FIL12_ETH,FIL36_ETH,UIP_ETH,SEER_ETH,BSB_ETH,CDC_ETH,GRAMS_ETH,DDMX_ETH,EAI_ETH,INC_ETH,BNB_USDT,HT_USDT,KBC_BTC,KBC_USDT,MAI_USDT,PHV_USDT,GT_USDT,B91_USDT,VOKEN_USDT,CYE_USDT,BRC_USDT,BTC_AUSD,CXC_BTC,CXC_USDT,DDMX_USDT,SEAL_USDT,SEOS_BTC,BTY_USDT,FO_USDT,VCC_ETH,DLX_USDT,KDS_USDT,BFC_USDT,LBK_USDT,SERO_USDT,MTV_USDT,CKB_USDT,ARPA_USDT,ZIP_USDT,AT_USDT", - "enabledPairs": "eth_btc", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_" + }, + "configFormat": { + "uppercase": false, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "eth_btc", + "available": "FBC_USDT,GALT_USDT,IOEX_USDT,OATH_USDT,BLOC_USDT,BTC_USDT,ETH_USDT,ETH_BTC,ABBC_BTC,KISC_ETH,BXA_USDT,ATP_USDT,MAT_USDT,SKY_BTC,RNT_USDT,VENA_USDT,GRIN_USDT,IDA_USDT,PNT_USDT,OPX_USDT,VTHO_BTC,AMO_ETH,UBEX_BTC,EOS_BTC,UBEX_USDT,TNS_BTC,SAIT_ETH,DAX_BTC,DAX_ETH,DALI_USDT,VET_USDT,BCH_BTC,BCH_USDT,NEO_USDT,QTUM_USDT,ZEC_USDT,VET_BTC,PAI_BTC,PNT_BTC,NEO_BTC,DASH_BTC,LTC_BTC,ETC_BTC,QTUM_BTC,ZEC_BTC,SC_BTC,BTS_BTC,CPX_BTC,XWC_BTC,FIL6_BTC,FIL12_BTC,FIL36_BTC,EOS_USDT,UT_ETH,ELA_ETH,VET_ETH,VTHO_ETH,PAI_ETH,HER_ETH,PTT_ETH,TAC_ETH,IDHUB_ETH,SSC_ETH,SKM_ETH,PLY_ETH,EXT_ETH,EOS_ETH,YOYOW_ETH,TRX_ETH,QTUM_ETH,ZEC_ETH,BTS_ETH,BTM_ETH,MITH_ETH,NAS_ETH,MAN_ETH,DBC_ETH,BTO_ETH,DDD_ETH,CPX_ETH,CS_ETH,IHT_ETH,OCN_ETH,EKO_ETH,XWC_ETH,PUT_ETH,PNT_ETH,AAC_ETH,FIL6_ETH,FIL12_ETH,FIL36_ETH,SEER_ETH,BSB_ETH,CDC_ETH,GRAMS_ETH,DDMX_ETH,EAI_ETH,BNB_USDT,HT_USDT,KBC_BTC,KBC_USDT,MAI_USDT,PHV_USDT,GT_USDT,VOKEN_USDT,CYE_USDT,BRC_USDT,BTC_AUSD,DDMX_USDT,SEAL_USDT,SEOS_BTC,BTY_USDT,FO_USDT,DLX_USDT,BFC_USDT,LBK_USDT,SERO_USDT,MTV_USDT,CKB_USDT,ARPA_USDT,ZIP_USDT,AT_USDT,DOT_USDT,DILI_USDT,DUO_USDT,TEP_USDT,BIKI_USDT,MX_USDT,DNS_USDT,OKB_USDT,FLDT_USDT,CCTC_USDT,WIN_USDT,BTT_USDT,TRX_USDT,GRS_BTC,GST_USDT,GST_ETH,ABBC_USDT,UTK_USDT,GKI_USDT,BPX_USDT,SUTER_USDT,LT_USDT,LM_USDT" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -493,40 +774,72 @@ "name": "Bittrex", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC-LTC,BTC-DOGE,BTC-VTC,BTC-PPC,BTC-FTC,BTC-RDD,BTC-NXT,BTC-DASH,BTC-POT,BTC-BLK,BTC-EMC2,BTC-XMY,BTC-GLC,BTC-GRS,BTC-NLG,BTC-MONA,BTC-VRC,BTC-CURE,BTC-XMR,BTC-XDN,BTC-NAV,BTC-XST,BTC-VIA,BTC-PINK,BTC-IOC,BTC-SYS,BTC-DGB,BTC-BURST,BTC-EXCL,BTC-BLOCK,BTC-BTS,BTC-XRP,BTC-GAME,BTC-NXS,BTC-GEO,BTC-FLDC,BTC-FLO,BTC-MUE,BTC-XEM,BTC-SPHR,BTC-OK,BTC-AEON,BTC-ETH,BTC-EXP,BTC-AMP,BTC-XLM,USDT-BTC,BTC-RVR,BTC-FCT,BTC-MAID,BTC-SLS,BTC-RADS,BTC-DCR,BTC-XVG,BTC-PIVX,BTC-MEME,BTC-STEEM,BTC-LSK,BTC-DGD,BTC-WAVES,BTC-LBC,BTC-SBD,BTC-ETC,ETH-ETC,BTC-STRAT,BTC-REP,BTC-SHIFT,BTC-ARDR,BTC-XZC,BTC-NEO,BTC-ZEC,BTC-IOP,BTC-UBQ,BTC-KMD,BTC-SIB,BTC-ION,BTC-CRW,BTC-SWT,BTC-MLN,BTC-ARK,BTC-INCNT,BTC-GBYTE,BTC-GNT,BTC-EDG,BTC-MORE,ETH-GNT,ETH-REP,USDT-ETH,BTC-WINGS,BTC-RLC,BTC-GNO,BTC-GUP,ETH-GNO,BTC-HMQ,BTC-ANT,ETH-ANT,BTC-SC,ETH-BAT,BTC-BAT,BTC-ZEN,BTC-1ST,BTC-QRL,BTC-PTOY,BTC-BNT,ETH-BNT,BTC-NMR,ETH-LTC,ETH-XRP,BTC-SNT,ETH-SNT,BTC-DCT,BTC-XEL,BTC-MCO,ETH-MCO,BTC-ADT,BTC-FUN,BTC-PAY,ETH-PAY,BTC-MTL,BTC-STORJ,BTC-ADX,ETH-ADX,ETH-DASH,ETH-SC,ETH-ZEC,USDT-ZEC,USDT-LTC,USDT-ETC,USDT-XRP,BTC-OMG,ETH-OMG,BTC-CVC,ETH-CVC,BTC-PART,BTC-QTUM,ETH-QTUM,ETH-XMR,ETH-XEM,ETH-XLM,ETH-NEO,USDT-XMR,USDT-DASH,ETH-BCH,USDT-BCH,BTC-BCH,BTC-DNT,USDT-NEO,ETH-WAVES,ETH-STRAT,ETH-DGB,USDT-OMG,BTC-ADA,BTC-MANA,ETH-MANA,BTC-RCN,BTC-VIB,ETH-VIB,BTC-MER,BTC-POWR,ETH-POWR,ETH-ADA,BTC-ENG,ETH-ENG,USDT-ADA,USDT-XVG,USDT-NXT,BTC-UKG,ETH-UKG,BTC-IGNIS,BTC-SRN,ETH-SRN,BTC-WAX,ETH-WAX,BTC-ZRX,ETH-ZRX,BTC-VEE,BTC-BCPT,BTC-TRX,ETH-TRX,BTC-TUSD,BTC-LRC,ETH-TUSD,BTC-UP,BTC-DMT,ETH-DMT,USDT-TUSD,BTC-POLY,ETH-POLY,BTC-PRO,USDT-SC,USDT-TRX,BTC-BLT,BTC-STORM,ETH-STORM,BTC-AID,BTC-NGC,BTC-GTO,USDT-DCR,BTC-OCN,ETH-OCN,USD-BTC,USD-USDT,USD-TUSD,BTC-TUBE,BTC-CMCT,USD-ETH,BTC-NLC2,BTC-BKX,BTC-MFT,BTC-LOOM,BTC-RFR,USDT-DGB,BTC-RVN,USD-XRP,USD-ETC,BTC-BFT,BTC-GO,BTC-HYDRO,BTC-UPP,USD-ADA,USD-ZEC,USDT-DOGE,BTC-ENJ,BTC-MET,USD-LTC,USD-TRX,BTC-DTA,BTC-EDR,BTC-BOXX,BTC-IHT,USD-BCH,BTC-XHV,USDT-ZRX,BTC-NPXS,BTC-PMA,USDT-BAT,USDT-RVN,BTC-PAL,USD-SC,BTC-PAX,USDT-PAX,BTC-ZIL,BTC-MOC,BTC-OST,BTC-SPC,BTC-MEDX,BTC-BSV,BTC-IOST,BTC-XNK,USDT-BSV,ETH-BSV,BTC-NCASH,BTC-SOLVE,BTC-USDS,USDT-PMA,ETH-NPXS,USDT-NPXS,USD-ZRX,BTC-JNT,BTC-LBA,BTC-MOBI,USD-BAT,USD-BSV,BTC-DENT,USD-USDS,BTC-DRGN,USD-PAX,BTC-VITE,BTC-IOTX,USD-DGB,BTC-BTM,BTC-ELF,USD-EDR,BTC-QNT,BTC-BTU,USD-ZEN,BTC-SPND,BTC-BTT,BTC-NKN,USD-KMD,USDT-BTT,BTC-GRIN,BTC-CTXC,BTC-HXRO,BTC-META,USDT-GRIN,BTC-FSN,BTC-HST,BTC-ANKR,USDT-XLM,BTC-TRAC,BTC-CRO,BTC-ONT,ETH-SOLVE,BTC-ONG,BTC-AERGO,BTC-TTC,USD-SPND,BTC-SLT,BTC-PTON,BTC-PI,ETH-ANKR,BTC-PLA,BTC-ART,BTC-ORBS,USDT-ENJ,BTC-VBK,BTC-BORA,BTC-CND,USDT-ONT,BTC-TRIO,BTC-FX,ETH-FX,BTC-ATOM,USDT-ATOM,ETH-ATOM,BTC-XYO,BTC-OCEAN,USDT-OCEAN,BTC-WIB,BTC-BWX,BTC-SNX,BTC-SUSD,BTC-VDX,USDT-VDX,ETH-VDX,BTC-COSM,BTC-OGO,USDT-OGO,BTC-ITM,BTC-LAMB,BTC-STPT,BTC-FET,BTC-MKR,ETH-MKR,BTC-DAI,ETH-DAI,USDT-DAI,BTC-CPT,BTC-ABT,BTC-PROM,BTC-FTM,BTC-ABYSS,BTC-EOS,ETH-EOS,USDT-EOS,BTC-FXC,BTC-DUSK,BTC-URAC,BTC-BLOC,BTC-BRZ,BTC-TEMCO,BTC-SPIN,BTC-HINT,BTC-LUNA,BTC-CHR,BTC-TUDA,BTC-UTK,BTC-PXL,BTC-AKRO,BTC-TSHP", - "enabledPairs": "USDT-BTC", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "USDT-BTC", + "available": "BTC-LTC,BTC-DOGE,BTC-VTC,BTC-PPC,BTC-FTC,BTC-RDD,BTC-NXT,BTC-DASH,BTC-POT,BTC-BLK,BTC-EMC2,BTC-XMY,BTC-GRS,BTC-NLG,BTC-MONA,BTC-VRC,BTC-CURE,BTC-XMR,BTC-XDN,BTC-NAV,BTC-XST,BTC-VIA,BTC-PINK,BTC-IOC,BTC-SYS,BTC-DGB,BTC-BURST,BTC-EXCL,BTC-BLOCK,BTC-BTS,BTC-XRP,BTC-GAME,BTC-NXS,BTC-GEO,BTC-FLO,BTC-MUE,BTC-XEM,BTC-SPHR,BTC-OK,BTC-AEON,BTC-ETH,BTC-EXP,BTC-XLM,USDT-BTC,BTC-FCT,BTC-MAID,BTC-SLS,BTC-RADS,BTC-DCR,BTC-XVG,BTC-PIVX,BTC-MEME,BTC-STEEM,BTC-LSK,BTC-WAVES,BTC-LBC,BTC-SBD,BTC-ETC,ETH-ETC,BTC-STRAT,BTC-REP,BTC-ARDR,BTC-XZC,BTC-NEO,BTC-ZEC,BTC-UBQ,BTC-KMD,BTC-SIB,BTC-ION,BTC-CRW,BTC-ARK,BTC-INCNT,BTC-GBYTE,BTC-GNT,BTC-EDG,BTC-MORE,ETH-GNT,ETH-REP,USDT-ETH,BTC-RLC,BTC-GNO,ETH-GNO,BTC-HMQ,BTC-ANT,ETH-ANT,BTC-SC,ETH-BAT,BTC-BAT,BTC-ZEN,BTC-1ST,BTC-QRL,BTC-PTOY,BTC-BNT,ETH-BNT,BTC-NMR,ETH-LTC,ETH-XRP,BTC-SNT,ETH-SNT,BTC-DCT,BTC-XEL,BTC-MCO,ETH-MCO,BTC-ADT,BTC-PAY,ETH-PAY,BTC-MTL,BTC-STORJ,BTC-ADX,ETH-ADX,ETH-DASH,ETH-SC,ETH-ZEC,USDT-ZEC,USDT-LTC,USDT-ETC,USDT-XRP,BTC-OMG,ETH-OMG,BTC-CVC,ETH-CVC,BTC-PART,BTC-QTUM,ETH-QTUM,ETH-XMR,ETH-XEM,ETH-XLM,ETH-NEO,USDT-XMR,USDT-DASH,ETH-BCH,USDT-BCH,BTC-BCH,BTC-DNT,USDT-NEO,ETH-WAVES,ETH-STRAT,ETH-DGB,USDT-OMG,BTC-ADA,BTC-MANA,ETH-MANA,BTC-RCN,BTC-VIB,ETH-VIB,BTC-MER,ETH-ADA,BTC-ENG,ETH-ENG,USDT-ADA,USDT-XVG,BTC-UKG,ETH-UKG,BTC-IGNIS,BTC-SRN,ETH-SRN,BTC-WAXP,ETH-WAXP,BTC-ZRX,ETH-ZRX,BTC-VEE,BTC-TRX,ETH-TRX,BTC-TUSD,BTC-LRC,ETH-TUSD,BTC-DMT,ETH-DMT,USDT-TUSD,USDT-SC,USDT-TRX,BTC-STORM,ETH-STORM,BTC-AID,BTC-GTO,USDT-DCR,USD-BTC,USD-USDT,USD-TUSD,BTC-TUBE,BTC-CMCT,USD-ETH,BTC-NLC2,BTC-BKX,BTC-MFT,BTC-LOOM,BTC-RFR,USDT-DGB,BTC-RVN,USD-XRP,USD-ETC,BTC-BFT,BTC-GO,BTC-HYDRO,BTC-UPP,USD-ADA,USD-ZEC,USDT-DOGE,BTC-ENJ,BTC-MET,USD-LTC,USD-TRX,BTC-DTA,BTC-EDR,BTC-IHT,USD-BCH,BTC-XHV,USDT-ZRX,BTC-NPXS,BTC-PMA,USDT-BAT,USDT-RVN,BTC-PAL,USD-SC,BTC-PAX,BTC-ZIL,BTC-MOC,BTC-OST,BTC-SPC,BTC-MED,BTC-BSV,BTC-IOST,USDT-BSV,ETH-BSV,BTC-SOLVE,BTC-USDS,USDT-PMA,ETH-NPXS,USDT-NPXS,USD-ZRX,BTC-JNT,BTC-LBA,USD-BAT,USD-BSV,BTC-DENT,USD-USDS,BTC-DRGN,USD-PAX,BTC-VITE,BTC-IOTX,USD-DGB,BTC-BTM,BTC-ELF,BTC-QNT,BTC-BTU,USD-ZEN,BTC-SPND,BTC-BTT,BTC-NKN,USD-KMD,USDT-BTT,BTC-GRIN,BTC-CTXC,BTC-HXRO,BTC-META,USDT-GRIN,BTC-FSN,BTC-ANKR,USDT-XLM,BTC-TRAC,BTC-CRO,BTC-ONT,ETH-SOLVE,BTC-ONG,BTC-TTC,BTC-PTON,BTC-PI,ETH-ANKR,BTC-PLA,BTC-ART,BTC-ORBS,USDT-ENJ,BTC-VBK,BTC-BORA,BTC-CND,USDT-ONT,BTC-FX,ETH-FX,BTC-ATOM,USDT-ATOM,ETH-ATOM,BTC-OCEAN,USDT-OCEAN,BTC-BWX,BTC-VDX,USDT-VDX,ETH-VDX,BTC-COSM,BTC-LAMB,BTC-STPT,BTC-DAI,ETH-DAI,USDT-DAI,BTC-CPT,BTC-FNB,BTC-PROM,BTC-ABYSS,BTC-EOS,ETH-EOS,USDT-EOS,BTC-FXC,BTC-DUSK,BTC-URAC,BTC-BLOC,BTC-TEMCO,BTC-SPIN,BTC-LUNA,BTC-CHR,BTC-TUDA,BTC-UTK,BTC-PXL,BTC-AKRO,BTC-TSHP,BTC-HEDG,BTC-MRPH,BTC-HBAR,ETH-HBAR,USD-HBAR,USDT-HBAR,BTC-PLG,BTC-VET,USDT-VET,BTC-SIX,BTC-WGP,BTC-APM,BTC-FLETA,USD-DCR,BTC-BLTV" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -539,40 +852,71 @@ "name": "BTSE", "enabled": true, "verbose": false, - "websocket": true, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC-CNY,BTC-EUR,BTC-GBP,BTC-HKD,BTC-JPY,BTC-SGD,BTC-USD,ETH-CNY,ETH-EUR,ETH-GBP,ETH-HKD,ETH-JPY,ETH-SGD,ETH-USD,LTC-CNY,LTC-EUR,LTC-GBP,LTC-HKD,LTC-JPY,LTC-SGD,LTC-USD,USDT-CNY,USDT-EUR,USDT-GBP,USDT-HKD,USDT-JPY,USDT-SGD,USDT-USD", - "enabledPairs": "BTC-USD", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USD", + "available": "BTC-CNY,BTC-EUR,BTC-GBP,BTC-HKD,BTC-JPY,BTC-SGD,BTC-USD,ETH-CNY,ETH-EUR,ETH-GBP,ETH-HKD,ETH-JPY,ETH-SGD,ETH-USD,LTC-CNY,LTC-EUR,LTC-GBP,LTC-HKD,LTC-JPY,LTC-SGD,LTC-USD,USDT-CNY,USDT-EUR,USDT-GBP,USDT-HKD,USDT-JPY,USDT-SGD,USDT-USD,XMR-CNY,XMR-EUR,XMR-GBP,XMR-HKD,XMR-JPY,XMR-SGD,XMR-USD" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -585,39 +929,71 @@ "name": "BTC Markets", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC-AUD,LTC-AUD,LTC-BTC,ETH-BTC,ETH-AUD,ETC-AUD,ETC-BTC,XRP-AUD,XRP-BTC,POWR-AUD,POWR-BTC,OMG-AUD,OMG-BTC,BCHABC-AUD,BCHABC-BTC,BCHSV-AUD,BCHSV-BTC,GNT-AUD,GNT-BTC,BAT-AUD,BAT-BTC,XLM-AUD,XLM-BTC", - "enabledPairs": "BTC-AUD", "baseCurrencies": "AUD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-AUD", + "available": "BTC-AUD,LTC-AUD,LTC-BTC,ETH-BTC,ETH-AUD,ETC-AUD,ETC-BTC,XRP-AUD,XRP-BTC,POWR-AUD,POWR-BTC,OMG-AUD,OMG-BTC,BCHABC-AUD,BCHABC-BTC,BCHSV-AUD,BCHSV-BTC,GNT-AUD,GNT-BTC,BAT-AUD,BAT-BTC,XLM-AUD,XLM-BTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresBase64DecodeSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -630,39 +1006,70 @@ "name": "COINUT", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "clientId": "ClientID", - "availablePairs": "ETCBTC,ETCSGD,LTCSGD,ZECCAD,BTCUSDT,XMRBTC,USDTUSD,LTCBTC,LTCCAD,ZECLTC,BTCUSD,ZECBTC,ETHLTC,ETCLTC,ETCUSDT,ETHBTC,ETHUSDT,LTCUSDT,XMRLTC,ZECSGD,BTCCAD,ZECUSD,XMRUSDT,ZECUSDT,ETHSGD,ETHCAD,ETHUSD,LTCUSD,USDTSGD,BTCSGD", - "enabledPairs": "LTCBTC,ETCBTC,ETHBTC", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "LTC-USDT", + "available": "LTC-CAD,LTC-SGD,USDT-USD,ETC-LTC,LTC-BTC,USDT-SGD,XMR-USDT,ZEC-SGD,ETH-USD,BTC-USDT,ETC-BTC,ETH-LTC,LTC-USD,BTC-USD,ETH-USDT,XMR-LTC,ZEC-USD,ETC-SGD,DAI-SGD,ZEC-CAD,BTC-SGD,ETH-BTC,ETH-SGD,LTC-USDT,ZEC-BTC,ZEC-USDT,BTC-CAD,XMR-BTC,ZEC-LTC,ETC-USDT,ETH-CAD" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -675,41 +1082,73 @@ "name": "EXMO", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "MNX_ETH,OMG_USD,XMR_USD,BTC_TRY,GUSD_RUB,TRX_USD,DAI_ETH,MNX_USD,ETH_USD,DCR_RUB,PTI_USDT,PTI_EOS,XLM_USD,ETH_BTC,EXM_BTC,BTC_PLN,ROOBEE_ETH,XRP_EUR,USDC_BTC,XLM_RUB,ETH_LTC,BCH_BTC,DASH_UAH,ETZ_BTC,NEO_USD,TRX_RUB,XMR_UAH,XLM_TRY,BCH_EUR,OMG_ETH,ETH_PLN,ZEC_BTC,XRP_BTC,ATMCASH_BTC,XRP_TRY,MNC_USD,WAVES_BTC,USDT_EUR,ETZ_USDT,MNC_BTC,ETH_UAH,STQ_BTC,HBZ_BTC,LTC_BTC,MKR_BTC,HB_BTC,GNT_BTC,STQ_RUB,BCH_ETH,ZEC_EUR,XMR_EUR,USD_RUB,USDC_USDT,ETH_TRY,LSK_BTC,XRP_USD,ZRX_ETH,DASH_BTC,ETH_EUR,STQ_USD,STQ_EUR,BTCZ_BTC,LTC_UAH,XTZ_BTC,DAI_RUB,ADA_USD,XLM_BTC,LTC_RUB,GNT_ETH,BCH_RUB,USDT_RUB,XTZ_RUB,ADA_BTC,ADA_ETH,LSK_USD,BCH_UAH,WAVES_ETH,ZRX_USD,ZRX_BTC,BTG_USD,DXT_USD,XEM_USD,GUSD_USD,ETC_RUB,BCH_USDT,DOGE_USD,ROOBEE_BTC,USDC_ETH,SMART_USD,USDT_UAH,DAI_BTC,EOS_USD,KICK_ETH,DAI_USD,QTUM_BTC,BTG_BTC,LTC_EUR,WAVES_RUB,BTC_EUR,BTC_RUB,USDC_USD,DOGE_BTC,MKR_DAI,QTUM_ETH,ZEC_USD,BCH_USD,DCR_UAH,SMART_BTC,NEO_BTC,DASH_USDT,XMR_RUB,LTC_USD,XMR_BTC,ETH_RUB,EOS_EUR,XRP_USDT,WAVES_USD,XRP_ETH,ETZ_ETH,LSK_RUB,OMG_BTC,USDT_USD,INK_ETH,INK_USD,ETC_BTC,XRP_UAH,QTUM_USD,XEM_BTC,MNX_BTC,HBZ_USD,KICK_USDT,XEM_UAH,BTC_USD,DCR_BTC,MNC_ETH,DASH_RUB,KICK_BTC,PTI_BTC,INK_BTC,EOS_BTC,GAS_BTC,XRP_RUB,ETH_USDT,BTG_ETH,PTI_RUB,KICK_RUB,NEO_RUB,SMART_RUB,ETC_USD,ZEC_RUB,BTC_UAH,XTZ_ETH,TRX_UAH,TRX_BTC,GAS_USD,DASH_USD,BTC_USDT,XMR_ETH,SMART_EUR,XEM_EUR,GUSD_BTC,XTZ_USD,HBZ_ETH,DXT_BTC", - "enabledPairs": "BTC_USD,LTC_USD", "baseCurrencies": "USD,EUR,RUB,PLN,UAH", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "_", + "separator": "," + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_USD,LTC_USD", + "available": "XEM_UAH,DASH_USDT,XMR_ETH,ROOBEE_BTC,PTI_EOS,GUSD_USD,XLM_RUB,KICK_USDT,SMART_BTC,ADA_BTC,TRX_BTC,DASH_RUB,USDC_BTC,ETC_BTC,XRP_USD,DAI_ETH,ETH_USD,ADA_USD,XRP_UAH,KICK_RUB,QTUM_BTC,XMR_EUR,WAVES_BTC,ZEC_EUR,BTT_RUB,XMR_RUB,GNT_ETH,BCH_BTC,OMG_ETH,ETH_EUR,BTG_ETH,QTUM_ETH,HB_BTC,LSK_USD,MNX_ETH,WAVES_USD,WAVES_ETH,BTC_TRY,VLX_BTC,ZRX_ETH,BTCZ_BTC,QTUM_USD,XEM_USD,ETH_RUB,DCR_UAH,DAI_USD,XRP_RUB,KICK_BTC,DXT_USD,BCH_USDT,XRP_ETH,ETZ_USDT,PTI_USDT,DAI_RUB,BTG_BTC,OMG_USD,XMR_BTC,LTC_UAH,XTZ_USD,ETZ_ETH,MNC_USD,ROOBEE_ETH,XTZ_ETH,BCH_ETH,XMR_USD,TRX_RUB,LTC_RUB,BCH_EUR,DCR_BTC,ETZ_BTC,LSK_BTC,NEO_RUB,INK_USD,BTG_USD,ETH_PLN,DASH_UAH,XRP_EUR,USDC_USDT,MNC_BTC,DXT_BTC,ETH_BTC,GAS_USD,ZEC_RUB,ETH_USDT,USDT_USD,DOGE_USD,USDC_ETH,MKR_BTC,ETC_RUB,LTC_USD,LTC_EUR,BTC_USDT,BCH_UAH,BTC_USD,ATMCASH_BTC,GNT_BTC,ETH_LTC,ZEC_BTC,EOS_EUR,MNX_USD,XLM_USD,EOS_USD,LTC_BTC,BTC_UAH,TRX_UAH,XEM_BTC,NEO_USD,XRP_BTC,WAVES_RUB,MKR_DAI,TRX_USD,BCH_RUB,BTC_RUB,ZEC_USD,MNC_ETH,ZRX_BTC,MNX_BTC,USD_RUB,BTC_PLN,XTZ_RUB,USDC_USD,XLM_TRY,XLM_BTC,DASH_USD,ETH_UAH,USDT_RUB,BTC_EUR,XTZ_BTC,XEM_EUR,NEO_BTC,ZRX_USD,SMART_EUR,GAS_BTC,BCH_USD,DCR_RUB,PTI_RUB,PTI_BTC,ETH_TRY,ZAG_BTC,BTT_BTC,GUSD_RUB,ETC_USD,SMART_USD,OMG_BTC,EOS_BTC,DOGE_BTC,USDT_EUR,BTT_UAH,SMART_RUB,ADA_ETH,KICK_ETH,LSK_RUB,INK_BTC,DASH_BTC,EXM_BTC,XRP_TRY,DAI_BTC,GUSD_BTC,XMR_UAH,INK_ETH,XRP_USDT,USDT_UAH" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_", - "separator": "," + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -722,40 +1161,73 @@ "name": "CoinbasePro", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "clientId": "ClientID", - "availablePairs": "REPUSD,LTCGBP,ZECBTC,ETCBTC,BCHUSD,DAIUSDC,MAN-USDC,BCHBTC,ETHUSD,XLMUSD,EOSUSD,XRPEUR,ZRXEUR,LTCEUR,ALG-USD,ETHUSDC,BTCGBP,LTCUSD,EOSEUR,ZRXUSD,DNTUSDC,LOO-USDC,GNTUSDC,XRPUSD,BCHEUR,ETHGBP,ZRXBTC,BATUSDC,REPBTC,ETCUSD,ETHBTC,ZECUSDC,XLMBTC,BTCUSDC,EOSBTC,XLMEUR,XTZBTC,LIN-USD,BTCUSD,BCHGBP,XRPBTC,BTCEUR,ETHDAI,LIN-ETH,BATETH,ETCGBP,LTCBTC,ETCEUR,ETHEUR,CVCUSDC,XTZUSD", - "enabledPairs": "BTCUSD,BTCGBP,BTCEUR", "baseCurrencies": "USD,GBP,EUR", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USD", + "available": "LTC-GBP,XLM-BTC,DASH-BTC,DAI-USDC,ZEC-USDC,XLM-EUR,ZRX-BTC,LTC-BTC,ETC-BTC,ETH-USD,XRP-EUR,BTC-USDC,REP-USD,EOS-BTC,ZEC-BTC,ETC-GBP,LINK-ETH,XRP-BTC,ZRX-USD,ETH-USDC,MANA-USDC,BTC-EUR,BCH-GBP,DNT-USDC,EOS-EUR,BCH-EUR,LTC-EUR,CVC-USDC,ETH-GBP,DASH-USD,ETH-EUR,XTZ-BTC,ZRX-EUR,BAT-ETH,BTC-GBP,ETC-USD,BAT-USDC,BCH-USD,GNT-USDC,ALGO-USD,LINK-USD,XLM-USD,ETH-BTC,EOS-USD,REP-BTC,ETH-DAI,XRP-USD,LTC-USD,ETC-EUR,BTC-USD,XTZ-USD,BCH-BTC,LOOM-USDC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true, + "requiresBase64DecodeSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -768,40 +1240,72 @@ "name": "GateIO", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "USDT_CNYX,BTC_CNYX,ETH_CNYX,EOS_CNYX,BCH_CNYX,XRP_CNYX,DOGE_CNYX,TIPS_CNYX,BTC_USDC,BTC_PAX,BTC_USDT,BCH_USDT,ETH_USDT,ETC_USDT,QTUM_USDT,LTC_USDT,DASH_USDT,ZEC_USDT,BTM_USDT,EOS_USDT,REQ_USDT,SNT_USDT,OMG_USDT,PAY_USDT,CVC_USDT,ZRX_USDT,TNT_USDT,XMR_USDT,XRP_USDT,DOGE_USDT,BAT_USDT,PST_USDT,BTG_USDT,DPY_USDT,LRC_USDT,STORJ_USDT,RDN_USDT,STX_USDT,KNC_USDT,LINK_USDT,CDT_USDT,AE_USDT,AE_ETH,AE_BTC,CDT_ETH,RDN_ETH,STX_ETH,KNC_ETH,LINK_ETH,REQ_ETH,RCN_ETH,TRX_ETH,ARN_ETH,KICK_ETH,BNT_ETH,VET_ETH,MCO_ETH,FUN_ETH,DATA_ETH,RLC_ETH,RLC_USDT,ZSC_ETH,WINGS_ETH,MDA_ETH,RCN_USDT,TRX_USDT,KICK_USDT,VET_USDT,MCO_USDT,FUN_USDT,DATA_USDT,ZSC_USDT,MDA_USDT,XTZ_USDT,XTZ_BTC,XTZ_ETH,GNT_USDT,GNT_ETH,GEM_USDT,GEM_ETH,RFR_USDT,RFR_ETH,DADI_USDT,DADI_ETH,ABT_USDT,ABT_ETH,LEDU_BTC,LEDU_ETH,OST_USDT,OST_ETH,XLM_USDT,XLM_ETH,XLM_BTC,MOBI_USDT,MOBI_ETH,MOBI_BTC,OCN_USDT,OCN_ETH,OCN_BTC,ZPT_USDT,ZPT_ETH,ZPT_BTC,COFI_USDT,COFI_ETH,JNT_USDT,JNT_ETH,JNT_BTC,BLZ_USDT,BLZ_ETH,GXS_USDT,GXS_BTC,MTN_USDT,MTN_ETH,RUFF_USDT,RUFF_ETH,RUFF_BTC,TNC_USDT,TNC_ETH,TNC_BTC,ZIL_USDT,ZIL_ETH,BTO_USDT,BTO_ETH,THETA_USDT,THETA_ETH,DDD_USDT,DDD_ETH,DDD_BTC,MKR_USDT,MKR_ETH,DAI_USDT,SMT_USDT,SMT_ETH,MDT_USDT,MDT_ETH,MDT_BTC,MANA_USDT,MANA_ETH,LUN_USDT,LUN_ETH,SALT_USDT,SALT_ETH,FUEL_USDT,FUEL_ETH,ELF_USDT,ELF_ETH,DRGN_USDT,DRGN_ETH,GTC_USDT,GTC_ETH,GTC_BTC,QLC_USDT,QLC_BTC,QLC_ETH,DBC_USDT,DBC_BTC,DBC_ETH,BNTY_USDT,BNTY_ETH,LEND_USDT,LEND_ETH,ICX_USDT,ICX_ETH,BTF_USDT,BTF_BTC,ADA_USDT,ADA_BTC,LSK_USDT,LSK_BTC,WAVES_USDT,WAVES_BTC,BIFI_USDT,BIFI_BTC,MDS_ETH,MDS_USDT,DGD_USDT,DGD_ETH,QASH_USDT,QASH_ETH,QASH_BTC,POWR_USDT,POWR_ETH,POWR_BTC,FIL_USDT,BCD_USDT,BCD_BTC,SBTC_USDT,SBTC_BTC,GOD_USDT,GOD_BTC,BCX_USDT,BCX_BTC,QSP_USDT,QSP_ETH,INK_BTC,INK_USDT,INK_ETH,INK_QTUM,MED_QTUM,MED_ETH,MED_USDT,QBT_QTUM,QBT_ETH,QBT_USDT,TSL_QTUM,TSL_USDT,GNX_USDT,GNX_ETH,NEO_USDT,GAS_USDT,NEO_BTC,GAS_BTC,IOTA_USDT,IOTA_BTC,NAS_USDT,NAS_ETH,NAS_BTC,ETH_BTC,ETC_BTC,ETC_ETH,ZEC_BTC,DASH_BTC,LTC_BTC,BCH_BTC,BTG_BTC,QTUM_BTC,QTUM_ETH,XRP_BTC,DOGE_BTC,XMR_BTC,ZRX_BTC,ZRX_ETH,DNT_ETH,DPY_ETH,OAX_BTC,OAX_USDT,OAX_ETH,REP_ETH,LRC_ETH,LRC_BTC,PST_ETH,BCDN_ETH,BCDN_USDT,TNT_ETH,SNT_ETH,SNT_BTC,BTM_ETH,BTM_BTC,SNET_ETH,SNET_USDT,LLT_SNET,OMG_ETH,OMG_BTC,PAY_ETH,PAY_BTC,BAT_ETH,BAT_BTC,CVC_ETH,STORJ_ETH,STORJ_BTC,EOS_ETH,EOS_BTC,BTS_USDT,BTS_BTC,TIPS_ETH,GT_BTC,GT_USDT,ATOM_BTC,ATOM_USDT,XEM_ETH,XEM_USDT,XEM_BTC,BU_USDT,BU_ETH,BU_BTC,BCHSV_USDT,BCHSV_CNYX,BCHSV_BTC,DCR_USDT,DCR_BTC,BCN_USDT,BCN_BTC,XMC_USDT,XMC_BTC,ATP_USDT,ATP_ETH,NBOT_ETH,NBOT_USDT,MEDX_USDT,MEDX_ETH,GRIN_USDT,GRIN_ETH,GRIN_BTC,BEAM_USDT,BEAM_ETH,BEAM_BTC,VTHO_ETH,BTT_USDT,BTT_ETH,BTT_TRX,TFUEL_ETH,TFUEL_USDT,CELR_ETH,CELR_USDT,CS_ETH,CS_USDT,MAN_ETH,MAN_USDT,REM_ETH,REM_USDT,LYM_ETH,LYM_BTC,LYM_USDT,ONG_ETH,ONG_USDT,ONT_ETH,ONT_USDT,BFT_ETH,BFT_USDT,IHT_ETH,IHT_USDT,SENC_ETH,SENC_USDT,TOMO_ETH,TOMO_USDT,ELEC_ETH,ELEC_USDT,HAV_ETH,HAV_USDT,SWTH_ETH,SWTH_USDT,NKN_ETH,NKN_USDT,SOUL_ETH,SOUL_USDT,LRN_ETH,LRN_USDT,EOSDAC_ETH,EOSDAC_USDT,DOCK_USDT,DOCK_ETH,GSE_USDT,GSE_ETH,RATING_USDT,RATING_ETH,HSC_USDT,HSC_ETH,HIT_USDT,HIT_ETH,DX_USDT,DX_ETH,CNNS_ETH,CNNS_USDT,DREP_ETH,DREP_USDT,MBL_USDT,MBL_ETH,GMAT_USDT,GMAT_ETH,MIX_USDT,MIX_ETH,LAMB_USDT,LAMB_ETH,LEO_USDT,LEO_BTC,WICC_USDT,WICC_ETH,SERO_USDT,SERO_ETH,VIDY_USDT,VIDY_ETH,KGC_USDT,FTM_USDT,FTM_ETH,ONE_USDT,ARPA_USDT,ARPA_ETH,ALGO_USDT,BKC_USDT,BXC_USDT,BXC_ETH,PAX_USDT,PAX_CNYX,USDC_CNYX,USDC_USDT,TUSD_CNYX,TUSD_USDT,HC_USDT,HC_BTC,HC_ETH,GARD_USDT,GARD_ETH,FTI_USDT,FTI_ETH,SOP_ETH,SOP_USDT,LEMO_USDT,LEMO_ETH,QKC_USDT,QKC_ETH,IOTX_USDT,IOTX_ETH,RED_USDT,RED_ETH,LBA_USDT,LBA_ETH,OPEN_USDT,OPEN_ETH,MITH_USDT,MITH_ETH,SKM_USDT,SKM_ETH,XVG_USDT,XVG_BTC,NANO_USDT,NANO_BTC,HT_USDT,BNB_USDT,MET_ETH,MET_USDT,TCT_ETH,TCT_USDT,MXC_USDT,MXC_BTC,MXC_ETH", - "enabledPairs": "BTC_USDT", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_USDT", + "available": "USDT_CNYX,BTC_CNYX,ETH_CNYX,EOS_CNYX,BCH_CNYX,XRP_CNYX,DOGE_CNYX,TIPS_CNYX,BTC_USDC,BTC_PAX,BTC_USDT,BCH_USDT,ETH_USDT,ETC_USDT,QTUM_USDT,LTC_USDT,DASH_USDT,ZEC_USDT,BTM_USDT,EOS_USDT,REQ_USDT,SNT_USDT,OMG_USDT,PAY_USDT,CVC_USDT,ZRX_USDT,TNT_USDT,XMR_USDT,XRP_USDT,DOGE_USDT,BAT_USDT,PST_USDT,BTG_USDT,DPY_USDT,LRC_USDT,STORJ_USDT,RDN_USDT,STX_USDT,KNC_USDT,LINK_USDT,CDT_USDT,AE_USDT,AE_ETH,AE_BTC,CDT_ETH,RDN_ETH,STX_ETH,KNC_ETH,LINK_ETH,REQ_ETH,RCN_ETH,TRX_ETH,ARN_ETH,KICK_ETH,BNT_ETH,VET_ETH,MCO_ETH,FUN_ETH,DATA_ETH,RLC_ETH,RLC_USDT,ZSC_ETH,WINGS_ETH,MDA_ETH,RCN_USDT,TRX_USDT,KICK_USDT,VET_USDT,MCO_USDT,FUN_USDT,DATA_USDT,ZSC_USDT,MDA_USDT,XTZ_USDT,XTZ_BTC,XTZ_ETH,GNT_USDT,GNT_ETH,GEM_USDT,GEM_ETH,RFR_USDT,RFR_ETH,DADI_USDT,DADI_ETH,ABT_USDT,ABT_ETH,LEDU_BTC,LEDU_ETH,OST_USDT,OST_ETH,XLM_USDT,XLM_ETH,XLM_BTC,MOBI_USDT,MOBI_ETH,MOBI_BTC,OCN_USDT,OCN_ETH,OCN_BTC,ZPT_USDT,ZPT_ETH,ZPT_BTC,COFI_USDT,COFI_ETH,JNT_USDT,JNT_ETH,JNT_BTC,BLZ_USDT,BLZ_ETH,GXS_USDT,GXS_BTC,MTN_USDT,MTN_ETH,RUFF_USDT,RUFF_ETH,RUFF_BTC,TNC_USDT,TNC_ETH,TNC_BTC,ZIL_USDT,ZIL_ETH,BTO_USDT,BTO_ETH,THETA_USDT,THETA_ETH,DDD_USDT,DDD_ETH,DDD_BTC,MKR_USDT,MKR_ETH,DAI_USDT,SMT_USDT,SMT_ETH,MDT_USDT,MDT_ETH,MDT_BTC,MANA_USDT,MANA_ETH,LUN_USDT,LUN_ETH,SALT_USDT,SALT_ETH,FUEL_USDT,FUEL_ETH,ELF_USDT,ELF_ETH,DRGN_USDT,DRGN_ETH,GTC_USDT,GTC_ETH,GTC_BTC,QLC_USDT,QLC_BTC,QLC_ETH,DBC_USDT,DBC_BTC,DBC_ETH,BNTY_USDT,BNTY_ETH,LEND_USDT,LEND_ETH,ICX_USDT,ICX_ETH,BTF_USDT,BTF_BTC,ADA_USDT,ADA_BTC,LSK_USDT,LSK_BTC,WAVES_USDT,WAVES_BTC,BIFI_USDT,BIFI_BTC,MDS_ETH,MDS_USDT,DGD_USDT,DGD_ETH,QASH_USDT,QASH_ETH,QASH_BTC,POWR_USDT,POWR_ETH,POWR_BTC,FIL_USDT,BCD_USDT,BCD_BTC,SBTC_USDT,SBTC_BTC,GOD_USDT,GOD_BTC,BCX_USDT,BCX_BTC,QSP_USDT,QSP_ETH,INK_BTC,INK_USDT,INK_ETH,INK_QTUM,QBT_QTUM,QBT_ETH,QBT_USDT,TSL_QTUM,TSL_USDT,GNX_USDT,GNX_ETH,NEO_USDT,GAS_USDT,NEO_BTC,GAS_BTC,IOTA_USDT,IOTA_BTC,NAS_USDT,NAS_ETH,NAS_BTC,ETH_BTC,ETC_BTC,ETC_ETH,ZEC_BTC,DASH_BTC,LTC_BTC,BCH_BTC,BTG_BTC,QTUM_BTC,QTUM_ETH,XRP_BTC,DOGE_BTC,XMR_BTC,ZRX_BTC,ZRX_ETH,DNT_ETH,DPY_ETH,OAX_BTC,OAX_USDT,OAX_ETH,REP_ETH,LRC_ETH,LRC_BTC,PST_ETH,BCDN_ETH,BCDN_USDT,TNT_ETH,SNT_ETH,SNT_BTC,BTM_ETH,BTM_BTC,SNET_ETH,SNET_USDT,LLT_SNET,OMG_ETH,OMG_BTC,PAY_ETH,PAY_BTC,BAT_ETH,BAT_BTC,CVC_ETH,STORJ_ETH,STORJ_BTC,EOS_ETH,EOS_BTC,BTS_USDT,BTS_BTC,TIPS_ETH,GT_BTC,GT_USDT,ATOM_BTC,ATOM_USDT,XEM_ETH,XEM_USDT,XEM_BTC,BU_USDT,BU_ETH,BU_BTC,BCHSV_USDT,BCHSV_CNYX,BCHSV_BTC,DCR_USDT,DCR_BTC,BCN_USDT,BCN_BTC,XMC_USDT,XMC_BTC,ATP_USDT,ATP_ETH,NAX_ETH,NBOT_ETH,NBOT_USDT,MED_USDT,MED_ETH,GRIN_USDT,GRIN_ETH,GRIN_BTC,BEAM_USDT,BEAM_ETH,BEAM_BTC,VTHO_ETH,BTT_USDT,BTT_ETH,BTT_TRX,TFUEL_ETH,TFUEL_USDT,CELR_ETH,CELR_USDT,CS_ETH,CS_USDT,MAN_ETH,MAN_USDT,REM_ETH,REM_USDT,LYM_ETH,LYM_BTC,LYM_USDT,ONG_ETH,ONG_USDT,ONT_ETH,ONT_USDT,BFT_ETH,BFT_USDT,IHT_ETH,IHT_USDT,SENC_ETH,SENC_USDT,TOMO_ETH,TOMO_USDT,ELEC_ETH,ELEC_USDT,HAV_ETH,HAV_USDT,SWTH_ETH,SWTH_USDT,NKN_ETH,NKN_USDT,SOUL_ETH,SOUL_USDT,LRN_ETH,LRN_USDT,EOSDAC_ETH,EOSDAC_USDT,DOCK_USDT,DOCK_ETH,GSE_USDT,GSE_ETH,RATING_USDT,RATING_ETH,HSC_USDT,HSC_ETH,HIT_USDT,HIT_ETH,DX_USDT,DX_ETH,CNNS_ETH,CNNS_USDT,DREP_ETH,DREP_USDT,MBL_USDT,MBL_ETH,GMAT_USDT,GMAT_ETH,MIX_USDT,MIX_ETH,LAMB_USDT,LAMB_ETH,LEO_USDT,LEO_BTC,WICC_USDT,WICC_ETH,SERO_USDT,SERO_ETH,VIDY_USDT,VIDY_ETH,KGC_USDT,FTM_USDT,FTM_ETH,COS_USDT,CRO_USDT,ALY_USDT,WIN_USDT,MTV_USDT,ONE_USDT,ARPA_USDT,ARPA_ETH,DILI_USDT,ALGO_USDT,PI_USDT,CKB_USDT,CKB_BTC,CKB_ETH,BKC_USDT,BXC_USDT,BXC_ETH,PAX_USDT,PAX_CNYX,USDC_CNYX,USDC_USDT,TUSD_CNYX,TUSD_USDT,HC_USDT,HC_BTC,HC_ETH,GARD_USDT,GARD_ETH,FTI_USDT,FTI_ETH,SOP_ETH,SOP_USDT,LEMO_USDT,LEMO_ETH,QKC_USDT,QKC_ETH,QKC_BTC,IOTX_USDT,IOTX_ETH,RED_USDT,RED_ETH,LBA_USDT,LBA_ETH,OPEN_USDT,OPEN_ETH,MITH_USDT,MITH_ETH,SKM_USDT,SKM_ETH,XVG_USDT,XVG_BTC,NANO_USDT,NANO_BTC,HT_USDT,BNB_USDT,MET_ETH,MET_USDT,TCT_ETH,TCT_USDT,MXC_USDT,MXC_BTC,MXC_ETH" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -814,38 +1318,69 @@ "name": "Gemini", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTCUSD,ETHBTC,ETHUSD,BCHUSD,BCHBTC,BCHETH,LTCUSD,LTCBTC,LTCETH,LTCBCH,ZECUSD,ZECBTC,ZECETH,ZECBCH,ZECLTC", - "enabledPairs": "BTCUSD", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCUSD", + "available": "BTCUSD,ETHBTC,ETHUSD,BCHUSD,BCHBTC,BCHETH,LTCUSD,LTCBTC,LTCETH,LTCBCH,ZECUSD,ZECBTC,ZECETH,ZECBCH,ZECLTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -858,39 +1393,71 @@ "name": "HitBTC", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BCN-BTC,BTC-USD,DASH-BTC,DOGE-BTC,DOGE-USD,EMC-BTC,ETH-BTC,LSK-BTC,LTC-BTC,LTC-USD,NXT-BTC,SBD-BTC,SC-BTC,STEEM-BTC,XDN-BTC,XEM-BTC,XMR-BTC,ARDR-BTC,ZEC-BTC,WAVES-BTC,MAID-BTC,AMP-BTC,DGD-BTC,SNGLS-BTC,1ST-BTC,TRST-BTC,TIME-BTC,GNO-BTC,REP-BTC,XMR-USD,DASH-USD,ETH-USD,NXT-USD,ZRC-BTC,BOS-BTC,DCT-BTC,ANT-BTC,AEON-BTC,GUP-BTC,PLU-BTC,LUN-BTC,EDG-BTC,RLC-BTC,SWT-BTC,TKN-BTC,WINGS-BTC,XAUR-BTC,AE-BTC,PTOY-BTC,ZEC-USD,XEM-USD,BCN-USD,XDN-USD,MAID-USD,ETC-BTC,ETC-USD,PLBT-BTC,BNT-BTC,SNT-ETH,CVC-USD,PAY-ETH,OAX-ETH,OMG-ETH,BQX-ETH,XTZ-BTC,DICE-BTC,PTOY-ETH,1ST-ETH,XAUR-ETH,TIME-ETH,DICE-ETH,SWT-ETH,XMR-ETH,ETC-ETH,DASH-ETH,ZEC-ETH,PLU-ETH,GNO-ETH,XRP-BTC,STRAT-USD,STRAT-BTC,SNC-ETH,ADX-ETH,BET-ETH,EOS-ETH,DENT-ETH,SAN-ETH,EOS-BTC,EOS-USD,XTZ-ETH,XTZ-USD,MYB-ETH,SUR-ETH,IXT-ETH,PLR-ETH,TIX-ETH,PRO-ETH,AVT-ETH,EVX-USD,DLT-BTC,BNT-ETH,BNT-USD,MANA-USD,DNT-BTC,FYP-BTC,OPT-BTC,TNT-ETH,STX-BTC,STX-ETH,STX-USD,TNT-USD,TNT-BTC,ENG-ETH,XUC-USD,SNC-BTC,SNC-USD,OAX-USD,OAX-BTC,ZRX-BTC,ZRX-ETH,ZRX-USD,RVT-BTC,PPC-BTC,PPC-USD,QTUM-ETH,IGNIS-ETH,BMC-BTC,BMC-ETH,BMC-USD,CND-BTC,CND-ETH,CND-USD,CDT-ETH,CDT-USD,FUN-BTC,FUN-ETH,FUN-USD,HVN-BTC,HVN-ETH,POE-BTC,POE-ETH,AMB-USD,AMB-ETH,AMB-BTC,HPC-BTC,PPT-ETH,MTH-BTC,MTH-ETH,LRC-BTC,LRC-ETH,ICX-BTC,ICX-ETH,NEO-BTC,NEO-ETH,NEO-USD,CSNO-BTC,ICX-USD,PIX-BTC,PIX-ETH,IND-ETH,KICK-BTC,YOYOW-BTC,CDT-BTC,XVG-BTC,XVG-ETH,XVG-USD,DGB-BTC,DGB-ETH,DGB-USD,DCN-BTC,DCN-ETH,DCN-USD,VIBE-BTC,ENJ-BTC,ENJ-ETH,ENJ-USD,ZSC-BTC,ZSC-ETH,ZSC-USD,TRX-BTC,TRX-ETH,TRX-USD,ART-BTC,EVX-BTC,EVX-ETH,SUB-BTC,SUB-ETH,SUB-USD,WTC-BTC,BTM-BTC,BTM-ETH,BTM-USD,LIFE-BTC,VIB-BTC,VIB-ETH,VIB-USD,DRT-ETH,STU-USD,OMG-BTC,PAY-BTC,PPT-BTC,SNT-BTC,BTG-BTC,BTG-ETH,BTG-USD,SMART-BTC,SMART-ETH,SMART-USD,XUC-ETH,XUC-BTC,LA-ETH,EDO-BTC,EDO-ETH,EDO-USD,HGT-ETH,IXT-BTC,SCL-BTC,ETP-BTC,ETP-ETH,ETP-USD,DRPU-BTC,NEBL-BTC,NEBL-ETH,ARN-BTC,ARN-ETH,STU-BTC,STU-ETH,GVT-ETH,BTX-BTC,LTC-ETH,BCN-ETH,MAID-ETH,NXT-ETH,STRAT-ETH,XDN-ETH,XEM-ETH,PLR-BTC,SUR-BTC,BQX-BTC,DOGE-ETH,AMM-BTC,AMM-ETH,AMM-USD,DBIX-BTC,PRE-BTC,ZAP-BTC,DOV-BTC,DOV-ETH,DRPU-ETH,XRP-ETH,XRP-USD,HSR-BTC,LEND-BTC,LEND-ETH,SPF-ETH,SBTC-BTC,SBTC-ETH,LOC-BTC,LOC-ETH,LOC-USD,SWFTC-BTC,SWFTC-ETH,SWFTC-USD,STAR-ETH,SBTC-USD,STORM-BTC,DIM-ETH,DIM-USD,DIM-BTC,NGC-BTC,NGC-ETH,NGC-USD,EMC-ETH,EMC-USD,MCO-BTC,MCO-ETH,MCO-USD,MANA-ETH,MANA-BTC,CPAY-ETH,DATA-BTC,DATA-ETH,DATA-USD,UTT-BTC,UTT-ETH,UTT-USD,KMD-BTC,KMD-ETH,KMD-USD,QTUM-USD,QTUM-BTC,SNT-USD,OMG-USD,EKO-BTC,EKO-ETH,ADX-BTC,ADX-USD,LSK-ETH,LSK-USD,PLR-USD,SUR-USD,BQX-USD,DRT-USD,REP-ETH,REP-USD,WAX-BTC,WAX-ETH,WAX-USD,C20-BTC,C20-ETH,IDH-BTC,IDH-ETH,IPL-BTC,COV-BTC,COV-ETH,SENT-BTC,SENT-ETH,SENT-USD,SMT-BTC,SMT-ETH,SMT-USD,CHAT-BTC,CHAT-ETH,CHAT-USD,TRAC-ETH,JNT-ETH,UTK-BTC,UTK-ETH,UTK-USD,GNX-ETH,CHSB-BTC,CHSB-ETH,DAY-BTC,DAY-ETH,DAY-USD,NEU-BTC,NEU-ETH,NEU-USD,TAU-BTC,FLP-BTC,FLP-ETH,FLP-USD,R-BTC,R-ETH,EKO-USD,BCPT-ETH,BCPT-USD,PKT-BTC,PKT-ETH,BETR-BTC,BETR-ETH,HAND-ETH,HAND-USD,CHP-ETH,BCPT-BTC,ACT-BTC,ACT-ETH,ACT-USD,ADA-BTC,ADA-ETH,ADA-USD,MTX-BTC,MTX-ETH,MTX-USD,WIZ-BTC,WIZ-ETH,WIZ-USD,DADI-BTC,DADI-ETH,BDG-ETH,DATX-BTC,DATX-ETH,TRUE-BTC,DRG-BTC,DRG-ETH,BANCA-BTC,BANCA-ETH,ZAP-ETH,ZAP-USD,AUTO-BTC,NOAH-BTC,SOC-BTC,OCN-BTC,OCN-ETH,STQ-BTC,STQ-ETH,XLM-BTC,XLM-ETH,XLM-USD,IOTA-BTC,IOTA-ETH,IOTA-USD,DRT-BTC,BETR-USD,ERT-BTC,CRPT-BTC,CRPT-USD,MESH-BTC,MESH-ETH,MESH-USD,IHT-BTC,IHT-ETH,IHT-USD,SCC-BTC,YCC-BTC,DAN-BTC,TEL-BTC,TEL-ETH,NCT-BTC,NCT-ETH,NCT-USD,BMH-BTC,BANCA-USD,NOAH-ETH,NOAH-USD,BERRY-BTC,BERRY-ETH,BERRY-USD,GBX-BTC,GBX-ETH,GBX-USD,SHIP-BTC,SHIP-ETH,NANO-BTC,NANO-ETH,NANO-USD,LNC-BTC,KIN-ETH,ARDR-USD,FOTA-ETH,FOTA-BTC,CVT-BTC,CVT-ETH,CVT-USD,STQ-USD,GNT-BTC,GNT-ETH,GNT-USD,GET-BTC,MITH-BTC,MITH-ETH,MITH-USD,SUNC-ETH,DADI-USD,TKY-BTC,ACAT-BTC,ACAT-ETH,ACAT-USD,BTX-USD,WIKI-BTC,WIKI-ETH,WIKI-USD,ONT-BTC,ONT-ETH,ONT-USD,FTX-BTC,FTX-ETH,FREC-BTC,NAVI-BTC,FREC-ETH,FREC-USD,VME-ETH,NAVI-ETH,LND-ETH,CSM-BTC,NANJ-BTC,NTK-BTC,NTK-ETH,NTK-USD,AUC-BTC,AUC-ETH,CMCT-BTC,CMCT-ETH,CMCT-USD,MAN-BTC,MAN-ETH,MAN-USD,PNT-BTC,PNT-ETH,FXT-BTC,NEXO-BTC,PAT-BTC,PAT-ETH,XMC-BTC,FXT-ETH,HERO-BTC,HERO-ETH,XMC-ETH,XMC-USD,FDZ-BTC,FDZ-ETH,FDZ-USD,SPD-BTC,SPD-ETH,MITX-BTC,TIV-BTC,B2G-BTC,B2G-USD,ZPT-BTC,ZPT-ETH,HBZ-BTC,FACE-BTC,FACE-ETH,HBZ-ETH,HBZ-USD,ZPT-USD,CPT-BTC,PAT-USD,HTML-BTC,HTML-ETH,MITX-ETH,JOT-BTC,JBC-BTC,JBC-ETH,BTS-BTC,BNK-BTC,KBC-BTC,KBC-ETH,BNK-ETH,BNK-USD,TIV-ETH,TIV-USD,CSM-ETH,CSM-USD,INK-BTC,IOST-BTC,INK-ETH,INK-USD,CBC-BTC,IOST-USD,ZIL-BTC,ABYSS-BTC,ABYSS-ETH,ZIL-USD,BCI-BTC,CBC-ETH,CBC-USD,PITCH-BTC,PITCH-ETH,HTML-USD,TDS-BTC,TDS-ETH,TDS-USD,SBD-ETH,SBD-USD,DPN-BTC,UUU-BTC,UUU-ETH,XBP-BTC,CLN-BTC,CLN-ETH,ELEC-BTC,ELEC-ETH,ELEC-USD,QNTU-BTC,QNTU-ETH,QNTU-USD,IPL-ETH,IPL-USD,CENNZ-BTC,CENNZ-ETH,SWM-BTC,SPF-USD,SPF-BTC,LCC-BTC,HGT-BTC,ETH-TUSD,BTC-TUSD,LTC-TUSD,XMR-TUSD,ZRX-TUSD,NEO-TUSD,USD-TUSD,BTC-DAI,ETH-DAI,MKR-DAI,EOS-DAI,USD-DAI,MKR-BTC,MKR-ETH,MKR-USD,TUSD-DAI,NEO-DAI,LTC-DAI,XMR-DAI,XRP-DAI,NEXO-ETH,NEXO-USD,DWS-BTC,DWS-ETH,DWS-USD,APPC-BTC,APPC-ETH,APPC-USD,BIT-ETH,SPC-BTC,SPC-ETH,SPC-USD,REX-BTC,REX-ETH,REX-USD,ELF-BTC,ELF-USD,BCD-BTC,BCD-USD,CVCOIN-BTC,CVCOIN-ETH,CVCOIN-USD,EDG-ETH,EDG-USD,NLC2-BTC,COSM-BTC,COSM-ETH,DASH-EURS,ZEC-EURS,BTC-EURS,EOS-EURS,ETH-EURS,LTC-EURS,NEO-EURS,XMR-EURS,XRP-EURS,EURS-USD,EURS-TUSD,EURS-DAI,MNX-USD,ROX-ETH,ZPR-ETH,MNX-BTC,MNX-ETH,KIND-BTC,KIND-ETH,ENGT-BTC,ENGT-ETH,PMA-BTC,PMA-ETH,TV-BTC,TV-ETH,TV-USD,XCLR-BTC,BAT-BTC,BAT-ETH,BAT-USD,SRN-BTC,SRN-ETH,SRN-USD,SVD-BTC,SVD-ETH,SVD-USD,GST-BTC,GST-ETH,GST-USD,BNB-BTC,BNB-ETH,BNB-USD,DIT-BTC,DIT-ETH,POA20-BTC,CCL-USD,PROC-BTC,POA20-ETH,POA20-USD,POA20-DAI,NIM-BTC,USE-BTC,USE-ETH,DAV-BTC,DAV-ETH,ABTC-BTC,NIM-ETH,ABA-BTC,ABA-ETH,ABA-USD,BCN-EOS,LTC-EOS,XMR-EOS,DASH-EOS,TRX-EOS,NEO-EOS,ZEC-EOS,LSK-EOS,XEM-EOS,XRP-EOS,MESSE-BTC,MESSE-ETH,MESSE-USD,CCL-ETH,RCN-BTC,RCN-ETH,RCN-USD,HMQ-BTC,HMQ-ETH,MYST-BTC,MYST-ETH,USD-GUSD,BTC-GUSD,ETH-GUSD,EOS-GUSD,AXPR-BTC,AXPR-ETH,DAG-BTC,DAG-ETH,BITS-BTC,BITS-ETH,BITS-USD,CDCC-BTC,CDCC-ETH,CDCC-USD,VET-BTC,VET-ETH,VET-USD,SILK-ETH,BOX-BTC,BOX-ETH,BOX-EURS,BOX-EOS,VOCO-BTC,VOCO-ETH,VOCO-USD,PASS-BTC,PASS-ETH,SLX-BTC,SLX-USD,PBTT-BTC,PMA-USD,TRAD-BTC,DGTX-BTC,DGTX-ETH,DGTX-USD,MRK-BTC,MRK-ETH,DGB-TUSD,MESSE-EOS,MESSE-EURS,SNBL-BTC,BCH-BTC,BCH-USD,BSV-BTC,BSV-USD,BKX-BTC,NPLC-BTC,NPLC-ETH,ETN-BTC,ETN-ETH,ETN-USD,MRS-BTC,MRS-ETH,MRS-USD,DTR-BTC,DTR-ETH,TDP-BTC,HBT-ETH,PXG-BTC,PXG-USD,BTC-PAX,ETH-PAX,USD-PAX,BTC-USDC,ETH-USDC,USD-USDC,TUSD-USDC,DAI-USDC,EOS-PAX,CLO-BTC,CLO-ETH,CLO-USD,PETH-BTC,PETH-ETH,PETH-USD,BRD-BTC,BRD-ETH,NMR-BTC,SALT-BTC,SALT-ETH,POLY-BTC,POLY-ETH,POWR-BTC,POWR-ETH,STORJ-BTC,STORJ-ETH,STORJ-USD,MLN-BTC,MLN-ETH,BDG-BTC,POA-ETH,POA-BTC,POA-USD,POA-DAI,KIN-BTC,VEO-BTC,PLA-BTC,PLA-ETH,PLA-USD,BTT-BTC,BTT-USD,BTT-ETH,ZEN-BTC,ZEN-ETH,ZEN-USD,GRIN-BTC,GRIN-ETH,GRIN-USD,FET-BTC,HT-BTC,HT-USD,XZC-BTC,XZC-ETH,XZC-USD,VRA-BTC,VRA-ETH,BTC-KRWB,USD-KRWB,WBTC-ETH,CRO-BTC,CRO-ETH,CRO-USD,GAS-BTC,GAS-ETH,GAS-USD,ORMEUS-BTC,ORMEUS-ETH,SWM-ETH,SWM-USD,PRE-ETH,PHX-BTC,PHX-ETH,PHX-USD,BET-BTC,USD-EOSDT,BTC-EOSDT,ETH-EOSDT,EOS-EOSDT,DAI-EOSDT,NUT-BTC,NUT-EOS,NUT-USD,CUTE-BTC,CUTE-ETH,CUTE-USD,CUTE-EOS,XCON-BTC,DCR-BTC,DCR-ETH,DCR-USD,MG-BTC,MG-ETH,MG-EOS,MG-USD,GNX-BTC,PRO-BTC,EURS-EOSDT,TUSD-EOSDT,ECOIN-BTC,ECOIN-ETH,ECOIN-USD,AGI-BTC,LOOM-BTC,LOOM-ETH,BLZ-BTC,QKC-BTC,QKC-ETH,KNC-BTC,KNC-ETH,KNC-USD,KEY-BTC,KEY-ETH,ATOM-BTC,ATOM-USD,ATOM-ETH,BRDG-BTC,BRDG-ETH,BRDG-USD,MTL-BTC,MTL-ETH,EXP-BTC,BTCB-BTC,PBT-BTC,PBT-ETH,LINK-BTC,LINK-ETH,LINK-USD,USD-USDT20,PHB-BTC,BCH-ETH,BCH-DAI,BCH-TUSD,BCH-EURS,DAPP-BTC,DAPP-EOS,BTC-USDT20,DENT-BTC,DENT-USD", - "enabledPairs": "BTC-USD", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USD", + "available": "BCN-BTC,BTC-USD,DASH-BTC,DOGE-BTC,DOGE-USD,EMC-BTC,ETH-BTC,LSK-BTC,LTC-BTC,LTC-USD,NXT-BTC,SBD-BTC,SC-BTC,STEEM-BTC,XDN-BTC,XEM-BTC,XMR-BTC,ARDR-BTC,ZEC-BTC,WAVES-BTC,MAID-BTC,DGD-BTC,SNGLS-BTC,1ST-BTC,TRST-BTC,TIME-BTC,GNO-BTC,REP-BTC,XMR-USD,DASH-USD,ETH-USD,NXT-USD,ZRC-BTC,BOS-BTC,DCT-BTC,ANT-BTC,AEON-BTC,GUP-BTC,PLU-BTC,LUN-BTC,EDG-BTC,RLC-BTC,SWT-BTC,TKN-BTC,WINGS-BTC,XAUR-BTC,AE-BTC,PTOY-BTC,ZEC-USD,XEM-USD,BCN-USD,XDN-USD,MAID-USD,ETC-BTC,ETC-USD,PLBT-BTC,BNT-BTC,SNT-ETH,CVC-USD,PAY-ETH,OAX-ETH,OMG-ETH,BQX-ETH,XTZ-BTC,DICE-BTC,PTOY-ETH,1ST-ETH,XAUR-ETH,TIME-ETH,DICE-ETH,SWT-ETH,XMR-ETH,ETC-ETH,DASH-ETH,ZEC-ETH,PLU-ETH,GNO-ETH,XRP-BTC,STRAT-USD,STRAT-BTC,SNC-ETH,ADX-ETH,BET-ETH,EOS-ETH,DENT-ETH,SAN-ETH,EOS-BTC,EOS-USD,XTZ-ETH,XTZ-USD,MYB-ETH,SUR-ETH,IXT-ETH,PLR-ETH,TIX-ETH,PRO-ETH,AVT-ETH,EVX-USD,DLT-BTC,BNT-ETH,BNT-USD,MANA-USD,DNT-BTC,FYP-BTC,OPT-BTC,TNT-ETH,STX-BTC,STX-ETH,STX-USD,TNT-USD,TNT-BTC,ENG-ETH,XUC-USD,SNC-BTC,SNC-USD,OAX-USD,OAX-BTC,ZRX-BTC,ZRX-ETH,ZRX-USD,RVT-BTC,PPC-BTC,PPC-USD,QTUM-ETH,IGNIS-ETH,BMC-BTC,BMC-ETH,BMC-USD,CND-BTC,CND-ETH,CND-USD,CDT-ETH,CDT-USD,FUN-BTC,FUN-ETH,FUN-USD,HVN-BTC,HVN-ETH,POE-BTC,POE-ETH,AMB-USD,AMB-ETH,AMB-BTC,HPC-BTC,PPT-ETH,MTH-BTC,MTH-ETH,LRC-BTC,LRC-ETH,ICX-BTC,ICX-ETH,NEO-BTC,NEO-ETH,NEO-USD,CSNO-BTC,ICX-USD,IND-ETH,KICK-BTC,YOYOW-BTC,CDT-BTC,XVG-BTC,XVG-ETH,XVG-USD,DGB-BTC,DGB-ETH,DGB-USD,DCN-ETH,DCN-USD,VIBE-BTC,ENJ-BTC,ENJ-ETH,ENJ-USD,ZSC-BTC,ZSC-ETH,ZSC-USD,TRX-BTC,TRX-ETH,TRX-USD,ART-BTC,EVX-BTC,EVX-ETH,SUB-BTC,SUB-ETH,SUB-USD,WTC-BTC,BTM-BTC,BTM-ETH,BTM-USD,LIFE-BTC,VIB-BTC,VIB-ETH,VIB-USD,DRT-ETH,STU-USD,OMG-BTC,PAY-BTC,PPT-BTC,SNT-BTC,BTG-BTC,BTG-ETH,BTG-USD,SMART-BTC,SMART-ETH,SMART-USD,XUC-ETH,XUC-BTC,LA-ETH,EDO-BTC,EDO-ETH,EDO-USD,HGT-ETH,IXT-BTC,SCL-BTC,ETP-BTC,ETP-ETH,ETP-USD,NEBL-BTC,NEBL-ETH,ARN-BTC,ARN-ETH,STU-BTC,STU-ETH,GVT-ETH,BTX-BTC,LTC-ETH,BCN-ETH,MAID-ETH,NXT-ETH,STRAT-ETH,XDN-ETH,XEM-ETH,PLR-BTC,SUR-BTC,BQX-BTC,DOGE-ETH,AMM-BTC,AMM-ETH,AMM-USD,DBIX-BTC,PRE-BTC,ZAP-BTC,DOV-BTC,DOV-ETH,XRP-ETH,XRP-USD,HSR-BTC,LEND-BTC,LEND-ETH,SPF-ETH,SBTC-BTC,SBTC-ETH,LOC-BTC,LOC-ETH,LOC-USD,SWFTC-BTC,SWFTC-ETH,SWFTC-USD,STAR-ETH,SBTC-USD,STORM-BTC,DIM-ETH,DIM-USD,DIM-BTC,NGC-BTC,NGC-ETH,NGC-USD,EMC-ETH,EMC-USD,MCO-BTC,MCO-ETH,MCO-USD,MANA-ETH,MANA-BTC,CPAY-ETH,DATA-BTC,DATA-ETH,DATA-USD,UTT-BTC,UTT-ETH,UTT-USD,KMD-BTC,KMD-ETH,KMD-USD,QTUM-USD,QTUM-BTC,SNT-USD,OMG-USD,EKO-BTC,EKO-ETH,ADX-BTC,ADX-USD,LSK-ETH,LSK-USD,PLR-USD,SUR-USD,BQX-USD,DRT-USD,REP-ETH,REP-USD,WAXP-BTC,WAXP-ETH,WAXP-USD,C20-BTC,C20-ETH,IDH-BTC,IDH-ETH,IPL-BTC,COV-BTC,COV-ETH,SENT-BTC,SENT-ETH,SENT-USD,SMT-BTC,SMT-ETH,SMT-USD,CHAT-BTC,CHAT-ETH,CHAT-USD,TRAC-ETH,JNT-ETH,UTK-BTC,UTK-ETH,UTK-USD,GNX-ETH,CHSB-BTC,CHSB-ETH,DAY-BTC,DAY-ETH,DAY-USD,NEU-BTC,NEU-ETH,NEU-USD,TAU-BTC,FLP-BTC,FLP-ETH,FLP-USD,R-BTC,R-ETH,EKO-USD,BCPT-ETH,BCPT-USD,PKT-BTC,PKT-ETH,BETR-BTC,BETR-ETH,HAND-ETH,HAND-USD,CHP-ETH,BCPT-BTC,ACT-BTC,ACT-ETH,ACT-USD,ADA-BTC,ADA-ETH,ADA-USD,SIG-BTC,MTX-BTC,MTX-ETH,MTX-USD,WIZ-BTC,WIZ-ETH,WIZ-USD,DADI-BTC,DADI-ETH,BDG-ETH,DATX-BTC,DATX-ETH,TRUE-BTC,DRG-BTC,DRG-ETH,BANCA-BTC,BANCA-ETH,ZAP-ETH,ZAP-USD,AUTO-BTC,SOC-BTC,OCN-BTC,OCN-ETH,STQ-BTC,STQ-ETH,XLM-BTC,XLM-ETH,XLM-USD,IOTA-BTC,IOTA-ETH,IOTA-USD,DRT-BTC,BETR-USD,ERT-BTC,CRPT-BTC,CRPT-USD,MESH-BTC,MESH-ETH,MESH-USD,IHT-BTC,IHT-ETH,IHT-USD,SCC-BTC,YCC-BTC,DAN-BTC,TEL-BTC,TEL-ETH,NCT-BTC,NCT-ETH,NCT-USD,BMH-BTC,BANCA-USD,BERRY-BTC,BERRY-ETH,BERRY-USD,GBX-BTC,GBX-ETH,GBX-USD,SHIP-BTC,SHIP-ETH,NANO-BTC,NANO-ETH,NANO-USD,LNC-BTC,KIN-ETH,ARDR-USD,FOTA-ETH,FOTA-BTC,CVT-BTC,CVT-ETH,CVT-USD,STQ-USD,GNT-BTC,GNT-ETH,GNT-USD,GET-BTC,MITH-BTC,MITH-ETH,MITH-USD,DADI-USD,TKY-BTC,ACAT-BTC,ACAT-ETH,ACAT-USD,BTX-USD,WIKI-BTC,WIKI-ETH,WIKI-USD,ONT-BTC,ONT-ETH,ONT-USD,FTX-BTC,FTX-ETH,NAVI-BTC,VME-ETH,NAVI-ETH,LND-ETH,CSM-BTC,NANJ-BTC,NTK-BTC,NTK-ETH,NTK-USD,AUC-BTC,AUC-ETH,CMCT-BTC,CMCT-ETH,CMCT-USD,MAN-BTC,MAN-ETH,MAN-USD,PNT-BTC,PNT-ETH,FXT-BTC,NEXO-BTC,PAT-BTC,PAT-ETH,XMC-BTC,FXT-ETH,HERO-BTC,HERO-ETH,XMC-ETH,XMC-USD,FDZ-BTC,FDZ-ETH,FDZ-USD,SPD-BTC,SPD-ETH,MITX-BTC,TIV-BTC,B2G-BTC,B2G-USD,HBZ-BTC,FACE-BTC,FACE-ETH,HBZ-ETH,HBZ-USD,CPT-BTC,PAT-USD,HTML-BTC,HTML-ETH,MITX-ETH,BTS-BTC,BNK-BTC,BNK-ETH,BNK-USD,TIV-ETH,TIV-USD,CSM-ETH,CSM-USD,INK-BTC,IOST-BTC,INK-ETH,INK-USD,CBC-BTC,IOST-USD,ZIL-BTC,ABYSS-BTC,ABYSS-ETH,ZIL-USD,BCI-BTC,CBC-ETH,CBC-USD,PITCH-BTC,PITCH-ETH,HTML-USD,TDS-BTC,TDS-ETH,TDS-USD,SBD-ETH,SBD-USD,DPN-BTC,UUU-BTC,UUU-ETH,XBP-BTC,ELEC-BTC,ELEC-ETH,ELEC-USD,QNTU-BTC,QNTU-ETH,QNTU-USD,IPL-ETH,IPL-USD,CENNZ-BTC,CENNZ-ETH,SWM-BTC,SPF-USD,SPF-BTC,LCC-BTC,HGT-BTC,ETH-TUSD,BTC-TUSD,LTC-TUSD,XMR-TUSD,ZRX-TUSD,NEO-TUSD,USD-TUSD,BTC-DAI,ETH-DAI,MKR-DAI,EOS-DAI,USD-DAI,MKR-BTC,MKR-ETH,MKR-USD,TUSD-DAI,NEO-DAI,LTC-DAI,XMR-DAI,XRP-DAI,NEXO-ETH,NEXO-USD,DWS-BTC,DWS-ETH,DWS-USD,APPC-BTC,APPC-ETH,APPC-USD,BIT-ETH,SPC-BTC,SPC-ETH,SPC-USD,REX-BTC,REX-ETH,REX-USD,ELF-BTC,ELF-USD,BCD-BTC,BCD-USD,CVCOIN-BTC,CVCOIN-ETH,CVCOIN-USD,EDG-ETH,EDG-USD,NLC2-BTC,DASH-EURS,ZEC-EURS,BTC-EURS,EOS-EURS,ETH-EURS,LTC-EURS,NEO-EURS,XMR-EURS,XRP-EURS,EURS-USD,EURS-TUSD,EURS-DAI,MNX-USD,ROX-ETH,ZPR-ETH,MNX-BTC,MNX-ETH,KIND-BTC,KIND-ETH,ENGT-BTC,ENGT-ETH,PMA-BTC,PMA-ETH,TV-BTC,TV-ETH,TV-USD,BAT-BTC,BAT-ETH,BAT-USD,SRN-BTC,SRN-ETH,SRN-USD,SVD-BTC,SVD-ETH,SVD-USD,GST-BTC,GST-ETH,GST-USD,BNB-BTC,BNB-ETH,BNB-USD,DIT-BTC,DIT-ETH,POA20-BTC,PROC-BTC,POA20-ETH,POA20-USD,POA20-DAI,NIM-BTC,USE-BTC,USE-ETH,DAV-BTC,DAV-ETH,ABTC-BTC,NIM-ETH,ABA-BTC,ABA-ETH,ABA-USD,BCN-EOS,LTC-EOS,XMR-EOS,DASH-EOS,TRX-EOS,NEO-EOS,ZEC-EOS,LSK-EOS,XEM-EOS,XRP-EOS,RCN-BTC,RCN-ETH,RCN-USD,HMQ-BTC,HMQ-ETH,MYST-BTC,MYST-ETH,USD-GUSD,BTC-GUSD,ETH-GUSD,EOS-GUSD,AXPR-BTC,AXPR-ETH,DAG-BTC,DAG-ETH,BITS-BTC,BITS-ETH,BITS-USD,CDCC-BTC,CDCC-ETH,CDCC-USD,VET-BTC,VET-ETH,VET-USD,SILK-ETH,BOX-BTC,BOX-ETH,BOX-EURS,BOX-EOS,VOCO-BTC,VOCO-ETH,VOCO-USD,PASS-BTC,PASS-ETH,SLX-BTC,SLX-USD,PBTT-BTC,PMA-USD,TRAD-BTC,DGTX-BTC,DGTX-ETH,DGTX-USD,MRK-BTC,MRK-ETH,DGB-TUSD,SNBL-BTC,BCH-BTC,BCH-USD,BSV-BTC,BSV-USD,BKX-BTC,NPLC-BTC,NPLC-ETH,ETN-BTC,ETN-ETH,ETN-USD,DTR-BTC,DTR-ETH,TDP-BTC,HBT-ETH,PXG-BTC,PXG-USD,BTC-PAX,ETH-PAX,USD-PAX,BTC-USDC,ETH-USDC,USD-USDC,TUSD-USDC,DAI-USDC,EOS-PAX,CLO-BTC,CLO-ETH,CLO-USD,PETH-BTC,PETH-ETH,PETH-USD,BRD-BTC,BRD-ETH,NMR-BTC,SALT-BTC,SALT-ETH,POLY-BTC,POLY-ETH,POWR-BTC,POWR-ETH,STORJ-BTC,STORJ-ETH,STORJ-USD,MLN-BTC,MLN-ETH,BDG-BTC,POA-ETH,POA-BTC,POA-USD,POA-DAI,KIN-BTC,VEO-BTC,PLA-BTC,PLA-ETH,PLA-USD,BTT-BTC,BTT-USD,BTT-ETH,ZEN-BTC,ZEN-ETH,ZEN-USD,GRIN-BTC,GRIN-ETH,GRIN-USD,FET-BTC,HT-BTC,HT-USD,XZC-BTC,XZC-ETH,XZC-USD,VRA-BTC,VRA-ETH,BTC-KRWB,USD-KRWB,WBTC-ETH,CRO-BTC,CRO-ETH,CRO-USD,GAS-BTC,GAS-ETH,GAS-USD,ORMEUS-BTC,ORMEUS-ETH,SWM-ETH,SWM-USD,PRE-ETH,PHX-BTC,PHX-ETH,PHX-USD,BET-BTC,USD-EOSDT,BTC-EOSDT,ETH-EOSDT,EOS-EOSDT,DAI-EOSDT,NUT-BTC,NUT-EOS,NUT-USD,CUTE-BTC,CUTE-ETH,CUTE-USD,CUTE-EOS,XCON-BTC,DCR-BTC,DCR-ETH,DCR-USD,MG-BTC,MG-ETH,MG-EOS,MG-USD,GNX-BTC,PRO-BTC,EURS-EOSDT,TUSD-EOSDT,ECOIN-BTC,ECOIN-ETH,ECOIN-USD,AGI-BTC,LOOM-BTC,LOOM-ETH,BLZ-BTC,QKC-BTC,QKC-ETH,KNC-BTC,KNC-ETH,KNC-USD,KEY-BTC,KEY-ETH,ATOM-BTC,ATOM-USD,ATOM-ETH,BRDG-BTC,BRDG-ETH,BRDG-USD,MTL-BTC,MTL-ETH,EXP-BTC,BTCB-BTC,PBT-BTC,PBT-ETH,LINK-BTC,LINK-ETH,LINK-USD,USD-USDT20,PHB-BTC,BCH-ETH,BCH-DAI,BCH-TUSD,BCH-EURS,DAPP-BTC,DAPP-EOS,BTC-USDT20,DENT-BTC,DENT-USD,NJBC-BTC,NJBC-ETH,XRC-BTC,EOS-BCH,LTC-BCH,XRP-BCH,TRX-BCH,XLM-BCH,ETC-BCH,DASH-BCH,ZEC-BCH,BKX-USD,LAMB-BTC,NPXS-BTC,HBAR-BTC,HBAR-USD,ONE-BTC,RFR-BTC,RFR-USD,BUSD-USD,PAXG-BTC,PAXG-USD,REN-BTC,IGNIS-BTC,CEL-BTC,CEL-ETH,WIN-USD,ADK-BTC,PART-BTC,SOZ-BTC,SOZ-ETH,SOZ-USD,WAVES-USD,ADA-BCH,ONT-BCH,XMR-BCH,ATOM-BCH,LINK-BCH,OMG-BCH,WAVES-BCH,IOTX-BTC,HOT-BTC,SLV-BTC,HEDG-BTC,CHZ-BTC,CHZ-USD,COCOS-BTC,COCOS-USD,SEELE-BTC,SEELE-USD,MDA-BTC,LEO-USD,REM-BTC,REM-ETH,REM-USD,SCD-DAI,BTC-BUSD,RVN-BTC,BST-BTC,ERD-BTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -903,40 +1470,72 @@ "name": "Huobi", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiAuthPemKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIPVSj8YkpXibCAL9HwpGkDNSEXR9jcpiCthdikJqipNooAoGCCqGSM49\nAwEHoUQDQgAEHiB7q/HCqUrCNqPeTtRmKjyi2T+2O2JgoU8Mjx2R4z1h81uOZHCk\nxbsDg1fb7ACRMpKWPs59QWpQxhqMQrNw8w==\n-----END EC PRIVATE KEY-----\n", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "HT-USDT,BAT-ETH,AST-ETH,TRX-BTC,NEW-BTC,AE-BTC,IIC-BTC,NEW-USDT,CDC-BTC,AE-USDT,DGB-BTC,NAS-ETH,QSP-BTC,LYM-ETH,YCC-BTC,BCH-HT,BIX-ETH,WXT-BTC,XRP-BTC,IOST-BTC,CHAT-BTC,BTC-USDT,XTZ-BTC,PVT-BTC,PVT-USDT,WAVES-ETH,ACT-BTC,RSR-BTC,ACT-USDT,WXT-USDT,XLM-ETH,HT-BTC,UUU-USDT,XRP-USDT,UGAS-BTC,BTS-ETH,IRIS-ETH,LUN-BTC,IOST-HT,DOCK-BTC,ABT-ETH,CRO-BTC,MAN-ETH,ENG-ETH,QUN-BTC,APPC-BTC,KAN-ETH,VET-USDT,SOC-ETH,RSR-HT,RUFF-ETH,RCCC-ETH,AAC-ETH,MCO-BTC,RSR-USDT,TNB-ETH,UTK-ETH,ADX-BTC,WAX-ETH,IOST-USDT,HOT-ETH,WTC-USDT,CVCOIN-BTC,NCASH-ETH,ATP-BTC,SWFTC-ETH,GTC-BTC,PNT-BTC,GT-HT,NEO-BTC,OMG-BTC,EOS-HUSD,WPR-ETH,ARPA-BTC,BTM-BTC,BTM-USDT,KCASH-ETH,SSP-ETH,ARPA-USDT,CNN-BTC,NKN-BTC,NPXS-BTC,OMG-USDT,TOPC-ETH,XEM-BTC,BCH-USDT,SNC-BTC,POLY-ETH,CMT-ETH,PAI-USDT,ZEC-USDT,LSK-ETH,SMT-ETH,DASH-USDT,GAS-ETH,DASH-BTC,GXC-ETH,FTT-HT,IOTA-ETH,FTI-BTC,TRIO-ETH,LET-BTC,ZRX-ETH,ETN-ETH,EVX-ETH,BFT-ETH,GRS-BTC,XRP-HT,DASH-HT,QTUM-ETH,HIT-ETH,NEXO-BTC,QASH-BTC,EOS-ETH,ARDR-ETH,ADA-BTC,NEO-USDT,BTT-TRX,COVA-ETH,REN-BTC,LOOM-BTC,CVC-ETH,NANO-ETH,ARPA-HT,NEW-HT,BLZ-ETH,LINK-ETH,XTZ-USDT,PAY-BTC,GNT-USDT,YEE-ETH,XZC-ETH,EGCC-ETH,PROPY-ETH,ZEC-BTC,EDU-ETH,RTE-BTC,DCR-USDT,FTT-BTC,DCR-BTC,EKO-BTC,SBTC-BTC,ZLA-ETH,TOP-HT,ALGO-BTC,DTA-ETH,EKT-ETH,ATOM-USDT,LXT-USDT,ZEN-ETH,LOL-USDT,LTC-USDT,DAT-BTC,REQ-ETH,ELA-ETH,NKN-HT,PC-BTC,HIT-BTC,EKO-ETH,STK-ETH,LAMB-USDT,LAMB-HT,DOGE-ETH,ATOM-BTC,THETA-USDT,LOL-BTC,THETA-BTC,LSK-BTC,ADA-USDT,RDN-BTC,OGO-HT,UIP-USDT,WICC-BTC,OCN-BTC,ELF-BTC,AKRO-USDT,USDC-HUSD,LAMB-BTC,DBC-ETH,BTT-ETH,FAIR-BTC,POWR-ETH,MUSK-ETH,MT-BTC,STEEM-USDT,RBTC-BTC,CTXC-BTC,MANA-USDT,ICX-ETH,GET-BTC,LTC-BTC,ITC-ETH,BCV-BTC,ZJLT-BTC,AKRO-HT,TNT-ETH,TOP-BTC,MEX-BTC,DATX-BTC,ALGO-USDT,LXT-BTC,GT-USDT,FSN-HT,FSN-USDT,MTX-ETH,LET-ETH,OGO-USDT,PHX-BTC,KCASH-HT,HC-USDT,LOL-HT,NKN-USDT,HOT-BTC,LBA-BTC,XMX-BTC,OST-ETH,VEN-USDT,LTC-HT,LBA-USDT,VEN-BTC,CRE-HT,BIFI-BTC,BT1-BTC,HPT-BTC,NULS-BTC,WAN-BTC,ZIL-BTC,ETC-HT,TOS-BTC,MANA-BTC,SHE-BTC,GT-BTC,FSN-BTC,MCO-ETH,MTN-BTC,MDS-BTC,SRN-ETH,GVE-BTC,XMR-ETH,MEET-ETH,NULS-USDT,BCH-BTC,PAI-BTC,NCC-ETH,BSV-BTC,AKRO-BTC,ELF-USDT,DGD-ETH,PVT-HT,UIP-BTC,ATP-USDT,SEELE-ETH,GSC-BTC,ETC-USDT,SOC-BTC,GNX-BTC,WICC-USDT,QSP-ETH,RUFF-BTC,KNC-ETH,ATP-HT,CTXC-USDT,KMD-ETH,OGO-BTC,BKBT-BTC,DGB-ETH,WAVES-USDT,BCD-BTC,HPT-HT,ZIL-USDT,BUT-ETH,CVNT-BTC,OCN-USDT,SALT-ETH,XLM-BTC,TRX-USDT,RCN-BTC,DAC-ETH,MT-HT,ETH-HUSD,HPT-USDT,XTZ-ETH,USDT-HUSD,CHAT-ETH,ONT-USDT,SKM-USDT,MAN-BTC,ARDR-BTC,BCX-BTC,SKM-BTC,EOS-USDT,GNX-ETH,CRE-USDT,PORTAL-ETH,COVA-BTC,BIX-BTC,UUU-ETH,AAC-BTC,TRX-ETH,NEXO-ETH,NAS-BTC,ENG-BTC,AST-BTC,TT-HT,QUN-ETH,EOS-BTC,18C-ETH,WTC-ETH,CVCOIN-ETH,CRE-BTC,CNNS-USDT,WAX-BTC,AIDOC-BTC,VET-ETH,CMT-USDT,BSV-USDT,IDT-ETH,IOST-ETH,BTC-HUSD,IOTA-BTC,TNB-BTC,LINK-BTC,TOPC-BTC,RCCC-BTC,ZRX-USDT,CNNS-BTC,BOX-BTC,MDS-USDT,XLM-USDT,BAT-BTC,LYM-BTC,UC-ETH,RUFF-USDT,LUN-ETH,BIX-USDT,CDC-ETH,BTS-USDT,YCC-ETH,KAN-USDT,MTL-BTC,WAVES-BTC,ONT-BTC,HT-HUSD,IRIS-USDT,SOC-USDT,WPR-BTC,ETC-BTC,TUSD-HUSD,CVC-USDT,PROPY-BTC,TRIO-BTC,CVC-BTC,BTT-USDT,NANO-BTC,GXC-BTC,NCASH-BTC,XRP-HUSD,TT-USDT,SHE-ETH,NANO-USDT,LOOM-ETH,POWR-BTC,QTUM-BTC,SSP-BTC,BTM-ETH,QTUM-USDT,XZC-BTC,GNT-ETH,OMG-ETH,NPXS-ETH,SNT-USDT,ETH-USDT,ABT-BTC,BTS-BTC,STEEM-BTC,VSYS-USDT,BLZ-BTC,CNNS-HT,ADX-ETH,SMT-USDT,IOTA-USDT,PAY-ETH,CMT-BTC,UTK-BTC,SWFTC-BTC,GTC-ETH,LINK-USDT,SNC-ETH,SNT-BTC,EOS-HT,REN-ETH,PAX-HUSD,KCASH-BTC,HC-BTC,IIC-ETH,QASH-ETH,GRS-ETH,EDU-BTC,HIT-USDT,TOP-USDT,XZC-USDT,KAN-BTC,SC-BTC,SKM-HT,AE-ETH,STORJ-USDT,XVG-ETH,ZRX-BTC,EVX-BTC,ETN-BTC,BFT-BTC,FTI-ETH,DAT-ETH,UGAS-ETH,BAT-USDT,GXC-USDT,GAS-BTC,TNT-BTC,HB10-USDT,MUSK-BTC,FTT-USDT,STK-BTC,ELF-ETH,KNC-BTC,CTXC-ETH,DBC-BTC,HC-ETH,EKT-BTC,DTA-USDT,ZLA-BTC,EKT-USDT,DTA-BTC,OCN-ETH,DGD-BTC,BHT-USDT,MTX-BTC,BCV-ETH,YEE-BTC,VSYS-HT,MEX-ETH,DATX-ETH,EGCC-BTC,LXT-ETH,ITC-USDT,TOS-ETH,ITC-BTC,RCN-ETH,XVG-BTC,SC-ETH,BT2-BTC,REQ-BTC,ELA-USDT,LET-USDT,STORJ-BTC,ALGO-ETH,POLY-BTC,LAMB-ETH,DCR-ETH,EGT-BTC,RTE-ETH,FAIR-ETH,CNN-ETH,BHT-BTC,GSC-ETH,GNT-BTC,PAI-ETH,PC-ETH,ADA-ETH,DOGE-BTC,ZEN-BTC,STEEM-ETH,XMR-BTC,XMR-USDT,MDS-ETH,TT-BTC,BTT-BTC,BHT-HT,ZJLT-ETH,UC-BTC,GVE-ETH,MXC-BTC,MANA-ETH,VSYS-BTC,THETA-ETH,NCC-BTC,APPC-ETH,SMT-BTC,IDT-BTC,UIP-ETH,ETH-BTC,BOX-ETH,LBA-ETH,NULS-ETH,PNT-ETH,BTG-BTC,CVNT-ETH,SALT-BTC,XEM-USDT,WXT-HT,BUT-BTC,DAC-BTC,DOCK-ETH,GET-ETH,AIDOC-ETH,EGT-USDT,WAN-ETH,KMD-BTC,MTN-ETH,CRO-USDT,ONT-ETH,BKBT-ETH,MEET-BTC,VEN-ETH,MT-ETH,SRN-BTC,UUU-BTC,SEELE-BTC,ICX-BTC,RDN-ETH,EGT-HT,ZIL-ETH,IRIS-BTC,CRO-HT,ACT-ETH,DOGE-USDT,NAS-USDT,PORTAL-BTC,ELA-BTC,OST-BTC,WICC-ETH,VET-BTC,XMX-ETH,WTC-BTC,HT-ETH,ATOM-ETH,18C-BTC", - "enabledPairs": "BTC-USDT", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": false + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USDT", + "available": "SWFTC-BTC,PAY-ETH,WICC-BTC,OST-ETH,FTI-BTC,EOS-HUSD,CRO-HT,LET-USDT,BTM-BTC,SSP-ETH,PNT-ETH,MAN-ETH,LYM-ETH,BUT-BTC,DGD-BTC,BTM-USDT,UC-BTC,BCV-BTC,ZIL-ETH,REQ-ETH,UTK-BTC,HC-USDT,EDU-BTC,AE-ETH,BTS-USDT,GTC-BTC,ENG-BTC,WPR-BTC,HOT-ETH,SNC-BTC,UUU-BTC,CVC-ETH,CRE-HT,TNT-ETH,MX-BTC,VET-USDT,WTC-BTC,GRS-BTC,NULS-BTC,WAXP-BTC,TRIO-BTC,LINK-ETH,GT-BTC,FTT-BTC,OMG-BTC,UUU-USDT,WXT-BTC,TOP-HT,BTS-BTC,TNB-BTC,VET-ETH,VIDY-USDT,CKB-BTC,MTL-BTC,ITC-ETH,BSV-BTC,POWR-BTC,ARDR-BTC,BTT-ETH,TRX-BTC,DAT-ETH,EGT-HT,LET-BTC,GAS-ETH,LOL-BTC,GAS-BTC,OMG-ETH,BFT-BTC,PC-ETH,GET-BTC,HOT-BTC,YEE-BTC,KAN-USDT,XTZ-BTC,SMT-BTC,NANO-ETH,ZJLT-ETH,OGO-HT,BCH-HT,BCV-ETH,BTS-ETH,RUFF-ETH,PAX-HUSD,SMT-USDT,FTT-USDT,WXT-USDT,SHE-ETH,CNNS-HT,MEX-ETH,ZIL-USDT,EM-BTC,TUSD-HUSD,SSP-BTC,OST-BTC,MTN-ETH,PAI-ETH,STK-BTC,TNT-BTC,EKO-BTC,KNC-BTC,EOS-USDT,ONT-USDT,DAC-ETH,SBTC-BTC,WPR-ETH,DGB-BTC,WTC-USDT,BFT-ETH,WAVES-ETH,NPXS-ETH,UTK-ETH,RCCC-BTC,EVX-BTC,UIP-ETH,XMR-ETH,RTE-ETH,LET-ETH,BTT-TRX,CHAT-BTC,XTZ-ETH,LINK-BTC,NULS-ETH,PC-BTC,BLZ-BTC,WICC-ETH,CKB-USDT,ADA-ETH,NEW-BTC,TRX-ETH,BSV-USDT,IIC-BTC,BTT-BTC,LAMB-USDT,TNB-ETH,BOX-BTC,ATP-HT,YCC-BTC,AAC-BTC,EM-HT,CNNS-BTC,HPT-USDT,SKM-USDT,RTE-BTC,XLM-ETH,XRP-HUSD,EGCC-ETH,TRIO-ETH,18C-ETH,BCH-HUSD,WAVES-BTC,TOPC-ETH,ETC-HT,NAS-USDT,MTN-BTC,MTX-ETH,LSK-BTC,RUFF-BTC,PROPY-BTC,ACT-USDT,AKRO-BTC,IRIS-ETH,LTC-USDT,DBC-BTC,REN-ETH,AE-BTC,BAT-BTC,TOS-ETH,ACT-BTC,ARPA-BTC,FOR-USDT,KNC-ETH,EKO-ETH,GXC-BTC,ELA-USDT,SNT-BTC,IOTA-ETH,ARPA-USDT,PAI-USDT,BIX-ETH,KCASH-BTC,TT-USDT,UIP-BTC,ZEN-ETH,NANO-BTC,RSR-USDT,CTXC-BTC,BKBT-ETH,BSV-HUSD,QTUM-USDT,RDN-ETH,DASH-HT,HT-HUSD,STEEM-BTC,ADX-ETH,PORTAL-BTC,EOS-BTC,LAMB-BTC,QUN-BTC,IIC-ETH,BHT-HT,GNX-BTC,MEET-ETH,BTM-ETH,ATP-BTC,XMR-USDT,BTC-USDT,NAS-ETH,VSYS-USDT,XTZ-USDT,KMD-BTC,EVX-ETH,CVC-BTC,CRE-BTC,HC-ETH,KAN-BTC,NEW-USDT,DAC-BTC,DOGE-USDT,IOST-USDT,RCN-BTC,AIDOC-BTC,EDU-ETH,MT-HT,SRN-ETH,MAN-BTC,GT-HT,ETC-BTC,QSP-BTC,ETH-HUSD,XZC-ETH,XRP-HT,DATX-ETH,XMX-ETH,FAIR-ETH,LOL-USDT,GVE-ETH,REN-USDT,IRIS-USDT,PVT-USDT,MANA-ETH,SEELE-BTC,CMT-ETH,THETA-USDT,ABT-BTC,ETN-BTC,GSC-ETH,MCO-BTC,TRX-USDT,ITC-BTC,CNN-ETH,AKRO-HT,COVA-BTC,PAY-BTC,ICX-ETH,FSN-USDT,NEXO-BTC,QASH-ETH,ZEC-USDT,DAT-BTC,NKN-USDT,MX-HT,LXT-BTC,ALGO-BTC,OMG-USDT,EGT-BTC,ARDR-ETH,WAXP-ETH,GET-ETH,WICC-USDT,POWR-ETH,VIDY-BTC,EKT-ETH,HIT-BTC,LOOM-BTC,BHT-BTC,ZRX-ETH,WTC-ETH,ATOM-ETH,ELF-BTC,ONT-ETH,SEELE-ETH,OCN-USDT,LBA-BTC,POLY-ETH,ZEN-BTC,OCN-BTC,LBA-USDT,CVCOIN-ETH,IOST-ETH,DOGE-ETH,OGO-BTC,QTUM-ETH,XVG-ETH,BKBT-BTC,IDT-ETH,AIDOC-ETH,QSP-ETH,GSC-BTC,OGO-USDT,LSK-ETH,DBC-ETH,BIFI-BTC,ZLA-ETH,APPC-ETH,GNT-ETH,MTX-BTC,UGAS-ETH,BAT-ETH,NCASH-ETH,MT-BTC,BCH-USDT,MUSK-ETH,RBTC-BTC,LUN-BTC,HIT-USDT,DTA-BTC,ALGO-ETH,ELA-ETH,SOC-USDT,SC-ETH,CRO-USDT,XZC-USDT,HT-BTC,AST-ETH,THETA-BTC,ZRX-BTC,DCR-BTC,MCO-ETH,CHAT-ETH,VIDY-HT,NKN-BTC,SALT-BTC,ABT-ETH,SOC-ETH,BIX-USDT,LXT-ETH,MDS-ETH,ATOM-BTC,TT-HT,ADA-BTC,XLM-USDT,ATOM-USDT,DOCK-BTC,DTA-ETH,NODE-USDT,IDT-BTC,NCC-ETH,MDS-USDT,IOTA-BTC,DOCK-USDT,SC-BTC,LXT-USDT,DOGE-BTC,CKB-HT,EGT-USDT,OCN-ETH,AST-BTC,ETH-BTC,CVNT-BTC,CRO-BTC,GNT-USDT,YCC-ETH,XZC-BTC,ETC-USDT,APPC-BTC,ARPA-HT,BHD-USDT,ZLA-BTC,MANA-BTC,GNT-BTC,BOX-ETH,NEO-BTC,BAT-USDT,XRP-BTC,EKT-USDT,SOC-BTC,CTXC-USDT,MEX-BTC,BCX-BTC,WAN-ETH,TOP-BTC,DCR-ETH,CVNT-ETH,HB10-USDT,PROPY-ETH,SKM-HT,WXT-HT,QTUM-BTC,CMT-USDT,THETA-ETH,SALT-ETH,STEEM-ETH,AAC-ETH,PORTAL-ETH,NCASH-BTC,CVCOIN-BTC,ZRX-USDT,RDN-BTC,LAMB-HT,EOS-HT,FTT-HT,DASH-BTC,BIX-BTC,LUN-ETH,ONE-HT,GVE-BTC,STORJ-BTC,XEM-BTC,ONE-USDT,STK-ETH,STORJ-USDT,ONE-BTC,ELA-BTC,ICX-BTC,WAVES-USDT,CNNS-USDT,SRN-BTC,GRS-ETH,MT-ETH,PHX-BTC,HT-ETH,AKRO-USDT,KCASH-HT,BHD-HT,STEEM-USDT,BTT-USDT,SKM-BTC,ZJLT-BTC,NCC-BTC,FOR-BTC,RCN-ETH,RSR-BTC,DGB-ETH,GXC-USDT,NAS-BTC,RUFF-USDT,HPT-BTC,MUSK-BTC,ENG-ETH,LTC-BTC,BCD-BTC,XMX-BTC,AE-USDT,BTC-HUSD,LOOM-ETH,POLY-BTC,EKT-BTC,MXC-BTC,PNT-BTC,NEW-HT,ZEC-BTC,MANA-USDT,FOR-HT,ELF-USDT,KMD-ETH,SWFTC-ETH,USDT-HUSD,XVG-BTC,VSYS-HT,ZIL-BTC,WAN-BTC,ELF-ETH,LYM-BTC,IOTA-USDT,NODE-HT,EM-USDT,LOL-HT,QASH-BTC,BLZ-ETH,NPXS-BTC,FSN-BTC,CNN-BTC,COVA-ETH,BHT-USDT,ONT-BTC,XEM-USDT,CMT-BTC,ALGO-USDT,ETN-ETH,ITC-USDT,HIT-ETH,USDC-HUSD,LINK-USDT,CVC-USDT,REN-BTC,NODE-BTC,GTC-ETH,IRIS-BTC,DASH-USDT,MDS-BTC,LTC-HT,ADA-USDT,XMR-BTC,NEXO-ETH,VSYS-BTC,IOST-BTC,TOPC-BTC,CRE-USDT,TOP-USDT,SNC-ETH,KCASH-ETH,DGD-ETH,RCCC-ETH,SHE-BTC,DATX-BTC,HT-USDT,SEELE-USDT,FTI-ETH,BHD-BTC,PVT-BTC,XRP-USDT,UGAS-BTC,MX-USDT,HPT-HT,RSR-HT,ETH-USDT,UUU-ETH,GXC-ETH,ACT-ETH,BTG-BTC,FAIR-BTC,GT-USDT,SMT-ETH,TOS-BTC,EGCC-BTC,TT-BTC,BUT-ETH,ADX-BTC,REQ-BTC,MEET-BTC,IOST-HT,YEE-ETH,PAI-BTC,UIP-USDT,NEO-USDT,VET-BTC,SNT-USDT,KAN-ETH,NULS-USDT,BCH-BTC,HC-BTC,PVT-HT,UC-ETH,18C-BTC,EOS-ETH,LAMB-ETH,DTA-USDT,CTXC-ETH,NANO-USDT,QUN-ETH,XLM-BTC,DOCK-ETH,ATP-USDT,NKN-HT,DCR-USDT,GNX-ETH,LBA-ETH,FSN-HT" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "pemKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIPVSj8YkpXibCAL9HwpGkDNSEXR9jcpiCthdikJqipNooAoGCCqGSM49\nAwEHoUQDQgAEHiB7q/HCqUrCNqPeTtRmKjyi2T+2O2JgoU8Mjx2R4z1h81uOZHCk\nxbsDg1fb7ACRMpKWPs59QWpQxhqMQrNw8w==\n-----END EC PRIVATE KEY-----\n" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -949,40 +1548,69 @@ "name": "ITBIT", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "clientId": "ClientID", - "availablePairs": "XBTUSD,XBTSGD", - "enabledPairs": "XBTUSD,XBTSGD", "baseCurrencies": "USD,SGD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": false, - "pairsLastUpdated": 1566798411, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "lastUpdated": 1566798411, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "XBTUSD,XBTSGD", + "available": "XBTUSD,XBTSGD" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresSecret": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": {}, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": false, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -995,40 +1623,73 @@ "name": "Kraken", "enabled": true, "verbose": false, - "websocket": true, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "ETH-GBP,XTZ-ETH,ATOM-USD,ETC-EUR,ATOM-XBT,QTUM-USD,USDT-USD,ETH-CAD,ETH-USD,XTZ-USD,ADA-CAD,ATOM-CAD,XLM-XBT,ZEC-USD,QTUM-ETH,REP-XBT,REP-ETH,GNO-XBT,WAVES-XBT,EOS-ETH,QTUM-XBT,ZEC-XBT,BAT-EUR,BCH-XBT,WAVES-USD,ETC-XBT,MLN-ETH,REP-USD,ADA-XBT,GNO-ETH,DASH-EUR,EOS-XBT,XLM-EUR,XLM-USD,XRP-USD,ADA-USD,WAVES-ETH,XMR-EUR,XRP-JPY,ZEC-EUR,EOS-EUR,GNO-USD,ETH-JPY,LTC-EUR,MLN-XBT,XTZ-EUR,XBT-CAD,BAT-ETH,BCH-EUR,BAT-XBT,EOS-USD,ADA-EUR,BAT-USD,ETC-ETH,LTC-XBT,REP-EUR,XMR-USD,XRP-XBT,XRP-CAD,DASH-XBT,GNO-EUR,ZEC-JPY,ETH-EUR,XBT-USD,ADA-ETH,ETH-XBT,XTZ-XBT,XBT-JPY,XDG-XBT,XMR-XBT,ATOM-EUR,WAVES-EUR,ETC-USD,XTZ-CAD,XBT-EUR,QTUM-CAD,QTUM-EUR,DASH-USD,LTC-USD,XBT-GBP,XRP-EUR,ATOM-ETH,BCH-USD", - "enabledPairs": "XBT-USD", "baseCurrencies": "EUR,USD,CAD,GBP,JPY", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "-" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "separator": "," + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "XBT-USD", + "available": "DAI-USDT,SC-USD,XBT-GBP,GNO-USD,ICX-ETH,LSK-USD,OMG-USD,MLN-XBT,REP-USD,XBT-EUR,XBT-USD,XDG-XBT,XRP-CAD,ICX-USD,REP-XBT,XLM-USD,LSK-XBT,SC-EUR,ETC-EUR,XMR-USD,ZEC-XBT,EOS-ETH,GNO-ETH,ETH-GBP,ATOM-CAD,EOS-USD,GNO-XBT,ADA-EUR,ATOM-EUR,DASH-EUR,PAXG-EUR,PAXG-USD,ETC-ETH,ETC-XBT,ETH-USD,LTC-EUR,MLN-ETH,XTZ-EUR,BAT-ETH,ETH-CAD,XTZ-XBT,ETC-USD,ETH-EUR,BAT-XBT,LSK-ETH,XTZ-ETH,XTZ-USD,ADA-CAD,ATOM-USD,XRP-USD,ZEC-EUR,ICX-XBT,LINK-ETH,LINK-XBT,LSK-EUR,NANO-XBT,WAVES-EUR,REP-ETH,XMR-XBT,ZEC-USD,USDT-USD,WAVES-ETH,XMR-EUR,BCH-USD,DAI-EUR,ETH-XBT,BCH-XBT,XBT-JPY,XLM-XBT,ADA-ETH,ADA-USD,NANO-USD,PAXG-ETH,QTUM-ETH,BAT-USD,BCH-EUR,SC-XBT,LTC-XBT,LINK-EUR,SC-ETH,DASH-XBT,QTUM-CAD,PAXG-XBT,ADA-XBT,ATOM-XBT,ETH-DAI,LINK-USD,QTUM-USD,WAVES-XBT,ATOM-ETH,GNO-EUR,NANO-ETH,OMG-ETH,WAVES-USD,REP-EUR,DASH-USD,OMG-EUR,DAI-USD,EOS-EUR,QTUM-XBT,ETH-JPY,LTC-USD,XTZ-CAD,EOS-XBT,XLM-EUR,BAT-EUR,ICX-EUR,XRP-XBT,XRP-EUR,NANO-EUR,QTUM-EUR,XBT-CAD,XRP-JPY,ZEC-JPY,OMG-XBT" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "separator": "," + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresBase64DecodeSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1041,38 +1702,70 @@ "name": "LakeBTC", "enabled": true, "verbose": false, - "websocket": true, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "USDJPY,USDCHF,BTCCAD,GBPUSD,USDHKD,ETHBTC,LTCBTC,BTCGBP,BTCHKD,BACETH,BTCNGN,USDCAD,BCHBTC,USDNGN,BTCEUR,AUDUSD,BTCUSD,BTCJPY,USDSGD,XRPBTC,BTCSGD,NZDUSD,BTCAUD,BTCNZD,BTCCHF,EURUSD", - "enabledPairs": "BTCUSD,BTCEUR,LTCBTC", "baseCurrencies": "USD,EUR,HKD,AUD,GBP,NZD,JPY,SGD,NGN,CHF,CAD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCUSD,BTCEUR,LTCBTC", + "available": "USDJPY,USDCHF,BTCCAD,GBPUSD,USDHKD,ETHBTC,LTCBTC,BTCGBP,BTCHKD,BACETH,BTCNGN,USDCAD,BCHBTC,USDNGN,BTCEUR,AUDUSD,BTCUSD,BTCJPY,USDSGD,XRPBTC,BTCSGD,NZDUSD,BTCAUD,BTCNZD,BTCCHF,EURUSD" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1085,38 +1778,70 @@ "name": "LocalBitcoins", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTCCOP,BTCTTD,BTCJOD,BTCXAF,BTCPLN,BTCGTQ,BTCBYN,BTCBRL,BTCTWD,BTCUAH,BTCSZL,BTCGHS,BTCLTC,BTCSEK,BTCKZT,BTCEGP,BTCLBP,BTCPEN,BTCRON,BTCVES,BTCCHF,BTCNZD,BTCTRY,BTCMAD,BTCQAR,BTCSGD,BTCPKR,BTCNAD,BTCHKD,BTCGEL,BTCMXN,BTCCNY,BTCGBP,BTCSAR,BTCDOP,BTCBOB,BTCNGN,BTCPYG,BTCCRC,BTCTZS,BTCCLP,BTCKES,BTCINR,BTCJPY,BTCPHP,BTCUGX,BTCKRW,BTCKWD,BTCILS,BTCETH,BTCDKK,BTCZMW,BTCRWF,BTCEUR,BTCXRP,BTCOMR,BTCAED,BTCIDR,BTCAUD,BTCTHB,BTCKHR,BTCVND,BTCNOK,BTCPAB,BTCZAR,BTCMYR,BTCCAD,BTCBDT,BTCRUB,BTCUSD,BTCLKR,BTCXOF,BTCIRR,BTCARS", - "enabledPairs": "BTCARS,BTCAUD,BTCBRL,BTCCAD,BTCCHF,BTCDKK,BTCEUR,BTCGBP,BTCHKD,BTCILS,BTCINR,BTCMXN,BTCNOK,BTCNZD,BTCPLN,BTCRUB,BTCSEK,BTCSGD,BTCTHB,BTCUSD,BTCZAR", "baseCurrencies": "ARS,AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,ILS,INR,MXN,NOK,NZD,PLN,RUB,SEK,SGD,THB,USD,ZAR", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCARS,BTCAUD,BTCBRL,BTCCAD,BTCCHF,BTCDKK,BTCEUR,BTCGBP,BTCHKD,BTCILS,BTCINR,BTCMXN,BTCNOK,BTCNZD,BTCPLN,BTCRUB,BTCSEK,BTCSGD,BTCTHB,BTCUSD,BTCZAR", + "available": "BTCXAF,BTCARS,BTCKES,BTCXRP,BTCNZD,BTCLTC,BTCZAR,BTCHKD,BTCCZK,BTCBDT,BTCAED,BTCPEN,BTCBAM,BTCLKR,BTCRON,BTCSGD,BTCBWP,BTCBYN,BTCBGN,BTCUYU,BTCTTD,BTCKRW,BTCAUD,BTCSAR,BTCTHB,BTCPYG,BTCILS,BTCJMD,BTCXOF,BTCTZS,BTCQAR,BTCCHF,BTCRUB,BTCBRL,BTCMWK,BTCKWD,BTCUGX,BTCGHS,BTCPHP,BTCJOD,BTCSZL,BTCGBP,BTCCAD,BTCPKR,BTCGEL,BTCDOP,BTCJPY,BTCHUF,BTCGTQ,BTCCLP,BTCKZT,BTCEGP,BTCMUR,BTCIDR,BTCCRC,BTCRWF,BTCVES,BTCMAD,BTCTWD,BTCBOB,BTCRSD,BTCMYR,BTCZMW,BTCUAH,BTCCNY,BTCEUR,BTCNGN,BTCNOK,BTCTRY,BTCINR,BTCMXN,BTCPLN,BTCVND,BTCSEK,BTCPAB,BTCETH,BTCUSD,BTCDKK,BTCCOP" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1129,40 +1854,74 @@ "name": "OKCOIN International", "enabled": true, "verbose": false, - "websocket": true, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC_USD,LTC_USD,ETH_USD,ETC_USD,TUSD_USD,BCH_USD,EOS_USD,XRP_USD,TRX_USD,BSV_USD,USDT_USD,USDK_USD,XLM_USD,ADA_USD,BAT_USD,DCR_USD,EURS_USD,GRIN_USD,GUSD_USD,PAX_USD,USDC_USD,ZEC_USD,ZRX_USD,BTC_USDT,BTC_GUSD,BTC_PAX,BTC_TUSD,BTC_EUR,BTC_EURS,BTC_USDC,ETH_EUR,BCH_EUR,EURS_EUR", - "enabledPairs": "BTC_USD", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot", + "margin" + ], + "pairs": { + "spot": { + "enabled": "BTC-USD", + "available": "BTC-USD,LTC-USD,ETH-USD,ETC-USD,TUSD-USD,BCH-USD,EOS-USD,XRP-USD,TRX-USD,BSV-USD,USDT-USD,USDK-USD,XLM-USD,ADA-USD,BAT-USD,DCR-USD,EURS-USD,HBAR-USD,PAX-USD,USDC-USD,ZEC-USD,BTC-USDT,BTC-EUR,BTC-EURS,ETH-EUR,BCH-EUR,EURS-EUR" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1175,40 +1934,108 @@ "name": "OKEX", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BCH_BTC,BSV_BTC,DASH_BTC,ADA_BTC,ABL_BTC,AE_BTC,ALGO_BTC,ARDR_BTC,ATOM_BTC,BLOC_BTC,BTT_BTC,CAI_BTC,CTXC_BTC,CVT_BTC,DCR_BTC,EGT_BTC,GUSD_BTC,HPB_BTC,HYC_BTC,KAN_BTC,LBA_BTC,LEO_BTC,LET_BTC,LSK_BTC,NXT_BTC,ORS_BTC,PAX_BTC,SC_BTC,TUSD_BTC,USDC_BTC,VITE_BTC,WAVES_BTC,WIN_BTC,WXT_BTC,XAS_BTC,YOU_BTC,ZCO_BTC,ZIL_BTC,XRP_BTC,ELF_BTC,LRC_BTC,MCO_BTC,NULS_BTC,BCX_BTC,CMT_BTC,EDO_BTC,ITC_BTC,SBTC_BTC,ZEC_BTC,NEO_BTC,GAS_BTC,HC_BTC,QTUM_BTC,IOTA_BTC,XUC_BTC,EOS_BTC,SNT_BTC,OMG_BTC,LTC_BTC,ETH_BTC,ETC_BTC,BCD_BTC,BTG_BTC,ACT_BTC,PAY_BTC,BTM_BTC,DGD_BTC,GNT_BTC,LINK_BTC,WTC_BTC,ZRX_BTC,BNT_BTC,CVC_BTC,MANA_BTC,KNC_BTC,GNX_BTC,ICX_BTC,XEM_BTC,ARK_BTC,YOYO_BTC,FUN_BTC,ACE_BTC,TRX_BTC,DGB_BTC,SWFTC_BTC,XMR_BTC,XLM_BTC,KCASH_BTC,MDT_BTC,NAS_BTC,UGC_BTC,DPY_BTC,SSC_BTC,AAC_BTC,VIB_BTC,QUN_BTC,INT_BTC,IOST_BTC,INS_BTC,MOF_BTC,TCT_BTC,STC_BTC,THETA_BTC,PST_BTC,SNC_BTC,MKR_BTC,LIGHT_BTC,OF_BTC,TRUE_BTC,SOC_BTC,ZEN_BTC,HMC_BTC,ZIP_BTC,NANO_BTC,CIC_BTC,GTO_BTC,CHAT_BTC,INSUR_BTC,R_BTC,BEC_BTC,MITH_BTC,ABT_BTC,BKX_BTC,RFR_BTC,TRIO_BTC,DADI_BTC,ONT_BTC,OKB_BTC,ADA_ETH,ABL_ETH,AE_ETH,ALGO_ETH,ATOM_ETH,BTT_ETH,CAI_ETH,CTXC_ETH,DCR_ETH,EGT_ETH,HPB_ETH,HYC_ETH,KAN_ETH,LEO_ETH,LSK_ETH,MVP_ETH,ORS_ETH,SC_ETH,SDA_ETH,WAVES_ETH,WIN_ETH,YOU_ETH,ZIL_ETH,ELF_ETH,LTC_ETH,CMT_ETH,PRA_ETH,LRC_ETH,MCO_ETH,NULS_ETH,DGD_ETH,SNT_ETH,STORJ_ETH,ACT_ETH,BTM_ETH,EOS_ETH,OMG_ETH,DASH_ETH,XRP_ETH,ZEC_ETH,NEO_ETH,GAS_ETH,HC_ETH,QTUM_ETH,IOTA_ETH,ETC_ETH,LINK_ETH,WTC_ETH,ZRX_ETH,BNT_ETH,CVC_ETH,MANA_ETH,GNX_ETH,ICX_ETH,XEM_ETH,YOYO_ETH,TRX_ETH,DGB_ETH,SWFTC_ETH,XMR_ETH,XLM_ETH,KCASH_ETH,MDT_ETH,NAS_ETH,SSC_ETH,AAC_ETH,FAIR_ETH,RCT_ETH,TOPC_ETH,QUN_ETH,INT_ETH,IOST_ETH,INS_ETH,MOF_ETH,REF_ETH,SNC_ETH,MKR_ETH,LIGHT_ETH,OF_ETH,TRUE_ETH,ZEN_ETH,HMC_ETH,ZIP_ETH,NANO_ETH,CIC_ETH,GTO_ETH,INSUR_ETH,UCT_ETH,MITH_ETH,ABT_ETH,AUTO_ETH,TRIO_ETH,TRA_ETH,ONT_ETH,OKB_ETH,BTC_USDK,LTC_USDK,ETH_USDK,OKB_USDK,ETC_USDK,BCH_USDT,BCH_USDK,EOS_USDK,XRP_USDK,TRX_USDK,BSV_USDT,BSV_USDK,USDT_USDK,ADA_USDT,AE_USDT,ALGO_USDT,ALGO_USDK,ALV_USDT,ATOM_USDT,BLOC_USDT,BTT_USDT,CAI_USDT,CRO_USDT,CRO_USDK,CTXC_USDT,CVT_USDT,DCR_USDT,DOGE_USDT,DOGE_USDK,EC_USDT,EC_USDK,EGT_USDT,EM_USDT,EM_USDK,ETM_USDT,ETM_USDK,FSN_USDT,FSN_USDK,FTM_USDT,FTM_USDK,GUSD_USDT,HPB_USDT,HYC_USDT,KAN_USDT,LAMB_USDT,LAMB_USDK,LBA_USDT,LEO_USDT,LEO_USDK,LET_USDT,LSK_USDT,MVP_USDT,ORBS_USDT,ORBS_USDK,ORS_USDT,PAX_USDT,PLG_USDT,PLG_USDK,SC_USDT,TUSD_USDT,USDC_USDT,VNT_USDT,VNT_USDK,WAVES_USDT,WIN_USDT,WXT_USDT,WXT_USDK,XAS_USDT,YOU_USDT,ZIL_USDT,TRX_OKB,ADA_OKB,AE_OKB,BLOC_OKB,DCR_OKB,EGT_OKB,SC_OKB,WAVES_OKB,WXT_OKB,ELF_USDT,DASH_USDT,BTG_USDT,LRC_USDT,MCO_USDT,NULS_USDT,DASH_OKB,XRP_USDT,ZEC_USDT,NEO_USDT,GAS_USDT,HC_USDT,QTUM_USDT,IOTA_USDT,BTC_USDT,BCD_USDT,XUC_USDT,CMT_USDT,EDO_USDT,ITC_USDT,PRA_USDT,ETH_USDT,LTC_USDT,ETC_USDT,EOS_USDT,OMG_USDT,ACT_USDT,BTM_USDT,DGD_USDT,GNT_USDT,PAY_USDT,STORJ_USDT,SNT_USDT,LINK_USDT,WTC_USDT,ZRX_USDT,BNT_USDT,CVC_USDT,MANA_USDT,KNC_USDT,ICX_USDT,XEM_USDT,ARK_USDT,YOYO_USDT,AST_USDT,TRX_USDT,MDA_USDT,DGB_USDT,PPT_USDT,SWFTC_USDT,XMR_USDT,XLM_USDT,KCASH_USDT,MDT_USDT,NAS_USDT,RNT_USDT,UGC_USDT,DPY_USDT,SSC_USDT,AAC_USDT,FAIR_USDT,UBTC_USDT,SHOW_USDT,VIB_USDT,MOT_USDT,UTK_USDT,TOPC_USDT,QUN_USDT,INT_USDT,IPC_USDT,IOST_USDT,INS_USDT,YEE_USDT,MOF_USDT,TCT_USDT,STC_USDT,THETA_USDT,PST_USDT,MKR_USDT,LIGHT_USDT,OF_USDT,TRUE_USDT,SOC_USDT,ZEN_USDT,HMC_USDT,ZIP_USDT,NANO_USDT,CIC_USDT,GTO_USDT,CHAT_USDT,INSUR_USDT,R_USDT,BEC_USDT,MITH_USDT,ABT_USDT,BKX_USDT,RFR_USDT,TRIO_USDT,DADI_USDT,ONT_USDT,OKB_USDT,NEO_OKB,LTC_OKB,ETC_OKB,XRP_OKB,ZEC_OKB,QTUM_OKB,IOTA_OKB,EOS_OKB", - "enabledPairs": "ltc_btc", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "assetTypes": [ + "spot", + "futures", + "perpetualswap", + "index" + ], + "pairs": { + "futures": { + "available": "XRP-USD_191206,XRP-USD_191213,XRP-USD_191227,BTC-USD_191206,BTC-USD_191213,BTC-USD_191227,BTC-USDT_191206,BTC-USDT_191213,BTC-USDT_191227,LTC-USD_191206,LTC-USD_191213,LTC-USD_191227,LTC-USDT_191206,LTC-USDT_191213,LTC-USDT_191227,ETH-USD_191206,ETH-USD_191213,ETH-USD_191227,ETH-USDT_191206,ETH-USDT_191213,ETH-USDT_191227,ETC-USD_191206,ETC-USD_191213,ETC-USD_191227,BCH-USD_191206,BCH-USD_191213,BCH-USD_191227,BCH-USDT_191206,BCH-USDT_191213,BCH-USDT_191227,BSV-USD_191206,BSV-USD_191213,BSV-USD_191227,EOS-USDT_191206,EOS-USDT_191213,EOS-USDT_191227,EOS-USD_191206,EOS-USD_191213,EOS-USD_191227,TRX-USD_191206,TRX-USD_191213,TRX-USD_191227", + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + } + }, + "index": { + "available": "XRP-USD,XRP-USD,XRP-USD,BTC-USD,BTC-USD,BTC-USD,BTC-USDT,BTC-USDT,BTC-USDT,LTC-USD,LTC-USD,LTC-USD,LTC-USDT,LTC-USDT,LTC-USDT,ETH-USD,ETH-USD,ETH-USD,ETH-USDT,ETH-USDT,ETH-USDT,ETC-USD,ETC-USD,ETC-USD,BCH-USD,BCH-USD,BCH-USD,BCH-USDT,BCH-USDT,BCH-USDT,BSV-USD,BSV-USD,BSV-USD,EOS-USDT,EOS-USDT,EOS-USDT,EOS-USD,EOS-USD,EOS-USD,TRX-USD,TRX-USD,TRX-USD", + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + } + }, + "perpetualswap": { + "available": "BTC-USD_SWAP,LTC-USD_SWAP,ETH-USD_SWAP,TRX-USD_SWAP,BCH-USD_SWAP,BSV-USD_SWAP,EOS-USD_SWAP,XRP-USD_SWAP,ETC-USD_SWAP", + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + } + }, + "spot": { + "enabled": "EOS-USDT", + "available": "XPO-USDT,SPND-USDK,SPND-BTC,ROAD-USDK,BCH-BTC,BSV-BTC,DASH-BTC,ADA-BTC,ABL-BTC,AE-BTC,ALGO-BTC,ARDR-BTC,ATOM-BTC,BLOC-BTC,BTT-BTC,CAI-BTC,CRO-BTC,CTXC-BTC,CVT-BTC,DCR-BTC,EGT-BTC,GUSD-BTC,HBAR-BTC,HPB-BTC,HYC-BTC,KAN-BTC,LBA-BTC,LEO-BTC,LET-BTC,LSK-BTC,NXT-BTC,ORS-BTC,PAX-BTC,PMA-BTC,SC-BTC,TUSD-BTC,USDC-BTC,VITE-BTC,VSYS-BTC,WAVES-BTC,WIN-BTC,WXT-BTC,XAS-BTC,XTZ-BTC,YOU-BTC,ZIL-BTC,XRP-BTC,ELF-BTC,LRC-BTC,MCO-BTC,NULS-BTC,BCX-BTC,CMT-BTC,EDO-BTC,ITC-BTC,SBTC-BTC,ZEC-BTC,NEO-BTC,GAS-BTC,HC-BTC,QTUM-BTC,IOTA-BTC,XUC-BTC,EOS-BTC,SNT-BTC,OMG-BTC,LTC-BTC,ETH-BTC,ETC-BTC,BCD-BTC,BTG-BTC,ACT-BTC,PAY-BTC,BTM-BTC,DGD-BTC,GNT-BTC,LINK-BTC,WTC-BTC,ZRX-BTC,BNT-BTC,CVC-BTC,MANA-BTC,KNC-BTC,GNX-BTC,ICX-BTC,XEM-BTC,ARK-BTC,YOYO-BTC,FUN-BTC,ACE-BTC,TRX-BTC,DGB-BTC,SWFTC-BTC,XMR-BTC,XLM-BTC,KCASH-BTC,MDT-BTC,NAS-BTC,UGC-BTC,DPY-BTC,SSC-BTC,AAC-BTC,VIB-BTC,QUN-BTC,INT-BTC,IOST-BTC,INS-BTC,MOF-BTC,TCT-BTC,STC-BTC,THETA-BTC,PST-BTC,SNC-BTC,MKR-BTC,LIGHT-BTC,OF-BTC,TRUE-BTC,SOC-BTC,ZEN-BTC,HMC-BTC,ZIP-BTC,NANO-BTC,CIC-BTC,GTO-BTC,CHAT-BTC,INSUR-BTC,R-BTC,BEC-BTC,MITH-BTC,ABT-BTC,BKX-BTC,RFR-BTC,TRIO-BTC,EDGE-BTC,ONT-BTC,OKB-BTC,ADA-ETH,ABL-ETH,AE-ETH,ALGO-ETH,ATOM-ETH,BTT-ETH,CAI-ETH,CTXC-ETH,DCR-ETH,EGT-ETH,HPB-ETH,HYC-ETH,KAN-ETH,LEO-ETH,MVP-ETH,ORS-ETH,SC-ETH,SDA-ETH,WAVES-ETH,WIN-ETH,YOU-ETH,ZIL-ETH,ELF-ETH,LTC-ETH,CMT-ETH,PRA-ETH,LRC-ETH,MCO-ETH,NULS-ETH,DGD-ETH,STORJ-ETH,BTM-ETH,EOS-ETH,OMG-ETH,DASH-ETH,XRP-ETH,ZEC-ETH,NEO-ETH,GAS-ETH,HC-ETH,QTUM-ETH,IOTA-ETH,ETC-ETH,LINK-ETH,WTC-ETH,ZRX-ETH,CVC-ETH,MANA-ETH,GNX-ETH,XEM-ETH,TRX-ETH,SWFTC-ETH,XMR-ETH,XLM-ETH,KCASH-ETH,MDT-ETH,NAS-ETH,SSC-ETH,AAC-ETH,FAIR-ETH,RCT-ETH,TOPC-ETH,INT-ETH,IOST-ETH,INS-ETH,MOF-ETH,REF-ETH,MKR-ETH,LIGHT-ETH,OF-ETH,TRUE-ETH,ZEN-ETH,NANO-ETH,CIC-ETH,GTO-ETH,UCT-ETH,MITH-ETH,ABT-ETH,AUTO-ETH,TRIO-ETH,ONT-ETH,OKB-ETH,BTC-USDK,LTC-USDK,ETH-USDK,OKB-USDK,ETC-USDK,BCH-USDT,BCH-USDK,EOS-USDK,XRP-USDK,TRX-USDK,BSV-USDT,BSV-USDK,USDT-USDK,ADA-USDT,AE-USDT,ALGO-USDT,ALGO-USDK,ALV-USDT,ATOM-USDT,BLOC-USDT,BTT-USDT,CAI-USDT,CRO-USDT,CRO-USDK,CTXC-USDT,CVT-USDT,DCR-USDT,DOGE-USDT,DOGE-USDK,EC-USDT,EC-USDK,EGT-USDT,EM-USDT,EM-USDK,ETM-USDT,ETM-USDK,FSN-USDT,FSN-USDK,FTM-USDT,FTM-USDK,GUSD-USDT,HBAR-USDT,HBAR-USDK,HPB-USDT,HYC-USDT,KAN-USDT,LAMB-USDT,LAMB-USDK,LBA-USDT,LEO-USDT,LEO-USDK,LET-USDT,LSK-USDT,MVP-USDT,ORBS-USDT,ORBS-USDK,ORS-USDT,PAX-USDT,PLG-USDT,PLG-USDK,PMA-USDK,ROAD-USDT,SC-USDT,TUSD-USDT,USDC-USDT,VNT-USDT,VNT-USDK,VSYS-USDT,VSYS-USDK,WAVES-USDT,WIN-USDT,WXT-USDT,WXT-USDK,XAS-USDT,XPO-USDK,XTZ-USDT,YOU-USDT,ZIL-USDT,TRX-OKB,AE-OKB,BLOC-OKB,EGT-OKB,SC-OKB,WXT-OKB,ELF-USDT,DASH-USDT,BTG-USDT,LRC-USDT,MCO-USDT,NULS-USDT,DASH-OKB,XRP-USDT,ZEC-USDT,NEO-USDT,GAS-USDT,HC-USDT,QTUM-USDT,IOTA-USDT,BTC-USDT,BCD-USDT,XUC-USDT,CMT-USDT,EDO-USDT,ITC-USDT,PRA-USDT,ETH-USDT,LTC-USDT,ETC-USDT,EOS-USDT,OMG-USDT,ACT-USDT,BTM-USDT,DGD-USDT,GNT-USDT,PAY-USDT,STORJ-USDT,SNT-USDT,LINK-USDT,WTC-USDT,ZRX-USDT,BNT-USDT,CVC-USDT,MANA-USDT,KNC-USDT,ICX-USDT,XEM-USDT,ARK-USDT,YOYO-USDT,AST-USDT,TRX-USDT,MDA-USDT,DGB-USDT,PPT-USDT,SWFTC-USDT,XMR-USDT,XLM-USDT,KCASH-USDT,MDT-USDT,NAS-USDT,RNT-USDT,UGC-USDT,DPY-USDT,SSC-USDT,AAC-USDT,FAIR-USDT,UBTC-USDT,SHOW-USDT,VIB-USDT,MOT-USDT,UTK-USDT,TOPC-USDT,QUN-USDT,INT-USDT,IPC-USDT,IOST-USDT,INS-USDT,YEE-USDT,MOF-USDT,TCT-USDT,STC-USDT,THETA-USDT,PST-USDT,MKR-USDT,LIGHT-USDT,OF-USDT,TRUE-USDT,SOC-USDT,ZEN-USDT,HMC-USDT,ZIP-USDT,NANO-USDT,CIC-USDT,GTO-USDT,CHAT-USDT,INSUR-USDT,R-USDT,BEC-USDT,MITH-USDT,ABT-USDT,BKX-USDT,RFR-USDT,TRIO-USDT,EDGE-USDT,ONT-USDT,OKB-USDT,NEO-OKB,LTC-OKB,ETC-OKB,XRP-OKB,ZEC-OKB,IOTA-OKB,EOS-OKB", + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + } + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1221,40 +2048,72 @@ "name": "Poloniex", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BTC_DASH,BTC_ETC,USDT_REP,BTC_OMG,USDT_BCHSV,BTC_DOGE,USDT_XMR,BTC_LBC,ETH_ZEC,BTC_PASC,BTC_CVC,BTC_EOS,USDT_DOGE,USDT_QTUM,BTC_BCN,BTC_VTC,BTC_XRP,BTC_FCT,BTC_SNT,USDC_ZEC,BTC_POLY,USDC_ATOM,BTC_MAID,ETH_REP,BTC_GAS,ETH_BAT,BTC_MANA,BTC_NAV,USDT_STR,USDT_XRP,BTC_STEEM,USDT_EOS,BTC_BNT,BTC_LTC,BTC_XEM,BTC_DCR,BTC_STORJ,BTC_BCHSV,USDC_DOGE,BTC_CLAM,BTC_DGB,BTC_SC,ETH_ETC,BTC_STRAT,USDT_BCHABC,USDT_ZEC,ETH_EOS,BTC_LOOM,USDT_LSK,USDC_XMR,BTC_LPT,USDT_DGB,USDT_LTC,USDT_ZRX,BTC_QTUM,BTC_BCHABC,USDC_STR,BTC_GRIN,USDC_ETC,USDT_DASH,USDT_NXT,USDT_ETH,USDT_GNT,USDT_MANA,USDC_BCHABC,USDC_XRP,BTC_FOAM,USDT_ATOM,BTC_OMNI,BTC_NXT,BTC_VIA,BTC_BAT,USDC_BTC,USDC_GRIN,USDT_BTC,BTC_LSK,BTC_REP,BTC_ARDR,BTC_ZEC,BTC_ZRX,USDT_BAT,BTC_GAME,BTC_XPM,BTC_ETH,USDT_ETC,ETH_ZRX,USDC_EOS,BTC_GNT,USDT_SC,USDC_ETH,USDC_USDT,BTC_NMR,BTC_ATOM,USDT_GRIN,BTC_BTS,BTC_XMR,USDC_LTC,USDC_DASH,BTC_STR,BTC_KNC,USDC_BCHSV", - "enabledPairs": "BTC_LTC,BTC_ETH,BTC_DOGE,BTC_DASH,BTC_XRP", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_LTC,BTC_ETH,BTC_DOGE,BTC_DASH,BTC_XRP", + "available": "USDC_GRIN,BTC_BCN,BTC_DGB,BTC_XMR,USDT_STR,BTC_SC,BTC_ZRX,USDC_XMR,BTC_TRX,BTC_STR,BTC_SNT,USDT_QTUM,USDC_BTC,BTC_NMR,BTC_DASH,BTC_NXT,USDT_LTC,BTC_DCR,USDT_ZRX,USDC_ZEC,USDT_REP,USDT_BAT,BTC_MANA,USDC_BCHABC,USDC_STR,BTC_XRP,USDT_ETH,BTC_REP,USDT_EOS,USDC_ATOM,USDT_XRP,BTC_ETH,USDT_LSK,USDT_SC,USDT_MANA,USDC_ETC,USDC_ETH,BTC_BTS,BTC_LTC,BTC_ETC,BTC_OMG,BTC_STORJ,USDC_XRP,USDT_GRIN,BTC_QTUM,BTC_MAID,BTC_XEM,USDT_BTC,USDT_DASH,ETH_REP,BTC_ZEC,BTC_STRAT,USDC_LTC,BTC_FOAM,USDC_TRX,BTC_DOGE,BTC_VIA,BTC_VTC,ETH_ETC,USDT_ETC,ETH_EOS,USDC_BCHSV,USDT_NXT,USDT_XMR,BTC_ARDR,BTC_CVC,ETH_BAT,USDC_DOGE,BTC_XPM,BTC_LOOM,BTC_LPT,USDC_EOS,USDT_DGB,USDT_BCHSV,BTC_OMNI,ETH_ZEC,BTC_EOS,BTC_KNC,BTC_BCHSV,BTC_POLY,USDC_DASH,USDT_GNT,BTC_BCHABC,BTC_GRIN,BTC_ATOM,USDT_ATOM,USDT_BCHABC,BTC_LSK,ETH_ZRX,BTC_GAS,BTC_BAT,BTC_BNT,USDT_TRX,BTC_FCT,USDT_ZEC,BTC_GNT,USDT_DOGE,USDC_USDT" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1267,42 +2126,74 @@ "name": "Yobit", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "DASH_BTC,WAVES_BTC,LSK_BTC,LIZA_BTC,BCC_BTC,ETH_BTC,LTC_BTC,TRX_BTC,DOGE_BTC,VNTX_BTC,SW_BTC,ZEC_BTC,DASH_ETH,WAVES_ETH,LSK_ETH,LIZA_ETH,BCC_ETH,LTC_ETH,TRX_ETH,DOGE_ETH,VNTX_ETH,SW_ETH,ZEC_ETH,DASH_DOGE,WAVES_DOGE,LSK_DOGE,LIZA_DOGE,BCC_DOGE,LTC_DOGE,TRX_DOGE,VNTX_DOGE,SW_DOGE,ZEC_DOGE,DASH_USD,WAVES_USD,LSK_USD,LIZA_USD,BCC_USD,LTC_USD,TRX_USD,VNTX_USD,SW_USD,ZEC_USD,ETH_USD,BTC_USD,DASH_RUR,WAVES_BTC,WAVES_RUR,LSK_RUR,LIZA_RUR,BCC_RUR,LTC_RUR,TRX_RUR,VNTX_RUR,SW_RUR,ETH_RUR,ZEC_RUR", - "enabledPairs": "LTC_BTC,ETH_BTC,BTC_USD,DASH_BTC", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": false, - "pairsLastUpdated": 1566798411, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_", + "separator": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "lastUpdated": 1566798411, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "LTC_BTC,ETH_BTC,BTC_USD,DASH_BTC", + "available": "DASH_BTC,WAVES_BTC,LSK_BTC,LIZA_BTC,BCC_BTC,ETH_BTC,LTC_BTC,TRX_BTC,DOGE_BTC,VNTX_BTC,SW_BTC,ZEC_BTC,DASH_ETH,WAVES_ETH,LSK_ETH,LIZA_ETH,BCC_ETH,LTC_ETH,TRX_ETH,DOGE_ETH,VNTX_ETH,SW_ETH,ZEC_ETH,DASH_DOGE,WAVES_DOGE,LSK_DOGE,LIZA_DOGE,BCC_DOGE,LTC_DOGE,TRX_DOGE,VNTX_DOGE,SW_DOGE,ZEC_DOGE,DASH_USD,WAVES_USD,LSK_USD,LIZA_USD,BCC_USD,LTC_USD,TRX_USD,VNTX_USD,SW_USD,ZEC_USD,ETH_USD,BTC_USD,DASH_RUR,WAVES_BTC,WAVES_RUR,LSK_RUR,LIZA_RUR,BCC_RUR,LTC_RUR,TRX_RUR,VNTX_RUR,SW_RUR,ETH_RUR,ZEC_RUR" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_", - "separator": "-" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": false, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1315,40 +2206,72 @@ "name": "ZB", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "BRC_BTC,ETH_USDT,BCHSV_QC,LTC_PAX,BITE_BTC,QUN_QC,NWT_USDT,NEO_QC,PDX_BTC,SLT_USDT,XEM_USDT,XMR_QC,KAN_BTC,QTUM_BTC,BTN_USDT,SLT_BTC,GRIN_USDT,USDT_QC,BAR_USDT,ETC_PAX,EOS_USDT,B91_QC,BTS_QC,HSR_BTC,BTS_BTC,XRP_QC,ETC_QC,BTC_PAX,SNT_USDT,ETH_PAX,BTM_QC,TRUE_USDT,INK_QC,BCW_USDT,LBTC_USDT,PAX_QC,LTC_BTC,VSYS_ZB,CHAT_USDT,CDC_USDT,AE_QC,BDS_QC,MCO_USDT,KAN_QC,EPC_QC,XLM_QC,UBTC_USDT,BRC_USDT,TRUE_BTC,MANA_QC,ACC_USDT,BAT_BTC,LBTC_BTC,AAA_QC,BTN_QC,MITH_QC,BCD_QC,BTC_QC,ZRX_USDT,B91_USDT,BCHSV_USDT,MCO_QC,ETC_BTC,RCN_USDT,LBTC_QC,PDX_QC,BCX_BTC,SAFE_USDT,MANA_BTC,TOPC_USDT,DOGE_BTC,NEO_USDT,YTNB_USDT,LTC_QC,HPY_USDT,TRX_BTC,ZRX_QC,DASH_BTC,HSR_USDT,CDC_QC,KNC_USDT,GNT_USDT,GRAM_BTC,AE_USDT,GRAM_QC,XWC_USDT,ETZ_QC,XEM_BTC,VSYS_QC,ADA_BTC,1ST_USDT,UBTC_QC,BITCNY_QC,TOPC_QC,HLC_USDT,XMR_USDT,SLT_QC,XLM_USDT,ETC_USDT,TRUE_QC,ICX_BTC,ADA_USDT,BCX_USDT,PDX_USDT,BAT_USDT,DOGE_QC,NEO_BTC,TUSD_USDT,TV_BTC,QUN_USDT,XUC_QC,OMG_USDT,BTC_USDT,TRX_QC,ZRX_BTC,ETH_QC,SBTC_USDT,VSYS_BTC,ZB_USDT,DASH_USDT,DDM_QC,ETZ_USDT,MTL_USDT,EDO_USDT,KNC_QC,BCHABC_QC,EOSDAC_USDT,ZB_BTC,SNT_QC,DDM_USDT,XRP_USDT,ICX_QC,INK_USDT,BTS_USDT,OMG_QC,ETH_BTC,QTUM_QC,TRX_USDT,SAFE_QC,XLM_BTC,BCD_USDT,SUB_QC,GRIN_QC,EOS_BTC,EPC_BTC,ENTC_USDT,HOTC_USDT,BTH_USDT,TV_USDT,BCHABC_USDT,BTP_USDT,TV_QC,XTZ_USDT,MANA_USDT,1ST_QC,PAX_USDT,BTM_USDT,HSR_QC,LTC_USDT,BCW_QC,LEO_USDT,ZB_QC,BTP_QC,ADA_QC,XRP_BTC,BTH_QC,HOTC_QC,GRAM_USDT,EOSDAC_QC,GNT_BTC,QTUM_USDT,BTM_BTC,EOS_QC,DOGE_USDT,XEM_QC,SNT_BTC,BRC_QC,CHAT_QC,GNT_QC,HPY_QC,DASH_QC,ICX_USDT,BAT_QC,HLC_QC,BCX_QC,XWC_QC,OMG_BTC,AE_BTC", - "enabledPairs": "BTC_USDT,ETH_USDT", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "_" + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_USDT,ETH_USDT", + "available": "BTP_USDT,SNT_QC,HSR_QC,BTM_USDT,INK_USDT,EOSDAC_QC,BCHSV_USDT,BCHABC_USDT,KAN_QC,LTC_BTC,DDM_USDT,B91_QC,GRIN_USDT,ZB_USDT,ETH_BTC,ZB_QC,TV_BTC,AE_QC,MANA_BTC,BITE_BTC,DOGE_BTC,BCX_BTC,NEO_QC,BAT_USDT,ICX_USDT,TRUE_BTC,ETH_USDT,BRC_BTC,BCHSV_QC,GRIN_QC,HPY_QC,LBTC_BTC,BCW_QC,XRP_USDT,DASH_QC,HX_USDT,GRAM_QC,ADA_BTC,SBTC_USDT,LVN_QC,BDS_QC,XLM_QC,UBTC_QC,MANA_QC,BCX_QC,UBTC_USDT,SLT_BTC,LTC_USDT,OMG_QC,XEM_BTC,TV_USDT,ZRX_QC,ETZ_USDT,XTZ_USDT,XEM_USDT,TRUE_USDT,TRX_QC,QTUM_BTC,XLM_USDT,HC_BTC,SNT_BTC,SNT_USDT,VSYS_QC,EDO_USDT,HC_QC,PDX_QC,CRO_QC,AE_BTC,BAR_USDT,DDM_QC,AAA_QC,XWC_USDT,XRP_QC,BTM_BTC,HSR_USDT,HOTC_USDT,ENTC_USDT,SUB_QC,ETZ_QC,HC_USDT,BCX_USDT,DOGE_USDT,1ST_QC,SLT_QC,EOS_QC,BTP_QC,OMG_BTC,ZB_BTC,CDC_QC,PDX_BTC,XMR_QC,VSYS_BTC,BRC_USDT,XEM_QC,XLM_BTC,BTS_QC,HX_QC,SAFE_USDT,GNT_QC,INK_QC,KNC_USDT,BTM_QC,FN_USDT,LBTC_USDT,BCW_USDT,HOTC_QC,BCD_USDT,HSR_BTC,LVN_USDT,PAX_QC,BTN_QC,DASH_BTC,ETH_QC,HLC_QC,YTNB_USDT,HPY_USDT,USDT_QC,DASH_USDT,SAFE_QC,MCO_QC,BTN_USDT,EPC_BTC,TV_QC,MTL_USDT,OMG_USDT,ETC_USDT,ZRX_USDT,TSR_USDT,DOGE_QC,MANA_USDT,CHAT_USDT,TRX_USDT,ETC_QC,LTC_QC,MITH_QC,ETC_BTC,PDX_USDT,B91_USDT,XWC_QC,GNT_BTC,KAN_BTC,GNT_USDT,SLT_USDT,ZRX_BTC,PAX_USDT,QTUM_USDT,BTS_USDT,BTC_QC,GRAM_USDT,BCD_QC,CRO_USDT,NEO_USDT,BAT_BTC,LEO_USDT,EOSDAC_USDT,CDC_USDT,LBTC_QC,XRP_BTC,1ST_USDT,TUSD_USDT,BCHABC_QC,ICX_QC,EOS_USDT,EPC_QC,GRAM_BTC,QTUM_QC,NEO_BTC,BTH_QC,KNC_QC,BRC_QC,BTC_USDT,HLC_USDT,VSYS_ZB,TOPC_USDT,MCO_USDT,TRUE_QC,TOPC_QC,QUN_QC,BTH_USDT,ICX_BTC,XUC_QC,NWT_USDT,EOS_BTC,ADA_USDT,TRX_BTC,ADA_QC,BITCNY_QC,XMR_USDT,ACC_USDT,BAT_QC,RCN_USDT,BTS_BTC,QUN_USDT,FN_QC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": false, - "delimiter": "_" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1361,38 +2284,106 @@ "name": "Bitmex", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, "httpTimeout": 15000000000, "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, "websocketOrderbookBufferLimit": 5, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "proxyAddress": "", - "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "XRPU19,BCHU19,ADAU19,EOSU19,TRXU19,XBTUSD,XBT7D_U105,XBT7D_D95,XBTU19,XBTZ19,ETHUSD,ETHU19,LTCU19", - "enabledPairs": "XBTUSD", "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true + "currencyPairs": { + "assetTypes": [ + "perpetualcontract", + "futures", + "downsideprofitcontract", + "upsideprofitcontract" + ], + "pairs": { + "downsideprofitcontract": { + "available": "XBT7D_D95", + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + } + }, + "futures": { + "available": "XRPZ19,BCHZ19,ADAZ19,EOSZ19,TRXZ19,XBTZ19,ETHZ19,LTCZ19", + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + } + }, + "perpetualcontract": { + "available": "XBTUSD,ETHUSD", + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + } + }, + "spot": { + "enabled": "XBTUSD", + "available": "XRPU19,BCHU19,ADAU19,EOSU19,TRXU19,XBTUSD,XBT7D_U105,XBT7D_D95,XBTU19,XBTZ19,ETHUSD,ETHU19,LTCU19" + }, + "upsideprofitcontract": { + "available": "XBT7D_U105", + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + } + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1405,40 +2396,71 @@ "name": "Coinbene", "enabled": true, "verbose": false, - "websocket": false, - "useSandbox": false, - "restPollingDelay": 10, - "httpTimeout": 0, - "websocketResponseCheckTimeout": 0, - "websocketResponseMaxLimit": 0, - "websocketOrderbookBufferLimit": 0, - "httpUserAgent": "", - "httpDebugging": false, - "authenticatedApiSupport": false, - "authenticatedWebsocketApiSupport": false, - "apiKey": "Key", - "apiSecret": "Secret", - "apiUrl": "", - "apiUrlSecondary": "", - "proxyAddress": "", - "websocketUrl": "", - "availablePairs": "BTC/USDT", - "enabledPairs": "BTC/USDT", + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, "baseCurrencies": "USD", - "assetTypes": "SPOT", - "supportsAutoPairUpdates": true, - "configCurrencyPairFormat": { - "uppercase": true, - "delimiter": "/" + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "/" + }, + "configFormat": { + "uppercase": true, + "delimiter": "/" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC/USDT", + "available": "ABBC/BTC,ABBC/USDT,ABT/ETH,ABT/USDT,ABYSS/ETH,ACDC/BTC,ACDC/USDT,ADI/ETH,ADK/BTC,ADN/BTC,AE/BTC,AE/USDT,AIDOC/BTC,AION/BTC,AIPE/USDT,AIT/USDT,ALGO/USDT,ALI/ETH,ALX/ETH,APL/ETH,ATX/BTC,B2G/BTC,BAAS/BTC,BAT/BTC,BCH/USDT,BEAUTY/ETH,BETHER/ETH,BEZ/BTC,BGC/USDT,BKG/BTC,BNB/USDT,BNT/BTC,BOA/USDT,BSTN/ETH,BSV/USDT,BTC/USDT,BTFM/USDT,BTNT/BTC,BTSC/BTC,BTT/USDT,BU/ETH,BVT/ETH,C3W/ETH,CAN/ETH,CCC/ETH,CCE/USDT,CC/USDT,CEDEX/ETH,CENT/BTC,CFT/USDT,CLO/BTC,CMT/ETH,CMT/USDT,CNN/BTC,CNN/ETH,CNN/USDT,CONI/USDT,COSM/BTC,COSM/ETH,COZP/BTC,CPC/BTC,CREDO/ETH,CRN/BTC,CSCC/USDT,CS/ETH,CS/USDT,CTXC/ETH,CUST/USDT,CVC/BTC,CXP/BTC,DCA/ETH,DCT/BTC,DDAM/ETH,DDAM/USDT,DENT/BTC,DGD/BTC,DOCK/ETH,DSCB/USDT,DTA/ETH,DUC/BTC,DVC/ETH,EBC/BTC,EBC/ETH,EBC/USDT,ECA/BTC,ECP/BTC,EDC/BTC,EDR/ETH,ELF/BTC,EMT/USDT,EOS/BTC,EOS/USDT,EQUAD/BTC,ETC/BTC,ETC/USDT,ETH/BTC,ETH/USDT,ETK/BTC,ETN/BTC,FAB/ETH,FCC/BTC,FND/ETH,FNKOS/ETH,FTN/BTC,FTN/USDT,FTT/BTC,FXT/ETH,GDC/BTC,GDC/ETH,GDC/USDT,GETX/ETH,GLDR/ETH,GMTK/ETH,GOM2/USDT,GRAM/USDT,GRIN/BTC,GRN/BTC,GUSD/USDT,GVT/BTC,HAPPY/BTC,HDAC/BTC,HMB/USDT,HNB/USDT,HPT/ETH,HT/USDT,HUP/USDT,INCX/ETH,IOST/BTC,IOTE/USDT,ISR/BTC,ISR/ETH,IVY/ETH,JOB/BTC,KBC/BTC,KBC/USDT,KMD/BTC,KNT/ETH,KST/BTC,KUE/BTC,KUE/ETH,LAMB/USDT,LATX/BTC,LBK/BTC,LINK/BTC,LOOM/BTC,LTC/BTC,LTC/USDT,LUC/ETH,LUX/BTC,LVTC/ETH,MC/USDT,MDC/USDT,MIB/BTC,MINX/BTC,MINX/ETH,MOAC/USDT,MPL/BTC,MTC/BTC,MT/ETH,MTN/ETH,MT/USDT,MVL/ETH,MVPT/ETH,MXM/ETH,MXM/USDT,MZG/USDT,NANO/BTC,NBAI/ETH,NCASH/BTC,NEO/BTC,NEO/USDT,NFT/USDT,NOBS/BTC,NPXS/ETH,NPXS/USDT,NTY/ETH,ODC/USDT,OMG/BTC,OMX/ETH,OVC/ETH,OZX/ETH,PAL/ETH,PAT/ETH,PAX/USDT,PKX/BTC,PLAY/BTC,PMA/ETH,POLL/BTC,POLY/BTC,PPT/BTC,PSM/BTC,QKC/BTC,QTUM/BTC,QTUM/USDT,RBTC/BTC,RCOIN/BTC,RCOIN/USDT,REP/BTC,REV/BTC,RIF/BTC,SBT/USDT,SCC/BTC,SCO/BTC,SEN/BTC,SENC/ETH,SHE/BTC,SHVR/BTC,SIM/BTC,SKB/BTC,SKM/ETH,SKYM/USDT,SLT/ETH,SMARTUP/ETH,SMARTUP/USDT,SMART/USDT,SORO/USDT,SRCOIN/BTC,SRCOIN/ETH,STORJ/BTC,STQ/BTC,SWET/BTC,SWTC/USDT,TCT/BTC,TEMCO/USDT,TEN/BTC,TEN/ETH,TIB/BTC,TMTG/BTC,TOC/ETH,TOOS/USDT,TOSC/BTC,TRUE/ETH,TRX/BTC,TRX/USDT,TSL/BTC,TVB/USDT,UNI/USDT,UTNP/BTC,VBT/USDT,VEEN/BTC,VME/BTC,VME/ETH,VSC/ETH,VSF/BTC,W12/BTC,W12/ETH,WBL/BTC,WFX/BTC,XEM/BTC,XLM/BTC,XMCT/ETH,XMCT/USDT,XMR/BTC,XNK/ETH,XRP/BTC,XRP/USDT,XSR/USDT,YAP/BTC,YAP/USDT,YTA/USDT,ZAT/ETH,ZDC/BTC,ZEC/BTC,ZGC/BTC,ZRX/BTC" + } + } }, - "requestCurrencyPairFormat": { - "uppercase": true, - "delimiter": "/" + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } }, "bankAccounts": [ { + "enabled": false, "bankName": "", "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "", "accountNumber": "", "swiftCode": "", @@ -1450,9 +2472,12 @@ ], "bankAccounts": [ { - "enabled": true, + "enabled": false, "bankName": "test", "bankAddress": "test", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", "accountName": "TestAccount", "accountNumber": "0234", "swiftCode": "91272837", @@ -1460,20 +2485,5 @@ "supportedCurrencies": "USD", "supportedExchanges": "ANX,Kraken" } - ], - "connectionMonitor": { - "preferredDNSList": [ - "8.8.8.8", - "8.8.4.4", - "1.1.1.1", - "1.0.0.1" - ], - "preferredDomainList": [ - "www.google.com", - "www.cloudflare.com", - "www.facebook.com" - ], - "checkInterval": 1000000000 - }, - "fiatDispayCurrency": "" + ] } \ No newline at end of file diff --git a/testdata/http_mock/anx/anx.json b/testdata/http_mock/anx/anx.json index 64c8143c..7875089f 100644 --- a/testdata/http_mock/anx/anx.json +++ b/testdata/http_mock/anx/anx.json @@ -2339,7 +2339,7 @@ "timestamp": "1565238020536" }, "queryString": "", - "bodyParams": "{\"nonce\":\"1565238020075\",\"order\":{\"orderType\":\"MARKET\",\"buyTradedCurrency\":true,\"tradedCurrency\":\"BTC\",\"settlementCurrency\":\"USD\",\"tradedCurrencyAmount\":\"1\",\"settlementCurrencyAmount\":\"0\",\"limitPriceInSettlementCurrency\":\"0\",\"replaceExistingOrderUuid\":\"\",\"replaceOnlyIfActive\":false}}", + "bodyParams": "{\"nonce\":\"1565238020075\",\"order\":{\"orderType\":\"LIMIT\",\"buyTradedCurrency\":true,\"tradedCurrency\":\"BTC\",\"settlementCurrency\":\"USD\",\"tradedCurrencyAmount\":\"1\",\"settlementCurrencyAmount\":\"0\",\"limitPriceInSettlementCurrency\":\"1\",\"replaceExistingOrderUuid\":\"\",\"replaceOnlyIfActive\":false}}", "headers": { "Content-Type": [ "application/json" @@ -2385,7 +2385,7 @@ "timestamp": "1565243984807" }, "queryString": "", - "bodyParams": "{\"address\":\"1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB\",\"amount\":\"100\",\"ccy\":\"BTC\",\"nonce\":\"1565243984355\"}", + "bodyParams": "{\"address\":\"1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB\",\"amount\":\"-1\",\"ccy\":\"BTC\",\"nonce\":\"1565243984355\"}", "headers": { "Content-Type": [ "application/json" diff --git a/testdata/http_mock/binance/binance.json b/testdata/http_mock/binance/binance.json index b9a04717..3bedf419 100644 --- a/testdata/http_mock/binance/binance.json +++ b/testdata/http_mock/binance/binance.json @@ -45378,7 +45378,7 @@ "POST": [ { "data": {}, - "queryString": "quantity=1\u0026recvWindow=5000\u0026side=BUY\u0026signature=d122bfd46f4dcf8dda98974aed14221916aabc050a6aa30194d124e81ffe7f44\u0026symbol=LTCBTC\u0026timeInForce=GTC\u0026timestamp=1560236466000\u0026type=MARKET", + "queryString": "price=1\u0026quantity=1000000000\u0026recvWindow=5000\u0026side=BUY\u0026symbol=LTCBTC\u0026timeInForce=GTC\u0026timestamp=1572330506000\u0026type=LIMIT\u0026signature=00954d00c69761017b3440e6e26d9bf6b394bea354c658ca09254b8c28995c73", "bodyParams": "", "headers": { "Key": [ diff --git a/testdata/http_mock/bitstamp/bitstamp.json b/testdata/http_mock/bitstamp/bitstamp.json index e1ce9694..8032e1f8 100644 --- a/testdata/http_mock/bitstamp/bitstamp.json +++ b/testdata/http_mock/bitstamp/bitstamp.json @@ -28,7 +28,7 @@ } }, "queryString": "", - "bodyParams": "address=1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB\u0026amount=100\u0026instant=1\u0026key=\u0026nonce=1560405519661130725\u0026signature=2CEEA36209A743A15CEF2C5AD724DEF2339E4FB5497DE58149507ED6CEEF98BA", + "bodyParams": "address=1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB\u0026amount=-1\u0026instant=1\u0026key=\u0026nonce=1560405519661130725\u0026signature=2CEEA36209A743A15CEF2C5AD724DEF2339E4FB5497DE58149507ED6CEEF98BA", "headers": { "Content-Type": [ "application/x-www-form-urlencoded" @@ -177,7 +177,7 @@ "error": "Order not found" }, "queryString": "", - "bodyParams": "id=1\u0026key=\u0026nonce=1560467884949723197\u0026signature=79D88FC2BC3AECBFDD27F70EF5DDBB485349F45129C58EE725BAEE0DF7818452", + "bodyParams": "id=1234\u0026key=\u0026nonce=1560467884949723197\u0026signature=79D88FC2BC3AECBFDD27F70EF5DDBB485349F45129C58EE725BAEE0DF7818452", "headers": { "Content-Type": [ "application/x-www-form-urlencoded" @@ -42798,7 +42798,7 @@ } }, "queryString": "", - "bodyParams": "account_currency=USD\u0026address=123+Fake+St\u0026amount=100\u0026bank_address=123+Fake+St\u0026bank_city=Tarry+Town\u0026bank_country=AU\u0026bank_name=Federal+Reserve+Bank\u0026bank_postal_code=2088\u0026bic=CTBAAU2S\u0026city=Tarry+Town\u0026comment=WITHDRAW+IT+ALL\u0026country=AU\u0026currency=USD\u0026iban=IT60X0542811101000000123456\u0026key=\u0026name=Satoshi+Nakamoto\u0026nonce=1560404081220544346\u0026postal_code=2088\u0026signature=17C71D3A9175255D46D786E9709B815E6C1B450B0E4E60B622EF35F0AD966B70\u0026type=international", + "bodyParams": "account_currency=USD\u0026address=123+Fake+St\u0026amount=-1\u0026bank_address=123+Fake+St\u0026bank_city=Tarry+Town\u0026bank_country=AU\u0026bank_name=Federal+Reserve+Bank\u0026bank_postal_code=2088\u0026bic=CTBAAU2S\u0026city=Tarry+Town\u0026comment=WITHDRAW+IT+ALL\u0026country=AU\u0026currency=USD\u0026iban=IT60X0542811101000000123456\u0026key=\u0026name=Satoshi+Nakamoto\u0026nonce=1560404081220544346\u0026postal_code=2088\u0026signature=17C71D3A9175255D46D786E9709B815E6C1B450B0E4E60B622EF35F0AD966B70\u0026type=international", "headers": { "Content-Type": [ "application/x-www-form-urlencoded" @@ -42815,7 +42815,7 @@ } }, "queryString": "", - "bodyParams": "account_currency=USD\u0026address=123+Fake+St\u0026amount=100\u0026bic=CTBAAU2S\u0026city=Tarry+Town\u0026comment=WITHDRAW+IT+ALL\u0026country=AU\u0026iban=IT60X0542811101000000123456\u0026key=\u0026name=Satoshi+Nakamoto\u0026nonce=1560405334882045192\u0026postal_code=2088\u0026signature=1EB01441F5FB1FA1335EF5586D86F210101F641768C7568A4F9D592AA529628B\u0026type=sepa", + "bodyParams": "account_currency=USD\u0026address=123+Fake+St\u0026amount=-1\u0026bic=CTBAAU2S\u0026city=Tarry+Town\u0026comment=WITHDRAW+IT+ALL\u0026country=AU\u0026iban=IT60X0542811101000000123456\u0026key=\u0026name=Satoshi+Nakamoto\u0026nonce=1560405334882045192\u0026postal_code=2088\u0026signature=1EB01441F5FB1FA1335EF5586D86F210101F641768C7568A4F9D592AA529628B\u0026type=sepa", "headers": { "Content-Type": [ "application/x-www-form-urlencoded" diff --git a/testdata/http_mock/exclusion.json b/testdata/http_mock/exclusion.json old mode 100755 new mode 100644 diff --git a/testdata/http_mock/gemini/gemini.json b/testdata/http_mock/gemini/gemini.json index 96bd94e6..e0e75a8c 100644 --- a/testdata/http_mock/gemini/gemini.json +++ b/testdata/http_mock/gemini/gemini.json @@ -1903,7 +1903,7 @@ "result": "error" }, "queryString": "", - "bodyParams": "{\"amount\":\"1\",\"nonce\":\"1565757283294274961\",\"price\":\"9000\",\"request\":\"/v1/order/new\",\"side\":\"buy\",\"symbol\":\"btcusd\",\"type\":\"exchange limit\"}", + "bodyParams": "{\"amount\":\"1\",\"nonce\":\"1565757283294274961\",\"price\":\"9000000\",\"request\":\"/v1/order/new\",\"side\":\"sell\",\"symbol\":\"btcusd\",\"type\":\"exchange limit\"}", "headers": { "Cache-Control": [ "no-cache" @@ -2706,7 +2706,7 @@ "result": "error" }, "queryString": "", - "bodyParams": "{\"address\":\"1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB\",\"amount\":\"100\",\"nonce\":\"1565755733919473409\",\"request\":\"/v1/withdraw/btc\"}", + "bodyParams": "{\"address\":\"1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB\",\"amount\":\"-1\",\"nonce\":\"1565755733919473409\",\"request\":\"/v1/withdraw/btc\"}", "headers": { "Cache-Control": [ "no-cache" @@ -2729,6 +2729,64 @@ } } ] + }, + "/v2/ticker/BTCUSD": { + "GET": [ + { + "data": { + "ask": "9663.28", + "bid": "9662.94", + "changes": [ + "9719", + "9730.13", + "9718.58", + "9672.54", + "9668.57", + "9701.67", + "10191.27", + "10225.8", + "10238.78", + "10210.5", + "10171.2", + "10156.31", + "10138.69", + "10121.71", + "10147.31", + "10120.74", + "10149.82", + "10185.68", + "10128.28", + "10070.56", + "10082.86", + "10114", + "10089.25", + "10142.29" + ], + "close": "9662.94", + "high": "11000", + "low": "9210", + "open": "10148.67", + "symbol": "BTCUSD" + }, + "queryString": "", + "bodyParams": "", + "headers": {} + } + ] + }, + "/v2/ticker/bla": { + "GET": [ + { + "data": { + "message": "Supplied value 'bla' is not a valid symbol. Please correct your API request to use one of the supported symbols: [zecbch, bchbtc, zecusd, ethusd, zecbtc, bcheth, zecltc, ltcbch, bchusd, ethbtc, ltcbtc, ltceth, zeceth, ltcusd, btcusd]", + "reason": "Bad Request", + "result": "error" + }, + "queryString": "", + "bodyParams": "", + "headers": {} + } + ] } } } \ No newline at end of file diff --git a/testdata/http_mock/localbitcoins/localbitcoins.json b/testdata/http_mock/localbitcoins/localbitcoins.json index f88cf267..1553be63 100644 --- a/testdata/http_mock/localbitcoins/localbitcoins.json +++ b/testdata/http_mock/localbitcoins/localbitcoins.json @@ -2640,7 +2640,7 @@ } }, "queryString": "", - "bodyParams": "address=1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB\u0026amount=100", + "bodyParams": "address=1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB\u0026amount=-1", "headers": { "Apiauth-Key": [ "" diff --git a/testdata/http_mock/poloniex/poloniex.json b/testdata/http_mock/poloniex/poloniex.json index 3c6186c0..10f443d0 100644 --- a/testdata/http_mock/poloniex/poloniex.json +++ b/testdata/http_mock/poloniex/poloniex.json @@ -8922,7 +8922,7 @@ "currencyPair": "BTC_LTC" }, "queryString": "", - "bodyParams": "amount=1\u0026command=buy\u0026currencyPair=BTC_LTC\u0026fillOrKill=1\u0026nonce=1560226024406623067\u0026rate=10", + "bodyParams": "amount=10000000\u0026command=buy\u0026currencyPair=BTC_LTC\u0026fillOrKill=1\u0026nonce=1560226024406623067\u0026rate=10", "headers": { "Content-Type": [ "application/x-www-form-urlencoded" diff --git a/testdata/preengine_config.json b/testdata/preengine_config.json new file mode 100644 index 00000000..7f058795 --- /dev/null +++ b/testdata/preengine_config.json @@ -0,0 +1,1469 @@ +{ + "name": "Skynet", + "encryptConfig": 0, + "globalHTTPTimeout": 15000000000, + "logging": { + "enabled": true, + "file": "debug.txt", + "colour": false, + "level": "DEBUG|WARN|INFO|ERROR|FATAL", + "rotate": false + }, + "profiler": { + "enabled": false + }, + "ntpclient": { + "enabled": 0, + "pool": [ + "pool.ntp.org:123" + ], + "allowedDifference": 50000000, + "allowedNegativeDifference": 50000000 + }, + "currencyConfig": { + "forexProviders": [ + { + "name": "CurrencyConverter", + "enabled": false, + "verbose": false, + "restPollingDelay": 600, + "apiKey": "Key", + "apiKeyLvl": -1, + "primaryProvider": false + }, + { + "name": "CurrencyLayer", + "enabled": false, + "verbose": false, + "restPollingDelay": 600, + "apiKey": "Key", + "apiKeyLvl": -1, + "primaryProvider": false + }, + { + "name": "Fixer", + "enabled": false, + "verbose": false, + "restPollingDelay": 600, + "apiKey": "Key", + "apiKeyLvl": -1, + "primaryProvider": false + }, + { + "name": "OpenExchangeRates", + "enabled": false, + "verbose": false, + "restPollingDelay": 600, + "apiKey": "Key", + "apiKeyLvl": -1, + "primaryProvider": false + }, + { + "name": "ExchangeRates", + "enabled": true, + "verbose": false, + "restPollingDelay": 600, + "apiKey": "Key", + "apiKeyLvl": -1, + "primaryProvider": true + } + ], + "cryptocurrencyProvider": { + "name": "CoinMarketCap", + "enabled": false, + "verbose": false, + "apiKey": "Key", + "accountPlan": "accountPlan" + }, + "cryptocurrencies": "BTC,LTC,ETH,XRP,NMC,NVC,PPC,XBT,DOGE,DASH", + "currencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "fiatDisplayCurrency": "USD", + "currencyFileUpdateDuration": 0, + "foreignExchangeUpdateDuration": 0 + }, + "communications": { + "slack": { + "name": "Slack", + "enabled": false, + "verbose": false, + "targetChannel": "general", + "verificationToken": "testtest" + }, + "smsGlobal": { + "name": "SMSGlobal", + "enabled": false, + "verbose": false, + "username": "Username", + "password": "Password", + "contacts": [ + { + "name": "Bob", + "number": "12345", + "enabled": false + } + ] + }, + "smtp": { + "name": "SMTP", + "enabled": false, + "verbose": false, + "host": "smtp.google.com", + "port": "537", + "accountName": "some", + "accountPassword": "password", + "recipientList": "lol123@gmail.com" + }, + "telegram": { + "name": "Telegram", + "enabled": false, + "verbose": false, + "verificationToken": "testest" + } + }, + "portfolioAddresses": { + "Addresses": [ + { + "Address": "1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy", + "CoinType": "BTC", + "Balance": 53000.01741264, + "Description": "" + }, + { + "Address": "3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v", + "CoinType": "BTC", + "Balance": 107848.28963408, + "Description": "" + }, + { + "Address": "LgY8ahfHRhvjVQC1zJnBhFMG5pCTMuKRqh", + "CoinType": "LTC", + "Balance": 0.03665026, + "Description": "" + }, + { + "Address": "0xb794f5ea0ba39494ce839613fffba74279579268", + "CoinType": "ETH", + "Balance": 0.25555604051325775, + "Description": "" + } + ] + }, + "webserver": { + "enabled": true, + "adminUsername": "admin", + "adminPassword": "Password", + "listenAddress": ":9050", + "websocketConnectionLimit": 1, + "websocketMaxAuthFailures": 3, + "websocketAllowInsecureOrigin": true + }, + "exchanges": [ + { + "name": "ANX", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "ATENC_GBP,ATENC_NZD,BTC_AUD,BTC_SGD,LTC_BTC,START_GBP,STR_BTC,XRP_BTC,ATENC_SGD,BTC_GBP,DOGE_BTC,OAX_ETH,START_AUD,START_JPY,ATENC_USD,BTC_EUR,GNT_ETH,START_EUR,ATENC_EUR,BTC_CAD,START_BTC,START_CAD,ATENC_HKD,ATENC_JPY,ETH_BTC,ETH_HKD,START_HKD,START_USD,ATENC_AUD,ETH_USD,START_SGD,ATENC_CAD,BTC_HKD,BTC_JPY,BTC_NZD,BTC_USD,START_NZD", + "enabledPairs": "BTC_USD,BTC_HKD,BTC_EUR,BTC_CAD,BTC_AUD,BTC_SGD,BTC_JPY,BTC_GBP,BTC_NZD,LTC_BTC,STR_BTC,XRP_BTC", + "baseCurrencies": "USD,HKD,EUR,CAD,AUD,SGD,JPY,GBP,NZD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Binance", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "ETH-BTC,LTC-BTC,BNB-BTC,NEO-BTC,QTUM-ETH,EOS-ETH,SNT-ETH,BNT-ETH,GAS-BTC,BNB-ETH,BTC-USDT,ETH-USDT,OAX-ETH,DNT-ETH,MCO-ETH,MCO-BTC,WTC-BTC,WTC-ETH,LRC-BTC,LRC-ETH,QTUM-BTC,YOYO-BTC,OMG-BTC,OMG-ETH,ZRX-BTC,ZRX-ETH,STRAT-BTC,STRAT-ETH,SNGLS-BTC,SNGLS-ETH,BQX-BTC,BQX-ETH,KNC-BTC,KNC-ETH,FUN-BTC,FUN-ETH,SNM-BTC,SNM-ETH,NEO-ETH,IOTA-BTC,IOTA-ETH,LINK-BTC,LINK-ETH,XVG-BTC,XVG-ETH,MDA-BTC,MDA-ETH,MTL-BTC,MTL-ETH,EOS-BTC,SNT-BTC,ETC-ETH,ETC-BTC,MTH-BTC,MTH-ETH,ENG-BTC,ENG-ETH,DNT-BTC,ZEC-BTC,ZEC-ETH,BNT-BTC,AST-BTC,AST-ETH,DASH-BTC,DASH-ETH,OAX-BTC,BTG-BTC,BTG-ETH,EVX-BTC,EVX-ETH,REQ-BTC,REQ-ETH,VIB-BTC,VIB-ETH,TRX-BTC,TRX-ETH,POWR-BTC,POWR-ETH,ARK-BTC,ARK-ETH,YOYO-ETH,XRP-BTC,XRP-ETH,ENJ-BTC,ENJ-ETH,STORJ-BTC,STORJ-ETH,BNB-USDT,YOYO-BNB,POWR-BNB,KMD-BTC,KMD-ETH,NULS-BNB,RCN-BTC,RCN-ETH,RCN-BNB,NULS-BTC,NULS-ETH,RDN-BTC,RDN-ETH,RDN-BNB,XMR-BTC,XMR-ETH,DLT-BNB,WTC-BNB,DLT-BTC,DLT-ETH,AMB-BTC,AMB-ETH,AMB-BNB,BAT-BTC,BAT-ETH,BAT-BNB,BCPT-BTC,BCPT-ETH,BCPT-BNB,ARN-BTC,ARN-ETH,GVT-BTC,GVT-ETH,CDT-BTC,CDT-ETH,GXS-BTC,GXS-ETH,NEO-USDT,NEO-BNB,POE-BTC,POE-ETH,QSP-BTC,QSP-ETH,QSP-BNB,BTS-BTC,BTS-ETH,BTS-BNB,XZC-BTC,XZC-ETH,XZC-BNB,LSK-BTC,LSK-ETH,LSK-BNB,TNT-BTC,TNT-ETH,FUEL-BTC,FUEL-ETH,MANA-BTC,MANA-ETH,BCD-BTC,BCD-ETH,DGD-BTC,DGD-ETH,IOTA-BNB,ADX-BTC,ADX-ETH,ADX-BNB,ADA-BTC,ADA-ETH,PPT-BTC,PPT-ETH,CMT-BTC,CMT-ETH,CMT-BNB,XLM-BTC,XLM-ETH,XLM-BNB,CND-BTC,CND-ETH,CND-BNB,LEND-BTC,LEND-ETH,WABI-BTC,WABI-ETH,WABI-BNB,LTC-ETH,LTC-USDT,LTC-BNB,TNB-BTC,TNB-ETH,WAVES-BTC,WAVES-ETH,WAVES-BNB,GTO-BTC,GTO-ETH,GTO-BNB,ICX-BTC,ICX-ETH,ICX-BNB,OST-BTC,OST-ETH,OST-BNB,ELF-BTC,ELF-ETH,AION-BTC,AION-ETH,AION-BNB,NEBL-BTC,NEBL-ETH,NEBL-BNB,BRD-BTC,BRD-ETH,BRD-BNB,MCO-BNB,EDO-BTC,EDO-ETH,NAV-BTC,NAV-ETH,NAV-BNB,LUN-BTC,LUN-ETH,APPC-BTC,APPC-ETH,APPC-BNB,VIBE-BTC,VIBE-ETH,RLC-BTC,RLC-ETH,RLC-BNB,INS-BTC,INS-ETH,PIVX-BTC,PIVX-ETH,PIVX-BNB,IOST-BTC,IOST-ETH,STEEM-BTC,STEEM-ETH,STEEM-BNB,NANO-BTC,NANO-ETH,NANO-BNB,VIA-BTC,VIA-ETH,VIA-BNB,BLZ-BTC,BLZ-ETH,BLZ-BNB,AE-BTC,AE-ETH,AE-BNB,NCASH-BTC,NCASH-ETH,NCASH-BNB,POA-BTC,POA-ETH,POA-BNB,ZIL-BTC,ZIL-ETH,ZIL-BNB,ONT-BTC,ONT-ETH,ONT-BNB,STORM-BTC,STORM-ETH,STORM-BNB,QTUM-BNB,QTUM-USDT,XEM-BTC,XEM-ETH,XEM-BNB,WAN-BTC,WAN-ETH,WAN-BNB,WPR-BTC,WPR-ETH,QLC-BTC,QLC-ETH,SYS-BTC,SYS-ETH,SYS-BNB,QLC-BNB,GRS-BTC,GRS-ETH,ADA-USDT,ADA-BNB,GNT-BTC,GNT-ETH,GNT-BNB,LOOM-BTC,LOOM-ETH,LOOM-BNB,XRP-USDT,REP-BTC,REP-ETH,REP-BNB,BTC-TUSD,ETH-TUSD,ZEN-BTC,ZEN-ETH,ZEN-BNB,SKY-BTC,SKY-ETH,SKY-BNB,EOS-USDT,EOS-BNB,CVC-BTC,CVC-ETH,CVC-BNB,THETA-BTC,THETA-ETH,THETA-BNB,XRP-BNB,TUSD-USDT,IOTA-USDT,XLM-USDT,IOTX-BTC,IOTX-ETH,QKC-BTC,QKC-ETH,AGI-BTC,AGI-ETH,AGI-BNB,NXS-BTC,NXS-ETH,NXS-BNB,ENJ-BNB,DATA-BTC,DATA-ETH,ONT-USDT,TRX-BNB,TRX-USDT,ETC-USDT,ETC-BNB,ICX-USDT,SC-BTC,SC-ETH,SC-BNB,NPXS-BTC,NPXS-ETH,KEY-BTC,KEY-ETH,NAS-BTC,NAS-ETH,NAS-BNB,MFT-BTC,MFT-ETH,MFT-BNB,DENT-BTC,DENT-ETH,ARDR-BTC,ARDR-ETH,ARDR-BNB,NULS-USDT,HOT-BTC,HOT-ETH,VET-BTC,VET-ETH,VET-USDT,VET-BNB,DOCK-BTC,DOCK-ETH,POLY-BTC,POLY-BNB,HC-BTC,HC-ETH,GO-BTC,GO-BNB,PAX-USDT,RVN-BTC,RVN-BNB,DCR-BTC,DCR-BNB,MITH-BTC,MITH-BNB,BCHABC-BTC,BCHABC-USDT,BNB-PAX,BTC-PAX,ETH-PAX,XRP-PAX,EOS-PAX,XLM-PAX,REN-BTC,REN-BNB,BNB-TUSD,XRP-TUSD,EOS-TUSD,XLM-TUSD,BNB-USDC,BTC-USDC,ETH-USDC,XRP-USDC,EOS-USDC,XLM-USDC,USDC-USDT,ADA-TUSD,TRX-TUSD,NEO-TUSD,TRX-XRP,XZC-XRP,PAX-TUSD,USDC-TUSD,USDC-PAX,LINK-USDT,LINK-TUSD,LINK-PAX,LINK-USDC,WAVES-USDT,WAVES-TUSD,WAVES-PAX,WAVES-USDC,BCHABC-TUSD,BCHABC-PAX,BCHABC-USDC,LTC-TUSD,LTC-PAX,LTC-USDC,TRX-PAX,TRX-USDC,BTT-BTC,BTT-BNB,BTT-USDT,BNB-USDS,BTC-USDS,USDS-USDT,USDS-PAX,USDS-TUSD,USDS-USDC,BTT-PAX,BTT-TUSD,BTT-USDC,ONG-BNB,ONG-BTC,ONG-USDT,HOT-BNB,HOT-USDT,ZIL-USDT,ZRX-BNB,ZRX-USDT,FET-BNB,FET-BTC,FET-USDT,BAT-USDT,XMR-BNB,XMR-USDT,ZEC-BNB,ZEC-USDT,ZEC-PAX,ZEC-TUSD,ZEC-USDC,IOST-BNB,IOST-USDT,CELR-BNB,CELR-BTC,CELR-USDT,ADA-PAX,ADA-USDC,NEO-PAX,NEO-USDC,DASH-BNB,DASH-USDT,NANO-USDT,OMG-BNB,OMG-USDT,THETA-USDT,ENJ-USDT,MITH-USDT,MATIC-BNB,MATIC-BTC,MATIC-USDT,ATOM-BNB,ATOM-BTC,ATOM-USDT,ATOM-USDC,ATOM-PAX,ATOM-TUSD,ETC-USDC,ETC-PAX,ETC-TUSD,BAT-USDC,BAT-PAX,BAT-TUSD,PHB-BNB,PHB-BTC,PHB-USDC,PHB-TUSD,PHB-PAX,TFUEL-BNB,TFUEL-BTC,TFUEL-USDT,TFUEL-USDC,TFUEL-TUSD,TFUEL-PAX,ONE-BNB,ONE-BTC,ONE-USDT,ONE-TUSD,ONE-PAX,ONE-USDC,FTM-BNB,FTM-BTC,FTM-USDT,FTM-TUSD,FTM-PAX,FTM-USDC,BTCB-BTC,BCPT-TUSD,BCPT-PAX,BCPT-USDC,ALGO-BNB,ALGO-BTC,ALGO-USDT,ALGO-TUSD,ALGO-PAX,ALGO-USDC,USDSB-USDT,USDSB-USDS,GTO-USDT,GTO-PAX,GTO-TUSD,GTO-USDC,ERD-BNB,ERD-BTC,ERD-USDT,ERD-PAX,ERD-USDC,DOGE-BNB,DOGE-BTC,DOGE-USDT,DOGE-PAX,DOGE-USDC,DUSK-BNB,DUSK-BTC,DUSK-USDT,DUSK-USDC,DUSK-PAX,BGBP-USDC,ANKR-BNB,ANKR-BTC,ANKR-USDT,ANKR-TUSD,ANKR-PAX,ANKR-USDC,ONT-PAX,ONT-USDC,WIN-BNB,WIN-BTC,WIN-USDT,WIN-USDC,COS-BNB,COS-BTC,COS-USDT,TUSDB-TUSD,NPXS-USDT,NPXS-USDC,COCOS-BNB,COCOS-BTC,COCOS-USDT,MTL-USDT,TOMO-BNB,TOMO-BTC,TOMO-USDT,TOMO-USDC", + "enabledPairs": "BTC-USDT,ETH-USDT,LTC-USDT,ADA-USDT,XRP-USDT", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Bitfinex", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC,BTCEUR,BTCJPY,XRPUSD,XRPBTC,IOTUSD,IOTBTC,IOTETH,EOSUSD,EOSBTC,EOSETH,SANUSD,SANBTC,SANETH,OMGUSD,OMGBTC,OMGETH,NEOUSD,NEOBTC,NEOETH,ETPUSD,ETPBTC,ETPETH,QTMUSD,QTMBTC,QTMETH,AVTUSD,AVTBTC,AVTETH,EDOUSD,EDOBTC,EDOETH,BTGUSD,BTGBTC,DATUSD,DATBTC,DATETH,QSHUSD,QSHBTC,QSHETH,YYWUSD,YYWBTC,YYWETH,GNTUSD,GNTBTC,GNTETH,SNTUSD,SNTBTC,SNTETH,IOTEUR,BATUSD,BATBTC,BATETH,MNAUSD,MNABTC,MNAETH,FUNUSD,FUNBTC,FUNETH,ZRXUSD,ZRXBTC,ZRXETH,TNBUSD,TNBBTC,TNBETH,SPKUSD,SPKBTC,SPKETH,TRXUSD,TRXBTC,TRXETH,RCNUSD,RCNBTC,RCNETH,RLCUSD,RLCBTC,RLCETH,AIDUSD,AIDBTC,AIDETH,SNGUSD,SNGBTC,SNGETH,REPUSD,REPBTC,REPETH,ELFUSD,ELFBTC,ELFETH,NECUSD,NECBTC,NECETH,BTCGBP,ETHEUR,ETHJPY,ETHGBP,NEOEUR,NEOJPY,NEOGBP,EOSEUR,EOSJPY,EOSGBP,IOTJPY,IOTGBP,IOSUSD,IOSBTC,IOSETH,AIOUSD,AIOBTC,AIOETH,REQUSD,REQBTC,REQETH,RDNUSD,RDNBTC,RDNETH,LRCUSD,LRCBTC,LRCETH,WAXUSD,WAXBTC,WAXETH,DAIUSD,DAIBTC,DAIETH,AGIUSD,AGIBTC,AGIETH,BFTUSD,BFTBTC,BFTETH,MTNUSD,MTNBTC,MTNETH,ODEUSD,ODEBTC,ODEETH,ANTUSD,ANTBTC,ANTETH,DTHUSD,DTHBTC,DTHETH,MITUSD,MITBTC,MITETH,STJUSD,STJBTC,STJETH,XLMUSD,XLMEUR,XLMJPY,XLMGBP,XLMBTC,XLMETH,XVGUSD,XVGEUR,XVGJPY,XVGGBP,XVGBTC,XVGETH,BCIUSD,BCIBTC,MKRUSD,MKRBTC,MKRETH,KNCUSD,KNCBTC,KNCETH,POAUSD,POABTC,POAETH,EVTUSD,LYMUSD,LYMBTC,LYMETH,UTKUSD,UTKBTC,UTKETH,VEEUSD,VEEBTC,VEEETH,DADUSD,DADBTC,DADETH,ORSUSD,ORSBTC,ORSETH,AUCUSD,AUCBTC,AUCETH,POYUSD,POYBTC,POYETH,FSNUSD,FSNBTC,FSNETH,CBTUSD,CBTBTC,CBTETH,ZCNUSD,ZCNBTC,ZCNETH,SENUSD,SENBTC,SENETH,NCAUSD,NCABTC,NCAETH,CNDUSD,CNDBTC,CNDETH,CTXUSD,CTXBTC,CTXETH,PAIUSD,PAIBTC,SEEUSD,SEEBTC,SEEETH,ESSUSD,ESSBTC,ESSETH,ATMUSD,ATMBTC,ATMETH,HOTUSD,HOTBTC,HOTETH,DTAUSD,DTABTC,DTAETH,IQXUSD,IQXBTC,IQXEOS,WPRUSD,WPRBTC,WPRETH,ZILUSD,ZILBTC,ZILETH,BNTUSD,BNTBTC,BNTETH,ABSUSD,ABSETH,XRAUSD,XRAETH,MANUSD,MANETH,BBNUSD,BBNETH,NIOUSD,NIOETH,DGXUSD,DGXETH,VETUSD,VETBTC,VETETH,UTNUSD,UTNETH,TKNUSD,TKNETH,GOTUSD,GOTEUR,GOTETH,XTZUSD,XTZBTC,CNNUSD,CNNETH,BOXUSD,BOXETH,TRXEUR,TRXGBP,TRXJPY,MGOUSD,MGOETH,RTEUSD,RTEETH,YGGUSD,YGGETH,MLNUSD,MLNETH,WTCUSD,WTCETH,CSXUSD,CSXETH,OMNUSD,OMNBTC,INTUSD,INTETH,DRNUSD,DRNETH,PNKUSD,PNKETH,DGBUSD,DGBBTC,BSVUSD,BSVBTC,BABUSD,BABBTC,WLOUSD,WLOXLM,VLDUSD,VLDETH,ENJUSD,ENJETH,ONLUSD,ONLETH,RBTUSD,RBTBTC,USTUSD,EUTEUR,EUTUSD,GSDUSD,UDCUSD,TSDUSD,PAXUSD,RIFUSD,RIFBTC,PASUSD,PASETH,VSYUSD,VSYBTC,ZRXDAI,MKRDAI,OMGDAI,BTTUSD,BTTBTC,BTCUST,ETHUST,CLOUSD,CLOBTC,IMPUSD,IMPETH,LTCUST,EOSUST,BABUST,SCRUSD,SCRETH,GNOUSD,GNOETH,GENUSD,GENETH,ATOUSD,ATOBTC,ATOETH,WBTUSD,XCHUSD,EUSUSD,WBTETH,XCHETH,EUSETH,LEOUSD,LEOBTC,LEOUST,LEOEOS,LEOETH,ASTUSD,ASTETH,FOAUSD,FOAETH,UFRUSD,UFRETH,ZBTUSD,ZBTUST,OKBUSD,USKUSD,GTXUSD,KANUSD,OKBUST,OKBETH,OKBBTC,USKUST,USKETH,USKBTC,USKEOS,GTXUST,KANUST,AMPUSD,ALGUSD,ALGBTC,ALGUST,BTCXCH,SWMUSD,SWMETH,TRIUSD,TRIETH,LOOUSD,LOOETH,AMPUST,DUSK:USD,DUSK:BTC,UOSUSD,UOSBTC,RRBUSD,RRBUST,DTXUSD,DTXUST,AMPBTC,FTTUSD,FTTUST,BTCF0:USTF0,ETHF0:USTF0", + "enabledPairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Bitflyer", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BTC_JPY,FXBTC_JPY,ETH_BTC,BCH_BTC", + "enabledPairs": "BTC_JPY,ETH_BTC,BCH_BTC", + "baseCurrencies": "JPY", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": false, + "pairsLastUpdated": 1566798411, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Bithumb", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "clientId": "ClientID", + "availablePairs": "VETKRW,REPKRW,ARNKRW,OCNKRW,ETHOSKRW,STEEMKRW,LRCKRW,ETCKRW,CMTKRW,HDACKRW,WTCKRW,PLYKRW,QTUMKRW,MCOKRW,NPXSKRW,ABTKRW,BSVKRW,SNTKRW,STRATKRW,BATKRW,ETHKRW,CTXCKRW,AUTOKRW,HYCKRW,POLYKRW,QKCKRW,TMTGKRW,BCHKRW,MXCKRW,XEMKRW,GTOKRW,BTTKRW,APISKRW,DACKRW,ELFKRW,XLMKRW,DACCKRW,GNTKRW,EOSKRW,TRXKRW,BZNTKRW,ETZKRW,XRPKRW,WAVESKRW,WETKRW,HCKRW,XMRKRW,PPTKRW,LOOMKRW,KNCKRW,MIXKRW,RDNKRW,ADAKRW,ENJKRW,ZRXKRW,DASHKRW,PIVXKRW,THETAKRW,VALORKRW,BHPKRW,OMGKRW,RNTKRW,GXCKRW,AMOKRW,CROKRW,LAMBKRW,LINKKRW,ROMKRW,ZILKRW,ORBSKRW,POWRKRW,INSKRW,CONKRW,XVGKRW,BCDKRW,ICXKRW,BTCKRW,BTGKRW,LBAKRW,MTLKRW,MITHKRW,PAYKRW,WAXKRW,ANKRKRW,IOSTKRW,AEKRW,LTCKRW,ITCKRW,SALTKRW,ZECKRW,TRUEKRW,PSTKRW", + "enabledPairs": "BTCKRW,ETHKRW,DASHKRW,LTCKRW,ETCKRW,XRPKRW,BCHKRW,XMRKRW,ZECKRW,QTUMKRW,BTGKRW,EOSKRW", + "baseCurrencies": "KRW", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "index": "KRW" + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Bitmex", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "XRPU19,BCHU19,ADAU19,EOSU19,TRXU19,XBTUSD,XBT7D_U105,XBT7D_D95,XBTU19,XBTZ19,ETHUSD,ETHU19,LTCU19", + "enabledPairs": "XBTUSD", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Bitstamp", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "clientId": "ClientID", + "availablePairs": "LTCUSD,ETHUSD,XRPEUR,BCHUSD,BCHEUR,BTCEUR,XRPBTC,EURUSD,BCHBTC,LTCEUR,BTCUSD,LTCBTC,XRPUSD,ETHBTC,ETHEUR", + "enabledPairs": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", + "baseCurrencies": "USD,EUR", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Bittrex", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BTC-LTC,BTC-DOGE,BTC-VTC,BTC-PPC,BTC-FTC,BTC-RDD,BTC-NXT,BTC-DASH,BTC-POT,BTC-BLK,BTC-EMC2,BTC-XMY,BTC-GLC,BTC-GRS,BTC-NLG,BTC-MONA,BTC-VRC,BTC-CURE,BTC-XMR,BTC-XDN,BTC-NAV,BTC-XST,BTC-VIA,BTC-PINK,BTC-IOC,BTC-SYS,BTC-DGB,BTC-BURST,BTC-EXCL,BTC-BLOCK,BTC-BTS,BTC-XRP,BTC-GAME,BTC-NXS,BTC-GEO,BTC-FLDC,BTC-FLO,BTC-MUE,BTC-XEM,BTC-SPHR,BTC-OK,BTC-AEON,BTC-ETH,BTC-EXP,BTC-AMP,BTC-XLM,USDT-BTC,BTC-RVR,BTC-FCT,BTC-MAID,BTC-SLS,BTC-RADS,BTC-DCR,BTC-XVG,BTC-PIVX,BTC-MEME,BTC-STEEM,BTC-LSK,BTC-DGD,BTC-WAVES,BTC-LBC,BTC-SBD,BTC-ETC,ETH-ETC,BTC-STRAT,BTC-REP,BTC-SHIFT,BTC-ARDR,BTC-XZC,BTC-NEO,BTC-ZEC,BTC-IOP,BTC-UBQ,BTC-KMD,BTC-SIB,BTC-ION,BTC-CRW,BTC-SWT,BTC-MLN,BTC-ARK,BTC-INCNT,BTC-GBYTE,BTC-GNT,BTC-EDG,BTC-MORE,ETH-GNT,ETH-REP,USDT-ETH,BTC-WINGS,BTC-RLC,BTC-GNO,BTC-GUP,ETH-GNO,BTC-HMQ,BTC-ANT,ETH-ANT,BTC-SC,ETH-BAT,BTC-BAT,BTC-ZEN,BTC-1ST,BTC-QRL,BTC-PTOY,BTC-BNT,ETH-BNT,BTC-NMR,ETH-LTC,ETH-XRP,BTC-SNT,ETH-SNT,BTC-DCT,BTC-XEL,BTC-MCO,ETH-MCO,BTC-ADT,BTC-FUN,BTC-PAY,ETH-PAY,BTC-MTL,BTC-STORJ,BTC-ADX,ETH-ADX,ETH-DASH,ETH-SC,ETH-ZEC,USDT-ZEC,USDT-LTC,USDT-ETC,USDT-XRP,BTC-OMG,ETH-OMG,BTC-CVC,ETH-CVC,BTC-PART,BTC-QTUM,ETH-QTUM,ETH-XMR,ETH-XEM,ETH-XLM,ETH-NEO,USDT-XMR,USDT-DASH,ETH-BCH,USDT-BCH,BTC-BCH,BTC-DNT,USDT-NEO,ETH-WAVES,ETH-STRAT,ETH-DGB,USDT-OMG,BTC-ADA,BTC-MANA,ETH-MANA,BTC-RCN,BTC-VIB,ETH-VIB,BTC-MER,BTC-POWR,ETH-POWR,ETH-ADA,BTC-ENG,ETH-ENG,USDT-ADA,USDT-XVG,USDT-NXT,BTC-UKG,ETH-UKG,BTC-IGNIS,BTC-SRN,ETH-SRN,BTC-WAX,ETH-WAX,BTC-ZRX,ETH-ZRX,BTC-VEE,BTC-BCPT,BTC-TRX,ETH-TRX,BTC-TUSD,BTC-LRC,ETH-TUSD,BTC-UP,BTC-DMT,ETH-DMT,USDT-TUSD,BTC-POLY,ETH-POLY,BTC-PRO,USDT-SC,USDT-TRX,BTC-BLT,BTC-STORM,ETH-STORM,BTC-AID,BTC-NGC,BTC-GTO,USDT-DCR,BTC-OCN,ETH-OCN,USD-BTC,USD-USDT,USD-TUSD,BTC-TUBE,BTC-CMCT,USD-ETH,BTC-NLC2,BTC-BKX,BTC-MFT,BTC-LOOM,BTC-RFR,USDT-DGB,BTC-RVN,USD-XRP,USD-ETC,BTC-BFT,BTC-GO,BTC-HYDRO,BTC-UPP,USD-ADA,USD-ZEC,USDT-DOGE,BTC-ENJ,BTC-MET,USD-LTC,USD-TRX,BTC-DTA,BTC-EDR,BTC-BOXX,BTC-IHT,USD-BCH,BTC-XHV,USDT-ZRX,BTC-NPXS,BTC-PMA,USDT-BAT,USDT-RVN,BTC-PAL,USD-SC,BTC-PAX,USDT-PAX,BTC-ZIL,BTC-MOC,BTC-OST,BTC-SPC,BTC-MEDX,BTC-BSV,BTC-IOST,BTC-XNK,USDT-BSV,ETH-BSV,BTC-NCASH,BTC-SOLVE,BTC-USDS,USDT-PMA,ETH-NPXS,USDT-NPXS,USD-ZRX,BTC-JNT,BTC-LBA,BTC-MOBI,USD-BAT,USD-BSV,BTC-DENT,USD-USDS,BTC-DRGN,USD-PAX,BTC-VITE,BTC-IOTX,USD-DGB,BTC-BTM,BTC-ELF,USD-EDR,BTC-QNT,BTC-BTU,USD-ZEN,BTC-SPND,BTC-BTT,BTC-NKN,USD-KMD,USDT-BTT,BTC-GRIN,BTC-CTXC,BTC-HXRO,BTC-META,USDT-GRIN,BTC-FSN,BTC-HST,BTC-ANKR,USDT-XLM,BTC-TRAC,BTC-CRO,BTC-ONT,ETH-SOLVE,BTC-ONG,BTC-AERGO,BTC-TTC,USD-SPND,BTC-SLT,BTC-PTON,BTC-PI,ETH-ANKR,BTC-PLA,BTC-ART,BTC-ORBS,USDT-ENJ,BTC-VBK,BTC-BORA,BTC-CND,USDT-ONT,BTC-TRIO,BTC-FX,ETH-FX,BTC-ATOM,USDT-ATOM,ETH-ATOM,BTC-XYO,BTC-OCEAN,USDT-OCEAN,BTC-WIB,BTC-BWX,BTC-SNX,BTC-SUSD,BTC-VDX,USDT-VDX,ETH-VDX,BTC-COSM,BTC-OGO,USDT-OGO,BTC-ITM,BTC-LAMB,BTC-STPT,BTC-FET,BTC-MKR,ETH-MKR,BTC-DAI,ETH-DAI,USDT-DAI,BTC-CPT,BTC-ABT,BTC-PROM,BTC-FTM,BTC-ABYSS,BTC-EOS,ETH-EOS,USDT-EOS,BTC-FXC,BTC-DUSK,BTC-URAC,BTC-BLOC,BTC-BRZ,BTC-TEMCO,BTC-SPIN,BTC-HINT,BTC-LUNA,BTC-CHR,BTC-TUDA,BTC-UTK,BTC-PXL,BTC-AKRO,BTC-TSHP", + "enabledPairs": "USDT-BTC", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "requestCurrencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "BTSE", + "enabled": true, + "verbose": false, + "websocket": true, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BTC-CNY,BTC-EUR,BTC-GBP,BTC-HKD,BTC-JPY,BTC-SGD,BTC-USD,ETH-CNY,ETH-EUR,ETH-GBP,ETH-HKD,ETH-JPY,ETH-SGD,ETH-USD,LTC-CNY,LTC-EUR,LTC-GBP,LTC-HKD,LTC-JPY,LTC-SGD,LTC-USD,USDT-CNY,USDT-EUR,USDT-GBP,USDT-HKD,USDT-JPY,USDT-SGD,USDT-USD", + "enabledPairs": "BTC-USD", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "requestCurrencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "BTC Markets", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BTC-AUD,LTC-AUD,LTC-BTC,ETH-BTC,ETH-AUD,ETC-AUD,ETC-BTC,XRP-AUD,XRP-BTC,POWR-AUD,POWR-BTC,OMG-AUD,OMG-BTC,BCHABC-AUD,BCHABC-BTC,BCHSV-AUD,BCHSV-BTC,GNT-AUD,GNT-BTC,BAT-AUD,BAT-BTC,XLM-AUD,XLM-BTC", + "enabledPairs": "BTC-AUD", + "baseCurrencies": "AUD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "COINUT", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "clientId": "ClientID", + "availablePairs": "BTCUSDT,ETCBTC,ETHUSDT,BTCCAD,ETHLTC,ETHUSD,LTCUSD,BTCSGD,ETCSGD,ETHSGD,ZECBTC,ZECUSD,ZECUSDT,ETHBTC,LTCBTC,USDTSGD,USDTUSD,XMRUSDT,BTCUSD,LTCCAD,LTCSGD,LTCUSDT,XMRBTC,XMRLTC,ZECLTC,ETCUSDT,ZECSGD,ETCLTC,ETHCAD,ZECCAD", + "enabledPairs": "LTCBTC,ETCBTC,ETHBTC", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "EXMO", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "WAVES_BTC,BTC_RUB,DCR_UAH,XMR_UAH,USDC_BTC,XEM_USD,XLM_RUB,ATMCASH_BTC,QTUM_USD,ADA_USD,TRX_BTC,XRP_BTC,MKR_DAI,STQ_USD,ETH_USD,KICK_USDT,ZRX_USD,USDC_ETH,GUSD_BTC,ZRX_ETH,DASH_BTC,ETC_BTC,LTC_RUB,BTC_USD,STQ_EUR,BCH_RUB,XRP_USDT,WAVES_ETH,XTZ_ETH,QTUM_BTC,XEM_BTC,LSK_BTC,TRX_RUB,ETH_PLN,PTI_USDT,MNC_ETH,DAI_BTC,NEO_USD,KICK_BTC,ETH_BTC,ZEC_BTC,ETZ_USDT,DAI_ETH,DAI_USD,GNT_ETH,HBZ_USD,DXT_BTC,XRP_TRY,DAI_RUB,MNX_BTC,BCH_ETH,WAVES_USD,TRX_USD,INK_ETH,XLM_BTC,XMR_USD,KICK_ETH,DASH_RUB,LTC_BTC,USDT_RUB,USDT_EUR,DOGE_USD,DASH_UAH,XTZ_USD,ETZ_ETH,HB_BTC,GUSD_RUB,BTC_TRY,ADA_BTC,ADA_ETH,BTG_BTC,BCH_USDT,USDT_UAH,PTI_RUB,XTZ_RUB,DASH_USD,LTC_USD,ETH_USDT,MNC_BTC,XEM_EUR,GUSD_USD,XMR_BTC,XRP_EUR,SMART_USD,HBZ_BTC,BCH_USD,ETH_RUB,XRP_ETH,ZEC_RUB,XRP_RUB,DCR_BTC,DCR_RUB,PTI_EOS,EOS_USD,DXT_USD,ETH_LTC,BTC_USDT,USDT_USD,DASH_USDT,BTG_ETH,BCH_UAH,ROOBEE_ETH,TRX_UAH,MNC_USD,QTUM_ETH,BTCZ_BTC,XRP_UAH,USDC_USDT,NEO_BTC,OMG_ETH,STQ_BTC,ETC_USD,XMR_EUR,EOS_EUR,BTC_PLN,NEO_RUB,ZRX_BTC,INK_BTC,MNX_ETH,ETH_UAH,LSK_RUB,BCH_BTC,ETH_EUR,XLM_USD,ETC_RUB,DOGE_BTC,EXM_BTC,ROOBEE_BTC,LSK_USD,HBZ_ETH,LTC_EUR,USD_RUB,KICK_RUB,USDC_USD,PTI_BTC,OMG_USD,XRP_USD,XEM_UAH,GNT_BTC,LTC_UAH,SMART_BTC,SMART_EUR,SMART_RUB,BTG_USD,GAS_USD,BTC_UAH,XTZ_BTC,ZEC_USD,MKR_BTC,INK_USD,EOS_BTC,STQ_RUB,ZEC_EUR,XMR_ETH,BTC_EUR,XMR_RUB,XLM_TRY,GAS_BTC,MNX_USD,WAVES_RUB,ETZ_BTC,ETH_TRY,OMG_BTC,BCH_EUR", + "enabledPairs": "BTC_USD,LTC_USD", + "baseCurrencies": "USD,EUR,RUB,PLN,UAH", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_", + "separator": "," + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "CoinbasePro", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "clientId": "ClientID", + "availablePairs": "EOSUSD,ETHBTC,ETHUSDC,ETHEUR,ZECUSDC,REPUSD,LIN-ETH,EOSBTC,LTCGBP,CVCUSDC,XLMEUR,ETCGBP,XTZBTC,XRPUSD,XRPBTC,ALG-USD,BTCUSDC,GNTUSDC,ZRXBTC,DNTUSDC,BTCUSD,LTCBTC,LTCUSD,ETHGBP,ZRXUSD,BATETH,ZRXEUR,REPBTC,ETCEUR,XRPEUR,EOSEUR,BCHEUR,MAN-USDC,XLMUSD,BATUSDC,LOO-USDC,BTCEUR,BCHGBP,LTCEUR,BCHBTC,LIN-USD,DAIUSDC,XTZUSD,ETCBTC,BCHUSD,BTCGBP,ETHUSD,XLMBTC,ETCUSD,ZECBTC,ETHDAI", + "enabledPairs": "BTCUSD,BTCGBP,BTCEUR", + "baseCurrencies": "USD,GBP,EUR", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true + }, + "requestCurrencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Coinbene", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "clientId": "ClientID", + "availablePairs": "ABBC/BTC,ABT/ETH,ABT/USDT,ABYSS/ETH,ACDC/BTC,ACDC/USDT,ADI/ETH,ADK/BTC,ADN/BTC,AE/BTC,AE/USDT,AID/BTC,AIDOC/BTC,AION/BTC,AIPE/USDT,AIT/USDT,ALGO/USDT,ALI/ETH,ALX/ETH,APL/ETH,ATX/BTC,B2G/BTC,B91/USDT,BAAS/BTC,BAT/BTC,BCHABC/USDT,BCHSV/USDT,BEAUTY/ETH,BETHER/ETH,BEZ/BTC,BGC/USDT,BKG/BTC,BNT/BTC,BOA/USDT,BSTN/ETH,BTC/USDT,BTFM/USDT,BTNT/BTC,BTSC/BTC,BTT/USDT,BU/ETH,BVT/ETH,C3W/ETH,CAN/ETH,CCC/ETH,CCE/USDT,CC/USDT,CEDEX/ETH,CENT/BTC,CFT/USDT,CLO/BTC,CMT/ETH,CMT/USDT,CNN/BTC,CNN/ETH,CNN/USDT,CONI/USDT,COSM/BTC,COSM/ETH,COZP/BTC,CPC/BTC,CPMS/USDT,CREDO/ETH,CRN/BTC,CS/ETH,CS/USDT,CTXC/ETH,CUST/USDT,CVC/BTC,CXC/USDT,CXP/BTC,DCA/ETH,DCT/BTC,DENT/BTC,DGD/BTC,DOCK/ETH,DSCB/USDT,DTA/ETH,DUC/BTC,DVC/ETH,EBC/BTC,EBC/ETH,EBC/USDT,ECA/BTC,EDC/BTC,EDR/ETH,ELF/BTC,EMT/USDT,EOS/BTC,EOS/USDT,EQUAD/BTC,ETC/BTC,ETC/USDT,ETH/BTC,ETH/USDT,ETK/BTC,ETN/BTC,FAB/ETH,FACC/ETH,FCC/BTC,FDS/USDT,FND/ETH,FNKOS/ETH,FTN/BTC,FTN/USDT,FTT/BTC,FXT/ETH,GETX/ETH,GLDR/ETH,GMTK/ETH,GOM/USDT,GRAM/USDT,GRIN/BTC,GRN/BTC,GSTT/USDT,GUSD/USDT,GVT/BTC,HAPPY/BTC,HDAC/BTC,HMB/USDT,HNB/USDT,HPT/ETH,HUP/USDT,INCX/ETH,IOST/BTC,IOTE/USDT,ISR/BTC,ISR/ETH,IVY/ETH,JOB/BTC,KBC/BTC,KBC/USDT,KMD/BTC,KNT/ETH,KST/BTC,KUE/BTC,KUE/ETH,KUKY/BTC,LAMB/USDT,LATX/BTC,LBK/BTC,LINK/BTC,LOOM/BTC,LTC/BTC,LTC/USDT,LUC/ETH,LUX/BTC,LVTC/ETH,MDC/USDT,MGC/USDT,MIB/BTC,MINX/BTC,MINX/ETH,MOAC/USDT,MPL/BTC,MTC/BTC,MT/ETH,MTN/ETH,MT/USDT,MVL/ETH,MVPT/ETH,MWT/USDT,NANO/BTC,NBAI/ETH,NCASH/BTC,NEO/BTC,NEO/USDT,NOBS/BTC,NPXS/ETH,NPXS/USDT,NTY/ETH,ODC/USDT,OMG/BTC,OMX/ETH,OVC/ETH,OZX/ETH,PAL/ETH,PAT/ETH,PAX/USDT,PKX/BTC,PLAY/BTC,PMA/ETH,POLL/BTC,POLY/BTC,PPT/BTC,PSM/BTC,QKC/BTC,QTUM/BTC,QTUM/USDT,RBG/BTC,RBG/ETH,RBG/USDT,RBTC/BTC,RBZ/USDT,RCOIN/BTC,RCOIN/USDT,REP/BTC,REV/BTC,RIF/BTC,SALT/BTC,SCC/BTC,SCO/BTC,SEN/BTC,SENC/ETH,SHE/BTC,SHVR/BTC,SIM/BTC,SKB/BTC,SKM/ETH,SKYM/USDT,SLT/ETH,SMARTUP/ETH,SMARTUP/USDT,SMART/USDT,SORO/USDT,SRCOIN/BTC,SRCOIN/ETH,STORJ/BTC,STQ/BTC,SWET/BTC,SWTC/USDT,TCT/BTC,TEMCO/USDT,TEN/BTC,TEN/ETH,THM/ETH,TIB/BTC,TIMO/USDT,TMTG/BTC,TOC/ETH,TOSC/BTC,TRUE/ETH,TRX/BTC,TRX/USDT,TSL/BTC,TVB/USDT,UTNP/BTC,VBT/USDT,VEEN/BTC,VME/BTC,VME/ETH,VOLLAR/USDT,VSC/ETH,W12/BTC,W12/ETH,WBL/BTC,WFX/BTC,XEM/BTC,XLM/BTC,XMCT/ETH,XMCT/USDT,XMR/BTC,XNK/ETH,XRP/BTC,XRP/USDT,XSR/USDT,YTA/USDT,ZAT/ETH,ZDC/BTC,ZEC/BTC,ZGC/BTC,ZRX/BTC", + "enabledPairs": "BTC/USDT", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "/" + }, + "requestCurrencyPairFormat": { + "uppercase": true, + "delimiter": "/" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "GateIO", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "USDT_CNYX,BTC_CNYX,ETH_CNYX,EOS_CNYX,BCH_CNYX,XRP_CNYX,DOGE_CNYX,TIPS_CNYX,BTC_USDC,BTC_PAX,BTC_USDT,BCH_USDT,ETH_USDT,ETC_USDT,QTUM_USDT,LTC_USDT,DASH_USDT,ZEC_USDT,BTM_USDT,EOS_USDT,REQ_USDT,SNT_USDT,OMG_USDT,PAY_USDT,CVC_USDT,ZRX_USDT,TNT_USDT,XMR_USDT,XRP_USDT,DOGE_USDT,BAT_USDT,PST_USDT,BTG_USDT,DPY_USDT,LRC_USDT,STORJ_USDT,RDN_USDT,STX_USDT,KNC_USDT,LINK_USDT,CDT_USDT,AE_USDT,AE_ETH,AE_BTC,CDT_ETH,RDN_ETH,STX_ETH,KNC_ETH,LINK_ETH,REQ_ETH,RCN_ETH,TRX_ETH,ARN_ETH,KICK_ETH,BNT_ETH,VET_ETH,MCO_ETH,FUN_ETH,DATA_ETH,RLC_ETH,RLC_USDT,ZSC_ETH,WINGS_ETH,MDA_ETH,RCN_USDT,TRX_USDT,KICK_USDT,VET_USDT,MCO_USDT,FUN_USDT,DATA_USDT,ZSC_USDT,MDA_USDT,XTZ_USDT,XTZ_BTC,XTZ_ETH,GNT_USDT,GNT_ETH,GEM_USDT,GEM_ETH,RFR_USDT,RFR_ETH,DADI_USDT,DADI_ETH,ABT_USDT,ABT_ETH,LEDU_BTC,LEDU_ETH,OST_USDT,OST_ETH,XLM_USDT,XLM_ETH,XLM_BTC,MOBI_USDT,MOBI_ETH,MOBI_BTC,OCN_USDT,OCN_ETH,OCN_BTC,ZPT_USDT,ZPT_ETH,ZPT_BTC,COFI_USDT,COFI_ETH,JNT_USDT,JNT_ETH,JNT_BTC,BLZ_USDT,BLZ_ETH,GXS_USDT,GXS_BTC,MTN_USDT,MTN_ETH,RUFF_USDT,RUFF_ETH,RUFF_BTC,TNC_USDT,TNC_ETH,TNC_BTC,ZIL_USDT,ZIL_ETH,BTO_USDT,BTO_ETH,THETA_USDT,THETA_ETH,DDD_USDT,DDD_ETH,DDD_BTC,MKR_USDT,MKR_ETH,DAI_USDT,SMT_USDT,SMT_ETH,MDT_USDT,MDT_ETH,MDT_BTC,MANA_USDT,MANA_ETH,LUN_USDT,LUN_ETH,SALT_USDT,SALT_ETH,FUEL_USDT,FUEL_ETH,ELF_USDT,ELF_ETH,DRGN_USDT,DRGN_ETH,GTC_USDT,GTC_ETH,GTC_BTC,QLC_USDT,QLC_BTC,QLC_ETH,DBC_USDT,DBC_BTC,DBC_ETH,BNTY_USDT,BNTY_ETH,LEND_USDT,LEND_ETH,ICX_USDT,ICX_ETH,BTF_USDT,BTF_BTC,ADA_USDT,ADA_BTC,LSK_USDT,LSK_BTC,WAVES_USDT,WAVES_BTC,BIFI_USDT,BIFI_BTC,MDS_ETH,MDS_USDT,DGD_USDT,DGD_ETH,QASH_USDT,QASH_ETH,QASH_BTC,POWR_USDT,POWR_ETH,POWR_BTC,FIL_USDT,BCD_USDT,BCD_BTC,SBTC_USDT,SBTC_BTC,GOD_USDT,GOD_BTC,BCX_USDT,BCX_BTC,QSP_USDT,QSP_ETH,INK_BTC,INK_USDT,INK_ETH,INK_QTUM,MED_QTUM,MED_ETH,MED_USDT,QBT_QTUM,QBT_ETH,QBT_USDT,TSL_QTUM,TSL_USDT,GNX_USDT,GNX_ETH,NEO_USDT,GAS_USDT,NEO_BTC,GAS_BTC,IOTA_USDT,IOTA_BTC,NAS_USDT,NAS_ETH,NAS_BTC,ETH_BTC,ETC_BTC,ETC_ETH,ZEC_BTC,DASH_BTC,LTC_BTC,BCH_BTC,BTG_BTC,QTUM_BTC,QTUM_ETH,XRP_BTC,DOGE_BTC,XMR_BTC,ZRX_BTC,ZRX_ETH,DNT_ETH,DPY_ETH,OAX_BTC,OAX_USDT,OAX_ETH,REP_ETH,LRC_ETH,LRC_BTC,PST_ETH,BCDN_ETH,BCDN_USDT,TNT_ETH,SNT_ETH,SNT_BTC,BTM_ETH,BTM_BTC,SNET_ETH,SNET_USDT,LLT_SNET,OMG_ETH,OMG_BTC,PAY_ETH,PAY_BTC,BAT_ETH,BAT_BTC,CVC_ETH,STORJ_ETH,STORJ_BTC,EOS_ETH,EOS_BTC,BTS_USDT,BTS_BTC,TIPS_ETH,GT_BTC,GT_USDT,ATOM_BTC,ATOM_USDT,XEM_ETH,XEM_USDT,XEM_BTC,BU_USDT,BU_ETH,BU_BTC,BCHSV_USDT,BCHSV_CNYX,BCHSV_BTC,DCR_USDT,DCR_BTC,BCN_USDT,BCN_BTC,XMC_USDT,XMC_BTC,ATP_USDT,ATP_ETH,NBOT_ETH,NBOT_USDT,MEDX_USDT,MEDX_ETH,GRIN_USDT,GRIN_ETH,GRIN_BTC,BEAM_USDT,BEAM_ETH,BEAM_BTC,VTHO_ETH,BTT_USDT,BTT_ETH,BTT_TRX,TFUEL_ETH,TFUEL_USDT,CELR_ETH,CELR_USDT,CS_ETH,CS_USDT,MAN_ETH,MAN_USDT,REM_ETH,REM_USDT,LYM_ETH,LYM_BTC,LYM_USDT,ONG_ETH,ONG_USDT,ONT_ETH,ONT_USDT,BFT_ETH,BFT_USDT,IHT_ETH,IHT_USDT,SENC_ETH,SENC_USDT,TOMO_ETH,TOMO_USDT,ELEC_ETH,ELEC_USDT,HAV_ETH,HAV_USDT,SWTH_ETH,SWTH_USDT,NKN_ETH,NKN_USDT,SOUL_ETH,SOUL_USDT,LRN_ETH,LRN_USDT,EOSDAC_ETH,EOSDAC_USDT,DOCK_USDT,DOCK_ETH,GSE_USDT,GSE_ETH,RATING_USDT,RATING_ETH,HSC_USDT,HSC_ETH,HIT_USDT,HIT_ETH,DX_USDT,DX_ETH,CNNS_ETH,CNNS_USDT,DREP_ETH,DREP_USDT,MBL_USDT,MBL_ETH,GMAT_USDT,GMAT_ETH,MIX_USDT,MIX_ETH,LAMB_USDT,LAMB_ETH,LEO_USDT,LEO_BTC,WICC_USDT,WICC_ETH,SERO_USDT,SERO_ETH,VIDY_USDT,VIDY_ETH,KGC_USDT,FTM_USDT,FTM_ETH,ONE_USDT,ARPA_USDT,ARPA_ETH,ALGO_USDT,BKC_USDT,BXC_USDT,BXC_ETH,PAX_USDT,PAX_CNYX,USDC_CNYX,USDC_USDT,TUSD_CNYX,TUSD_USDT,HC_USDT,HC_BTC,HC_ETH,GARD_USDT,GARD_ETH,FTI_USDT,FTI_ETH,SOP_ETH,SOP_USDT,LEMO_USDT,LEMO_ETH,QKC_USDT,QKC_ETH,IOTX_USDT,IOTX_ETH,RED_USDT,RED_ETH,LBA_USDT,LBA_ETH,OPEN_USDT,OPEN_ETH,MITH_USDT,MITH_ETH,SKM_USDT,SKM_ETH,XVG_USDT,XVG_BTC,NANO_USDT,NANO_BTC,HT_USDT,BNB_USDT,MET_ETH,MET_USDT,TCT_ETH,TCT_USDT,MXC_USDT,MXC_BTC,MXC_ETH", + "enabledPairs": "BTC_USDT", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": false, + "delimiter": "_" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Gemini", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BTCUSD,ETHBTC,ETHUSD,BCHUSD,BCHBTC,BCHETH,LTCUSD,LTCBTC,LTCETH,LTCBCH,ZECUSD,ZECBTC,ZECETH,ZECBCH,ZECLTC", + "enabledPairs": "BTCUSD", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "HitBTC", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BCN-BTC,BTC-USD,DASH-BTC,DOGE-BTC,DOGE-USD,EMC-BTC,ETH-BTC,LSK-BTC,LTC-BTC,LTC-USD,NXT-BTC,SBD-BTC,SC-BTC,STEEM-BTC,XDN-BTC,XEM-BTC,XMR-BTC,ARDR-BTC,ZEC-BTC,WAVES-BTC,MAID-BTC,AMP-BTC,DGD-BTC,SNGLS-BTC,1ST-BTC,TRST-BTC,TIME-BTC,GNO-BTC,REP-BTC,XMR-USD,DASH-USD,ETH-USD,NXT-USD,ZRC-BTC,BOS-BTC,DCT-BTC,ANT-BTC,AEON-BTC,GUP-BTC,PLU-BTC,LUN-BTC,EDG-BTC,RLC-BTC,SWT-BTC,TKN-BTC,WINGS-BTC,XAUR-BTC,AE-BTC,PTOY-BTC,ZEC-USD,XEM-USD,BCN-USD,XDN-USD,MAID-USD,ETC-BTC,ETC-USD,PLBT-BTC,BNT-BTC,SNT-ETH,CVC-USD,PAY-ETH,OAX-ETH,OMG-ETH,BQX-ETH,XTZ-BTC,DICE-BTC,PTOY-ETH,1ST-ETH,XAUR-ETH,TIME-ETH,DICE-ETH,SWT-ETH,XMR-ETH,ETC-ETH,DASH-ETH,ZEC-ETH,PLU-ETH,GNO-ETH,XRP-BTC,STRAT-USD,STRAT-BTC,SNC-ETH,ADX-ETH,BET-ETH,EOS-ETH,DENT-ETH,SAN-ETH,EOS-BTC,EOS-USD,XTZ-ETH,XTZ-USD,MYB-ETH,SUR-ETH,IXT-ETH,PLR-ETH,TIX-ETH,PRO-ETH,AVT-ETH,EVX-USD,DLT-BTC,BNT-ETH,BNT-USD,MANA-USD,DNT-BTC,FYP-BTC,OPT-BTC,TNT-ETH,STX-BTC,STX-ETH,STX-USD,TNT-USD,TNT-BTC,ENG-ETH,XUC-USD,SNC-BTC,SNC-USD,OAX-USD,OAX-BTC,ZRX-BTC,ZRX-ETH,ZRX-USD,RVT-BTC,PPC-BTC,PPC-USD,QTUM-ETH,IGNIS-ETH,BMC-BTC,BMC-ETH,BMC-USD,CND-BTC,CND-ETH,CND-USD,CDT-ETH,CDT-USD,FUN-BTC,FUN-ETH,FUN-USD,HVN-BTC,HVN-ETH,POE-BTC,POE-ETH,AMB-USD,AMB-ETH,AMB-BTC,HPC-BTC,PPT-ETH,MTH-BTC,MTH-ETH,LRC-BTC,LRC-ETH,ICX-BTC,ICX-ETH,NEO-BTC,NEO-ETH,NEO-USD,CSNO-BTC,ICX-USD,PIX-BTC,PIX-ETH,IND-ETH,KICK-BTC,YOYOW-BTC,CDT-BTC,XVG-BTC,XVG-ETH,XVG-USD,DGB-BTC,DGB-ETH,DGB-USD,DCN-BTC,DCN-ETH,DCN-USD,VIBE-BTC,ENJ-BTC,ENJ-ETH,ENJ-USD,ZSC-BTC,ZSC-ETH,ZSC-USD,TRX-BTC,TRX-ETH,TRX-USD,ART-BTC,EVX-BTC,EVX-ETH,SUB-BTC,SUB-ETH,SUB-USD,WTC-BTC,BTM-BTC,BTM-ETH,BTM-USD,LIFE-BTC,VIB-BTC,VIB-ETH,VIB-USD,DRT-ETH,STU-USD,OMG-BTC,PAY-BTC,PPT-BTC,SNT-BTC,BTG-BTC,BTG-ETH,BTG-USD,SMART-BTC,SMART-ETH,SMART-USD,XUC-ETH,XUC-BTC,LA-ETH,EDO-BTC,EDO-ETH,EDO-USD,HGT-ETH,IXT-BTC,SCL-BTC,ETP-BTC,ETP-ETH,ETP-USD,DRPU-BTC,NEBL-BTC,NEBL-ETH,ARN-BTC,ARN-ETH,STU-BTC,STU-ETH,GVT-ETH,BTX-BTC,LTC-ETH,BCN-ETH,MAID-ETH,NXT-ETH,STRAT-ETH,XDN-ETH,XEM-ETH,PLR-BTC,SUR-BTC,BQX-BTC,DOGE-ETH,AMM-BTC,AMM-ETH,AMM-USD,DBIX-BTC,PRE-BTC,ZAP-BTC,DOV-BTC,DOV-ETH,DRPU-ETH,XRP-ETH,XRP-USD,HSR-BTC,LEND-BTC,LEND-ETH,SPF-ETH,SBTC-BTC,SBTC-ETH,LOC-BTC,LOC-ETH,LOC-USD,SWFTC-BTC,SWFTC-ETH,SWFTC-USD,STAR-ETH,SBTC-USD,STORM-BTC,DIM-ETH,DIM-USD,DIM-BTC,NGC-BTC,NGC-ETH,NGC-USD,EMC-ETH,EMC-USD,MCO-BTC,MCO-ETH,MCO-USD,MANA-ETH,MANA-BTC,CPAY-ETH,DATA-BTC,DATA-ETH,DATA-USD,UTT-BTC,UTT-ETH,UTT-USD,KMD-BTC,KMD-ETH,KMD-USD,QTUM-USD,QTUM-BTC,SNT-USD,OMG-USD,EKO-BTC,EKO-ETH,ADX-BTC,ADX-USD,LSK-ETH,LSK-USD,PLR-USD,SUR-USD,BQX-USD,DRT-USD,REP-ETH,REP-USD,WAX-BTC,WAX-ETH,WAX-USD,C20-BTC,C20-ETH,IDH-BTC,IDH-ETH,IPL-BTC,COV-BTC,COV-ETH,SENT-BTC,SENT-ETH,SENT-USD,SMT-BTC,SMT-ETH,SMT-USD,CHAT-BTC,CHAT-ETH,CHAT-USD,TRAC-ETH,JNT-ETH,UTK-BTC,UTK-ETH,UTK-USD,GNX-ETH,CHSB-BTC,CHSB-ETH,DAY-BTC,DAY-ETH,DAY-USD,NEU-BTC,NEU-ETH,NEU-USD,TAU-BTC,FLP-BTC,FLP-ETH,FLP-USD,R-BTC,R-ETH,EKO-USD,BCPT-ETH,BCPT-USD,PKT-BTC,PKT-ETH,BETR-BTC,BETR-ETH,HAND-ETH,HAND-USD,CHP-ETH,BCPT-BTC,ACT-BTC,ACT-ETH,ACT-USD,ADA-BTC,ADA-ETH,ADA-USD,MTX-BTC,MTX-ETH,MTX-USD,WIZ-BTC,WIZ-ETH,WIZ-USD,DADI-BTC,DADI-ETH,BDG-ETH,DATX-BTC,DATX-ETH,TRUE-BTC,DRG-BTC,DRG-ETH,BANCA-BTC,BANCA-ETH,ZAP-ETH,ZAP-USD,AUTO-BTC,NOAH-BTC,SOC-BTC,OCN-BTC,OCN-ETH,STQ-BTC,STQ-ETH,XLM-BTC,XLM-ETH,XLM-USD,IOTA-BTC,IOTA-ETH,IOTA-USD,DRT-BTC,BETR-USD,ERT-BTC,CRPT-BTC,CRPT-USD,MESH-BTC,MESH-ETH,MESH-USD,IHT-BTC,IHT-ETH,IHT-USD,SCC-BTC,YCC-BTC,DAN-BTC,TEL-BTC,TEL-ETH,NCT-BTC,NCT-ETH,NCT-USD,BMH-BTC,BANCA-USD,NOAH-ETH,NOAH-USD,BERRY-BTC,BERRY-ETH,BERRY-USD,GBX-BTC,GBX-ETH,GBX-USD,SHIP-BTC,SHIP-ETH,NANO-BTC,NANO-ETH,NANO-USD,LNC-BTC,KIN-ETH,ARDR-USD,FOTA-ETH,FOTA-BTC,CVT-BTC,CVT-ETH,CVT-USD,STQ-USD,GNT-BTC,GNT-ETH,GNT-USD,GET-BTC,MITH-BTC,MITH-ETH,MITH-USD,SUNC-ETH,DADI-USD,TKY-BTC,ACAT-BTC,ACAT-ETH,ACAT-USD,BTX-USD,WIKI-BTC,WIKI-ETH,WIKI-USD,ONT-BTC,ONT-ETH,ONT-USD,FTX-BTC,FTX-ETH,FREC-BTC,NAVI-BTC,FREC-ETH,FREC-USD,VME-ETH,NAVI-ETH,LND-ETH,CSM-BTC,NANJ-BTC,NTK-BTC,NTK-ETH,NTK-USD,AUC-BTC,AUC-ETH,CMCT-BTC,CMCT-ETH,CMCT-USD,MAN-BTC,MAN-ETH,MAN-USD,PNT-BTC,PNT-ETH,FXT-BTC,NEXO-BTC,PAT-BTC,PAT-ETH,XMC-BTC,FXT-ETH,HERO-BTC,HERO-ETH,XMC-ETH,XMC-USD,FDZ-BTC,FDZ-ETH,FDZ-USD,SPD-BTC,SPD-ETH,MITX-BTC,TIV-BTC,B2G-BTC,B2G-USD,ZPT-BTC,ZPT-ETH,HBZ-BTC,FACE-BTC,FACE-ETH,HBZ-ETH,HBZ-USD,ZPT-USD,CPT-BTC,PAT-USD,HTML-BTC,HTML-ETH,MITX-ETH,JOT-BTC,JBC-BTC,JBC-ETH,BTS-BTC,BNK-BTC,KBC-BTC,KBC-ETH,BNK-ETH,BNK-USD,TIV-ETH,TIV-USD,CSM-ETH,CSM-USD,INK-BTC,IOST-BTC,INK-ETH,INK-USD,CBC-BTC,IOST-USD,ZIL-BTC,ABYSS-BTC,ABYSS-ETH,ZIL-USD,BCI-BTC,CBC-ETH,CBC-USD,PITCH-BTC,PITCH-ETH,HTML-USD,TDS-BTC,TDS-ETH,TDS-USD,SBD-ETH,SBD-USD,DPN-BTC,UUU-BTC,UUU-ETH,XBP-BTC,CLN-BTC,CLN-ETH,ELEC-BTC,ELEC-ETH,ELEC-USD,QNTU-BTC,QNTU-ETH,QNTU-USD,IPL-ETH,IPL-USD,CENNZ-BTC,CENNZ-ETH,SWM-BTC,SPF-USD,SPF-BTC,LCC-BTC,HGT-BTC,ETH-TUSD,BTC-TUSD,LTC-TUSD,XMR-TUSD,ZRX-TUSD,NEO-TUSD,USD-TUSD,BTC-DAI,ETH-DAI,MKR-DAI,EOS-DAI,USD-DAI,MKR-BTC,MKR-ETH,MKR-USD,TUSD-DAI,NEO-DAI,LTC-DAI,XMR-DAI,XRP-DAI,NEXO-ETH,NEXO-USD,DWS-BTC,DWS-ETH,DWS-USD,APPC-BTC,APPC-ETH,APPC-USD,BIT-ETH,SPC-BTC,SPC-ETH,SPC-USD,REX-BTC,REX-ETH,REX-USD,ELF-BTC,ELF-USD,BCD-BTC,BCD-USD,CVCOIN-BTC,CVCOIN-ETH,CVCOIN-USD,EDG-ETH,EDG-USD,NLC2-BTC,COSM-BTC,COSM-ETH,DASH-EURS,ZEC-EURS,BTC-EURS,EOS-EURS,ETH-EURS,LTC-EURS,NEO-EURS,XMR-EURS,XRP-EURS,EURS-USD,EURS-TUSD,EURS-DAI,MNX-USD,ROX-ETH,ZPR-ETH,MNX-BTC,MNX-ETH,KIND-BTC,KIND-ETH,ENGT-BTC,ENGT-ETH,PMA-BTC,PMA-ETH,TV-BTC,TV-ETH,TV-USD,XCLR-BTC,BAT-BTC,BAT-ETH,BAT-USD,SRN-BTC,SRN-ETH,SRN-USD,SVD-BTC,SVD-ETH,SVD-USD,GST-BTC,GST-ETH,GST-USD,BNB-BTC,BNB-ETH,BNB-USD,DIT-BTC,DIT-ETH,POA20-BTC,CCL-USD,PROC-BTC,POA20-ETH,POA20-USD,POA20-DAI,NIM-BTC,USE-BTC,USE-ETH,DAV-BTC,DAV-ETH,ABTC-BTC,NIM-ETH,ABA-BTC,ABA-ETH,ABA-USD,BCN-EOS,LTC-EOS,XMR-EOS,DASH-EOS,TRX-EOS,NEO-EOS,ZEC-EOS,LSK-EOS,XEM-EOS,XRP-EOS,MESSE-BTC,MESSE-ETH,MESSE-USD,CCL-ETH,RCN-BTC,RCN-ETH,RCN-USD,HMQ-BTC,HMQ-ETH,MYST-BTC,MYST-ETH,USD-GUSD,BTC-GUSD,ETH-GUSD,EOS-GUSD,AXPR-BTC,AXPR-ETH,DAG-BTC,DAG-ETH,BITS-BTC,BITS-ETH,BITS-USD,CDCC-BTC,CDCC-ETH,CDCC-USD,VET-BTC,VET-ETH,VET-USD,SILK-ETH,BOX-BTC,BOX-ETH,BOX-EURS,BOX-EOS,VOCO-BTC,VOCO-ETH,VOCO-USD,PASS-BTC,PASS-ETH,SLX-BTC,SLX-USD,PBTT-BTC,PMA-USD,TRAD-BTC,DGTX-BTC,DGTX-ETH,DGTX-USD,MRK-BTC,MRK-ETH,DGB-TUSD,MESSE-EOS,MESSE-EURS,SNBL-BTC,BCH-BTC,BCH-USD,BSV-BTC,BSV-USD,BKX-BTC,NPLC-BTC,NPLC-ETH,ETN-BTC,ETN-ETH,ETN-USD,MRS-BTC,MRS-ETH,MRS-USD,DTR-BTC,DTR-ETH,TDP-BTC,HBT-ETH,PXG-BTC,PXG-USD,BTC-PAX,ETH-PAX,USD-PAX,BTC-USDC,ETH-USDC,USD-USDC,TUSD-USDC,DAI-USDC,EOS-PAX,CLO-BTC,CLO-ETH,CLO-USD,PETH-BTC,PETH-ETH,PETH-USD,BRD-BTC,BRD-ETH,NMR-BTC,SALT-BTC,SALT-ETH,POLY-BTC,POLY-ETH,POWR-BTC,POWR-ETH,STORJ-BTC,STORJ-ETH,STORJ-USD,MLN-BTC,MLN-ETH,BDG-BTC,POA-ETH,POA-BTC,POA-USD,POA-DAI,KIN-BTC,VEO-BTC,PLA-BTC,PLA-ETH,PLA-USD,BTT-BTC,BTT-USD,BTT-ETH,ZEN-BTC,ZEN-ETH,ZEN-USD,GRIN-BTC,GRIN-ETH,GRIN-USD,FET-BTC,HT-BTC,HT-USD,XZC-BTC,XZC-ETH,XZC-USD,VRA-BTC,VRA-ETH,BTC-KRWB,USD-KRWB,WBTC-ETH,CRO-BTC,CRO-ETH,CRO-USD,GAS-BTC,GAS-ETH,GAS-USD,ORMEUS-BTC,ORMEUS-ETH,SWM-ETH,SWM-USD,PRE-ETH,PHX-BTC,PHX-ETH,PHX-USD,BET-BTC,USD-EOSDT,BTC-EOSDT,ETH-EOSDT,EOS-EOSDT,DAI-EOSDT,NUT-BTC,NUT-EOS,NUT-USD,CUTE-BTC,CUTE-ETH,CUTE-USD,CUTE-EOS,XCON-BTC,DCR-BTC,DCR-ETH,DCR-USD,MG-BTC,MG-ETH,MG-EOS,MG-USD,GNX-BTC,PRO-BTC,EURS-EOSDT,TUSD-EOSDT,ECOIN-BTC,ECOIN-ETH,ECOIN-USD,AGI-BTC,LOOM-BTC,LOOM-ETH,BLZ-BTC,QKC-BTC,QKC-ETH,KNC-BTC,KNC-ETH,KNC-USD,KEY-BTC,KEY-ETH,ATOM-BTC,ATOM-USD,ATOM-ETH,BRDG-BTC,BRDG-ETH,BRDG-USD,MTL-BTC,MTL-ETH,EXP-BTC,BTCB-BTC,PBT-BTC,PBT-ETH,LINK-BTC,LINK-ETH,LINK-USD,USD-USDT20,PHB-BTC,BCH-ETH,BCH-DAI,BCH-TUSD,BCH-EURS,DAPP-BTC,DAPP-EOS,BTC-USDT20,DENT-BTC,DENT-USD", + "enabledPairs": "BTC-USD", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Huobi", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiAuthPemKey": "-----BEGIN EC PRIVATE KEY-----\nJUSTADUMMY\n-----END EC PRIVATE KEY-----\n", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "HT-USDT,BAT-ETH,AST-ETH,TRX-BTC,NEW-BTC,AE-BTC,IIC-BTC,NEW-USDT,CDC-BTC,AE-USDT,DGB-BTC,NAS-ETH,QSP-BTC,LYM-ETH,YCC-BTC,BCH-HT,BIX-ETH,WXT-BTC,XRP-BTC,IOST-BTC,CHAT-BTC,BTC-USDT,XTZ-BTC,PVT-BTC,PVT-USDT,WAVES-ETH,ACT-BTC,RSR-BTC,ACT-USDT,WXT-USDT,XLM-ETH,HT-BTC,UUU-USDT,XRP-USDT,UGAS-BTC,BTS-ETH,IRIS-ETH,LUN-BTC,IOST-HT,DOCK-BTC,ABT-ETH,CRO-BTC,MAN-ETH,ENG-ETH,QUN-BTC,APPC-BTC,KAN-ETH,VET-USDT,SOC-ETH,RSR-HT,RUFF-ETH,RCCC-ETH,AAC-ETH,MCO-BTC,RSR-USDT,TNB-ETH,UTK-ETH,ADX-BTC,WAX-ETH,IOST-USDT,HOT-ETH,WTC-USDT,CVCOIN-BTC,NCASH-ETH,ATP-BTC,SWFTC-ETH,GTC-BTC,PNT-BTC,GT-HT,NEO-BTC,OMG-BTC,EOS-HUSD,WPR-ETH,ARPA-BTC,BTM-BTC,BTM-USDT,KCASH-ETH,SSP-ETH,ARPA-USDT,CNN-BTC,NKN-BTC,NPXS-BTC,OMG-USDT,TOPC-ETH,XEM-BTC,BCH-USDT,SNC-BTC,POLY-ETH,CMT-ETH,PAI-USDT,ZEC-USDT,LSK-ETH,SMT-ETH,DASH-USDT,GAS-ETH,DASH-BTC,GXC-ETH,FTT-HT,IOTA-ETH,FTI-BTC,TRIO-ETH,LET-BTC,ZRX-ETH,ETN-ETH,EVX-ETH,BFT-ETH,GRS-BTC,XRP-HT,DASH-HT,QTUM-ETH,HIT-ETH,NEXO-BTC,QASH-BTC,EOS-ETH,ARDR-ETH,ADA-BTC,NEO-USDT,BTT-TRX,COVA-ETH,REN-BTC,LOOM-BTC,CVC-ETH,NANO-ETH,ARPA-HT,NEW-HT,BLZ-ETH,LINK-ETH,XTZ-USDT,PAY-BTC,GNT-USDT,YEE-ETH,XZC-ETH,EGCC-ETH,PROPY-ETH,ZEC-BTC,EDU-ETH,RTE-BTC,DCR-USDT,FTT-BTC,DCR-BTC,EKO-BTC,SBTC-BTC,ZLA-ETH,TOP-HT,ALGO-BTC,DTA-ETH,EKT-ETH,ATOM-USDT,LXT-USDT,ZEN-ETH,LOL-USDT,LTC-USDT,DAT-BTC,REQ-ETH,ELA-ETH,NKN-HT,PC-BTC,HIT-BTC,EKO-ETH,STK-ETH,LAMB-USDT,LAMB-HT,DOGE-ETH,ATOM-BTC,THETA-USDT,LOL-BTC,THETA-BTC,LSK-BTC,ADA-USDT,RDN-BTC,OGO-HT,UIP-USDT,WICC-BTC,OCN-BTC,ELF-BTC,AKRO-USDT,USDC-HUSD,LAMB-BTC,DBC-ETH,BTT-ETH,FAIR-BTC,POWR-ETH,MUSK-ETH,MT-BTC,STEEM-USDT,RBTC-BTC,CTXC-BTC,MANA-USDT,ICX-ETH,GET-BTC,LTC-BTC,ITC-ETH,BCV-BTC,ZJLT-BTC,AKRO-HT,TNT-ETH,TOP-BTC,MEX-BTC,DATX-BTC,ALGO-USDT,LXT-BTC,GT-USDT,FSN-HT,FSN-USDT,MTX-ETH,LET-ETH,OGO-USDT,PHX-BTC,KCASH-HT,HC-USDT,LOL-HT,NKN-USDT,HOT-BTC,LBA-BTC,XMX-BTC,OST-ETH,VEN-USDT,LTC-HT,LBA-USDT,VEN-BTC,CRE-HT,BIFI-BTC,BT1-BTC,HPT-BTC,NULS-BTC,WAN-BTC,ZIL-BTC,ETC-HT,TOS-BTC,MANA-BTC,SHE-BTC,GT-BTC,FSN-BTC,MCO-ETH,MTN-BTC,MDS-BTC,SRN-ETH,GVE-BTC,XMR-ETH,MEET-ETH,NULS-USDT,BCH-BTC,PAI-BTC,NCC-ETH,BSV-BTC,AKRO-BTC,ELF-USDT,DGD-ETH,PVT-HT,UIP-BTC,ATP-USDT,SEELE-ETH,GSC-BTC,ETC-USDT,SOC-BTC,GNX-BTC,WICC-USDT,QSP-ETH,RUFF-BTC,KNC-ETH,ATP-HT,CTXC-USDT,KMD-ETH,OGO-BTC,BKBT-BTC,DGB-ETH,WAVES-USDT,BCD-BTC,HPT-HT,ZIL-USDT,BUT-ETH,CVNT-BTC,OCN-USDT,SALT-ETH,XLM-BTC,TRX-USDT,RCN-BTC,DAC-ETH,MT-HT,ETH-HUSD,HPT-USDT,XTZ-ETH,USDT-HUSD,CHAT-ETH,ONT-USDT,SKM-USDT,MAN-BTC,ARDR-BTC,BCX-BTC,SKM-BTC,EOS-USDT,GNX-ETH,CRE-USDT,PORTAL-ETH,COVA-BTC,BIX-BTC,UUU-ETH,AAC-BTC,TRX-ETH,NEXO-ETH,NAS-BTC,ENG-BTC,AST-BTC,TT-HT,QUN-ETH,EOS-BTC,18C-ETH,WTC-ETH,CVCOIN-ETH,CRE-BTC,CNNS-USDT,WAX-BTC,AIDOC-BTC,VET-ETH,CMT-USDT,BSV-USDT,IDT-ETH,IOST-ETH,BTC-HUSD,IOTA-BTC,TNB-BTC,LINK-BTC,TOPC-BTC,RCCC-BTC,ZRX-USDT,CNNS-BTC,BOX-BTC,MDS-USDT,XLM-USDT,BAT-BTC,LYM-BTC,UC-ETH,RUFF-USDT,LUN-ETH,BIX-USDT,CDC-ETH,BTS-USDT,YCC-ETH,KAN-USDT,MTL-BTC,WAVES-BTC,ONT-BTC,HT-HUSD,IRIS-USDT,SOC-USDT,WPR-BTC,ETC-BTC,TUSD-HUSD,CVC-USDT,PROPY-BTC,TRIO-BTC,CVC-BTC,BTT-USDT,NANO-BTC,GXC-BTC,NCASH-BTC,XRP-HUSD,TT-USDT,SHE-ETH,NANO-USDT,LOOM-ETH,POWR-BTC,QTUM-BTC,SSP-BTC,BTM-ETH,QTUM-USDT,XZC-BTC,GNT-ETH,OMG-ETH,NPXS-ETH,SNT-USDT,ETH-USDT,ABT-BTC,BTS-BTC,STEEM-BTC,VSYS-USDT,BLZ-BTC,CNNS-HT,ADX-ETH,SMT-USDT,IOTA-USDT,PAY-ETH,CMT-BTC,UTK-BTC,SWFTC-BTC,GTC-ETH,LINK-USDT,SNC-ETH,SNT-BTC,EOS-HT,REN-ETH,PAX-HUSD,KCASH-BTC,HC-BTC,IIC-ETH,QASH-ETH,GRS-ETH,EDU-BTC,HIT-USDT,TOP-USDT,XZC-USDT,KAN-BTC,SC-BTC,SKM-HT,AE-ETH,STORJ-USDT,XVG-ETH,ZRX-BTC,EVX-BTC,ETN-BTC,BFT-BTC,FTI-ETH,DAT-ETH,UGAS-ETH,BAT-USDT,GXC-USDT,GAS-BTC,TNT-BTC,HB10-USDT,MUSK-BTC,FTT-USDT,STK-BTC,ELF-ETH,KNC-BTC,CTXC-ETH,DBC-BTC,HC-ETH,EKT-BTC,DTA-USDT,ZLA-BTC,EKT-USDT,DTA-BTC,OCN-ETH,DGD-BTC,BHT-USDT,MTX-BTC,BCV-ETH,YEE-BTC,VSYS-HT,MEX-ETH,DATX-ETH,EGCC-BTC,LXT-ETH,ITC-USDT,TOS-ETH,ITC-BTC,RCN-ETH,XVG-BTC,SC-ETH,BT2-BTC,REQ-BTC,ELA-USDT,LET-USDT,STORJ-BTC,ALGO-ETH,POLY-BTC,LAMB-ETH,DCR-ETH,EGT-BTC,RTE-ETH,FAIR-ETH,CNN-ETH,BHT-BTC,GSC-ETH,GNT-BTC,PAI-ETH,PC-ETH,ADA-ETH,DOGE-BTC,ZEN-BTC,STEEM-ETH,XMR-BTC,XMR-USDT,MDS-ETH,TT-BTC,BTT-BTC,BHT-HT,ZJLT-ETH,UC-BTC,GVE-ETH,MXC-BTC,MANA-ETH,VSYS-BTC,THETA-ETH,NCC-BTC,APPC-ETH,SMT-BTC,IDT-BTC,UIP-ETH,ETH-BTC,BOX-ETH,LBA-ETH,NULS-ETH,PNT-ETH,BTG-BTC,CVNT-ETH,SALT-BTC,XEM-USDT,WXT-HT,BUT-BTC,DAC-BTC,DOCK-ETH,GET-ETH,AIDOC-ETH,EGT-USDT,WAN-ETH,KMD-BTC,MTN-ETH,CRO-USDT,ONT-ETH,BKBT-ETH,MEET-BTC,VEN-ETH,MT-ETH,SRN-BTC,UUU-BTC,SEELE-BTC,ICX-BTC,RDN-ETH,EGT-HT,ZIL-ETH,IRIS-BTC,CRO-HT,ACT-ETH,DOGE-USDT,NAS-USDT,PORTAL-BTC,ELA-BTC,OST-BTC,WICC-ETH,VET-BTC,XMX-ETH,WTC-BTC,HT-ETH,ATOM-ETH,18C-BTC", + "enabledPairs": "BTC-USDT", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "requestCurrencyPairFormat": { + "uppercase": false + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "ITBIT", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "clientId": "ClientID", + "availablePairs": "XBTUSD,XBTSGD", + "enabledPairs": "XBTUSD,XBTSGD", + "baseCurrencies": "USD,SGD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": false, + "pairsLastUpdated": 1566798411, + "configCurrencyPairFormat": { + "uppercase": true + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Kraken", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "ATOM-ETH,QTUM-EUR,QTUM-USD,LTC-XBT,XTZ-ETH,XMR-XBT,ADA-EUR,BAT-ETH,BAT-EUR,BAT-XBT,QTUM-CAD,WAVES-USD,ETC-EUR,MLN-ETH,XLM-USD,XRP-CAD,ADA-USD,DASH-XBT,REP-XBT,XBT-CAD,XBT-EUR,XBT-GBP,USDT-USD,ETH-JPY,XBT-USD,ZEC-USD,ETH-EUR,ETH-USD,XTZ-XBT,ZEC-EUR,ZEC-JPY,ADA-ETH,EOS-ETH,QTUM-ETH,ETH-CAD,XTZ-EUR,EOS-EUR,REP-USD,XMR-USD,BCH-XBT,EOS-XBT,ETC-ETH,XLM-XBT,ADA-CAD,ADA-XBT,ATOM-EUR,ATOM-XBT,DASH-EUR,GNO-USD,GNO-XBT,WAVES-XBT,ETH-GBP,XBT-JPY,ZEC-XBT,QTUM-XBT,WAVES-ETH,XDG-XBT,XRP-XBT,EOS-USD,XMR-EUR,XRP-EUR,ATOM-CAD,DASH-USD,ETC-USD,ETH-XBT,GNO-EUR,ETC-XBT,LTC-EUR,REP-ETH,XTZ-USD,XLM-EUR,GNO-ETH,LTC-USD,REP-EUR,XRP-JPY,XRP-USD,ATOM-USD,BCH-USD,WAVES-EUR,BAT-USD,BCH-EUR,MLN-XBT,XTZ-CAD", + "enabledPairs": "XBT-USD", + "baseCurrencies": "EUR,USD,CAD,GBP,JPY", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "requestCurrencyPairFormat": { + "uppercase": true, + "separator": "," + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "LakeBTC", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BACETH,BTCUSD,USDSGD,USDJPY,LTCBTC,BTCCHF,BTCNZD,BTCJPY,USDNGN,BCHBTC,BTCAUD,NZDUSD,EURUSD,USDHKD,BTCEUR,USDCHF,GBPUSD,XRPBTC,AUDUSD,BTCHKD,BTCGBP,BTCCAD,BTCNGN,BTCSGD,USDCAD,ETHBTC", + "enabledPairs": "BTCUSD,BTCAUD", + "baseCurrencies": "USD,EUR,HKD,AUD,GBP,NZD,JPY,SGD,NGN,CHF,CAD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "LBank", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "FBC_USDT,HDS_USDT,GALT_USDT,IOG_USDT,IOEX_USDT,VOLLAR_USDT,OATH_USDT,BLOC_USDT,BTC_USDT,ETH_USDT,ETH_BTC,ABBC_BTC,BZKY_ETH,ONOT_ETH,KISC_ETH,BXA_USDT,ATP_USDT,MAT_USDT,SKY_BTC,RNT_USDT,VENA_USDT,GRIN_USDT,IDA_USDT,PNT_USDT,BSV_USDT,OPX_USDT,TENA_ETH,VTHO_BTC,VNX_BTC,AMO_ETH,UBEX_BTC,EOS_BTC,UBEX_USDT,TNS_BTC,ALI_ETH,SDC_ETH,SAIT_ETH,ARTCN_USDT,DAX_BTC,DAX_ETH,DALI_USDT,VET_USDT,BCH_BTC,BCH_USDT,NEO_USDT,QTUM_USDT,ZEC_USDT,VET_BTC,PAI_BTC,PNT_BTC,NEO_BTC,DASH_BTC,LTC_BTC,ETC_BTC,QTUM_BTC,ZEC_BTC,SC_BTC,BTS_BTC,CPX_BTC,XWC_BTC,FIL6_BTC,FIL12_BTC,FIL36_BTC,EOS_USDT,UT_ETH,ELA_ETH,VET_ETH,VTHO_ETH,PAI_ETH,BFDT_ETH,HER_ETH,PTT_ETH,TAC_ETH,IDHUB_ETH,SSC_ETH,SKM_ETH,IIC_ETH,PLY_ETH,EXT_ETH,EOS_ETH,YOYOW_ETH,TRX_ETH,QTUM_ETH,ZEC_ETH,BTS_ETH,BTM_ETH,MITH_ETH,NAS_ETH,MAN_ETH,DBC_ETH,BTO_ETH,DDD_ETH,CPX_ETH,CS_ETH,IHT_ETH,TKY_ETH,OCN_ETH,DCT_ETH,ZPT_ETH,EKO_ETH,MDA_ETH,PST_ETH,XWC_ETH,PUT_ETH,PNT_ETH,AAC_ETH,FIL6_ETH,FIL12_ETH,FIL36_ETH,UIP_ETH,SEER_ETH,BSB_ETH,CDC_ETH,GRAMS_ETH,DDMX_ETH,EAI_ETH,INC_ETH,BNB_USDT,HT_USDT,KBC_BTC,KBC_USDT,MAI_USDT,PHV_USDT,GT_USDT,B91_USDT,VOKEN_USDT,CYE_USDT,BRC_USDT,BTC_AUSD,CXC_BTC,CXC_USDT,DDMX_USDT,SEAL_USDT,SEOS_BTC,BTY_USDT,FO_USDT,VCC_ETH,DLX_USDT,KDS_USDT,BFC_USDT,LBK_USDT,SERO_USDT,MTV_USDT,CKB_USDT,ARPA_USDT,ZIP_USDT,AT_USDT", + "enabledPairs": "btc_usdt", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": false, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": false, + "delimiter": "_" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "LocalBitcoins", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BTCXAF,BTCHKD,BTCBRL,BTCPLN,BTCGHS,BTCPEN,BTCSAR,BTCCAD,BTCJOD,BTCVES,BTCXOF,BTCRWF,BTCEUR,BTCNOK,BTCLTC,BTCZMW,BTCXRP,BTCPAB,BTCUSD,BTCCRC,BTCTTD,BTCLBP,BTCOMR,BTCRON,BTCGEL,BTCKRW,BTCCLP,BTCSZL,BTCNGN,BTCILS,BTCDKK,BTCMYR,BTCRUB,BTCKES,BTCINR,BTCJPY,BTCKHR,BTCCOP,BTCIRR,BTCARS,BTCKZT,BTCTZS,BTCVND,BTCEGP,BTCGBP,BTCTHB,BTCAED,BTCGTQ,BTCCHF,BTCIDR,BTCAUD,BTCNZD,BTCKWD,BTCBOB,BTCUGX,BTCETH,BTCUAH,BTCSGD,BTCCNY,BTCPHP,BTCTWD,BTCLKR,BTCNAD,BTCMXN,BTCBYN,BTCBDT,BTCDOP,BTCTRY,BTCPYG,BTCPKR,BTCQAR,BTCSEK,BTCMAD,BTCZAR", + "enabledPairs": "BTCAUD,BTCUSD", + "baseCurrencies": "ARS,AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,ILS,INR,MXN,NOK,NZD,PLN,RUB,SEK,SGD,THB,USD,ZAR", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true + }, + "requestCurrencyPairFormat": { + "uppercase": true + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "OKCOIN International", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BTC_USD,LTC_USD,ETH_USD,ETC_USD,TUSD_USD,BCH_USD,EOS_USD,XRP_USD,TRX_USD,BSV_USD,USDT_USD,USDK_USD,XLM_USD,ADA_USD,BAT_USD,DCR_USD,EURS_USD,GRIN_USD,GUSD_USD,PAX_USD,USDC_USD,ZEC_USD,ZRX_USD,BTC_USDT,BTC_GUSD,BTC_PAX,BTC_TUSD,BTC_EUR,BTC_EURS,BTC_USDC,ETH_EUR,BCH_EUR,EURS_EUR", + "enabledPairs": "BTC_USD", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": false, + "delimiter": "_" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "OKEX", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BCH_BTC,BSV_BTC,DASH_BTC,ADA_BTC,ABL_BTC,AE_BTC,ALGO_BTC,ARDR_BTC,ATOM_BTC,BLOC_BTC,BTT_BTC,CAI_BTC,CTXC_BTC,CVT_BTC,DCR_BTC,EGT_BTC,GUSD_BTC,HPB_BTC,HYC_BTC,KAN_BTC,LBA_BTC,LEO_BTC,LET_BTC,LSK_BTC,NXT_BTC,ORS_BTC,PAX_BTC,SC_BTC,TUSD_BTC,USDC_BTC,VITE_BTC,WAVES_BTC,WIN_BTC,WXT_BTC,XAS_BTC,YOU_BTC,ZCO_BTC,ZIL_BTC,XRP_BTC,ELF_BTC,LRC_BTC,MCO_BTC,NULS_BTC,BCX_BTC,CMT_BTC,EDO_BTC,ITC_BTC,SBTC_BTC,ZEC_BTC,NEO_BTC,GAS_BTC,HC_BTC,QTUM_BTC,IOTA_BTC,XUC_BTC,EOS_BTC,SNT_BTC,OMG_BTC,LTC_BTC,ETH_BTC,ETC_BTC,BCD_BTC,BTG_BTC,ACT_BTC,PAY_BTC,BTM_BTC,DGD_BTC,GNT_BTC,LINK_BTC,WTC_BTC,ZRX_BTC,BNT_BTC,CVC_BTC,MANA_BTC,KNC_BTC,GNX_BTC,ICX_BTC,XEM_BTC,ARK_BTC,YOYO_BTC,FUN_BTC,ACE_BTC,TRX_BTC,DGB_BTC,SWFTC_BTC,XMR_BTC,XLM_BTC,KCASH_BTC,MDT_BTC,NAS_BTC,UGC_BTC,DPY_BTC,SSC_BTC,AAC_BTC,VIB_BTC,QUN_BTC,INT_BTC,IOST_BTC,INS_BTC,MOF_BTC,TCT_BTC,STC_BTC,THETA_BTC,PST_BTC,SNC_BTC,MKR_BTC,LIGHT_BTC,OF_BTC,TRUE_BTC,SOC_BTC,ZEN_BTC,HMC_BTC,ZIP_BTC,NANO_BTC,CIC_BTC,GTO_BTC,CHAT_BTC,INSUR_BTC,R_BTC,BEC_BTC,MITH_BTC,ABT_BTC,BKX_BTC,RFR_BTC,TRIO_BTC,DADI_BTC,ONT_BTC,OKB_BTC,ADA_ETH,ABL_ETH,AE_ETH,ALGO_ETH,ATOM_ETH,BTT_ETH,CAI_ETH,CTXC_ETH,DCR_ETH,EGT_ETH,HPB_ETH,HYC_ETH,KAN_ETH,LEO_ETH,LSK_ETH,MVP_ETH,ORS_ETH,SC_ETH,SDA_ETH,WAVES_ETH,WIN_ETH,YOU_ETH,ZIL_ETH,ELF_ETH,LTC_ETH,CMT_ETH,PRA_ETH,LRC_ETH,MCO_ETH,NULS_ETH,DGD_ETH,SNT_ETH,STORJ_ETH,ACT_ETH,BTM_ETH,EOS_ETH,OMG_ETH,DASH_ETH,XRP_ETH,ZEC_ETH,NEO_ETH,GAS_ETH,HC_ETH,QTUM_ETH,IOTA_ETH,ETC_ETH,LINK_ETH,WTC_ETH,ZRX_ETH,BNT_ETH,CVC_ETH,MANA_ETH,GNX_ETH,ICX_ETH,XEM_ETH,YOYO_ETH,TRX_ETH,DGB_ETH,SWFTC_ETH,XMR_ETH,XLM_ETH,KCASH_ETH,MDT_ETH,NAS_ETH,SSC_ETH,AAC_ETH,FAIR_ETH,RCT_ETH,TOPC_ETH,QUN_ETH,INT_ETH,IOST_ETH,INS_ETH,MOF_ETH,REF_ETH,SNC_ETH,MKR_ETH,LIGHT_ETH,OF_ETH,TRUE_ETH,ZEN_ETH,HMC_ETH,ZIP_ETH,NANO_ETH,CIC_ETH,GTO_ETH,INSUR_ETH,UCT_ETH,MITH_ETH,ABT_ETH,AUTO_ETH,TRIO_ETH,TRA_ETH,ONT_ETH,OKB_ETH,BTC_USDK,LTC_USDK,ETH_USDK,OKB_USDK,ETC_USDK,BCH_USDT,BCH_USDK,EOS_USDK,XRP_USDK,TRX_USDK,BSV_USDT,BSV_USDK,USDT_USDK,ADA_USDT,AE_USDT,ALGO_USDT,ALGO_USDK,ALV_USDT,ATOM_USDT,BLOC_USDT,BTT_USDT,CAI_USDT,CRO_USDT,CRO_USDK,CTXC_USDT,CVT_USDT,DCR_USDT,DOGE_USDT,DOGE_USDK,EC_USDT,EC_USDK,EGT_USDT,EM_USDT,EM_USDK,ETM_USDT,ETM_USDK,FSN_USDT,FSN_USDK,FTM_USDT,FTM_USDK,GUSD_USDT,HPB_USDT,HYC_USDT,KAN_USDT,LAMB_USDT,LAMB_USDK,LBA_USDT,LEO_USDT,LEO_USDK,LET_USDT,LSK_USDT,MVP_USDT,ORBS_USDT,ORBS_USDK,ORS_USDT,PAX_USDT,PLG_USDT,PLG_USDK,SC_USDT,TUSD_USDT,USDC_USDT,VNT_USDT,VNT_USDK,WAVES_USDT,WIN_USDT,WXT_USDT,WXT_USDK,XAS_USDT,YOU_USDT,ZIL_USDT,TRX_OKB,ADA_OKB,AE_OKB,BLOC_OKB,DCR_OKB,EGT_OKB,SC_OKB,WAVES_OKB,WXT_OKB,ELF_USDT,DASH_USDT,BTG_USDT,LRC_USDT,MCO_USDT,NULS_USDT,DASH_OKB,XRP_USDT,ZEC_USDT,NEO_USDT,GAS_USDT,HC_USDT,QTUM_USDT,IOTA_USDT,BTC_USDT,BCD_USDT,XUC_USDT,CMT_USDT,EDO_USDT,ITC_USDT,PRA_USDT,ETH_USDT,LTC_USDT,ETC_USDT,EOS_USDT,OMG_USDT,ACT_USDT,BTM_USDT,DGD_USDT,GNT_USDT,PAY_USDT,STORJ_USDT,SNT_USDT,LINK_USDT,WTC_USDT,ZRX_USDT,BNT_USDT,CVC_USDT,MANA_USDT,KNC_USDT,ICX_USDT,XEM_USDT,ARK_USDT,YOYO_USDT,AST_USDT,TRX_USDT,MDA_USDT,DGB_USDT,PPT_USDT,SWFTC_USDT,XMR_USDT,XLM_USDT,KCASH_USDT,MDT_USDT,NAS_USDT,RNT_USDT,UGC_USDT,DPY_USDT,SSC_USDT,AAC_USDT,FAIR_USDT,UBTC_USDT,SHOW_USDT,VIB_USDT,MOT_USDT,UTK_USDT,TOPC_USDT,QUN_USDT,INT_USDT,IPC_USDT,IOST_USDT,INS_USDT,YEE_USDT,MOF_USDT,TCT_USDT,STC_USDT,THETA_USDT,PST_USDT,MKR_USDT,LIGHT_USDT,OF_USDT,TRUE_USDT,SOC_USDT,ZEN_USDT,HMC_USDT,ZIP_USDT,NANO_USDT,CIC_USDT,GTO_USDT,CHAT_USDT,INSUR_USDT,R_USDT,BEC_USDT,MITH_USDT,ABT_USDT,BKX_USDT,RFR_USDT,TRIO_USDT,DADI_USDT,ONT_USDT,OKB_USDT,NEO_OKB,LTC_OKB,ETC_OKB,XRP_OKB,ZEC_OKB,QTUM_OKB,IOTA_OKB,EOS_OKB", + "enabledPairs": "eos_usdt", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": false, + "delimiter": "_" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Poloniex", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BTC_DASH,BTC_VIA,USDC_ZEC,USDT_BCHSV,BTC_XEM,USDT_STR,ETH_REP,BTC_MANA,USDC_STR,USDC_ETH,USDT_BTC,BTC_REP,BTC_PASC,BTC_GNT,ETH_ZRX,BTC_SNT,ETH_BAT,USDC_DOGE,BTC_POLY,BTC_ATOM,BTC_BAT,BTC_DGB,BTC_NXT,BTC_STR,BTC_STRAT,BTC_EOS,ETH_EOS,BTC_KNC,USDT_SC,BTC_BCHSV,BTC_XMR,BTC_STEEM,BTC_ZEC,BTC_GAS,USDT_BCHABC,USDC_ATOM,BTC_XRP,USDT_DASH,USDT_ETH,USDT_DOGE,BTC_QTUM,USDC_BTC,BTC_LPT,USDT_DGB,BTC_DOGE,USDT_BAT,USDC_BCHSV,BTC_BTS,BTC_GAME,BTC_SC,BTC_OMG,USDT_MANA,BTC_GRIN,BTC_LTC,BTC_ETH,USDT_EOS,USDT_LSK,USDC_BCHABC,USDC_XMR,BTC_NMR,USDT_REP,BTC_ZRX,USDT_GNT,USDT_QTUM,BTC_BNT,USDC_EOS,BTC_BCN,USDT_ATOM,USDC_DASH,BTC_OMNI,BTC_FCT,BTC_LSK,USDC_XRP,BTC_FOAM,BTC_CVC,BTC_NAV,USDT_LTC,USDT_NXT,USDT_XMR,USDT_XRP,BTC_LBC,USDT_ETC,BTC_LOOM,USDT_GRIN,BTC_DCR,BTC_ETC,ETH_ETC,BTC_ARDR,USDT_ZEC,BTC_BCHABC,USDC_GRIN,BTC_STORJ,USDT_ZRX,USDC_USDT,USDC_ETC,BTC_CLAM,BTC_MAID,BTC_VTC,BTC_XPM,ETH_ZEC,USDC_LTC", + "enabledPairs": "BTC_LTC,BTC_ETH,BTC_DOGE,BTC_DASH,BTC_XRP", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "Yobit", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "DASH_BTC,WAVES_BTC,LSK_BTC,LIZA_BTC,BCC_BTC,ETH_BTC,LTC_BTC,TRX_BTC,DOGE_BTC,VNTX_BTC,SW_BTC,ZEC_BTC,DASH_ETH,WAVES_ETH,LSK_ETH,LIZA_ETH,BCC_ETH,LTC_ETH,TRX_ETH,DOGE_ETH,VNTX_ETH,SW_ETH,ZEC_ETH,DASH_DOGE,WAVES_DOGE,LSK_DOGE,LIZA_DOGE,BCC_DOGE,LTC_DOGE,TRX_DOGE,VNTX_DOGE,SW_DOGE,ZEC_DOGE,DASH_USD,WAVES_USD,LSK_USD,LIZA_USD,BCC_USD,LTC_USD,TRX_USD,VNTX_USD,SW_USD,ZEC_USD,ETH_USD,BTC_USD,DASH_RUR,WAVES_BTC,WAVES_RUR,LSK_RUR,LIZA_RUR,BCC_RUR,LTC_RUR,TRX_RUR,VNTX_RUR,SW_RUR,ETH_RUR,ZEC_RUR", + "enabledPairs": "LTC_BTC,ETH_BTC,BTC_USD,DASH_BTC", + "baseCurrencies": "USD,RUR", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": false, + "pairsLastUpdated": 1566798411, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": false, + "delimiter": "_", + "separator": "-" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, + { + "name": "ZB", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "DASH_USDT,XLM_QC,DOGE_QC,SBTC_USDT,SNT_USDT,BRC_BTC,BCHSV_QC,TUSD_USDT,ZB_BTC,GRIN_USDT,BAT_USDT,HPY_USDT,ADA_BTC,XTZ_USDT,XWC_USDT,YTNB_USDT,QTUM_USDT,EDO_USDT,BTC_QC,ETC_PAX,TV_BTC,HSR_BTC,XWC_QC,TRX_USDT,VSYS_ZB,LTC_PAX,OMG_QC,ETH_BTC,NEO_BTC,HPY_QC,TOPC_USDT,ICX_USDT,BCX_USDT,GNT_QC,B91_QC,EOS_QC,PAX_QC,BTC_PAX,XRP_QC,LTC_USDT,MANA_BTC,BITE_BTC,EOS_BTC,XUC_QC,HOTC_QC,BAR_USDT,ETZ_QC,XRP_USDT,HOTC_USDT,DOGE_BTC,ZRX_BTC,TRUE_USDT,GRAM_USDT,BTH_QC,HLC_QC,SLT_QC,BCD_USDT,ETC_USDT,GNT_BTC,BTP_QC,ZRX_USDT,BCW_QC,PDX_QC,QTUM_BTC,LTC_QC,BRC_USDT,EPC_QC,GRAM_QC,CHAT_USDT,KNC_QC,DASH_BTC,XMR_QC,XEM_QC,BTP_USDT,HSR_QC,BCD_QC,EOSDAC_USDT,MTL_USDT,ENTC_USDT,KNC_USDT,MITH_QC,SAFE_USDT,1ST_USDT,TRX_QC,OMG_BTC,BRC_QC,MCO_QC,LBTC_BTC,KAN_BTC,1ST_QC,BTM_QC,INK_USDT,GRIN_QC,UBTC_QC,EPC_BTC,XEM_BTC,TV_USDT,ETC_BTC,XEM_USDT,UBTC_USDT,TRUE_BTC,HSR_USDT,BCHSV_USDT,AE_BTC,BCX_QC,ETH_PAX,ACC_USDT,OMG_USDT,ETZ_USDT,DDM_QC,KAN_QC,INK_QC,DOGE_USDT,BCHABC_QC,BITCNY_QC,TRUE_QC,DASH_QC,QUN_USDT,ZRX_QC,BTM_BTC,BTM_USDT,HLC_USDT,SLT_USDT,BTC_USDT,CDC_QC,AE_QC,LBTC_USDT,MCO_USDT,XLM_BTC,LEO_USDT,BTN_QC,SAFE_QC,XRP_BTC,BTS_BTC,BCX_BTC,DDM_USDT,TRX_BTC,QUN_QC,BTS_USDT,PDX_BTC,ETC_QC,BCHABC_USDT,QTUM_QC,ADA_USDT,EOSDAC_QC,BDS_QC,BTN_USDT,SLT_BTC,PDX_USDT,SUB_QC,USDT_QC,TOPC_QC,XMR_USDT,BAT_QC,SNT_QC,B91_USDT,GNT_USDT,PAX_USDT,AE_USDT,ZB_USDT,NWT_USDT,CDC_USDT,RCN_USDT,NEO_QC,MANA_USDT,TV_QC,VSYS_BTC,ZB_QC,GRAM_BTC,BTH_USDT,AAA_QC,ICX_QC,LTC_BTC,ETH_QC,CHAT_QC,BCW_USDT,SNT_BTC,ADA_QC,VSYS_QC,XLM_USDT,BAT_BTC,ETH_USDT,EOS_USDT,ICX_BTC,LBTC_QC,NEO_USDT,MANA_QC,BTS_QC", + "enabledPairs": "BTC_USDT,ETH_USDT", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": false, + "delimiter": "_" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + } + ], + "bankAccounts": [ + { + "bankName": "test", + "bankAddress": "test", + "accountName": "TestAccount", + "accountNumber": "0234", + "swiftCode": "91272837", + "iban": "98218738671897", + "supportedCurrencies": "USD", + "supportedExchanges": "ANX,Kraken" + } + ], + "connectionMonitor": { + "preferredDNSList": [ + "8.8.8.8", + "8.8.4.4", + "1.1.1.1", + "1.0.0.1" + ], + "preferredDomainList": [ + "www.google.com", + "www.cloudflare.com", + "www.facebook.com" + ], + "checkInterval": 1000000000 + }, + "fiatDispayCurrency": "" + } \ No newline at end of file diff --git a/tools/README.md b/tools/README.md deleted file mode 100644 index e89451ca..00000000 --- a/tools/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# GoCryptoTrader package Tools - - - - -[![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader) -[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) -[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/) -[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) -[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) - - -This tools package is part of the GoCryptoTrader codebase. - -## This is still in active development - -You can track ideas, planned features and what's in progresss on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). - -Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) - -## Current Features - -This folder contains an assortment of tools. - -+ Configuration -+ Documentation creation -+ Portfolio monitoring -+ Exchange deployment -+ Websocket client - -Please see individual tool's README file - -## Contribution - -Please feel free to submit any pull requests or suggest any desired features to be added. - -When submitting a PR, please abide by our coding guidelines: - -+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). -+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. -+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). -+ Pull requests need to be based on and opened against the `master` branch. - -## Donations - - - -If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: - -***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/tools/documentation/documentation.go b/tools/documentation/documentation.go deleted file mode 100644 index 707460d3..00000000 --- a/tools/documentation/documentation.go +++ /dev/null @@ -1,375 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "html/template" - "log" - "os" - "strings" - "time" - - "github.com/thrasher-corp/gocryptotrader/common" -) - -const ( - commonPath = "..%s..%scommon%s" - communicationsPath = "..%s..%scommunications%s" - communicationsBasePath = "..%s..%scommunications%sbase%s" - communicationsSlackPath = "..%s..%scommunications%sslack%s" - communicationsSmsglobalPath = "..%s..%scommunications%ssmsglobal%s" - communicationsSMTPPath = "..%s..%scommunications%ssmtpservice%s" - communicationsTelegramPath = "..%s..%scommunications%stelegram%s" - configPath = "..%s..%sconfig%s" - currencyPath = "..%s..%scurrency%s" - currencyFXPath = "..%s..%scurrency%sforexprovider%s" - currencyFXBasePath = "..%s..%scurrency%sforexprovider%sbase%s" - currencyFXCurrencyConverterPath = "..%s..%scurrency%sforexprovider%scurrencyconverterapi%s" - currencyFXCurrencylayerPath = "..%s..%scurrency%sforexprovider%scurrencylayer%s" - currencyFXFixerPath = "..%s..%scurrency%sforexprovider%sfixer.io%s" - currencyFXOpenExchangeRatesPath = "..%s..%scurrency%sforexprovider%sopenexchangerates%s" - eventsPath = "..%s..%sevents%s" - exchangesPath = "..%s..%sexchanges%s" - exchangesNoncePath = "..%s..%sexchanges%snonce%s" - exchangesOrderbookPath = "..%s..%sexchanges%sorderbook%s" - exchangesStatsPath = "..%s..%sexchanges%sstats%s" - exchangesTickerPath = "..%s..%sexchanges%sticker%s" - exchangesOrdersPath = "..%s..%sexchanges%sorders%s" - exchangesRequestPath = "..%s..%sexchanges%srequest%s" - portfolioPath = "..%s..%sportfolio%s" - testdataPath = "..%s..%stestdata%s" - toolsPath = "..%s..%stools%s" - webPath = "..%s..%sweb%s" - rootPath = "..%s..%s" - - // exchange packages - alphapoint = "..%s..%sexchanges%salphapoint%s" - anx = "..%s..%sexchanges%sanx%s" - binance = "..%s..%sexchanges%sbinance%s" - bitfinex = "..%s..%sexchanges%sbitfinex%s" - bitflyer = "..%s..%sexchanges%sbitflyer%s" - bithumb = "..%s..%sexchanges%sbithumb%s" - bitmex = "..%s..%sexchanges%sbitmex%s" - bitstamp = "..%s..%sexchanges%sbitstamp%s" - bittrex = "..%s..%sexchanges%sbittrex%s" - btcmarkets = "..%s..%sexchanges%sbtcmarkets%s" - coinbasepro = "..%s..%sexchanges%scoinbasepro%s" - coinbene = "..%s..%sexchanges%scoinbene%s" - coinut = "..%s..%sexchanges%scoinut%s" - exmo = "..%s..%sexchanges%sexmo%s" - gateio = "..%s..%sexchanges%sgateio%s" - gemini = "..%s..%sexchanges%sgemini%s" - hitbtc = "..%s..%sexchanges%shitbtc%s" - huobi = "..%s..%sexchanges%shuobi%s" - itbit = "..%s..%sexchanges%sitbit%s" - kraken = "..%s..%sexchanges%skraken%s" - lakebtc = "..%s..%sexchanges%slakebtc%s" - lbank = "..%s..%sexchanges%slbank%s" - localbitcoins = "..%s..%sexchanges%slocalbitcoins%s" - okcoin = "..%s..%sexchanges%sokcoin%s" - okex = "..%s..%sexchanges%sokex%s" - poloniex = "..%s..%sexchanges%spoloniex%s" - yobit = "..%s..%sexchanges%syobit%s" - zb = "..%s..%sexchanges%szb%s" - - contributorsList = "https://api.github.com/repos/thrasher-corp/gocryptotrader/contributors" - - licenseName = "LICENSE" - contributorName = "CONTRIBUTORS" -) - -var ( - verbose, replace bool - codebasePaths map[string]string - codebaseTemplatePath map[string]string - codebaseReadme map[string]readme - tmpl *template.Template - path string - contributors []contributor -) - -type readme struct { - Name string - Contributors []contributor - NameURL string - Year int - CapitalName string -} - -type contributor struct { - Login string `json:"login"` - URL string `json:"html_url"` - Contributions int `json:"contributions"` -} - -func main() { - flag.BoolVar(&verbose, "v", false, "-v Verbose flag prints more information to the std output") - flag.BoolVar(&replace, "r", false, "-r Replace flag generates and replaces all documentation across the code base") - flag.Parse() - - fmt.Println(` - GoCryptoTrader: Exchange documentation tool - - This will update and regenerate documentation for the different packages - in GoCryptoTrader.`) - - codebasePaths = make(map[string]string) - codebaseTemplatePath = make(map[string]string) - codebaseReadme = make(map[string]readme) - path = common.GetOSPathSlash() - - if err := getContributorList(); err != nil { - log.Fatal("GoCryptoTrader: Exchange documentation tool GET error ", err) - } - - fmt.Println("Contributor list fetched") - - if err := addTemplates(); err != nil { - log.Fatal("GoCryptoTrader: Exchange documentation tool add template error ", err) - } - - fmt.Println("Templates parsed") - - if err := updateReadme(); err != nil { - log.Fatal("GoCryptoTrader: Exchange documentation tool update readme error ", err) - } - - fmt.Println("\nTool finished") -} - -// updateReadme iterates through codebase paths to check for readme files and either adds -// or replaces with new readme files. -func updateReadme() error { - addPaths() - - for packageName := range codebasePaths { - addReadmeData(packageName) - - if !checkReadme(packageName) { - if verbose { - fmt.Printf("* %s Readme file FOUND.\n", packageName) - } - if replace { - if verbose { - fmt.Println("file replacement") - } - if err := replaceReadme(packageName); err != nil { - return err - } - continue - } - continue - } - if verbose { - fmt.Printf("* %s Readme file NOT FOUND.\n", packageName) - } - if replace { - if verbose { - log.Println("file creation") - } - if err := createReadme(packageName); err != nil { - return err - } - continue - } - } - return nil -} - -// addPaths adds paths to different potential README.md files in the codebase -func addPaths() { - codebasePaths["common"] = fmt.Sprintf(commonPath, path, path, path) - - codebasePaths["communications comms"] = fmt.Sprintf(communicationsPath, path, path, path) - codebasePaths["communications base"] = fmt.Sprintf(communicationsBasePath, path, path, path, path) - codebasePaths["communications slack"] = fmt.Sprintf(communicationsSlackPath, path, path, path, path) - codebasePaths["communications smsglobal"] = fmt.Sprintf(communicationsSmsglobalPath, path, path, path, path) - codebasePaths["communications smtp"] = fmt.Sprintf(communicationsSMTPPath, path, path, path, path) - codebasePaths["communications telegram"] = fmt.Sprintf(communicationsTelegramPath, path, path, path, path) - - codebasePaths["config"] = fmt.Sprintf(configPath, path, path, path) - - codebasePaths["currency"] = fmt.Sprintf(currencyPath, path, path, path) - codebasePaths["currency forexprovider"] = fmt.Sprintf(currencyFXPath, path, path, path, path) - codebasePaths["currency forexprovider base"] = fmt.Sprintf(currencyFXBasePath, path, path, path, path, path) - codebasePaths["currency forexprovider currencyconverter"] = fmt.Sprintf(currencyFXCurrencyConverterPath, path, path, path, path, path) - codebasePaths["currency forexprovider currencylayer"] = fmt.Sprintf(currencyFXCurrencylayerPath, path, path, path, path, path) - codebasePaths["currency forexprovider fixer"] = fmt.Sprintf(currencyFXFixerPath, path, path, path, path, path) - codebasePaths["currency forexprovider openexchangerates"] = fmt.Sprintf(currencyFXOpenExchangeRatesPath, path, path, path, path, path) - - codebasePaths["events"] = fmt.Sprintf(eventsPath, path, path, path) - - codebasePaths["portfolio"] = fmt.Sprintf(portfolioPath, path, path, path) - codebasePaths["testdata"] = fmt.Sprintf(testdataPath, path, path, path) - codebasePaths["tools"] = fmt.Sprintf(toolsPath, path, path, path) - codebasePaths["web"] = fmt.Sprintf(webPath, path, path, path) - codebasePaths["root"] = fmt.Sprintf(rootPath, path, path) - - codebasePaths["exchanges"] = fmt.Sprintf(exchangesPath, path, path, path) - codebasePaths["exchanges nonce"] = fmt.Sprintf(exchangesNoncePath, path, path, path, path) - codebasePaths["exchanges orderbook"] = fmt.Sprintf(exchangesOrderbookPath, path, path, path, path) - codebasePaths["exchanges stats"] = fmt.Sprintf(exchangesStatsPath, path, path, path, path) - codebasePaths["exchanges ticker"] = fmt.Sprintf(exchangesTickerPath, path, path, path, path) - codebasePaths["exchanges orders"] = fmt.Sprintf(exchangesOrdersPath, path, path, path, path) - codebasePaths["exchanges request"] = fmt.Sprintf(exchangesRequestPath, path, path, path, path) - - codebasePaths["exchanges alphapoint"] = fmt.Sprintf(alphapoint, path, path, path, path) - codebasePaths["exchanges anx"] = fmt.Sprintf(anx, path, path, path, path) - codebasePaths["exchanges binance"] = fmt.Sprintf(binance, path, path, path, path) - codebasePaths["exchanges bitfinex"] = fmt.Sprintf(bitfinex, path, path, path, path) - codebasePaths["exchanges bitflyer"] = fmt.Sprintf(bitflyer, path, path, path, path) - codebasePaths["exchanges bithumb"] = fmt.Sprintf(bithumb, path, path, path, path) - codebasePaths["exchanges bitmex"] = fmt.Sprintf(bitmex, path, path, path, path) - codebasePaths["exchanges bitstamp"] = fmt.Sprintf(bitstamp, path, path, path, path) - codebasePaths["exchanges bittrex"] = fmt.Sprintf(bittrex, path, path, path, path) - codebasePaths["exchanges btcmarkets"] = fmt.Sprintf(btcmarkets, path, path, path, path) - codebasePaths["exchanges coinut"] = fmt.Sprintf(coinut, path, path, path, path) - codebasePaths["exchanges exmo"] = fmt.Sprintf(exmo, path, path, path, path) - codebasePaths["exchanges coinbasepro"] = fmt.Sprintf(coinbasepro, path, path, path, path) - codebasePaths["exchanges coinbene"] = fmt.Sprintf(coinbene, path, path, path, path) - codebasePaths["exchanges gateio"] = fmt.Sprintf(gateio, path, path, path, path) - codebasePaths["exchanges gemini"] = fmt.Sprintf(gemini, path, path, path, path) - codebasePaths["exchanges hitbtc"] = fmt.Sprintf(hitbtc, path, path, path, path) - codebasePaths["exchanges huobi"] = fmt.Sprintf(huobi, path, path, path, path) - codebasePaths["exchanges itbit"] = fmt.Sprintf(itbit, path, path, path, path) - codebasePaths["exchanges kraken"] = fmt.Sprintf(kraken, path, path, path, path) - codebasePaths["exchanges lakebtc"] = fmt.Sprintf(lakebtc, path, path, path, path) - codebasePaths["exchanges lbank"] = fmt.Sprintf(lbank, path, path, path, path) - codebasePaths["exchanges localbitcoins"] = fmt.Sprintf(localbitcoins, path, path, path, path) - codebasePaths["exchanges okcoin"] = fmt.Sprintf(okcoin, path, path, path, path) - codebasePaths["exchanges okex"] = fmt.Sprintf(okex, path, path, path, path) - codebasePaths["exchanges poloniex"] = fmt.Sprintf(poloniex, path, path, path, path) - codebasePaths["exchanges yobit"] = fmt.Sprintf(yobit, path, path, path, path) - codebasePaths["exchanges zb"] = fmt.Sprintf(zb, path, path, path, path) - - codebasePaths["CONTRIBUTORS"] = fmt.Sprintf(rootPath, path, path) - codebasePaths["LICENSE"] = fmt.Sprintf(rootPath, path, path) -} - -func addReadmeData(packageName string) { - readmeInfo := readme{ - Name: getName(packageName, false), - Contributors: contributors, - NameURL: getslashFromName(packageName), - Year: time.Now().Year(), - CapitalName: getName(packageName, true), - } - codebaseReadme[packageName] = readmeInfo -} - -func getName(name string, capital bool) string { - newStrings := strings.Split(name, " ") - if len(newStrings) > 1 { - if capital { - return getCapital(newStrings[1]) - } - return newStrings[1] - } - if capital { - return getCapital(name) - } - return name -} - -func getCapital(name string) string { - capLetter := strings.ToUpper(string(name[0])) - last := name[1:] - - return capLetter + last -} - -// getslashFromName returns a string for godoc package names -func getslashFromName(packageName string) string { - if strings.Contains(packageName, " ") { - s := strings.Split(packageName, " ") - return strings.Join(s, "/") - } - if packageName == "testdata" || packageName == "tools" || packageName == contributorName || packageName == licenseName { - return "" - } - return packageName -} - -var globS = []string{ - fmt.Sprintf("common_templates%s*", common.GetOSPathSlash()), - fmt.Sprintf("communications_templates%s*", common.GetOSPathSlash()), - fmt.Sprintf("config_templates%s*", common.GetOSPathSlash()), - fmt.Sprintf("currency_templates%s*", common.GetOSPathSlash()), - fmt.Sprintf("events_templates%s*", common.GetOSPathSlash()), - fmt.Sprintf("exchanges_templates%s*", common.GetOSPathSlash()), - fmt.Sprintf("portfolio_templates%s*", common.GetOSPathSlash()), - fmt.Sprintf("root_templates%s*", common.GetOSPathSlash()), - fmt.Sprintf("sub_templates%s*", common.GetOSPathSlash()), - fmt.Sprintf("testdata_templates%s*", common.GetOSPathSlash()), - fmt.Sprintf("tools_templates%s*", common.GetOSPathSlash()), - fmt.Sprintf("web_templates%s*", common.GetOSPathSlash()), -} - -// addTemplates adds all the template files -func addTemplates() error { - tmpl = template.New("") - - for _, s := range globS { - _, err := tmpl.ParseGlob(s) - if err != nil { - return err - } - } - return nil -} - -// checkReadme checks to see if the file exists -func checkReadme(packageName string) bool { - if packageName == licenseName || packageName == contributorName { - _, err := os.Stat(codebasePaths[packageName] + packageName) - return os.IsNotExist(err) - } - _, err := os.Stat(codebasePaths[packageName] + "README.md") - return os.IsNotExist(err) -} - -// replaceReadme replaces readme file -func replaceReadme(packageName string) error { - if packageName == licenseName || packageName == contributorName { - if err := deleteFile(codebasePaths[packageName] + packageName); err != nil { - return err - } - return createReadme(packageName) - } - if err := deleteFile(codebasePaths[packageName] + "README.md"); err != nil { - return err - } - return createReadme(packageName) -} - -// createReadme creates new readme file and executes template -func createReadme(packageName string) error { - if packageName == licenseName || packageName == contributorName { - file, err := os.Create(codebasePaths[packageName] + packageName) - if err != nil { - return err - } - defer file.Close() - if verbose { - fmt.Println("File done") - } - return tmpl.ExecuteTemplate(file, packageName, codebaseReadme[packageName]) - } - file, err := os.Create(codebasePaths[packageName] + "README.md") - if err != nil { - return err - } - defer file.Close() - if verbose { - fmt.Println("File done") - } - return tmpl.ExecuteTemplate(file, packageName, codebaseReadme[packageName]) -} - -func deleteFile(path string) error { - return os.Remove(path) -} - -func getContributorList() error { - return common.SendHTTPGetRequest(contributorsList, true, false, &contributors) -} diff --git a/tools/documentation/sub_templates/contributors.tmpl b/tools/documentation/sub_templates/contributors.tmpl deleted file mode 100644 index 0d124405..00000000 --- a/tools/documentation/sub_templates/contributors.tmpl +++ /dev/null @@ -1,12 +0,0 @@ -{{define "contributors"}} -## Contributor List - -### A very special thank you to all who have contributed to this program: - -|User|Github|Contribution Amount| -|--|--|--| -{{ range $contributor := .Contributors -}} -| {{$contributor.Login}} | {{$contributor.URL}} | {{$contributor.Contributions}} | -{{ end }} - -{{end}} diff --git a/tools/exchange_template/main_file.tmpl b/tools/exchange_template/main_file.tmpl deleted file mode 100644 index b72be2b7..00000000 --- a/tools/exchange_template/main_file.tmpl +++ /dev/null @@ -1,112 +0,0 @@ -{{define "main"}} -package {{.Name}} - -import ( - "time" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/config" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/exchanges/wshandler" - log "github.com/thrasher-corp/gocryptotrader/logger" -) - -// {{.CapitalName}} is the overarching type across this package -type {{.CapitalName}} struct { - exchange.Base -} - -const ( - {{.Name}}APIURL = "" - {{.Name}}APIVersion = "" - - // Public endpoints - - // Authenticated endpoints - -) - -// SetDefaults sets the basic defaults for {{.CapitalName}} -func ({{.Variable}} *{{.CapitalName}}) SetDefaults() { - {{.Variable}}.Name = "{{.CapitalName}}" - {{.Variable}}.Enabled = false - {{.Variable}}.Verbose = false - {{.Variable}}.RESTPollingDelay = 10 - {{.Variable}}.RequestCurrencyPairFormat.Delimiter = "" - {{.Variable}}.RequestCurrencyPairFormat.Uppercase = true - {{.Variable}}.ConfigCurrencyPairFormat.Delimiter = "" - {{.Variable}}.ConfigCurrencyPairFormat.Uppercase = true - {{.Variable}}.AssetTypes = []string{ticker.Spot} - {{.Variable}}.SupportsAutoPairUpdating = false - {{.Variable}}.SupportsRESTTickerBatching = false - {{.Variable}}.Requester = request.New({{.Variable}}.Name, - request.NewRateLimit(time.Second, 0), - request.NewRateLimit(time.Second, 0), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - {{.Variable}}.APIUrlDefault = {{.Name}}APIURL - {{.Variable}}.APIUrl = {{.Variable}}.APIUrlDefault - {{.Variable}}.Websocket = monitor.New() - {{.Variable}}.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - {{.Variable}}.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout -} - -// Setup takes in the supplied exchange configuration details and sets params -func ({{.Variable}} *{{.CapitalName}}) Setup(exch *config.ExchangeConfig) { - if !exch.Enabled { - {{.Variable}}.SetEnabled(false) - } else { - {{.Variable}}.Enabled = true - {{.Variable}}.AuthenticatedAPISupport = exch.AuthenticatedAPISupport - {{.Variable}}.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport - {{.Variable}}.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) - {{.Variable}}.SetHTTPClientTimeout(exch.HTTPTimeout) - {{.Variable}}.SetHTTPClientUserAgent(exch.HTTPUserAgent) - {{.Variable}}.RESTPollingDelay = exch.RESTPollingDelay - {{.Variable}}.Verbose = exch.Verbose - {{.Variable}}.Websocket.SetWsStatusAndConnection(exch.Websocket) - {{.Variable}}.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") - {{.Variable}}.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") - {{.Variable}}.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") - err := {{.Variable}}.SetCurrencyPairFormat() - if err != nil { - log.Fatal(err) - } - err = {{.Variable}}.SetAssetTypes() - if err != nil { - log.Fatal(err) - } - err = {{.Variable}}.SetAutoPairDefaults() - if err != nil { - log.Fatal(err) - } - err = {{.Variable}}.SetAPIURL(exch) - if err != nil { - log.Fatal(err) - } - err = {{.Variable}}.SetClientProxyAddress(exch.ProxyAddress) - if err != nil { - log.Fatal(err) - } - - // If the exchange supports websocket, update the below block - // err = {{.Variable}}.WebsocketSetup({{.Variable}}.WsConnect, - // exch.Name, - // exch.Websocket, - // {{.Name}}Websocket, - // exch.WebsocketURL) - // if err != nil { - // log.Fatal(err) - // } - // {{.Variable}}.WebsocketConn = &wshandler.WebsocketConnection{ - // ExchangeName: {{.Variable}}.Name, - // URL: {{.Variable}}.Websocket.GetWebsocketURL(), - // ProxyURL: {{.Variable}}.Websocket.GetProxyAddress(), - // Verbose: {{.Variable}}.Verbose, - // ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - // ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - // } - } -} -{{end}} diff --git a/tools/exchange_template/test_file.tmpl b/tools/exchange_template/test_file.tmpl deleted file mode 100644 index c31b03e7..00000000 --- a/tools/exchange_template/test_file.tmpl +++ /dev/null @@ -1,36 +0,0 @@ -{{define "test"}} -package {{.Name}} - -import ( - "testing" - - "github.com/thrasher-corp/gocryptotrader/config" -) - -// Please supply your own keys here for due diligence testing -const ( - testAPIKey = "" - testAPISecret = "" -) - -var {{.Variable}} {{.CapitalName}} - -func TestSetDefaults(t *testing.T) { - {{.Variable}}.SetDefaults() -} - -func TestSetup(t *testing.T) { - cfg := config.GetConfig() - cfg.LoadConfig("../../testdata/configtest.json") - {{.Name}}Config, err := cfg.GetExchangeConfig("{{.CapitalName}}") - if err != nil { - t.Error("Test Failed - {{.CapitalName}} Setup() init error") - } - - {{.Name}}Config.AuthenticatedAPISupport = true - {{.Name}}Config.APIKey = testAPIKey - {{.Name}}Config.APISecret = testAPISecret - - {{.Variable}}.Setup({{.Name}}Config) -} -{{end}} diff --git a/tools/exchange_template/wrapper_file.tmpl b/tools/exchange_template/wrapper_file.tmpl deleted file mode 100644 index 2e169090..00000000 --- a/tools/exchange_template/wrapper_file.tmpl +++ /dev/null @@ -1,212 +0,0 @@ -{{define "wrapper"}} -package {{.Name}} - -import ( - "sync" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/currency/pair" - "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - log "github.com/thrasher-corp/gocryptotrader/logger" -) - -// Start starts the {{.CapitalName}} go routine -func ({{.Variable}} *{{.CapitalName}}) Start(wg *sync.WaitGroup) { - wg.Add(1) - go func() { - {{.Variable}}.Run() - wg.Done() - }() -} - -// Run implements the {{.CapitalName}} wrapper -func ({{.Variable}} *{{.CapitalName}}) Run() { - if {{.Variable}}.Verbose { -{{if .WS}} log.Debugf("%s Websocket: %s. (url: %s).\n", {{.Variable}}.GetName(), common.IsEnabled({{.Variable}}.Websocket.IsEnabled()), {{.Variable}}.Websocket.GetWebsocketURL()) {{end}} - log.Debugf("%s polling delay: %ds.\n", {{.Variable}}.GetName(), {{.Variable}}.RESTPollingDelay) - log.Debugf("%s %d currencies enabled: %s.\n", {{.Variable}}.GetName(), len({{.Variable}}.EnabledPairs), {{.Variable}}.EnabledPairs) - } -} - -// UpdateTicker updates and returns the ticker for a currency pair -func ({{.Variable}} *{{.CapitalName}}) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { - var tickerPrice ticker.Price - // NOTE EXAMPLE FOR GETTING TICKER PRICE - //tick, err := {{.Variable}}.GetTickers() - //if err != nil { - // return tickerPrice, err - //} - - //for _, x := range {{.Variable}}.GetEnabledCurrencies() { - //curr := exchange.FormatExchangeCurrency({{.Variable}}.Name, x) - //for y := range tick { - // if tick[y].Symbol == curr.String() { - // tickerPrice.Pair = x - // tickerPrice.Ask = tick[y].AskPrice - // tickerPrice.Bid = tick[y].BidPrice - // tickerPrice.High = tick[y].HighPrice - // tickerPrice.Last = tick[y].LastPrice - // tickerPrice.Low = tick[y].LowPrice - // tickerPrice.Volume = tick[y].Volume - // ticker.ProcessTicker({{.Variable}}.Name, x, &tickerPrice, assetType) - // } - // } - //} - //return ticker.GetTicker({{.Variable}}.Name, p, assetType) - return tickerPrice, nil // NOTE DO NOT USE AS RETURN -} - -// GetTickerPrice returns the ticker for a currency pair -func ({{.Variable}} *{{.CapitalName}}) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker({{.Variable}}.GetName(), p, assetType) - if err != nil { - return {{.Variable}}.UpdateTicker(p, assetType) - } - return tickerNew, nil -} - -// GetOrderbookEx returns orderbook base on the currency pair -func ({{.Variable}} *{{.CapitalName}}) GetOrderbookEx(currency currency.Pair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.Get({{.Variable}}.GetName(), currency, assetType) - if err != nil { - return {{.Variable}}.UpdateOrderbook(currency, assetType) - } - return ob, nil -} - -// UpdateOrderbook updates and returns the orderbook for a currency pair -func ({{.Variable}} *{{.CapitalName}}) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { - var orderBook orderbook.Base - //NOTE UPDATE ORDERBOOK EXAMPLE - //orderbookNew, err := {{.Variable}}.GetOrderBook(exchange.FormatExchangeCurrency({{.Variable}}.Name, p).String(), 1000) - //if err != nil { - // return orderBook, err - //} - - //for _, bids := range orderbookNew.Bids { - // orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: bids.Quantity, Price: bids.Price}) - //} - - //for _, asks := range orderbookNew.Asks { - // orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: asks.Quantity, Price: asks.Price}) - //} - - //orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType) - //return orderbook.Get({{.Variable}}.Name, p, assetType) - return orderBook, nil // NOTE DO NOT USE AS RETURN -} - -// GetAccountInfo retrieves balances for all enabled currencies for the -// {{.CapitalName}} exchange -func ({{.Variable}} *{{.CapitalName}}) GetAccountInfo() (exchange.AccountInfo, error) { - return exchange.AccountInfo{}, common.ErrNotYetImplemented -} - -// GetFundingHistory returns funding history, deposits and -// withdrawals -func ({{.Variable}} *{{.CapitalName}}) GetFundingHistory() ([]exchange.FundHistory, error) { - return nil, common.ErrNotYetImplemented -} - -// GetExchangeHistory returns historic trade data since exchange opening. -func ({{.Variable}} *{{.CapitalName}}) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { - return nil, common.ErrNotYetImplemented -} - -// SubmitOrder submits a new order -func ({{.Variable}} *{{.CapitalName}}) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (exchange.SubmitOrderResponse, error) { - return exchange.SubmitOrderResponse{}, common.ErrNotYetImplemented -} - -// ModifyOrder will allow of changing orderbook placement and limit to -// market conversion -func ({{.Variable}} *{{.CapitalName}}) ModifyOrder(action *exchange.ModifyOrder) (string, error) { - return "", common.ErrNotYetImplemented -} - -// CancelOrder cancels an order by its corresponding ID number -func ({{.Variable}} *{{.CapitalName}}) CancelOrder(order *exchange.OrderCancellation) error { - return common.ErrNotYetImplemented -} - -// CancelAllOrders cancels all orders associated with a currency pair -func ({{.Variable}} *{{.CapitalName}}) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { - return exchange.CancelAllOrdersResponse{}, common.ErrNotYetImplemented -} - -// GetOrderInfo returns information on a current open order -func ({{.Variable}} *{{.CapitalName}}) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - return exchange.OrderDetail{}, common.ErrNotYetImplemented -} - -// GetDepositAddress returns a deposit address for a specified currency -func ({{.Variable}} *{{.CapitalName}}) GetDepositAddress(cryptocurrency pair.CurrencyItem, accountID string) (string, error) { - return "", common.ErrNotYetImplemented -} - -// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is -// submitted -func ({{.Variable}} *{{.CapitalName}}) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { - return "", common.ErrNotYetImplemented -} - -// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is -// submitted -func ({{.Variable}} *{{.CapitalName}}) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { - return "", common.ErrNotYetImplemented -} - -// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is -// submitted -func ({{.Variable}} *{{.CapitalName}}) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { - return "", common.ErrNotYetImplemented -} - -// GetWebsocket returns a pointer to the exchange websocket -func ({{.Variable}} *{{.CapitalName}}) GetWebsocket() (*exchange.Websocket, error) { - return nil, common.ErrNotYetImplemented -} - -// GetActiveOrders retrieves any orders that are active/open -func ({{.Variable}} *{{.CapitalName}}) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - return nil, common.ErrNotYetImplemented -} - -// GetOrderHistory retrieves account order information -// Can Limit response to specific order status -func ({{.Variable}} *{{.CapitalName}}) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - return nil, common.ErrNotYetImplemented -} - -// GetFeeByType returns an estimate of fee based on the type of transaction -func ({{.Variable}} *{{.CapitalName}}) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { - return 0, common.ErrNotYetImplemented -} - -// SubscribeToWebsocketChannels appends to ChannelsToSubscribe -// which lets websocket.manageSubscriptions handle subscribing -func ({{.Variable}} *{{.CapitalName}}) SubscribeToWebsocketChannels(channels []monitor.WebsocketChannelSubscription) error { - {{.Variable}}.Websocket.SubscribeToChannels(channels) - return nil -} - -// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe -// which lets websocket.manageSubscriptions handle unsubscribing -func ({{.Variable}} *{{.CapitalName}}) UnsubscribeToWebsocketChannels(channels []monitor.WebsocketChannelSubscription) error { - {{.Variable}}.Websocket.UnubscribeToChannels(channels) - return nil -} - -// GetSubscriptions returns a copied list of subscriptions -func ({{.Variable}} *{{.CapitalName}}) GetSubscriptions() ([]monitor.WebsocketChannelSubscription, error) { - return nil, common.ErrNotYetImplemented -} - -// AuthenticateWebsocket sends an authentication message to the websocket -func ({{.Variable}} *{{.CapitalName}}) AuthenticateWebsocket() error { - return common.ErrNotYetImplemented -} - -{{end}} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 00000000..40e96ef7 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,48 @@ +package utils + +import ( + "errors" + "path/filepath" + "runtime" +) + +const ( + defaultTLSDir = "tls" +) + +// Util vars +var ( + ErrGoMaxProcsFailure = errors.New("failed to set GOMAXPROCS") +) + +// AdjustGoMaxProcs sets the runtime GOMAXPROCS val +// Since Go 1.5, Go will use the total number of logical processors that the +// system has available. Caveats to this are if someone has set the GOMAXPROCS +// env var set or wish to limit usage of the number of logical processors +// between a range from 1 to NumCPUs +func AdjustGoMaxProcs(procs int) error { + // Check for default settings, plus respecting GOMAXPROCS env but + // don't allow for values which will cause thread contention + n := runtime.NumCPU() + if procs == runtime.GOMAXPROCS(-1) { + if procs <= n { + return nil + } + } + + // Sanitise the procs value (defaults to NumCPUs) + if procs < 1 || procs > n { + procs = n + } + + runtime.GOMAXPROCS(procs) + if i := runtime.GOMAXPROCS(procs); i != procs { + return ErrGoMaxProcsFailure + } + return nil +} + +// GetTLSDir returns the default TLS dir +func GetTLSDir(dir string) string { + return filepath.Join(dir, defaultTLSDir) +} diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 00000000..3bbf1569 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,57 @@ +package utils + +import ( + "fmt" + "runtime" + "testing" +) + +func TestAdjustGoMaxProcs(t *testing.T) { + // Test default settings + curr := runtime.GOMAXPROCS(-1) + numCPUs := runtime.NumCPU() + + // This func both checks for an error of AdjustGoMaxProcs, plus + // ensures that the value it sets is the one that is expected + checker := func(setting, expected int) error { + if err := AdjustGoMaxProcs(setting); err != nil { + return err + } + if i := runtime.GOMAXPROCS(expected); i != expected { + return fmt.Errorf("expected %d, got %d", expected, i) + } + return nil + } + + tester := []struct { + Setting int + Expected int + }{ + { + // Test setting to current runtime val + Setting: curr, + Expected: curr, + }, + { + // Test setting to num of logical CPUs + Setting: numCPUs, + Expected: numCPUs, + }, + { + // Test crazy value and make sure it defaults to numCPUs + Setting: 1000, + Expected: numCPUs, + }, + { + // Test another crazy value and make sure it defaults to numCPUs + Setting: -1, + Expected: numCPUs, + }, + } + + for x := range tester { + if err := checker(tester[x].Setting, tester[x].Expected); err != nil { + t.Errorf("%d failed. %s", x, err) + } + } +} diff --git a/web/README.md b/web/README.md index 351ea81f..a4887b58 100644 --- a/web/README.md +++ b/web/README.md @@ -98,4 +98,3 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** - diff --git a/web/src/app/pages/settings/settings.component.html b/web/src/app/pages/settings/settings.component.html index e61f9d24..5dd47adb 100644 --- a/web/src/app/pages/settings/settings.component.html +++ b/web/src/app/pages/settings/settings.component.html @@ -226,13 +226,13 @@ Enabled
- + - + - +

diff --git a/web/src/app/services/websocket-response-handler/websocket-response-handler.service.ts b/web/src/app/services/websocket-response-handler/websocket-response-handler.service.ts index 4618cf65..ec5603d8 100644 --- a/web/src/app/services/websocket-response-handler/websocket-response-handler.service.ts +++ b/web/src/app/services/websocket-response-handler/websocket-response-handler.service.ts @@ -5,7 +5,7 @@ import { Observable, Subject } from 'rxjs/Rx'; import { WebsocketService } from './../../services/websocket/websocket.service'; import { WebSocketMessage } from './../../shared/classes/websocket'; -const WEBSOCKET_URL = 'ws://localhost:9050/ws'; +const WEBSOCKET_URL = 'ws://localhost:9051/ws'; @NgModule({ })