From ace5e64c10dcf57032d2b63752e3e83e1c055d28 Mon Sep 17 00:00:00 2001 From: samuael <39623015+samuael@users.noreply.github.com> Date: Fri, 12 Aug 2022 08:30:11 +0300 Subject: [PATCH] exchanges: Add Binance.us support (#949) * public endpoints methods added * Completing mapping REST endpoints * Binanceus Wrapper methods -Partially * BinaWra functions with test funs; Not Completed * Finalizing wrapper methods & test * Finalizing wrapper methods & test * Fix & Complete wrapper functions * Adding Stream Datas * WS Test functions * CI: Fix golangci-lint linter issues * CI: Fix reverting unnessesary changes and type conversion issues * CI: Fix reverting unnessesary changes and type conversion issues * CI: Fix comment, method use, error handling, and code handling issues * build(deps): bump github.com/urfave/cli/v2 from 2.4.0 to 2.4.8 (#932) Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.4.0 to 2.4.8. - [Release notes](https://github.com/urfave/cli/releases) - [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/urfave/cli/compare/v2.4.0...v2.4.8) --- updated-dependencies: - dependency-name: github.com/urfave/cli/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump google.golang.org/grpc from 1.45.0 to 1.46.0 (#931) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.45.0 to 1.46.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.45.0...v1.46.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * asset: bitmask type optimisation (#922) * asset: basic optim. bitmask * glorious: nits * currency: forgot parralel in testttttt * ticker/orderbook: test fixes * engine/rpcserver: fix and expand tests * test: use `T.TempDir` to create temporary test directory (#934) * test: use `T.TempDir` to create temporary test directory This commit replaces `ioutil.TempDir` with `t.TempDir` in tests. The directory created by `t.TempDir` is automatically removed when the test and all its subtests complete. Prior to this commit, temporary directory created using `ioutil.TempDir` needs to be removed manually by calling `os.RemoveAll`, which is omitted in some tests. The error handling boilerplate e.g. defer func() { if err := os.RemoveAll(dir); err != nil { t.Fatal(err) } } is also tedious, but `t.TempDir` handles this for us nicely. Reference: https://pkg.go.dev/testing#T.TempDir Signed-off-by: Eng Zer Jun * test: fix TestEncryptTwiceReusesSaltButNewCipher on Windows Signed-off-by: Eng Zer Jun * test: fix TestCheckConnection on Windows Signed-off-by: Eng Zer Jun * test: fix TestRPCServer_GetTicker_LastUpdatedNanos on Windows Signed-off-by: Eng Zer Jun * test: cleanup TestGenerateReport Signed-off-by: Eng Zer Jun * account: storage, processing and method on balances update (#916) * account: update account storage, retrieval and implement alert functionality when a currency change occurs. * account: Add cancel channel * account: remove old code * account: don't embed mutex * Update exchanges/account/account.go Co-authored-by: Scott * Update exchanges/account/account.go Co-authored-by: Scott * Update exchanges/account/account.go Co-authored-by: Scott * account: addr nits * account: Pull out test into indiv. * account: Add test for update method * account: add no change to test * Update exchanges/account/account.go Co-authored-by: Scott * glorious: nits * account/portfolio: differentiate between asset type segregation and default to spot holdings. * glorious: nits * thrasher: nit * ticker: fix spelling * Update engine/portfolio_manager.go Co-authored-by: Adrian Gallagher * thrasher: nits Co-authored-by: Scott Co-authored-by: Adrian Gallagher * build(deps): bump github.com/urfave/cli/v2 from 2.4.8 to 2.5.1 (#936) Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.4.8 to 2.5.1. - [Release notes](https://github.com/urfave/cli/releases) - [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/urfave/cli/compare/v2.4.8...v2.5.1) --- updated-dependencies: - dependency-name: github.com/urfave/cli/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * orderbook/buffer: data integrity and resubscription pass (#910) * orderbook/buffer: data integrity and resubscription pass * btcmarkets: REMOVE THAT LIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIINE!!!!!!!!!!!!!!!!! * buffer: reinstate publish, refaactor, invalidate more and comments * buffer/orderbook: improve update and snapshot performance. Move Update type to orderbook package to util. pointer through entire function calls. (cleanup). Change action string to uint8 for easier comparison. Add parsing helper. Update current test benchmark comments. * dispatch: change publish func to variadic id param * dispatch: remove sender receiver wait time as this adds overhead and complexity. update tests. * dispatch: don't create pointers for every job container * rpcserver: fix assertion issues with data publishing change * linter: fixes * glorious: nits addr * depth: change validation handling to incorporate and store err * linter: fix more issues * dispatch: fix race * travis: update before fetching * depth: wrap and return wrapped error in invalidate call and fix tests * btcmarkets: fix commenting * workflow: check * workflow: check * orderbook: check error * buffer/depth: return invalidation error and fix tests * gctcli: display errors on orderbook streams * buffer: remove unused types * orderbook/bitmex: shift function to bitmex * orderbook: Add specific comments to unexported functions that don't have locking require locking. * orderbook: restrict published data functionality to orderbook.Outbound interface * common: add assertion failure helper for error * dispatch: remove atomics, add mutex protection, remove add/remove worker, redo main tests * dispatch: export function * engine: revert and change sub logger to manager * engine: remove old test * dispatch: add common variable ;) * btcmarket: don't overflow int in tests on 32bit systems * ci: force 1.17.7 usage for go * Revert "ci: force 1.17.7 usage for go" This reverts commit af2f95563bf218cf2b9f36a9fcf3258e2c6a2d91. * golangci: bump version add and remove linter items * Revert "golangci: bump version add and remove linter items" This reverts commit 3c98bffc9d030e39faca0387ea40c151df2ab06b. * dispatch: remove unsused mutex from mux * order: slight optimizations * nits: glorious * dispatch: fix regression on uuid generation and input inline with master * linter: fix * linter: fix * glorious: nit - rm slice segration * account: fix test after merge * coinbasepro: revert change * account: close channel instead of needing a receiver, push alert in routine to prepare for waiter. Co-authored-by: Ryan O'Hara-Reid * exchange/wrapper: add GetServerTime() for exchange analytics (#938) * exchange/wrapper: add GetServerTime() for exchange analytics * binance: fix linter issue * glorious: nits * glorious: nits rides again * thrasher: nits implement huobi * thrasher: nits add to exchange_wrapper_issues cmd * order: slight optimizations (#917) * order: slight optimizations * orders: add benchmarks, small optimize and change order side to uin8 for comparitive optimizations. * orders: continue to convert string type -> uint * orders/backtester: interim move type to orders package, later can expand or deprecate. * orders: handle errors * orders: optimize filters and remove error returns when its clearly not needed * orders: remove log call * backtester: zero value check * orders/futures: zero value -> flag * linter: fix * linter: more fixes * linters: rides again * glorious: nits * common: Add zero value unix check for time values; also addresses glorious nits * glorious scott: nits Co-authored-by: Ryan O'Hara-Reid * btcm: add order execution limit wrapper support (#941) * btcm: add in order execution limit fetching * btcm/test: add t.Parrrrrralllleeellllllllll * btcm/wrapper: add update on startup * glorious: nits * thrasher: nit add status field * build(deps): bump github.com/urfave/cli/v2 from 2.5.1 to 2.6.0 (#944) Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.5.1 to 2.6.0. - [Release notes](https://github.com/urfave/cli/releases) - [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/urfave/cli/compare/v2.5.1...v2.6.0) --- updated-dependencies: - dependency-name: github.com/urfave/cli/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * proto/lint: Add protobuf GitHub action and linter (#943) * Buf upgrades * Buf format and basic endpoint fixes * gRPC linter fixes * Amend buf.yaml linter exceptions * Update README * Freshly generated gRPC code after depends update * Nitterinos * ordermanager: fix test error introduced in #917 (#942) * ordermanager: fix residual test issue from #917 and reduce some racey action * glorious: nits; also removed functions that weren't being used and were unexported * rm: pew * linter: fix issues * glourious: nits * credentials: fix test issue with racey racey horse basey * engine: Add websocket data handler register function (#935) * engine: Add websocket interceptor register function * Update engine/engine.go Co-authored-by: Scott * Update engine/websocketroutine_manager_types.go Co-authored-by: Scott * engine/websock: switch to data handler function register and range over handlers to still include default gct handling * engine/websocket: change name * glorious: nits * linter: fix * glorious: nits Co-authored-by: Scott * btcm: add modify order functionality, change return to pointer (#940) * btcm: add modify order functionality, change return to pointer * glorious: nits * glorious: nits * btcm: Adjust function name * thrasher: nits * thrasher: nits cont... * request: adds WithVerbose function to package to add verbosity to request context (#950) * request: adds WithVerbose function to package to add verbosity to request context * request: add t.Parr.... * thrasher: nits * build(deps): bump google.golang.org/grpc from 1.46.0 to 1.46.2 (#951) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.46.0 to 1.46.2. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.46.0...v1.46.2) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * exchange: expose GetCredentials() and split GetAuthenticatedAPISupport() (#954) * exchange/wrapper: expose GetCredentials func to IBotInterface * exchanges: split up GetAuthenticatedAPISupport into specific function calls, organize IBotExchange functionality getter functions * interface: change name - RPCSercer: rm GetBase func call. * glorious: nits (fix panic) Co-authored-by: Ryan O'Hara-Reid * CI: merge fixes * CI: fixing github generated lint issues * orders: Add method for creating cancel struct from order details (#947) * orders: Add method for creating cancel struct from order details * orders: remove uneeded fields * glorious: nit * grpc: add shutdown call for external management (#957) * grpc: add shutdown call for external management * go mod: tidy * glorious: suggestion * Update engine/engine.go Co-authored-by: Adrian Gallagher * Update engine/rpcserver.go Co-authored-by: Adrian Gallagher * Update main.go Co-authored-by: Adrian Gallagher * Update engine/rpcserver.go Co-authored-by: Scott Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Adrian Gallagher Co-authored-by: Scott * build(deps): bump github.com/lib/pq from 1.10.5 to 1.10.6 (#960) Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.10.5 to 1.10.6. - [Release notes](https://github.com/lib/pq/releases) - [Commits](https://github.com/lib/pq/compare/v1.10.5...v1.10.6) --- updated-dependencies: - dependency-name: github.com/lib/pq dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github.com/urfave/cli/v2 from 2.6.0 to 2.8.0 (#958) Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.6.0 to 2.8.0. - [Release notes](https://github.com/urfave/cli/releases) - [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/urfave/cli/compare/v2.6.0...v2.8.0) --- updated-dependencies: - dependency-name: github.com/urfave/cli/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github.com/grpc-ecosystem/grpc-gateway/v2 (#963) Bumps [github.com/grpc-ecosystem/grpc-gateway/v2](https://github.com/grpc-ecosystem/grpc-gateway) from 2.10.0 to 2.10.2. - [Release notes](https://github.com/grpc-ecosystem/grpc-gateway/releases) - [Changelog](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/.goreleaser.yml) - [Commits](https://github.com/grpc-ecosystem/grpc-gateway/compare/v2.10.0...v2.10.2) --- updated-dependencies: - dependency-name: github.com/grpc-ecosystem/grpc-gateway/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * orders: adds method to retrieve snapshot of order execution limit values (#946) * orders: add method to Limit to retrieve order execution limit snapshots * currency/btcmarkets: add error and update field name to standard * linter: fix * limts: don't return pointer * limit: Add notes * glorious: nits * linter: fix * limit: reinstate nil check * exchanges: change field names to be more consistent (@thrasher-) suggestion Co-authored-by: Ryan O'Hara-Reid * orders: Add derive modify struct method from order.Detail (#948) * orders: Add derive modify struct method to order.Detail and then subsequent method to derive and standardize response details * exchanges: call modify method in wrappers * linter: fixes * engine/wsroutineman: remove print summary * glorious: nits, removed modifyOrder functionality for Bithumb. There are not docs to support this. * Update exchanges/order/orders.go Co-authored-by: Scott * glorious: nits Co-authored-by: Scott * build(deps): bump github.com/spf13/viper from 1.11.0 to 1.12.0 (#965) Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.11.0 to 1.12.0. - [Release notes](https://github.com/spf13/viper/releases) - [Commits](https://github.com/spf13/viper/compare/v1.11.0...v1.12.0) --- updated-dependencies: - dependency-name: github.com/spf13/viper dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github.com/urfave/cli/v2 from 2.8.0 to 2.8.1 (#964) Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.8.0 to 2.8.1. - [Release notes](https://github.com/urfave/cli/releases) - [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/urfave/cli/compare/v2.8.0...v2.8.1) --- updated-dependencies: - dependency-name: github.com/urfave/cli/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * orders: Add methods to derive SubmitResponse and Detail types (#955) * orders: deprecate SubmitResponse return and change to *order.Detail construct detail from order.Submit struct * orders: add coverage, fix tests * coinut: rm test for checking * orders: revert change for return and change field ID to a more explicit name OrderID * orders: Add method to see if the order was placed * order: change field name in Cancel type to be more explicit * orders: standardize field -> OrderID * backtester: populate change * orders: add test * gctscript: fix field name * linter: fix issues * linter: more fixes * linter: forever * exchanges_tests: populate order.Submit field exchange name * Update exchanges/order/order_types.go Co-authored-by: Scott * Update exchanges/order/orders.go Co-authored-by: Scott * glorious: nits * glorious: nits * thrasher: nits Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott * build(deps): bump bufbuild/buf-setup-action from 1.4.0 to 1.5.0 (#973) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.4.0 to 1.5.0. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.4.0...v1.5.0) --- updated-dependencies: - dependency-name: bufbuild/buf-setup-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump google.golang.org/grpc from 1.46.2 to 1.47.0 (#972) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.46.2 to 1.47.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.46.2...v1.47.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github.com/grpc-ecosystem/grpc-gateway/v2 (#971) Bumps [github.com/grpc-ecosystem/grpc-gateway/v2](https://github.com/grpc-ecosystem/grpc-gateway) from 2.10.2 to 2.10.3. - [Release notes](https://github.com/grpc-ecosystem/grpc-gateway/releases) - [Changelog](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/.goreleaser.yml) - [Commits](https://github.com/grpc-ecosystem/grpc-gateway/compare/v2.10.2...v2.10.3) --- updated-dependencies: - dependency-name: github.com/grpc-ecosystem/grpc-gateway/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github.com/d5/tengo/v2 from 2.10.1 to 2.11.2 (#975) Bumps [github.com/d5/tengo/v2](https://github.com/d5/tengo) from 2.10.1 to 2.11.2. - [Release notes](https://github.com/d5/tengo/releases) - [Changelog](https://github.com/d5/tengo/blob/master/.goreleaser.yml) - [Commits](https://github.com/d5/tengo/compare/v2.10.1...v2.11.2) --- updated-dependencies: - dependency-name: github.com/d5/tengo/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github.com/urfave/cli/v2 from 2.8.1 to 2.10.1 (#979) Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.8.1 to 2.10.1. - [Release notes](https://github.com/urfave/cli/releases) - [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/urfave/cli/compare/v2.8.1...v2.10.1) --- updated-dependencies: - dependency-name: github.com/urfave/cli/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github.com/d5/tengo/v2 from 2.11.2 to 2.12.0 (#978) Bumps [github.com/d5/tengo/v2](https://github.com/d5/tengo) from 2.11.2 to 2.12.0. - [Release notes](https://github.com/d5/tengo/releases) - [Changelog](https://github.com/d5/tengo/blob/master/.goreleaser.yml) - [Commits](https://github.com/d5/tengo/compare/v2.11.2...v2.12.0) --- updated-dependencies: - dependency-name: github.com/d5/tengo/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * CI: adding WS and REST tests, and minor fixes * CI: fixes on available asset and related minor issues * CI: fixes on endpoint function, test functions, and types * CI: updating templates and slight fixes * CI: updating slight fixes on tests and withdraws Request model * build(deps): bump styfle/cancel-workflow-action from 0.9.1 to 0.10.0 (#985) Bumps [styfle/cancel-workflow-action](https://github.com/styfle/cancel-workflow-action) from 0.9.1 to 0.10.0. - [Release notes](https://github.com/styfle/cancel-workflow-action/releases) - [Commits](https://github.com/styfle/cancel-workflow-action/compare/0.9.1...0.10.0) --- updated-dependencies: - dependency-name: styfle/cancel-workflow-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump bufbuild/buf-setup-action from 1.5.0 to 1.6.0 (#984) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.5.0 to 1.6.0. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.5.0...v1.6.0) --- updated-dependencies: - dependency-name: bufbuild/buf-setup-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * backtester: Futures handling & FTX Cash and Carry example strategy (#930) * implements futures functions and GRPC functions on new branch * lint and test fixes * Fix uneven split pnl. Adds collateral weight test. docs. New clear func * Test protection if someone has zero collateral * Uses string instead of double for accuracy * Fixes old code panic * context, match, docs * Addresses Shazniterinos, var names, expanded tests * Returns subaccount name, provides USD values when offlinecalc * Fixes oopsie * Fixes cool bug which allowed made up subaccount results * Subaccount override on FTX, subaccount results for collateral * Strenghten collateral account info checks. Improve FTX test * English is my first language * Fixes oopsies * Adds some conceptual futures order details to track PNL * Initial design of future order processing in the backtester * Introduces futures concept for collateral and spot/futures config diffs * Fixes most tests * Simple designs for collateral funding pair concept * Expands interface use so much it hurts * Implements more collateral interfaces * Adds liquidation, adds strategy, struggles with Binance * Attempts at getting FTX to work * Adds calculatePNL as a wrapper function and adds an `IsFutures` asset check * Successfully loads backtester with collateral currency * Fails to really get much going for supporting futures * Merges master changes * Fleshes out how FTX processes collateral * Further FTX collateral workings * hooks up more ftx collateral and pnl calculations * more funcs to flesh out handling * Adds more links, just can't fit the pieces together :( * Greatly expands futures order processing * Fleshes out position tracker to also handle asset and exchange +testing * RM linkedOrderID. rn positioncontroller, unexport * Successfully tracks futures order positions * Fails to calculate PNL * Calculates pnl from orders accurately with exception to flipping orders * Calculates PNL from orders * Adds another controller layer to make it ez from orderstore * Backtester now compiles. Adds test coverage * labels things add scaling collateral test * Calculates pnl in line with fees * Mostly accurate PNL, with exception to appending with diff prices * Adds locks, adds rpc function * grpc implementations * Gracefully handles rpc function * beautiful tests! * rejiggles tests to polish * Finishes FTX testing, adds comments * Exposes collateral calculations to rpc * Adds commands and testing for rpcserver.go functions * Increase testing and fix up backtester code * Returns cool changes to original branch * end of day fixes * Fixing some tests * Fixing tests :tada: * Fixes all the tests * Splits the backtester setup and running into different files * Merge, minor fixes * Messing with some strategy updates * Failed understanding at collateral usage * Begins the creation of cash and carry strategy * Adds underlying pair, adds filldependentevent for futures * Completes fill prerequsite event implementation. Can't short though * Some bug fixes * investigating funds * CAN NOW CREATE A SHORT ORDER * Minor change in short size * Fixes for unrealised PNL & collateral rendering * Fixes lint and tests * Adds some verbosity * Updates to pnl calc * Tracks pnl for short orders, minor update to strategy * Close and open event based on conditions * Adds pnl data for currency statistics * Working through PNL calculation automatically. Now panics * Adds tracking, is blocked from design * Work to flesh out closing a position * vain attempts at tracking zeroing out bugs * woww, super fun new subloggers :tada: * Begins attempt at automatically handling contracts and collateral based on direction * Merge master + fixes * Investigating issues with pnl and holdings * Minor pnl fixes * Fixes future position sizing, needs contract sizing * Can render pnl results, focussing on funding statistics * tracking candles for futures, but why not btc * Improves funding statistics * Colours and stats * Fixes collateral and snapshot bugs * Completes test * Fixes totals bug * Fix double buy, expand stats, fixes usd totals, introduce interface * Begins report formatting and calculations * Appends pnl to receiving curr. Fixes map[time]. accurate USD * Improves report output rendering * PNL stats in report. New tests for futures * Fixes existing tests before adding new coverage * Test coverage * Completes portfolio coverage * Increase coverage exchange, portfolio. fix size bug. NEW CHART * WHAT IS GOING ON WITH PNL * Fixes PNL calculation. Adds ability to skip om futures tracking * minor commit before merge * Adds basic liquidation to backtester * Changes liquidation to order based * Liquidationnnnnn * Further fleshes out liquidations * Completes liquidations in a honorable manner. Adds AppendReasonf * Beginnings of spot futures gap chart. Needs to link currencies to render difference * Removes fake liquidation. Adds cool new chart * Fixes somet tests,allows for zero fee value v nil distinction,New tests * Some annoying test fixes that took too long * portfolio coverage * holding coverage, privatisation funding * Testwork * boring tests * engine coverage * More backtesting coverage * Funding, strategy, report test coverage * Completes coverage of report package * Documentation, fixes some assumptions on asset errors * Changes before master merge * Lint and Tests * defaults to non-coloured rendering * Chart rendering * Fixes surprise non-local-lints * Niterinos to the extremeos * Fixes merge problems * The linter splintered across the glinting plinths * Many nits addressed. Now sells spot position on final candle * Adds forgotten coverage * Adds ability to size futures contracts to match spot positions. * fixes order sell sizing * Adds tests to sizing. Fixes charting issue * clint splintered the linters with flint * Improves stats, stat rendering * minifix * Fixes tests and fee bug * Merge fixeroos * Microfixes * Updates orderPNL on first Correctly utilises fees. Adds committed funds * New base funcs. New order summary * Fun test updates * Fix logo colouring * Fixes niteroonies * Fix report * BAD COMMIT * Fixes funding issues.Updates default fee rates.Combines cashcarry case * doc regen * Now returns err * Fixes sizing bug issue introduced in PR * Fixes fun fee/total US value bug * Fix chart bug. Show log charts with disclaimer * sellside fee * fixes fee and slippage view * Fixed slippage price issue * Fixes calculation and removes rendering * Fixes stats and some rendering * Merge fix * Fixes merge issues * go mod tidy, lint updates * New linter attempt * Version bump in appveyor and makefile * Regex filename, config fixes, template h2 fixes * Removes bad stats. * neatens config builder. Moves filename generator * Fixes issue where linter wants to fix my spelling * Fixes pointers and starts * build(deps): bump github.com/urfave/cli/v2 from 2.10.1 to 2.10.3 (#982) Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.10.1 to 2.10.3. - [Release notes](https://github.com/urfave/cli/releases) - [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/urfave/cli/compare/v2.10.1...v2.10.3) --- updated-dependencies: - dependency-name: github.com/urfave/cli/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * alert: Add optimizations (#939) * alert: Add optimizations * alert: add basic benchmarks * alert: fix linter issue * documentation: change to text/template as html/template escapes to protect against code injection. Add readme.md for alert. * README: Add package name * alert: link up with engine settings * request: isVerbose refactor * Update exchanges/alert/alert_test.go Co-authored-by: Scott * Update exchanges/alert/alert.go Co-authored-by: Scott * glorious: nits * glorious: fun police * documentation: regen Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott * technical_analysis: TWAP & VWAP + TA methods to candles and link to existing RPC server for GCTCLI prototyping (#970) * kline: add weighted price helpers for candles * twap/vwap: basic implementation and hook to rpc for protype * ta: cont implementation. (WIP) * kline: Add tests * kline: add helpers * ta: full impl. * kline: remove support for macd and add in correlation-coefficient handling * rpc: change naming convention * linter: fix * protolinter: fix * linter: ++ * kline: reinstate macd handling after adding in check * glorious: nits * gctcl: linter * Update exchanges/kline/weighted_price.go Co-authored-by: Scott * glorious: nits * glorious: nits v2.0 * kline: fix test * huobi-tests: shift from next quarter to this weeks contracts as they were erroring out in tests. * btcmarkets: update supported kline intervals * zb: fix test * rpcserver: fix bug and tests Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott * build(deps): bump github.com/urfave/cli/v2 from 2.10.3 to 2.11.0 (#993) Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.10.3 to 2.11.0. - [Release notes](https://github.com/urfave/cli/releases) - [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/urfave/cli/compare/v2.10.3...v2.11.0) --- updated-dependencies: - dependency-name: github.com/urfave/cli/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * FTX: Margin lending/borrow rate history (#981) * Adds lending rates/borrows to FTX and the command * Movements, renames, rpc test * Fleshing out rpc response * Allows rpcserver to calculate offline (but not gctcli). Expands tests * rn structs. add exchange_wrapper_issues support * Adds a nice yearly rate * Surprise yearly borrow rate! * Rn+Mv to margin package. Fixes some serious whoopsies * Adds average lend/borrow rates instead of sum * rm oopsie whoopsie * This is what the linter was having an issue with * re-gen * lintl * niteroos * build(deps): bump google.golang.org/grpc from 1.47.0 to 1.48.0 (#995) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.47.0 to 1.48.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.47.0...v1.48.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * account: segregate holdings by credentials for future multi-key management (#956) * exchanges/account: shift credentials to account package and segregate funds to keys * merge: fixes * linter: fix * Update exchanges/account/account.go Co-authored-by: Scott * glorious: nits + protection for string panic * glorious_suggestion: add method for matching keys * linter: fix tests * account: add protected method for credentials minimizing access, display full account details to rpc. * linter: spelling kweeeeeeen * accounts/portfolio: clean/check portfolio code and quickly check balances from change. Add protected method for future matching. * accounts: theres no point in pointerising everything * linter: ok pointerise this then... * exchanges: fix regression add in little notes. * glorious: nits * Update exchanges/account/credentials.go Co-authored-by: Scott * Update exchanges/account/credentials_test.go Co-authored-by: Scott * Update exchanges/account/credentials_test.go Co-authored-by: Scott * glorious: nits * gloriously: fix glorious glorious test gloriously Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott * CI: fixing linter issue and conflicts * CI: fixing ratelimit and other slight issues * build(deps): bump github.com/grpc-ecosystem/grpc-gateway/v2 (#998) Bumps [github.com/grpc-ecosystem/grpc-gateway/v2](https://github.com/grpc-ecosystem/grpc-gateway) from 2.10.3 to 2.11.0. - [Release notes](https://github.com/grpc-ecosystem/grpc-gateway/releases) - [Changelog](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/.goreleaser.yml) - [Commits](https://github.com/grpc-ecosystem/grpc-gateway/compare/v2.10.3...v2.11.0) --- updated-dependencies: - dependency-name: github.com/grpc-ecosystem/grpc-gateway/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github.com/d5/tengo/v2 from 2.12.0 to 2.12.1 (#997) Bumps [github.com/d5/tengo/v2](https://github.com/d5/tengo) from 2.12.0 to 2.12.1. - [Release notes](https://github.com/d5/tengo/releases) - [Changelog](https://github.com/d5/tengo/blob/master/.goreleaser.yml) - [Commits](https://github.com/d5/tengo/compare/v2.12.0...v2.12.1) --- updated-dependencies: - dependency-name: github.com/d5/tengo/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github.com/urfave/cli/v2 from 2.11.0 to 2.11.1 (#996) Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.11.0 to 2.11.1. - [Release notes](https://github.com/urfave/cli/releases) - [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/urfave/cli/compare/v2.11.0...v2.11.1) --- updated-dependencies: - dependency-name: github.com/urfave/cli/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * CI: fixing type and other slight issues * Cleanup after merge * Endpoints rate limit update Signed-off-by: Eng Zer Jun Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Eng Zer Jun Co-authored-by: Scott Co-authored-by: Adrian Gallagher Co-authored-by: Ryan O'Hara-Reid --- README.md | 1 + .../exchanges_templates/binanceus.tmpl | 106 + .../exchanges_trade_readme.tmpl | 1 + .../root_templates/root_readme.tmpl | 1 + common/common.go | 13 + common/common_test.go | 19 + config_example.json | 84 + docs/MULTICHAIN_TRANSFER_SUPPORT.md | 1 + docs/OHLCV.md | 1 + engine/exchange_manager.go | 4 +- engine/exchange_manager_test.go | 2 +- exchanges/binanceus/README.md | 140 ++ exchanges/binanceus/binanceus.go | 2002 +++++++++++++++++ exchanges/binanceus/binanceus_test.go | 1943 ++++++++++++++++ exchanges/binanceus/binanceus_types.go | 1180 ++++++++++ exchanges/binanceus/binanceus_websocket.go | 1098 +++++++++ exchanges/binanceus/binanceus_wrapper.go | 996 ++++++++ exchanges/binanceus/ratelimit.go | 130 ++ exchanges/binanceus/type_convert.go | 617 +++++ exchanges/support.go | 1 + exchanges/trade/README.md | 1 + portfolio/withdraw/withdraw_types.go | 3 + testdata/configtest.json | 84 + testdata/exchangelist.csv | 1 + 24 files changed, 8427 insertions(+), 2 deletions(-) create mode 100644 cmd/documentation/exchanges_templates/binanceus.tmpl create mode 100644 exchanges/binanceus/README.md create mode 100644 exchanges/binanceus/binanceus.go create mode 100644 exchanges/binanceus/binanceus_test.go create mode 100644 exchanges/binanceus/binanceus_types.go create mode 100644 exchanges/binanceus/binanceus_websocket.go create mode 100644 exchanges/binanceus/binanceus_wrapper.go create mode 100644 exchanges/binanceus/ratelimit.go create mode 100644 exchanges/binanceus/type_convert.go diff --git a/README.md b/README.md index 4e63bbc6..518d6ad9 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | Exchange | REST API | Streaming API | FIX API | |----------|------|-----------|-----| | Alphapoint | Yes | Yes | NA | +| Binance.US| Yes | Yes | NA | | Binance| Yes | Yes | NA | | Bitfinex | Yes | Yes | NA | | Bitflyer | Yes | No | NA | diff --git a/cmd/documentation/exchanges_templates/binanceus.tmpl b/cmd/documentation/exchanges_templates/binanceus.tmpl new file mode 100644 index 00000000..5ddeeb80 --- /dev/null +++ b/cmd/documentation/exchanges_templates/binanceus.tmpl @@ -0,0 +1,106 @@ +{{define "exchanges binanceus" -}} +{{template "header" .}} +## Binanceus 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() == "Binanceus" { + 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 +} +``` + +### How to do Websocket public/private calls + +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} diff --git a/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl b/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl index 6cbce671..b9a6a168 100644 --- a/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl +++ b/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl @@ -42,6 +42,7 @@ _b in this context is an `IBotExchange` implemented struct_ | Exchange | Recent Trades via REST | Live trade updates via Websocket | Trade history via REST | |----------|------|-----------|-----| | Alphapoint | No | No | No | +| Binance.US | Yes | Yes | NA | | Binance| Yes | Yes | Yes | | Bitfinex | Yes | Yes | Yes | | Bitflyer | Yes | No | No | diff --git a/cmd/documentation/root_templates/root_readme.tmpl b/cmd/documentation/root_templates/root_readme.tmpl index 97a84f20..62fcd306 100644 --- a/cmd/documentation/root_templates/root_readme.tmpl +++ b/cmd/documentation/root_templates/root_readme.tmpl @@ -20,6 +20,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | Exchange | REST API | Streaming API | FIX API | |----------|------|-----------|-----| | Alphapoint | Yes | Yes | NA | +| Binance.US| Yes | Yes | NA | | Binance| Yes | Yes | NA | | Bitfinex | Yes | Yes | NA | | Bitflyer | Yes | No | NA | diff --git a/common/common.go b/common/common.go index 72918c14..d64c4cee 100644 --- a/common/common.go +++ b/common/common.go @@ -31,6 +31,11 @@ const ( defaultTimeout = time.Second * 15 ) +var ( + // emailRX represents email address matching pattern + emailRX = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") +) + // Vars for common.go operations var ( _HTTPClient *http.Client @@ -69,6 +74,14 @@ var ( ErrTypeAssertFailure = errors.New("type assert failure") ) +// MatchesEmailPattern ensures that the string is an email address by regexp check +func MatchesEmailPattern(value string) bool { + if len(value) < 3 || len(value) > 254 { + return false + } + return emailRX.MatchString(value) +} + // SetHTTPClientWithTimeout sets a new *http.Client with different timeout // settings func SetHTTPClientWithTimeout(t time.Duration) error { diff --git a/common/common_test.go b/common/common_test.go index 5d9cc17a..42453976 100644 --- a/common/common_test.go +++ b/common/common_test.go @@ -702,3 +702,22 @@ func TestGetAssertError(t *testing.T) { t.Fatalf("received: '%v' but expected: '%v'", err, ErrTypeAssertFailure) } } + +func TestMatchesEmailPattern(t *testing.T) { + success := MatchesEmailPattern("someone semail") + if success { + t.Error("MatchesEmailPattern() unexpected test validation result") + } + success = MatchesEmailPattern("someon esemail@gmail") + if success { + t.Error("MatchesEmailPattern() unexpected test validation result") + } + success = MatchesEmailPattern("123@gmail") + if !success { + t.Error("MatchesEmailPattern() unexpected test validation result") + } + success = MatchesEmailPattern("someonesemail@email.com") + if !success { + t.Error("MatchesEmailPattern() unexpected test validation result") + } +} diff --git a/config_example.json b/config_example.json index 98c4de98..3a230d2d 100644 --- a/config_example.json +++ b/config_example.json @@ -239,6 +239,90 @@ ] }, "exchanges": [ + { + "name": "Binanceus", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "baseCurrencies": "USD", + "currencyPairs": { + "bypassConfigFormatUpgrades": false, + "pairs": { + "spot": { + "assetEnabled": true, + "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,ADA-USDT", + "available": "BTC-USD,BCH-USD,LTC-USD,USDT-USD,BTC-USDT,ETH-USDT,BCH-USDT,LTC-USDT,BNB-USD,BNB-USDT,ETH-BTC,BNB-BTC,LTC-BTC,BCH-BTC,ADA-USD,BAT-USD,ETC-USD,XLM-USD,ZRX-USD,ADA-USDT,BAT-USDT,ETC-USDT,XLM-USDT,ZRX-USDT,LINK-USD,RVN-USD,DASH-USD,ZEC-USD,ALGO-USD,IOTA-USD,BUSD-USD,BTCB-USD,DOGE-USDT,WAVES-USD,ATOM-USDT,ATOM-USD,NEO-USDT,NEO-USD,VET-USDT,QTUM-USDT,QTUM-USD,ICX-USD,ENJ-USD,ONT-USD,ONT-USDT,ZIL-USD,ZILB-USD,VET-USD,BNBB-USD,ETHB-USD,ALGO-BUSD,XTZ-USD,XTZ-BUSD,HBAR-USD,HBAR-BUSD,OMG-USD,OMG-BUSD,MATIC-USD,MATIC-BUSD,XTZ-BTC,ADA-BTC,REP-BUSD,REP-USD,EOS-BUSD,EOS-USD,DOGE-USD,KNC-USD,KNC-USDT,VTHO-USDT,VTHO-USD,USDC-USD,COMP-USDT,COMP-USD,MANA-USD,HNT-USD,HNT-USDT,MKR-USD,MKR-USDT,DAI-USD,ONE-USDT,ONE-USD,BAND-USDT,BAND-USD,STORJ-USDT,STORJ-USD,UNI-USD,UNI-USDT,SOL-USD,SOL-USDT,LINK-BTC,VET-BTC,UNI-BTC,EGLD-USDT,EGLD-USD,PAXG-USDT,PAXG-USD,OXT-USDT,OXT-USD,ZEN-USDT,ZEN-USD,BTC-USDC,ONEB-USD,FIL-USDT,FIL-USD,AAVE-USDT,AAVE-USD,GRT-USDT,GRT-USD,SUSHI-USD,ANKR-USD,AMP-USD,SHIB-USDT,SHIB-BUSD,CRV-USDT,CRV-USD,AXS-USDT,AXS-USD,SOL-BTC,AVAX-USDT,AVAX-USD,CTSI-USDT,CTSI-USD,DOT-USDT,DOT-USD,YFI-USDT,YFI-USD,1INCH-USDT,1INCH-USD,FTM-USDT,FTM-USD,USDC-USDT,ETH-USDC,USDC-BUSD,MATIC-USDT,MANA-USDT,MANA-BUSD,ALGO-USDT,ADA-BUSD,SOL-BUSD,EOS-USDT,ENJ-USDT,NEAR-USDT,NEAR-BUSD,NEAR-USD,OMG-USDT,SUSHI-USDT,LRC-USDT,LRC-USD,LRC-BTC,KSHI-BUSD,LPT-USDT,LPT-BUSD,LPT-USD,POLY-USDT,POLY-BUSD,POLY-USD,POLY-BTC,MATIC-BTC,DOT-BTC,NMR-USDT,NMR-USD,SLP-USDT,ANT-USD,XNO-USD,CHZ-USDT,CHZ-USD,OGN-USDT,OGN-USD,GALA-USDT,GALA-USD,TLM-USDT,TLM-USD,SNX-USDT,SNX-USD,AUDIO-USDT,AUDIO-USD,ENS-USDT,MANA-BTC,ATOM-BTC,AVAX-BTC,WBTC-BTC,REQ-USDT,REQ-USD,APE-USDT,APE-USD,FLUX-USDT,FLUX-USD,TRX-BTC,TRX-BUSD,TRX-USDT,TRX-USD,COTI-USDT,COTI-USD,VOXEL-USDT,VOXEL-USD,RLC-USDT,RLC-USD,UST-USDT,UST-USD,BICO-USDT,BICO-USD,API3-USDT,API3-USD,ENS-USD,BTC-UST,BNT-USDT,BNT-USD,IMX-USDT,IMX-USD,SPELL-USDT,SPELL-USD,JASMY-USDT,JASMY-USD,FLOW-USDT,FLOW-USD,GTC-USDT,GTC-USD,BTC-BUSD,ZIL-BUSD,BNB-BUSD,ETH-BUSD,BUSD-USDT,ONE-BUSD,LINK-USDT,ZEC-USDT,SLP-USD,ANT-USDT", + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + } + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "credentials": { + "key": "", + "secret": "" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + }, + "urlEndpoints": { + "RestSpotSupplementaryURL": "https://api.binance.us", + "RestSpotURL": "https://api.binance.us", + "WebsocketSpotSupplementaryURL": "wss://stream.binance.us:9443/stream", + "WebsocketSpotURL": "wss://stream.binance.us:9443/stream" + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true, + "saveTradeData": false, + "tradeFeed": false, + "fillsFeed": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ], + "orderbook": { + "verificationBypass": false, + "websocketBufferLimit": 5, + "websocketBufferEnabled": false, + "publishPeriod": 10000000000 + } + }, { "name": "Binance", "enabled": true, diff --git a/docs/MULTICHAIN_TRANSFER_SUPPORT.md b/docs/MULTICHAIN_TRANSFER_SUPPORT.md index 95f7b390..7792434a 100644 --- a/docs/MULTICHAIN_TRANSFER_SUPPORT.md +++ b/docs/MULTICHAIN_TRANSFER_SUPPORT.md @@ -43,6 +43,7 @@ $ ./gctcli withdrawcryptofunds --exchange=ftx --currency=USDT --address=TJU9piX2 | Exchange | Deposits | Withdrawals | Notes| |----------|----------|-------------|------| | Alphapoint | No | No | | +| Binance.US | Yes | Yes | | | Binance | Yes | Yes | | | Bitfinex | Yes | Yes | Only supports USDT | | Bitflyer | No | No | | diff --git a/docs/OHLCV.md b/docs/OHLCV.md index f2a20ff2..e86f046c 100644 --- a/docs/OHLCV.md +++ b/docs/OHLCV.md @@ -65,6 +65,7 @@ A helper tool [cmd/dbseed](../cmd/dbseed/README.md) has been created for assisti ## Exchange status | Exchange | Supported | |----------------|-------------| +| Binance.US | Y | | Binance | Y | | Bitfinex | Y | | Bitflyer | | diff --git a/engine/exchange_manager.go b/engine/exchange_manager.go index 0d0e7e4f..16632ded 100644 --- a/engine/exchange_manager.go +++ b/engine/exchange_manager.go @@ -8,6 +8,7 @@ import ( exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/binance" + "github.com/thrasher-corp/gocryptotrader/exchanges/binanceus" "github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex" "github.com/thrasher-corp/gocryptotrader/exchanges/bitflyer" "github.com/thrasher-corp/gocryptotrader/exchanges/bithumb" @@ -146,6 +147,8 @@ func (m *ExchangeManager) NewExchangeByName(name string) (exchange.IBotExchange, var exch exchange.IBotExchange switch nameLower { + case "binanceus": + exch = new(binanceus.Binanceus) case "binance": exch = new(binance.Binance) case "bitfinex": @@ -206,6 +209,5 @@ func (m *ExchangeManager) NewExchangeByName(name string) (exchange.IBotExchange, } return nil, fmt.Errorf("%s, %w", nameLower, ErrExchangeNotFound) } - return exch, nil } diff --git a/engine/exchange_manager_test.go b/engine/exchange_manager_test.go index 69ce9bb0..b5a9a0d9 100644 --- a/engine/exchange_manager_test.go +++ b/engine/exchange_manager_test.go @@ -82,7 +82,7 @@ func TestExchangeManagerRemoveExchange(t *testing.T) { func TestNewExchangeByName(t *testing.T) { m := SetupExchangeManager() - exchanges := []string{"binance", "bitfinex", "bitflyer", "bithumb", "bitmex", "bitstamp", "bittrex", "btc markets", "btse", "bybit", "coinut", "exmo", "coinbasepro", "ftx", "gateio", "gemini", "hitbtc", "huobi", "itbit", "kraken", "lbank", "localbitcoins", "okcoin international", "okex", "poloniex", "yobit", "zb", "fake"} + exchanges := []string{"binanceus", "binance", "bitfinex", "bitflyer", "bithumb", "bitmex", "bitstamp", "bittrex", "btc markets", "btse", "bybit", "coinut", "exmo", "coinbasepro", "ftx", "gateio", "gemini", "hitbtc", "huobi", "itbit", "kraken", "lbank", "localbitcoins", "okcoin international", "okex", "poloniex", "yobit", "zb", "fake"} for i := range exchanges { exch, err := m.NewExchangeByName(exchanges[i]) if err != nil && exchanges[i] != "fake" { diff --git a/exchanges/binanceus/README.md b/exchanges/binanceus/README.md new file mode 100644 index 00000000..4a6e4bfa --- /dev/null +++ b/exchanges/binanceus/README.md @@ -0,0 +1,140 @@ +# GoCryptoTrader package Binanceus + + + + +[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml) +[![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/binanceus) +[![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 binanceus 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) + +## Binanceus 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() == "Binanceus" { + 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 +} +``` + +### How to do Websocket public/private calls + +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### 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: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/exchanges/binanceus/binanceus.go b/exchanges/binanceus/binanceus.go new file mode 100644 index 00000000..47997c90 --- /dev/null +++ b/exchanges/binanceus/binanceus.go @@ -0,0 +1,2002 @@ +package binanceus + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "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/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/log" + "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" +) + +// Binanceus is the overarching type across this package +type Binanceus struct { + validLimits []int64 + exchange.Base + obm *orderbookManager +} + +const ( + // General Data Endpoints + serverTime = "/api/v3/time" + systemStatus = "/sapi/v1/system/status" + + // Public endpoints + exchangeInfo = "/api/v3/exchangeInfo" + recentTrades = "/api/v3/trades" + aggregatedTrades = "/api/v3/aggTrades" + orderBookDepth = "/api/v3/depth" + candleStick = "/api/v3/klines" + tickerPrice = "/api/v3/ticker/price" + averagePrice = "/api/v3/avgPrice" + bestPrice = "/api/v3/ticker/bookTicker" + priceChange = "/api/v3/ticker/24hr" + historicalTrades = "/api/v3/historicalTrades" + + // Withdraw API endpoints + tradingStatus = "/sapi/v3/apiTradingStatus" + tradeFee = "/wapi/v3/tradeFee.html" + + // Subaccounts + subaccountsInformation = "/sapi/v3/sub-account/list" + subaccountTransferHistory = "/sapi/v3/sub-account/transfer/history" + subaccountTransfer = "/sapi/v3/sub-account/transfer" + subaccountAssets = "/sapi/v3/sub-account/assets" + + // Account Endpoint + accountInfo = "/api/v3/account" + accountStatus = "/sapi/v3/accountStatus" + accountEnableCryptoWithdrawalEndpoint = "/sapi/v1/account/quickEnableWithdrawal" + accountDisableCryptoWithdrawalEndpoint = "/sapi/v1/account/quickDisableWithdrawal" + masterAccounts = "/sapi/v1/sub-account/spotSummary" + subAccountStatusList = "/sapi/v1/sub-account/status" + usersSpotAssetsSnapshot = "/sapi/v1/accountSnapshot" + + // Trade Order Endpoints + orderRateLimit = "/api/v3/rateLimit/order" + testCreateNeworder = "/api/v3/order/test" // Method: POST + orderRequest = "/api/v3/order" // Used in Create {Method: POST}, Cancel {DELETE}, and get{GET} OrderRequest + openOrders = "/api/v3/openOrders" + myTrades = "/api/v3/myTrades" + + // One-Cancels-the-Other Orders (OCO Orders) + ocoOrder = "/api/v3/order/oco" + ocoOrderList = "/api/v3/orderList" + ocoAllOrderList = "/api/v3/allOrderList" + ocoOpenOrders = "/api/v3/openOrderList" + + // OTC Endpoints + // Over-The-Counter Endpoints + otcSelectors = "/sapi/v1/otc/coinPairs" + otcQuotes = "/sapi/v1/otc/quotes" + otcTradeOrder = "/sapi/v1/otc/orders" + otcTradeOrders = "/sapi/v1/otc/orders/" + ocbsTradeOrders = "/sapi/v1/ocbs/orders" + + // Wallet endpoints + assetDistributionHistory = "/sapi/v1/asset/assetDistributionHistory" + assetFeeAndWalletStatus = "/sapi/v1/capital/config/getall" + applyWithdrawal = "/sapi/v1/capital/withdraw/apply" + withdrawalHistory = "/sapi/v1/capital/withdraw/history" + withdrawFiat = "/sapi/v1/fiatpayment/apply/withdraw" + fiatWithdrawalHistory = "/sapi/v1/fiatpayment/query/withdraw/history" + fiatDepositHistory = "/sapi/v1/fiatpayment/query/deposit/history" + depositAddress = "/sapi/v1/capital/deposit/address" + depositHistory = "/sapi/v1/capital/deposit/hisrec" + subAccountDepositAddress = "/sapi/v1/capital/sub-account/deposit/address" + subAccountDepositHistory = "/sapi/v1/capital/sub-account/deposit/history" + + // Referral Reward Endpoints + referralRewardHistory = "/sapi/v1/marketing/referral/reward/history" + + // Web socket related route + userAccountStream = "/api/v3/userDataStream" + + // Other Consts + defaultRecvWindow = 5 * time.Second + binanceUSAPITimeLayout = "2006-01-02 15:04:05" + + // recvWindowSize5000 + recvWindowSize5000 = 5000 +) + +var ( + recvWindowSize5000String = strconv.Itoa(recvWindowSize5000) +) + +// This is a list of error Messages to be returned by binanceus endpoint methods. +var ( + errNotValidEmailAddress = errors.New("invalid email address") + errUnacceptableSenderEmail = errors.New("senders address email is missing") + errUnacceptableReceiverEmail = errors.New("receiver address email is missing") + errInvalidAssetValue = errors.New("invalid asset ") + errInvalidAssetAmount = errors.New("invalid asset amount") + errIncompleteArguments = errors.New("missing required argument") + errStartTimeOrFromIDNotSet = errors.New("please set StartTime or FromId, but not both") + errIncorrectLimitValues = errors.New("incorrect limit values - valid values are 5, 10, 20, 50, 100, 500, 1000") + errUnableToTypeAssertResponseData = errors.New("unable to type assert responseData") + errUnableToTypeAssertInvalidData = errors.New("unable to type assert individualData") + errUnexpectedKlineDataLength = errors.New("unexpected kline data length") + errUnableToTypeAssertTradeCount = errors.New("unable to type assert trade count") + errMissingRequiredArgumentCoin = errors.New("missing required argument,coin") + errMissingRequiredArgumentNetwork = errors.New("missing required argument,network") + errAmountValueMustBeGreaterThan0 = errors.New("amount must be greater than 0") + errMissingPaymentAccountInfo = errors.New("error: missing payment account") + errUnixMilliSecTypeAssertion = errors.New("error while asserting unix time integer") + errMissingRequiredParameterAddress = errors.New("missing required parameter \"address\"") + errMissingCurrencySymbol = errors.New("missing currency symbol") + errEitherOrderIDOrClientOrderIDIsRequired = errors.New("either order id or client order id is required") + errMissingRequestAmount = errors.New("missing required value \"requestAmount\"") + errMissingRequestCoin = errors.New("missing required value \"requestCoin\" name") + errMissingToCoinName = errors.New("missing required value \"toCoin\" name") + errMissingFromCoinName = errors.New("missing required value \"fromCoin\" name") + errMissingQuoteID = errors.New("missing quote id") + errMissingSubAccountEmail = errors.New("missing sub-account email address") + errMissingCurrencyCoin = errors.New("missing currency coin") + errInvalidUserBusinessType = errors.New("only 0: referrer and 1: referee are allowed") + errMissingPageNumber = errors.New("missing page number") + errInvalidRowNumber = errors.New("invalid row number") +) + +// SetValues sets the default valid values +func (bi *Binanceus) SetValues() { + bi.validLimits = []int64{5, 10, 20, 50, 100, 500, 1000, 5000} +} + +// General Data Endpoints + +// GetServerTime this endpoint returns the exchange server time. +func (bi *Binanceus) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) { + var response ServerTime + return response.Timestamp, + bi.SendHTTPRequest(ctx, + exchange.RestSpotSupplementary, + serverTime, spotDefaultRate, + &response) +} + +// GetSystemStatus endpoint to fetch whether the system status is normal or under maintenance. +func (bi *Binanceus) GetSystemStatus(ctx context.Context) (int, error) { + resp := struct { + Status int `json:"status"` + }{} + return resp.Status, bi.SendAuthHTTPRequest( + ctx, exchange.RestSpotSupplementary, + http.MethodGet, systemStatus, + nil, spotDefaultRate, &resp) +} + +// GetExchangeInfo to get the current exchange trading rules and trading pair information. +func (bi *Binanceus) GetExchangeInfo(ctx context.Context) (ExchangeInfo, error) { + var respo ExchangeInfo + return respo, bi.SendHTTPRequest(ctx, + exchange.RestSpotSupplementary, + exchangeInfo, spotExchangeInfo, &respo) +} + +// GetMostRecentTrades to get older trades. maximum limit in the RecentTradeRequestParams is 1,000 trades. +func (bi *Binanceus) GetMostRecentTrades(ctx context.Context, rtr RecentTradeRequestParams) ([]RecentTrade, error) { + params := url.Values{} + symbol, err := bi.FormatSymbol(rtr.Symbol, asset.Spot) + if err != nil { + return nil, err + } + params.Set("symbol", symbol) + params.Set("limit", strconv.FormatInt(rtr.Limit, 10)) + path := common.EncodeURLValues(recentTrades, params) + var resp []RecentTrade + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, spotDefaultRate, &resp) +} + +// GetHistoricalTrades returns historical trade activity +// symbol: string of currency pair +// limit: Optional. Default 500; max 1000. +func (bi *Binanceus) GetHistoricalTrades(ctx context.Context, hist HistoricalTradeParams) ([]HistoricalTrade, error) { + var resp []HistoricalTrade + params := url.Values{} + params.Set("symbol", hist.Symbol) + params.Set("limit", strconv.FormatInt(hist.Limit, 10)) + if hist.FromID > 0 { + params.Set("fromId", strconv.FormatUint(hist.FromID, 10)) + } + path := common.EncodeURLValues(historicalTrades, params) + return resp, bi.SendAPIKeyHTTPRequest(ctx, exchange.RestSpotSupplementary, path, spotHistoricalTradesRate, &resp) +} + +// GetAggregateTrades to get compressed, aggregate trades. Trades that fill at the time, from the same order, with the same price will have the quantity aggregated. +func (bi *Binanceus) GetAggregateTrades(ctx context.Context, agg *AggregatedTradeRequestParams) ([]AggregatedTrade, error) { + params := url.Values{} + symbol, err := bi.FormatSymbol(agg.Symbol, asset.Spot) + if err != nil { + return nil, err + } + params.Set("symbol", symbol) + needBatch := false + if agg.Limit > 0 { + if agg.Limit > 1000 { + needBatch = true + } else { + params.Set("limit", strconv.Itoa(agg.Limit)) + } + } + if agg.FromID != 0 { + params.Set("fromId", strconv.FormatInt(agg.FromID, 10)) + } + startTime := time.UnixMilli(int64(agg.StartTime)) + endTime := time.UnixMilli(int64(agg.EndTime)) + + if (endTime.UnixNano() - startTime.UnixNano()) >= int64(time.Hour) { + endTime = startTime.Add(time.Minute * 59) + } + + if !startTime.IsZero() && startTime.Unix() != 0 { + params.Set("startTime", strconv.Itoa(int(agg.StartTime))) + } + if !endTime.IsZero() && endTime.Unix() != 0 { + params.Set("endTime", strconv.Itoa(int(agg.EndTime))) + } + needBatch = needBatch || (!startTime.IsZero() && !endTime.IsZero() && endTime.Sub(startTime) > time.Hour) + if needBatch { + // fromId xor start time must be set + canBatch := agg.FromID == 0 != startTime.IsZero() + if canBatch { + return bi.batchAggregateTrades(ctx, agg, params) + } + // Can't handle this request locally or remotely + // We would receive {"code":-1128,"msg":"Combination of optional parameters invalid."} + return nil, errStartTimeOrFromIDNotSet + } + var resp []AggregatedTrade + path := common.EncodeURLValues(aggregatedTrades, params) + return resp, bi.SendHTTPRequest(ctx, + exchange.RestSpotSupplementary, path, spotDefaultRate, &resp) +} + +// batchAggregateTrades fetches trades in multiple requests <-- copied and amended from the binance +// first phase, hourly requests until the first trade (or end time) is reached +// second phase, limit requests from previous trade until end time (or limit) is reached +func (bi *Binanceus) batchAggregateTrades(ctx context.Context, arg *AggregatedTradeRequestParams, params url.Values) ([]AggregatedTrade, error) { + var resp []AggregatedTrade + // prepare first request with only first hour and max limit + if arg.Limit == 0 || arg.Limit > 1000 { + // Extend from the default of 500 + params.Set("limit", "1000") + } + startTime := time.UnixMilli(int64(arg.StartTime)) + endTime := time.UnixMilli(int64(arg.EndTime)) + var fromID int64 + if arg.FromID > 0 { + fromID = arg.FromID + } else { + // Only 10 seconds is used to prevent limit of 1000 being reached in the first request, + // cutting off trades for high activity pairs + increment := time.Second * 10 + for len(resp) == 0 { + startTime = startTime.Add(increment) + if !endTime.IsZero() && !startTime.Before(endTime) { + // All requests returned empty + return nil, nil + } + params.Set("startTime", strconv.Itoa(int(startTime.UnixMilli()))) + params.Set("endTime", strconv.Itoa(int(startTime.Add(increment).UnixMilli()))) + path := common.EncodeURLValues(aggregatedTrades, params) + err := bi.SendHTTPRequest(ctx, + exchange.RestSpotSupplementary, path, spotDefaultRate, &resp) + if err != nil { + log.Warn(log.ExchangeSys, err.Error()) + return resp, err + } + } + fromID = resp[len(resp)-1].ATradeID + } + + // other requests follow from the last aggregate trade id and have no time window + params.Del("startTime") + params.Del("endTime") + // while we haven't reached the limit + for ; arg.Limit == 0 || len(resp) < arg.Limit; fromID = resp[len(resp)-1].ATradeID { + // Keep requesting new data after last retrieved trade + params.Set("fromId", strconv.FormatInt(fromID, 10)) + path := common.EncodeURLValues(aggregatedTrades, params) + var additionalTrades []AggregatedTrade + err := bi.SendHTTPRequest(ctx, + exchange.RestSpotSupplementary, + path, + spotDefaultRate, + &additionalTrades) + if err != nil { + return resp, err + } + lastIndex := len(additionalTrades) + if !endTime.IsZero() && endTime.Unix() != 0 { + // get index for truncating to end time + lastIndex = sort.Search(len(additionalTrades), func(i int) bool { + return endTime.Before(additionalTrades[i].TimeStamp) + }) + } + // don't include the first as the request was inclusive from last ATradeID + resp = append(resp, additionalTrades[1:lastIndex]...) + // If only the starting trade is returned or if we received trades after end time + if len(additionalTrades) == 1 || lastIndex < len(additionalTrades) { + break + } + } + // Truncate if necessary + if arg.Limit > 0 && len(resp) > arg.Limit { + resp = resp[:arg.Limit] + } + return resp, nil +} + +// GetOrderBookDepth to get the order book depth. Please note the limits in the table below. +func (bi *Binanceus) GetOrderBookDepth(ctx context.Context, arg *OrderBookDataRequestParams) (*OrderBook, error) { + if err := bi.CheckLimit(arg.Limit); err != nil { + return nil, err + } + params := url.Values{} + symbol, err := bi.FormatSymbol(arg.Symbol, asset.Spot) + if err != nil { + return nil, err + } + params.Set("symbol", symbol) + params.Set("limit", fmt.Sprintf("%d", arg.Limit)) + var resp OrderBookData + if err := bi.SendHTTPRequest(ctx, + exchange.RestSpotSupplementary, + common.EncodeURLValues(orderBookDepth, params), + orderbookLimit(arg.Limit), &resp); err != nil { + return nil, err + } + orderbook := OrderBook{ + Bids: make([]OrderbookItem, len(resp.Bids)), + Asks: make([]OrderbookItem, len(resp.Asks)), + LastUpdateID: resp.LastUpdateID, + } + for x := range resp.Bids { + price, err := strconv.ParseFloat(resp.Bids[x][0], 64) + if err != nil { + return nil, err + } + amount, err := strconv.ParseFloat(resp.Bids[x][1], 64) + if err != nil { + return nil, err + } + orderbook.Bids[x] = OrderbookItem{ + Price: price, + Quantity: amount, + } + } + for x := range resp.Asks { + price, err := strconv.ParseFloat(resp.Asks[x][0], 64) + if err != nil { + return nil, err + } + amount, err := strconv.ParseFloat(resp.Asks[x][1], 64) + if err != nil { + return nil, err + } + orderbook.Asks[x] = OrderbookItem{ + Price: price, + Quantity: amount, + } + } + return &orderbook, nil +} + +// CheckLimit checks value against a variable list +func (bi *Binanceus) CheckLimit(limit int64) error { + for x := range bi.validLimits { + if bi.validLimits[x] == limit { + return nil + } + } + return errIncorrectLimitValues +} + +// GetIntervalEnum allowed interval params by Binanceus +func (bi *Binanceus) GetIntervalEnum(interval kline.Interval) string { + switch interval { + case kline.OneMin: + return "1m" + case kline.ThreeMin: + return "3m" + case kline.FiveMin: + return "5m" + case kline.FifteenMin: + return "15m" + case kline.ThirtyMin: + return "30m" + case kline.OneHour: + return "1h" + case kline.TwoHour: + return "2h" + case kline.FourHour: + return "4h" + case kline.SixHour: + return "6h" + case kline.EightHour: + return "8h" + case kline.TwelveHour: + return "12h" + case kline.OneDay: + return "1d" + case kline.ThreeDay: + return "3d" + case kline.OneWeek: + return "1w" + case kline.OneMonth: + return "1M" + default: + return "notfound" + } +} + +// GetSpotKline to get Kline/candlestick bars for a token symbol. Klines are uniquely identified by their open time. +func (bi *Binanceus) GetSpotKline(ctx context.Context, arg *KlinesRequestParams) ([]CandleStick, error) { + symbol, err := bi.FormatSymbol(arg.Symbol, asset.Spot) + if err != nil { + return nil, err + } + params := url.Values{} + params.Set("symbol", symbol) + params.Set("interval", arg.Interval) + if arg.Limit != 0 { + params.Set("limit", strconv.FormatInt(arg.Limit, 10)) + } + if !arg.StartTime.IsZero() && arg.StartTime.Unix() != 0 { + params.Set("startTime", strconv.FormatInt((arg.StartTime).UnixMilli(), 10)) + } + if !arg.EndTime.IsZero() && arg.EndTime.Unix() != 0 { + params.Set("endTime", strconv.FormatInt((arg.EndTime).UnixMilli(), 10)) + } + path := common.EncodeURLValues(candleStick, params) + var resp interface{} + err = bi.SendHTTPRequest(ctx, + exchange.RestSpotSupplementary, + path, + spotDefaultRate, + &resp) + if err != nil { + return nil, err + } + responseData, ok := resp.([]interface{}) + if !ok { + return nil, errUnableToTypeAssertResponseData + } + + klineData := make([]CandleStick, len(responseData)) + for x := range responseData { + individualData, ok := responseData[x].([]interface{}) + if !ok { + return nil, errUnableToTypeAssertInvalidData + } + if len(individualData) != 12 { + return nil, errUnexpectedKlineDataLength + } + var candle CandleStick + val, ok := individualData[0].(float64) + if !ok { + return nil, errUnixMilliSecTypeAssertion + } + candle.OpenTime = time.UnixMilli(int64(val)) + if candle.Open, err = convert.FloatFromString(individualData[1]); err != nil { + return nil, err + } + if candle.High, err = convert.FloatFromString(individualData[2]); err != nil { + return nil, err + } + if candle.Low, err = convert.FloatFromString(individualData[3]); err != nil { + return nil, err + } + if candle.Close, err = convert.FloatFromString(individualData[4]); err != nil { + return nil, err + } + if candle.Volume, err = convert.FloatFromString(individualData[5]); err != nil { + return nil, err + } + val, ok = individualData[6].(float64) + if !ok { + return nil, errUnixMilliSecTypeAssertion + } + candle.CloseTime = time.UnixMilli(int64(val)) + if candle.QuoteAssetVolume, err = convert.FloatFromString(individualData[7]); err != nil { + return nil, err + } + if candle.TradeCount, ok = individualData[8].(float64); !ok { + return nil, errUnableToTypeAssertTradeCount + } + if candle.TakerBuyAssetVolume, err = convert.FloatFromString(individualData[9]); err != nil { + return nil, err + } + if candle.TakerBuyQuoteAssetVolume, err = convert.FloatFromString(individualData[10]); err != nil { + return nil, err + } + klineData[x] = candle + } + return klineData, nil +} + +// GetSinglePriceData to get the latest price for a token symbol or symbols. +func (bi *Binanceus) GetSinglePriceData(ctx context.Context, symbol currency.Pair) (SymbolPrice, error) { + var res SymbolPrice + params := url.Values{} + symbolValue, err := bi.FormatSymbol(symbol, asset.Spot) + if err != nil { + return res, err + } + params.Set("symbol", symbolValue) + path := common.EncodeURLValues(tickerPrice, params) + return res, bi.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, spotDefaultRate, &res) +} + +// GetPriceDatas to get the latest price for symbols. +func (bi *Binanceus) GetPriceDatas(ctx context.Context) (SymbolPrices, error) { + var res SymbolPrices + return res, bi.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, tickerPrice, spotSymbolPriceAllRate, &res) +} + +// GetAveragePrice returns current average price for a symbol. +// +// symbol: string of currency pair +func (bi *Binanceus) GetAveragePrice(ctx context.Context, symbol currency.Pair) (AveragePrice, error) { + resp := AveragePrice{} + params := url.Values{} + symbolValue, err := bi.FormatSymbol(symbol, asset.Spot) + if err != nil { + return resp, err + } + params.Set("symbol", symbolValue) + + path := common.EncodeURLValues(averagePrice, params) + + return resp, bi.SendHTTPRequest(ctx, + exchange.RestSpotSupplementary, path, spotDefaultRate, &resp) +} + +// GetBestPrice returns the latest best price for symbol +// symbol: string of currency pair +func (bi *Binanceus) GetBestPrice(ctx context.Context, symbol currency.Pair) (BestPrice, error) { + resp := BestPrice{} + params := url.Values{} + rateLimit := spotOrderbookTickerAllRate + if !symbol.IsEmpty() { + rateLimit = spotDefaultRate + symbolValue, err := bi.FormatSymbol(symbol, asset.Spot) + if err != nil { + return resp, err + } + params.Set("symbol", symbolValue) + } + path := common.EncodeURLValues(bestPrice, params) + + return resp, + bi.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, rateLimit, &resp) +} + +// GetPriceChangeStats returns price change statistics for the last 24 hours +// symbol: string of currency pair +func (bi *Binanceus) GetPriceChangeStats(ctx context.Context, symbol currency.Pair) (PriceChangeStats, error) { + resp := PriceChangeStats{} + params := url.Values{} + rateLimit := spotPriceChangeAllRate + if !symbol.IsEmpty() { + rateLimit = spotDefaultRate + symbolValue, err := bi.FormatSymbol(symbol, asset.Spot) + if err != nil { + return resp, err + } + params.Set("symbol", symbolValue) + } + path := common.EncodeURLValues(priceChange, params) + + return resp, bi.SendHTTPRequest(ctx, + exchange.RestSpotSupplementary, path, rateLimit, &resp) +} + +// GetTickers returns the ticker data for the last 24 hrs +func (bi *Binanceus) GetTickers(ctx context.Context) ([]PriceChangeStats, error) { + var resp []PriceChangeStats + return resp, bi.SendHTTPRequest(ctx, + exchange.RestSpotSupplementary, priceChange, spotPriceChangeAllRate, &resp) +} + +// GetAccount returns binance user accounts +func (bi *Binanceus) GetAccount(ctx context.Context) (*Account, error) { + type response struct { + Response + Account + } + var resp response + params := url.Values{} + if err := bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, accountInfo, + params, spotAccountInformationRate, + &resp); err != nil { + return &resp.Account, err + } + + if resp.Code != 0 { + return &resp.Account, errors.New(resp.Msg) + } + + return &resp.Account, nil +} + +// GetUserAccountStatus to fetch account status detail. +func (bi *Binanceus) GetUserAccountStatus(ctx context.Context, recvWindow uint) (*AccountStatusResponse, error) { + var resp AccountStatusResponse + params := url.Values{} + timestamp := time.Now().UnixMilli() + params.Set("timestamp", strconv.Itoa(int(timestamp))) + if recvWindow > 0 && recvWindow < 60000 { + if recvWindow < 2000 { + recvWindow += 1500 + } + params.Set("recvWindow", strconv.Itoa(int(recvWindow))) + } + + return &resp, + bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, + accountStatus, + params, + spotDefaultRate, + &resp) +} + +// GetUserAPITradingStatus to fetch account API trading status details. +func (bi *Binanceus) GetUserAPITradingStatus(ctx context.Context, recvWindow uint) (*TradeStatus, error) { + type response struct { + Success bool `json:"success"` + TC TradeStatus `json:"status"` + } + var resp response + params := url.Values{} + timestamp := time.Now().UnixMilli() + params.Set("timestamp", strconv.Itoa(int(timestamp))) + if recvWindow > 0 && recvWindow < 2000 { + recvWindow += 1500 + } + params.Set("recvWindow", strconv.Itoa(int(recvWindow))) + return &resp.TC, + bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, + tradingStatus, + params, + spotDefaultRate, + &resp) +} + +// GetFee to fetch trading fees. +func (bi *Binanceus) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { + var fee float64 + switch feeBuilder.FeeType { + case exchange.CryptocurrencyTradeFee: + multiplier, er := bi.getMultiplier(ctx, feeBuilder.IsMaker, feeBuilder) + if er != nil { + return 0, er + } + fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount, multiplier) + case exchange.CryptocurrencyWithdrawalFee: + wallet, er := bi.GetAssetFeesAndWalletStatus(ctx) + if er != nil { + return fee, er + } + for x := range wallet { + for y := range wallet[x].NetworkList { + if wallet[x].NetworkList[y].IsDefault { + return wallet[x].NetworkList[y].WithdrawFee, nil + } + } + } + case exchange.OfflineTradeFee: + fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount) + } + if fee < 0 { + fee = 0 + } + return fee, nil +} + +// getMultiplier retrieves account based taker/maker fees +func (bi *Binanceus) getMultiplier(ctx context.Context, isMaker bool, feeBuilder *exchange.FeeBuilder) (float64, error) { + symbol, er := bi.FormatSymbol(feeBuilder.Pair, asset.Spot) + if er != nil { + return 0, er + } + trades, er := bi.GetTradeFee(ctx, 0, symbol) + if er != nil { + return 0, er + } + for x := range trades.TradeFee { + if trades.TradeFee[x].Symbol == symbol { + if isMaker { + return trades.TradeFee[x].Maker, nil + } + return trades.TradeFee[x].Taker, nil + } + } + return 0, nil +} + +// getOfflineTradeFee calculates the worst case-scenario trading fee +func getOfflineTradeFee(price, amount float64) float64 { + return 0.001 * price * amount +} + +// calculateTradingFee returns the fee for trading any currency on Binanceus +func calculateTradingFee(purchasePrice, amount, multiplier float64) float64 { + return (multiplier / 100) * purchasePrice * amount +} + +// GetTradeFee to fetch trading fees. +func (bi *Binanceus) GetTradeFee(ctx context.Context, recvWindow uint, symbol string) (TradeFeeList, error) { + timestamp := time.Now().UnixMilli() + params := url.Values{} + var resp TradeFeeList + params.Set("timestamp", strconv.FormatInt(timestamp, 10)) + if recvWindow > 0 { + if recvWindow < 2000 { + recvWindow += 3000 + } else if recvWindow > 60000 { + recvWindow = recvWindowSize5000 + } + params.Set("recvWindow", strconv.Itoa(int(recvWindow))) + } + if symbol != "" { + params.Set("symbol", symbol) + } + return resp, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, + tradeFee, + params, + spotDefaultRate, + &resp) +} + +// GetAssetDistributionHistory this endpoint to query +// asset distribution records, including for staking, referrals and airdrops etc. +// INPUTS: +// asset: string , startTime & endTime unix time in Milli seconds, recvWindow(duration in milli seconds > 2000 to < 6000) +func (bi *Binanceus) GetAssetDistributionHistory(ctx context.Context, asset string, startTime, endTime uint64, recvWindow uint) (*AssetDistributionHistories, error) { + params := url.Values{} + timestamp := time.Now().UnixMilli() + var resp AssetDistributionHistories + params.Set("timestamp", strconv.Itoa(int(timestamp))) + if startTime > 0 && time.UnixMilli(int64(startTime)).Before(time.Now()) { + params.Set("startTime", strconv.Itoa(int(startTime))) + } + if startTime > 0 { + params.Set("endTime", strconv.Itoa(int(endTime))) + } + if recvWindow > 0 && recvWindow < 60000 { + if recvWindow < 2000 { + recvWindow += 2000 + } else if recvWindow > 6000 { + recvWindow = recvWindowSize5000 + } + params.Set("recvWindow", strconv.Itoa(int(recvWindow))) + } + + if asset != "" { + params.Set("asset", asset) + } + return &resp, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, assetDistributionHistory, + params, + spotDefaultRate, &resp) +} + +// QuickEnableCryptoWithdrawal use this endpoint to enable crypto withdrawals. +func (bi *Binanceus) QuickEnableCryptoWithdrawal(ctx context.Context) error { + params := url.Values{} + response := struct { + Data interface{} + }{} + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + return bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, + http.MethodPost, + accountEnableCryptoWithdrawalEndpoint, params, spotDefaultRate, &(response.Data)) +} + +// QuickDisableCryptoWithdrawal use this endpoint to disable crypto withdrawals. +func (bi *Binanceus) QuickDisableCryptoWithdrawal(ctx context.Context) error { + params := url.Values{} + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + return bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, + http.MethodPost, + accountDisableCryptoWithdrawalEndpoint, params, spotDefaultRate, nil) +} + +// GetUsersSpotAssetSnapshot retrieves a snapshot of list of assets in the account. +func (bi *Binanceus) GetUsersSpotAssetSnapshot(ctx context.Context, startTime, endTime time.Time, limit, offset uint) (*SpotAssetsSnapshotResponse, error) { + params := url.Values{} + params.Set("type", "SPOT") + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + if !(startTime.IsZero() && startTime.Unix() <= 0) && startTime.Before(time.Now()) { + params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10)) + } + if !(endTime.IsZero() && endTime.Unix() <= 0) && endTime.After(time.Now()) { + if (params.Get("startTime") != "" && endTime.After(startTime)) || params.Get("startTime") == "" { + params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10)) + } + } + if limit > 0 { + params.Set("limit", strconv.Itoa(int(limit))) + } + if offset > 0 { + params.Set("offset", strconv.Itoa(int(offset))) + } + var resp SpotAssetsSnapshotResponse + return &resp, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, + http.MethodGet, usersSpotAssetsSnapshot, + params, spotDefaultRate, &resp) +} + +// GetSubaccountInformation to fetch your sub-account list. +func (bi *Binanceus) GetSubaccountInformation(ctx context.Context, page, limit uint, status, email string) ([]SubAccount, error) { + params := url.Values{} + type response struct { + Success bool `json:"success"` + Subaccounts []SubAccount `json:"subAccounts"` + } + var resp response + + if email != "" { + params.Set("email", email) + } + if status != "" && (status == "enabled" || status == "disabled") { + params.Set("status", status) + } + if page != 0 { + params.Set("page", strconv.Itoa(int(page))) + } + if limit != 0 { + params.Set("limit", strconv.Itoa(int(limit))) + } + timestamp := time.Now().UnixMilli() + params.Set("timestamp", strconv.FormatInt(timestamp, 10)) + return resp.Subaccounts, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, + subaccountsInformation, + params, + spotDefaultRate, + resp) +} + +// GetSubaccountTransferHistory to fetch sub-account asset transfer history. +func (bi *Binanceus) GetSubaccountTransferHistory(ctx context.Context, + email string, + startTime uint64, + endTime uint64, + page, limit int) ([]TransferHistory, error) { + timestamp := time.Now().UnixMilli() + params := url.Values{} + type response struct { + Success bool `json:"success"` + Transfers []TransferHistory `json:"transfers"` + } + var resp response + if !common.MatchesEmailPattern(email) { + return nil, errNotValidEmailAddress + } + params.Set("email", email) + params.Set("timestamp", strconv.Itoa(int(timestamp))) + if page != 0 { + params.Set("page", strconv.Itoa(page)) + } + if limit != 0 { + params.Set("limit", strconv.Itoa(limit)) + } + startTimeT := time.UnixMilli(int64(startTime)) + endTimeT := time.UnixMilli(int64(endTime)) + + hundredDayBefore := time.Now() + hundredDayBefore.Sub(time.UnixMilli(int64((time.Hour * 24 * 10) / time.Millisecond))) + if !(startTimeT.Before(hundredDayBefore)) || !startTimeT.After(time.Now()) { + params.Set("startTime", strconv.Itoa(int(startTime))) + } + if !(endTimeT.Before(hundredDayBefore)) || !endTimeT.After(time.Now()) { + params.Set("startTime", strconv.Itoa(int(endTime))) + } + return resp.Transfers, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, + subaccountTransferHistory, + params, + spotDefaultRate, + resp) +} + +// ExecuteSubAccountTransfer to execute sub-account asset transfers. +func (bi *Binanceus) ExecuteSubAccountTransfer(ctx context.Context, arg *SubAccountTransferRequestParams) (*SubAccountTransferResponse, error) { + params := url.Values{} + var response SubAccountTransferResponse + if !common.MatchesEmailPattern(arg.FromEmail) { + return nil, errUnacceptableSenderEmail + } + if !common.MatchesEmailPattern(arg.ToEmail) { + return nil, errUnacceptableReceiverEmail + } + if len(arg.Asset) <= 2 { + return nil, errInvalidAssetValue + } + if arg.Amount <= 0.0 { + return nil, errInvalidAssetAmount + } + params.Set("fromEmail", arg.FromEmail) + params.Set("toEmail", arg.ToEmail) + params.Set("asset", arg.Asset) + params.Set("amount", strconv.FormatFloat(arg.Amount, 'f', 0, 64)) + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + return &response, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodPost, subaccountTransfer, params, spotDefaultRate, &response) +} + +// GetSubaccountAssets to fetch sub-account assets. +func (bi *Binanceus) GetSubaccountAssets(ctx context.Context, email string) (*SubAccountAssets, error) { + var resp SubAccountAssets + if !common.MatchesEmailPattern(email) { + return nil, errNotValidEmailAddress + } + params := url.Values{} + timestamp := time.Now().UnixMilli() + params.Set("timestamp", fmt.Sprintf("%d", timestamp)) + params.Set("email", email) + // + return &resp, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, http.MethodGet, + subaccountAssets, params, + spotDefaultRate, + &resp) +} + +// GetMasterAccountTotalUSDValue this endpoint to get the total value of assets in the master account in USD. +func (bi *Binanceus) GetMasterAccountTotalUSDValue(ctx context.Context, email string, page, size int) (*SpotUSDMasterAccounts, error) { + var response SpotUSDMasterAccounts + params := url.Values{} + if email != "" { + params.Set("email", email) + } + if page > 0 { + params.Set("page", strconv.Itoa(page)) + } + if size > 0 { + params.Set("size", strconv.Itoa(size)) + } + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + return &response, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, + http.MethodGet, masterAccounts, params, + spotDefaultRate, &response) +} + +// GetSubaccountStatusList this endpoint retrieves a status list of sub-accounts. +func (bi *Binanceus) GetSubaccountStatusList(ctx context.Context, email string) ([]SubAccountStatus, error) { + params := url.Values{} + if !common.MatchesEmailPattern(email) { + return nil, errMissingSubAccountEmail + } + params.Set("email", email) + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + var response []SubAccountStatus + return response, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, + http.MethodGet, subAccountStatusList, params, + spotDefaultRate, &response) +} + +// Trade Order Endpoints + +// GetOrderRateLimits get the current trade order count rate limits for all time intervals. +// INPUTS: recvWindow <= 60000 +func (bi *Binanceus) GetOrderRateLimits(ctx context.Context, recvWindow uint) ([]OrderRateLimit, error) { + params := url.Values{} + timestamp := time.Now().UnixMilli() + params.Set("timestamp", strconv.Itoa(int(timestamp))) + if recvWindow > 1000 && recvWindow < 60000 { + params.Set("recvWindow", strconv.Itoa(int(recvWindow))) + } else { + params.Set("recvWindow", strconv.Itoa(30000)) + } + var resp []OrderRateLimit + return resp, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, orderRateLimit, params, spotOrderRateLimitRate, &resp) +} + +// NewOrder sends a new order to Binanceus +func (bi *Binanceus) NewOrder(ctx context.Context, o *NewOrderRequest) (NewOrderResponse, error) { + var resp NewOrderResponse + if err := bi.newOrder(ctx, orderRequest, o, &resp); err != nil { + return resp, err + } + if resp.Code != 0 { + return resp, errors.New(resp.Msg) + } + return resp, nil +} + +// NewOrderTest sends a new test order to Binanceus +// to test new order creation and signature/recvWindow long. The endpoint creates and validates a new order but does not send it into the matching engine. +func (bi *Binanceus) NewOrderTest(ctx context.Context, o *NewOrderRequest) (*NewOrderResponse, error) { + var resp NewOrderResponse + return &resp, bi.newOrder(ctx, testCreateNeworder, o, &resp) +} + +// newOrder this endpoint is used by both new order and NewOrderTest passing their route and order information to send new order. +func (bi *Binanceus) newOrder(ctx context.Context, api string, o *NewOrderRequest, resp *NewOrderResponse) error { + params := url.Values{} + symbol, err := bi.FormatSymbol(o.Symbol, asset.Spot) + if err != nil { + return err + } + params.Set("symbol", symbol) + params.Set("side", o.Side) + params.Set("type", string(o.TradeType)) + if o.QuoteOrderQty > 0 { + params.Set("quoteOrderQty", strconv.FormatFloat(o.QuoteOrderQty, 'f', -1, 64)) + } else { + params.Set("quantity", strconv.FormatFloat(o.Quantity, 'f', -1, 64)) + } + if o.TradeType == BinanceRequestParamsOrderLimit { + params.Set("price", strconv.FormatFloat(o.Price, 'f', -1, 64)) + } + if o.TimeInForce != "" { + params.Set("timeInForce", string(o.TimeInForce)) + } + if o.NewClientOrderID != "" { + params.Set("newClientOrderId", o.NewClientOrderID) + } + if o.StopPrice != 0 { + params.Set("stopPrice", strconv.FormatFloat(o.StopPrice, 'f', -1, 64)) + } + if o.IcebergQty != 0 { + params.Set("icebergQty", strconv.FormatFloat(o.IcebergQty, 'f', -1, 64)) + } + if o.NewOrderRespType != "" { + params.Set("newOrderRespType", o.NewOrderRespType) + } + return bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodPost, api, params, + spotOrderRate, resp) +} + +// GetOrder to check a trade order's status. +func (bi *Binanceus) GetOrder(ctx context.Context, arg *OrderRequestParams) (*Order, error) { + var resp Order + params := url.Values{} + if arg.Symbol == "" { + return nil, errIncompleteArguments + } + params.Set("symbol", strings.ToUpper(arg.Symbol)) + if arg.OrderID > 0 { + params.Set("orderId", strconv.Itoa(int(arg.OrderID))) + } + timestamp := time.Now().UnixMilli() + params.Set("timestamp", strconv.Itoa(int(timestamp))) + if arg.OrigClientOrderID != "" { + params.Set("origClientOrderId", arg.OrigClientOrderID) + } + if arg.recvWindow > 200 && arg.recvWindow <= 6000 { + params.Set("recvWindow", strconv.Itoa(int(arg.recvWindow))) + } + return &resp, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, orderRequest, + params, spotOrderQueryRate, + &resp) +} + +// GetAllOpenOrders to get all open trade orders on a token symbol. Do not access this without a token symbol as this would return all pair data. +func (bi *Binanceus) GetAllOpenOrders(ctx context.Context, symbol string) ([]Order, error) { + var response []Order + params := url.Values{} + + timestamp := time.Now().UnixMilli() + if symbol != "" { + params.Set("symbol", symbol) + } + params.Set("timestamp", strconv.Itoa(int(timestamp))) + params.Set("recvWindow", recvWindowSize5000String) + var rateLimit request.EndpointLimit + if symbol != "" { + rateLimit = spotOpenOrdersSpecificRate + } else { + rateLimit = spotOpenOrdersAllRate + } + return response, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, http.MethodGet, + openOrders, params, + rateLimit, &response) +} + +// CancelExistingOrder to cancel an active trade order. +func (bi *Binanceus) CancelExistingOrder(ctx context.Context, arg *CancelOrderRequestParams) (*Order, error) { + params := url.Values{} + var response Order + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + symbolValue, err := bi.FormatSymbol(arg.Symbol, asset.Spot) + if err != nil || symbolValue == "" { + return nil, errMissingCurrencySymbol + } + params.Set("symbol", symbolValue) + if arg.OrderID == "" && arg.ClientSuppliedOrderID == "" { + return nil, errEitherOrderIDOrClientOrderIDIsRequired + } + if arg.ClientSuppliedOrderID != "" { + params.Set("origClientOrderId", arg.ClientSuppliedOrderID) + } else { + params.Set("orderId", arg.OrderID) + } + params.Set("recvWindow", recvWindowSize5000String) + return &response, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodDelete, orderRequest, + params, spotOrderRate, &response) +} + +// CancelOpenOrdersForSymbol request to cancel an open orders. +func (bi *Binanceus) CancelOpenOrdersForSymbol(ctx context.Context, symbol string) ([]Order, error) { + params := url.Values{} + if symbol == "" || len(symbol) < 4 { + return nil, errMissingCurrencySymbol + } + params.Set("symbol", symbol) + params.Set("timestamp", strconv.Itoa(int(time.Now().UnixMilli()))) + params.Set("recvWindow", "5000") + var response []Order + return response, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, + http.MethodDelete, openOrders, + params, spotOrderRate, response) +} + +// GetTrades to get trade data for a specific account and token symbol. +func (bi *Binanceus) GetTrades(ctx context.Context, arg *GetTradesParams) ([]Trade, error) { + var resp []Trade + params := url.Values{} + if arg.Symbol == "" || len(arg.Symbol) <= 2 { + return nil, errIncompleteArguments + } + params.Set("symbol", arg.Symbol) + params.Set("timestamp", strconv.Itoa(int(time.Now().UnixMilli()))) + if arg.RecvWindow > 3000 { + params.Set("recvWindow", strconv.Itoa(int(arg.RecvWindow))) + } + if arg.StartTime != nil { + params.Set("startTime", strconv.Itoa(int(arg.StartTime.UnixMilli()))) + } + if arg.EndTime != nil { + params.Set("endTime", strconv.Itoa(int(arg.EndTime.UnixMilli()))) + } + if arg.FromID > 0 { + params.Set("fromId", strconv.Itoa(int(arg.FromID))) + } + if arg.Limit > 0 && arg.Limit < 1000 { + params.Set("limit", fmt.Sprint(arg.Limit)) + } else if arg.Limit > 1000 { + params.Set("limit", strconv.Itoa(1000)) + } + return resp, bi.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, myTrades, params, spotTradesQueryRate, &resp) +} + +// OCO Orders + +// CreateNewOCOOrder o place a new OCO(one-cancels-the-other) order. +func (bi *Binanceus) CreateNewOCOOrder(ctx context.Context, arg *OCOOrderInputParams) (*OCOFullOrderResponse, error) { + params := url.Values{} + if arg == nil || arg.Symbol == "" || len(arg.Symbol) <= 2 || arg.Quantity == 0 || arg.Side == "" || arg.Price == 0 || arg.StopPrice == 0 { + return nil, errIncompleteArguments + } + params.Set("symbol", arg.Symbol) + params.Set("quantity", strconv.FormatFloat(arg.Quantity, 'f', 5, 64)) + params.Set("side", arg.Side) + params.Set("price", strconv.FormatFloat(arg.Price, 'f', 5, 64)) + params.Set("stopPrice", strconv.FormatFloat(arg.StopPrice, 'f', 5, 64)) + if arg.ListClientOrderID != "" { + params.Set("listClientOrderId", arg.ListClientOrderID) + } + if arg.LimitClientOrderID != "" { + params.Set("limitClientOrderId", arg.LimitClientOrderID) + } + if arg.LimitIcebergQty > 0 { + params.Set("limitIcebergQty", strconv.FormatFloat(arg.LimitIcebergQty, 'f', 5, 64)) + } + if arg.StopClientOrderID != "" { + params.Set("stopClientOrderId", arg.StopClientOrderID) + } + if arg.StopLimitPrice > 0.0 { + params.Set("stopLimitPrice", strconv.FormatFloat(arg.StopLimitPrice, 'f', 5, 64)) + } + if arg.StopIcebergQty > 0.0 { + params.Set("stopIcebergQty", strconv.FormatFloat(arg.StopIcebergQty, 'f', 5, 64)) + } + if arg.StopLimitTimeInForce != "" { + params.Set("stopLimitTimeInForce", arg.StopLimitTimeInForce) + } + if arg.NewOrderRespType != "" { + params.Set("newOrderRespType", arg.NewOrderRespType) + } + if arg.RecvWindow > 200 { + params.Set("recvWindow", strconv.Itoa(int(arg.RecvWindow))) + } else { + params.Set("recvWindow", "6000") + } + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + var response OCOFullOrderResponse + return &response, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodPost, ocoOrder, params, + spotOrderRate, &response) +} + +// GetOCOOrder to retrieve a specific OCO order based on provided optional parameters. +func (bi *Binanceus) GetOCOOrder(ctx context.Context, arg *GetOCOOrderRequestParams) (*OCOOrderResponse, error) { + params := url.Values{} + params.Set("timestamp", fmt.Sprint(time.Now().UnixMilli())) + switch { + case arg.OrderListID != "": + params.Set("orderListId", arg.OrderListID) + case arg.OrigClientOrderID != "": + params.Set("origClientOrderId", arg.OrigClientOrderID) + default: + return nil, errIncompleteArguments + } + params.Set("recvWindow", "60000") + var response OCOOrderResponse + return &response, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, ocoOrderList, params, spotSingleOCOOrderRate, &response) +} + +// GetAllOCOOrder to retrieve all OCO orders based on provided optional parameters. Please note the maximum limit is 1,000 orders. +func (bi *Binanceus) GetAllOCOOrder(ctx context.Context, arg *OCOOrdersRequestParams) ([]OCOOrderResponse, error) { + params := url.Values{} + params.Set("timestamp", fmt.Sprint(time.Now().UnixMilli())) + var response []OCOOrderResponse + if arg.FromID > 0 { + params.Set("fromId", fmt.Sprint(arg.FromID)) + } else { + if arg.StartTime.Unix() > 0 && arg.StartTime.Before(arg.EndTime) { + params.Set("startTime", fmt.Sprint(arg.StartTime.UnixMilli())) + params.Set("endTime", fmt.Sprint(arg.EndTime.UnixMilli())) + } else if arg.StartTime.Unix() > 0 { + params.Set("startTime", fmt.Sprint(arg.StartTime.UnixMilli())) + } + } + if arg.Limit > 0 { + params.Set("limit", fmt.Sprint(arg.Limit)) + } + if arg.RecvWindow > 0 { + params.Set("recvWindow", fmt.Sprint(arg.RecvWindow)) + } + return response, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, + http.MethodGet, ocoAllOrderList, + params, spotAllOCOOrdersRate, + &response) +} + +// GetOpenOCOOrders to query open OCO orders. +func (bi *Binanceus) GetOpenOCOOrders(ctx context.Context, recvWindow uint) ([]OCOOrderResponse, error) { + params := url.Values{} + params.Set("timestamp", fmt.Sprint(time.Now().UnixMilli())) + if recvWindow > 0 { + params.Set("recvWindow", fmt.Sprint(recvWindow)) + } else { + params.Set("recvWindow", "30000") + } + var response []OCOOrderResponse + return response, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, + ocoOpenOrders, params, + spotOpenOrdersSpecificRate, &response) +} + +// CancelOCOOrder to cancel an entire order list. +func (bi *Binanceus) CancelOCOOrder(ctx context.Context, arg *OCOOrdersDeleteRequestParams) (*OCOFullOrderResponse, error) { + var response OCOFullOrderResponse + params := url.Values{} + params.Set("timestamp", fmt.Sprint(time.Now().UnixMilli())) + switch { + case arg.OrderListID > 0: + params.Set("orderListId", strconv.Itoa(int(arg.OrderListID))) + case arg.ListClientOrderID != "": + params.Set("listClientOrderId", arg.ListClientOrderID) + default: + return nil, errIncompleteArguments + } + if arg.RecvWindow > 0 { + params.Set("recvWindow", fmt.Sprint(arg.RecvWindow)) + } + return &response, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, + http.MethodGet, ocoOrderList, + params, spotOrderRate, &response) +} + +// OTC end points + +// GetSupportedCoinPairs to get a list of supported coin pairs for convert. +// returns list of CoinPairInfo +func (bi *Binanceus) GetSupportedCoinPairs(ctx context.Context, symbol currency.Pair) ([]CoinPairInfo, error) { + params := url.Values{} + if !symbol.IsEmpty() { + params.Set("fromCoin", symbol.Base.String()) + params.Set("toCoin", symbol.Quote.String()) + } + var resp []CoinPairInfo + return resp, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, http.MethodGet, otcSelectors, + params, spotDefaultRate, &resp) +} + +// RequestForQuote endpoint to request a quote for a from-to coin pair. +func (bi *Binanceus) RequestForQuote(ctx context.Context, arg *RequestQuoteParams) (*Quote, error) { + params := url.Values{} + var resp Quote + if arg.FromCoin == "" { + return nil, errMissingFromCoinName + } + if arg.ToCoin == "" { + return nil, errMissingToCoinName + } + if arg.RequestCoin == "" { + return nil, errMissingRequestCoin + } + if arg.RequestAmount <= 0 { + return nil, errMissingRequestAmount + } + params.Set("fromCoin", arg.FromCoin) + params.Set("toCoin", arg.ToCoin) + params.Set("requestAmount", strconv.FormatFloat(arg.RequestAmount, 'f', 0, 64)) + params.Set("requestCoin", arg.RequestCoin) + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + return &resp, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpot, + http.MethodPost, otcQuotes, params, + spotDefaultRate, &resp) +} + +// PlaceOTCTradeOrder to place an order using an acquired quote. +// returns OTCTradeOrderResponse response containing the OrderID,OrderStatus, and CreateTime information of an order. +func (bi *Binanceus) PlaceOTCTradeOrder(ctx context.Context, quoteID string) (*OTCTradeOrderResponse, error) { + params := url.Values{} + if strings.Trim(quoteID, " ") == "" { + return nil, errMissingQuoteID + } + params.Set("quoteId", quoteID) + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + var response OTCTradeOrderResponse + return &response, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpot, http.MethodPost, + otcTradeOrder, params, + spotOrderRate, &response) +} + +// GetOTCTradeOrder returns a single OTC Trade Order instance. +func (bi *Binanceus) GetOTCTradeOrder(ctx context.Context, orderID uint64) (*OTCTradeOrder, error) { + var response OTCTradeOrder + params := url.Values{} + if orderID <= 0 { + return nil, errIncompleteArguments + } + orderIDStr := strconv.FormatUint(orderID, 10) + params.Set("orderId", orderIDStr) + params.Set("timestamp", fmt.Sprint(time.Now().UnixMilli())) + path := otcTradeOrders + orderIDStr + return &response, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, + path, params, + spotOrderRate, response) +} + +// GetAllOTCTradeOrders returns list of OTC Trade Orders +func (bi *Binanceus) GetAllOTCTradeOrders(ctx context.Context, arg *OTCTradeOrderRequestParams) ([]OTCTradeOrder, error) { + params := url.Values{} + if arg.OrderID != "" { + params.Set("orderId", arg.OrderID) + } + if arg.FromCoin != "" { + params.Set("fromCoin", arg.FromCoin) + } + if !(arg.StartTime.IsZero()) { + params.Set("startTime", strconv.FormatInt(arg.StartTime.UnixMilli(), 10)) + } + if !(arg.EndTime.IsZero()) { + params.Set("endTime", strconv.FormatInt(arg.EndTime.UnixMilli(), 10)) + } + if arg.ToCoin != "" { + params.Set("toCoin", arg.ToCoin) + } + if arg.Limit > 0 { + params.Set("limit", strconv.Itoa(int(arg.Limit))) + } + var response []OTCTradeOrder + return response, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, otcTradeOrder, + params, spotOrderRate, &response) +} + +// GetAllOCBSTradeOrders use this endpoint to query all OCBS orders by condition. +func (bi *Binanceus) GetAllOCBSTradeOrders(ctx context.Context, arg OCBSOrderRequestParams) (*OCBSTradeOrdersResponse, error) { + var resp OCBSTradeOrdersResponse + params := url.Values{} + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + if arg.OrderID != "" { + params.Set("orderId", arg.OrderID) + } + if !arg.StartTime.IsZero() { + params.Set("startTime", strconv.FormatInt(arg.StartTime.UnixMilli(), 10)) + } + if !arg.EndTime.IsZero() { + params.Set("endTime", strconv.FormatInt(arg.StartTime.UnixMilli(), 10)) + } + if arg.Limit > 0 && arg.Limit < 100 { + params.Set("limit", strconv.Itoa(int(arg.Limit))) + } + return &resp, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, ocbsTradeOrders, + params, spotOrderRate, &resp) +} + +// Wallet End points + +// GetAssetFeesAndWalletStatus to fetch the details of all crypto assets, including fees, withdrawal limits and network status. +// returns the asset wallet detail as a list. +func (bi *Binanceus) GetAssetFeesAndWalletStatus(ctx context.Context) (AssetWalletList, error) { + params := url.Values{} + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + var response AssetWalletList + return response, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, assetFeeAndWalletStatus, + params, spotDefaultRate, &response) +} + +// WithdrawCrypto method to withdraw crypto +func (bi *Binanceus) WithdrawCrypto(ctx context.Context, arg *withdraw.Request) (string, error) { + params := url.Values{} + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + if arg.Currency.String() == "" { + return "", errMissingRequiredArgumentCoin + } + params.Set("coin", arg.Currency.String()) + if arg.Crypto.Chain == "" { + return "", errMissingRequiredArgumentNetwork + } + params.Set("network", arg.Crypto.Chain) + if arg.ClientOrderID != "" { + params.Set("withdrawOrderId", arg.ClientOrderID) + } + if arg.Crypto.Address == "" { + return "", errMissingRequiredParameterAddress + } + params.Set("address", arg.Crypto.Address) + if arg.Crypto.AddressTag != "" { + params.Set("addressTag", arg.Crypto.AddressTag) + } + if arg.Amount <= 0 { + return "", errAmountValueMustBeGreaterThan0 + } + params.Set("amount", strconv.FormatFloat(arg.Amount, 'f', 0, 64)) + var response WithdrawalResponse + var er = bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodPost, applyWithdrawal, + params, spotDefaultRate, &response) + if er != nil { + return "", er + } + return response.ID, er +} + +// WithdrawalHistory gets the status of recent withdrawals +// status `param` used as string to prevent default value 0 (for int) interpreting as EmailSent status +func (bi *Binanceus) WithdrawalHistory(ctx context.Context, c currency.Code, status string, startTime, endTime time.Time, offset, limit int) ([]WithdrawStatusResponse, error) { + params := url.Values{} + if !c.IsEmpty() { + params.Set("coin", c.String()) + } + if status != "" { + i, err := strconv.Atoi(status) + if err != nil { + return nil, fmt.Errorf("wrong param (status): %s. Error: %v", status, err) + } + switch i { + case EmailSent, Cancelled, AwaitingApproval, Rejected, Processing, Failure, Completed: + default: + return nil, fmt.Errorf("wrong param (status): %s", status) + } + params.Set("status", status) + } + if !startTime.IsZero() && startTime.Unix() != 0 { + params.Set("startTime", strconv.FormatInt(startTime.UTC().Unix(), 10)) + } + if !endTime.IsZero() && endTime.Unix() != 0 { + params.Set("endTime", strconv.FormatInt(endTime.UTC().Unix(), 10)) + } + if offset != 0 { + params.Set("offset", strconv.Itoa(offset)) + } + if limit != 0 { + params.Set("limit", strconv.Itoa(limit)) + } + var withdrawStatus []WithdrawStatusResponse + if err := bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, + withdrawalHistory, + params, + spotDefaultRate, + &withdrawStatus); err != nil { + return nil, err + } + return withdrawStatus, nil +} + +// FiatWithdrawalHistory to fetch your fiat (USD) withdrawal history. +// returns FiatAssetHistory containing list of fiat asset records. +func (bi *Binanceus) FiatWithdrawalHistory(ctx context.Context, arg *FiatWithdrawalRequestParams) (FiatAssetsHistory, error) { + var response FiatAssetsHistory + params := url.Values{} + if !(arg.EndTime.IsZero()) && !(arg.EndTime.Before(time.Now())) { + params.Set("endTime", strconv.Itoa(int(arg.EndTime.UnixMilli()))) + } + if !arg.StartTime.IsZero() && !(arg.StartTime.After(time.Now())) { + params.Set("startTime", strconv.Itoa(int(arg.StartTime.UnixMilli()))) + } + if arg.FiatCurrency != "" { + params.Set("fiatCurrency", arg.FiatCurrency) + } + if arg.Offset > 0 { + params.Set("offset", strconv.FormatInt(arg.Offset, 10)) + } + if arg.PaymentChannel != "" { + params.Set("paymentChannel", arg.PaymentChannel) + } + if arg.PaymentMethod != "" { + params.Set("paymentMethod", arg.PaymentMethod) + } + params.Set("timestamp", strconv.Itoa(int(time.Now().UnixMilli()))) + return response, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, fiatWithdrawalHistory, + params, spotDefaultRate, &response) +} + +// WithdrawFiat to submit a USD withdraw request via Silvergate Exchange Network (SEN). +// returns the Order ID as string +func (bi *Binanceus) WithdrawFiat(ctx context.Context, arg *WithdrawFiatRequestParams) (string, error) { + params := url.Values{} + timestamp := strconv.Itoa(int(time.Now().UnixMilli())) + if arg == nil { + return "", errIncompleteArguments + } + params.Set("timestamp", timestamp) + if arg.PaymentChannel != "" { + params.Set("paymentChannel", arg.PaymentChannel) + } + if arg.PaymentMethod != "" { + params.Set("paymentMethod", arg.PaymentMethod) + } + if arg.PaymentAccount == "" { + return "", errMissingPaymentAccountInfo + } + if arg.FiatCurrency != "" { + params.Set("fiatCurrency", arg.FiatCurrency) + } + if arg.Amount <= 0 { + return "", errAmountValueMustBeGreaterThan0 + } + type response struct { + OrderID string `json:"orderId"` + } + var resp response + return resp.OrderID, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, + http.MethodPost, withdrawFiat, + params, spotDefaultRate, &resp, + ) +} + +/* + Deposits + Get Crypto Deposit Address +*/ + +// GetDepositAddressForCurrency retrieves the wallet address for a given currency +func (bi *Binanceus) GetDepositAddressForCurrency(ctx context.Context, currency, chain string) (*DepositAddress, error) { + params := url.Values{} + if currency == "" { + return nil, errMissingRequiredArgumentCoin + } + params.Set("coin", currency) + if chain != "" { + params.Set("network", chain) + } + params.Set("recvWindow", "10000") + var d DepositAddress + return &d, + bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, depositAddress, params, spotDefaultRate, &d) +} + +// DepositHistory returns the deposit history based on the supplied params +// status `param` used as string to prevent default value 0 (for int) interpreting as EmailSent status +func (bi *Binanceus) DepositHistory(ctx context.Context, c currency.Code, status uint8, startTime, endTime time.Time, offset, limit int) ([]DepositHistory, error) { + var response []DepositHistory + params := url.Values{} + if !c.IsEmpty() { + params.Set("coin", c.String()) + } + + if status > 0 { + switch status { + case 0 /*Pending*/, 1 /*Success*/, 6 /*Credited but cannot withdraw*/ : + params.Set("status", strconv.Itoa(int(status))) + default: + return nil, fmt.Errorf("wrong param (status) 0 Pending, 1 success, 6 credited but cannot withdraw are allowed: %d ", status) + } + } + if !startTime.IsZero() && startTime.Unix() != 0 { + params.Set("startTime", strconv.FormatInt(startTime.UTC().Unix(), 10)) + } + + if !endTime.IsZero() && endTime.Unix() != 0 { + params.Set("endTime", strconv.FormatInt(endTime.UTC().Unix(), 10)) + } + + if offset != 0 { + params.Set("offset", strconv.Itoa(offset)) + } + + if limit != 0 { + params.Set("limit", strconv.Itoa(limit)) + } + + if err := bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, + http.MethodGet, + depositHistory, + params, + spotDefaultRate, + &response); err != nil { + return nil, err + } + + return response, nil +} + +// FiatDepositHistory fetch your fiat (USD) deposit history as Fiat Assets History +func (bi *Binanceus) FiatDepositHistory(ctx context.Context, arg *FiatWithdrawalRequestParams) (FiatAssetsHistory, error) { + params := url.Values{} + if !(arg.EndTime.IsZero()) && !(arg.EndTime.Before(time.Now())) { + params.Set("endTime", fmt.Sprint(arg.EndTime.UnixMilli())) + } + if !(arg.StartTime.IsZero()) && !(arg.StartTime.After(time.Now())) { + params.Set("startTime", fmt.Sprint(arg.StartTime.UnixMilli())) + } + if arg.FiatCurrency != "" { + params.Set("fiatCurrency", arg.FiatCurrency) + } + if arg.Offset > 0 { + params.Set("offset", fmt.Sprint(arg.Offset)) + } + if arg.PaymentChannel != "" { + params.Set("paymentChannel", arg.PaymentChannel) + } + if arg.PaymentMethod != "" { + params.Set("paymentMethod", arg.PaymentMethod) + } + params.Set("timestamp", fmt.Sprint(time.Now().UnixMilli())) + var response FiatAssetsHistory + return response, bi.SendAuthHTTPRequest(ctx, + exchange.RestSpotSupplementary, http.MethodGet, + fiatDepositHistory, params, spotDefaultRate, &response) +} + +// GetSubAccountDepositAddress retrieves sub-account’s deposit address. +func (bi *Binanceus) GetSubAccountDepositAddress(ctx context.Context, arg SubAccountDepositAddressRequestParams) (*SubAccountDepositAddress, error) { + params := url.Values{} + if !common.MatchesEmailPattern(arg.Email) { + return nil, errMissingSubAccountEmail + } else if arg.Coin.String() == "" { + return nil, errMissingCurrencyCoin + } + params.Set("email", arg.Email) + params.Set("coin", arg.Coin.String()) + var response SubAccountDepositAddress + return &response, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, + subAccountDepositAddress, params, spotDefaultRate, &response) +} + +// GetSubAccountDepositHistory retrieves sub-account deposit history. +func (bi *Binanceus) GetSubAccountDepositHistory(ctx context.Context, email string, coin currency.Code, + status int, startTime, endTime time.Time, limit, offset int) ([]SubAccountDepositItem, error) { + params := url.Values{} + if !common.MatchesEmailPattern(email) { + return nil, errMissingSubAccountEmail + } + params.Set("email", email) + if coin.String() != "" { + params.Set("coin", coin.String()) + } + if status == 0 || status == 6 || status == 1 { + params.Set("status", strconv.Itoa(status)) + } + if !startTime.IsZero() && startTime.Unix() != 0 && startTime.Before(time.Now()) { + params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10)) + } + if !endTime.IsZero() && endTime.Unix() != 0 && endTime.Before(time.Now()) { + params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.Itoa(limit)) + } + if offset > 0 { + params.Set("offset", strconv.Itoa(offset)) + } + var response []SubAccountDepositItem + return response, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, + subAccountDepositHistory, params, spotDefaultRate, &response) +} + +// Referral Endpoints + +// GetReferralRewardHistory retrieves the user’s referral reward history. +func (bi *Binanceus) GetReferralRewardHistory(ctx context.Context, userBusinessType, page, rows int) (*ReferralRewardHistoryResponse, error) { + params := url.Values{} + switch { + case !(userBusinessType == 0 || userBusinessType == 1): + return nil, errInvalidUserBusinessType + case page == 0: + return nil, errMissingPageNumber + case rows < 1 || rows > 200: + return nil, errInvalidRowNumber + } + params.Set("userBizType", strconv.Itoa(userBusinessType)) + params.Set("page", strconv.Itoa(page)) + params.Set("rows", strconv.Itoa(rows)) + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + var response ReferralRewardHistoryResponse + return &response, bi.SendAuthHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, referralRewardHistory, params, spotDefaultRate, &response) +} + +// SendHTTPRequest sends an unauthenticated request +func (bi *Binanceus) SendHTTPRequest(ctx context.Context, ePath exchange.URL, path string, f request.EndpointLimit, result interface{}) error { + endpointPath, err := bi.API.Endpoints.GetURL(ePath) + if err != nil { + return err + } + item := &request.Item{ + Method: http.MethodGet, + Path: endpointPath + path, + Result: result, + Verbose: bi.Verbose, + HTTPDebugging: bi.HTTPDebugging, + HTTPRecording: bi.HTTPRecording, + } + return bi.SendPayload(ctx, f, func() (*request.Item, error) { + return item, nil + }) +} + +// SendAPIKeyHTTPRequest is a special API request where the api key is +// appended to the headers without a secret +func (bi *Binanceus) SendAPIKeyHTTPRequest(ctx context.Context, ePath exchange.URL, path string, f request.EndpointLimit, result interface{}) error { + endpointPath, err := bi.API.Endpoints.GetURL(ePath) + if err != nil { + return err + } + creds, err := bi.GetCredentials(ctx) + if err != nil { + return err + } + + headers := make(map[string]string) + headers["X-MBX-APIKEY"] = creds.Key + item := &request.Item{ + Method: http.MethodGet, + Path: endpointPath + path, + Headers: headers, + Result: result, + Verbose: bi.Verbose, + HTTPDebugging: bi.HTTPDebugging, + HTTPRecording: bi.HTTPRecording} + + return bi.SendPayload(ctx, f, func() (*request.Item, error) { + return item, nil + }) +} + +// SendAuthHTTPRequest sends an authenticated HTTP request +func (bi *Binanceus) SendAuthHTTPRequest(ctx context.Context, ePath exchange.URL, method, path string, params url.Values, f request.EndpointLimit, result interface{}) error { + creds, err := bi.GetCredentials(ctx) + if err != nil { + return err + } + endpointPath, err := bi.API.Endpoints.GetURL(ePath) + if err != nil { + return err + } + if params == nil { + params = url.Values{} + } + if params.Get("recvWindow") == "" { + params.Set("recvWindow", strconv.FormatInt(defaultRecvWindow.Milliseconds(), 10)) + } + interim := json.RawMessage{} + err = bi.SendPayload(ctx, f, func() (*request.Item, error) { + fullPath := endpointPath + path + params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) + signature := params.Encode() + var hmacSigned []byte + hmacSigned, err = crypto.GetHMAC(crypto.HashSHA256, + []byte(signature), + []byte(creds.Secret)) + if err != nil { + return nil, err + } + hmacSignedStr := crypto.HexEncodeToString(hmacSigned) + headers := make(map[string]string) + headers["X-MBX-APIKEY"] = creds.Key + fullPath = common.EncodeURLValues(fullPath, params) + fullPath += "&signature=" + hmacSignedStr + return &request.Item{ + Method: method, + Path: fullPath, + Headers: headers, + Result: &interim, + AuthRequest: true, + Verbose: bi.Verbose, + HTTPDebugging: bi.HTTPDebugging, + HTTPRecording: bi.HTTPRecording}, nil + }) + if err != nil { + return err + } + errCap := struct { + Success bool `json:"success"` + Message string `json:"msg"` + Code int64 `json:"code"` + }{} + if err := json.Unmarshal(interim, &errCap); err == nil { + if !errCap.Success && errCap.Message != "" && errCap.Code != 200 { + return errors.New(errCap.Message) + } + } + return json.Unmarshal(interim, result) +} + +// ----- Web socket related methods + +// GetWsAuthStreamKey this method 'Creates User Data Stream' will retrieve a key to use for authorised WS streaming +// Same as that of Binance +// Start a new user data stream. The stream will close after 60 minutes unless a keepalive is sent. +// If the account has an active listenKey, +// that listenKey will be returned and its validity will be extended for 60 minutes. +func (bi *Binanceus) GetWsAuthStreamKey(ctx context.Context) (string, error) { + endpointPath, err := bi.API.Endpoints.GetURL(exchange.RestSpotSupplementary) + if err != nil { + return "", err + } + + creds, err := bi.GetCredentials(ctx) + if err != nil { + return "", err + } + + var resp UserAccountStream + headers := make(map[string]string) + headers["X-MBX-APIKEY"] = creds.Key + item := &request.Item{ + Method: http.MethodPost, + Path: endpointPath + userAccountStream, + Headers: headers, + Result: &resp, + AuthRequest: true, + Verbose: bi.Verbose, + HTTPDebugging: bi.HTTPDebugging, + HTTPRecording: bi.HTTPRecording, + } + + err = bi.SendPayload(ctx, spotDefaultRate, func() (*request.Item, error) { + return item, nil + }) + if err != nil { + return "", err + } + return resp.ListenKey, nil +} + +// MaintainWsAuthStreamKey will Extend User Data Stream +// Similar functionality to the same method of Binance. +// Keepalive a user data stream to prevent a time out. +// User data streams will close after 60 minutes. +// It's recommended to send a ping about every 30 minutes. +func (bi *Binanceus) MaintainWsAuthStreamKey(ctx context.Context) error { + endpointPath, err := bi.API.Endpoints.GetURL(exchange.RestSpotSupplementary) + if err != nil { + return err + } + if listenKey == "" { + listenKey, err = bi.GetWsAuthStreamKey(ctx) + return err + } + + creds, err := bi.GetCredentials(ctx) + if err != nil { + return err + } + + path := endpointPath + userAccountStream + params := url.Values{} + params.Set("listenKey", listenKey) + path = common.EncodeURLValues(path, params) + headers := make(map[string]string) + headers["X-MBX-APIKEY"] = creds.Key + item := &request.Item{ + Method: http.MethodPut, + Path: path, + Headers: headers, + AuthRequest: true, + Verbose: bi.Verbose, + HTTPDebugging: bi.HTTPDebugging, + HTTPRecording: bi.HTTPRecording, + } + + return bi.SendPayload(ctx, spotDefaultRate, func() (*request.Item, error) { + return item, nil + }) +} + +// CloseUserDataStream Close out a user data stream. +func (bi *Binanceus) CloseUserDataStream(ctx context.Context) error { + endpointPath, err := bi.API.Endpoints.GetURL(exchange.RestSpotSupplementary) + if err != nil { + return err + } + if listenKey == "" { + listenKey, err = bi.GetWsAuthStreamKey(ctx) + return err + } + + creds, err := bi.GetCredentials(ctx) + if err != nil { + return err + } + + path := endpointPath + userAccountStream + params := url.Values{} + params.Set("listenKey", listenKey) + path = common.EncodeURLValues(path, params) + headers := make(map[string]string) + headers["X-MBX-APIKEY"] = creds.Key + item := &request.Item{ + Method: http.MethodDelete, + Path: path, + Headers: headers, + AuthRequest: true, + Verbose: bi.Verbose, + HTTPDebugging: bi.HTTPDebugging, + HTTPRecording: bi.HTTPRecording, + } + + return bi.SendPayload(ctx, spotDefaultRate, func() (*request.Item, error) { + return item, nil + }) +} diff --git a/exchanges/binanceus/binanceus_test.go b/exchanges/binanceus/binanceus_test.go new file mode 100644 index 00000000..c2d8851f --- /dev/null +++ b/exchanges/binanceus/binanceus_test.go @@ -0,0 +1,1943 @@ +package binanceus + +import ( + "context" + "encoding/json" + "errors" + "log" + "os" + reflects "reflect" + "strings" + "sync" + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/core" + "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/kline" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" + "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" +) + +// Please supply your own keys here to test authenticated endpoints +const ( + apiKey = "" + apiSecret = "" + canManipulateRealOrders = false +) + +var ( + bi Binanceus + testPairMapping = currency.NewPair(currency.BTC, currency.USDT) + // this lock guards against orderbook tests race + binanceusOrderBookLock = &sync.Mutex{} +) + +func TestMain(m *testing.M) { + cfg := config.GetConfig() + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal("Binanceus load config error", err) + } + + exchCfg, err := cfg.GetExchangeConfig("Binanceus") + if err != nil { + log.Fatal(err) + } + exchCfg.API.AuthenticatedSupport = true + exchCfg.API.AuthenticatedWebsocketSupport = true + exchCfg.API.Credentials.Key = apiKey + exchCfg.API.Credentials.Secret = apiSecret + bi.SetDefaults() + bi.Websocket = sharedtestvalues.NewTestWebsocket() + bi.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + err = bi.Setup(exchCfg) + if err != nil { + log.Fatal("Binanceus TestMain()", err) + } + bi.setupOrderbookManager() + err = bi.Start(nil) + if !errors.Is(err, common.ErrNilPointer) { + log.Fatalf("%s received: '%v' but expected: '%v'", bi.Name, err, common.ErrNilPointer) + } + var testWg sync.WaitGroup + err = bi.Start(&testWg) + if err != nil { + log.Fatal("Binanceus Starting error ", err) + } + os.Exit(m.Run()) +} + +func areTestAPIKeysSet() bool { + return bi.ValidateAPICredentials(bi.GetDefaultCredentials()) == nil +} + +func TestServerTime(t *testing.T) { + t.Parallel() + if _, er := bi.GetServerTime(context.Background(), asset.Spot); er != nil { + t.Error("Binanceus SystemTime() error", er) + } +} + +func TestServerStatus(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + if _, er := bi.GetSystemStatus(context.Background()); er != nil { + t.Error("Binanceus GetSystemStatus() error", er) + } +} + +func TestGetExchangeInfo(t *testing.T) { + t.Parallel() + _, err := bi.GetExchangeInfo(context.Background()) + if err != nil { + t.Error("Binanceus GetExchangeInfo() error", err) + } +} + +func TestUpdateTicker(t *testing.T) { + t.Parallel() + r, err := bi.UpdateTicker(context.Background(), testPairMapping, asset.Spot) + if err != nil { + t.Error(err) + } + if r.Pair.Base != currency.BTC && r.Pair.Quote != currency.USDT { + t.Error("Binanceus UpdateTicker() invalid pair values") + } +} + +func TestUpdateTickers(t *testing.T) { + t.Parallel() + err := bi.UpdateTickers(context.Background(), asset.Spot) + if err != nil { + t.Error(err) + } +} + +func TestUpdateOrderBook(t *testing.T) { + t.Parallel() + _, er := bi.UpdateOrderbook(context.Background(), testPairMapping, asset.Spot) + if er != nil { + t.Error("Binanceus UpdateOrderBook() error", er) + } +} + +func TestFetchTradablePairs(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, err := bi.FetchTradablePairs(context.Background(), asset.Spot) + if err != nil { + t.Error("Binanceus FetchTradablePairs() error", err) + } +} + +func TestUpdateTradablePairs(t *testing.T) { + t.Parallel() + err := bi.UpdateTradablePairs(context.Background(), false) + if err != nil { + t.Error("Binanceus UpdateTradablePairs() error", err) + } +} + +func TestFetchAccountInfo(t *testing.T) { + if !areTestAPIKeysSet() { + t.SkipNow() + } + t.Parallel() + if _, err := bi.FetchAccountInfo(context.Background(), asset.Spot); err != nil { + t.Error("Binanceus FetchAccountInfo() error", err) + } +} + +func TestUpdateAccountInfo(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, err := bi.UpdateAccountInfo(context.Background(), asset.Spot) + if err != nil { + t.Error("Binanceus UpdateAccountInfo() error", err) + } +} + +func TestGetRecentTrades(t *testing.T) { + t.Parallel() + pair := currency.Pair{Base: currency.BTC, Quote: currency.USD} + _, err := bi.GetRecentTrades(context.Background(), pair, asset.Spot) + if err != nil { + t.Error("Binanceus GetRecentTrades() error", err) + } +} + +func TestGetHistoricTrades(t *testing.T) { + t.Parallel() + pair := currency.Pair{Base: currency.BTC, Quote: currency.USD} + _, err := bi.GetHistoricTrades(context.Background(), pair, asset.Spot, time.Time{}, time.Time{}) + if err != nil { + t.Error("Binanceus GetHistoricTrades() error", err) + } +} + +func TestGetFeeByType(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + if _, er := bi.GetFeeByType(context.Background(), &exchange.FeeBuilder{ + IsMaker: true, + Pair: currency.NewPair(currency.USD, currency.BTC), + FeeType: exchange.CryptocurrencyTradeFee, + }); er != nil { + t.Error("Binanceus GetFeeByType() error", er) + } + if _, er := bi.GetFeeByType(context.Background(), &exchange.FeeBuilder{ + IsMaker: true, + Pair: currency.NewPair(currency.USD, currency.BTC), + FeeType: exchange.CryptocurrencyWithdrawalFee, + }); er != nil { + t.Error("Binanceus GetFeeByType() error", er) + } +} + +func TestSubmitOrder(t *testing.T) { + t.Parallel() + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip(bi.Name, "API keys set, canManipulateRealOrders false, skipping test") + } + var orderSubmission = &order.Submit{ + Pair: currency.Pair{ + Base: currency.XRP, + Quote: currency.USD, + }, + AssetType: asset.Spot, + Side: order.Sell, + Type: order.Limit, + Price: 1000, + Amount: 20, + ClientID: "binanceSamOrder", + Exchange: bi.Name, + } + response, err := bi.SubmitOrder(context.Background(), orderSubmission) + switch { + case areTestAPIKeysSet() && err != nil && strings.Contains(err.Error(), "{\"code\":-1013,\"msg\":\"Market is closed.\""): + t.Skip("Binanceus SubmitOrder() Market is Closed") + case areTestAPIKeysSet() && err != nil: + t.Errorf("Binanceus SubmitOrder() Could not place order: %v", err) + case areTestAPIKeysSet() && response.Status != order.Filled: + t.Error("Binanceus SubmitOrder() Order not placed") + case !areTestAPIKeysSet() && err == nil: + t.Error("Binanceus SubmitOrder() Expecting an error when no keys are set") + } +} + +func TestCancelOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.SkipNow() + } + pair := currency.NewPair(currency.BTC, currency.USD) + err := bi.CancelOrder(context.Background(), &order.Cancel{ + AssetType: asset.Spot, + OrderID: "1337", + }) + if err != nil && !errors.Is(err, errMissingCurrencySymbol) { + t.Error("Binanceus CancelOrder() error", err) + } + err = bi.CancelOrder(context.Background(), &order.Cancel{ + AssetType: asset.Spot, + Pair: currency.NewPair(currency.BTC, currency.USDT), + }) + if err != nil && !(errors.Is(err, errEitherOrderIDOrClientOrderIDIsRequired) || strings.Contains(err.Error(), "ID not set")) { + t.Errorf("Binanceus CancelOrder() expecting %v, but found %v", errEitherOrderIDOrClientOrderIDIsRequired, err) + } + var cancellationOrder = &order.Cancel{ + OrderID: "1", + Pair: pair, + AssetType: asset.Spot, + } + err = bi.CancelOrder(context.Background(), cancellationOrder) + if err != nil && !strings.Contains(err.Error(), "Unknown order sent.") { + t.Error("Binanceus CancelOrder() error", err) + } +} + +func TestCancelAllOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("Binanceus API keys set, canManipulateRealOrders false, skipping test") + } + var orderCancellation = &order.Cancel{ + Pair: currency.NewPair(currency.LTC, currency.BTC), + AssetType: asset.Spot, + } + if _, err := bi.CancelAllOrders(context.Background(), orderCancellation); err != nil { + t.Error("Binanceus CancelAllOrders() error", err) + } +} + +func TestGetOrderInfo(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("Binanceus GetOrderInfo() skipping test: api keys not set") + } + tradablePairs, err := bi.FetchTradablePairs(context.Background(), + asset.Spot) + if err != nil { + t.Error(err) + } + if len(tradablePairs) == 0 { + t.Fatal("Binanceus GetOrderInfo() no tradable pairs") + } + cp, err := currency.NewPairFromString(tradablePairs[0]) + if err != nil { + t.Error("Binanceus GetOrderInfo() error", err) + } + _, err = bi.GetOrderInfo(context.Background(), + "123", cp, asset.Spot) + if !strings.Contains(err.Error(), "Order does not exist.") { + t.Error("Binanceus GetOrderInfo() error", err) + } +} + +func TestGetDepositAddress(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, err := bi.GetDepositAddress(context.Background(), currency.EMPTYCODE, "", currency.BNB.String()) + if err != nil && !errors.Is(err, errMissingRequiredArgumentCoin) { + t.Errorf("Binanceus GetDepositAddress() expecting %v, but found %v", errMissingRequiredArgumentCoin, err) + } + if _, err := bi.GetDepositAddress(context.Background(), currency.USDT, "", currency.BNB.String()); err != nil { + t.Error("Binanceus GetDepositAddress() error", err) + } +} + +func TestGetWithdrawalHistory(t *testing.T) { + t.Parallel() + if areTestAPIKeysSet() && !canManipulateRealOrders { + t.Skip("Binanceus API keys set, canManipulateRealOrders false, skipping test") + } + _, err := bi.GetWithdrawalsHistory(context.Background(), currency.ETH, asset.Spot) + switch { + case areTestAPIKeysSet() && err != nil: + t.Error("Binanceus GetWithdrawalsHistory() error", err) + case !areTestAPIKeysSet() && err == nil: + t.Error("Binanceus GetWithdrawalsHistory() expecting an error when no keys are set") + } +} + +func TestWithdrawFiat(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.SkipNow() + } + if _, er := bi.WithdrawFiat(context.Background(), &WithdrawFiatRequestParams{ + PaymentChannel: "SILVERGATE", + PaymentAccount: "myaccount", + PaymentMethod: "SEN", + Amount: 1, + }); er != nil && !strings.Contains(er.Error(), "You are not authorized to execute this request.") { + t.Error("Binanceus WithdrawFiat() error", er) + } +} + +func TestGetActiveOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + var getOrdersRequest = order.GetOrdersRequest{ + Type: order.AnyType, + AssetType: asset.Spot, + } + _, err := bi.GetActiveOrders(context.Background(), &getOrdersRequest) + if err != nil { + t.Error("Binanceus GetActiveOrders() error", err) + } +} + +func TestWithdraw(t *testing.T) { + t.Parallel() + if !(areTestAPIKeysSet() && canManipulateRealOrders) { + t.Skip("Binanceus API keys set, canManipulateRealOrders false, skipping test") + } + withdrawCryptoRequest := withdraw.Request{ + Exchange: bi.Name, + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + Crypto: withdraw.CryptoRequest{ + Address: core.BitcoinDonationAddress, + Chain: "BSC", + }, + } + _, err := bi.WithdrawCryptocurrencyFunds(context.Background(), &withdrawCryptoRequest) + if err != nil && !strings.EqualFold(errAmountValueMustBeGreaterThan0.Error(), err.Error()) { + t.Errorf("Binanceus Withdraw() expecting %v, but found %v", errAmountValueMustBeGreaterThan0, err) + } else if !areTestAPIKeysSet() && err == nil { + t.Error("Binanceus Withdraw() expecting an error when no keys are set") + } + withdrawCryptoRequest.Amount = 1 + _, err = bi.WithdrawCryptocurrencyFunds(context.Background(), &withdrawCryptoRequest) + if err != nil && !strings.Contains(err.Error(), "You are not authorized to execute this request.") { + t.Error("Binanceus WithdrawCryptocurrencyFunds() error", err) + } +} + +func TestGetFee(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + var feeBuilder = &exchange.FeeBuilder{ + Amount: 1, + FeeType: exchange.CryptocurrencyTradeFee, + Pair: currency.NewPair(currency.BTC, currency.LTC), + PurchasePrice: 1, + } + _, er := bi.GetFeeByType(context.Background(), feeBuilder) + if er != nil { + t.Fatal("Binanceus GetFeeByType() error", er) + } + var withdrawalFeeBuilder = &exchange.FeeBuilder{ + Amount: 1, + FeeType: exchange.CryptocurrencyWithdrawalFee, + Pair: currency.NewPair(currency.BTC, currency.LTC), + PurchasePrice: 1, + } + _, er = bi.GetFeeByType(context.Background(), withdrawalFeeBuilder) + if er != nil { + t.Fatal("Binanceus GetFeeByType() error", er) + } + var offlineFeeTradeBuilder = &exchange.FeeBuilder{ + Amount: 1, + FeeType: exchange.OfflineTradeFee, + Pair: currency.NewPair(currency.BTC, currency.LTC), + PurchasePrice: 1, + } + _, er = bi.GetFeeByType(context.Background(), offlineFeeTradeBuilder) + if er != nil { + t.Fatal("Binanceus GetFeeByType() error", er) + } +} + +func TestGetHistoricCandles(t *testing.T) { + t.Parallel() + pair := currency.NewPair(currency.BTC, currency.USDT) + startTime := time.Date(2020, 9, 1, 0, 0, 0, 0, time.UTC) + endTime := time.Date(2021, 2, 15, 0, 0, 0, 0, time.UTC) + _, er := bi.GetHistoricCandles(context.Background(), pair, asset.Spot, startTime, endTime, kline.Interval(time.Hour*5)) + if !strings.Contains(er.Error(), "interval not supported") { + t.Errorf("Binanceus GetHistoricCandles() expected %s, but found %v", "interval not supported", er) + } + _, er = bi.GetHistoricCandles(context.Background(), pair, asset.Spot, time.Time{}, time.Time{}, kline.FourHour) + if er != nil { + t.Error("Binanceus GetHistoricCandles() error", er) + } +} + +func TestGetHistoricCandlesExtended(t *testing.T) { + t.Parallel() + pair := currency.NewPair(currency.BTC, currency.USDT) + startTime := time.Date(2020, 9, 1, 0, 0, 0, 0, time.UTC) + endTime := time.Date(2021, 2, 15, 0, 0, 0, 0, time.UTC) + _, er := bi.GetHistoricCandlesExtended(context.Background(), pair, asset.Spot, startTime, endTime, kline.FourHour) + if er != nil && !strings.Contains(er.Error(), "interval not supported") { + t.Errorf("Binanceus GetHistoricCandlesExtended() expected %s, but found %v", "interval not supported", er) + } + startTime = time.Now().Add(-time.Hour * 30) + endTime = time.Now() + _, er = bi.GetHistoricCandlesExtended(context.Background(), pair, asset.Spot, startTime, endTime, kline.FourHour) + if er != nil { + t.Error("Binanceus GetHistoricCandlesExtended() error", er) + } +} + +/************************************************************************/ + +// TestGetMostRecentTrades -- test most recent trades end-point +func TestGetMostRecentTrades(t *testing.T) { + t.Parallel() + _, err := bi.GetMostRecentTrades(context.Background(), RecentTradeRequestParams{ + Symbol: currency.NewPair(currency.BTC, currency.USDT), + Limit: 15, + }) + if err != nil { + t.Error("Binanceus GetMostRecentTrades() error", err) + } +} + +func TestGetHistoricalTrades(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, err := bi.GetHistoricalTrades(context.Background(), HistoricalTradeParams{ + Symbol: "BTCUSDT", + Limit: 5, + FromID: 0, + }) + if err != nil { + t.Errorf("Binanceus GetHistoricalTrades() error: %v", err) + } +} + +func TestGetAggregateTrades(t *testing.T) { + t.Parallel() + _, err := bi.GetAggregateTrades(context.Background(), + &AggregatedTradeRequestParams{ + Symbol: currency.NewPair(currency.BTC, currency.USDT), + Limit: 5, + }) + if err != nil { + t.Error("Binanceus GetAggregateTrades() error", err) + } +} + +func TestGetOrderBookDepth(t *testing.T) { + t.Parallel() + _, er := bi.GetOrderBookDepth(context.Background(), &OrderBookDataRequestParams{ + Symbol: currency.NewPair(currency.BTC, currency.USDT), + Limit: 1000, + }) + if er != nil { + t.Error("Binanceus GetOrderBook() error", er) + } +} + +func TestGetCandlestickData(t *testing.T) { + t.Parallel() + _, er := bi.GetSpotKline(context.Background(), &KlinesRequestParams{ + Symbol: currency.NewPair(currency.BTC, currency.USDT), + Interval: kline.FiveMin.Short(), + Limit: 24, + StartTime: time.Unix(1577836800, 0), + EndTime: time.Unix(1580515200, 0), + }) + if er != nil { + t.Error("Binanceus GetSpotKline() error", er) + } +} + +func TestGetPriceDatas(t *testing.T) { + t.Parallel() + _, er := bi.GetPriceDatas(context.Background()) + if er != nil { + t.Error("Binanceus GetPriceDatas() error", er) + } +} + +func TestGetSinglePriceData(t *testing.T) { + t.Parallel() + _, er := bi.GetSinglePriceData(context.Background(), currency.Pair{ + Base: currency.BTC, + Quote: currency.USDT, + }) + if er != nil { + t.Error("Binanceus GetSinglePriceData() error", er) + } +} + +func TestGetAveragePrice(t *testing.T) { + t.Parallel() + _, err := bi.GetAveragePrice(context.Background(), currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Error("Binance GetAveragePrice() error", err) + } +} + +func TestGetBestPrice(t *testing.T) { + t.Parallel() + _, err := bi.GetBestPrice(context.Background(), currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Error("Binanceus GetBestPrice() error", err) + } +} + +func TestGetPriceChangeStats(t *testing.T) { + t.Parallel() + _, err := bi.GetPriceChangeStats(context.Background(), currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Error("Binance GetPriceChangeStats() error", err) + } +} + +func TestGetTickers(t *testing.T) { + t.Parallel() + _, err := bi.GetTickers(context.Background()) + if err != nil { + t.Error("Binance TestGetTickers error", err) + } +} + +func TestGetAccount(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetAccount(context.Background()) + if er != nil { + t.Error("Binanceus GetAccount() error", er) + } +} + +func TestGetUserAccountStatus(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetUserAccountStatus(context.Background(), 3000) + if er != nil { + t.Error("Binanceus GetUserAccountStatus() error", er) + } +} + +func TestGetUserAPITradingStatus(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetUserAPITradingStatus(context.Background(), 3000) + if er != nil { + t.Error("Binanceus GetUserAPITradingStatus() error", er) + } +} +func TestGetTradeFee(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetTradeFee(context.Background(), 3000, "BTC-USDT") + if er != nil { + t.Error("Binanceus GetTradeFee() error", er) + } +} + +func TestGetAssetDistributionHistory(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetAssetDistributionHistory(context.Background(), "", 0, 0, 3000) + if er != nil { + t.Error("Binanceus GetAssetDistributionHistory() error", er) + } +} + +func TestGetMasterAccountTotalUSDValue(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + if _, er := bi.GetMasterAccountTotalUSDValue(context.Background(), "", 0, 0); er != nil && !strings.Contains(er.Error(), "Sub-account function is not enabled.") { + t.Errorf("Binanceus GetMasterAccountTotalUSDValue() expecting %s, but found %v", "Sub-account function is not enabled.", er) + } +} + +func TestGetSubaccountStatusList(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + if _, er := bi.GetSubaccountStatusList(context.Background(), ""); er != nil && !errors.Is(er, errMissingSubAccountEmail) { + t.Errorf("Binanceus GetSubaccountStatusList() expecting %v, but found %v", errMissingSubAccountEmail, er) + } + if _, er := bi.GetSubaccountStatusList(context.Background(), "someone@thrasher.corp"); er != nil && !strings.Contains(er.Error(), "Sub-account function is not enabled.") { + t.Errorf("Binanceus GetSubaccountStatusList() expecting %s, but found %v", "Sub-account function is not enabled.", er) + } +} + +func TestGetSubAccountDepositAddress(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + if _, er := bi.GetSubAccountDepositAddress(context.Background(), SubAccountDepositAddressRequestParams{}); er != nil && !errors.Is(er, errMissingSubAccountEmail) { + t.Errorf("Binanceus GetSubAccountDepositAddress() %v, but found %v", errMissingSubAccountEmail, er) + } + if _, er := bi.GetSubAccountDepositAddress(context.Background(), SubAccountDepositAddressRequestParams{ + Email: "someone@thrasher.io", + }); er != nil && !errors.Is(er, errMissingCurrencyCoin) { + t.Errorf("Binanceus GetSubAccountDepositAddress() %v, but found %v", errMissingCurrencyCoin, er) + } + if _, er := bi.GetSubAccountDepositAddress(context.Background(), SubAccountDepositAddressRequestParams{ + Email: "someone@thrasher.io", + Coin: currency.BTC, + }); er != nil && !strings.Contains(er.Error(), "This parent sub have no relation") { + t.Errorf("Binanceus GetSubAccountDepositAddress() %v, but found %v", errMissingCurrencyCoin, er) + } +} + +var subAccountDepositHistoryItemJSON = `{ + "amount": "9.9749", + "coin": "BTC", + "network": "btc", + "status": 4, + "address": "bc1qxurvdd7tzn09agdvg3j8xpm3f7e978y07wg83s", + "addressTag": "", + "txId": "0x1b4b8c8090d15e3c1b0476b1c19118b1f00066e01de567cd7bc5b6e9c100193f", + "insertTime": 1652942429211, + "transferType": 0, + "confirmTimes": "0/0" +}` + +func TestGetSubAccountDepositHistory(t *testing.T) { + t.Parallel() + var resp SubAccountDepositItem + if er := json.Unmarshal([]byte(subAccountDepositHistoryItemJSON), &resp); er != nil { + t.Error("Binanceus Decerializing to SubAccountDepositItem error", er) + } + if !areTestAPIKeysSet() { + t.SkipNow() + } + if _, er := bi.GetSubAccountDepositHistory(context.Background(), "", currency.BTC, 1, time.Time{}, time.Time{}, 0, 0); er != nil && !errors.Is(er, errMissingSubAccountEmail) { + t.Errorf("Binanceus GetSubAccountDepositHistory() expecting %v, but found %v", errMissingSubAccountEmail, er) + } + if _, er := bi.GetSubAccountDepositHistory(context.Background(), "someone@thrasher.io", currency.BTC, 1, time.Time{}, time.Time{}, 0, 0); er != nil && !strings.Contains(er.Error(), "This parent sub have no relation") { + t.Errorf("Binanceus GetSubAccountDepositHistory() expecting %s, but found %v", "This parent sub have no relation", er) + } +} + +var subaccountItemJSON = `{ + "email": "123@test.com", + "status": "enabled", + "activated": true, + "mobile": "91605290", + "gAuth": true, + "createTime": 1544433328000 +}` + +func TestGetSubaccountInformation(t *testing.T) { + t.Parallel() + var resp SubAccount + if er := json.Unmarshal([]byte(subaccountItemJSON), &resp); er != nil { + t.Error("Binanceus decerializing to SubAccount error", er) + } + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetSubaccountInformation(context.Background(), 1, 100, "", "") + if er != nil && !strings.Contains(er.Error(), "Sub-account function is not enabled.") { + t.Error("Binanceus GetSubaccountInformation() error", er) + } +} + +var referalRewardHistoryResponse = `{ + "total": 1, + "rows": [ + { + "userId": 350991652, + "rewardAmount": "8", + "receiveDateTime": 1651131084091, + "rewardType": "USD" + } + ] +}` + +func TestGetReferralRewardHistory(t *testing.T) { + t.Parallel() + var resp ReferralRewardHistoryResponse + if er := json.Unmarshal([]byte(referalRewardHistoryResponse), &resp); er != nil { + t.Error("Binanceus decerializing to ReferalRewardHistoryResponse error", er) + } + if !areTestAPIKeysSet() { + t.SkipNow() + } + if _, er := bi.GetReferralRewardHistory(context.Background(), 9, 5, 50); !errors.Is(er, errInvalidUserBusinessType) { + t.Errorf("Binanceus GetReferralRewardHistory() expecting %v, but found %v", errInvalidUserBusinessType, er) + } + if _, er := bi.GetReferralRewardHistory(context.Background(), 1, 0, 50); !errors.Is(er, errMissingPageNumber) { + t.Errorf("Binanceus GetReferralRewardHistory() expecting %v, but found %v", errMissingPageNumber, er) + } + if _, er := bi.GetReferralRewardHistory(context.Background(), 1, 5, 0); !errors.Is(er, errInvalidRowNumber) { + t.Errorf("Binanceus GetReferralRewardHistory() expecting %v, but found %v", errInvalidRowNumber, er) + } + if _, er := bi.GetReferralRewardHistory(context.Background(), 1, 5, 50); er != nil { + t.Error("Binanceus GetReferralRewardHistory() error", er) + } +} + +func TestGetSubaccountTransferHistory(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetSubaccountTransferHistory(context.Background(), "", 0, 0, 0, 0) + if !errors.Is(er, errNotValidEmailAddress) { + t.Errorf("Binanceus GetSubaccountTransferHistory() expected %v, but received: %s", errNotValidEmailAddress, er) + } + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Binanceus GetSubaccountTransferHistory() skipping test, either api keys or canManipulateRealOrders isn't set") + } + _, er = bi.GetSubaccountTransferHistory(context.Background(), "example@golang.org", 0, 0, 0, 0) + if er != nil && !(errors.Is(er, errNotValidEmailAddress) || strings.Contains(er.Error(), "Sub-account function is not enabled.")) { + t.Fatalf("Binanceus GetSubaccountTransferHistory() error %v", er) + } +} + +func TestExecuteSubAccountTransfer(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.SkipNow() + } + _, er := bi.ExecuteSubAccountTransfer(context.Background(), &SubAccountTransferRequestParams{}) + if !errors.Is(er, errUnacceptableSenderEmail) { + t.Errorf("binanceus error: expected %v, but found %v", errUnacceptableSenderEmail, er) + } + _, er = bi.ExecuteSubAccountTransfer(context.Background(), &SubAccountTransferRequestParams{ + FromEmail: "fromemail@thrasher.io", + ToEmail: "toemail@threasher.io", + Asset: "BTC", + Amount: 0.000005, + }) + if er != nil && !strings.Contains(er.Error(), "You are not authorized to execute this request.") { + t.Errorf("Binanceus GetSubaccountTransferHistory() error %v", er) + } +} + +func TestGetSubaccountAssets(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetSubaccountAssets(context.Background(), "") + if !errors.Is(er, errNotValidEmailAddress) { + t.Errorf("Binanceus GetSubaccountAssets() expected %v, but found %v", er, errNotValidEmailAddress) + } + _, er = bi.GetSubaccountAssets(context.Background(), "subaccount@thrasher.io") + if er != nil && !strings.Contains(er.Error(), "This account does not exist.") { + t.Fatal("Binanceus GetSubaccountAssets() error", er) + } +} + +func TestGetOrderRateLimits(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetOrderRateLimits(context.Background(), 0) + if er != nil { + t.Error("Binanceus GetOrderRateLimits() error", er) + } +} + +var testNewOrderResponseJSON = `{ + "symbol": "BTCUSDT", + "orderId": 28, + "orderListId": -1, + "clientOrderId": "6gCrw2kRUAF9CvJDGP16IP", + "transactTime": 1507725176595, + "price": "0.00000000", + "origQty": "10.00000000", + "executedQty": "10.00000000", + "cummulativeQuoteQty": "10.00000000", + "status": "FILLED", + "timeInForce": "GTC", + "type": "MARKET", + "side": "SELL" + }` + +func TestNewOrderTest(t *testing.T) { + t.Parallel() + var resp NewOrderResponse + if er := json.Unmarshal([]byte(testNewOrderResponseJSON), &resp); er != nil { + t.Error("Binanceus decerializing to Order error", er) + } + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.SkipNow() + } + req := &NewOrderRequest{ + Symbol: currency.NewPair(currency.LTC, currency.BTC), + Side: order.Buy.String(), + TradeType: BinanceRequestParamsOrderLimit, + Price: 0.0025, + Quantity: 100000, + TimeInForce: BinanceRequestParamsTimeGTC, + } + _, err := bi.NewOrderTest(context.Background(), req) + if err != nil { + t.Error("Binanceus NewOrderTest() error", err) + } + req = &NewOrderRequest{ + Symbol: currency.NewPair(currency.LTC, currency.BTC), + Side: order.Sell.String(), + TradeType: BinanceRequestParamsOrderMarket, + Price: 0.0045, + QuoteOrderQty: 10, + } + _, err = bi.NewOrderTest(context.Background(), req) + if err != nil { + t.Error("NewOrderTest() error", err) + } +} + +func TestNewOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.SkipNow() + } + req := &NewOrderRequest{ + Symbol: currency.NewPair(currency.LTC, currency.BTC), + Side: order.Buy.String(), + TradeType: BinanceRequestParamsOrderLimit, + Price: 0.0025, + Quantity: 100000, + TimeInForce: BinanceRequestParamsTimeGTC, + } + if _, err := bi.NewOrder(context.Background(), req); err != nil && !strings.Contains(err.Error(), "Account has insufficient balance for requested action") { + t.Error("Binanceus NewOrder() error", err) + } +} + +func TestGetOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetOrder(context.Background(), &OrderRequestParams{}) + if !errors.Is(er, errIncompleteArguments) { + t.Errorf("Binanceus GetOrder() error expecting %v, but found %v", errIncompleteArguments, er) + } + _, er = bi.GetOrder(context.Background(), &OrderRequestParams{ + Symbol: "BTCUSDT", + OrigClientOrderID: "something", + }) + // You can check the existence of an order using a valid Symbol and OrigClient Order ID + if er != nil && !strings.Contains(er.Error(), "Order does not exist.") { + t.Error("Binanceus GetOrder() error", er) + } +} + +var openOrdersItemJSON = `{ + "symbol": "LTCBTC", + "orderId": 1, + "orderListId": -1, + "clientOrderId": "myOrder1", + "price": "0.1", + "origQty": "1.0", + "executedQty": "0.0", + "cummulativeQuoteQty": "0.0", + "status": "NEW", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "stopPrice": "0.0", + "icebergQty": "0.0", + "time": 1499827319559, + "updateTime": 1499827319559, + "isWorking": true, + "origQuoteOrderQty": "0.000000" + }` + +func TestGetAllOpenOrders(t *testing.T) { + t.Parallel() + var resp Order + if er := json.Unmarshal([]byte(openOrdersItemJSON), &resp); er != nil { + t.Error("Binanceus decerializing to Order error", er) + } + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetAllOpenOrders(context.Background(), "") + if er != nil { + t.Error("Binanceus GetAllOpenOrders() error", er) + } +} + +func TestCancelExistingOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Binanceus CancelExistingOrder() skipping test, either api keys or canManipulateRealOrders isn't set") + } + _, er := bi.CancelExistingOrder(context.Background(), &CancelOrderRequestParams{Symbol: currency.NewPair(currency.BTC, currency.USDT)}) + if er != nil && !errors.Is(er, errEitherOrderIDOrClientOrderIDIsRequired) { + t.Errorf("Binanceus CancelExistingOrder() error expecting %v, but found %v", errEitherOrderIDOrClientOrderIDIsRequired, er) + } + _, er = bi.CancelExistingOrder(context.Background(), &CancelOrderRequestParams{ + Symbol: currency.NewPair(currency.BTC, currency.USDT), + ClientSuppliedOrderID: "1234", + }) + if er != nil && !strings.Contains(er.Error(), "Unknown order sent.") { + t.Error("Binanceus CancelExistingorder() error", er) + } +} + +func TestCancelOpenOrdersForSymbol(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.SkipNow() + } + _, er := bi.CancelOpenOrdersForSymbol(context.Background(), "") + if !errors.Is(er, errMissingCurrencySymbol) { + t.Errorf("Binanceus CancelOpenOrdersForSymbol() error expecting %v, but found %v", errIncompleteArguments, er) + } + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("Binanceus CancelOpenOrdersForSymbol() skipping test, either api keys or canManipulateRealOrders isn't set") + } + _, er = bi.CancelOpenOrdersForSymbol(context.Background(), "BTCUSDT") + if er != nil && !strings.Contains(er.Error(), "Unknown order sent") { + t.Error("Binanceus CancelOpenOrdersForSymbol() error", er) + } +} + +// TestGetTrades test for fetching the list of +// trades attached with this account. +func TestGetTrades(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetTrades(context.Background(), &GetTradesParams{}) + if !errors.Is(er, errIncompleteArguments) { + t.Errorf(" Binanceus GetTrades() expecting error %v, but found %v", errIncompleteArguments, er) + } + _, er = bi.GetTrades(context.Background(), &GetTradesParams{Symbol: "BTCUSDT"}) + if er != nil { + t.Error("Binanceus GetTrades() error", er) + } +} + +func TestCreateNewOCOOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.SkipNow() + } + _, er := bi.CreateNewOCOOrder(context.Background(), + &OCOOrderInputParams{ + StopPrice: 1000, + Side: order.Buy.String(), + Quantity: 0.0000001, + Price: 1232334.00, + }) + if !errors.Is(er, errIncompleteArguments) { + t.Errorf("Binanceus CreatenewOCOOrder() error expected %v, but found %v", errIncompleteArguments, er) + } + _, er = bi.CreateNewOCOOrder( + context.Background(), + &OCOOrderInputParams{ + Symbol: "XTZUSD", + Price: 100, + StopPrice: 3, + StopLimitPrice: 2.5, + Side: order.Buy.String(), + Quantity: 1, + StopLimitTimeInForce: "GTC", + RecvWindow: 6000, + }) + if er != nil && !strings.Contains(er.Error(), "Precision is over the maximum defined for this asset.") { + t.Error("Binanceus CreateNewOCOOrder() error", er) + } +} + +var ocoOrderJSON = `{ + "orderListId": 27, + "contingencyType": "OCO", + "listStatusType": "EXEC_STARTED", + "listOrderStatus": "EXECUTING", + "listClientOrderId": "h2USkA5YQpaXHPIrkd96xE", + "transactionTime": 1565245656253, + "symbol": "LTCBTC", + "orders": [ + { + "symbol": "LTCBTC", + "orderId": 4, + "clientOrderId": "qD1gy3kc3Gx0rihm9Y3xwS" + }, + { + "symbol": "LTCBTC", + "orderId": 5, + "clientOrderId": "ARzZ9I00CPM8i3NhmU9Ega" + } + ] + }` + +func TestGetOCOOrder(t *testing.T) { + t.Parallel() + var resp OCOOrderResponse + if er := json.Unmarshal([]byte(ocoOrderJSON), &resp); er != nil { + t.Error("Binanceus decerializing OCOOrderResponse error", er) + } + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetOCOOrder(context.Background(), &GetOCOOrderRequestParams{}) + if !errors.Is(er, errIncompleteArguments) { + t.Errorf("Binanceus GetOCOOrder() error expecting %v, but found %v", errIncompleteArguments, er) + } + _, er = bi.GetOCOOrder(context.Background(), &GetOCOOrderRequestParams{ + OrderListID: "123445", + }) + if er != nil && !strings.Contains(er.Error(), "Order list does not exist.") { + t.Error("Binanceus GetOCOOrder() error", er) + } +} + +func TestGetAllOCOOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetAllOCOOrder(context.Background(), &OCOOrdersRequestParams{}) + if er != nil { + t.Error("Binanceus GetAllOCOOrder() error", er) + } +} + +func TestGetOpenOCOOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetOpenOCOOrders(context.Background(), 0) + if er != nil { + t.Error("Binanceus GetOpenOCOOrders() error", er) + } +} + +func TestCancelOCOOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.SkipNow() + } + _, er := bi.CancelOCOOrder(context.Background(), &OCOOrdersDeleteRequestParams{}) + if !errors.Is(er, errIncompleteArguments) { + t.Errorf("Binanceus CancelOCOOrder() error expected %v, but found %v", errIncompleteArguments, er) + } +} + +// OTC end Points test code. +func TestGetSupportedCoinPairs(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetSupportedCoinPairs(context.Background(), currency.Pair{Base: currency.BTC, Quote: currency.USDT}) + if er != nil { + t.Error("Binanceus GetSupportedCoinPairs() error", er) + } +} + +func TestRequestForQuote(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.RequestForQuote(context.Background(), &RequestQuoteParams{ToCoin: "BTC", RequestCoin: "USDT", RequestAmount: 1}) + if er != nil && !errors.Is(er, errMissingFromCoinName) { + t.Errorf("Binanceus RequestForQuote() expecting %v, but found %v", errMissingFromCoinName, er) + } + _, er = bi.RequestForQuote(context.Background(), &RequestQuoteParams{FromCoin: "ETH", RequestCoin: "USDT", RequestAmount: 1}) + if er != nil && !errors.Is(er, errMissingToCoinName) { + t.Errorf("Binanceus RequestForQuote() expecting %v, but found %v", errMissingToCoinName, er) + } + _, er = bi.RequestForQuote(context.Background(), &RequestQuoteParams{FromCoin: "ETH", ToCoin: "BTC", RequestCoin: "USDT"}) + if er != nil && !errors.Is(er, errMissingRequestAmount) { + t.Errorf("Binanceus RequestForQuote() expecting %v, but found %v", errMissingRequestAmount, er) + } + _, er = bi.RequestForQuote(context.Background(), &RequestQuoteParams{FromCoin: "ETH", ToCoin: "BTC", RequestAmount: 1}) + if er != nil && !errors.Is(er, errMissingRequestCoin) { + t.Errorf("Binanceus RequestForQuote() expecting %v, but found %v", errMissingRequestCoin, er) + } + _, er = bi.RequestForQuote(context.Background(), &RequestQuoteParams{FromCoin: "BTC", ToCoin: "USDT", RequestCoin: "BTC", RequestAmount: 1}) + if er != nil { + t.Error("Binanceus RequestForQuote() error", er) + } +} + +var testPlaceOTCTradeOrderJSON = `{ + "orderId": "10002349", + "createTime": 1641906714, + "orderStatus": "PROCESS" +}` + +func TestPlaceOTCTradeOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.SkipNow() + } + var res OTCTradeOrderResponse + er := json.Unmarshal([]byte(testPlaceOTCTradeOrderJSON), &res) + if er != nil { + t.Error("Binanceus PlaceOTCTradeOrder() error", er) + } + _, er = bi.PlaceOTCTradeOrder(context.Background(), "") + if !errors.Is(er, errMissingQuoteID) { + t.Errorf("Binanceus PlaceOTCTradeOrder() expecting %v, but found %v", errMissingQuoteID, er) + } + _, er = bi.PlaceOTCTradeOrder(context.Background(), "15848701022") + if er != nil && !strings.Contains(er.Error(), "-9000") { + t.Error("Binanceus PlaceOTCTradeOrder() error", er) + } +} + +var testGetOTCTradeOrderJSON = `{ + "quoteId": "4e5446f2cc6f44ab86ab02abf19a2fd2", + "orderId": "10002349", + "orderStatus": "SUCCESS", + "fromCoin": "BTC", + "fromAmount": 1, + "toCoin": "USDT", + "toAmount": 50550.26, + "ratio": 50550.26, + "inverseRatio": 0.00001978, + "createTime": 1641806714 +}` + +func TestGetOTCTradeOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + var val OTCTradeOrder + er := json.Unmarshal([]byte(testGetOTCTradeOrderJSON), &val) + if er != nil { + t.Error("Binanceus JSON GetOTCTradeOrder() error", er) + } + _, er = bi.GetOTCTradeOrder(context.Background(), 10002349) + if er != nil && !strings.Contains(er.Error(), "status code: 400") { + t.Error("Binanceus GetOTCTradeOrder() error ", er) + } +} + +var getAllOTCTradeOrders = `[ + { + "quoteId": "4e5446f2cc6f44ab86ab02abf19a2fd2", + "orderId": "10002349", + "orderStatus": "SUCCESS", + "fromCoin": "BTC", + "fromAmount": 1, + "toCoin": "USDT", + "toAmount": 50550.26, + "ratio": 50550.26, + "inverseRatio": 0.00001978, + "createTime": 1641806714 + }, + { + "quoteId": "15848645308", + "orderId": "10002380", + "orderStatus": "PROCESS", + "fromCoin": "SHIB", + "fromAmount": 10000, + "toCoin": "KSHIB", + "toAmount": 10, + "ratio": 0.001, + "inverseRatio": 1000, + "createTime": 1641916714 + } +] +` + +func TestGetAllOTCTradeOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + var orders []OTCTradeOrder + er := json.Unmarshal([]byte(getAllOTCTradeOrders), &orders) + if er != nil { + t.Error(er) + } + _, er = bi.GetAllOTCTradeOrders(context.Background(), &OTCTradeOrderRequestParams{}) + if er != nil { + t.Error("Binanceus GetAllOTCTradeOrders() error", er) + } +} + +var ocbsTradeOrderJSON = ` +{ + "quoteId": "4e5446f2cc6f44ab86ab02abf19abvd", + "orderId": "1000238000", + "orderStatus": "FAIL", + "fromCoin": "USD", + "fromAmount": 1000.5, + "toCoin": "ETH", + "toAmount": 0.5, + "feeCoin": "USD", + "feeAmount": 0.5, + "ratio": 2000, + "createTime": 1641916714 +}` + +func TestGetAllOCBSTradeOrders(t *testing.T) { + t.Parallel() + var orderDetail OCBSOrder + if er := json.Unmarshal([]byte(ocbsTradeOrderJSON), &orderDetail); er != nil { + t.Error("Binanceus decerializing to OCBSOrder error", er) + } + if !areTestAPIKeysSet() { + t.SkipNow() + } + if _, er := bi.GetAllOCBSTradeOrders(context.Background(), OCBSOrderRequestParams{}); er != nil { + t.Error("Binanceus GetAllOCBSTradeOrders() error", er) + } +} + +func TestGetAssetFeesAndWalletStatus(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetAssetFeesAndWalletStatus(context.Background()) + if er != nil { + t.Error("Binanceus GetAssetFeesAndWalletStatus() error", er) + } +} + +func TestWithdrawCrypto(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.SkipNow() + } + _, er := bi.WithdrawCrypto(context.Background(), &withdraw.Request{}) + if !errors.Is(er, errMissingRequiredArgumentCoin) { + t.Errorf("Binanceus WithdrawCrypto() error expecting %v, but found %v", errMissingRequiredArgumentCoin, er) + } + if _, er = bi.WithdrawCrypto(context.Background(), &withdraw.Request{ + Currency: currency.BTC, + }); !errors.Is(er, errMissingRequiredArgumentNetwork) { + t.Errorf("Binanceus WithdrawCrypto() expecting %v, but found %v", errMissingRequiredArgumentNetwork, er) + } + params := &withdraw.Request{ + Currency: currency.BTC, + } + params.Crypto.Chain = "BSC" + if _, er = bi.WithdrawCrypto(context.Background(), params); !errors.Is(er, errMissingRequiredParameterAddress) { + t.Errorf("Binanceus WithdrawCrypto() expecting %v, but found %v", errMissingRequiredParameterAddress, er) + } + params.Crypto.Address = "1234567" + if _, er = bi.WithdrawCrypto(context.Background(), params); !errors.Is(er, errAmountValueMustBeGreaterThan0) { + t.Errorf("Binanceus WithdrawCrypto() expecting %v, but found %v", errAmountValueMustBeGreaterThan0, er) + } + params.Amount = 1 + if _, er = bi.WithdrawCrypto(context.Background(), params); er != nil && !strings.Contains(er.Error(), "You are not authorized to execute this request.") { + t.Error("Binanceus WithdrawCrypto() error", er) + } +} + +func TestFiatWithdrawalHistory(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.FiatWithdrawalHistory(context.Background(), &FiatWithdrawalRequestParams{ + FiatCurrency: "USDT", + }) + if er != nil { + t.Errorf("%s FiatWithdrawalHistory() error %v", bi.Name, er) + } +} + +func TestDepositHistory(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.DepositHistory(context.Background(), currency.USD, 1, time.Time{}, time.Time{}, 0, 100) + if er != nil { + t.Error("Binanceus DepositHistory() error", er) + } +} +func TestFiatDepositHistory(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.FiatDepositHistory(context.Background(), &FiatWithdrawalRequestParams{}) + if er != nil { + t.Error("Binanceus FiatDepositHistory() error", er) + } +} + +// WEBSOCKET support testing +// Since both binance and Binance US has same websocket functions, +// the tests functions are also similar + +// TestWebsocketStreamKey this test mmethod handles the +// creating, updating, and deleting of user stream key or "listenKey" +// all the three methods in one test methods. +func TestWebsocketStreamKey(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + _, er := bi.GetWsAuthStreamKey(context.Background()) + if er != nil { + t.Error("Binanceus GetWsAuthStreamKey() error", er) + } + er = bi.MaintainWsAuthStreamKey(context.Background()) + if er != nil { + t.Error("Binanceus MaintainWsAuthStreamKey() error", er) + } + er = bi.CloseUserDataStream(context.Background()) + if er != nil { + t.Error("Binanceus CloseUserDataStream() error", er) + } +} + +var subscriptionRequestString = `{ + "method": "SUBSCRIBE", + "params": [ + "btcusdt@aggTrade", + "btcusdt@depth" + ], + "id": 1 + }` + +func TestWebsocketSubscriptionHandling(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + rawData := []byte(subscriptionRequestString) + err := bi.wsHandleData(rawData) + if err != nil { + t.Error("Binanceus wsHandleData() error", err) + } +} + +func TestWebsocketUnsubscriptionHandling(t *testing.T) { + pressXToJSON := []byte(`{ + "method": "UNSUBSCRIBE", + "params": [ + "btcusdt@depth" + ], + "id": 312 + }`) + err := bi.wsHandleData(pressXToJSON) + if err != nil { + t.Error(err) + } +} + +func TestGetSubscriptions(t *testing.T) { + t.Parallel() + if _, err := bi.GetSubscriptions(); err != nil { + t.Error("Binanceus GetSubscriptions() error", err) + } +} + +var ticker24hourChangeStream = `{ + "stream":"btcusdt@ticker", + "data" :{ + "e": "24hrTicker", + "E": 123456789, + "s": "BNBBTC", + "p": "0.0015", + "P": "250.00", + "w": "0.0018", + "x": "0.0009", + "c": "0.0025", + "Q": "10", + "b": "0.0024", + "B": "10", + "a": "0.0026", + "A": "100", + "o": "0.0010", + "h": "0.0025", + "l": "0.0010", + "v": "10000", + "q": "18", + "O": 0, + "C": 86400000, + "F": 0, + "L": 18150, + "n": 18151 + } +}` + +func TestWebsocketTickerUpdate(t *testing.T) { + t.Parallel() + if err := bi.wsHandleData([]byte(ticker24hourChangeStream)); err != nil { + t.Error("Binanceus wsHandleData() for Ticker 24h Change Stream", err) + } +} + +func TestWebsocketKlineUpdate(t *testing.T) { + t.Parallel() + pressXToJSON := []byte(` + { + "stream":"btcusdt@kline_1m", + "data":{ + "e": "kline", + "E": 123456789, + "s": "BNBBTC", + "k": { + "t": 123400000, + "T": 123460000, + "s": "BNBBTC", + "i": "1m", + "f": 100, + "L": 200, + "o": "0.0010", + "c": "0.0020", + "h": "0.0025", + "l": "0.0015", + "v": "1000", + "n": 100, + "x": false, + "q": "1.0000", + "V": "500", + "Q": "0.500", + "B": "123456" + } + } + }`) + if err := bi.wsHandleData(pressXToJSON); err != nil { + t.Error("Binanceus wsHandleData() btcusdt@kline_1m stream data conversion ", err) + } +} + +func TestWebsocketStreamTradeUpdate(t *testing.T) { + t.Parallel() + pressXToJSON := []byte(`{"stream":"btcusdt@trade","data":{ + "e": "trade", + "E": 123456789, + "s": "BNBBTC", + "t": 12345, + "p": "0.001", + "q": "100", + "b": 88, + "a": 50, + "T": 123456785, + "m": true, + "M": true + }}`) + if err := bi.wsHandleData(pressXToJSON); err != nil { + t.Error("Binanceus wsHandleData() error", err) + } +} + +// TestWsDepthUpdate copied from the Binance Test +func TestWebsocketOrderBookDepthDiffStream(t *testing.T) { + binanceusOrderBookLock.Lock() + defer binanceusOrderBookLock.Unlock() + bi.setupOrderbookManager() + seedLastUpdateID := int64(161) + book := OrderBook{ + Asks: []OrderbookItem{ + {Price: 6621.80000000, Quantity: 0.00198100}, + {Price: 6622.14000000, Quantity: 4.00000000}, + {Price: 6622.46000000, Quantity: 2.30000000}, + {Price: 6622.47000000, Quantity: 1.18633300}, + {Price: 6622.64000000, Quantity: 4.00000000}, + {Price: 6622.73000000, Quantity: 0.02900000}, + {Price: 6622.76000000, Quantity: 0.12557700}, + {Price: 6622.81000000, Quantity: 2.08994200}, + {Price: 6622.82000000, Quantity: 0.01500000}, + {Price: 6623.17000000, Quantity: 0.16831300}, + }, + Bids: []OrderbookItem{ + {Price: 6621.55000000, Quantity: 0.16356700}, + {Price: 6621.45000000, Quantity: 0.16352600}, + {Price: 6621.41000000, Quantity: 0.86091200}, + {Price: 6621.25000000, Quantity: 0.16914100}, + {Price: 6621.23000000, Quantity: 0.09193600}, + {Price: 6621.22000000, Quantity: 0.00755100}, + {Price: 6621.13000000, Quantity: 0.08432000}, + {Price: 6621.03000000, Quantity: 0.00172000}, + {Price: 6620.94000000, Quantity: 0.30506700}, + {Price: 6620.93000000, Quantity: 0.00200000}, + }, + LastUpdateID: seedLastUpdateID, + } + update1 := []byte(`{"stream":"btcusdt@depth","data":{ + "e": "depthUpdate", + "E": 123456788, + "s": "BTCUSDT", + "U": 157, + "u": 160, + "b": [ + ["6621.45", "0.3"] + ], + "a": [ + ["6622.46", "1.5"] + ] + }}`) + + p := currency.NewPairWithDelimiter("BTC", "USDT", "-") + if err := bi.SeedLocalCacheWithBook(p, &book); err != nil { + t.Error(err) + } + if err := bi.wsHandleData(update1); err != nil { + t.Error(err) + } + bi.obm.state[currency.BTC][currency.USDT][asset.Spot].fetchingBook = false + ob, err := bi.Websocket.Orderbook.GetOrderbook(p, asset.Spot) + if err != nil { + t.Fatal(err) + } + if exp, got := seedLastUpdateID, ob.LastUpdateID; got != exp { + t.Fatalf("Unexpected Last update id of orderbook for old update. Exp: %d, got: %d", exp, got) + } + if exp, got := 2.3, ob.Asks[2].Amount; got != exp { + t.Fatalf("Ask altered by outdated update. Exp: %f, got %f", exp, got) + } + if exp, got := 0.163526, ob.Bids[1].Amount; got != exp { + t.Fatalf("Bid altered by outdated update. Exp: %f, got %f", exp, got) + } + update2 := []byte(`{ + "stream":"btcusdt@depth","data":{ + "e": "depthUpdate", + "E": 123456789, + "s": "BTCUSDT", + "U": 161, + "u": 165, + "b": [ + ["6621.45", "0.163526"] + ], + "a": [ + ["6622.46", "2.3"], + ["6622.47", "1.9"] + ] + } + }`) + if err = bi.wsHandleData(update2); err != nil { + t.Error("Binanceus wshandlerData error", err) + } + ob, err = bi.Websocket.Orderbook.GetOrderbook(p, asset.Spot) + if err != nil { + t.Fatal("Binanceus GetOrderBook error", err) + } + if exp, got := int64(165), ob.LastUpdateID; got != exp { + t.Fatalf("Binanceus Unexpected Last update id of orderbook for new update. Exp: %d, got: %d", exp, got) + } + if exp, got := 2.3, ob.Asks[2].Amount; got != exp { + t.Fatalf("Binanceus Unexpected Ask amount. Exp: %f, got %f", exp, got) + } + if exp, got := 1.9, ob.Asks[3].Amount; got != exp { + t.Fatalf("Binanceus Unexpected Ask amount. Exp: %f, got %f", exp, got) + } + if exp, got := 0.163526, ob.Bids[1].Amount; got != exp { + t.Fatalf("Binanceus Unexpected Bid amount. Exp: %f, got %f", exp, got) + } + bi.obm.state[currency.BTC][currency.USDT][asset.Spot].lastUpdateID = 0 +} + +// TestWebsocketPartialOrderBookDepthStream copied from the Binance Test +func TestWebsocketPartialOrderBookDepthStream(t *testing.T) { + t.Parallel() + update1 := []byte(`{"stream":"btcusdt@depth5","data": + { + "lastUpdateId": 160, + "bids": [ + [ + "0.0024", + "10" + ] + ], + "asks": [ + [ + "0.0026", + "100" + ] + ] + }}`) + var err error + if err = bi.wsHandleData(update1); err != nil { + t.Error("Binanceus Partial Order Book Depth Sream error", err) + } + update2 := []byte(`{ + "stream":"btcusdt@depth10", + "data":{ + "lastUpdateId": 160, + "bids": [ + [ + "0.0024", + "10" + ] + ], + "asks": [ + [ + "0.0026", + "100" + ] + ] + } + }`) + if err = bi.wsHandleData(update2); err != nil { + t.Error("Binanceus Partial Order Book Depth Sream error", err) + } +} + +func TestWebsocketBookTicker(t *testing.T) { + t.Parallel() + var bookTickerJSON = []byte( + `{ + "stream": "btcusdt@bookTicker", + "data": { + "u":400900217, + "s":"BNBUSDT", + "b":"25.35190000", + "B":"31.21000000", + "a":"25.36520000", + "A":"40.66000000" + } + }`) + if err := bi.wsHandleData(bookTickerJSON); err != nil { + t.Error("Binanceus Book Ticker error", err) + } + var bookTickerForAllSymbols = []byte(` + { + "stream" : "!bookTicker", + "data":{ + "u":400900217, + "s":"BNBUSDT", + "b":"25.35190000", + "B":"31.21000000", + "a":"25.36520000", + "A":"40.66000000" + } + }`) + if err := bi.wsHandleData(bookTickerForAllSymbols); err != nil { + t.Error("Binanceus Web socket Book ticker for all symbols error", err) + } +} + +func TestWebsocketAggTrade(t *testing.T) { + t.Parallel() + var aggTradejson = []byte( + `{ + "stream":"btcusdt@aggTrade", + "data": { + "e": "aggTrade", + "E": 123456789, + "s": "BNBBTC", + "a": 12345, + "p": "0.001", + "q": "100", + "f": 100, + "l": 105, + "T": 123456785, + "m": true, + "M": true + } + }`) + if err := bi.wsHandleData(aggTradejson); err != nil { + t.Error("Binanceus Aggregated Trade Order Json() error", err) + } +} + +var balanceUpdateInputJSON = ` +{ + "stream":"jTfvpakT2yT0hVIo5gYWVihZhdM2PrBgJUZ5PyfZ4EVpCkx4Uoxk5timcrQc", + "data":{ + "e": "balanceUpdate", + "E": 1573200697110, + "a": "BTC", + "d": "100.00000000", + "T": 1573200697068 + } +}` + +func TestWebsocketBalanceUpdate(t *testing.T) { + t.Parallel() + thejson := []byte(balanceUpdateInputJSON) + if err := bi.wsHandleData(thejson); err != nil { + t.Error(err) + } +} + +var listStatusUserDataStreamPayload = ` +{ + "stream":"jTfvpakT2yT0hVIo5gYWVihZhdM2PrBgJUZ5PyfZ4EVpCkx4Uoxk5timcrQc", + "data":{ + "e": "listStatus", + "E": 1564035303637, + "s": "ETHBTC", + "g": 2, + "c": "OCO", + "l": "EXEC_STARTED", + "L": "EXECUTING", + "r": "NONE", + "C": "F4QN4G8DlFATFlIUQ0cjdD", + "T": 1564035303625, + "O": [ + { + "s": "ETHBTC", + "i": 17, + "c": "AJYsMjErWJesZvqlJCTUgL" + }, + { + "s": "ETHBTC", + "i": 18, + "c": "bfYPSQdLoqAJeNrOr9adzq" + } + ] + } +}` + +func TestWebsocketListStatus(t *testing.T) { + t.Parallel() + if err := bi.wsHandleData([]byte(listStatusUserDataStreamPayload)); err != nil { + t.Error(err) + } +} + +func TestExecutionTypeToOrderStatus(t *testing.T) { + type TestCases struct { + Case string + Result order.Status + } + testCases := []TestCases{ + {Case: "NEW", Result: order.New}, + {Case: "PARTIALLY_FILLED", Result: order.PartiallyFilled}, + {Case: "FILLED", Result: order.Filled}, + {Case: "CANCELED", Result: order.Cancelled}, + {Case: "PENDING_CANCEL", Result: order.PendingCancel}, + {Case: "REJECTED", Result: order.Rejected}, + {Case: "EXPIRED", Result: order.Expired}, + {Case: "LOL", Result: order.UnknownStatus}, + } + for i := range testCases { + result, _ := stringToOrderStatus(testCases[i].Case) + if result != testCases[i].Result { + t.Errorf("Binanceus Exepcted: %v, received: %v", testCases[i].Result, result) + } + } +} + +var websocketDepthUpdate = []byte( + `{ + "e": "depthUpdate", + "E": 123456789, + "s": "BNBBTC", + "U": 157, + "u": 160, + "b": [ + [ + "0.0024", + "10" + ] + ], + "a": [ + [ + "0.0026", + "100" + ] + ] + } + `) + +func TestProcessUpdate(t *testing.T) { + t.Parallel() + binanceusOrderBookLock.Lock() + defer binanceusOrderBookLock.Unlock() + p := currency.NewPair(currency.BTC, currency.USDT) + var depth WebsocketDepthStream + err := json.Unmarshal(websocketDepthUpdate, &depth) + if err != nil { + t.Fatal(err) + } + err = bi.obm.stageWsUpdate(&depth, p, asset.Spot) + if err != nil { + t.Fatal(err) + } + err = bi.obm.fetchBookViaREST(p) + if err != nil { + t.Fatal(err) + } + err = bi.obm.cleanup(p) + if err != nil { + t.Fatal(err) + } + bi.obm.state[currency.BTC][currency.USDT][asset.Spot].lastUpdateID = 0 +} + +func TestWebsocketOrderExecutionReport(t *testing.T) { + payload := []byte(`{"stream":"jTfvpakT2yT0hVIo5gYWVihZhdM2PrBgJUZ5PyfZ4EVpCkx4Uoxk5timcrQc","data":{"e":"executionReport","E":1616627567900,"s":"BTCUSDT","c":"c4wyKsIhoAaittTYlIVLqk","S":"BUY","o":"LIMIT","f":"GTC","q":"0.00028400","p":"52789.10000000","P":"0.00000000","F":"0.00000000","g":-1,"C":"","x":"NEW","X":"NEW","r":"NONE","i":5340845958,"l":"0.00000000","z":"0.00000000","L":"0.00000000","n":"0","N":"BTC","T":1616627567900,"t":-1,"I":11388173160,"w":true,"m":false,"M":false,"O":1616627567900,"Z":"0.00000000","Y":"0.00000000","Q":"0.00000000"}}`) + expRes := order.Detail{ + Price: 52789.1, + Amount: 0.00028400, + RemainingAmount: 0.00028400, + CostAsset: currency.USDT, + FeeAsset: currency.BTC, + Exchange: "Binanceus", + OrderID: "5340845958", + ClientOrderID: "c4wyKsIhoAaittTYlIVLqk", + Type: order.Limit, + Side: order.Buy, + Status: order.New, + AssetType: asset.Spot, + Date: time.UnixMilli(1616627567900), + LastUpdated: time.UnixMilli(1616627567900), + Pair: currency.NewPair(currency.BTC, currency.USDT), + } + for len(bi.Websocket.DataHandler) > 0 { + <-bi.Websocket.DataHandler + } + err := bi.wsHandleData(payload) + if err != nil { + t.Fatal(err) + } + res := <-bi.Websocket.DataHandler + switch r := res.(type) { + case *order.Detail: + if !reflects.DeepEqual(expRes, *r) { + t.Errorf("Binanceus Results do not match:\nexpected: %v\nreceived: %v", expRes, *r) + } + default: + t.Fatalf("Binanceus expected type order.Detail, found %T", res) + } + payload = []byte(`{"stream":"jTfvpakT2yT0hVIo5gYWVihZhdM2PrBgJUZ5PyfZ4EVpCkx4Uoxk5timcrQc","data":{"e":"executionReport","E":1616633041556,"s":"BTCUSDT","c":"YeULctvPAnHj5HXCQo9Mob","S":"BUY","o":"LIMIT","f":"GTC","q":"0.00028600","p":"52436.85000000","P":"0.00000000","F":"0.00000000","g":-1,"C":"","x":"TRADE","X":"FILLED","r":"NONE","i":5341783271,"l":"0.00028600","z":"0.00028600","L":"52436.85000000","n":"0.00000029","N":"BTC","T":1616633041555,"t":726946523,"I":11390206312,"w":false,"m":false,"M":true,"O":1616633041555,"Z":"14.99693910","Y":"14.99693910","Q":"0.00000000"}}`) + err = bi.wsHandleData(payload) + if err != nil { + t.Fatal("Binanceus OrderExecutionReport json conversion error", err) + } +} + +func TestWebsocketOutboundAccountPosition(t *testing.T) { + t.Parallel() + payload := []byte(`{"stream":"jTfvpakT2yT0hVIo5gYWVihZhdM2PrBgJUZ5PyfZ4EVpCkx4Uoxk5timcrQc","data":{"e":"outboundAccountPosition","E":1616628815745,"u":1616628815745,"B":[{"a":"BTC","f":"0.00225109","l":"0.00123000"},{"a":"BNB","f":"0.00000000","l":"0.00000000"},{"a":"USDT","f":"54.43390661","l":"0.00000000"}]}}`) + if err := bi.wsHandleData(payload); err != nil { + t.Fatal("Binanceus testing \"outboundAccountPosition\" data conversion error", err) + } +} + +func TestGetAvailableTransferChains(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.SkipNow() + } + if _, er := bi.GetAvailableTransferChains(context.Background(), currency.BTC); er != nil { + t.Error("Binanceus GetAvailableTransferChains() error", er) + } +} + +func TestQuickEnableCryptoWithdrawal(t *testing.T) { + if !areTestAPIKeysSet() { + t.SkipNow() + } + if er := bi.QuickEnableCryptoWithdrawal(context.Background()); er != nil && !strings.Contains(er.Error(), "unexpected end of JSON input") { + t.Errorf("Binanceus QuickEnableCryptoWithdrawal() expecting %s, but found %v", "unexpected end of JSON input", er) + } +} +func TestQuickDisableCryptoWithdrawal(t *testing.T) { + if !areTestAPIKeysSet() { + t.SkipNow() + } + if er := bi.QuickDisableCryptoWithdrawal(context.Background()); er != nil && !strings.Contains(er.Error(), "unexpected end of JSON input") { + t.Errorf("Binanceus QuickDisableCryptoWithdrawal() expecting %s, but found %v", "unexpected end of JSON input", er) + } +} + +func TestGetUsersSpotAssetSnapshot(t *testing.T) { + if !areTestAPIKeysSet() { + t.SkipNow() + } + if _, er := bi.GetUsersSpotAssetSnapshot(context.Background(), time.Time{}, time.Time{}, 10, 6); er != nil { + t.Error("Binanceus GetUsersSpotAssetSnapshot() error", er) + } +} diff --git a/exchanges/binanceus/binanceus_types.go b/exchanges/binanceus/binanceus_types.go new file mode 100644 index 00000000..904ef246 --- /dev/null +++ b/exchanges/binanceus/binanceus_types.go @@ -0,0 +1,1180 @@ +package binanceus + +import ( + "strconv" + "sync" + "time" + + "github.com/shopspring/decimal" + "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/trade" +) + +var ( + // BinanceRequestParamsOrderLimit Limit order + BinanceRequestParamsOrderLimit = RequestParamsOrderType("LIMIT") + // BinanceRequestParamsOrderMarket Market order + BinanceRequestParamsOrderMarket = RequestParamsOrderType("MARKET") + // BinanceRequestParamsOrderStopLoss STOP_LOSS + BinanceRequestParamsOrderStopLoss = RequestParamsOrderType("STOP_LOSS") + // BinanceRequestParamsOrderStopLossLimit STOP_LOSS_LIMIT + BinanceRequestParamsOrderStopLossLimit = RequestParamsOrderType("STOP_LOSS_LIMIT") + // BinanceRequestParamsOrderTakeProfit TAKE_PROFIT + BinanceRequestParamsOrderTakeProfit = RequestParamsOrderType("TAKE_PROFIT") + // BinanceRequestParamsOrderTakeProfitLimit TAKE_PROFIT_LIMIT + BinanceRequestParamsOrderTakeProfitLimit = RequestParamsOrderType("TAKE_PROFIT_LIMIT") + // BinanceRequestParamsOrderLimitMarker LIMIT_MAKER + BinanceRequestParamsOrderLimitMarker = RequestParamsOrderType("LIMIT_MAKER") +) + +const wsRateLimitMilliseconds = 300 + +// crypto withdrawals status codes description +const ( + EmailSent = iota + Cancelled + AwaitingApproval + Rejected + Processing + Failure + Completed +) + +// ExchangeInfo holds the full exchange information type +type ExchangeInfo struct { + Code int64 `json:"code"` + Msg string `json:"msg"` + Timezone string `json:"timezone"` + ServerTime time.Time `json:"serverTime"` + RateLimits []struct { + RateLimitType string `json:"rateLimitType"` + Interval string `json:"interval"` + Limit int64 `json:"limit"` + } `json:"rateLimits"` + ExchangeFilters interface{} `json:"exchangeFilters"` + Symbols []struct { + Symbol string `json:"symbol"` + Status string `json:"status"` + BaseAsset string `json:"baseAsset"` + BaseAssetPrecision int64 `json:"baseAssetPrecision"` + QuoteAsset string `json:"quoteAsset"` + QuotePrecision int64 `json:"quotePrecision"` + OrderTypes []string `json:"orderTypes"` + IcebergAllowed bool `json:"icebergAllowed"` + OCOAllowed bool `json:"ocoAllowed"` + QuoteOrderQtyMarketAllowed bool `json:"quoteOrderQtyMarketAllowed"` + IsSpotTradingAllowed bool `json:"isSpotTradingAllowed"` + IsMarginTradingAllowed bool `json:"isMarginTradingAllowed"` + Filters []struct { + FilterType string `json:"filterType"` + MinPrice float64 `json:"minPrice,string"` + MaxPrice float64 `json:"maxPrice,string"` + TickSize float64 `json:"tickSize,string"` + MultiplierUp float64 `json:"multiplierUp,string"` + MultiplierDown float64 `json:"multiplierDown,string"` + AvgPriceMinutes int64 `json:"avgPriceMins"` + MinQty float64 `json:"minQty,string"` + MaxQty float64 `json:"maxQty,string"` + StepSize float64 `json:"stepSize,string"` + MinNotional float64 `json:"minNotional,string"` + ApplyToMarket bool `json:"applyToMarket"` + Limit int64 `json:"limit"` + MaxNumAlgoOrders int64 `json:"maxNumAlgoOrders"` + MaxNumIcebergOrders int64 `json:"maxNumIcebergOrders"` + MaxNumOrders int64 `json:"maxNumOrders"` + } `json:"filters"` + Permissions []string `json:"permissions"` + } `json:"symbols"` +} + +// RecentTradeRequestParams represents Klines request data. +type RecentTradeRequestParams struct { + Symbol currency.Pair `json:"symbol"` // Required field. example LTCBTC, BTCUSDT + Limit int64 `json:"limit"` // Default 500; max 1000. +} + +// RecentTrade holds recent trade data +type RecentTrade struct { + ID int64 `json:"id"` + Price float64 `json:"price,string"` + Quantity float64 `json:"qty,string"` + Time time.Time `json:"time"` + IsBuyerMaker bool `json:"isBuyerMaker"` + IsBestMatch bool `json:"isBestMatch"` +} + +// HistoricalTradeParams represents historical trades request params. +type HistoricalTradeParams struct { + Symbol string `json:"symbol"` // Required field. example LTCBTC, BTCUSDT + Limit int64 `json:"limit"` // Default 500; max 1000. + FromID uint64 `json:"fromId"` // Optional Field. Specifies the trade ID to fetch most recent trade histories from +} + +// HistoricalTrade holds recent trade data +type HistoricalTrade struct { + ID int64 `json:"id"` + Price float64 `json:"price,string"` + Quantity float64 `json:"qty,string"` + QuoteQuantity float64 `json:"quoteQty,string"` + Time time.Time `json:"time"` + IsBuyerMaker bool `json:"isBuyerMaker"` + IsBestMatch bool `json:"isBestMatch"` +} + +// AggregatedTradeRequestParams holds request params +type AggregatedTradeRequestParams struct { + Symbol currency.Pair // Required field; example LTCBTC, BTCUSDT + // The first trade to retrieve + FromID int64 + // The API seems to accept (start and end time) or FromID and no other combinations + StartTime uint64 + EndTime uint64 + // Default 500; max 1000. + Limit int +} + +// AggregatedTrade holds aggregated trade information +type AggregatedTrade struct { + ATradeID int64 `json:"a"` + Price float64 `json:"p,string"` + Quantity float64 `json:"q,string"` + FirstTradeID int64 `json:"f"` + LastTradeID int64 `json:"l"` + TimeStamp time.Time `json:"T"` + Maker bool `json:"m"` + BestMatchPrice bool `json:"M"` +} + +// toTradeData this method converts the AggregatedTrade data into an instance of trade.Data +func (a *AggregatedTrade) toTradeData(p currency.Pair, exchange string, aType asset.Item) *trade.Data { + return &trade.Data{ + CurrencyPair: p, + TID: strconv.FormatInt(a.ATradeID, 10), + Amount: a.Quantity, + Exchange: exchange, + Price: a.Price, + Timestamp: a.TimeStamp, + AssetType: aType, + Side: order.AnySide, + } +} + +// OrderBookDataRequestParams represents Klines request data. +type OrderBookDataRequestParams struct { + Symbol currency.Pair `json:"symbol"` // Required field; example LTCBTC,BTCUSDT + Limit int64 `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 { + LastUpdateID int64 `json:"lastUpdateId"` + Bids [][2]string `json:"bids"` + Asks [][2]string `json:"asks"` +} + +// OrderBook actual structured data that can be used for orderbook +type OrderBook struct { + Symbol string + LastUpdateID int64 + Code int + Msg string + Bids []OrderbookItem + Asks []OrderbookItem +} + +// KlinesRequestParams represents Klines request data. +type KlinesRequestParams struct { + Symbol currency.Pair // Required field; example LTCBTC, BTCUSDT + Interval string // Time interval period + Limit int64 // Default 500; max 500. + StartTime time.Time + EndTime time.Time +} + +// CandleStick holds kline data +type CandleStick struct { + OpenTime time.Time + Open float64 + High float64 + Low float64 + Close float64 + Volume float64 + CloseTime time.Time + QuoteAssetVolume float64 + TradeCount float64 + TakerBuyAssetVolume float64 + TakerBuyQuoteAssetVolume float64 +} + +// SymbolPrice represents a symbol and it's price. +type SymbolPrice struct { + Symbol string `json:"symbol"` + Price float64 `json:"price,string"` +} + +// SymbolPrices lis tof Symbol Price +type SymbolPrices []SymbolPrice + +// AveragePrice holds current average symbol price +type AveragePrice struct { + Mins int64 `json:"mins"` + Price float64 `json:"price,string"` +} + +// BestPrice holds best price data +type BestPrice struct { + Symbol string `json:"symbol"` + BidPrice float64 `json:"bidPrice,string"` + BidQty float64 `json:"bidQty,string"` + AskPrice float64 `json:"askPrice,string"` + AskQty float64 `json:"askQty,string"` +} + +// PriceChangeStats contains statistics for the last 24 hours trade +type PriceChangeStats struct { + Symbol string `json:"symbol"` + PriceChange float64 `json:"priceChange,string"` + PriceChangePercent float64 `json:"priceChangePercent,string"` + WeightedAvgPrice float64 `json:"weightedAvgPrice,string"` + PrevClosePrice float64 `json:"prevClosePrice,string"` + LastPrice float64 `json:"lastPrice,string"` + LastQty float64 `json:"lastQty,string"` + BidPrice float64 `json:"bidPrice,string"` + AskPrice float64 `json:"askPrice,string"` + OpenPrice float64 `json:"openPrice,string"` + HighPrice float64 `json:"highPrice,string"` + LowPrice float64 `json:"lowPrice,string"` + Volume float64 `json:"volume,string"` + QuoteVolume float64 `json:"quoteVolume,string"` + OpenTime time.Time `json:"openTime"` + CloseTime time.Time `json:"closeTime"` + FirstID int64 `json:"firstId"` + LastID int64 `json:"lastId"` + Count int64 `json:"count"` +} + +// Response holds basic binance api response data +type Response struct { + Code int64 `json:"code"` + Msg string `json:"msg"` +} + +// Account holds the account data +type Account struct { + MakerCommission int64 `json:"makerCommission"` + TakerCommission int64 `json:"takerCommission"` + BuyerCommission int64 `json:"buyerCommission"` + SellerCommission int64 `json:"sellerCommission"` + CanTrade bool `json:"canTrade"` + CanWithdraw bool `json:"canWithdraw"` + CanDeposit bool `json:"canDeposit"` + UpdateTime time.Time `json:"updateTime"` + AccountType string `json:"accountType"` + Balances []Balance `json:"balances"` + Permissions []string `json:"permissions"` +} + +// Balance holds query order data +type Balance struct { + Asset string `json:"asset"` + Free decimal.Decimal `json:"free"` + Locked decimal.Decimal `json:"locked"` +} + +// AccountStatusResponse holds information related to the +// User Account status information request +type AccountStatusResponse struct { + Msg string `json:"msg"` + Success bool `json:"success"` + Objs []string `json:"objs,omitempty"` +} + +// TradeStatus represents trade status and holds list of trade status indicator Item instances. +type TradeStatus struct { + IsLocked bool `json:"isLocked"` + PlannedRecoverTime uint `json:"plannedRecoverTime"` + TriggerCondition map[string]uint `json:"triggerCondition"` + Indicators map[string]TradingStatusIndicatorItem `json:"indicators"` + UpdateTime time.Time `json:"updateTime"` +} + +// TradingStatusIndicatorItem represents Trade Status Indication +type TradingStatusIndicatorItem struct { + IndicatorSymbol string `json:"i"` + CountOfAllOrders float32 `json:"c"` + CurrentValue float32 `json:"v"` + TriggerValue float32 `json:"t"` +} + +// TradeFee represents the symbol and corresponding maker and taker trading fee value. +type TradeFee struct { + Symbol string `json:"symbol"` + Maker float64 `json:"maker"` + Taker float64 `json:"taker"` +} + +// TradeFeeList list of trading fee for different trade symbols. +type TradeFeeList struct { + TradeFee []TradeFee `json:"tradeFee"` + Success bool `json:"success,omitempty"` +} + +// AssetHistory holds the asset type and translation info +type AssetHistory struct { + Amount float64 `json:"amount,string"` // Amount + Asset string `json:"asset"` // Asset Type eg. BHFT + DivTime uint64 `json:"divTime"` // DivTime + EnInfo string `json:"enInfo"` // + TranID uint64 `json:"tranId"` // Transaction ID +} + +// AssetDistributionHistories this endpoint to query asset distribution records, +// including for staking, referrals and airdrops etc. +type AssetDistributionHistories struct { + Rows []AssetHistory `json:"rows"` + Total uint `json:"total"` +} + +// SubAccount holds a single sub account instance in a Binance US account. +// including the email and related information related to it. +type SubAccount struct { + Email string `json:"email"` + Status string `json:"status"` + Activated bool `json:"activated"` + Mobile string `json:"mobile"` + GAuth bool `json:"gAuth"` + CreateTime time.Time `json:"createTime"` +} + +// TransferHistory a single asset transfer history between Sub accounts +type TransferHistory struct { + From string `json:"from"` + To string `json:"to"` + Asset string `json:"asset"` + Qty uint `json:"qty,string"` + TimeStamp time.Time `json:"time"` +} + +// SubAccountTransferRequestParams contains argument variables holder used to transfer an +// asset from one account to another subaccount +type SubAccountTransferRequestParams struct { + FromEmail string // Mandatory + ToEmail string // Mandatory + Asset string // Mandatory + Amount float64 // Mandatory + RecvWindow uint64 +} + +// SubAccountTransferResponse represents a suabccount transfer history +// having the transaction id which is to be returned due to the transfer +type SubAccountTransferResponse struct { + Success bool `json:"success"` + TxnID uint64 `json:"txnId,string"` +} + +// AssetInfo holds asset information +type AssetInfo struct { + Asset string `json:"asset"` + Free uint64 `json:"free"` + Locked uint64 `json:"locked"` +} + +// SubAccountAssets holds all the balance and email of a subaccount +type SubAccountAssets struct { + Balances []AssetInfo `json:"balances"` + Success bool `json:"success"` + SubaccountEmail string `json:"email,omitempty"` +} + +// OrderRateLimit holds rate limits type, interval, and related information of trade orders. +type OrderRateLimit struct { + RateLimitType string `json:"rateLimitType"` + Interval string `json:"interval"` + IntervalNum uint `json:"intervalNum"` + Limit uint `json:"limit"` + Count uint `json:"count"` +} + +// RequestParamsOrderType trade order type +type RequestParamsOrderType string + +// RequestParamsTimeForceType Time in force +type RequestParamsTimeForceType string + +var ( + // BinanceRequestParamsTimeGTC GTC + BinanceRequestParamsTimeGTC = RequestParamsTimeForceType("GTC") + + // BinanceRequestParamsTimeIOC IOC + BinanceRequestParamsTimeIOC = RequestParamsTimeForceType("IOC") + + // BinanceRequestParamsTimeFOK FOK + BinanceRequestParamsTimeFOK = RequestParamsTimeForceType("FOK") +) + +// NewOrderRequest request type +type NewOrderRequest struct { + Symbol currency.Pair + Side string + TradeType RequestParamsOrderType + TimeInForce RequestParamsTimeForceType + Quantity float64 + QuoteOrderQty float64 + Price float64 + NewClientOrderID string + StopPrice float64 // Used with STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, and TAKE_PROFIT_LIMIT orders. + IcebergQty float64 // Used with LIMIT, STOP_LOSS_LIMIT, and TAKE_PROFIT_LIMIT to create an iceberg order. + NewOrderRespType string +} + +// NewOrderResponse represents trade order's detailed information. +type NewOrderResponse struct { + Symbol string `json:"symbol"` + OrderID int64 `json:"orderId"` + OrderListID int8 `json:"orderListId"` + ClientOrderID string `json:"clientOrderId"` + TransactionTime time.Time `json:"transactTime"` + Price float64 `json:"price,string"` + OrigQty float64 `json:"origQty,string"` + ExecutedQty float64 `json:"executedQty,string"` + // The cumulative amount of the quote that has been spent (with a BUY order) or received (with a SELL order). + CumulativeQuoteQty float64 `json:"cummulativeQuoteQty,string"` + Status string `json:"status"` + TimeInForce string `json:"timeInForce"` + Type string `json:"type"` + Side string `json:"side"` + // -- + Code int64 `json:"code"` + Msg string `json:"msg"` + // -- + Fills []struct { + Price float64 `json:"price,string"` + Qty float64 `json:"qty,string"` + Commission float64 `json:"commission,string"` + CommissionAsset string `json:"commissionAsset"` + } `json:"fills"` +} + +// CommonOrder instance holds the order information common to both +// for Order and OrderReportItem +type CommonOrder struct { + Symbol string `json:"symbol"` + OrderID uint64 `json:"orderId"` + OrderListID int8 `json:"orderListId"` + ClientOrderID string `json:"clientOrderId"` + + Price float64 `json:"price,string"` + OrigQty float64 `json:"origQty,string"` + ExecutedQty float64 `json:"executedQty,string"` + CummulativeQuoteQty float64 `json:"cummulativeQuoteQty,string"` + Status string `json:"status"` + TimeInForce string `json:"timeInForce"` + Type string `json:"type"` + Side string `json:"side"` + StopPrice float64 `json:"stopPrice,string"` +} + +// Order struct represents an ordinary order response. +type Order struct { + CommonOrder + IcebergQty float64 `json:"icebergQty,string"` + Time time.Time `json:"time"` + UpdateTime time.Time `json:"updateTime"` + IsWorking bool `json:"isWorking"` + OrigQuoteOrderQty float64 `json:"origQuoteOrderQty,string"` +} + +// OCOOrderReportItem this is used by the OCO order creating response +type OCOOrderReportItem struct { + CommonOrder + TransactionTime time.Time `json:"transactionTime"` +} + +// OrderRequestParams this struct will be used to get a +// order and its related information +type OrderRequestParams struct { + Symbol string `json:"symbol"` // REQUIRED + OrderID uint64 `json:"orderId"` + OrigClientOrderID string `json:"origClientOrderId"` + recvWindow uint +} + +// CancelOrderRequestParams this struct will be used as a parameter for +// cancel order method. +type CancelOrderRequestParams struct { + Symbol currency.Pair + OrderID string + ClientSuppliedOrderID string + NewClientOrderID string + RecvWindow uint +} + +// GetTradesParams request param to get the trade history +type GetTradesParams struct { + Symbol string `json:"symbol"` + OrderID uint64 `json:"orderId"` + StartTime *time.Time `json:"startTime"` + EndTime *time.Time `json:"endTime"` + FromID uint64 `json:"fromId"` + Limit uint `json:"limit"` + RecvWindow uint `json:"recvWindow"` +} + +// Trade this struct represents a trade response. +type Trade struct { + Symbol string `json:"symbol"` + ID uint64 `json:"id"` + OrderID uint64 `json:"orderId"` + OrderListID int64 `json:"orderListId"` + Price float64 `json:"price"` + Qty float64 `json:"qty"` + QuoteQty float64 `json:"quoteQty"` + Commission float64 `json:"commission"` + CommissionAsset float64 `json:"commissionAsset"` + Time time.Time `json:"time"` + IsBuyer bool `json:"isBuyer"` + IsMaker bool `json:"isMaker"` + IsBestMatch bool `json:"isBestMatch"` +} + +// OCOOrderInputParams One-cancel-the-other order creation input Parameter +type OCOOrderInputParams struct { + Symbol string `json:"symbol"` // Required + StopPrice float64 `json:"stopPrice"` // Required + Side string `json:"side"` // Required + Quantity float64 `json:"quantity"` // Required + Price float64 `json:"price"` // Required + ListClientOrderID string `json:"listClientOrderId"` + LimitClientOrderID string `json:"limitClientOrderId"` + LimitIcebergQty float64 `json:"limitIcebergQty"` + StopClientOrderID string `json:"stopClientOrderId"` + StopLimitPrice float64 `json:"stopLimitPrice"` + StopIcebergQty float64 `json:"stopIcebergQty"` + StopLimitTimeInForce string `json:"stopLimitTimeInForce"` + NewOrderRespType string `json:"newOrderRespType"` + RecvWindow uint64 `json:"recvWindow"` +} + +// GetOCOOrderRequestParams a parameter model to query specific list of OCO orders using their id +type GetOCOOrderRequestParams struct { + OrderListID string + OrigClientOrderID string +} + +// OrderShortResponse holds symbol Identification information of trade orders. +type OrderShortResponse struct { + Symbol string `json:"symbol"` + OrderID uint64 `json:"orderId"` + ClientOrderID string `json:"clientOrderId"` +} + +// OCOOrderResponse this model is to be used to fetch the response of create new OCO order response +type OCOOrderResponse struct { + OrderListID int64 `json:"orderListId"` + ContingencyType string `json:"contingencyType"` + ListStatusType string `json:"listStatusType"` + ListOrderStatus string `json:"listOrderStatus"` + ListClientOrderID string `json:"listClientOrderId"` + TransactionTime time.Time `json:"transactionTime"` + Symbol string `json:"symbol"` + Orders []OrderShortResponse `json:"orders"` +} + +// OCOFullOrderResponse holds detailed OCO order information with the corresponding transaction time +type OCOFullOrderResponse struct { + *OCOOrderResponse + OrderReports []OCOOrderReportItem `json:"orderReports"` +} + +// OCOOrdersRequestParams a parameter model to query from list of OCO orders. +type OCOOrdersRequestParams struct { + FromID uint64 + StartTime time.Time + EndTime time.Time + Limit uint + RecvWindow uint +} + +// OCOOrdersDeleteRequestParams holds the params to delete a new order +type OCOOrdersDeleteRequestParams struct { + Symbol string + OrderListID uint64 + ListClientOrderID string + NewClientOrderID string + RecvWindow uint +} + +// OTC endpoints + +// CoinPairInfo holds supported coin pair for conversion with its detailed information +type CoinPairInfo struct { + FromCoin string `json:"fromCoin"` + ToCoin string `json:"toCoin"` + FromCoinMinAmount float64 `json:"fromCoinMinAmount,string"` + FromCoinMaxAmount float64 `json:"fromCoinMaxAmount,string"` + ToCoinMinAmount float64 `json:"toCoinMinAmount,string"` + ToCoinMaxAmount float64 `json:"toCoinMaxAmount,string"` +} + +// RequestQuoteParams a parameter model to query quote information +type RequestQuoteParams struct { + FromCoin string `json:"fromCoin"` + ToCoin string `json:"toCoin"` + RequestCoin string `json:"requestCoin"` + RequestAmount float64 `json:"requestAmount"` +} + +// Quote holds quote information for from-to-coin pair +type Quote struct { + Symbol string `json:"symbol"` + Ratio float64 `json:"ratio,string"` + InverseRatio float64 `json:"inverseRatio,string"` + ValidTimestamp time.Time `json:"validTimestamp"` + ToAmount float64 `json:"toAmount,string"` + FromAmount float64 `json:"fromAmount,string"` +} + +// OTCTradeOrderResponse holds OTC(over-the-counter) order identification and status information +type OTCTradeOrderResponse struct { + OrderID uint64 `json:"orderId,string"` + OrderStatus string `json:"orderStatus"` + CreateTime time.Time `json:"createTime"` +} + +// OTCTradeOrder holds OTC(over-the-counter) orders response +type OTCTradeOrder struct { + QuoteID string `json:"quoteId"` + OrderID uint64 `json:"orderId,string"` + OrderStatus string `json:"orderStatus"` + FromCoin string `json:"fromCoin"` + FromAmount float64 `json:"fromAmount"` + ToCoin string `json:"toCoin"` + ToAmount float64 `json:"toAmount"` + Ratio float64 `json:"ratio"` + InverseRatio float64 `json:"inverseRatio"` + CreateTime time.Time `json:"createTime"` +} + +// OTCTradeOrderRequestParams request param for Over-the-Counter trade order params. +type OTCTradeOrderRequestParams struct { + OrderID string + FromCoin string + ToCoin string + StartTime time.Time + EndTime time.Time + Limit int8 +} + +// Wallet Endpoints + +// AssetWalletDetail represents the wallet asset information. +type AssetWalletDetail struct { + Coin string `json:"coin"` + DepositAllEnable bool `json:"depositAllEnable"` + WithdrawAllEnable bool `json:"withdrawAllEnable"` + Name string `json:"name"` + Free string `json:"free"` + Locked string `json:"locked"` + Freeze string `json:"freeze"` + Withdrawing string `json:"withdrawing"` + Ipoing string `json:"ipoing"` + Ipoable string `json:"ipoable"` + Storage string `json:"storage"` + IsLegalMoney bool `json:"isLegalMoney"` + Trading bool `json:"trading"` + NetworkList []struct { + Network string `json:"network"` + Coin string `json:"coin"` + WithdrawIntegerMultiple string `json:"withdrawIntegerMultiple"` + IsDefault bool `json:"isDefault"` + DepositEnable bool `json:"depositEnable"` + WithdrawEnable bool `json:"withdrawEnable"` + DepositDesc string `json:"depositDesc"` + WithdrawDesc string `json:"withdrawDesc"` + Name string `json:"name"` + ResetAddressStatus bool `json:"resetAddressStatus"` + WithdrawFee float64 `json:"withdrawFee,string"` + WithdrawMin float64 `json:"withdrawMin,string"` + WithdrawMax float64 `json:"withdrawMax,string"` + AddressRegex string `json:"addressRegex,omitempty"` + MemoRegex string `json:"memoRegex,omitempty"` + MinConfirm int64 `json:"minConfirm,omitempty"` + UnLockConfirm int64 `json:"unLockConfirm,omitempty"` + } `json:"networkList"` +} + +// AssetWalletList list of asset wallet details +type AssetWalletList []AssetWalletDetail + +// WithdrawalRequestParam represents the params for the +// input parameters of Withdraw Crypto +type WithdrawalRequestParam struct { + Coin string `json:"coin"` + Network string `json:"network"` + WithdrawOrderID string `json:"withdrawOrderId"` // Client ID for withdraw + Address string `json:"address"` + AddressTag string `json:"addressTag"` + Amount float64 `json:"amount"` + RecvWindow uint64 `json:"recvWindow"` +} + +// WithdrawalResponse holds the transaction id for a withdrawal action. +type WithdrawalResponse struct { + ID string `json:"id"` +} + +// WithdrawStatusResponse defines a withdrawal status response +type WithdrawStatusResponse struct { + ID string `json:"id"` + Amount float64 `json:"amount,string"` + TransactionFee float64 `json:"transactionFee,string"` + Coin string `json:"coin"` + Status int64 `json:"status"` + Address string `json:"address"` + ApplyTime string `json:"applyTime"` + Network string `json:"network"` + TransferType int64 `json:"transferType"` +} + +// FiatAssetRecord asset information for fiat. +type FiatAssetRecord struct { + OrderID string `json:"orderId"` + PaymentAccount string `json:"paymentAccount"` + PaymentChannel string `json:"paymentChannel"` + PaymentMethod string `json:"paymentMethod"` + OrderStatus string `json:"orderStatus"` + Amount string `json:"amount"` + TransactionFee string `json:"transactionFee"` + PlatformFee string `json:"platformFee"` +} + +// FiatAssetsHistory holds list of available fiat asset records. +type FiatAssetsHistory struct { + AssetLogRecordList []FiatAssetRecord `json:"assetLogRecordList"` +} + +// WithdrawFiatRequestParams represents the fiat withdrawal request params. +type WithdrawFiatRequestParams struct { + PaymentChannel string + PaymentMethod string + PaymentAccount string + FiatCurrency string + Amount float64 + RecvWindow uint64 +} + +// FiatWithdrawalRequestParams to fetch your fiat (USD) withdrawal history. +type FiatWithdrawalRequestParams struct { + FiatCurrency string + OrderID string + Offset int64 + PaymentChannel string + PaymentMethod string + StartTime time.Time + EndTime time.Time +} + +// DepositAddress stores the deposit address info +type DepositAddress struct { + Address string `json:"address"` + Coin string `json:"coin"` + Tag string `json:"tag"` + URL string `json:"url"` +} + +// DepositHistory stores deposit history info. +type DepositHistory struct { + Amount string `json:"amount"` + Coin string `json:"coin"` + Network string `json:"network"` + Status int64 `json:"status"` + Address string `json:"address"` + AddressTag string `json:"addressTag"` + TxID string `json:"txId"` + InsertTime int64 `json:"insertTime"` + TransferType int64 `json:"transferType"` + ConfirmTimes string `json:"confirmTimes"` +} + +// UserAccountStream represents the response for getting the listen key for the websocket +type UserAccountStream struct { + ListenKey string `json:"listenKey"` +} + +// WebsocketPayload defines the payload through the websocket connection +type WebsocketPayload struct { + Method string `json:"method"` + Params []interface{} `json:"params"` + ID int64 `json:"id"` +} + +// orderbookManager defines a way of managing and maintaining synchronisation +// across connections and assets. +type orderbookManager struct { + state map[currency.Code]map[currency.Code]map[asset.Item]*update + sync.Mutex + + jobs chan job +} + +// job defines a synchronisation job that tells a go routine to fetch an +// orderbook via the REST protocol +type job struct { + Pair currency.Pair +} + +// update holds websocket depth stream response data and update information +type update struct { + buffer chan *WebsocketDepthStream + fetchingBook bool + initialSync bool + needsFetchingBook bool + lastUpdateID int64 +} + +// WebsocketDepthStream is the difference for the update depth stream +type WebsocketDepthStream struct { + Event string `json:"e"` + Timestamp time.Time `json:"E"` + Pair string `json:"s"` + FirstUpdateID int64 `json:"U"` + LastUpdateID int64 `json:"u"` + UpdateBids [][2]string `json:"b"` + UpdateAsks [][2]string `json:"a"` +} + +// WebsocketDepthDiffStream websocket response of depth diff stream +type WebsocketDepthDiffStream struct { + LastUpdateID int64 `json:"lastUpdateId"` + Bids [][2]string `json:"bids"` + Asks [][2]string `json:"asks"` +} + +// WsAccountInfoData defines websocket account info data +type WsAccountInfoData struct { + CanDeposit bool `json:"D"` + CanTrade bool `json:"T"` + CanWithdraw bool `json:"W"` + EventTime time.Time `json:"E"` + LastUpdated time.Time `json:"u"` + BuyerCommission float64 `json:"b"` + MakerCommission float64 `json:"m"` + SellerCommission float64 `json:"s"` + TakerCommission float64 `json:"t"` + EventType string `json:"e"` + Currencies []struct { + Asset string `json:"a"` + Available float64 `json:"f,string"` + Locked float64 `json:"l,string"` + } `json:"B"` +} + +// wsAccountPosition websocket response of account position. +type wsAccountPosition struct { + Stream string `json:"stream"` + Data WsAccountPositionData `json:"data"` +} + +// WsAccountPositionData defines websocket account position data +type WsAccountPositionData struct { + Currencies []struct { + Asset string `json:"a"` + Available float64 `json:"f,string"` + Locked float64 `json:"l,string"` + } `json:"B"` + EventTime time.Time `json:"E"` + LastUpdated time.Time `json:"u"` + EventType string `json:"e"` +} + +// wsBalanceUpdate represents the websocket response of update balance. +type wsBalanceUpdate struct { + Stream string `json:"stream"` + Data WsBalanceUpdateData `json:"data"` +} + +// WsBalanceUpdateData defines websocket account balance data. +type WsBalanceUpdateData struct { + EventTime time.Time `json:"E"` + ClearTime time.Time `json:"T"` + BalanceDelta float64 `json:"d,string"` + Asset string `json:"a"` + EventType string `json:"e"` +} + +type wsOrderUpdate struct { + Stream string `json:"stream"` + Data WsOrderUpdateData `json:"data"` +} + +// WsOrderUpdateData defines websocket account order update data +type WsOrderUpdateData struct { + EventType string `json:"e"` + EventTime time.Time `json:"E"` + Symbol string `json:"s"` + ClientOrderID string `json:"c"` + Side string `json:"S"` + OrderType string `json:"o"` + TimeInForce string `json:"f"` + Quantity float64 `json:"q,string"` + Price float64 `json:"p,string"` + StopPrice float64 `json:"P,string"` + IcebergQuantity float64 `json:"F,string"` + OrderListID int64 `json:"g"` + CancelledClientOrderID string `json:"C"` + CurrentExecutionType string `json:"x"` + OrderStatus string `json:"X"` + RejectionReason string `json:"r"` + OrderID int64 `json:"i"` + LastExecutedQuantity float64 `json:"l,string"` + CumulativeFilledQuantity float64 `json:"z,string"` + LastExecutedPrice float64 `json:"L,string"` + Commission float64 `json:"n,string"` + CommissionAsset string `json:"N"` + TransactionTime time.Time `json:"T"` + TradeID int64 `json:"t"` + Ignored int64 `json:"I"` // Must be ignored explicitly, otherwise it overwrites 'i'. + IsOnOrderBook bool `json:"w"` + IsMaker bool `json:"m"` + Ignored2 bool `json:"M"` // See the comment for "I". + OrderCreationTime time.Time `json:"O"` + CumulativeQuoteTransactedQuantity float64 `json:"Z,string"` + LastQuoteAssetTransactedQuantity float64 `json:"Y,string"` + QuoteOrderQuantity float64 `json:"Q,string"` +} + +// WsListStatus holder for websocket account listing status response including the stream information +type WsListStatus struct { + Stream string `json:"stream"` + Data WsListStatusData `json:"data"` +} + +// WsListStatusData holder for websocket account listing status response. +type WsListStatusData struct { + ListClientOrderID string `json:"C"` + EventTime time.Time `json:"E"` + ListOrderStatus string `json:"L"` + Orders []struct { + ClientOrderID string `json:"c"` + OrderID int64 `json:"i"` + Symbol string `json:"s"` + } `json:"O"` + TransactionTime time.Time `json:"T"` + ContingencyType string `json:"c"` + EventType string `json:"e"` + OrderListID int64 `json:"g"` + ListStatusType string `json:"l"` + RejectionReason string `json:"r"` + Symbol string `json:"s"` +} + +// TradeStream holds the trade stream data +type TradeStream struct { + EventType string `json:"e"` + EventTime time.Time `json:"E"` + Symbol string `json:"s"` + TradeID int64 `json:"t"` + Price string `json:"p"` + Quantity string `json:"q"` + BuyerOrderID int64 `json:"b"` + SellerOrderID int64 `json:"a"` + TimeStamp time.Time `json:"T"` + Maker bool `json:"m"` + BestMatchPrice bool `json:"M"` +} + +// KlineStream holds the kline stream data +type KlineStream struct { + EventType string `json:"e"` + EventTime time.Time `json:"E"` + Symbol string `json:"s"` + Kline KlineStreamData `json:"k"` +} + +// KlineStreamData defines kline streaming data +type KlineStreamData struct { + StartTime time.Time `json:"t"` + CloseTime time.Time `json:"T"` + Symbol string `json:"s"` + Interval string `json:"i"` + FirstTradeID int64 `json:"f"` + LastTradeID int64 `json:"L"` + OpenPrice float64 `json:"o,string"` + ClosePrice float64 `json:"c,string"` + HighPrice float64 `json:"h,string"` + LowPrice float64 `json:"l,string"` + Volume float64 `json:"v,string"` + NumberOfTrades int64 `json:"n"` + KlineClosed bool `json:"x"` + Quote float64 `json:"q,string"` + TakerBuyBaseAssetVolume float64 `json:"V,string"` + TakerBuyQuoteAssetVolume float64 `json:"Q,string"` +} + +// TickerStream holds the ticker stream data +type TickerStream struct { + EventType string `json:"e"` + EventTime time.Time `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 time.Time `json:"O"` + CloseTime time.Time `json:"C"` + FirstTradeID int64 `json:"F"` + LastTradeID int64 `json:"L"` + NumberOfTrades int64 `json:"n"` +} + +// OrderBookTickerStream contains websocket orderbook data +type OrderBookTickerStream struct { + LastUpdateID int64 `json:"u"` + S string `json:"s"` + Symbol currency.Pair + BestBidPrice float64 `json:"b,string"` + BestBidQty float64 `json:"B,string"` + BestAskPrice float64 `json:"a,string"` + BestAskQty float64 `json:"A,string"` +} + +// WebsocketAggregateTradeStream aggregate trade streams push data +type WebsocketAggregateTradeStream struct { + EventType string `json:"e"` + EventTime time.Time `json:"E"` + Symbol string `json:"s"` + AggregateTradeID int64 `json:"a"` + Price float64 `json:"p,string"` + Quantity float64 `json:"q,string"` + FirstTradeID int64 `json:"f"` + LastTradeID int64 `json:"l"` + TradeTime time.Time `json:"T"` + IsMaker bool `json:"m"` +} + +// OCBSOrderRequestParams holds parameters to retrieve OCBS orders. +type OCBSOrderRequestParams struct { + OrderID string + StartTime time.Time + EndTime time.Time + Limit uint +} + +// OCBSTradeOrdersResponse holds the quantity and list of OCBS Orders. +type OCBSTradeOrdersResponse struct { + Total int64 `json:"total"` + OCBSOrder []OCBSOrder `json:"dataList"` +} + +// OCBSOrder holds OCBS orders details. +type OCBSOrder struct { + QuoteID string `json:"quoteId"` + OrderID string `json:"orderId"` + OrderStatus string `json:"orderStatus"` + FromCoin string `json:"fromCoin"` + FromAmount float64 `json:"fromAmount"` + ToCoin string `json:"toCoin"` + ToAmount float64 `json:"toAmount"` + FeeCoin string `json:"feeCoin"` + FeeAmount float64 `json:"feeAmount"` + Ratio float64 `json:"ratio"` + CreateTime time.Time `json:"createTime"` +} + +// ServerTime holds the exchange server time +type ServerTime struct { + Timestamp time.Time `json:"serverTime"` +} + +// SubUserToBTCAssets holds the number of BTC assets and the corresponding sub user email. +type SubUserToBTCAssets struct { + Email string `json:"email"` + TotalAsset int64 `json:"totalAsset"` +} + +// SpotUSDMasterAccounts holds the USD assets of a sub user. +type SpotUSDMasterAccounts struct { + TotalCount int64 `json:"totalCount"` + MasterAccountTotalAsset int64 `json:"masterAccountTotalAsset"` + SpotSubUserAssetBTCVolumeList []SubUserToBTCAssets `json:"spotSubUserAssetBtcVoList"` +} + +// SubAccountStatus represents single sub accounts status information. +type SubAccountStatus struct { + Email string `json:"email"` + InsertTime time.Time `json:"insertTime"` + Mobile string `json:"mobile"` + IsUserActive bool `json:"isUserActive"` + IsMarginEnabled bool `json:"isMarginEnabled"` + IsSubUserEnabled bool `json:"isSubUserEnabled"` + IsFutureEnabled bool `json:"isFutureEnabled"` +} + +// SubAccountDepositAddressRequestParams holds query parameters for Sub-account deposit addresses. +type SubAccountDepositAddressRequestParams struct { + Email string // [Required] Sub-account email + Coin currency.Code // [Required] + Network string // Network (If empty, returns the default network) +} + +// SubAccountDepositAddress holds sub-accounts deposit address information +type SubAccountDepositAddress struct { + Coin string `json:"coin"` + Address string `json:"address"` + Tag string `json:"tag"` + URL string `json:"url"` +} + +// SubAccountDepositItem holds the sub-account deposit information +type SubAccountDepositItem struct { + Amount string `json:"amount"` + Coin string `json:"coin"` + Network string `json:"network"` + Status int64 `json:"status"` + Address string `json:"address"` + AddressTag string `json:"addressTag"` + TransactionID string `json:"txId"` + InsertTime time.Time `json:"insertTime"` + TransferType int64 `json:"transferType"` + ConfirmTimes string `json:"confirmTimes"` +} + +// ReferralRewardHistoryResponse holds reward history response +type ReferralRewardHistoryResponse struct { + Total int64 `json:"total"` + Rows []ReferralWithdrawalItem `json:"rows"` +} + +// ReferralWithdrawalItem holds reward history item +type ReferralWithdrawalItem struct { + UserID int64 `json:"userId"` + RewardAmount string `json:"rewardAmount"` + ReceiveDateTime time.Time `json:"receiveDateTime"` + RewardType string `json:"rewardType"` +} + +// SpotAssetsSnapshotResponse represents spot asset types snapshot information. +type SpotAssetsSnapshotResponse struct { + Code int64 `json:"code"` + Msg string `json:"msg"` + SnapshotVos []string `json:"snapshotVos"` +} diff --git a/exchanges/binanceus/binanceus_websocket.go b/exchanges/binanceus/binanceus_websocket.go new file mode 100644 index 00000000..cc50cbfa --- /dev/null +++ b/exchanges/binanceus/binanceus_websocket.go @@ -0,0 +1,1098 @@ +package binanceus + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/websocket" + "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/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/trade" + "github.com/thrasher-corp/gocryptotrader/log" +) + +const ( + binanceusDefaultWebsocketURL = "wss://stream.binance.us:9443/stream" + binanceusAPIURL = "https://api.binance.us" + pingDelay = time.Minute * 9 +) + +var listenKey string + +var ( + // maxWSUpdateBuffer defines max websocket updates to apply when an + // orderbook is initially fetched + maxWSUpdateBuffer = 150 + // maxWSOrderbookJobs defines max websocket orderbook jobs in queue to fetch + // an orderbook snapshot via REST + maxWSOrderbookJobs = 2000 + // maxWSOrderbookWorkers defines a max amount of workers allowed to execute + // jobs from the job channel + maxWSOrderbookWorkers = 10 +) + +// WsConnect initiates a websocket connection +func (bi *Binanceus) WsConnect() error { + if !bi.Websocket.IsEnabled() || !bi.IsEnabled() { + return errors.New(stream.WebsocketNotEnabled) + } + var dialer websocket.Dialer + dialer.HandshakeTimeout = bi.Config.HTTPTimeout + dialer.Proxy = http.ProxyFromEnvironment + var err error + if bi.Websocket.CanUseAuthenticatedEndpoints() { + listenKey, err = bi.GetWsAuthStreamKey(context.TODO()) + if err != nil { + bi.Websocket.SetCanUseAuthenticatedEndpoints(false) + log.Errorf(log.ExchangeSys, + "%v unable to connect to authenticated Websocket. Error: %s", + bi.Name, + err) + } else { + // cleans on failed connection + clean := strings.Split(bi.Websocket.GetWebsocketURL(), "?streams=") + authPayload := clean[0] + "?streams=" + listenKey + err = bi.Websocket.SetWebsocketURL(authPayload, false, false) + if err != nil { + return err + } + } + } + err = bi.Websocket.Conn.Dial(&dialer, http.Header{}) + if err != nil { + return fmt.Errorf("%v - Unable to connect to Websocket. Error: %s", + bi.Name, + err) + } + + if bi.Websocket.CanUseAuthenticatedEndpoints() { + bi.Websocket.Wg.Add(1) + go bi.KeepAuthKeyAlive() + } + + bi.Websocket.Conn.SetupPingHandler(stream.PingHandler{ + UseGorillaHandler: true, + MessageType: websocket.PongMessage, + Delay: pingDelay, + }) + + bi.Websocket.Wg.Add(1) + go bi.wsReadData() + + bi.setupOrderbookManager() + return nil +} + +// KeepAuthKeyAlive will continuously send messages to +// keep the WS auth key active +func (bi *Binanceus) KeepAuthKeyAlive() { + defer bi.Websocket.Wg.Done() + // ClosUserDataStream closes the User data stream and remove the listen key when closing the websocket. + defer func() { + er := bi.CloseUserDataStream(context.Background()) + log.Errorf(log.WebsocketMgr, + "%s closing user data stream error %v", + bi.Name, er) + }() + // Looping in 30 Minutes and updating the listenKey + ticks := time.NewTicker(time.Minute * 30) + for { + select { + case <-bi.Websocket.ShutdownC: + ticks.Stop() + return + case <-ticks.C: + err := bi.MaintainWsAuthStreamKey(context.TODO()) + if err != nil { + bi.Websocket.DataHandler <- err + log.Warnf(log.ExchangeSys, + bi.Name+" - Unable to renew auth websocket token, may experience shutdown") + } + } + } +} + +// wsReadData receives and passes on websocket messages for processing +func (bi *Binanceus) wsReadData() { + defer bi.Websocket.Wg.Done() + + for { + resp := bi.Websocket.Conn.ReadMessage() + if resp.Raw == nil { + return + } + err := bi.wsHandleData(resp.Raw) + if err != nil { + bi.Websocket.DataHandler <- err + } + } +} + +func stringToOrderStatus(status string) (order.Status, error) { + switch status { + case "NEW": + return order.New, nil + case "PARTIALLY_FILLED": + return order.PartiallyFilled, nil + case "FILLED": + return order.Filled, nil + case "CANCELED": + return order.Cancelled, nil + case "PENDING_CANCEL": + return order.PendingCancel, nil + case "REJECTED": + return order.Rejected, nil + case "EXPIRED": + return order.Expired, nil + default: + return order.UnknownStatus, errors.New(status + " not recognised as order status") + } +} + +func (bi *Binanceus) wsHandleData(respRaw []byte) error { + var multiStreamData map[string]interface{} + err := json.Unmarshal(respRaw, &multiStreamData) + if err != nil { + return err + } + + if r, ok := multiStreamData["result"]; ok { + if r == nil { + return nil + } + } + + if method, ok := multiStreamData["method"].(string); ok { + if strings.EqualFold(method, "subscribe") { + return nil + } + if strings.EqualFold(method, "unsubscribe") { + return nil + } + } + if newData, ok := multiStreamData["data"].(map[string]interface{}); ok { + if e, ok := newData["e"].(string); ok { + switch e { + case "outboundAccountPosition": + var data wsAccountPosition + err = json.Unmarshal(respRaw, &data) + if err != nil { + return fmt.Errorf("%v - Could not convert to outboundAccountPosition structure %s", + bi.Name, + err) + } + bi.Websocket.DataHandler <- data + return nil + case "balanceUpdate": + var data wsBalanceUpdate + err := json.Unmarshal(respRaw, &data) + if err != nil { + return fmt.Errorf("%v - Could not convert to balanceUpdate structure %s", + bi.Name, + err) + } + bi.Websocket.DataHandler <- data + return nil + case "executionReport": + var data wsOrderUpdate + err := json.Unmarshal(respRaw, &data) + if err != nil { + return fmt.Errorf("%v - Could not convert to executionReport structure %s", + bi.Name, + err) + } + averagePrice := 0.0 + if data.Data.CumulativeFilledQuantity != 0 { + averagePrice = data.Data.CumulativeQuoteTransactedQuantity / data.Data.CumulativeFilledQuantity + } + remainingAmount := data.Data.Quantity - data.Data.CumulativeFilledQuantity + pair, assetType, err := bi.GetRequestFormattedPairAndAssetType(data.Data.Symbol) + if err != nil { + return err + } + var feeAsset currency.Code + if data.Data.CommissionAsset != "" { + feeAsset = currency.NewCode(data.Data.CommissionAsset) + } + orderID := strconv.FormatInt(data.Data.OrderID, 10) + orderStatus, err := stringToOrderStatus(data.Data.OrderStatus) + if err != nil { + bi.Websocket.DataHandler <- order.ClassificationError{ + Exchange: bi.Name, + OrderID: orderID, + Err: err, + } + } + clientOrderID := data.Data.ClientOrderID + if orderStatus == order.Cancelled { + clientOrderID = data.Data.CancelledClientOrderID + } + orderType, err := order.StringToOrderType(data.Data.OrderType) + if err != nil { + bi.Websocket.DataHandler <- order.ClassificationError{ + Exchange: bi.Name, + OrderID: orderID, + Err: err, + } + } + orderSide, err := order.StringToOrderSide(data.Data.Side) + if err != nil { + bi.Websocket.DataHandler <- order.ClassificationError{ + Exchange: bi.Name, + OrderID: orderID, + Err: err, + } + } + bi.Websocket.DataHandler <- &order.Detail{ + Price: data.Data.Price, + Amount: data.Data.Quantity, + AverageExecutedPrice: averagePrice, + ExecutedAmount: data.Data.CumulativeFilledQuantity, + RemainingAmount: remainingAmount, + Cost: data.Data.CumulativeQuoteTransactedQuantity, + CostAsset: pair.Quote, + Fee: data.Data.Commission, + FeeAsset: feeAsset, + Exchange: bi.Name, + OrderID: orderID, + ClientOrderID: clientOrderID, + Type: orderType, + Side: orderSide, + Status: orderStatus, + AssetType: assetType, + Date: data.Data.OrderCreationTime, + LastUpdated: data.Data.TransactionTime, + Pair: pair, + } + return nil + case "listStatus": + var data WsListStatus + err := json.Unmarshal(respRaw, &data) + if err != nil { + return fmt.Errorf("%v - Could not convert to listStatus structure %s", + bi.Name, + err) + } + bi.Websocket.DataHandler <- data + return nil + } + } + } + // Market Data Streams + if wsStream, ok := multiStreamData["stream"].(string); ok { + var pairs currency.Pairs + streamType := strings.Split(wsStream, "@") + if len(streamType) > 1 { + if data, ok := multiStreamData["data"]; ok { + rawData, err := json.Marshal(data) + if err != nil { + return err + } + + pairs, err = bi.GetEnabledPairs(asset.Spot) + if err != nil { + return err + } + + format, err := bi.GetPairFormat(asset.Spot, true) + if err != nil { + return err + } + + switch streamType[1] { + case "trade": + saveTradeData := bi.IsSaveTradeDataEnabled() + + if !saveTradeData && + !bi.IsTradeFeedEnabled() { + return nil + } + var t TradeStream + err = json.Unmarshal(rawData, &t) + if err != nil { + return fmt.Errorf("%v - Could not unmarshal trade data: %s", + bi.Name, + err) + } + + price, err := strconv.ParseFloat(t.Price, 64) + if err != nil { + return fmt.Errorf("%v - price conversion error: %s", + bi.Name, + err) + } + amount, err := strconv.ParseFloat(t.Quantity, 64) + if err != nil { + return fmt.Errorf("%v - amount conversion error: %s", + bi.Name, + err) + } + + pair, err := currency.NewPairFromFormattedPairs(t.Symbol, pairs, format) + if err != nil { + return err + } + + return bi.Websocket.Trade.Update(saveTradeData, + trade.Data{ + CurrencyPair: pair, + Timestamp: t.TimeStamp, + Price: price, + Amount: amount, + Exchange: bi.Name, + AssetType: asset.Spot, + TID: strconv.FormatInt(t.TradeID, 10), + }) + case "ticker": + var t TickerStream + err := json.Unmarshal(rawData, &t) + if err != nil { + return fmt.Errorf("%v - Could not convert to a TickerStream structure %s", + bi.Name, + err.Error()) + } + + pair, err := currency.NewPairFromFormattedPairs(t.Symbol, pairs, format) + if err != nil { + return err + } + + bi.Websocket.DataHandler <- &ticker.Price{ + ExchangeName: bi.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, + LastUpdated: t.EventTime, + AssetType: asset.Spot, + Pair: pair, + } + return nil + case "kline_1m", "kline_3m", "kline_5m", "kline_15m", "kline_30m", "kline_1h", "kline_2h", "kline_4h", + "kline_6h", "kline_8h", "kline_12h", "kline_1d", "kline_3d", "kline_1w", "kline_1M": + var kline KlineStream + err := json.Unmarshal(rawData, &kline) + if err != nil { + return fmt.Errorf("%v - Could not convert to a KlineStream structure %s", + bi.Name, + err) + } + + pair, err := currency.NewPairFromFormattedPairs(kline.Symbol, pairs, format) + if err != nil { + return err + } + + bi.Websocket.DataHandler <- stream.KlineData{ + Timestamp: kline.EventTime, + Pair: pair, + AssetType: asset.Spot, + Exchange: bi.Name, + StartTime: kline.Kline.StartTime, + CloseTime: kline.Kline.CloseTime, + Interval: kline.Kline.Interval, + OpenPrice: kline.Kline.OpenPrice, + ClosePrice: kline.Kline.ClosePrice, + HighPrice: kline.Kline.HighPrice, + LowPrice: kline.Kline.LowPrice, + Volume: kline.Kline.Volume, + } + return nil + case "depth": + var depth WebsocketDepthStream + err := json.Unmarshal(rawData, &depth) + if err != nil { + return fmt.Errorf("%v - Could not convert to depthStream structure %s", + bi.Name, + err) + } + init, err := bi.UpdateLocalBuffer(&depth) + if err != nil { + if init { + return nil + } + return fmt.Errorf("%v - UpdateLocalCache error: %s", + bi.Name, + err) + } + return nil + case "depth5", "depth10", "depth20": + var depth WebsocketDepthDiffStream + err := json.Unmarshal(rawData, &depth) + if err != nil { + return fmt.Errorf("%v - Could not convert to depthStream structure %s", + bi.Name, + err) + } + bi.Websocket.DataHandler <- depth + return nil + case "bookTicker": + var bo OrderBookTickerStream + err := json.Unmarshal(rawData, &bo) + if err != nil { + return fmt.Errorf("%v - Could not convert to bookOrder structure %s ", err, bi.Name) + } + pair, err := currency.NewPairFromFormattedPairs(bo.S, pairs, format) + if err != nil { + return err + } + bo.Symbol = pair + bi.Websocket.DataHandler <- &bo + return nil + case "aggTrade": + var agg WebsocketAggregateTradeStream + err := json.Unmarshal(rawData, &agg) + if err != nil { + return fmt.Errorf("%v - Could not convert to aggTrade structure %s ", err, bi.Name) + } + bi.Websocket.DataHandler <- agg + return nil + default: + bi.Websocket.DataHandler <- stream.UnhandledMessageWarning{ + Message: bi.Name + stream.UnhandledMessage + string(respRaw), + } + } + } + } else if wsStream == "!bookTicker" { + var bt OrderBookTickerStream + if data, ok := multiStreamData["data"]; ok { + rawData, err := json.Marshal(data) + if err != nil { + return err + } + pairs, err := bi.GetEnabledPairs(asset.Spot) + if err != nil { + return err + } + + format, err := bi.GetPairFormat(asset.Spot, true) + if err != nil { + return err + } + err = json.Unmarshal(rawData, &bt) + if err != nil { + return fmt.Errorf("%v - Could not convert to bookOrder structure %s ", err, bi.Name) + } + pair, err := currency.NewPairFromFormattedPairs(bt.S, pairs, format) + if err != nil { + return err + } + bt.Symbol = pair + bi.Websocket.DataHandler <- &bt + return nil + } + } + } + return fmt.Errorf("unhandled stream data %s", string(respRaw)) +} + +// UpdateLocalBuffer updates and returns the most recent iteration of the orderbook +func (bi *Binanceus) UpdateLocalBuffer(wsdp *WebsocketDepthStream) (bool, error) { + enabledPairs, err := bi.GetEnabledPairs(asset.Spot) + if err != nil { + return false, err + } + + format, err := bi.GetPairFormat(asset.Spot, true) + if err != nil { + return false, err + } + + currencyPair, err := currency.NewPairFromFormattedPairs(wsdp.Pair, + enabledPairs, + format) + if err != nil { + return false, err + } + + err = bi.obm.stageWsUpdate(wsdp, currencyPair, asset.Spot) + if err != nil { + init, err2 := bi.obm.checkIsInitialSync(currencyPair) + if err2 != nil { + return false, err2 + } + return init, err + } + + err = bi.applyBufferUpdate(currencyPair) + if err != nil { + bi.flushAndCleanup(currencyPair) + } + + return false, err +} + +// GenerateSubscriptions generates the default subscription set +func (bi *Binanceus) GenerateSubscriptions() ([]stream.ChannelSubscription, error) { + var channels = []string{"@ticker", "@trade", "@kline_1m", "@depth@100ms"} + var subscriptions []stream.ChannelSubscription + + pairs, err := bi.GetEnabledPairs(asset.Spot) + if err != nil { + return nil, err + } + +subs: + for y := range pairs { + for z := range channels { + lp := pairs[y].Lower() + lp.Delimiter = "" + if len(subscriptions) >= 1023 { + log.Warnf(log.WebsocketMgr, "BinanceUS has 1024 subscription limit, only subscribing within limit. Requested %v", len(pairs)*len(channels)) + break subs + } + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: lp.String() + channels[z], + Currency: pairs[y], + Asset: asset.Spot, + }) + } + } + + return subscriptions, nil +} + +// Subscribe subscribes to a set of channels +func (bi *Binanceus) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error { + payload := WebsocketPayload{ + Method: "SUBSCRIBE", + } + for i := range channelsToSubscribe { + payload.Params = append(payload.Params, channelsToSubscribe[i].Channel) + if i%50 == 0 && i != 0 { + err := bi.Websocket.Conn.SendJSONMessage(payload) + if err != nil { + return err + } + payload.Params = []interface{}{} + } + } + if len(payload.Params) > 0 { + err := bi.Websocket.Conn.SendJSONMessage(payload) + if err != nil { + return err + } + } + bi.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe...) + return nil +} + +// Unsubscribe unsubscribes from a set of channels +func (bi *Binanceus) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { + payload := WebsocketPayload{ + Method: "UNSUBSCRIBE", + } + for i := range channelsToUnsubscribe { + payload.Params = append(payload.Params, channelsToUnsubscribe[i].Channel) + if i%50 == 0 && i != 0 { + err := bi.Websocket.Conn.SendJSONMessage(payload) + if err != nil { + return err + } + payload.Params = []interface{}{} + } + } + if len(payload.Params) > 0 { + err := bi.Websocket.Conn.SendJSONMessage(payload) + if err != nil { + return err + } + } + bi.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...) + return nil +} + +func (bi *Binanceus) setupOrderbookManager() { + if bi.obm == nil { + bi.obm = &orderbookManager{ + state: make(map[currency.Code]map[currency.Code]map[asset.Item]*update), + jobs: make(chan job, maxWSOrderbookJobs), + } + } else { + // Change state on reconnect for initial sync. + for x := range bi.obm.state { + for _, m2 := range bi.obm.state[x] { + for y := range m2 { + m2[y].initialSync = true + m2[y].needsFetchingBook = true + m2[y].lastUpdateID = 0 + } + } + } + } + for i := 0; i < maxWSOrderbookWorkers; i++ { + // 10 workers for synchronising book + bi.SynchroniseWebsocketOrderbook() + } +} + +// SynchroniseWebsocketOrderbook synchronises full orderbook for currency pair asset +func (bi *Binanceus) SynchroniseWebsocketOrderbook() { + bi.Websocket.Wg.Add(1) + go func() { + defer bi.Websocket.Wg.Done() + for { + select { + case <-bi.Websocket.ShutdownC: + for { + select { + case <-bi.obm.jobs: + default: + return + } + } + case j := <-bi.obm.jobs: + err := bi.processJob(j.Pair) + if err != nil { + log.Errorf(log.WebsocketMgr, + "%s processing websocket orderbook error %v", + bi.Name, err) + } + } + } + }() +} + +// ProcessUpdate processes the websocket orderbook update +func (bi *Binanceus) ProcessUpdate(cp currency.Pair, a asset.Item, ws *WebsocketDepthStream) error { + updateBid := make([]orderbook.Item, len(ws.UpdateBids)) + for i := range ws.UpdateBids { + price := ws.UpdateBids[i][0] + p, err := strconv.ParseFloat(price, 64) + if err != nil { + return err + } + amount := ws.UpdateBids[i][1] + a, err := strconv.ParseFloat(amount, 64) + if err != nil { + return err + } + updateBid[i] = orderbook.Item{Price: p, Amount: a} + } + + updateAsk := make([]orderbook.Item, len(ws.UpdateAsks)) + for i := range ws.UpdateAsks { + price := ws.UpdateAsks[i][0] + p, err := strconv.ParseFloat(price, 64) + if err != nil { + return err + } + amount := ws.UpdateAsks[i][1] + a, err := strconv.ParseFloat(amount, 64) + if err != nil { + return err + } + updateAsk[i] = orderbook.Item{Price: p, Amount: a} + } + + return bi.Websocket.Orderbook.Update(&orderbook.Update{ + Bids: updateBid, + Asks: updateAsk, + Pair: cp, + UpdateID: ws.LastUpdateID, + UpdateTime: ws.Timestamp, + Asset: a, + }) +} + +// fetchBookViaREST pushes a job of fetching the orderbook via the REST protocol +// to get an initial full book that we can apply our buffered updates too. +func (o *orderbookManager) fetchBookViaREST(pair currency.Pair) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return fmt.Errorf("fetch book via rest cannot match currency pair %s asset type %s", + pair, + asset.Spot) + } + + state.initialSync = true + state.fetchingBook = true + + select { + case o.jobs <- job{pair}: + return nil + default: + return fmt.Errorf("%s %s book synchronisation channel blocked up", + pair, + asset.Spot) + } +} + +// applyBufferUpdate applies the buffer to the orderbook or initiates a new +// orderbook sync by the REST protocol which is off handed to go routine. +func (bi *Binanceus) applyBufferUpdate(pair currency.Pair) error { + fetching, needsFetching, err := bi.obm.handleFetchingBook(pair) + if err != nil { + return err + } + if fetching { + return nil + } + if needsFetching { + if bi.Verbose { + log.Debugf(log.WebsocketMgr, "%s Orderbook: Fetching via REST\n", bi.Name) + } + return bi.obm.fetchBookViaREST(pair) + } + + recent, err := bi.Websocket.Orderbook.GetOrderbook(pair, asset.Spot) + if err != nil { + log.Errorf( + log.WebsocketMgr, + "%s error fetching recent orderbook when applying updates: %s\n", + bi.Name, + err) + } + + if recent != nil { + err = bi.obm.checkAndProcessUpdate(bi.ProcessUpdate, pair, recent) + if err != nil { + log.Errorf( + log.WebsocketMgr, + "%s error processing update - initiating new orderbook sync via REST: %s\n", + bi.Name, + err) + err = bi.obm.setNeedsFetchingBook(pair) + if err != nil { + return err + } + } + } + + return nil +} + +// stopFetchingBook completes the book fetching. +func (o *orderbookManager) stopFetchingBook(pair currency.Pair) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return fmt.Errorf("could not match pair %s and asset type %s in hash table", + pair, + asset.Spot) + } + if !state.fetchingBook { + return fmt.Errorf("fetching book already set to false for %s %s", + pair, + asset.Spot) + } + state.fetchingBook = false + return nil +} + +// processJob fetches and processes orderbook updates +func (bi *Binanceus) processJob(p currency.Pair) error { + err := bi.SeedLocalCache(context.TODO(), p) + if err != nil { + return fmt.Errorf("%s %s seeding local cache for orderbook error: %v", + p, asset.Spot, err) + } + + err = bi.obm.stopFetchingBook(p) + if err != nil { + return err + } + + // Immediately apply the buffer updates so we don't wait for a + // new update to initiate this. + err = bi.applyBufferUpdate(p) + if err != nil { + bi.flushAndCleanup(p) + return err + } + return nil +} + +// SeedLocalCache seeds depth data +func (bi *Binanceus) SeedLocalCache(ctx context.Context, p currency.Pair) error { + ob, err := bi.GetOrderBookDepth(ctx, + &OrderBookDataRequestParams{ + Symbol: p, + Limit: 1000, + }) + if err != nil { + return err + } + return bi.SeedLocalCacheWithBook(p, ob) +} + +// SeedLocalCacheWithBook seeds the local orderbook cache +func (bi *Binanceus) SeedLocalCacheWithBook(p currency.Pair, orderbookNew *OrderBook) error { + newOrderBook := orderbook.Base{ + Pair: p, + Asset: asset.Spot, + Exchange: bi.Name, + LastUpdateID: orderbookNew.LastUpdateID, + VerifyOrderbook: bi.CanVerifyOrderbook, + Bids: make(orderbook.Items, len(orderbookNew.Bids)), + Asks: make(orderbook.Items, len(orderbookNew.Asks)), + } + for i := range orderbookNew.Bids { + newOrderBook.Bids[i] = orderbook.Item{ + Amount: orderbookNew.Bids[i].Quantity, + Price: orderbookNew.Bids[i].Price, + } + } + for i := range orderbookNew.Asks { + newOrderBook.Asks[i] = orderbook.Item{ + Amount: orderbookNew.Asks[i].Quantity, + Price: orderbookNew.Asks[i].Price, + } + } + return bi.Websocket.Orderbook.LoadSnapshot(&newOrderBook) +} + +// handleFetchingBook checks if a full book is being fetched or needs to be +// fetched +func (o *orderbookManager) handleFetchingBook(pair currency.Pair) (fetching, needsFetching bool, err error) { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return false, + false, + fmt.Errorf("check is fetching book cannot match currency pair %s asset type %s", + pair, + asset.Spot) + } + + if state.fetchingBook { + return true, false, nil + } + + if state.needsFetchingBook { + state.needsFetchingBook = false + state.fetchingBook = true + return false, true, nil + } + return false, false, nil +} + +// flushAndCleanup flushes orderbook and clean local cache +func (bi *Binanceus) flushAndCleanup(p currency.Pair) { + errClean := bi.Websocket.Orderbook.FlushOrderbook(p, asset.Spot) + if errClean != nil { + log.Errorf(log.WebsocketMgr, + "%s flushing websocket error: %v", + bi.Name, + errClean) + } + errClean = bi.obm.cleanup(p) + if errClean != nil { + log.Errorf(log.WebsocketMgr, "%s cleanup websocket error: %v", + bi.Name, + errClean) + } +} + +// stageWsUpdate stages websocket update to roll through updates that need to +// be applied to a fetched orderbook via REST. +func (o *orderbookManager) stageWsUpdate(u *WebsocketDepthStream, pair currency.Pair, a asset.Item) error { + o.Lock() + defer o.Unlock() + m1, ok := o.state[pair.Base] + if !ok { + m1 = make(map[currency.Code]map[asset.Item]*update) + o.state[pair.Base] = m1 + } + + m2, ok := m1[pair.Quote] + if !ok { + m2 = make(map[asset.Item]*update) + m1[pair.Quote] = m2 + } + + state, ok := m2[a] + if !ok { + state = &update{ + buffer: make(chan *WebsocketDepthStream, maxWSUpdateBuffer), + fetchingBook: false, + initialSync: true, + needsFetchingBook: true, + } + m2[a] = state + } + + if state.lastUpdateID != 0 && u.FirstUpdateID != state.lastUpdateID+1 { + // While listening to the stream, each new event's U should be + // equal to the previous event's u+1. + return fmt.Errorf("websocket orderbook synchronisation failure for pair %s and asset %s", pair, a) + } + state.lastUpdateID = u.LastUpdateID + + select { + // Put update in the channel buffer to be processed + case state.buffer <- u: + return nil + default: + <-state.buffer // pop one element + state.buffer <- u // to shift buffer on fail + return fmt.Errorf("channel blockage for %s, asset %s and connection", + pair, a) + } +} + +// completeInitialSync sets if an asset type has completed its initial sync +func (o *orderbookManager) completeInitialSync(pair currency.Pair) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return fmt.Errorf("complete initial sync cannot match currency pair %s asset type %s", + pair, + asset.Spot) + } + if !state.initialSync { + return fmt.Errorf("initital sync already set to false for %s %s", + pair, + asset.Spot) + } + state.initialSync = false + return nil +} + +// cleanup cleans up buffer and reset fetch and init +func (o *orderbookManager) cleanup(pair currency.Pair) error { + o.Lock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + o.Unlock() + return fmt.Errorf("cleanup cannot match %s %s to hash table", + pair, + asset.Spot) + } + +bufferEmpty: + for { + select { + case <-state.buffer: + // bleed and discard buffer + default: + break bufferEmpty + } + } + o.Unlock() + // disable rest orderbook synchronisation + _ = o.stopFetchingBook(pair) + _ = o.completeInitialSync(pair) + _ = o.stopNeedsFetchingBook(pair) + return nil +} + +// stopNeedsFetchingBook completes the book fetching initiation. +func (o *orderbookManager) stopNeedsFetchingBook(pair currency.Pair) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return fmt.Errorf("could not match pair %s and asset type %s in hash table", + pair, + asset.Spot) + } + if !state.needsFetchingBook { + return fmt.Errorf("needs fetching book already set to false for %s %s", + pair, + asset.Spot) + } + state.needsFetchingBook = false + return nil +} + +func (o *orderbookManager) checkAndProcessUpdate(processor func(currency.Pair, asset.Item, *WebsocketDepthStream) error, pair currency.Pair, recent *orderbook.Base) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return fmt.Errorf("could not match pair [%s] asset type [%s] in hash table to process websocket orderbook update", + pair, asset.Spot) + } + + // This will continuously remove updates from the buffered channel and + // apply them to the current orderbook. +buffer: + for { + select { + case d := <-state.buffer: + process, err := state.validate(d, recent) + if err != nil { + return err + } + if process { + err := processor(pair, asset.Spot, d) + if err != nil { + return fmt.Errorf("%s %s processing update error: %w", + pair, asset.Spot, err) + } + } + default: + break buffer + } + } + return nil +} + +// validate checks for correct update alignment +func (u *update) validate(updt *WebsocketDepthStream, recent *orderbook.Base) (bool, error) { + if updt.LastUpdateID <= recent.LastUpdateID { + // Drop any event where u is <= lastUpdateId in the snapshot. + return false, nil + } + + id := recent.LastUpdateID + 1 + if u.initialSync { + if updt.FirstUpdateID > id || updt.LastUpdateID < id { + return false, fmt.Errorf("initial websocket orderbook sync failure for pair %s and asset %s", + recent.Pair, + asset.Spot) + } + u.initialSync = false + } + return true, nil +} + +// setNeedsFetchingBook completes the book fetching initiation. +func (o *orderbookManager) setNeedsFetchingBook(pair currency.Pair) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return fmt.Errorf("could not match pair %s and asset type %s in hash table", + pair, + asset.Spot) + } + state.needsFetchingBook = true + return nil +} + +// checkIsInitialSync checks status if the book is Initial Sync being via the REST +// protocol. +func (o *orderbookManager) checkIsInitialSync(pair currency.Pair) (bool, error) { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][asset.Spot] + if !ok { + return false, + fmt.Errorf("checkIsInitialSync of orderbook cannot match currency pair %s asset type %s", + pair, + asset.Spot) + } + return state.initialSync, nil +} diff --git a/exchanges/binanceus/binanceus_wrapper.go b/exchanges/binanceus/binanceus_wrapper.go new file mode 100644 index 00000000..9e0dd00e --- /dev/null +++ b/exchanges/binanceus/binanceus_wrapper.go @@ -0,0 +1,996 @@ +package binanceus + +import ( + "context" + "errors" + "fmt" + "sort" + "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/account" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" + "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/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/trade" + "github.com/thrasher-corp/gocryptotrader/log" + "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" +) + +// GetDefaultConfig returns a default exchange config +func (bi *Binanceus) GetDefaultConfig() (*config.Exchange, error) { + bi.SetDefaults() + exchCfg := new(config.Exchange) + exchCfg.Name = bi.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = bi.BaseCurrencies + + err := bi.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if bi.Features.Supports.RESTCapabilities.AutoPairUpdates { + err := bi.UpdateTradablePairs(context.TODO(), true) + if err != nil { + return nil, err + } + } + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for Binanceus +func (bi *Binanceus) SetDefaults() { + bi.Name = "Binanceus" + bi.Enabled = true + bi.Verbose = true + bi.API.CredentialsValidator.RequiresKey = true + bi.API.CredentialsValidator.RequiresSecret = true + bi.SetValues() + + fmt1 := currency.PairStore{ + RequestFormat: ¤cy.PairFormat{Uppercase: true}, + ConfigFormat: ¤cy.PairFormat{ + Delimiter: currency.DashDelimiter, + Uppercase: true, + }, + } + err := bi.StoreAssetPairFormat(asset.Spot, fmt1) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + + bi.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, + GetOrder: true, + GetOrders: true, + CancelOrders: true, + CancelOrder: true, + SubmitOrder: true, + SubmitOrders: true, + DepositHistory: true, + WithdrawalHistory: true, + TradeFetching: true, + UserTradeHistory: true, + TradeFee: true, + CryptoDepositFee: true, + CryptoWithdrawalFee: true, + MultiChainDeposits: true, + MultiChainWithdrawals: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + AccountInfo: true, + GetOrders: true, + TradeFetching: true, + KlineFetching: true, + GetOrder: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto | + exchange.AutoWithdrawFiat, + Kline: kline.ExchangeCapabilitiesSupported{ + DateRanges: true, + Intervals: true, + }, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + Kline: kline.ExchangeCapabilitiesEnabled{ + Intervals: map[string]bool{ + kline.OneMin.Word(): true, + kline.ThreeMin.Word(): true, + kline.FiveMin.Word(): true, + kline.FifteenMin.Word(): true, + kline.ThirtyMin.Word(): true, + kline.OneHour.Word(): true, + kline.TwoHour.Word(): true, + kline.FourHour.Word(): true, + kline.SixHour.Word(): true, + kline.EightHour.Word(): true, + kline.TwelveHour.Word(): true, + kline.OneDay.Word(): true, + kline.ThreeDay.Word(): true, + kline.OneWeek.Word(): true, + kline.OneMonth.Word(): true, + }, + ResultLimit: 1000, + }, + }, + } + bi.Requester, err = request.New(bi.Name, + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), + request.WithLimiter(SetRateLimit())) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + + bi.API.Endpoints = bi.NewEndpoints() + if err := bi.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{ + exchange.RestSpot: binanceusAPIURL, + exchange.RestSpotSupplementary: binanceusAPIURL, + exchange.WebsocketSpot: binanceusDefaultWebsocketURL, + exchange.WebsocketSpotSupplementary: binanceusDefaultWebsocketURL, + }); err != nil { + log.Errorf(log.ExchangeSys, + "%s setting default endpoints error %v", + bi.Name, err) + } + bi.Websocket = stream.New() + bi.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + bi.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + bi.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup takes in the supplied exchange configuration details and sets params +func (bi *Binanceus) Setup(exch *config.Exchange) error { + err := exch.Validate() + if err != nil { + return err + } + if !exch.Enabled { + bi.SetEnabled(false) + return nil + } + err = bi.SetupDefaults(exch) + if err != nil { + return err + } + + ePoint, err := bi.API.Endpoints.GetURL(exchange.WebsocketSpot) + if err != nil { + return err + } + + err = bi.Websocket.Setup(&stream.WebsocketSetup{ + ExchangeConfig: exch, + DefaultURL: binanceusDefaultWebsocketURL, + RunningURL: ePoint, + Connector: bi.WsConnect, + Subscriber: bi.Subscribe, + Unsubscriber: bi.Unsubscribe, + GenerateSubscriptions: bi.GenerateSubscriptions, + Features: &bi.Features.Supports.WebsocketCapabilities, + OrderbookBufferConfig: buffer.Config{ + SortBuffer: true, + SortBufferByUpdateIDs: true, + }, + TradeFeed: bi.Features.Enabled.TradeFeed, + }) + if err != nil { + return err + } + + return bi.Websocket.SetupNewConnection(stream.ConnectionSetup{ + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + RateLimit: wsRateLimitMilliseconds, + }) +} + +// Start starts the Binanceus go routine +func (bi *Binanceus) Start(wg *sync.WaitGroup) error { + if wg == nil { + return fmt.Errorf("%T %w", wg, common.ErrNilPointer) + } + wg.Add(1) + go func() { + bi.Run() + wg.Done() + }() + return nil +} + +// Run implements the Binanceus wrapper +func (bi *Binanceus) Run() { + if bi.Verbose { + log.Debugf(log.ExchangeSys, + "%s Websocket: %s.", + bi.Name, + common.IsEnabled(bi.Websocket.IsEnabled())) + bi.PrintEnabledPairs() + } + + if !bi.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := bi.UpdateTradablePairs(context.TODO(), false) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + bi.Name, + err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (bi *Binanceus) FetchTradablePairs(ctx context.Context, a asset.Item) ([]string, error) { + if !bi.SupportsAsset(a) { + return nil, fmt.Errorf("asset type of %s is not supported by %s", a, bi.Name) + } + format, err := bi.GetPairFormat(a, false) + if err != nil { + return nil, err + } + tradingStatus := "TRADING" + var info ExchangeInfo + info, err = bi.GetExchangeInfo(ctx) + if err != nil { + return nil, err + } + pairs := make([]string, 0, len(info.Symbols)) + for x := range info.Symbols { + if info.Symbols[x].Status != tradingStatus || !info.Symbols[x].IsSpotTradingAllowed { + continue + } + pair := info.Symbols[x].BaseAsset + + format.Delimiter + + info.Symbols[x].QuoteAsset + pairs = append(pairs, pair) + } + return pairs, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (bi *Binanceus) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error { + pairs, err := bi.FetchTradablePairs(ctx, asset.Spot) + if err != nil { + return err + } + p, err := currency.NewPairsFromStrings(pairs) + if err != nil { + return err + } + return bi.UpdatePairs(p, asset.Spot, false, forceUpdate) +} + +// UpdateTicker updates and returns the ticker for a currency pair +func (bi *Binanceus) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) { + if a != asset.Spot { + return nil, fmt.Errorf("%w '%v'", asset.ErrNotSupported, a) + } + tick, err := bi.GetPriceChangeStats(ctx, p) + if err != nil { + return nil, err + } + err = ticker.ProcessTicker(&ticker.Price{ + Last: tick.LastPrice, + High: tick.HighPrice, + Low: tick.LowPrice, + Bid: tick.BidPrice, + Ask: tick.AskPrice, + Volume: tick.Volume, + QuoteVolume: tick.QuoteVolume, + Open: tick.OpenPrice, + Close: tick.PrevClosePrice, + Pair: p, + ExchangeName: bi.Name, + AssetType: a, + }) + if err != nil { + return nil, err + } + return ticker.GetTicker(bi.Name, p, a) +} + +// UpdateTickers updates all currency pairs of a given asset type +func (bi *Binanceus) UpdateTickers(ctx context.Context, a asset.Item) error { + if a != asset.Spot { + return fmt.Errorf("assetType not supported: %v", a) + } + tick, err := bi.GetTickers(ctx) + if err != nil { + return err + } + + pairs, err := bi.GetEnabledPairs(a) + if err != nil { + return err + } + for i := range pairs { + for y := range tick { + pairFmt, err := bi.FormatExchangeCurrency(pairs[i], a) + if err != nil { + return err + } + if tick[y].Symbol != pairFmt.String() { + continue + } + err = ticker.ProcessTicker(&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: pairFmt, + ExchangeName: bi.Name, + AssetType: a, + }) + if err != nil { + return err + } + } + } + return nil +} + +// FetchTicker returns the ticker for a currency pair +func (bi *Binanceus) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) { + fPairs, er := bi.FormatExchangeCurrency(p, assetType) + if er != nil { + return nil, er + } + + tickerNew, er := ticker.GetTicker(bi.Name, fPairs, assetType) + if er != nil { + return bi.UpdateTicker(ctx, p, assetType) + } + return tickerNew, nil +} + +// FetchOrderbook returns orderbook base on the currency pair +func (bi *Binanceus) FetchOrderbook(ctx context.Context, pair currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + fPair, err := bi.FormatExchangeCurrency(pair, assetType) + if err != nil { + return nil, err + } + ob, err := orderbook.Get(bi.Name, fPair, assetType) + if err != nil { + return bi.UpdateOrderbook(ctx, pair, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (bi *Binanceus) UpdateOrderbook(ctx context.Context, pair currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + book := &orderbook.Base{ + Exchange: bi.Name, + Pair: pair, + Asset: assetType, + VerifyOrderbook: bi.CanVerifyOrderbook, + } + + orderbookNew, err := bi.GetOrderBookDepth(ctx, &OrderBookDataRequestParams{ + Symbol: pair, + Limit: 1000}) + if err != nil { + return book, err + } + book.Bids = make([]orderbook.Item, len(orderbookNew.Bids)) + for x := range orderbookNew.Bids { + book.Bids[x] = orderbook.Item{ + Amount: orderbookNew.Bids[x].Quantity, + Price: orderbookNew.Bids[x].Price, + } + } + book.Asks = make([]orderbook.Item, len(orderbookNew.Asks)) + for x := range orderbookNew.Asks { + book.Asks[x] = orderbook.Item{ + Amount: orderbookNew.Asks[x].Quantity, + Price: orderbookNew.Asks[x].Price, + } + } + err = book.Process() + if err != nil { + return book, err + } + return orderbook.Get(bi.Name, pair, assetType) +} + +// UpdateAccountInfo retrieves balances for all enabled currencies +func (bi *Binanceus) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { + var info account.Holdings + var acc account.SubAccount + info.Exchange = bi.Name + if assetType != asset.Spot { + return info, fmt.Errorf("%v assetType is not supported", assetType) + } + theAccount, err := bi.GetAccount(ctx) + if err != nil { + return info, err + } + currencyBalance := make([]account.Balance, len(theAccount.Balances)) + for i := range theAccount.Balances { + freeBalance := theAccount.Balances[i].Free.InexactFloat64() + locked := theAccount.Balances[i].Locked.InexactFloat64() + + currencyBalance[i] = account.Balance{ + CurrencyName: currency.NewCode(theAccount.Balances[i].Asset), + Total: freeBalance + locked, + Hold: locked, + Free: freeBalance, + } + } + acc.Currencies = currencyBalance + acc.AssetType = assetType + info.Accounts = append(info.Accounts, acc) + creds, err := bi.GetCredentials(ctx) + if err != nil { + return info, err + } + if err := account.Process(&info, creds); err != nil { + return account.Holdings{}, err + } + return info, nil +} + +// FetchAccountInfo retrieves balances for all enabled currencies +func (bi *Binanceus) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { + creds, err := bi.GetCredentials(ctx) + if err != nil { + return account.Holdings{}, err + } + acc, err := account.GetHoldings(bi.Name, creds, assetType) + if err != nil { + return bi.UpdateAccountInfo(ctx, assetType) + } + return acc, nil +} + +// GetFundingHistory returns funding history, deposits and withdrawals +func (bi *Binanceus) GetFundingHistory(ctx context.Context) ([]exchange.FundHistory, error) { + return nil, common.ErrFunctionNotSupported +} + +// GetWithdrawalsHistory returns previous withdrawals data +func (bi *Binanceus) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) (resp []exchange.WithdrawalHistory, err error) { + w, err := bi.WithdrawalHistory(ctx, c, "", time.Time{}, time.Time{}, 0, 10000) + if err != nil { + return nil, err + } + for i := range w { + tm, err := time.Parse(binanceUSAPITimeLayout, w[i].ApplyTime) + if err != nil { + return nil, err + } + resp = append(resp, exchange.WithdrawalHistory{ + Status: fmt.Sprint(w[i].Status), + TransferID: w[i].ID, + Currency: w[i].Coin, + Amount: w[i].Amount, + Fee: w[i].TransactionFee, + CryptoToAddress: w[i].Address, + CryptoTxID: w[i].ID, + CryptoChain: w[i].Network, + Timestamp: tm, + }) + } + return resp, nil +} + +// GetRecentTrades returns the most recent trades for a currency and asset +func (bi *Binanceus) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) { + const limit = 1000 + tradeData, err := bi.GetMostRecentTrades(ctx, RecentTradeRequestParams{p, limit}) + if err != nil { + return nil, err + } + resp := make([]trade.Data, len(tradeData)) + for i := range tradeData { + resp[i] = trade.Data{ + TID: fmt.Sprint(tradeData[i].ID), + Exchange: bi.Name, + AssetType: assetType, + CurrencyPair: p, + Price: tradeData[i].Price, + Amount: tradeData[i].Quantity, + Timestamp: tradeData[i].Time, + } + } + + if bi.IsSaveTradeDataEnabled() { + err := trade.AddTradesToBuffer(bi.Name, resp...) + if err != nil { + return nil, err + } + } + sort.Sort(trade.ByDate(resp)) + return resp, nil +} + +// GetHistoricTrades returns historic trade data within the timeframe provided +func (bi *Binanceus) GetHistoricTrades(ctx context.Context, p currency.Pair, + assetType asset.Item, timestampStart, timestampEnd time.Time) ([]trade.Data, error) { + req := AggregatedTradeRequestParams{ + Symbol: p, + StartTime: uint64(timestampStart.UnixMilli()), + EndTime: uint64(timestampEnd.UnixMilli()), + } + trades, err := bi.GetAggregateTrades(ctx, &req) + if err != nil { + return nil, err + } + result := make([]trade.Data, len(trades)) + exName := bi.Name + for i := range trades { + t := trades[i].toTradeData(p, exName, assetType) + result[i] = *t + } + return result, nil +} + +// SubmitOrder submits a new order +func (bi *Binanceus) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { + var submitOrderResponse order.SubmitResponse + var timeInForce RequestParamsTimeForceType + var sideType string + err := s.Validate() + if err != nil { + return nil, err + } + if s.AssetType != asset.Spot { + return nil, fmt.Errorf("%s %w", s.AssetType, asset.ErrNotSupported) + } + if s.Side == order.Buy { + sideType = order.Buy.String() + } else { + sideType = order.Sell.String() + } + var requestParamOrderType RequestParamsOrderType + switch s.Type { + case order.Market: + requestParamOrderType = BinanceRequestParamsOrderMarket + case order.Limit: + timeInForce = BinanceRequestParamsTimeGTC + requestParamOrderType = BinanceRequestParamsOrderLimit + default: + return nil, errors.New(bi.Name + " unsupported order type") + } + var response NewOrderResponse + response, err = bi.NewOrder(ctx, &NewOrderRequest{ + Symbol: s.Pair, + Side: sideType, + Price: s.Price, + Quantity: s.Amount, + TradeType: requestParamOrderType, + TimeInForce: timeInForce, + NewClientOrderID: s.ClientOrderID, + }) + if err != nil { + return nil, err + } + if response.OrderID > 0 { + submitOrderResponse.OrderID = strconv.FormatInt(response.OrderID, 10) + } + if response.ExecutedQty == response.OrigQty { + submitOrderResponse.Status = order.Filled + } + for i := range response.Fills { + submitOrderResponse.Trades = append(submitOrderResponse.Trades, order.TradeHistory{ + Price: response.Fills[i].Price, + Amount: response.Fills[i].Qty, + Fee: response.Fills[i].Commission, + FeeAsset: response.Fills[i].CommissionAsset, + Exchange: bi.Name, + }) + } + + return &submitOrderResponse, nil +} + +// ModifyOrder will allow of changing orderbook placement and limit to +// market conversion +func (bi *Binanceus) ModifyOrder(_ context.Context, _ *order.Modify) (*order.ModifyResponse, error) { + return nil, common.ErrFunctionNotSupported +} + +// CancelOrder cancels an order by its corresponding ID number +func (bi *Binanceus) CancelOrder(ctx context.Context, o *order.Cancel) error { + if err := o.Validate(o.StandardCancel()); err != nil { + return err + } + if o.AssetType != asset.Spot { + return fmt.Errorf("%w '%v'", asset.ErrNotSupported, o.AssetType) + } + _, err := bi.CancelExistingOrder(ctx, + &CancelOrderRequestParams{ + Symbol: o.Pair, + OrderID: o.OrderID, + ClientSuppliedOrderID: o.ClientOrderID, + }) + return err +} + +// CancelBatchOrders cancels orders by their corresponding ID numbers +func (bi *Binanceus) CancelBatchOrders(ctx context.Context, orders []order.Cancel) (order.CancelBatchResponse, error) { + return order.CancelBatchResponse{}, common.ErrFunctionNotSupported +} + +// CancelAllOrders cancels all orders associated with a currency pair +func (bi *Binanceus) CancelAllOrders(ctx context.Context, orderCancellation *order.Cancel) (order.CancelAllResponse, error) { + if err := orderCancellation.Validate(); err != nil { + return order.CancelAllResponse{}, err + } + var cancelAllOrdersResponse order.CancelAllResponse + cancelAllOrdersResponse.Status = make(map[string]string) + if orderCancellation.AssetType == asset.Spot { + symbolValue, err := bi.FormatSymbol(orderCancellation.Pair, asset.Spot) + if err != nil { + return cancelAllOrdersResponse, err + } + openOrders, err := bi.GetAllOpenOrders(ctx, symbolValue) + if err != nil { + return cancelAllOrdersResponse, err + } + for ind := range openOrders { + pair, err := currency.NewPairFromString(openOrders[ind].Symbol) + if err != nil { + return cancelAllOrdersResponse, err + } + _, err = bi.CancelExistingOrder(ctx, &CancelOrderRequestParams{ + Symbol: pair, + OrderID: strconv.FormatUint(openOrders[ind].OrderID, 10), + ClientSuppliedOrderID: openOrders[ind].ClientOrderID, + }) + if err != nil { + return cancelAllOrdersResponse, err + } + } + } else { + return cancelAllOrdersResponse, fmt.Errorf("%w '%v'", asset.ErrNotSupported, orderCancellation.AssetType) + } + return cancelAllOrdersResponse, nil +} + +// GetOrderInfo returns order information based on order ID +func (bi *Binanceus) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (order.Detail, error) { + var respData order.Detail + orderIDInt, err := strconv.ParseInt(orderID, 10, 64) + if err != nil { + return respData, fmt.Errorf("invalid orderID %w", err) + } + symbolValue, err := bi.FormatSymbol(pair, asset.Spot) + if err != nil { + return respData, err + } + if assetType != asset.Spot { + return respData, fmt.Errorf("%s %w", assetType, asset.ErrNotSupported) + } + var orderType order.Type + resp, err := bi.GetOrder(ctx, &OrderRequestParams{ + Symbol: symbolValue, + OrderID: uint64(orderIDInt), + }) + if err != nil { + return respData, err + } + orderSide, err := order.StringToOrderSide(resp.Side) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", bi.Name, err) + } + status, err := order.StringToOrderStatus(resp.Status) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", bi.Name, err) + } + orderType, err = order.StringToOrderType(resp.Type) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", bi.Name, err) + } + + return order.Detail{ + Amount: resp.OrigQty, + Exchange: bi.Name, + OrderID: strconv.FormatInt(int64(resp.OrderID), 10), + ClientOrderID: resp.ClientOrderID, + Side: orderSide, + Type: orderType, + Pair: pair, + Cost: resp.CummulativeQuoteQty, + AssetType: assetType, + Status: status, + Price: resp.Price, + ExecutedAmount: resp.ExecutedQty, + Date: resp.Time, + LastUpdated: resp.UpdateTime, + }, nil +} + +// GetDepositAddress returns a deposit address for a specified currency +func (bi *Binanceus) GetDepositAddress(ctx context.Context, c currency.Code, _ /*accountID*/, chain string) (*deposit.Address, error) { + address, err := bi.GetDepositAddressForCurrency(ctx, c.String(), chain) + if err != nil { + return nil, err + } + return &deposit.Address{ + Address: address.Address, + Tag: address.Tag, + }, nil +} + +// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is submitted +func (bi *Binanceus) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { + if err := withdrawRequest.Validate(); err != nil { + return nil, err + } + withdrawID, err := bi.WithdrawCrypto(ctx, withdrawRequest) + if err != nil { + return nil, err + } + return &withdraw.ExchangeResponse{ + ID: withdrawID, + }, nil +} + +// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted. But, GCT has no concept of withdrawal via SEN +// the fiat withdrawal end point of Binance.US is built to submit a USD withdraw request via Silvergate Exchange Network (SEN). +// So, this method is not implemented. +func (bi *Binanceus) WithdrawFiatFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { + return nil, common.ErrNotYetImplemented +} + +// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is submitted +// But, GCT has no concept of withdrawal via SEN the fiat withdrawal end point of Binance.US is built to submit a USD withdraw request via Silvergate Exchange Network (SEN). +func (bi *Binanceus) WithdrawFiatFundsToInternationalBank(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { + return nil, common.ErrNotYetImplemented +} + +// GetActiveOrders retrieves any orders that are active/open +func (bi *Binanceus) GetActiveOrders(ctx context.Context, getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { + if err := getOrdersRequest.Validate(); err != nil { + return nil, err + } + var err error + var symbol string + var pair currency.Pair + var selectedOrders []Order + if getOrdersRequest.AssetType != asset.Spot { + return nil, fmt.Errorf("%s %w", getOrdersRequest.AssetType, asset.ErrNotSupported) + } + if len(getOrdersRequest.Pairs) != 1 { + symbol = "" + } else { + symbol, err = bi.FormatSymbol(getOrdersRequest.Pairs[0], asset.Spot) + if err != nil { + return nil, err + } + } + resp, err := bi.GetAllOpenOrders(ctx, symbol) + if err != nil { + return nil, err + } + for s := range resp { + ord := resp[s] + pair, err = currency.NewPairFromString(ord.Symbol) + if err != nil { + continue + } + for p := range getOrdersRequest.Pairs { + if getOrdersRequest.Pairs[p].Equal(pair) { + selectedOrders = append(selectedOrders, ord) + } + } + } + orders := make([]order.Detail, len(selectedOrders)) + for x := range selectedOrders { + var orderSide order.Side + var orderType order.Type + var orderStatus order.Status + orderSide, err = order.StringToOrderSide(strings.ToUpper(resp[x].Side)) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", bi.Name, err) + } + orderType, err = order.StringToOrderType(strings.ToUpper(resp[x].Type)) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", bi.Name, err) + } + orderStatus, err = order.StringToOrderStatus(resp[x].Status) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", bi.Name, err) + } + orders[x] = order.Detail{ + Amount: resp[x].OrigQty, + Date: resp[x].Time, + Exchange: bi.Name, + OrderID: strconv.FormatInt(int64(resp[x].OrderID), 10), + ClientOrderID: resp[x].ClientOrderID, + Side: orderSide, + Type: orderType, + Price: resp[x].Price, + Status: orderStatus, + Pair: getOrdersRequest.Pairs[0], + AssetType: getOrdersRequest.AssetType, + LastUpdated: resp[x].UpdateTime, + } + } + + order.FilterOrdersByPairs(&orders, getOrdersRequest.Pairs) + order.FilterOrdersByType(&orders, getOrdersRequest.Type) + order.FilterOrdersBySide(&orders, getOrdersRequest.Side) + err = order.FilterOrdersByTimeRange(&orders, getOrdersRequest.StartTime, getOrdersRequest.EndTime) + return orders, err +} + +// GetOrderHistory retrieves account order information Can Limit response to specific order status +func (bi *Binanceus) GetOrderHistory(ctx context.Context, getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { + // An endpoint like /api/v3/allOrders does not exist in the binance us + // so This end point is left unimplemented + return nil, common.ErrFunctionNotSupported +} + +// GetFeeByType returns an estimate of fee based on the type of transaction +func (bi *Binanceus) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { + if feeBuilder == nil { + return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) + } + if (!bi.AreCredentialsValid(ctx) || bi.SkipAuthCheck) && + feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { + feeBuilder.FeeType = exchange.OfflineTradeFee + } + return bi.GetFee(ctx, feeBuilder) +} + +// ValidateCredentials validates current credentials used for wrapper +func (bi *Binanceus) ValidateCredentials(ctx context.Context, assetType asset.Item) error { + _, err := bi.UpdateAccountInfo(ctx, assetType) + return bi.CheckTransientError(err) +} + +// GetHistoricCandles returns candles between a time period for a set time interval +func (bi *Binanceus) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) { + if err := bi.ValidateKline(pair, a, interval); err != nil { + return kline.Item{}, err + } + if kline.TotalCandlesPerInterval(start, end, interval) > float64(bi.Features.Enabled.Kline.ResultLimit) { + return kline.Item{}, errors.New(kline.ErrRequestExceedsExchangeLimits) + } + req := KlinesRequestParams{ + Interval: bi.GetIntervalEnum(interval), + Symbol: pair, + StartTime: start, + EndTime: end, + Limit: int64(bi.Features.Enabled.Kline.ResultLimit), + } + ret := kline.Item{ + Exchange: bi.Name, + Pair: pair, + Asset: a, + Interval: interval, + } + + candles, err := bi.GetSpotKline(ctx, &req) + if err != nil { + return kline.Item{}, err + } + for x := range candles { + ret.Candles = append(ret.Candles, kline.Candle{ + Time: candles[x].OpenTime, + Open: candles[x].Open, + High: candles[x].High, + Low: candles[x].Low, + Close: candles[x].Close, + Volume: candles[x].Volume, + }) + } + ret.SortCandlesByTimestamp(false) + return ret, nil +} + +// GetHistoricCandlesExtended returns candles between a time period for a set time interval +func (bi *Binanceus) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) { + if err := bi.ValidateKline(pair, a, interval); err != nil { + return kline.Item{}, err + } + ret := kline.Item{ + Exchange: bi.Name, + Pair: pair, + Asset: a, + Interval: interval, + } + dates, err := kline.CalculateCandleDateRanges(start, end, interval, bi.Features.Enabled.Kline.ResultLimit) + if err != nil { + return kline.Item{}, err + } + var candles []CandleStick + for x := range dates.Ranges { + req := KlinesRequestParams{ + Interval: bi.GetIntervalEnum(interval), + Symbol: pair, + StartTime: dates.Ranges[x].Start.Time, + EndTime: dates.Ranges[x].End.Time, + Limit: int64(bi.Features.Enabled.Kline.ResultLimit), + } + + candles, err = bi.GetSpotKline(ctx, &req) + if err != nil { + return kline.Item{}, err + } + + for i := range candles { + for j := range ret.Candles { + if ret.Candles[j].Time.Equal(candles[i].OpenTime) { + continue + } + } + ret.Candles = append(ret.Candles, kline.Candle{ + Time: candles[i].OpenTime, + Open: candles[i].Open, + High: candles[i].High, + Low: candles[i].Low, + Close: candles[i].Close, + Volume: candles[i].Volume, + }) + } + } + dates.SetHasDataFromCandles(ret.Candles) + summary := dates.DataSummary(false) + if len(summary) > 0 { + log.Warnf(log.ExchangeSys, "%v - %v", bi.Name, summary) + } + ret.RemoveDuplicates() + ret.RemoveOutsideRange(start, end) + ret.SortCandlesByTimestamp(false) + return ret, nil +} + +// GetAvailableTransferChains returns the available transfer blockchains for the specific +// cryptocurrency +func (bi *Binanceus) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) { + coinInfo, err := bi.GetAssetFeesAndWalletStatus(ctx) + if err != nil { + return nil, err + } + + var availableChains []string + for x := range coinInfo { + if strings.EqualFold(coinInfo[x].Coin, cryptocurrency.String()) { + for y := range coinInfo[x].NetworkList { + if coinInfo[x].NetworkList[y].DepositEnable { + availableChains = append(availableChains, coinInfo[x].NetworkList[y].Network) + } + } + } + } + return availableChains, nil +} diff --git a/exchanges/binanceus/ratelimit.go b/exchanges/binanceus/ratelimit.go new file mode 100644 index 00000000..be9e510c --- /dev/null +++ b/exchanges/binanceus/ratelimit.go @@ -0,0 +1,130 @@ +package binanceus + +import ( + "context" + "fmt" + "time" + + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "golang.org/x/time/rate" +) + +const ( + spotInterval = time.Minute + spotRequestRate = 1200 + // Order related limits which are segregated from the global rate limits + // 100 requests per 10 seconds and max 100000 requests per day. + spotOrderInterval = 10 * time.Second + spotOrderRequestRate = 100 +) + +// Binance Spot rate limits +const ( + spotDefaultRate request.EndpointLimit = iota + spotExchangeInfo + spotHistoricalTradesRate + spotOrderbookDepth500Rate + spotOrderbookDepth1000Rate + spotOrderbookDepth5000Rate + spotOrderbookTickerAllRate + spotPriceChangeAllRate + spotSymbolPriceAllRate + spotSingleOCOOrderRate + spotOpenOrdersAllRate + spotOpenOrdersSpecificRate + spotOrderRate + spotOrderQueryRate + spotTradesQueryRate + spotAllOrdersRate + spotAllOCOOrdersRate + spotOrderRateLimitRate + spotAccountInformationRate +) + +// RateLimit implements the request.Limiter interface +type RateLimit struct { + SpotRate *rate.Limiter + SpotOrdersRate *rate.Limiter +} + +// Limit executes rate limiting functionality for Binance +func (r *RateLimit) Limit(ctx context.Context, f request.EndpointLimit) error { + var limiter *rate.Limiter + var tokens int + switch f { + case spotDefaultRate: + limiter, tokens = r.SpotRate, 1 + case spotOrderbookTickerAllRate, + spotSymbolPriceAllRate: + limiter, tokens = r.SpotRate, 2 + case spotHistoricalTradesRate, + spotOrderbookDepth500Rate: + limiter, tokens = r.SpotRate, 5 + case spotOrderbookDepth1000Rate, + spotAccountInformationRate, + spotExchangeInfo, + spotTradesQueryRate: + limiter, tokens = r.SpotRate, 10 + case spotPriceChangeAllRate: + limiter, tokens = r.SpotRate, 40 + case spotOrderbookDepth5000Rate: + limiter, tokens = r.SpotRate, 50 + case spotOrderRate: + limiter, tokens = r.SpotOrdersRate, 1 + case spotOrderQueryRate, + spotSingleOCOOrderRate: + limiter, tokens = r.SpotOrdersRate, 2 + case spotOpenOrdersSpecificRate: + limiter, tokens = r.SpotOrdersRate, 3 + case spotAllOrdersRate, + spotAllOCOOrdersRate: + limiter, tokens = r.SpotOrdersRate, 10 + case spotOrderRateLimitRate: + limiter, tokens = r.SpotOrdersRate, 20 + case spotOpenOrdersAllRate: + limiter, tokens = r.SpotOrdersRate, 40 + default: + limiter, tokens = r.SpotRate, 1 + } + var finalDelay time.Duration + var reserves = make([]*rate.Reservation, tokens) + for i := 0; i < tokens; i++ { + // Consume tokens 1 at a time as this avoids needing burst capacity in the limiter, + // which would otherwise allow the rate limit to be exceeded over short periods + reserves[i] = limiter.Reserve() + finalDelay = reserves[i].Delay() + } + if dl, ok := ctx.Deadline(); ok && dl.Before(time.Now().Add(finalDelay)) { + // Cancel all potential reservations to free up rate limiter if deadline + // is exceeded. + for x := range reserves { + reserves[x].Cancel() + } + return fmt.Errorf("rate limit delay of %s will exceed deadline: %w", + finalDelay, + context.DeadlineExceeded) + } + time.Sleep(finalDelay) + return nil +} + +// SetRateLimit returns the rate limit for the exchange +func SetRateLimit() *RateLimit { + return &RateLimit{ + SpotRate: request.NewRateLimit(spotInterval, spotRequestRate), + SpotOrdersRate: request.NewRateLimit(spotOrderInterval, spotOrderRequestRate), + } +} + +// orderbookLimit returns the endpoint rate limit representing enum given order depth +func orderbookLimit(depth int64) request.EndpointLimit { + switch { + case depth <= 100: + return spotDefaultRate + case depth <= 500: + return spotOrderbookDepth500Rate + case depth <= 1000: + return spotOrderbookDepth1000Rate + } + return spotOrderbookDepth5000Rate +} diff --git a/exchanges/binanceus/type_convert.go b/exchanges/binanceus/type_convert.go new file mode 100644 index 00000000..72d371e7 --- /dev/null +++ b/exchanges/binanceus/type_convert.go @@ -0,0 +1,617 @@ +package binanceus + +import ( + "encoding/json" + "time" +) + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *RecentTrade) UnmarshalJSON(data []byte) error { + type Alias RecentTrade + chil := &struct { + Time int64 `json:"time"` + *Alias + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.Time > 0 { + a.Time = time.UnixMilli(chil.Time) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *HistoricalTrade) UnmarshalJSON(data []byte) error { + type Alias HistoricalTrade + chil := &struct { + Time int64 `json:"time"` + *Alias + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.Time > 0 { + a.Time = time.UnixMilli(chil.Time) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *AggregatedTrade) UnmarshalJSON(data []byte) error { + type Alias AggregatedTrade + chil := &struct { + TimeStamp int64 `json:"T"` + *Alias + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.TimeStamp > 0 { + a.TimeStamp = time.UnixMilli(chil.TimeStamp) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *PriceChangeStats) UnmarshalJSON(data []byte) error { + type Alias PriceChangeStats + chil := &struct { + OpenTime int64 `json:"openTime"` + CloseTime int64 `json:"closeTime"` + *Alias + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.OpenTime > 0 { + a.OpenTime = time.UnixMilli(chil.OpenTime) + } + if chil.CloseTime > 0 { + a.CloseTime = time.UnixMilli(chil.CloseTime) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *TradeStatus) UnmarshalJSON(data []byte) error { + type Alias TradeStatus + chil := &struct { + UpdateTime int64 `json:"updateTime"` + *Alias + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.UpdateTime > 0 { + a.UpdateTime = time.UnixMilli(chil.UpdateTime) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *SubAccount) UnmarshalJSON(data []byte) error { + type Alias SubAccount + chil := &struct { + CreateTime int64 `json:"createTime"` + *Alias + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.CreateTime > 0 { + a.CreateTime = time.UnixMilli(chil.CreateTime) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *Account) UnmarshalJSON(data []byte) error { + type Alias Account + chil := &struct { + UpdateTime int64 `json:"updateTime"` + *Alias + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.UpdateTime > 0 { + a.UpdateTime = time.UnixMilli(chil.UpdateTime) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *NewOrderResponse) UnmarshalJSON(data []byte) error { + type Alias NewOrderResponse + aux := &struct { + TransactionTime binanceusTime `json:"transactTime"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + if aux != nil { + a.TransactionTime = aux.TransactionTime.Time() + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *TransferHistory) UnmarshalJSON(data []byte) error { + type Alias TransferHistory + aux := &struct { + TimeStamp uint64 `json:"time"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, aux); err != nil { + return err + } + if aux.TimeStamp == 0 { + a.TimeStamp = time.UnixMilli(int64(aux.TimeStamp)) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the server Time timestamp +func (a *ExchangeInfo) UnmarshalJSON(data []byte) error { + type Alias ExchangeInfo + chil := &struct { + Servertime uint64 `json:"serverTime"` + *Alias + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.Servertime > 0 { + a.ServerTime = time.UnixMilli(int64(chil.Servertime)) + } + return nil +} + +// UnmarshalJSON deserialises the JSON infos, including the order time and update time timestamps +func (a *Order) UnmarshalJSON(data []byte) error { + type Alias Order + chil := &struct { + Time int64 `json:"time"` + UpdateTime int64 `json:"updateTime"` + *Alias + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.Time > 0 { + a.Time = time.UnixMilli(chil.Time) + } + if chil.UpdateTime > 0 { + a.UpdateTime = time.UnixMilli(chil.UpdateTime) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *Trade) UnmarshalJSON(data []byte) error { + type Alie Trade + chil := &struct { + Time int64 `json:"time"` + *Alie + }{ + Alie: (*Alie)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.Time > 0 { + a.Time = time.UnixMilli(chil.Time) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the ( TransactionTime )timestamp +func (a *OCOOrderReportItem) UnmarshalJSON(data []byte) error { + type Alias OCOOrderReportItem + chil := &struct { + *Alias + TransactionTime int64 `json:"transactionTime"` + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.TransactionTime > 0 { + a.TransactionTime = time.UnixMilli(chil.TransactionTime) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the (TransactioTime) timestamp +func (a *OCOOrderResponse) UnmarshalJSON(data []byte) error { + type Alias OCOOrderResponse + chil := &struct { + *Alias + TransactionTime int64 `json:"transactionTime"` + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.TransactionTime > 0 { + a.TransactionTime = time.UnixMilli(chil.TransactionTime) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the (Create Time) timestamp +func (a *OTCTradeOrderResponse) UnmarshalJSON(data []byte) error { + type Alias OTCTradeOrderResponse + chil := &struct { + CreateTime int64 `json:"createTime"` + *Alias + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.CreateTime > 0 { + a.CreateTime = time.UnixMilli(chil.CreateTime) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the (Create Time) timestamp +func (a *OTCTradeOrder) UnmarshalJSON(data []byte) error { + type Alias OTCTradeOrder + chil := &struct { + CreateTime int64 `json:"createTime"` + *Alias + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.CreateTime > 0 { + a.CreateTime = time.UnixMilli(chil.CreateTime) + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the (EventTime , and TransactionTime) timestamp +func (a *WsListStatus) UnmarshalJSON(data []byte) error { + type Alias WsListStatus + aux := &struct { + Data struct { + EventTime binanceusTime `json:"E"` + TransactionTime binanceusTime `json:"T"` + *WsListStatusData + } `json:"data"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + a.Data = *aux.Data.WsListStatusData + a.Data.EventTime = aux.Data.EventTime.Time() + a.Data.TransactionTime = aux.Data.TransactionTime.Time() + return nil +} + +// UnmarshalJSON deserialises the JSON info, including (EventTime , OpenTime, and TransactionTime) timestamp +func (a *TickerStream) UnmarshalJSON(data []byte) error { + type Alias TickerStream + aux := &struct { + EventTime binanceusTime `json:"E"` + OpenTime binanceusTime `json:"O"` + CloseTime binanceusTime `json:"C"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + a.EventTime = aux.EventTime.Time() + a.OpenTime = aux.OpenTime.Time() + a.CloseTime = aux.CloseTime.Time() + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *KlineStream) UnmarshalJSON(data []byte) error { + type Alias KlineStream + aux := &struct { + EventTime binanceusTime `json:"E"` + Kline struct { + StartTime binanceusTime `json:"t"` + CloseTime binanceusTime `json:"T"` + *KlineStreamData + } `json:"k"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + a.Kline = *aux.Kline.KlineStreamData + a.EventTime = aux.EventTime.Time() + a.Kline.StartTime = aux.Kline.StartTime.Time() + a.Kline.CloseTime = aux.Kline.CloseTime.Time() + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the (Timesamp and EventTime) timestamp +func (a *TradeStream) UnmarshalJSON(data []byte) error { + type Alias TradeStream + aux := &struct { + TimeStamp binanceusTime `json:"T"` + EventTime binanceusTime `json:"E"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + a.TimeStamp = aux.TimeStamp.Time() + a.EventTime = aux.EventTime.Time() + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the (EventTime, OrderCreationTime, and TransactionTime)timestamp +func (a *wsOrderUpdate) UnmarshalJSON(data []byte) error { + type Alias wsOrderUpdate + aux := &struct { + Data struct { + EventTime binanceusTime `json:"E"` + OrderCreationTime binanceusTime `json:"O"` + TransactionTime binanceusTime `json:"T"` + *WsOrderUpdateData + } `json:"data"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + a.Data = *aux.Data.WsOrderUpdateData + a.Data.EventTime = aux.Data.EventTime.Time() + a.Data.OrderCreationTime = aux.Data.OrderCreationTime.Time() + a.Data.TransactionTime = aux.Data.TransactionTime.Time() + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the (EventTime and ClearTime) timestamp +func (a *wsBalanceUpdate) UnmarshalJSON(data []byte) error { + type Alias wsBalanceUpdate + aux := &struct { + Data struct { + EventTime binanceusTime `json:"E"` + ClearTime binanceusTime `json:"T"` + *WsBalanceUpdateData + } `json:"data"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + a.Data = *aux.Data.WsBalanceUpdateData + a.Data.EventTime = aux.Data.EventTime.Time() + a.Data.ClearTime = aux.Data.ClearTime.Time() + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the (EventTime and LastUpdated) timestamp +func (a *wsAccountPosition) UnmarshalJSON(data []byte) error { + type Alias wsAccountPosition + aux := &struct { + Data struct { + EventTime binanceusTime `json:"E"` + LastUpdated binanceusTime `json:"u"` + *WsAccountPositionData + } `json:"data"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + a.Data = *aux.Data.WsAccountPositionData + a.Data.EventTime = aux.Data.EventTime.Time() + a.Data.LastUpdated = aux.Data.LastUpdated.Time() + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the (Timestamp)timestamp +func (a *WebsocketDepthStream) UnmarshalJSON(data []byte) error { + type Alias WebsocketDepthStream + aux := &struct { + Timestamp binanceusTime `json:"E"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + a.Timestamp = aux.Timestamp.Time() + return nil +} + +// UnmarshalJSON .. . +func (a *WebsocketAggregateTradeStream) UnmarshalJSON(data []byte) error { + type Alias WebsocketAggregateTradeStream + chil := &struct { + *Alias + TradeTime int64 `json:"T"` + EventTime int64 `json:"E"` + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, chil); err != nil { + return err + } + if chil.TradeTime > 0 { + a.TradeTime = time.UnixMilli(chil.TradeTime) + } + if chil.EventTime > 0 { + a.EventTime = time.UnixMilli(chil.EventTime) + } + return nil +} + +// binanceTime provides an internal conversion helper +type binanceusTime time.Time + +func (t *binanceusTime) UnmarshalJSON(data []byte) error { + var timestamp int64 + if err := json.Unmarshal(data, ×tamp); err != nil { + return err + } + *t = binanceusTime(time.UnixMilli(timestamp)) + return nil +} + +// Time returns a time.Time object +func (t binanceusTime) Time() time.Time { + return time.Time(t) +} + +// UnmarshalJSON deserialises createTime timestamp to built in time. +func (a *OCBSOrder) UnmarshalJSON(data []byte) error { + type Alias OCBSOrder + chil := &struct { + *Alias + CreateTime int64 `json:"createTime"` + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.CreateTime > 0 { + a.CreateTime = time.UnixMilli(chil.CreateTime) + } + return nil +} + +// UnmarshalJSON deserialises createTime timestamp to built in time. +func (a *ServerTime) UnmarshalJSON(data []byte) error { + type Alias ServerTime + chil := &struct { + *Alias + Timestamp int64 `json:"serverTime"` + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.Timestamp > 0 { + a.Timestamp = time.UnixMilli(chil.Timestamp) + } + return nil +} + +// UnmarshalJSON deserialises createTime timestamp to built in time. +func (a *SubAccountStatus) UnmarshalJSON(data []byte) error { + type Alias SubAccountStatus + chil := &struct { + *Alias + InsertTime int64 `json:"insertTime"` + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.InsertTime > 0 { + a.InsertTime = time.UnixMilli(chil.InsertTime) + } + return nil +} + +// UnmarshalJSON deserialises ValidTimestamp timestamp to built in time.Time instance. +func (a *Quote) UnmarshalJSON(data []byte) error { + type Alias Quote + chil := &struct { + *Alias + ValidTimestamp int64 `json:"validTimestamp"` + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.ValidTimestamp > 0 { + a.ValidTimestamp = time.UnixMilli(chil.ValidTimestamp) + } + return nil +} + +// UnmarshalJSON deserialises createTime timestamp to built in time. +func (a *SubAccountDepositItem) UnmarshalJSON(data []byte) error { + type Alias SubAccountDepositItem + chil := &struct { + *Alias + InsertTime int64 `json:"insertTime"` + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.InsertTime > 0 { + a.InsertTime = time.UnixMilli(chil.InsertTime) + } + return nil +} + +// UnmarshalJSON deserialises createTime timestamp to built in time. +func (a *ReferralWithdrawalItem) UnmarshalJSON(data []byte) error { + type Alias ReferralWithdrawalItem + chil := &struct { + *Alias + ReceiveDateTime int64 `json:"receiveDateTime"` + }{ + Alias: (*Alias)(a), + } + if er := json.Unmarshal(data, chil); er != nil { + return er + } + if chil.ReceiveDateTime > 0 { + a.ReceiveDateTime = time.UnixMilli(chil.ReceiveDateTime) + } + return nil +} diff --git a/exchanges/support.go b/exchanges/support.go index 6f30550d..4a59869f 100644 --- a/exchanges/support.go +++ b/exchanges/support.go @@ -14,6 +14,7 @@ func IsSupported(exchangeName string) bool { // Exchanges stores a list of supported exchanges var Exchanges = []string{ + "binanceus", "binance", "bitfinex", "bitflyer", diff --git a/exchanges/trade/README.md b/exchanges/trade/README.md index 6a359d25..e53fe283 100644 --- a/exchanges/trade/README.md +++ b/exchanges/trade/README.md @@ -60,6 +60,7 @@ _b in this context is an `IBotExchange` implemented struct_ | Exchange | Recent Trades via REST | Live trade updates via Websocket | Trade history via REST | |----------|------|-----------|-----| | Alphapoint | No | No | No | +| Binance.US | Yes | Yes | NA | | Binance| Yes | Yes | Yes | | Bitfinex | Yes | Yes | Yes | | Bitflyer | Yes | No | No | diff --git a/portfolio/withdraw/withdraw_types.go b/portfolio/withdraw/withdraw_types.go index 72e84af2..04c077b4 100644 --- a/portfolio/withdraw/withdraw_types.go +++ b/portfolio/withdraw/withdraw_types.go @@ -91,6 +91,9 @@ type Request struct { Amount float64 `json:"amount"` Type RequestType `json:"type"` + // Used exclusively in Binance.US + ClientOrderID string `json:"clientID"` + TradePassword string OneTimePassword int64 PIN int64 diff --git a/testdata/configtest.json b/testdata/configtest.json index f3cd307b..2e286dea 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -238,6 +238,90 @@ ] }, "exchanges": [ + { + "name": "Binanceus", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "baseCurrencies": "USD", + "currencyPairs": { + "bypassConfigFormatUpgrades": false, + "pairs": { + "spot": { + "assetEnabled": true, + "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,ADA-USDT", + "available": "BTC-USD,BCH-USD,LTC-USD,USDT-USD,BTC-USDT,ETH-USDT,BCH-USDT,LTC-USDT,BNB-USD,BNB-USDT,ETH-BTC,BNB-BTC,LTC-BTC,BCH-BTC,ADA-USD,BAT-USD,ETC-USD,XLM-USD,ZRX-USD,ADA-USDT,BAT-USDT,ETC-USDT,XLM-USDT,ZRX-USDT,LINK-USD,RVN-USD,DASH-USD,ZEC-USD,ALGO-USD,IOTA-USD,BUSD-USD,BTCB-USD,DOGE-USDT,WAVES-USD,ATOM-USDT,ATOM-USD,NEO-USDT,NEO-USD,VET-USDT,QTUM-USDT,QTUM-USD,ICX-USD,ENJ-USD,ONT-USD,ONT-USDT,ZIL-USD,ZILB-USD,VET-USD,BNBB-USD,ETHB-USD,ALGO-BUSD,XTZ-USD,XTZ-BUSD,HBAR-USD,HBAR-BUSD,OMG-USD,OMG-BUSD,MATIC-USD,MATIC-BUSD,XTZ-BTC,ADA-BTC,REP-BUSD,REP-USD,EOS-BUSD,EOS-USD,DOGE-USD,KNC-USD,KNC-USDT,VTHO-USDT,VTHO-USD,USDC-USD,COMP-USDT,COMP-USD,MANA-USD,HNT-USD,HNT-USDT,MKR-USD,MKR-USDT,DAI-USD,ONE-USDT,ONE-USD,BAND-USDT,BAND-USD,STORJ-USDT,STORJ-USD,UNI-USD,UNI-USDT,SOL-USD,SOL-USDT,LINK-BTC,VET-BTC,UNI-BTC,EGLD-USDT,EGLD-USD,PAXG-USDT,PAXG-USD,OXT-USDT,OXT-USD,ZEN-USDT,ZEN-USD,BTC-USDC,ONEB-USD,FIL-USDT,FIL-USD,AAVE-USDT,AAVE-USD,GRT-USDT,GRT-USD,SUSHI-USD,ANKR-USD,AMP-USD,SHIB-USDT,SHIB-BUSD,CRV-USDT,CRV-USD,AXS-USDT,AXS-USD,SOL-BTC,AVAX-USDT,AVAX-USD,CTSI-USDT,CTSI-USD,DOT-USDT,DOT-USD,YFI-USDT,YFI-USD,1INCH-USDT,1INCH-USD,FTM-USDT,FTM-USD,USDC-USDT,ETH-USDC,USDC-BUSD,MATIC-USDT,MANA-USDT,MANA-BUSD,ALGO-USDT,ADA-BUSD,SOL-BUSD,EOS-USDT,ENJ-USDT,NEAR-USDT,NEAR-BUSD,NEAR-USD,OMG-USDT,SUSHI-USDT,LRC-USDT,LRC-USD,LRC-BTC,KSHI-BUSD,LPT-USDT,LPT-BUSD,LPT-USD,POLY-USDT,POLY-BUSD,POLY-USD,POLY-BTC,MATIC-BTC,DOT-BTC,NMR-USDT,NMR-USD,SLP-USDT,ANT-USD,XNO-USD,CHZ-USDT,CHZ-USD,OGN-USDT,OGN-USD,GALA-USDT,GALA-USD,TLM-USDT,TLM-USD,SNX-USDT,SNX-USD,AUDIO-USDT,AUDIO-USD,ENS-USDT,MANA-BTC,ATOM-BTC,AVAX-BTC,WBTC-BTC,REQ-USDT,REQ-USD,APE-USDT,APE-USD,FLUX-USDT,FLUX-USD,TRX-BTC,TRX-BUSD,TRX-USDT,TRX-USD,COTI-USDT,COTI-USD,VOXEL-USDT,VOXEL-USD,RLC-USDT,RLC-USD,UST-USDT,UST-USD,BICO-USDT,BICO-USD,API3-USDT,API3-USD,ENS-USD,BTC-UST,BNT-USDT,BNT-USD,IMX-USDT,IMX-USD,SPELL-USDT,SPELL-USD,JASMY-USDT,JASMY-USD,FLOW-USDT,FLOW-USD,GTC-USDT,GTC-USD,BTC-BUSD,ZIL-BUSD,BNB-BUSD,ETH-BUSD,BUSD-USDT,ONE-BUSD,LINK-USDT,ZEC-USDT,SLP-USD,ANT-USDT", + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + } + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "credentials": { + "key": "", + "secret": "" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + }, + "urlEndpoints": { + "RestSpotSupplementaryURL": "https://api.binance.us", + "RestSpotURL": "https://api.binance.us", + "WebsocketSpotSupplementaryURL": "wss://stream.binance.us:9443/stream", + "WebsocketSpotURL": "wss://stream.binance.us:9443/stream" + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true, + "saveTradeData": false, + "tradeFeed": false, + "fillsFeed": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ], + "orderbook": { + "verificationBypass": false, + "websocketBufferLimit": 5, + "websocketBufferEnabled": false, + "publishPeriod": 10000000000 + } + }, { "name": "Binance", "enabled": true, diff --git a/testdata/exchangelist.csv b/testdata/exchangelist.csv index 02077e62..0e1869b6 100644 --- a/testdata/exchangelist.csv +++ b/testdata/exchangelist.csv @@ -1,3 +1,4 @@ +binanceus, binance, bitfinex, bitflyer,