From 9e6383705f62001986d59d341c2d37a29c87832a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:13:54 +1100 Subject: [PATCH 01/40] build(deps): bump golang.org/x/net from 0.15.0 to 0.16.0 (#1366) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.15.0 to 0.16.0. - [Commits](https://github.com/golang/net/compare/v0.15.0...v0.16.0) --- updated-dependencies: - dependency-name: golang.org/x/net 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> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 9a466edf8b7..1627321f48f 100644 --- a/go.mod +++ b/go.mod @@ -22,8 +22,8 @@ require ( github.com/thrasher-corp/sqlboiler v1.0.1-0.20191001234224-71e17f37a85e github.com/urfave/cli/v2 v2.25.7 github.com/volatiletech/null v8.0.0+incompatible - golang.org/x/crypto v0.13.0 - golang.org/x/net v0.15.0 + golang.org/x/crypto v0.14.0 + golang.org/x/net v0.16.0 golang.org/x/text v0.13.0 golang.org/x/time v0.3.0 google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d @@ -52,7 +52,7 @@ require ( github.com/volatiletech/inflect v0.0.1 // indirect github.com/volatiletech/sqlboiler v3.7.1+incompatible // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect diff --git a/go.sum b/go.sum index 0daeb87a62e..4c41bd6cafd 100644 --- a/go.sum +++ b/go.sum @@ -349,8 +349,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -419,8 +419,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -484,8 +484,8 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 6107896d6653cbc4d4e84f0379efe8bf3dbbc706 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:15:56 +1100 Subject: [PATCH 02/40] build(deps): bump bufbuild/buf-setup-action from 1.26.1 to 1.27.0 (#1362) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.26.1 to 1.27.0. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.26.1...v1.27.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> --- .github/workflows/proto-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/proto-lint.yml b/.github/workflows/proto-lint.yml index 4b2d77eb78d..2671fca6f0f 100644 --- a/.github/workflows/proto-lint.yml +++ b/.github/workflows/proto-lint.yml @@ -23,7 +23,7 @@ jobs: go install google.golang.org/protobuf/cmd/protoc-gen-go go install google.golang.org/grpc/cmd/protoc-gen-go-grpc - - uses: bufbuild/buf-setup-action@v1.26.1 + - uses: bufbuild/buf-setup-action@v1.27.0 - name: buf generate working-directory: ./gctrpc From cd293c43b750d4560cc1241c8cd3b9712ca9a93b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:18:52 +1100 Subject: [PATCH 03/40] build(deps): bump styfle/cancel-workflow-action from 0.11.0 to 0.12.0 (#1363) Bumps [styfle/cancel-workflow-action](https://github.com/styfle/cancel-workflow-action) from 0.11.0 to 0.12.0. - [Release notes](https://github.com/styfle/cancel-workflow-action/releases) - [Commits](https://github.com/styfle/cancel-workflow-action/compare/0.11.0...0.12.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> --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a89d408b344..cb7c4817c23 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Cancel previous workflow runs - uses: styfle/cancel-workflow-action@0.11.0 + uses: styfle/cancel-workflow-action@0.12.0 with: access_token: ${{ github.token }} @@ -76,7 +76,7 @@ jobs: steps: - name: Cancel previous workflow runs - uses: styfle/cancel-workflow-action@0.11.0 + uses: styfle/cancel-workflow-action@0.12.0 with: access_token: ${{ github.token }} @@ -125,7 +125,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cancel previous workflow runs - uses: styfle/cancel-workflow-action@0.11.0 + uses: styfle/cancel-workflow-action@0.12.0 with: access_token: ${{ github.token }} From 2799f404015312ebfda8c51d6ba370b3326f55e8 Mon Sep 17 00:00:00 2001 From: Bea <103050835+Beadko@users.noreply.github.com> Date: Mon, 9 Oct 2023 13:22:06 +0700 Subject: [PATCH 04/40] Bitfinex: Fix websocket panic when seqNo is not sent (#1356) --- exchanges/bitfinex/bitfinex_test.go | 5 ++++- exchanges/bitfinex/bitfinex_types.go | 1 + exchanges/bitfinex/bitfinex_websocket.go | 9 ++++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index 8f0117df9cc..42e4367d219 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -1242,7 +1242,7 @@ func TestWsSubscribedResponse(t *testing.T) { } } -func TestWsTradingPairSnapshot(t *testing.T) { +func TestWsOrderBook(t *testing.T) { b.WebsocketSubdChannels[23405] = &stream.ChannelSubscription{Asset: asset.Spot, Currency: btcusdPair, Channel: wsBook, Params: map[string]interface{}{"chanId": 23405}} pressXToJSON := `[23405,[[38334303613,9348.8,0.53],[38334308111,9348.8,5.98979404],[38331335157,9344.1,1.28965787],[38334302803,9343.8,0.08230094],[38334279092,9343,0.8],[38334307036,9342.938663676,0.8],[38332749107,9342.9,0.2],[38332277330,9342.8,0.85],[38329406786,9342,0.1432012],[38332841570,9341.947288638,0.3],[38332163238,9341.7,0.3],[38334303384,9341.6,0.324],[38332464840,9341.4,0.5],[38331935870,9341.2,0.5],[38334312082,9340.9,0.02126899],[38334261292,9340.8,0.26763],[38334138680,9340.625455254,0.12],[38333896802,9339.8,0.85],[38331627527,9338.9,1.57863959],[38334186713,9338.9,0.26769],[38334305819,9338.8,2.999],[38334211180,9338.75285796,3.999],[38334310699,9337.8,0.10679883],[38334307414,9337.5,1],[38334179822,9337.1,0.26773],[38334306600,9336.659955102,1.79],[38334299667,9336.6,1.1],[38334306452,9336.6,0.13979771],[38325672859,9336.3,1.25],[38334311646,9336.2,1],[38334258509,9336.1,0.37],[38334310592,9336,1.79],[38334310378,9335.6,1.43],[38334132444,9335.2,0.26777],[38331367325,9335,0.07],[38334310703,9335,0.10680562],[38334298209,9334.7,0.08757301],[38334304857,9334.456899462,0.291],[38334309940,9334.088390727,0.0725],[38334310377,9333.7,1.2868],[38334297615,9333.607784,0.1108],[38334095188,9333.3,0.26785],[38334228913,9332.7,0.40861186],[38334300526,9332.363996604,0.3884],[38334310701,9332.2,0.10680562],[38334303548,9332.005382871,0.07],[38334311798,9331.8,0.41285228],[38334301012,9331.7,1.7952],[38334089877,9331.4,0.2679],[38321942150,9331.2,0.2],[38334310670,9330,1.069],[38334063096,9329.6,0.26796],[38334310700,9329.4,0.10680562],[38334310404,9329.3,1],[38334281630,9329.1,6.57150597],[38334036864,9327.7,0.26801],[38334310702,9326.6,0.10680562],[38334311799,9326.1,0.50220625],[38334164163,9326,0.219638],[38334309722,9326,1.5],[38333051682,9325.8,0.26807],[38334302027,9325.7,0.75],[38334203435,9325.366592,0.32397696],[38321967613,9325,0.05],[38334298787,9324.9,0.3],[38334301719,9324.8,3.6227592],[38331316716,9324.763454646,0.71442],[38334310698,9323.8,0.10680562],[38334035499,9323.7,0.23431017],[38334223472,9322.670551788,0.42150603],[38334163459,9322.560399006,0.143967],[38321825171,9320.8,2],[38334075805,9320.467496148,0.30772633],[38334075800,9319.916732238,0.61457592],[38333682302,9319.7,0.0011],[38331323088,9319.116771762,0.12913],[38333677480,9319,0.0199],[38334277797,9318.6,0.89],[38325235155,9318.041088,1.20249],[38334310910,9317.82382938,1.79],[38334311811,9317.2,0.61079138],[38334311812,9317.2,0.71937652],[38333298214,9317.1,50],[38334306359,9317,1.79],[38325531545,9316.382823951,0.21263],[38333727253,9316.3,0.02316372],[38333298213,9316.1,45],[38333836479,9316,2.135],[38324520465,9315.9,2.7681],[38334307411,9315.5,1],[38330313617,9315.3,0.84455],[38334077770,9315.294024,0.01248397],[38334286663,9315.294024,1],[38325533762,9315.290315394,2.40498],[38334310018,9315.2,3],[38333682617,9314.6,0.0011],[38334304794,9314.6,0.76364676],[38334304798,9314.3,0.69242113],[38332915733,9313.8,0.0199],[38334084411,9312.8,1],[38334311893,9350.1,-1.015],[38334302734,9350.3,-0.26737],[38334300732,9350.8,-5.2],[38333957619,9351,-0.90677089],[38334300521,9351,-1.6457],[38334301600,9351.012829557,-0.0523],[38334308878,9351.7,-2.5],[38334299570,9351.921544,-0.1015],[38334279367,9352.1,-0.26732],[38334299569,9352.411802928,-0.4036],[38334202773,9353.4,-0.02139404],[38333918472,9353.7,-1.96412776],[38334278782,9354,-0.26731],[38334278606,9355,-1.2785],[38334302105,9355.439221251,-0.79191542],[38313897370,9355.569409242,-0.43363],[38334292995,9355.584296,-0.0979],[38334216989,9355.8,-0.03686414],[38333894025,9355.9,-0.26721],[38334293798,9355.936691952,-0.4311],[38331159479,9356,-0.4204022],[38333918888,9356.1,-1.10885563],[38334298205,9356.4,-0.20124428],[38328427481,9356.5,-0.1],[38333343289,9356.6,-0.41034213],[38334297205,9356.6,-0.08835018],[38334277927,9356.741101161,-0.0737],[38334311645,9356.8,-0.5],[38334309002,9356.9,-5],[38334309736,9357,-0.10680107],[38334306448,9357.4,-0.18645275],[38333693302,9357.7,-0.2672],[38332815159,9357.8,-0.0011],[38331239824,9358.2,-0.02],[38334271608,9358.3,-2.999],[38334311971,9358.4,-0.55],[38333919260,9358.5,-1.9972841],[38334265365,9358.5,-1.7841],[38334277960,9359,-3],[38334274601,9359.020969848,-3],[38326848839,9359.1,-0.84],[38334291080,9359.247048,-0.16199869],[38326848844,9359.4,-1.84],[38333680200,9359.6,-0.26713],[38331326606,9359.8,-0.84454],[38334309738,9359.8,-0.10680107],[38331314707,9359.9,-0.2],[38333919803,9360.9,-1.41177599],[38323651149,9361.33417827,-0.71442],[38333656906,9361.5,-0.26705],[38334035500,9361.5,-0.40861586],[38334091886,9362.4,-6.85940815],[38334269617,9362.5,-4],[38323629409,9362.545858872,-2.40497],[38334309737,9362.7,-0.10680107],[38334312380,9362.7,-3],[38325280830,9362.8,-1.75123],[38326622800,9362.8,-1.05145],[38333175230,9363,-0.0011],[38326848745,9363.2,-0.79],[38334308960,9363.206775564,-0.12],[38333920234,9363.3,-1.25318113],[38326848843,9363.4,-1.29],[38331239823,9363.4,-0.02],[38333209613,9363.4,-0.26719],[38334299964,9364,-0.05583123],[38323470224,9364.161816648,-0.12912],[38334284711,9365,-0.21346019],[38334299594,9365,-2.6757062],[38323211816,9365.073132585,-0.21262],[38334312456,9365.1,-0.11167861],[38333209612,9365.2,-0.26719],[38327770474,9365.3,-0.0073],[38334298788,9365.3,-0.3],[38334075803,9365.409831204,-0.30772637],[38334309740,9365.5,-0.10680107],[38326608767,9365.7,-2.76809],[38333920657,9365.7,-1.25848083],[38329594226,9366.6,-0.02587],[38334311813,9366.7,-4.72290945],[38316386301,9367.39258128,-2.37581],[38334302026,9367.4,-4.5],[38334228915,9367.9,-0.81725458],[38333921381,9368.1,-1.72213641],[38333175678,9368.2,-0.0011],[38334301150,9368.2,-2.654604],[38334297208,9368.3,-0.78036466],[38334309739,9368.3,-0.10680107],[38331227515,9368.7,-0.02],[38331184470,9369,-0.003975],[38334203436,9369.319616,-0.32397695],[38334269964,9369.7,-0.5],[38328386732,9370,-4.11759935],[38332719555,9370,-0.025],[38333921935,9370.5,-1.2224398],[38334258511,9370.5,-0.35],[38326848842,9370.8,-0.34],[38333985038,9370.9,-0.8551502],[38334283018,9370.9,-1],[38326848744,9371,-1.34]],5]` err := b.wsHandleData([]byte(pressXToJSON)) @@ -1254,6 +1254,9 @@ func TestWsTradingPairSnapshot(t *testing.T) { if err != nil { t.Error(err) } + pressXToJSON = `[23405,[7617,52.98726298,7617.1,53.601795929999994,-550.9,-0.0674,7617,8318.92961981,8257.8,7500]]` + assert.NotPanics(t, func() { err = b.wsHandleData([]byte(pressXToJSON)) }, "handleWSBookUpdate should not panic when seqNo is not configured to be sent") + assert.ErrorIs(t, err, errNoSeqNo, "handleWSBookUpdate should send correct error") } func TestWsTradeResponse(t *testing.T) { diff --git a/exchanges/bitfinex/bitfinex_types.go b/exchanges/bitfinex/bitfinex_types.go index 6b2856d2a8d..665247038c9 100644 --- a/exchanges/bitfinex/bitfinex_types.go +++ b/exchanges/bitfinex/bitfinex_types.go @@ -14,6 +14,7 @@ var ( errSetCannotBeEmpty = errors.New("set cannot be empty") errSubNotFound = errors.New("could not find matching subscription") errTypeAssert = errors.New("type assertion failed") + errNoSeqNo = errors.New("no sequence number") ) // AccountV2Data stores account v2 data diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index a6785f62647..29a35727e5d 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -499,7 +499,9 @@ func (b *Bitfinex) handleWSChecksum(chanID int, d []interface{}) error { } else { //nolint:revive // using lexical variable requires else statement token = int(f) } - + if len(d) < 4 { + return errNoSeqNo + } var seqNo int64 if f, ok := d[3].(float64); !ok { return common.GetTypeAssertError("float64", d[3], "seqNo") @@ -525,12 +527,13 @@ func (b *Bitfinex) handleWSBookUpdate(c *stream.ChannelSubscription, chanID int, if len(obSnapBundle) == 0 { return errors.New("no data within orderbook snapshot") } - + if len(d) < 3 { + return errNoSeqNo + } sequenceNo, ok := d[2].(float64) if !ok { return errors.New("type assertion failure") } - var fundingRate bool switch id := obSnapBundle[0].(type) { case []interface{}: From ae7d812478d7523ab1cb47acd2044a9ed51704bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 10:12:50 +1100 Subject: [PATCH 05/40] build(deps): bump github.com/spf13/viper from 1.16.0 to 1.17.0 (#1365) Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.16.0 to 1.17.0. - [Release notes](https://github.com/spf13/viper/releases) - [Commits](https://github.com/spf13/viper/compare/v1.16.0...v1.17.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> --- go.mod | 25 +++++++++++++++---------- go.sum | 50 +++++++++++++++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 1627321f48f..dfe96c07930 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 github.com/shopspring/decimal v1.3.1 - github.com/spf13/viper v1.16.0 + github.com/spf13/viper v1.17.0 github.com/stretchr/testify v1.8.4 github.com/thrasher-corp/gct-ta v0.0.0-20200623072738-f2b55b7f9f41 github.com/thrasher-corp/goose v2.7.0-rc4.0.20191002032028-0f2c2a27abdb+incompatible @@ -26,7 +26,7 @@ require ( golang.org/x/net v0.16.0 golang.org/x/text v0.13.0 golang.org/x/time v0.3.0 - google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d + google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb google.golang.org/grpc v1.58.2 google.golang.org/protobuf v1.31.0 ) @@ -34,28 +34,33 @@ require ( require ( github.com/boombuler/barcode v1.0.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/friendsofgo/errors v0.9.2 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/spf13/afero v1.9.5 // indirect + github.com/sagikazarmark/locafero v0.3.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.4.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/volatiletech/inflect v0.0.1 // indirect github.com/volatiletech/sqlboiler v3.7.1+incompatible // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4c41bd6cafd..2c4bb9af1f8 100644 --- a/go.sum +++ b/go.sum @@ -17,7 +17,7 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.110.6 h1:8uYAkj3YHTP/1iwReuHPxLSbdcyc+dSBbzFMrVwDR6Q= +cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -77,8 +77,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/d5/tengo/v2 v2.16.1 h1:/N6dqiGu9toqANInZEOQMM8I06icdZnmb+81DG/lZdw= github.com/d5/tengo/v2 v2.16.1/go.mod h1:XRGjEs5I9jYIKTxly6HCF8oiiilk5E/RYXOZ5b0DZC8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.0.0-20190924004331-208c0a498538/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -230,15 +231,16 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -257,31 +259,36 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= +github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= -github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= +github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -293,11 +300,10 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/thrasher-corp/gct-ta v0.0.0-20200623072738-f2b55b7f9f41 h1:oFqn2u2F6cnHskAlQ3j702hBbEfn+5bbIl90pQz9IPo= github.com/thrasher-corp/gct-ta v0.0.0-20200623072738-f2b55b7f9f41/go.mod h1:z51vdK6i7okTmwu9tPh9+W8nqPWv80B/nMZUCX17fwY= github.com/thrasher-corp/goose v2.7.0-rc4.0.20191002032028-0f2c2a27abdb+incompatible h1:SPqQlzFu3g4P9wK2iwJaWVLJWcQ5rYc43rvXBJ8RSCY= @@ -334,9 +340,13 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -361,6 +371,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -623,12 +635,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= -google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= -google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA= +google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= From 233a65a7785a61ed60fe46c75cef1b82971c4ebf Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Tue, 10 Oct 2023 10:17:07 +1100 Subject: [PATCH 06/40] exchange: refactor credentials.go from PR #1320 (#1360) * refactor credentials * glorious: nits * shazbert: nits --------- Co-authored-by: romanornr Co-authored-by: Ryan O'Hara-Reid --- exchanges/credentials.go | 48 +++++------------------------------ exchanges/credentials_test.go | 31 +++++++++++----------- exchanges/exchange.go | 4 --- exchanges/exchange_types.go | 14 ++-------- 4 files changed, 24 insertions(+), 73 deletions(-) diff --git a/exchanges/credentials.go b/exchanges/credentials.go index cfc925cdc9a..56c9d80dd8c 100644 --- a/exchanges/credentials.go +++ b/exchanges/credentials.go @@ -21,7 +21,7 @@ var ( // completely empty but an attempt at retrieving credentials was made to // undertake an authenticated HTTP request. ErrCredentialsAreEmpty = errors.New("credentials are empty") - + // Errors related to API requirements and failures errRequiresAPIKey = errors.New("requires API key but default/empty one set") errRequiresAPISecret = errors.New("requires API secret but default/empty one set") errRequiresAPIPEMKey = errors.New("requires API PEM key but default/empty one set") @@ -34,9 +34,6 @@ var ( func (a *API) SetKey(key string) { a.credMu.Lock() defer a.credMu.Unlock() - if a.credentials == nil { - a.credentials = &account.Credentials{} - } a.credentials.Key = key } @@ -44,9 +41,6 @@ func (a *API) SetKey(key string) { func (a *API) SetSecret(secret string) { a.credMu.Lock() defer a.credMu.Unlock() - if a.credentials == nil { - a.credentials = &account.Credentials{} - } a.credentials.Secret = secret } @@ -54,9 +48,6 @@ func (a *API) SetSecret(secret string) { func (a *API) SetClientID(clientID string) { a.credMu.Lock() defer a.credMu.Unlock() - if a.credentials == nil { - a.credentials = &account.Credentials{} - } a.credentials.ClientID = clientID } @@ -64,9 +55,6 @@ func (a *API) SetClientID(clientID string) { func (a *API) SetPEMKey(pem string) { a.credMu.Lock() defer a.credMu.Unlock() - if a.credentials == nil { - a.credentials = &account.Credentials{} - } a.credentials.PEMKey = pem } @@ -74,9 +62,6 @@ func (a *API) SetPEMKey(pem string) { func (a *API) SetSubAccount(sub string) { a.credMu.Lock() defer a.credMu.Unlock() - if a.credentials == nil { - a.credentials = &account.Credentials{} - } a.credentials.SubAccount = sub } @@ -116,10 +101,10 @@ func (b *Base) AreCredentialsValid(ctx context.Context) bool { func (b *Base) GetDefaultCredentials() *account.Credentials { b.API.credMu.RLock() defer b.API.credMu.RUnlock() - if b.API.credentials == nil { + if b.API.credentials == (account.Credentials{}) { return nil } - creds := *b.API.credentials + creds := b.API.credentials return &creds } @@ -142,7 +127,8 @@ func (b *Base) GetCredentials(ctx context.Context) (*account.Credentials, error) return creds, nil } - err := b.CheckCredentials(b.API.credentials, false) + creds := b.API.credentials + err := b.CheckCredentials(&creds, false) if err != nil { // NOTE: Return empty credentials on error to limit panic on websocket // handling. @@ -151,7 +137,6 @@ func (b *Base) GetCredentials(ctx context.Context) (*account.Credentials, error) subAccountOverride, ok := ctx.Value(account.ContextSubAccountFlag).(string) b.API.credMu.RLock() defer b.API.credMu.RUnlock() - creds := *b.API.credentials if ok { creds.SubAccount = subAccountOverride } @@ -201,9 +186,6 @@ func (b *Base) VerifyAPICredentials(creds *account.Credentials) error { func (b *Base) SetCredentials(apiKey, apiSecret, clientID, subaccount, pemKey, oneTimePassword string) { b.API.credMu.Lock() defer b.API.credMu.Unlock() - if b.API.credentials == nil { - b.API.credentials = &account.Credentials{} - } b.API.credentials.Key = apiKey b.API.credentials.ClientID = clientID b.API.credentials.SubAccount = subaccount @@ -231,29 +213,13 @@ func (b *Base) SetCredentials(apiKey, apiSecret, clientID, subaccount, pemKey, o func (b *Base) SetAPICredentialDefaults() { b.API.credMu.Lock() defer b.API.credMu.Unlock() + // Exchange hardcoded settings take precedence and overwrite the config settings if b.Config.API.CredentialsValidator == nil { b.Config.API.CredentialsValidator = new(config.APICredentialsValidatorConfig) } - if b.Config.API.CredentialsValidator.RequiresKey != b.API.CredentialsValidator.RequiresKey { - b.Config.API.CredentialsValidator.RequiresKey = b.API.CredentialsValidator.RequiresKey - } - - if b.Config.API.CredentialsValidator.RequiresSecret != b.API.CredentialsValidator.RequiresSecret { - b.Config.API.CredentialsValidator.RequiresSecret = b.API.CredentialsValidator.RequiresSecret - } - - if b.Config.API.CredentialsValidator.RequiresBase64DecodeSecret != b.API.CredentialsValidator.RequiresBase64DecodeSecret { - b.Config.API.CredentialsValidator.RequiresBase64DecodeSecret = b.API.CredentialsValidator.RequiresBase64DecodeSecret - } - - if b.Config.API.CredentialsValidator.RequiresClientID != b.API.CredentialsValidator.RequiresClientID { - b.Config.API.CredentialsValidator.RequiresClientID = b.API.CredentialsValidator.RequiresClientID - } - if b.Config.API.CredentialsValidator.RequiresPEM != b.API.CredentialsValidator.RequiresPEM { - b.Config.API.CredentialsValidator.RequiresPEM = b.API.CredentialsValidator.RequiresPEM - } + *b.Config.API.CredentialsValidator = b.API.CredentialsValidator } // IsWebsocketAuthenticationSupported returns whether the exchange supports diff --git a/exchanges/credentials_test.go b/exchanges/credentials_test.go index b7de7571805..9c978fd1139 100644 --- a/exchanges/credentials_test.go +++ b/exchanges/credentials_test.go @@ -186,7 +186,7 @@ func TestVerifyAPICredentials(t *testing.T) { setupBase := func(tData *tester) *Base { b := &Base{ API: API{ - CredentialsValidator: CredentialsValidator{ + CredentialsValidator: config.APICredentialsValidatorConfig{ RequiresKey: tData.RequiresKey, RequiresSecret: tData.RequiresSecret, RequiresClientID: tData.RequiresClientID, @@ -211,7 +211,7 @@ func TestVerifyAPICredentials(t *testing.T) { t.Run("", func(t *testing.T) { t.Parallel() b := setupBase(&tc) - if err := b.VerifyAPICredentials(b.API.credentials); !errors.Is(err, tc.Expected) { + if err := b.VerifyAPICredentials(&b.API.credentials); !errors.Is(err, tc.Expected) { t.Errorf("Test %d: expected: %v: got %v", x+1, tc.Expected, err) } if tc.CheckBase64DecodedOutput { @@ -236,7 +236,6 @@ func TestCheckCredentials(t *testing.T) { name: "Test SkipAuthCheck", base: &Base{ SkipAuthCheck: true, - API: API{credentials: &account.Credentials{}}, }, expectedErr: nil, }, @@ -244,8 +243,8 @@ func TestCheckCredentials(t *testing.T) { name: "Test credentials failure", base: &Base{ API: API{ - CredentialsValidator: CredentialsValidator{RequiresKey: true}, - credentials: &account.Credentials{OneTimePassword: "wow"}, + CredentialsValidator: config.APICredentialsValidatorConfig{RequiresKey: true}, + credentials: account.Credentials{OneTimePassword: "wow"}, }, }, expectedErr: errRequiresAPIKey, @@ -255,8 +254,8 @@ func TestCheckCredentials(t *testing.T) { base: &Base{ LoadedByConfig: true, API: API{ - CredentialsValidator: CredentialsValidator{RequiresKey: true}, - credentials: &account.Credentials{Key: "k3y"}, + CredentialsValidator: config.APICredentialsValidatorConfig{RequiresKey: true}, + credentials: account.Credentials{Key: "k3y"}, }, }, expectedErr: ErrAuthenticationSupportNotEnabled, @@ -267,8 +266,8 @@ func TestCheckCredentials(t *testing.T) { LoadedByConfig: true, API: API{ AuthenticatedSupport: true, - CredentialsValidator: CredentialsValidator{RequiresKey: true}, - credentials: &account.Credentials{}, + CredentialsValidator: config.APICredentialsValidatorConfig{RequiresKey: true}, + credentials: account.Credentials{}, }, }, expectedErr: ErrCredentialsAreEmpty, @@ -277,8 +276,8 @@ func TestCheckCredentials(t *testing.T) { name: "Test base64 decoded invalid credentials", base: &Base{ API: API{ - CredentialsValidator: CredentialsValidator{RequiresBase64DecodeSecret: true}, - credentials: &account.Credentials{Secret: "invalid"}, + CredentialsValidator: config.APICredentialsValidatorConfig{RequiresBase64DecodeSecret: true}, + credentials: account.Credentials{Secret: "invalid"}, }, }, expectedErr: errBase64DecodeFailure, @@ -287,8 +286,8 @@ func TestCheckCredentials(t *testing.T) { name: "Test base64 decoded valid credentials", base: &Base{ API: API{ - CredentialsValidator: CredentialsValidator{RequiresBase64DecodeSecret: true}, - credentials: &account.Credentials{Secret: "aGVsbG8gd29ybGQ="}, + CredentialsValidator: config.APICredentialsValidatorConfig{RequiresBase64DecodeSecret: true}, + credentials: account.Credentials{Secret: "aGVsbG8gd29ybGQ="}, }, }, checkBase64Output: true, @@ -299,8 +298,8 @@ func TestCheckCredentials(t *testing.T) { base: &Base{ API: API{ AuthenticatedSupport: true, - CredentialsValidator: CredentialsValidator{RequiresKey: true}, - credentials: &account.Credentials{Key: "k3y"}, + CredentialsValidator: config.APICredentialsValidatorConfig{RequiresKey: true}, + credentials: account.Credentials{Key: "k3y"}, }, }, expectedErr: nil, @@ -311,7 +310,7 @@ func TestCheckCredentials(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - if err := tc.base.CheckCredentials(tc.base.API.credentials, false); !errors.Is(err, tc.expectedErr) { + if err := tc.base.CheckCredentials(&tc.base.API.credentials, false); !errors.Is(err, tc.expectedErr) { t.Errorf("%s: received '%v' but expected '%v'", tc.name, err, tc.expectedErr) } if tc.checkBase64Output { diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 98f7f6bac4f..94ccfc0beb9 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -14,7 +14,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" "github.com/thrasher-corp/gocryptotrader/exchanges/currencystate" @@ -548,9 +547,6 @@ func (b *Base) SetupDefaults(exch *config.Exchange) error { b.API.AuthenticatedSupport = exch.API.AuthenticatedSupport b.API.AuthenticatedWebsocketSupport = exch.API.AuthenticatedWebsocketSupport - if b.API.credentials == nil { - b.API.credentials = &account.Credentials{} - } b.API.credentials.SubAccount = exch.API.Credentials.Subaccount if b.API.AuthenticatedSupport || b.API.AuthenticatedWebsocketSupport { b.SetCredentials(exch.API.Credentials.Key, diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go index 651a2bb95e4..c5a1f7486e3 100644 --- a/exchanges/exchange_types.go +++ b/exchanges/exchange_types.go @@ -209,20 +209,10 @@ type API struct { Endpoints *Endpoints - credentials *account.Credentials + credentials account.Credentials credMu sync.RWMutex - CredentialsValidator CredentialsValidator -} - -// CredentialsValidator determines what is required -// to make authenticated requests for an exchange -type CredentialsValidator struct { - RequiresPEM bool - RequiresKey bool - RequiresSecret bool - RequiresClientID bool - RequiresBase64DecodeSecret bool + CredentialsValidator config.APICredentialsValidatorConfig } // Base stores the individual exchange information From 843050980744f4b7f572f5abcd412c921fe783da Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Fri, 13 Oct 2023 06:18:55 +0200 Subject: [PATCH 07/40] Okx: Websocket order channel fixes (#1346) * Okx: Fix WS order fields * Fixes float64 with string annotation erroring on empty strings: Okx Order Push Data error json: invalid use of ,string struct tag, trying to unmarshal "" into float64 Specifically this came from px field from a market order * Switch to convert.StringToFloat64 instead of okxNumericalValue * Fix typo in Notional* field names; Ironically prevented them from erroring * Okx: Add tests for first order fields * Okx: CID and maybe set WS order Filled time * Tests: Set TestFixtureToDataHandler to t.Helper * Orders: Add UnmarshalJSON to order.Side * Okx: Fix FillTime not parsed for PendingOrder * Okx: Switch to order.Side Unmarshal throughout * Okx: Add Fee and FeeAsset to order processing * Okx: Fix WS order.Detail amounts and Test This fixes Amount vs QuoteAmount for market sells where tgtCcy is quote_ccy * Add comment to order.Side.UnmarshalJSON * Okx: Replace PendingOrderItem Unmarshal with local types * Okx: string type for WS order reduceOnly Note: Not yet in unit tests, since it's not part of the spot tests I was originally fixing. I'll circle back to adding full test support for Reduce only and deleveraging positions. * Okx: Fix TestOrderPushData Amount We were expecting 0 when we're given a quoteAmount In reality, we'll calculate the size from the price * Okx: Fix order and remAmount in wsOrders Improved handling for Float64 issues and boundaries when the order is fully executed but not yet marked as Filled * Fix ErrSideIsInvalid in tests --- exchanges/okx/okx_test.go | 65 ++++++++- exchanges/okx/okx_type_convert.go | 78 ----------- exchanges/okx/okx_types.go | 128 +++++++++--------- exchanges/okx/okx_websocket.go | 65 +++++---- exchanges/okx/okx_wrapper.go | 14 +- exchanges/okx/testdata/wsOrders.json | 4 + exchanges/order/order_test.go | 12 ++ exchanges/order/orders.go | 15 ++ .../sharedtestvalues/sharedtestvalues.go | 1 + 9 files changed, 200 insertions(+), 182 deletions(-) create mode 100644 exchanges/okx/testdata/wsOrders.json diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index 083bed360dd..e2eb5d2439d 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -2490,13 +2490,70 @@ func TestBalanceAndPosition(t *testing.T) { } } -const orderPushDataJSON = `{"arg": { "channel": "orders", "instType": "SPOT", "instId": "BTC-USDT", "uid": "614488474791936"},"data": [ { "accFillSz": "0.001", "amendResult": "", "avgPx": "31527.1", "cTime": "1654084334977", "category": "normal", "ccy": "", "clOrdId": "", "code": "0", "execType": "M", "fee": "-0.02522168", "feeCcy": "USDT", "fillFee": "-0.02522168", "fillFeeCcy": "USDT", "fillNotionalUsd": "31.50818374", "fillPx": "31527.1", "fillSz": "0.001", "fillTime": "1654084353263", "instId": "BTC-USDT", "instType": "SPOT", "lever": "0", "msg": "", "notionalUsd": "31.50818374", "ordId": "452197707845865472", "ordType": "limit", "pnl": "0", "posSide": "", "px": "31527.1", "rebate": "0", "rebateCcy": "BTC", "reduceOnly": "false", "reqId": "", "side": "sell", "slOrdPx": "", "slTriggerPx": "", "slTriggerPxType": "last", "source": "", "state": "filled", "sz": "0.001", "tag": "", "tdMode": "cash", "tgtCcy": "", "tpOrdPx": "", "tpTriggerPx": "", "tpTriggerPxType": "last", "tradeId": "242589207", "uTime": "1654084353264" }]}` - func TestOrderPushData(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(orderPushDataJSON)); err != nil { - t.Error("Okx Order Push Data error", err) + n := new(Okx) + sharedtestvalues.TestFixtureToDataHandler(t, ok, n, "testdata/wsOrders.json", n.WsHandleData) + seen := 0 + for reading := true; reading; { + select { + default: + reading = false + case resp := <-n.GetBase().Websocket.DataHandler: + seen++ + switch v := resp.(type) { + case *order.Detail: + switch seen { + case 1: + assert.Equal(t, "452197707845865472", v.OrderID, "OrderID") + assert.Equal(t, "HamsterParty14", v.ClientOrderID, "ClientOrderID") + assert.Equal(t, asset.Spot, v.AssetType, "AssetType") + assert.Equal(t, order.Sell, v.Side, "Side") + assert.Equal(t, order.Filled, v.Status, "Status") + assert.Equal(t, order.Limit, v.Type, "Type") + assert.Equal(t, currency.NewPairWithDelimiter("BTC", "USDT", "-"), v.Pair, "Pair") + assert.Equal(t, 31527.1, v.AverageExecutedPrice, "AverageExecutedPrice") + assert.Equal(t, time.UnixMilli(1654084334977), v.Date, "Date") + assert.Equal(t, time.UnixMilli(1654084353263), v.CloseTime, "CloseTime") + assert.Equal(t, 0.001, v.Amount, "Amount") + assert.Equal(t, 0.001, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 0.000, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 31527.1, v.Price, "Price") + assert.Equal(t, 0.02522168, v.Fee, "Fee") + assert.Equal(t, currency.USDT, v.FeeAsset, "FeeAsset") + case 2: + assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") + assert.Equal(t, asset.Spot, v.AssetType, "AssetType") + assert.Equal(t, order.Market, v.Type, "Type") + assert.Equal(t, order.Sell, v.Side, "Side") + assert.Equal(t, order.Active, v.Status, "Status") + assert.Equal(t, 0.0, v.Amount, "Amount should be 0 for a market sell") + assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + case 3: + assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") + assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + assert.Equal(t, 0.00038127046945832905, v.Amount, "Amount") + assert.Equal(t, 0.010000249968, v.Fee, "Fee") + assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, order.PartiallyFilled, v.Status, "Status") + case 4: + assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") + assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + assert.Equal(t, 0.010000249968, v.Fee, "Fee") + assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 0.00038128, v.Amount, "Amount should be derived because order filled") + assert.Equal(t, order.Filled, v.Status, "Status") + } + case error: + t.Error(v) + default: + t.Errorf("Got unexpected data: %T %v", v, v) + } + } } + assert.Equal(t, 4, seen, "Saw 4 records") } const algoOrdersPushDataJSON = `{"arg": {"channel": "orders-algo","uid": "77982378738415879","instType": "FUTURES","instId": "BTC-USD-200329"},"data": [{"instType": "FUTURES","instId": "BTC-USD-200329","ordId": "312269865356374016","ccy": "BTC","algoId": "1234","px": "999","sz": "3","tdMode": "cross","tgtCcy": "","notionalUsd": "","ordType": "trigger","side": "buy","posSide": "long","state": "live","lever": "20","tpTriggerPx": "","tpTriggerPxType": "","tpOrdPx": "","slTriggerPx": "","slTriggerPxType": "","triggerPx": "99","triggerPxType": "last","ordPx": "12","actualSz": "","actualPx": "","tag": "adadadadad","actualSide": "","triggerTime": "1597026383085","cTime": "1597026383000"}]}` diff --git a/exchanges/okx/okx_type_convert.go b/exchanges/okx/okx_type_convert.go index 42530fa328b..4d911327eae 100644 --- a/exchanges/okx/okx_type_convert.go +++ b/exchanges/okx/okx_type_convert.go @@ -139,7 +139,6 @@ func (a *OrderDetail) UnmarshalJSON(data []byte) error { type Alias OrderDetail chil := &struct { *Alias - Side string `json:"side"` UpdateTime int64 `json:"uTime,string"` CreationTime int64 `json:"cTime,string"` FillTime string `json:"fillTime"` @@ -152,7 +151,6 @@ func (a *OrderDetail) UnmarshalJSON(data []byte) error { var err error a.UpdateTime = time.UnixMilli(chil.UpdateTime) a.CreationTime = time.UnixMilli(chil.CreationTime) - a.Side, err = order.StringToOrderSide(chil.Side) if chil.FillTime == "" { a.FillTime = time.Time{} } else { @@ -169,38 +167,6 @@ func (a *OrderDetail) UnmarshalJSON(data []byte) error { return nil } -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *PendingOrderItem) UnmarshalJSON(data []byte) error { - type Alias PendingOrderItem - chil := &struct { - *Alias - Side string `json:"side"` - UpdateTime string `json:"uTime"` - CreationTime string `json:"cTime"` - }{ - Alias: (*Alias)(a), - } - err := json.Unmarshal(data, chil) - if err != nil { - return err - } - uTime, err := strconv.ParseInt(chil.UpdateTime, 10, 64) - if err != nil { - return err - } - cTime, err := strconv.ParseInt(chil.CreationTime, 10, 64) - if err != nil { - return err - } - a.Side, err = order.StringToOrderSide(chil.Side) - if err != nil { - return err - } - a.CreationTime = time.UnixMilli(cTime) - a.UpdateTime = time.UnixMilli(uTime) - return nil -} - // UnmarshalJSON deserializes JSON, and timestamp information. func (a *RfqTradeResponse) UnmarshalJSON(data []byte) error { type Alias RfqTradeResponse @@ -233,29 +199,6 @@ func (a *BlockTicker) UnmarshalJSON(data []byte) error { return nil } -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *BlockTrade) UnmarshalJSON(data []byte) error { - type Alias BlockTrade - chil := &struct { - *Alias - Side string `json:"side"` - }{ - Alias: (*Alias)(a), - } - if err := json.Unmarshal(data, chil); err != nil { - return err - } - switch { - case strings.EqualFold(chil.Side, "buy"): - a.Side = order.Buy - case strings.EqualFold(chil.Side, "sell"): - a.Side = order.Sell - default: - a.Side = order.UnknownSide - } - return nil -} - // UnmarshalJSON deserializes JSON, and timestamp information. func (a *UnitConvertResponse) UnmarshalJSON(data []byte) error { type Alias UnitConvertResponse @@ -277,27 +220,6 @@ func (a *UnitConvertResponse) UnmarshalJSON(data []byte) error { return nil } -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *QuoteLeg) UnmarshalJSON(data []byte) error { - type Alias QuoteLeg - chil := &struct { - *Alias - Side string `json:"side"` - }{ - Alias: (*Alias)(a), - } - if err := json.Unmarshal(data, chil); err != nil { - return err - } - chil.Side = strings.ToLower(chil.Side) - if chil.Side == "buy" { - a.Side = order.Buy - } else { - a.Side = order.Sell - } - return nil -} - // MarshalJSON serialized QuoteLeg instance into bytes func (a *QuoteLeg) MarshalJSON() ([]byte, error) { type Alias QuoteLeg diff --git a/exchanges/okx/okx_types.go b/exchanges/okx/okx_types.go index d0b803c0b29..54b4aa2cfa0 100644 --- a/exchanges/okx/okx_types.go +++ b/exchanges/okx/okx_types.go @@ -230,7 +230,7 @@ type TradeResponse struct { TradeID string `json:"tradeId"` Price float64 `json:"px,string"` Quantity float64 `json:"sz,string"` - Side string `json:"side"` + Side order.Side `json:"side"` Timestamp okxUnixMilliTime `json:"ts"` } @@ -433,7 +433,7 @@ type LiquidationOrderDetailItem struct { BankruptcyPx string `json:"bkPx"` Currency string `json:"ccy"` PosSide string `json:"posSide"` - Side string `json:"side"` + Side string `json:"side"` // May be empty QuantityOfLiquidation float64 `json:"sz,string"` Timestamp okxUnixMilliTime `json:"ts"` } @@ -717,42 +717,42 @@ type OrderHistoryRequestParams struct { // PendingOrderItem represents a pending order Item in pending orders list. type PendingOrderItem struct { - AccumulatedFillSize okxNumericalValue `json:"accFillSz"` - AveragePrice okxNumericalValue `json:"avgPx"` - CreationTime time.Time `json:"cTime"` - Category string `json:"category"` - Currency string `json:"ccy"` - ClientOrderID string `json:"clOrdId"` - TransactionFee string `json:"fee"` - FeeCurrency string `json:"feeCcy"` - LastFilledPrice string `json:"fillPx"` - LastFilledSize okxNumericalValue `json:"fillSz"` - FillTime string `json:"fillTime"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - Leverage okxNumericalValue `json:"lever"` - OrderID string `json:"ordId"` - OrderType string `json:"ordType"` - ProfitAndLose string `json:"pnl"` - PositionSide string `json:"posSide"` - RebateAmount string `json:"rebate"` - RebateCurrency string `json:"rebateCcy"` - Side order.Side `json:"side"` - StopLossOrdPrice string `json:"slOrdPx"` - StopLossTriggerPrice string `json:"slTriggerPx"` - StopLossTriggerPriceType string `json:"slTriggerPxType"` - State string `json:"state"` - Price float64 `json:"px,string"` - Size float64 `json:"sz,string"` - Tag string `json:"tag"` - QuantityType string `json:"tgtCcy"` - TradeMode string `json:"tdMode"` - Source string `json:"source"` - TakeProfitOrdPrice string `json:"tpOrdPx"` - TakeProfitTriggerPrice string `json:"tpTriggerPx"` - TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` - TradeID string `json:"tradeId"` - UpdateTime time.Time `json:"uTime"` + AccumulatedFillSize convert.StringToFloat64 `json:"accFillSz"` + AveragePrice convert.StringToFloat64 `json:"avgPx"` + CreationTime okxUnixMilliTime `json:"cTime"` + Category string `json:"category"` + Currency string `json:"ccy"` + ClientOrderID string `json:"clOrdId"` + Fee convert.StringToFloat64 `json:"fee"` + FeeCurrency currency.Code `json:"feeCcy"` + LastFilledPrice convert.StringToFloat64 `json:"fillPx"` + LastFilledSize convert.StringToFloat64 `json:"fillSz"` + FillTime okxUnixMilliTime `json:"fillTime"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Leverage convert.StringToFloat64 `json:"lever"` + OrderID string `json:"ordId"` + OrderType string `json:"ordType"` + ProfitAndLoss string `json:"pnl"` + PositionSide string `json:"posSide"` + RebateAmount convert.StringToFloat64 `json:"rebate"` + RebateCurrency string `json:"rebateCcy"` + Side order.Side `json:"side"` + StopLossOrdPrice convert.StringToFloat64 `json:"slOrdPx"` + StopLossTriggerPrice convert.StringToFloat64 `json:"slTriggerPx"` + StopLossTriggerPriceType string `json:"slTriggerPxType"` + State string `json:"state"` + Price convert.StringToFloat64 `json:"px"` + Size convert.StringToFloat64 `json:"sz"` + Tag string `json:"tag"` + SizeType string `json:"tgtCcy"` + TradeMode string `json:"tdMode"` + Source string `json:"source"` + TakeProfitOrdPrice convert.StringToFloat64 `json:"tpOrdPx"` + TakeProfitTriggerPrice convert.StringToFloat64 `json:"tpTriggerPx"` + TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` + TradeID string `json:"tradeId"` + UpdateTime okxUnixMilliTime `json:"uTime"` } // TransactionDetailRequestParams retrieve recently-filled transaction details in the last 3 day. @@ -780,7 +780,7 @@ type TransactionDetail struct { Tag string `json:"tag"` FillPrice float64 `json:"fillPx,string"` FillSize float64 `json:"fillSz,string"` - Side string `json:"side"` + Side order.Side `json:"side"` PositionSide string `json:"posSide"` ExecType string `json:"execType"` FeeCurrency string `json:"feeCcy"` @@ -862,7 +862,7 @@ type AlgoOrderResponse struct { AlgoOrderID string `json:"algoId"` Quantity string `json:"sz"` OrderType string `json:"ordType"` - Side string `json:"side"` + Side order.Side `json:"side"` PositionSide string `json:"posSide"` TradeMode string `json:"tdMode"` QuantityType string `json:"tgtCcy"` @@ -1175,7 +1175,7 @@ type EstimateQuoteResponse struct { QuoteTime okxUnixMilliTime `json:"quoteTime"` RfqSize string `json:"rfqSz"` RfqSizeCurrency string `json:"rfqSzCcy"` - Side string `json:"side"` + Side order.Side `json:"side"` TTLMs string `json:"ttlMs"` // Validity period of quotation in milliseconds } @@ -1201,7 +1201,7 @@ type ConvertTradeResponse struct { InstrumentID string `json:"instId"` QuoteCurrency string `json:"quoteCcy"` QuoteID string `json:"quoteId"` - Side string `json:"side"` + Side order.Side `json:"side"` State string `json:"state"` TradeID string `json:"tradeId"` Timestamp okxUnixMilliTime `json:"ts"` @@ -1210,7 +1210,7 @@ type ConvertTradeResponse struct { // ConvertHistory holds convert trade history response type ConvertHistory struct { InstrumentID string `json:"instId"` - Side string `json:"side"` + Side order.Side `json:"side"` FillPrice float64 `json:"fillPx,string"` BaseCurrency string `json:"baseCcy"` QuoteCurrency string `json:"quoteCcy"` @@ -1482,12 +1482,12 @@ type LeverageResponse struct { // MaximumLoanInstrument represents maximum loan of an instrument id. type MaximumLoanInstrument struct { - InstrumentID string `json:"instId"` - MgnMode string `json:"mgnMode"` - MgnCcy string `json:"mgnCcy"` - MaxLoan string `json:"maxLoan"` - Ccy string `json:"ccy"` - Side string `json:"side"` + InstrumentID string `json:"instId"` + MgnMode string `json:"mgnMode"` + MgnCcy string `json:"mgnCcy"` + MaxLoan string `json:"maxLoan"` + Ccy string `json:"ccy"` + Side order.Side `json:"side"` } // TradeFeeRate holds trade fee rate information for a given instrument type. @@ -1564,7 +1564,7 @@ type LoanBorrowAndReplay struct { Currency string `json:"ccy"` LoanQuota string `json:"loanQuota"` PosLoan string `json:"posLoan"` - Side string `json:"side"` + Side string `json:"side"` // borrow or repay UsedLoan string `json:"usedLoan"` } @@ -2374,7 +2374,7 @@ type WSTradeData struct { TradeID string `json:"tradeId"` Price float64 `json:"px,string"` Size float64 `json:"sz,string"` - Side string `json:"side"` + Side order.Side `json:"side"` Timestamp okxUnixMilliTime `json:"ts"` } @@ -2500,16 +2500,16 @@ type WsBalanceAndPosition struct { // WsOrder represents a websocket order. type WsOrder struct { PendingOrderItem - AmendResult string `json:"amendResult"` - Code string `json:"code"` - ExecType string `json:"execType"` - FillFee string `json:"fillFee"` - FillFeeCurrency string `json:"fillFeeCcy"` - FillNationalUsd float64 `json:"fillNationalUsd,string"` - Msg string `json:"msg"` - NationalUSD string `json:"nationalUsd"` - ReduceOnly bool `json:"reduceOnly"` - RequestID string `json:"reqId"` + AmendResult string `json:"amendResult"` + Code string `json:"code"` + ExecType string `json:"execType"` + FillFee convert.StringToFloat64 `json:"fillFee"` + FillFeeCurrency string `json:"fillFeeCcy"` + FillNotionalUsd convert.StringToFloat64 `json:"fillNotionalUsd"` + Msg string `json:"msg"` + NotionalUSD convert.StringToFloat64 `json:"notionalUsd"` + ReduceOnly bool `json:"reduceOnly,string"` + RequestID string `json:"reqId"` } // WsOrderResponse holds order list push data through the websocket connection @@ -2537,7 +2537,7 @@ type WsAlgoOrderDetail struct { TargetCurrency string `json:"tgtCcy"` NotionalUsd string `json:"notionalUsd"` OrderType string `json:"ordType"` - Side string `json:"side"` + Side order.Side `json:"side"` PositionSide string `json:"posSide"` State string `json:"state"` Leverage string `json:"lever"` @@ -2581,7 +2581,7 @@ type WsAdvancedAlgoOrderDetail struct { PriceLimit string `json:"pxLimit"` PriceSpread string `json:"pxSpread"` PriceVariation string `json:"pxVar"` - Side string `json:"side"` + Side order.Side `json:"side"` StopLossOrderPrice string `json:"slOrdPx"` StopLossTriggerPrice string `json:"slTriggerPx"` State string `json:"state"` @@ -2839,7 +2839,7 @@ type GridSubOrderData struct { ProfitAdLoss string `json:"pnl"` PositionSide string `json:"posSide"` Price string `json:"px"` - Side string `json:"side"` + Side order.Side `json:"side"` State string `json:"state"` Size string `json:"sz"` Tag string `json:"tag"` diff --git a/exchanges/okx/okx_websocket.go b/exchanges/okx/okx_websocket.go index d59755f9fdd..4a1eb77c697 100644 --- a/exchanges/okx/okx_websocket.go +++ b/exchanges/okx/okx_websocket.go @@ -1009,18 +1009,13 @@ func (ok *Okx) wsProcessTrades(data []byte) error { if err != nil { return err } - var side order.Side - side, err = order.StringToOrderSide(response.Data[i].Side) - if err != nil { - return err - } for j := range assets { trades = append(trades, trade.Data{ Amount: response.Data[i].Quantity, AssetType: assets[j], CurrencyPair: pair, Exchange: ok.Name, - Side: side, + Side: response.Data[i].Side, Timestamp: response.Data[i].Timestamp.Time(), TID: response.Data[i].TradeID, Price: response.Data[i].Price, @@ -1060,40 +1055,62 @@ func (ok *Okx) wsProcessOrders(respRaw []byte) error { if err != nil { return err } + avgPrice := response.Data[x].AveragePrice.Float64() - orderAmount := response.Data[x].Size + orderAmount := response.Data[x].Size.Float64() + execAmount := response.Data[x].AccumulatedFillSize.Float64() + var quoteAmount float64 - if response.Data[x].QuantityType == "quote_ccy" { + if response.Data[x].SizeType == "quote_ccy" { // Size is quote amount. quoteAmount = orderAmount - if avgPrice > 0 { - orderAmount /= avgPrice + if orderStatus == order.Filled { + // We prefer to take execAmount over calculating from quoteAmount / avgPrice + // because it avoids rounding issues + orderAmount = execAmount } else { - // Size not in Base, and we can't derive a sane value for it - orderAmount = 0 + if avgPrice > 0 { + orderAmount /= avgPrice + } else { + // Size not in Base, and we can't derive a sane value for it + orderAmount = 0 + } } } + var remainingAmount float64 - if orderStatus != order.Filled { - remainingAmount = orderAmount - response.Data[x].AccumulatedFillSize.Float64() + // Float64 rounding may lead to execAmount > orderAmount by a tiny fraction + // noting that the order can be fully executed before it's marked as status Filled + if orderStatus != order.Filled && orderAmount > execAmount { + remainingAmount = orderAmount - execAmount } - ok.Websocket.DataHandler <- &order.Detail{ - Price: response.Data[x].Price, + + d := &order.Detail{ Amount: orderAmount, - QuoteAmount: quoteAmount, - ExecutedAmount: response.Data[x].AccumulatedFillSize.Float64(), - RemainingAmount: remainingAmount, + AssetType: a, AverageExecutedPrice: avgPrice, + ClientOrderID: response.Data[x].ClientOrderID, + Date: response.Data[x].CreationTime.Time(), Exchange: ok.Name, + ExecutedAmount: execAmount, + Fee: 0.0 - response.Data[x].Fee.Float64(), + FeeAsset: response.Data[x].FeeCurrency, OrderID: response.Data[x].OrderID, - ClientOrderID: response.Data[x].ClientOrderID, - Type: orderType, + Pair: pair, + Price: response.Data[x].Price.Float64(), + QuoteAmount: quoteAmount, + RemainingAmount: remainingAmount, Side: response.Data[x].Side, Status: orderStatus, - AssetType: a, - Date: response.Data[x].CreationTime, - Pair: pair, + Type: orderType, + } + if orderStatus == order.Filled { + d.CloseTime = response.Data[x].FillTime.Time() + if d.Amount == 0 { + d.Amount = d.ExecutedAmount + } } + ok.Websocket.DataHandler <- d } return nil } diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index e06df5b410e..034d611ef16 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -684,18 +684,13 @@ func (ok *Okx) GetRecentTrades(ctx context.Context, p currency.Pair, assetType a } resp := make([]trade.Data, len(tradeData)) - var side order.Side for x := range tradeData { - side, err = order.StringToOrderSide(tradeData[x].Side) - if err != nil { - return nil, err - } resp[x] = trade.Data{ TID: tradeData[x].TradeID, Exchange: ok.Name, CurrencyPair: p, AssetType: assetType, - Side: side, + Side: tradeData[x].Side, Price: tradeData[x].Price, Amount: tradeData[x].Quantity, Timestamp: tradeData[x].Timestamp.Time(), @@ -744,11 +739,6 @@ allTrades: // reached end of trades to crawl break allTrades } - var tradeSide order.Side - tradeSide, err = order.StringToOrderSide(trades[i].Side) - if err != nil { - return nil, err - } resp = append(resp, trade.Data{ TID: trades[i].TradeID, Exchange: ok.Name, @@ -757,7 +747,7 @@ allTrades: Price: trades[i].Price, Amount: trades[i].Quantity, Timestamp: trades[i].Timestamp.Time(), - Side: tradeSide, + Side: trades[i].Side, }) } tradeIDEnd = trades[len(trades)-1].TradeID diff --git a/exchanges/okx/testdata/wsOrders.json b/exchanges/okx/testdata/wsOrders.json new file mode 100644 index 00000000000..c043280dd63 --- /dev/null +++ b/exchanges/okx/testdata/wsOrders.json @@ -0,0 +1,4 @@ +{"arg":{"channel":"orders","instType":"SPOT","instId":"BTC-USDT","uid":"614488474791936"},"data":[{"accFillSz":"0.001","amendResult":"","avgPx":"31527.1","cTime":"1654084334977","category":"normal","ccy":"","clOrdId":"HamsterParty14","code":"0","execType":"M","fee":"-0.02522168","feeCcy":"USDT","fillFee":"-0.02522168","fillFeeCcy":"USDT","fillNotionalUsd":"31.50818374","fillPx":"31527.1","fillSz":"0.001","fillTime":"1654084353263","instId":"BTC-USDT","instType":"SPOT","lever":"0","msg":"","notionalUsd":"31.50818374","ordId":"452197707845865472","ordType":"limit","pnl":"0","posSide":"","px":"31527.1","rebate":"0","rebateCcy":"BTC","reduceOnly":"false","reqId":"","side":"sell","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"last","source":"","state":"filled","sz":"0.001","tag":"","tdMode":"cash","tgtCcy":"","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"last","tradeId":"242589207","uTime":"1654084353264"}]} +{"arg":{"channel":"orders","instType":"SPOT","uid":"448743607034327908"},"data":[{"accFillSz":"0","algoClOrdId":"","algoId":"","amendResult":"","amendSource":"","attachAlgoClOrdId":"","avgPx":"0","cTime":"1694153250532","cancelSource":"","category":"normal","ccy":"","clOrdId":"","code":"0","execType":"","fee":"0","feeCcy":"USDT","fillFee":"0","fillFeeCcy":"","fillFwdPx":"","fillMarkPx":"","fillMarkVol":"","fillNotionalUsd":"","fillPnl":"0","fillPx":"","fillPxUsd":"","fillPxVol":"","fillSz":"0","fillTime":"","instId":"BTC-USDT","instType":"SPOT","lever":"0","msg":"","notionalUsd":"10.000599999999999","ordId":"620258920632008725","ordType":"market","pnl":"0","posSide":"","px":"","pxType":"","pxUsd":"","pxVol":"","quickMgnType":"","rebate":"0","rebateCcy":"BTC","reduceOnly":"false","reqId":"","side":"sell","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"","source":"","state":"live","stpId":"","stpMode":"","sz":"10","tag":"","tdMode":"cash","tgtCcy":"quote_ccy","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"","tradeId":"","uTime":"1694153250532"}]} +{"arg":{"channel":"orders","instType":"SPOT","uid":"448743607034327908"},"data":[{"accFillSz":"0.00038128","algoClOrdId":"","algoId":"","amendResult":"","amendSource":"","attachAlgoClOrdId":"","avgPx":"26228.1","cTime":"1694153250532","cancelSource":"","category":"normal","ccy":"","clOrdId":"","code":"0","execType":"T","fee":"-0.010000249968","feeCcy":"USDT","fillFee":"-0.010000249968","fillFeeCcy":"USDT","fillFwdPx":"","fillMarkPx":"","fillMarkVol":"","fillNotionalUsd":"10.00084998299808","fillPnl":"0","fillPx":"26228.1","fillPxUsd":"","fillPxVol":"","fillSz":"0.00038128","fillTime":"1694153250535","instId":"BTC-USDT","instType":"SPOT","lever":"0","msg":"","notionalUsd":"10.000599999999999","ordId":"620258920632008725","ordType":"market","pnl":"0","posSide":"","px":"","pxType":"","pxUsd":"","pxVol":"","quickMgnType":"","rebate":"0","rebateCcy":"BTC","reduceOnly":"false","reqId":"","side":"sell","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"","source":"","state":"partially_filled","stpId":"","stpMode":"","sz":"10","tag":"","tdMode":"cash","tgtCcy":"quote_ccy","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"","tradeId":"435550732","uTime":"1694153250535"}]} +{"arg":{"channel":"orders","instType":"SPOT","uid":"448743607034327908"},"data":[{"accFillSz":"0.00038128","algoClOrdId":"","algoId":"","amendResult":"","amendSource":"","attachAlgoClOrdId":"","avgPx":"26228.1","cTime":"1694153250532","cancelSource":"","category":"normal","ccy":"","clOrdId":"","code":"0","execType":"","fee":"-0.010000249968","feeCcy":"USDT","fillFee":"0","fillFeeCcy":"","fillFwdPx":"","fillMarkPx":"","fillMarkVol":"","fillNotionalUsd":"10.00084998299808","fillPnl":"0","fillPx":"","fillPxUsd":"","fillPxVol":"","fillSz":"0","fillTime":"","instId":"BTC-USDT","instType":"SPOT","lever":"0","msg":"","notionalUsd":"10.000599999999999","ordId":"620258920632008725","ordType":"market","pnl":"0","posSide":"","px":"","pxType":"","pxUsd":"","pxVol":"","quickMgnType":"","rebate":"0","rebateCcy":"BTC","reduceOnly":"false","reqId":"","side":"sell","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"","source":"","state":"filled","stpId":"","stpMode":"","sz":"10","tag":"","tdMode":"cash","tgtCcy":"quote_ccy","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"","tradeId":"","uTime":"1694153250535"}]} diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 8c5f1dff421..c4fb0a14a0e 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -1,6 +1,7 @@ package order import ( + "encoding/json" "errors" "fmt" "reflect" @@ -10,6 +11,7 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -2045,3 +2047,13 @@ func TestAdjustQuoteAmount(t *testing.T) { t.Fatalf("received: '%v' but expected: '%v'", s.Amount, 5.22222222) } } + +func TestSideUnmarshal(t *testing.T) { + t.Parallel() + var s Side + assert.Nil(t, s.UnmarshalJSON([]byte(`"SELL"`)), "Quoted valid side okay") + assert.Equal(t, Sell, s, "Correctly set order Side") + assert.ErrorIs(t, s.UnmarshalJSON([]byte(`"STEAL"`)), ErrSideIsInvalid, "Quoted invalid side errors") + var jErr *json.UnmarshalTypeError + assert.ErrorAs(t, s.UnmarshalJSON([]byte(`14`)), &jErr, "non-string valid json is rejected") +} diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 077ec7eff20..98400f66028 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -1,8 +1,11 @@ package order import ( + "bytes" + "encoding/json" "errors" "fmt" + "reflect" "sort" "strings" "time" @@ -1060,6 +1063,18 @@ func StringToOrderSide(side string) (Side, error) { } } +// UnmarshalJSON parses the JSON-encoded order side and stores the result +// It expects a quoted string input, and uses StringToOrderSide to parse it +func (s *Side) UnmarshalJSON(data []byte) (err error) { + if !bytes.HasPrefix(data, []byte(`"`)) { + // Note that we don't need to worry about invalid JSON here, it wouldn't have made it past the deserialiser far + // TODO: Can use reflect.TypeFor[s]() when it's released, probably 1.21 + return &json.UnmarshalTypeError{Value: string(data), Type: reflect.TypeOf(s), Offset: 1} + } + *s, err = StringToOrderSide(string(data[1 : len(data)-1])) // Remove quotes + return +} + // StringToOrderType for converting case insensitive order type // and returning a real Type func StringToOrderType(oType string) (Type, error) { diff --git a/exchanges/sharedtestvalues/sharedtestvalues.go b/exchanges/sharedtestvalues/sharedtestvalues.go index 8d0cce6134e..a8d105198d4 100644 --- a/exchanges/sharedtestvalues/sharedtestvalues.go +++ b/exchanges/sharedtestvalues/sharedtestvalues.go @@ -154,6 +154,7 @@ func ForceFileStandard(t *testing.T, pattern string) error { // TestFixtureToDataHandler takes a new empty exchange and configures a new websocket handler for it, and squirts the json path contents to it // It accepts a reader function, which is probably e.wsHandleData but could be anything func TestFixtureToDataHandler(t *testing.T, seed, e exchange.IBotExchange, fixturePath string, reader func([]byte) error) { + t.Helper() b := e.GetBase() seedBase := seed.GetBase() From 859c4512fb06d654a31304dee82581a1ef83feff Mon Sep 17 00:00:00 2001 From: Bea <103050835+Beadko@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:34:21 +0700 Subject: [PATCH 08/40] Bitstamp: Add new trading fees API endpoint; refine fee calcuations and test coverage (#1289) * Bitstamp: WIP fixing trading fees * Bitstamp/amended the TestGetFee test and currected the getTradingFee function * Bitstamp: TestGetFee implemented for maker and taker * Bitfinex: added a wrapped error * Bitstamp: GetTradingFee and test updated, fetched from the API Progresses #1271 * Bitstamp: minor changes- whitespace removal and comment added * Bitstamp: fixed lint issues in TestGetBalance and TestGetTransactions * Bitstamp:Typo in types TradingFee comment * Bitstamp: GetAccountTradingFees at view the fees on all pairs modified: exchanges/bitstamp/bitstamp.go * Bitstamp: returning the TradingFee info instead of just MakerTakerFees * Bitstamp:TestGetAccountTradingFees + TestGetAccountTradingFees added,data structure amended to match the outcome * Bitstamp:error and a test for empty pair added in GetAccountTradingFee * Bitstamp: RM the whitespace linter error --- exchanges/bitstamp/bitstamp.go | 77 ++++++++++++----------- exchanges/bitstamp/bitstamp_live_test.go | 12 +++- exchanges/bitstamp/bitstamp_test.go | 72 ++++++++++++++------- exchanges/bitstamp/bitstamp_types.go | 15 ++++- testdata/http_mock/bitstamp/bitstamp.json | 50 +++++++++++++++ 5 files changed, 158 insertions(+), 68 deletions(-) diff --git a/exchanges/bitstamp/bitstamp.go b/exchanges/bitstamp/bitstamp.go index e30f98e3abf..c279bed0f0c 100644 --- a/exchanges/bitstamp/bitstamp.go +++ b/exchanges/bitstamp/bitstamp.go @@ -29,6 +29,7 @@ const ( bitstampAPIOrderbook = "order_book" bitstampAPITransactions = "transactions" bitstampAPIEURUSD = "eur_usd" + bitstampAPITradingFees = "fees/trading" bitstampAPIBalance = "balance" bitstampAPIUserTransactions = "user_transactions" bitstampAPIOHLC = "ohlc" @@ -67,15 +68,11 @@ func (b *Bitstamp) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) switch feeBuilder.FeeType { case exchange.CryptocurrencyTradeFee: - balance, err := b.GetBalance(ctx) + tradingFee, err := b.getTradingFee(ctx, feeBuilder) if err != nil { - return 0, err + return 0, fmt.Errorf("error getting trading fee: %w", err) } - fee = b.CalculateTradingFee(feeBuilder.Pair.Base, - feeBuilder.Pair.Quote, - feeBuilder.PurchasePrice, - feeBuilder.Amount, - balance) + fee = tradingFee case exchange.CryptocurrencyDepositFee: fee = 0 case exchange.InternationalBankDepositFee: @@ -91,6 +88,41 @@ func (b *Bitstamp) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) return fee, nil } +// GetTradingFee returns a trading fee based on a currency +func (b *Bitstamp) getTradingFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { + tradingFees, err := b.GetAccountTradingFee(ctx, feeBuilder.Pair) + if err != nil { + return 0, err + } + fees := tradingFees.Fees + fee := fees.Taker + if feeBuilder.IsMaker { + fee = fees.Maker + } + return fee / 100 * feeBuilder.PurchasePrice * feeBuilder.Amount, nil +} + +// GetAccountTradingFee returns a TradingFee for a pair +func (b *Bitstamp) GetAccountTradingFee(ctx context.Context, pair currency.Pair) (TradingFees, error) { + path := bitstampAPITradingFees + "/" + strings.ToLower(pair.String()) + + var resp TradingFees + if pair.IsEmpty() { + return resp, currency.ErrCurrencyPairEmpty + } + err := b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, path, true, nil, &resp) + + return resp, err +} + +// GetAccountTradingFees returns a slice of TradingFee +func (b *Bitstamp) GetAccountTradingFees(ctx context.Context) ([]TradingFees, error) { + path := bitstampAPITradingFees + var resp []TradingFees + err := b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, path, true, nil, &resp) + return resp, err +} + // getOfflineTradeFee calculates the worst case-scenario trading fee func getOfflineTradeFee(price, amount float64) float64 { return 0.0025 * price * amount @@ -119,22 +151,6 @@ func getInternationalBankDepositFee(amount float64) float64 { return fee } -// CalculateTradingFee returns fee on a currency pair -func (b *Bitstamp) CalculateTradingFee(base, quote currency.Code, purchasePrice, amount float64, balances Balances) float64 { - var fee float64 - if v, ok := balances[base.String()]; ok { - switch quote { - case currency.BTC: - fee = v.BTCFee - case currency.USD: - fee = v.USDFee - case currency.EUR: - fee = v.EURFee - } - } - return fee * purchasePrice * amount -} - // GetTicker returns ticker information func (b *Bitstamp) GetTicker(ctx context.Context, currency string, hourly bool) (*Ticker, error) { response := Ticker{} @@ -251,21 +267,6 @@ func (b *Bitstamp) GetBalance(ctx context.Context) (Balances, error) { Reserved: reserved, WithdrawalFee: withdrawalFee, } - switch strings.ToUpper(curr) { - case currency.USD.String(): - eurFee, _ := strconv.ParseFloat(balance[curr+"eur_fee"], 64) - currBalance.EURFee = eurFee - case currency.EUR.String(): - usdFee, _ := strconv.ParseFloat(balance[curr+"usd_fee"], 64) - currBalance.USDFee = usdFee - default: - btcFee, _ := strconv.ParseFloat(balance[curr+"btc_fee"], 64) - currBalance.BTCFee = btcFee - eurFee, _ := strconv.ParseFloat(balance[curr+"eur_fee"], 64) - currBalance.EURFee = eurFee - usdFee, _ := strconv.ParseFloat(balance[curr+"usd_fee"], 64) - currBalance.USDFee = usdFee - } balances[strings.ToUpper(curr)] = currBalance } return balances, nil diff --git a/exchanges/bitstamp/bitstamp_live_test.go b/exchanges/bitstamp/bitstamp_live_test.go index b13a2b34fae..1623c82aeb5 100644 --- a/exchanges/bitstamp/bitstamp_live_test.go +++ b/exchanges/bitstamp/bitstamp_live_test.go @@ -26,9 +26,15 @@ func TestMain(m *testing.M) { log.Fatal("Bitstamp Setup() init error", err) } bitstampConfig.API.AuthenticatedSupport = true - bitstampConfig.API.Credentials.Key = apiKey - bitstampConfig.API.Credentials.Secret = apiSecret - bitstampConfig.API.Credentials.ClientID = customerID + if apiKey != "" { + bitstampConfig.API.Credentials.Key = apiKey + } + if apiSecret != "" { + bitstampConfig.API.Credentials.Secret = apiSecret + } + if customerID != "" { + bitstampConfig.API.Credentials.ClientID = customerID + } b.SetDefaults() b.Websocket = sharedtestvalues.NewTestWebsocket() err = b.Setup(bitstampConfig) diff --git a/exchanges/bitstamp/bitstamp_test.go b/exchanges/bitstamp/bitstamp_test.go index 70e717bb4e2..6234d53c36f 100644 --- a/exchanges/bitstamp/bitstamp_test.go +++ b/exchanges/bitstamp/bitstamp_test.go @@ -33,10 +33,10 @@ var b = &Bitstamp{} func setFeeBuilder() *exchange.FeeBuilder { return &exchange.FeeBuilder{ - Amount: 1, + Amount: 5, FeeType: exchange.CryptocurrencyTradeFee, - Pair: currency.NewPair(currency.BTC, currency.LTC), - PurchasePrice: 1, + Pair: currency.NewPair(currency.LTC, currency.BTC), + PurchasePrice: 1800, } } @@ -99,8 +99,21 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if _, err := b.GetFee(context.Background(), feeBuilder); err != nil { + fee, err := b.GetFee(context.Background(), feeBuilder) + if err != nil { t.Error(err) + } else if expected := 0.003 * feeBuilder.PurchasePrice * feeBuilder.Amount; fee != expected { + t.Errorf("Bitstamp GetFee wrong Maker fee; Pair: %s Expected: %v Got: %v", feeBuilder.Pair, expected, fee) + } + + // CryptocurrencyTradeFee IsTaker + feeBuilder = setFeeBuilder() + feeBuilder.IsMaker = false + fee, err = b.GetFee(context.Background(), feeBuilder) + if err != nil { + t.Error(err) + } else if expected := 0.002 * feeBuilder.PurchasePrice * feeBuilder.Amount; fee != expected { + t.Errorf("Bitstamp GetFee wrong Taker fee; Pair: %s Expected: %v Got: %v", feeBuilder.Pair, expected, fee) } // CryptocurrencyTradeFee Negative purchase price @@ -141,28 +154,44 @@ func TestGetFee(t *testing.T) { } } -func TestCalculateTradingFee(t *testing.T) { +func TestGetAccountTradingFee(t *testing.T) { t.Parallel() - newBalance := make(Balances) - newBalance["BTC"] = Balance{ - USDFee: 1, - EURFee: 0, + if !mockTests { + sharedtestvalues.SkipTestIfCredentialsUnset(t, b) } - if resp := b.CalculateTradingFee(currency.BTC, currency.USD, 0, 0, newBalance); resp != 0 { - t.Error("GetFee() error") - } - if resp := b.CalculateTradingFee(currency.BTC, currency.USD, 2, 2, newBalance); resp != float64(4) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(4), resp) + fee, err := b.GetAccountTradingFee(context.Background(), currency.NewPair(currency.LTC, currency.BTC)) + if assert.NoError(t, err, "GetAccountTradingFee should not error") { + if mockTests { + assert.Positive(t, fee.Fees.Maker, "Maker should be positive") + assert.Positive(t, fee.Fees.Taker, "Taker should be positive") + } + assert.NotEmpty(t, fee.Symbol, "Symbol should not be empty") + assert.Equal(t, fee.Symbol, "ltcbtc", "Symbol should be correct") } - if resp := b.CalculateTradingFee(currency.BTC, currency.EUR, 2, 2, newBalance); resp != float64(0) { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) + + _, err = b.GetAccountTradingFee(context.Background(), currency.EMPTYPAIR) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty, "Should get back the right error") +} + +func TestGetAccountTradingFees(t *testing.T) { + t.Parallel() + + if !mockTests { + sharedtestvalues.SkipTestIfCredentialsUnset(t, b) } - dummy1, dummy2 := currency.NewCode(""), currency.NewCode("") - if resp := b.CalculateTradingFee(dummy1, dummy2, 0, 0, newBalance); resp != 0 { - t.Error("GetFee() error") + fees, err := b.GetAccountTradingFees(context.Background()) + if assert.NoError(t, err, "GetAccountTradingFee should not error") { + if assert.Positive(t, len(fees), "Should get back multiple fees") { + fee := fees[0] + assert.NotEmpty(t, fee.Symbol, "Should get back a symbol") + if mockTests { + assert.Positive(t, fee.Fees.Maker, "Maker should be positive") + assert.Positive(t, fee.Fees.Taker, "Taker should be positive") + } + } } } @@ -287,14 +316,12 @@ func TestGetBalance(t *testing.T) { Balance: 1337.42, Reserved: 1295.00, WithdrawalFee: 5.0, - USDFee: 0, }, "BTC": { Available: 9.1, Balance: 11.2, Reserved: 2.1, WithdrawalFee: 0.00050000, - USDFee: 0.25, }, } { if got, ok := bal[k]; !ok { @@ -312,9 +339,6 @@ func TestGetBalance(t *testing.T) { if got.WithdrawalFee != e.WithdrawalFee { t.Errorf("Incorrect WithdrawalFee for %s; Expected: %v Got: %v", k, e.WithdrawalFee, got.WithdrawalFee) } - if got.USDFee != e.USDFee { - t.Errorf("Incorrect USDFee for %s; Expected: %v Got: %v", k, e.USDFee, got.USDFee) - } } } } diff --git a/exchanges/bitstamp/bitstamp_types.go b/exchanges/bitstamp/bitstamp_types.go index a544e9e6778..723c039423a 100644 --- a/exchanges/bitstamp/bitstamp_types.go +++ b/exchanges/bitstamp/bitstamp_types.go @@ -75,15 +75,24 @@ type EURUSDConversionRate struct { Sell float64 `json:"sell,string"` } +// TradingFees holds trading fee information +type TradingFees struct { + Symbol string `json:"currency_pair"` + Fees MakerTakerFees `json:"fees"` +} + +// MakerTakerFees holds maker and taker fee information +type MakerTakerFees struct { + Maker float64 `json:"maker,string"` + Taker float64 `json:"taker,string"` +} + // Balance stores the balance info type Balance struct { Available float64 Balance float64 Reserved float64 WithdrawalFee float64 - BTCFee float64 // for cryptocurrency pairs - USDFee float64 - EURFee float64 } // Balances holds full balance information with the supplied APIKEYS diff --git a/testdata/http_mock/bitstamp/bitstamp.json b/testdata/http_mock/bitstamp/bitstamp.json index 373a21bdf6c..86f8879f434 100644 --- a/testdata/http_mock/bitstamp/bitstamp.json +++ b/testdata/http_mock/bitstamp/bitstamp.json @@ -148,6 +148,56 @@ } ] }, + "/api/v2/fees/trading/ltcbtc/": { + "POST": [ + { + "data": { + "currency_pair": "ltcbtc", + "fees": + { + "maker": "0.3", + "taker": "0.2" + } + }, + "queryString": "", + "bodyParams": "key=\u0026nonce=1690610275327495000\u0026signature=B619E1AEF317B95D3BB28025F36EEF94CABCDE425CE3E13E8A1861C0A9777F2A", + "headers": { + "Content-Type": [ + "application/x-www-form-urlencoded" + ] + } + } + ] +}, +"/api/v2/fees/trading/": { + "POST": [ + { + "data": [ + { + "currency_pair":"ltcbtc", + "fees": { + "maker": "0.3", + "taker": "0.2" + } + }, + { + "currency_pair":"btcusd", + "fees": { + "maker": "0.3", + "taker": "0.2" + } + } + ], + "queryString": "", + "bodyParams": "key=&nonce=1696325404980771000&signature=85B2B7E60567E352241BD98BDD08C0A435C2A77CB8F386739DD471BFFFA6C660", + "headers": { + "Content-Type": [ + "application/x-www-form-urlencoded" + ] + } + } + ] + }, "/api/v2/buy/market/btcusd/": { "POST": [ { From 773441d5a7cd1323b1f2b28be1ad77233962e663 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Fri, 13 Oct 2023 15:54:49 +1100 Subject: [PATCH 09/40] Okx: Integrate websocket book5 processing and add configurable max subscriptions per connection (#1275) * okx: books 5 (cherry-pick) * okx: shift types to types file, remove commented code and updated field name to better reflect pushed type * linter: fix * remove slowness * * Introduce function checksubscriptions and shift check of subscriptions to internal websocket package * Shift Max websocket connection int to Websocket setup (temp) for this use case only. * glorious: nits * linter: fix * websocket: don't try and subscribed with nothing to subscribe to. * Update exchanges/stream/websocket_test.go Co-authored-by: Scott * glorious: nits --------- Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott --- engine/websocketroutine_manager.go | 79 ++++++++++++--------------- exchanges/okx/okx_test.go | 39 +++++++++++++ exchanges/okx/okx_type_convert.go | 17 ------ exchanges/okx/okx_types.go | 18 ++++++ exchanges/okx/okx_websocket.go | 85 +++++++++++++++++++++++------ exchanges/okx/okx_wrapper.go | 17 +++--- exchanges/stream/websocket.go | 72 +++++++++++++++++------- exchanges/stream/websocket_test.go | 39 ++++++++++++- exchanges/stream/websocket_types.go | 8 +++ 9 files changed, 270 insertions(+), 104 deletions(-) diff --git a/engine/websocketroutine_manager.go b/engine/websocketroutine_manager.go index 55bb744285a..6494b5e1542 100644 --- a/engine/websocketroutine_manager.go +++ b/engine/websocketroutine_manager.go @@ -106,54 +106,47 @@ func (m *WebsocketRoutineManager) websocketRoutine() { if err != nil { log.Errorf(log.WebsocketMgr, "websocket routine manager cannot get exchanges: %v", err) } - wg := sync.WaitGroup{} - wg.Add(len(exchanges)) - for i := range exchanges { - go func(i int) { - defer wg.Done() - if exchanges[i].SupportsWebsocket() { - if m.verbose { - log.Debugf(log.WebsocketMgr, - "Exchange %s websocket support: Yes Enabled: %v", - exchanges[i].GetName(), - common.IsEnabled(exchanges[i].IsWebsocketEnabled()), - ) - } + var wg sync.WaitGroup + for _, exch := range exchanges { + if !exch.SupportsWebsocket() { + if m.verbose { + log.Debugf(log.WebsocketMgr, "Exchange %s websocket support: No", + exch.GetName()) + } + continue + } - ws, err := exchanges[i].GetWebsocket() - if err != nil { - log.Errorf( - log.WebsocketMgr, - "Exchange %s GetWebsocket error: %s", - exchanges[i].GetName(), - err, - ) - return - } + if m.verbose { + log.Debugf(log.WebsocketMgr, "Exchange %s websocket support: Yes Enabled: %v", + exch.GetName(), + common.IsEnabled(exch.IsWebsocketEnabled())) + } - if ws.IsEnabled() { - err = ws.Connect() - if err != nil { - log.Errorf(log.WebsocketMgr, "%v", err) - } + ws, err := exch.GetWebsocket() + if err != nil { + log.Errorf(log.WebsocketMgr, "Exchange %s GetWebsocket error: %s", + exch.GetName(), + err) + continue + } - err = m.websocketDataReceiver(ws) - if err != nil { - log.Errorf(log.WebsocketMgr, "%v", err) - } + if !ws.IsEnabled() { + continue + } - err = ws.FlushChannels() - if err != nil { - log.Errorf(log.WebsocketMgr, "Failed to subscribe: %v", err) - } - } - } else if m.verbose { - log.Debugf(log.WebsocketMgr, - "Exchange %s websocket support: No", - exchanges[i].GetName(), - ) + wg.Add(1) + go func() { + defer wg.Done() + err = ws.Connect() + if err != nil { + log.Errorf(log.WebsocketMgr, "%v", err) + } + + err = m.websocketDataReceiver(ws) + if err != nil { + log.Errorf(log.WebsocketMgr, "%v", err) } - }(i) + }() } wg.Wait() } diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index e2eb5d2439d..6e9f452d032 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -3646,3 +3646,42 @@ func TestGetFuturesContractDetails(t *testing.T) { t.Error(err) } } + +func TestWsProcessOrderbook5(t *testing.T) { + t.Parallel() + + var ob5payload = []byte(`{"arg":{"channel":"books5","instId":"OKB-USDT"},"data":[{"asks":[["0.0000007465","2290075956","0","4"],["0.0000007466","1747284705","0","4"],["0.0000007467","1338861655","0","3"],["0.0000007468","1661668387","0","6"],["0.0000007469","2715477116","0","5"]],"bids":[["0.0000007464","15693119","0","1"],["0.0000007463","2330835024","0","4"],["0.0000007462","1182926517","0","2"],["0.0000007461","3818684357","0","4"],["0.000000746","6021641435","0","7"]],"instId":"OKB-USDT","ts":"1695864901807","seqId":4826378794}]}`) + err := ok.wsProcessOrderbook5(ob5payload) + if err != nil { + t.Error(err) + } + + required := currency.NewPairWithDelimiter("OKB", "USDT", "-") + + got, err := orderbook.Get("okx", required, asset.Spot) + if err != nil { + t.Fatal(err) + } + + if len(got.Asks) != 5 { + t.Errorf("expected %v, received %v", 5, len(got.Asks)) + } + + if len(got.Bids) != 5 { + t.Errorf("expected %v, received %v", 5, len(got.Bids)) + } + + // Book replicated to margin + got, err = orderbook.Get("okx", required, asset.Margin) + if err != nil { + t.Fatal(err) + } + + if len(got.Asks) != 5 { + t.Errorf("expected %v, received %v", 5, len(got.Asks)) + } + + if len(got.Bids) != 5 { + t.Errorf("expected %v, received %v", 5, len(got.Bids)) + } +} diff --git a/exchanges/okx/okx_type_convert.go b/exchanges/okx/okx_type_convert.go index 4d911327eae..89c5db9f20b 100644 --- a/exchanges/okx/okx_type_convert.go +++ b/exchanges/okx/okx_type_convert.go @@ -6,7 +6,6 @@ import ( "strings" "time" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) @@ -41,22 +40,6 @@ func (a *okxNumericalValue) Float64() float64 { return float64(*a) } type okxUnixMilliTime int64 -type okxAssetType struct { - asset.Item -} - -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *okxAssetType) UnmarshalJSON(data []byte) error { - var t string - err := json.Unmarshal(data, &t) - if err != nil { - return err - } - - a.Item = GetAssetTypeFromInstrumentType(strings.ToUpper(t)) - return nil -} - // UnmarshalJSON deserializes byte data to okxunixMilliTime instance. func (a *okxUnixMilliTime) UnmarshalJSON(data []byte) error { var num string diff --git a/exchanges/okx/okx_types.go b/exchanges/okx/okx_types.go index 54b4aa2cfa0..47509dcfe9a 100644 --- a/exchanges/okx/okx_types.go +++ b/exchanges/okx/okx_types.go @@ -3189,3 +3189,21 @@ type wsSubscriptionParameters struct { Underlying bool Currency bool } + +// WsOrderbook5 stores the orderbook data for orderbook 5 websocket +type WsOrderbook5 struct { + Argument struct { + Channel string `json:"channel"` + InstrumentID string `json:"instId"` + } `json:"arg"` + Data []Book5Data `json:"data"` +} + +// Book5Data stores the orderbook data for orderbook 5 websocket +type Book5Data struct { + Asks [][4]string `json:"asks"` + Bids [][4]string `json:"bids"` + InstrumentID string `json:"instId"` + TimestampMilli int64 `json:"ts,string"` + SequenceID int64 `json:"seqId"` +} diff --git a/exchanges/okx/okx_websocket.go b/exchanges/okx/okx_websocket.go index 4a1eb77c697..dee1cd884f7 100644 --- a/exchanges/okx/okx_websocket.go +++ b/exchanges/okx/okx_websocket.go @@ -359,15 +359,8 @@ func (ok *Okx) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) e // handleSubscription sends a subscription and unsubscription information thought the websocket endpoint. // as of the okx, exchange this endpoint sends subscription and unsubscription messages but with a list of json objects. func (ok *Okx) handleSubscription(operation string, subscriptions []stream.ChannelSubscription) error { - request := WSSubscriptionInformationList{ - Operation: operation, - Arguments: []SubscriptionInfo{}, - } - - authRequests := WSSubscriptionInformationList{ - Operation: operation, - Arguments: []SubscriptionInfo{}, - } + request := WSSubscriptionInformationList{Operation: operation} + authRequests := WSSubscriptionInformationList{Operation: operation} ok.WsRequestSemaphore <- 1 defer func() { <-ok.WsRequestSemaphore }() var channels []stream.ChannelSubscription @@ -650,8 +643,9 @@ func (ok *Okx) WsHandleData(respRaw []byte) error { okxChannelPriceLimit: var response WsMarkPrice return ok.wsProcessPushData(respRaw, &response) + case okxChannelOrderBooks5: + return ok.wsProcessOrderbook5(respRaw) case okxChannelOrderBooks, - okxChannelOrderBooks5, okxChannelOrderBooks50TBT, okxChannelBBOTBT, okxChannelOrderBooksTBT: @@ -738,11 +732,74 @@ func (ok *Okx) wsProcessIndexCandles(respRaw []byte) error { return nil } +// wsProcessOrderbook5 processes orderbook data +func (ok *Okx) wsProcessOrderbook5(data []byte) error { + var resp WsOrderbook5 + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + + if len(resp.Data) != 1 { + return fmt.Errorf("%s - no data returned", ok.Name) + } + + assets, err := ok.GetAssetsFromInstrumentTypeOrID("", resp.Argument.InstrumentID) + if err != nil { + return err + } + + pair, err := ok.GetPairFromInstrumentID(resp.Argument.InstrumentID) + if err != nil { + return err + } + + asks := make([]orderbook.Item, len(resp.Data[0].Asks)) + for x := range resp.Data[0].Asks { + asks[x].Price, err = strconv.ParseFloat(resp.Data[0].Asks[x][0], 64) + if err != nil { + return err + } + + asks[x].Amount, err = strconv.ParseFloat(resp.Data[0].Asks[x][1], 64) + if err != nil { + return err + } + } + + bids := make([]orderbook.Item, len(resp.Data[0].Bids)) + for x := range resp.Data[0].Bids { + bids[x].Price, err = strconv.ParseFloat(resp.Data[0].Bids[x][0], 64) + if err != nil { + return err + } + + bids[x].Amount, err = strconv.ParseFloat(resp.Data[0].Bids[x][1], 64) + if err != nil { + return err + } + } + + for x := range assets { + err = ok.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ + Asset: assets[x], + Asks: asks, + Bids: bids, + LastUpdated: time.UnixMilli(resp.Data[0].TimestampMilli), + Pair: pair, + Exchange: ok.Name, + VerifyOrderbook: ok.CanVerifyOrderbook}) + if err != nil { + return err + } + } + return nil +} + // wsProcessOrderBooks processes "snapshot" and "update" order book func (ok *Okx) wsProcessOrderBooks(data []byte) error { var response WsOrderBook - var err error - err = json.Unmarshal(data, &response) + err := json.Unmarshal(data, &response) if err != nil { return err } @@ -1277,10 +1334,6 @@ func (ok *Okx) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, err }) } } - if len(subscriptions) >= 240 { - log.Warnf(log.WebsocketMgr, "OKx has 240 subscription limit, only subscribing within limit. Requested %v", len(subscriptions)) - subscriptions = subscriptions[:239] - } return subscriptions, nil } diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index 034d611ef16..da66dad4742 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -214,14 +214,15 @@ func (ok *Okx) Setup(exch *config.Exchange) error { return err } if err := ok.Websocket.Setup(&stream.WebsocketSetup{ - ExchangeConfig: exch, - DefaultURL: okxAPIWebsocketPublicURL, - RunningURL: wsRunningEndpoint, - Connector: ok.WsConnect, - Subscriber: ok.Subscribe, - Unsubscriber: ok.Unsubscribe, - GenerateSubscriptions: ok.GenerateDefaultSubscriptions, - Features: &ok.Features.Supports.WebsocketCapabilities, + ExchangeConfig: exch, + DefaultURL: okxAPIWebsocketPublicURL, + RunningURL: wsRunningEndpoint, + Connector: ok.WsConnect, + Subscriber: ok.Subscribe, + Unsubscriber: ok.Unsubscribe, + GenerateSubscriptions: ok.GenerateDefaultSubscriptions, + Features: &ok.Features.Supports.WebsocketCapabilities, + MaxWebsocketSubscriptionsPerConnection: 240, OrderbookBufferConfig: buffer.Config{ Checksum: ok.CalculateUpdateOrderbookChecksum, }, diff --git a/exchanges/stream/websocket.go b/exchanges/stream/websocket.go index 4a6a318a385..11f692a11d8 100644 --- a/exchanges/stream/websocket.go +++ b/exchanges/stream/websocket.go @@ -10,6 +10,7 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -48,6 +49,10 @@ var ( errWebsocketConnectorUnset = errors.New("websocket connector function not set") errWebsocketSubscriptionsGeneratorUnset = errors.New("websocket subscriptions generator function needs to be set") errClosedConnection = errors.New("use of closed network connection") + errSubscriptionsExceedsLimit = errors.New("subscriptions exceeds limit") + errInvalidMaxSubscriptions = errors.New("max subscriptions cannot be less than 0") + errNoSubscriptionsSupplied = errors.New("no subscriptions supplied") + errChannelSubscriptionAlreadySubscribed = errors.New("channel subscription already subscribed") ) var globalReporter Reporter @@ -167,6 +172,11 @@ func (w *Websocket) Setup(s *WebsocketSetup) error { w.Trade.Setup(w.exchangeName, s.TradeFeed, w.DataHandler) w.Fills.Setup(s.FillsFeed, w.DataHandler) + + if s.MaxWebsocketSubscriptionsPerConnection < 0 { + return fmt.Errorf("%s %w", w.exchangeName, errInvalidMaxSubscriptions) + } + w.MaxSubscriptionsPerConnection = s.MaxWebsocketSubscriptionsPerConnection return nil } @@ -275,11 +285,18 @@ func (w *Websocket) Connect() error { subs, err := w.GenerateSubs() // regenerate state on new connection if err != nil { - return fmt.Errorf("%v %w: %v", w.exchangeName, ErrSubscriptionFailure, err) + return fmt.Errorf("%s websocket: %w", w.exchangeName, common.AppendError(ErrSubscriptionFailure, err)) + } + if len(subs) == 0 { + return nil + } + err = w.checkSubscriptions(subs) + if err != nil { + return fmt.Errorf("%s websocket: %w", w.exchangeName, common.AppendError(ErrSubscriptionFailure, err)) } err = w.Subscriber(subs) if err != nil { - return fmt.Errorf("%v %w: %v", w.exchangeName, ErrSubscriptionFailure, err) + return fmt.Errorf("%s websocket: %w", w.exchangeName, common.AppendError(ErrSubscriptionFailure, err)) } return nil } @@ -905,24 +922,13 @@ func (w *Websocket) ResubscribeToChannel(subscribedChannel *ChannelSubscription) // SubscribeToChannels appends supplied channels to channelsToSubscribe func (w *Websocket) SubscribeToChannels(channels []ChannelSubscription) error { - if len(channels) == 0 { - return fmt.Errorf("%s websocket: cannot subscribe no channels supplied", - w.exchangeName) - } - w.subscriptionMutex.Lock() - for x := range channels { - for y := range w.subscriptions { - if channels[x].Equal(&w.subscriptions[y]) { - w.subscriptionMutex.Unlock() - return fmt.Errorf("%s websocket: %v already subscribed", - w.exchangeName, - channels[x]) - } - } + err := w.checkSubscriptions(channels) + if err != nil { + return fmt.Errorf("%s websocket: %w", w.exchangeName, common.AppendError(ErrSubscriptionFailure, err)) } - w.subscriptionMutex.Unlock() - if err := w.Subscriber(channels); err != nil { - return fmt.Errorf("%v %w: %v", w.exchangeName, ErrSubscriptionFailure, err) + err = w.Subscriber(channels) + if err != nil { + return fmt.Errorf("%s websocket: %w", w.exchangeName, common.AppendError(ErrSubscriptionFailure, err)) } return nil } @@ -1004,3 +1010,31 @@ func checkWebsocketURL(s string) error { } return nil } + +// checkSubscriptions checks subscriptions against the max subscription limit +// and if the subscription already exists. +func (w *Websocket) checkSubscriptions(subs []ChannelSubscription) error { + if len(subs) == 0 { + return errNoSubscriptionsSupplied + } + + w.subscriptionMutex.Lock() + defer w.subscriptionMutex.Unlock() + + if w.MaxSubscriptionsPerConnection > 0 && len(w.subscriptions)+len(subs) > w.MaxSubscriptionsPerConnection { + return fmt.Errorf("%w: current subscriptions: %v, incoming subscriptions: %v, max subscriptions per connection: %v - please reduce enabled pairs", + errSubscriptionsExceedsLimit, + len(w.subscriptions), + len(subs), + w.MaxSubscriptionsPerConnection) + } + + for x := range subs { + for y := range w.subscriptions { + if subs[x].Equal(&w.subscriptions[y]) { + return fmt.Errorf("%w for %+v", errChannelSubscriptionAlreadySubscribed, subs[x]) + } + } + } + return nil +} diff --git a/exchanges/stream/websocket_test.go b/exchanges/stream/websocket_test.go index a5651d0f7df..2f68e5ba3a1 100644 --- a/exchanges/stream/websocket_test.go +++ b/exchanges/stream/websocket_test.go @@ -554,7 +554,15 @@ func TestSubscribeUnsubscribe(t *testing.T) { func TestResubscribe(t *testing.T) { t.Parallel() ws := *New() - err := ws.Setup(defaultSetup) + + wackedOutSetup := *defaultSetup + wackedOutSetup.MaxWebsocketSubscriptionsPerConnection = -1 + err := ws.Setup(&wackedOutSetup) + if !errors.Is(err, errInvalidMaxSubscriptions) { + t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidMaxSubscriptions) + } + + err = ws.Setup(defaultSetup) if err != nil { t.Fatal(err) } @@ -1390,3 +1398,32 @@ func TestLatency(t *testing.T) { t.Errorf("expected %v, got %v", exch, r.name) } } + +func TestCheckSubscriptions(t *testing.T) { + t.Parallel() + ws := Websocket{} + err := ws.checkSubscriptions(nil) + if !errors.Is(err, errNoSubscriptionsSupplied) { + t.Fatalf("received: %v, but expected: %v", err, errNoSubscriptionsSupplied) + } + + ws.MaxSubscriptionsPerConnection = 1 + + err = ws.checkSubscriptions([]ChannelSubscription{{}, {}}) + if !errors.Is(err, errSubscriptionsExceedsLimit) { + t.Fatalf("received: %v, but expected: %v", err, errSubscriptionsExceedsLimit) + } + + ws.MaxSubscriptionsPerConnection = 2 + + ws.subscriptions = []ChannelSubscription{{Channel: "test"}} + err = ws.checkSubscriptions([]ChannelSubscription{{Channel: "test"}}) + if !errors.Is(err, errChannelSubscriptionAlreadySubscribed) { + t.Fatalf("received: %v, but expected: %v", err, errChannelSubscriptionAlreadySubscribed) + } + + err = ws.checkSubscriptions([]ChannelSubscription{{}}) + if !errors.Is(err, nil) { + t.Fatalf("received: %v, but expected: %v", err, nil) + } +} diff --git a/exchanges/stream/websocket_types.go b/exchanges/stream/websocket_types.go index 2db60384f1c..640fb74a5bb 100644 --- a/exchanges/stream/websocket_types.go +++ b/exchanges/stream/websocket_types.go @@ -93,6 +93,10 @@ type Websocket struct { // Latency reporter ExchangeLevelReporter Reporter + + // MaxSubScriptionsPerConnection defines the maximum number of + // subscriptions per connection that is allowed by the exchange. + MaxSubscriptionsPerConnection int } // WebsocketSetup defines variables for setting up a websocket connection @@ -114,6 +118,10 @@ type WebsocketSetup struct { // Fill data config values FillsFeed bool + + // MaxWebsocketSubscriptionsPerConnection defines the maximum number of + // subscriptions per connection that is allowed by the exchange. + MaxWebsocketSubscriptionsPerConnection int } // WebsocketConnection contains all the data needed to send a message to a WS From 51e2e42a1999b5eabb0076bc44dab6bc012756a9 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Fri, 13 Oct 2023 16:38:49 +1100 Subject: [PATCH 10/40] orderbook: Add optional orderbook.Item string fields for potential checksum calculations (#1354) * orderbook: Add optional orderbook.Item string fields for potential checksum calculations. * glorious: nits * glorious: nits * thrasher: nits * glorious: nits --------- Co-authored-by: Ryan O'Hara-Reid --- exchanges/kraken/kraken_test.go | 43 ++++---- exchanges/kraken/kraken_websocket.go | 130 ++++++++++++------------ exchanges/orderbook/depth.go | 46 +++++---- exchanges/orderbook/depth_test.go | 23 +++-- exchanges/orderbook/linked_list.go | 32 +++--- exchanges/orderbook/linked_list_test.go | 6 +- exchanges/orderbook/orderbook.go | 10 +- exchanges/orderbook/orderbook_test.go | 19 +++- exchanges/orderbook/orderbook_types.go | 64 +++++++----- 9 files changed, 205 insertions(+), 168 deletions(-) diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index a2e3ef76ff5..03a9552840c 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -2040,28 +2040,29 @@ func TestGetHistoricTrades(t *testing.T) { var testOb = orderbook.Base{ Asks: []orderbook.Item{ - {Price: 0.05005, Amount: 0.00000500}, - {Price: 0.05010, Amount: 0.00000500}, - {Price: 0.05015, Amount: 0.00000500}, - {Price: 0.05020, Amount: 0.00000500}, - {Price: 0.05025, Amount: 0.00000500}, - {Price: 0.05030, Amount: 0.00000500}, - {Price: 0.05035, Amount: 0.00000500}, - {Price: 0.05040, Amount: 0.00000500}, - {Price: 0.05045, Amount: 0.00000500}, - {Price: 0.05050, Amount: 0.00000500}, + // NOTE: 0.00000500 float64 == 0.000005 + {Price: 0.05005, StrPrice: "0.05005", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05010, StrPrice: "0.05010", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05015, StrPrice: "0.05015", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05020, StrPrice: "0.05020", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05025, StrPrice: "0.05025", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05030, StrPrice: "0.05030", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05035, StrPrice: "0.05035", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05040, StrPrice: "0.05040", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05045, StrPrice: "0.05045", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.05050, StrPrice: "0.05050", Amount: 0.00000500, StrAmount: "0.00000500"}, }, Bids: []orderbook.Item{ - {Price: 0.05000, Amount: 0.00000500}, - {Price: 0.04995, Amount: 0.00000500}, - {Price: 0.04990, Amount: 0.00000500}, - {Price: 0.04980, Amount: 0.00000500}, - {Price: 0.04975, Amount: 0.00000500}, - {Price: 0.04970, Amount: 0.00000500}, - {Price: 0.04965, Amount: 0.00000500}, - {Price: 0.04960, Amount: 0.00000500}, - {Price: 0.04955, Amount: 0.00000500}, - {Price: 0.04950, Amount: 0.00000500}, + {Price: 0.05000, StrPrice: "0.05000", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04995, StrPrice: "0.04995", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04990, StrPrice: "0.04990", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04980, StrPrice: "0.04980", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04975, StrPrice: "0.04975", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04970, StrPrice: "0.04970", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04965, StrPrice: "0.04965", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04960, StrPrice: "0.04960", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04955, StrPrice: "0.04955", Amount: 0.00000500, StrAmount: "0.00000500"}, + {Price: 0.04950, StrPrice: "0.04950", Amount: 0.00000500, StrAmount: "0.00000500"}, }, } @@ -2079,7 +2080,7 @@ func TestChecksumCalculation(t *testing.T) { t.Errorf("expected %s but received %s", expected, v) } - err := validateCRC32(&testOb, krakenAPIDocChecksum, 5, 8) + err := validateCRC32(&testOb, krakenAPIDocChecksum) if err != nil { t.Error(err) } diff --git a/exchanges/kraken/kraken_websocket.go b/exchanges/kraken/kraken_websocket.go index 59fb179a41c..4b422751170 100644 --- a/exchanges/kraken/kraken_websocket.go +++ b/exchanges/kraken/kraken_websocket.go @@ -878,12 +878,13 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[ // wsProcessOrderBookPartial creates a new orderbook entry for a given currency pair func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, askData, bidData []interface{}) error { base := orderbook.Base{ - Pair: channelData.Pair, - Asset: asset.Spot, - VerifyOrderbook: k.CanVerifyOrderbook, - Bids: make(orderbook.Items, len(bidData)), - Asks: make(orderbook.Items, len(askData)), - MaxDepth: channelData.MaxDepth, + Pair: channelData.Pair, + Asset: asset.Spot, + VerifyOrderbook: k.CanVerifyOrderbook, + Bids: make(orderbook.Items, len(bidData)), + Asks: make(orderbook.Items, len(askData)), + MaxDepth: channelData.MaxDepth, + ChecksumStringRequired: true, } // Kraken ob data is timestamped per price, GCT orderbook data is // timestamped per entry using the highest last update time, we can attempt @@ -897,21 +898,35 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as if len(asks) < 3 { return errors.New("unexpected asks length") } - price, err := strconv.ParseFloat(asks[0].(string), 64) + priceStr, ok := asks[0].(string) + if !ok { + return common.GetTypeAssertError("string", asks[0], "price") + } + price, err := strconv.ParseFloat(priceStr, 64) if err != nil { return err } - amount, err := strconv.ParseFloat(asks[1].(string), 64) + amountStr, ok := asks[1].(string) + if !ok { + return common.GetTypeAssertError("string", asks[1], "amount") + } + amount, err := strconv.ParseFloat(amountStr, 64) if err != nil { return err } - timeData, err := strconv.ParseFloat(asks[2].(string), 64) + tdStr, ok := asks[2].(string) + if !ok { + return common.GetTypeAssertError("string", asks[2], "time") + } + timeData, err := strconv.ParseFloat(tdStr, 64) if err != nil { return err } base.Asks[i] = orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + StrAmount: amountStr, + Price: price, + StrPrice: priceStr, } askUpdatedTime := convert.TimeFromUnixTimestampDecimal(timeData) if highestLastUpdate.Before(askUpdatedTime) { @@ -927,22 +942,36 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as if len(bids) < 3 { return errors.New("unexpected bids length") } - price, err := strconv.ParseFloat(bids[0].(string), 64) + priceStr, ok := bids[0].(string) + if !ok { + return common.GetTypeAssertError("string", bids[0], "price") + } + price, err := strconv.ParseFloat(priceStr, 64) if err != nil { return err } - amount, err := strconv.ParseFloat(bids[1].(string), 64) + amountStr, ok := bids[1].(string) + if !ok { + return common.GetTypeAssertError("string", bids[1], "amount") + } + amount, err := strconv.ParseFloat(amountStr, 64) if err != nil { return err } - timeData, err := strconv.ParseFloat(bids[2].(string), 64) + tdStr, ok := bids[2].(string) + if !ok { + return common.GetTypeAssertError("string", bids[2], "time") + } + timeData, err := strconv.ParseFloat(tdStr, 64) if err != nil { return err } base.Bids[i] = orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + StrAmount: amountStr, + Price: price, + StrPrice: priceStr, } bidUpdateTime := convert.TimeFromUnixTimestampDecimal(timeData) @@ -968,7 +997,6 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask // price and amount as there is no set standard between currency pairs. This // is calculated per update as opposed to snapshot because changes to // decimal amounts could occur at any time. - var priceDP, amtDP int var highestLastUpdate time.Time // Ask data is not always sent for i := range askData { @@ -1008,29 +1036,16 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask } update.Asks[i] = orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + StrAmount: amountStr, + Price: price, + StrPrice: priceStr, } askUpdatedTime := convert.TimeFromUnixTimestampDecimal(timeData) if highestLastUpdate.Before(askUpdatedTime) { highestLastUpdate = askUpdatedTime } - - if i == len(askData)-1 { - pSplit := strings.Split(priceStr, ".") - if len(pSplit) != 2 { - return errors.New("incorrect decimal data returned for price") - } - - priceDP = len(pSplit[1]) - aSplit := strings.Split(amountStr, ".") - if len(aSplit) != 2 { - return errors.New("incorrect decimal data returned for amount") - } - - amtDP = len(aSplit[1]) - } } // Bid data is not always sent @@ -1071,29 +1086,16 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask } update.Bids[i] = orderbook.Item{ - Amount: amount, - Price: price, + Amount: amount, + StrAmount: amountStr, + Price: price, + StrPrice: priceStr, } bidUpdatedTime := convert.TimeFromUnixTimestampDecimal(timeData) if highestLastUpdate.Before(bidUpdatedTime) { highestLastUpdate = bidUpdatedTime } - - if i == len(bidData)-1 { - pSplit := strings.Split(priceStr, ".") - if len(pSplit) != 2 { - return errors.New("incorrect decimal data returned for price") - } - - priceDP = len(pSplit[1]) - aSplit := strings.Split(amountStr, ".") - if len(aSplit) != 2 { - return errors.New("incorrect decimal data returned for amount") - } - - amtDP = len(aSplit[1]) - } } update.UpdateTime = highestLastUpdate @@ -1115,29 +1117,23 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask return err } - return validateCRC32(book, uint32(token), priceDP, amtDP) + return validateCRC32(book, uint32(token)) } -func validateCRC32(b *orderbook.Base, token uint32, decPrice, decAmount int) error { - if decPrice == 0 || decAmount == 0 { - return fmt.Errorf("%s %s trailing decimal count not calculated", - b.Pair, - b.Asset) - } - +func validateCRC32(b *orderbook.Base, token uint32) error { var checkStr strings.Builder for i := 0; i < 10 && i < len(b.Asks); i++ { - priceStr := trim(strconv.FormatFloat(b.Asks[i].Price, 'f', decPrice, 64)) - checkStr.WriteString(priceStr) - amountStr := trim(strconv.FormatFloat(b.Asks[i].Amount, 'f', decAmount, 64)) - checkStr.WriteString(amountStr) + _, err := checkStr.WriteString(trim(b.Asks[i].StrPrice + trim(b.Asks[i].StrAmount))) + if err != nil { + return err + } } for i := 0; i < 10 && i < len(b.Bids); i++ { - priceStr := trim(strconv.FormatFloat(b.Bids[i].Price, 'f', decPrice, 64)) - checkStr.WriteString(priceStr) - amountStr := trim(strconv.FormatFloat(b.Bids[i].Amount, 'f', decAmount, 64)) - checkStr.WriteString(amountStr) + _, err := checkStr.WriteString(trim(b.Bids[i].StrPrice) + trim(b.Bids[i].StrAmount)) + if err != nil { + return err + } } if check := crc32.ChecksumIEEE([]byte(checkStr.String())); check != token { diff --git a/exchanges/orderbook/depth.go b/exchanges/orderbook/depth.go index 9827491dc8c..cde8842cf17 100644 --- a/exchanges/orderbook/depth.go +++ b/exchanges/orderbook/depth.go @@ -78,17 +78,18 @@ func (d *Depth) Retrieve() (*Base, error) { return nil, d.validationError } return &Base{ - Bids: d.bids.retrieve(0), - Asks: d.asks.retrieve(0), - Exchange: d.exchange, - Asset: d.asset, - Pair: d.pair, - LastUpdated: d.lastUpdated, - LastUpdateID: d.lastUpdateID, - PriceDuplication: d.priceDuplication, - IsFundingRate: d.isFundingRate, - VerifyOrderbook: d.VerifyOrderbook, - MaxDepth: d.maxDepth, + Bids: d.bids.retrieve(0), + Asks: d.asks.retrieve(0), + Exchange: d.exchange, + Asset: d.asset, + Pair: d.pair, + LastUpdated: d.lastUpdated, + LastUpdateID: d.lastUpdateID, + PriceDuplication: d.priceDuplication, + IsFundingRate: d.isFundingRate, + VerifyOrderbook: d.VerifyOrderbook, + MaxDepth: d.maxDepth, + ChecksumStringRequired: d.checksumStringRequired, }, nil } @@ -282,17 +283,18 @@ func (d *Depth) UpdateInsertByID(update *Update) error { func (d *Depth) AssignOptions(b *Base) { d.m.Lock() d.options = options{ - exchange: b.Exchange, - pair: b.Pair, - asset: b.Asset, - lastUpdated: b.LastUpdated, - lastUpdateID: b.LastUpdateID, - priceDuplication: b.PriceDuplication, - isFundingRate: b.IsFundingRate, - VerifyOrderbook: b.VerifyOrderbook, - restSnapshot: b.RestSnapshot, - idAligned: b.IDAlignment, - maxDepth: b.MaxDepth, + exchange: b.Exchange, + pair: b.Pair, + asset: b.Asset, + lastUpdated: b.LastUpdated, + lastUpdateID: b.LastUpdateID, + priceDuplication: b.PriceDuplication, + isFundingRate: b.IsFundingRate, + VerifyOrderbook: b.VerifyOrderbook, + restSnapshot: b.RestSnapshot, + idAligned: b.IDAlignment, + maxDepth: b.MaxDepth, + checksumStringRequired: b.ChecksumStringRequired, } d.m.Unlock() } diff --git a/exchanges/orderbook/depth_test.go b/exchanges/orderbook/depth_test.go index d3ac0c324d6..ef2dc8a9969 100644 --- a/exchanges/orderbook/depth_test.go +++ b/exchanges/orderbook/depth_test.go @@ -92,17 +92,18 @@ func TestRetrieve(t *testing.T) { d.asks.load([]Item{{Price: 1337}}, d.stack, time.Now()) d.bids.load([]Item{{Price: 1337}}, d.stack, time.Now()) d.options = options{ - exchange: "THE BIG ONE!!!!!!", - pair: currency.NewPair(currency.THETA, currency.USD), - asset: asset.DownsideProfitContract, - lastUpdated: time.Now(), - lastUpdateID: 1337, - priceDuplication: true, - isFundingRate: true, - VerifyOrderbook: true, - restSnapshot: true, - idAligned: true, - maxDepth: 10, + exchange: "THE BIG ONE!!!!!!", + pair: currency.NewPair(currency.THETA, currency.USD), + asset: asset.DownsideProfitContract, + lastUpdated: time.Now(), + lastUpdateID: 1337, + priceDuplication: true, + isFundingRate: true, + VerifyOrderbook: true, + restSnapshot: true, + idAligned: true, + maxDepth: 10, + checksumStringRequired: true, } // If we add anymore options to the options struct later this will complain diff --git a/exchanges/orderbook/linked_list.go b/exchanges/orderbook/linked_list.go index d5e7a0eaae0..0b64f309e37 100644 --- a/exchanges/orderbook/linked_list.go +++ b/exchanges/orderbook/linked_list.go @@ -100,8 +100,10 @@ updates: // Only apply changes when zero values are not present, Bitmex // for example sends 0 price values. tip.Value.Price = updts[x].Price + tip.Value.StrPrice = updts[x].StrPrice } tip.Value.Amount = updts[x].Amount + tip.Value.StrAmount = updts[x].StrAmount continue updates } return fmt.Errorf("update error: %w ID: %d not found", @@ -190,7 +192,7 @@ func (ll *linkedList) updateInsertByPrice(updts Items, stack *stack, maxChainLen for x := range updts { for tip := &ll.head; ; tip = &(*tip).Next { if *tip == nil { - insertHeadSpecific(ll, updts[x], stack) + insertHeadSpecific(ll, &updts[x], stack) break } if (*tip).Value.Price == updts[x].Price { // Match check @@ -198,6 +200,7 @@ func (ll *linkedList) updateInsertByPrice(updts Items, stack *stack, maxChainLen stack.Push(deleteAtTip(ll, tip), tn) } else { // Amend current amount value (*tip).Value.Amount = updts[x].Amount + (*tip).Value.StrAmount = updts[x].StrAmount } break // Continue updates } @@ -208,7 +211,7 @@ func (ll *linkedList) updateInsertByPrice(updts Items, stack *stack, maxChainLen // to a non-existent price level (OTC/Hidden order) so we can // break instantly and reduce the traversal of the entire chain. if updts[x].Amount > 0 { - insertAtTip(ll, tip, updts[x], stack) + insertAtTip(ll, tip, &updts[x], stack) } break // Continue updates } @@ -217,7 +220,7 @@ func (ll *linkedList) updateInsertByPrice(updts Items, stack *stack, maxChainLen // This check below is just a catch all in the event the above // zero value check fails if updts[x].Amount > 0 { - insertAtTail(ll, tip, updts[x], stack) + insertAtTail(ll, tip, &updts[x], stack) } break } @@ -255,7 +258,9 @@ updates: if tip.Next == nil { // no movement needed just a re-adjustment tip.Value.Price = updts[x].Price + tip.Value.StrPrice = updts[x].StrPrice tip.Value.Amount = updts[x].Amount + tip.Value.StrAmount = updts[x].StrAmount continue updates } // bookmark tip to move this node to correct price level @@ -264,6 +269,7 @@ updates: } // no price change, amend amount and continue update tip.Value.Amount = updts[x].Amount + tip.Value.StrAmount = updts[x].StrAmount continue updates // continue to next update } @@ -305,7 +311,7 @@ updates: } if tip.Next == nil { - if shiftBookmark(tip, &bookmark, &ll.head, updts[x]) { + if shiftBookmark(tip, &bookmark, &ll.head, &updts[x]) { continue updates } } @@ -352,7 +358,7 @@ func (ll *linkedList) insertUpdates(updts Items, stack *stack, comp comparison) } if (*tip).Next == nil { // Tail - insertAtTail(ll, tip, updts[x], stack) + insertAtTail(ll, tip, &updts[x], stack) break // Continue updates } prev = *tip @@ -778,9 +784,9 @@ func deleteAtTip(ll *linkedList, tip **Node) *Node { } // insertAtTip inserts at a tip target (can inline) -func insertAtTip(ll *linkedList, tip **Node, updt Item, stack *stack) { +func insertAtTip(ll *linkedList, tip **Node, updt *Item, stack *stack) { n := stack.Pop() - n.Value = updt + n.Value = *updt n.Next = *tip n.Prev = (*tip).Prev if (*tip).Prev == nil { // Tip is at head @@ -797,9 +803,9 @@ func insertAtTip(ll *linkedList, tip **Node, updt Item, stack *stack) { } // insertAtTail inserts at tail end of node chain (can inline) -func insertAtTail(ll *linkedList, tip **Node, updt Item, stack *stack) { +func insertAtTail(ll *linkedList, tip **Node, updt *Item, stack *stack) { n := stack.Pop() - n.Value = updt + n.Value = *updt // Reference tip to new node (*tip).Next = n // Reference new node with current tip @@ -810,9 +816,9 @@ func insertAtTail(ll *linkedList, tip **Node, updt Item, stack *stack) { // insertHeadSpecific inserts at head specifically there might be an instance // where the liquidity on an exchange does fall to zero through a streaming // endpoint then it comes back online. (can inline) -func insertHeadSpecific(ll *linkedList, updt Item, stack *stack) { +func insertHeadSpecific(ll *linkedList, updt *Item, stack *stack) { n := stack.Pop() - n.Value = updt + n.Value = *updt ll.head = n ll.length++ } @@ -842,12 +848,12 @@ func insertNodeAtBookmark(ll *linkedList, bookmark, n *Node) { // shiftBookmark moves a bookmarked node to the tip's next position or if nil, // sets tip as bookmark (can inline) -func shiftBookmark(tip *Node, bookmark, head **Node, updt Item) bool { +func shiftBookmark(tip *Node, bookmark, head **Node, updt *Item) bool { if *bookmark == nil { // End of the chain and no bookmark set *bookmark = tip // Set tip to bookmark so we can set a new node there return false } - (*bookmark).Value = updt + (*bookmark).Value = *updt (*bookmark).Next.Prev = (*bookmark).Prev if (*bookmark).Prev == nil { // Bookmark is at head *head = (*bookmark).Next diff --git a/exchanges/orderbook/linked_list_test.go b/exchanges/orderbook/linked_list_test.go index c5d66312c3c..93768937d1f 100644 --- a/exchanges/orderbook/linked_list_test.go +++ b/exchanges/orderbook/linked_list_test.go @@ -1421,7 +1421,7 @@ func TestShiftBookmark(t *testing.T) { // associate tips prev field with the correct prev node tip.Prev = tipprev - if !shiftBookmark(tip, &bookmarkedNode, nil, Item{Amount: 1336, ID: 1337, Price: 9999}) { + if !shiftBookmark(tip, &bookmarkedNode, nil, &Item{Amount: 1336, ID: 1337, Price: 9999}) { t.Fatal("There should be liquidity so we don't need to set tip to bookmark") } @@ -1453,7 +1453,7 @@ func TestShiftBookmark(t *testing.T) { var nilBookmark *Node - if shiftBookmark(tip, &nilBookmark, nil, Item{Amount: 1336, ID: 1337, Price: 9999}) { + if shiftBookmark(tip, &nilBookmark, nil, &Item{Amount: 1336, ID: 1337, Price: 9999}) { t.Fatal("there should not be a bookmarked node") } @@ -1466,7 +1466,7 @@ func TestShiftBookmark(t *testing.T) { bookmarkedNode.Next = originalBookmarkNext tip.Next = nil - if !shiftBookmark(tip, &bookmarkedNode, &head, Item{Amount: 1336, ID: 1337, Price: 9999}) { + if !shiftBookmark(tip, &bookmarkedNode, &head, &Item{Amount: 1336, ID: 1337, Price: 9999}) { t.Fatal("There should be liquidity so we don't need to set tip to bookmark") } diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index 93a936660d7..d5a8ca5eee3 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -225,11 +225,11 @@ func (b *Base) Verify() error { len(b.Bids), len(b.Asks)) } - err := checkAlignment(b.Bids, b.IsFundingRate, b.PriceDuplication, b.IDAlignment, dsc, b.Exchange) + err := checkAlignment(b.Bids, b.IsFundingRate, b.PriceDuplication, b.IDAlignment, b.ChecksumStringRequired, dsc, b.Exchange) if err != nil { return fmt.Errorf(bidLoadBookFailure, b.Exchange, b.Pair, b.Asset, err) } - err = checkAlignment(b.Asks, b.IsFundingRate, b.PriceDuplication, b.IDAlignment, asc, b.Exchange) + err = checkAlignment(b.Asks, b.IsFundingRate, b.PriceDuplication, b.IDAlignment, b.ChecksumStringRequired, asc, b.Exchange) if err != nil { return fmt.Errorf(askLoadBookFailure, b.Exchange, b.Pair, b.Asset, err) } @@ -257,7 +257,7 @@ var dsc = func(current Item, previous Item) error { } // checkAlignment validates full orderbook -func checkAlignment(depth Items, fundingRate, priceDuplication, isIDAligned bool, c checker, exch string) error { +func checkAlignment(depth Items, fundingRate, priceDuplication, isIDAligned, requiresChecksumString bool, c checker, exch string) error { for i := range depth { if depth[i].Price == 0 { switch { @@ -273,6 +273,10 @@ func checkAlignment(depth Items, fundingRate, priceDuplication, isIDAligned bool if fundingRate && depth[i].Period == 0 { return errPeriodUnset } + if requiresChecksumString && (depth[i].StrAmount == "" || depth[i].StrPrice == "") { + return errChecksumStringNotSet + } + if i != 0 { prev := i - 1 if err := c(depth[i], depth[prev]); err != nil { diff --git a/exchanges/orderbook/orderbook_test.go b/exchanges/orderbook/orderbook_test.go index b78e8551671..5c8b0debb78 100644 --- a/exchanges/orderbook/orderbook_test.go +++ b/exchanges/orderbook/orderbook_test.go @@ -759,16 +759,29 @@ func TestCheckAlignment(t *testing.T) { Period: 1337, }, } - err := checkAlignment(itemWithFunding, true, true, false, dsc, "Bitfinex") + err := checkAlignment(itemWithFunding, true, true, false, false, dsc, "Bitfinex") if err != nil { t.Error(err) } - err = checkAlignment(itemWithFunding, false, true, false, dsc, "Bitfinex") + err = checkAlignment(itemWithFunding, false, true, false, false, dsc, "Bitfinex") if !errors.Is(err, errPriceNotSet) { t.Fatalf("received: %v but expected: %v", err, errPriceNotSet) } - err = checkAlignment(itemWithFunding, true, true, false, dsc, "Binance") + err = checkAlignment(itemWithFunding, true, true, false, false, dsc, "Binance") if !errors.Is(err, errPriceNotSet) { t.Fatalf("received: %v but expected: %v", err, errPriceNotSet) } + + itemWithFunding[0].Price = 1337 + err = checkAlignment(itemWithFunding, true, true, false, true, dsc, "Binance") + if !errors.Is(err, errChecksumStringNotSet) { + t.Fatalf("received: %v but expected: %v", err, errChecksumStringNotSet) + } + + itemWithFunding[0].StrAmount = "1337.0000000" + itemWithFunding[0].StrPrice = "1337.0000000" + err = checkAlignment(itemWithFunding, true, true, false, true, dsc, "Binance") + if !errors.Is(err, nil) { + t.Fatalf("received: %v but expected: %v", err, nil) + } } diff --git a/exchanges/orderbook/orderbook_types.go b/exchanges/orderbook/orderbook_types.go index 3235f9a1101..229cdcd8b18 100644 --- a/exchanges/orderbook/orderbook_types.go +++ b/exchanges/orderbook/orderbook_types.go @@ -21,18 +21,19 @@ const ( // Vars for the orderbook package var ( - errExchangeNameUnset = errors.New("orderbook exchange name not set") - errPairNotSet = errors.New("orderbook currency pair not set") - errAssetTypeNotSet = errors.New("orderbook asset type not set") - errCannotFindOrderbook = errors.New("cannot find orderbook(s)") - errPriceNotSet = errors.New("price cannot be zero") - errAmountInvalid = errors.New("amount cannot be less or equal to zero") - errPriceOutOfOrder = errors.New("pricing out of order") - errIDOutOfOrder = errors.New("ID out of order") - errDuplication = errors.New("price duplication") - errIDDuplication = errors.New("id duplication") - errPeriodUnset = errors.New("funding rate period is unset") - errNotEnoughLiquidity = errors.New("not enough liquidity") + errExchangeNameUnset = errors.New("orderbook exchange name not set") + errPairNotSet = errors.New("orderbook currency pair not set") + errAssetTypeNotSet = errors.New("orderbook asset type not set") + errCannotFindOrderbook = errors.New("cannot find orderbook(s)") + errPriceNotSet = errors.New("price cannot be zero") + errAmountInvalid = errors.New("amount cannot be less or equal to zero") + errPriceOutOfOrder = errors.New("pricing out of order") + errIDOutOfOrder = errors.New("ID out of order") + errDuplication = errors.New("price duplication") + errIDDuplication = errors.New("id duplication") + errPeriodUnset = errors.New("funding rate period is unset") + errNotEnoughLiquidity = errors.New("not enough liquidity") + errChecksumStringNotSet = errors.New("checksum string not set") ) var service = Service{ @@ -57,8 +58,16 @@ type Exchange struct { // Item stores the amount and price values type Item struct { Amount float64 - Price float64 - ID int64 + // StrAmount is a string representation of the amount. e.g. 0.00000100 this + // parsed as a float will constrict comparison to 1e-6 not 1e-8 or + // potentially will round value which is not ideal. + StrAmount string + Price float64 + // StrPrice is a string representation of the price. e.g. 0.00000100 this + // parsed as a float will constrict comparison to 1e-6 not 1e-8 or + // potentially will round value which is not ideal. + StrPrice string + ID int64 // Funding rate field Period int64 @@ -100,6 +109,10 @@ type Base struct { // should remove any items that are outside of this scope. Bittrex and // Kraken utilise this field. MaxDepth int + // ChecksumStringRequired defines if the checksum is built from the raw + // string representations of the price and amount. This helps alleviate any + // potential rounding issues. + ChecksumStringRequired bool } type byOBPrice []Item @@ -109,17 +122,18 @@ func (a byOBPrice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byOBPrice) Less(i, j int) bool { return a[i].Price < a[j].Price } type options struct { - exchange string - pair currency.Pair - asset asset.Item - lastUpdated time.Time - lastUpdateID int64 - priceDuplication bool - isFundingRate bool - VerifyOrderbook bool - restSnapshot bool - idAligned bool - maxDepth int + exchange string + pair currency.Pair + asset asset.Item + lastUpdated time.Time + lastUpdateID int64 + priceDuplication bool + isFundingRate bool + VerifyOrderbook bool + restSnapshot bool + idAligned bool + checksumStringRequired bool + maxDepth int } // Action defines a set of differing states required to implement an incoming From 7083f774cbc17cfdbaa47698eac41925d7a452e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:06:01 +1100 Subject: [PATCH 11/40] build(deps): bump google.golang.org/grpc from 1.58.2 to 1.58.3 (#1374) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.58.2 to 1.58.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.58.2...v1.58.3) --- 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> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index dfe96c07930..4339c271325 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( golang.org/x/text v0.13.0 golang.org/x/time v0.3.0 google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb - google.golang.org/grpc v1.58.2 + google.golang.org/grpc v1.58.3 google.golang.org/protobuf v1.31.0 ) diff --git a/go.sum b/go.sum index 2c4bb9af1f8..c85eb41618a 100644 --- a/go.sum +++ b/go.sum @@ -658,8 +658,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= -google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From d3bf4a460a4a89eb6332922b8c71689ccd6ed961 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 09:20:34 +1100 Subject: [PATCH 12/40] build(deps): bump golang.org/x/net from 0.16.0 to 0.17.0 (#1375) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.16.0 to 0.17.0. - [Commits](https://github.com/golang/net/compare/v0.16.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/net 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> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4339c271325..c1f19226b9b 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/urfave/cli/v2 v2.25.7 github.com/volatiletech/null v8.0.0+incompatible golang.org/x/crypto v0.14.0 - golang.org/x/net v0.16.0 + golang.org/x/net v0.17.0 golang.org/x/text v0.13.0 golang.org/x/time v0.3.0 google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb diff --git a/go.sum b/go.sum index c85eb41618a..ca6a0985b2b 100644 --- a/go.sum +++ b/go.sum @@ -431,8 +431,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= -golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= From ceef7a14e0e34026963033a54d09bdeeb3b08bb8 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Wed, 18 Oct 2023 11:57:27 +1100 Subject: [PATCH 13/40] currency: Adds matching lookup table built from available pairs (#1312) * currency: Add pair matching update (cherry-pick) * exchange/currency: Add tests and update func * linter fix, also if using json unmarshal functionality stop usage of string conversion without delimiter * gemini: fix test * currency/manager: potential optimisation * exchanges: purge derive from wrapper cases and add warning comment * glorious: nits * glorious: nits * linter: fix * glorious: nits * whoops * whoops * glorious: nits continued * glorious: diff THANKS! * hitbtc: fix update tradable pairs strings splitting. continue if not enabled tickers update pair. * glorious: nits * linter: fix * Update exchanges/exmo/exmo_wrapper.go Co-authored-by: Scott * bitstamp: fix test when 32 biterinos architecturinos * capture more strings for speed * swapsies because whos running 32bit \0/? --------- Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott --- .../exchange_wrapper_standards_test.go | 3 + currency/currency_types.go | 8 -- currency/manager.go | 73 +++++++--- currency/manager_test.go | 87 +++++++++-- currency/manager_types.go | 21 ++- currency/pair.go | 25 ++-- currency/pair_methods.go | 18 ++- currency/pair_types.go | 2 + currency/pairs.go | 3 +- engine/rpcserver.go | 20 ++- engine/rpcserver_test.go | 21 ++- exchanges/bitfinex/bitfinex_wrapper.go | 14 +- exchanges/bitmex/bitmex_test.go | 1 + exchanges/bitmex/bitmex_wrapper.go | 18 +-- exchanges/bitstamp/bitstamp_type_convert.go | 11 +- exchanges/bybit/bybit_test.go | 6 +- exchanges/bybit/bybit_websocket.go | 36 ++++- exchanges/bybit/bybit_wrapper.go | 77 +++++----- exchanges/bybit/bybit_ws_cfutures.go | 44 ++++-- exchanges/bybit/bybit_ws_futures.go | 26 ++-- exchanges/bybit/bybit_ws_ufutures.go | 26 ++-- exchanges/coinut/coinut_types.go | 12 +- exchanges/exchange.go | 36 +++++ exchanges/exchange_test.go | 136 ++++++++++++++++++ exchanges/exmo/exmo_test.go | 23 +-- exchanges/exmo/exmo_wrapper.go | 19 +-- exchanges/gateio/gateio_wrapper.go | 7 +- exchanges/gemini/gemini_types.go | 22 +-- exchanges/hitbtc/hitbtc_test.go | 15 +- exchanges/hitbtc/hitbtc_wrapper.go | 23 ++- exchanges/interfaces.go | 12 ++ exchanges/okx/okx.go | 30 +++- 32 files changed, 616 insertions(+), 259 deletions(-) diff --git a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go index 493a2927b10..995c40ecce4 100644 --- a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go +++ b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go @@ -300,6 +300,8 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr // Crypto Chain input = reflect.ValueOf(cryptoChainPerExchange[exchName]) } + case "MatchSymbolWithAvailablePairs", "MatchSymbolCheckEnabled": + input = reflect.ValueOf(argGenerator.AssetParams.Pair.Base.Lower().String() + argGenerator.AssetParams.Pair.Quote.Lower().String()) default: // OrderID input = reflect.ValueOf("1337") @@ -602,6 +604,7 @@ var acceptableErrors = []error{ order.ErrPairIsEmpty, // Is thrown when the empty pair and asset scenario for an order submission is sent in the Validate() function deposit.ErrAddressNotFound, // Is thrown when an address is not found due to the exchange requiring valid API keys futures.ErrNotFuturesAsset, // Is thrown when a futures function receives a non-futures asset + currency.ErrSymbolStringEmpty, // Is thrown when a symbol string is empty for blank MatchSymbol func checks } // warningErrors will t.Log(err) when thrown to diagnose things, but not necessarily suggest diff --git a/currency/currency_types.go b/currency/currency_types.go index f69e75c9a18..de9d1aa464c 100644 --- a/currency/currency_types.go +++ b/currency/currency_types.go @@ -81,11 +81,3 @@ const ( ForwardSlashDelimiter = "/" ColonDelimiter = ":" ) - -// delimiters is a delimiter list -var delimiters = []string{ - DashDelimiter, - UnderscoreDelimiter, - ForwardSlashDelimiter, - ColonDelimiter, -} diff --git a/currency/manager.go b/currency/manager.go index c3d22769ac4..22e68bd3f76 100644 --- a/currency/manager.go +++ b/currency/manager.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" @@ -29,9 +30,12 @@ var ( ErrPairManagerNotInitialised = errors.New("pair manager not initialised") // ErrAssetNotFound is returned when an asset does not exist in the pairstore ErrAssetNotFound = errors.New("asset type not found in pair store") + // ErrSymbolStringEmpty is an error when a symbol string is empty + ErrSymbolStringEmpty = errors.New("symbol string is empty") - errPairStoreIsNil = errors.New("pair store is nil") - errPairFormatIsNil = errors.New("pair format is nil") + errPairStoreIsNil = errors.New("pair store is nil") + errPairFormatIsNil = errors.New("pair format is nil") + errPairMatcherIsNil = errors.New("pair matcher is nil") ) // GetAssetTypes returns a list of stored asset types @@ -64,6 +68,24 @@ func (p *PairsManager) Get(a asset.Item) (*PairStore, error) { return c.copy() } +// Match returns a currency pair based on the supplied symbol and asset type +func (p *PairsManager) Match(symbol string, a asset.Item) (Pair, error) { + if symbol == "" { + return EMPTYPAIR, ErrSymbolStringEmpty + } + symbol = strings.ToLower(symbol) + p.mutex.RLock() + defer p.mutex.RUnlock() + if p.matcher == nil { + return EMPTYPAIR, errPairMatcherIsNil + } + pair, ok := p.matcher[key{symbol, a}] + if !ok { + return EMPTYPAIR, fmt.Errorf("%w for %v %v", ErrPairNotFound, symbol, a) + } + return *pair, nil +} + // Store stores a new currency pair config based on its asset type func (p *PairsManager) Store(a asset.Item, ps *PairStore) error { if !a.IsValid() { @@ -78,6 +100,14 @@ func (p *PairsManager) Store(a asset.Item, ps *PairStore) error { p.Pairs = make(map[asset.Item]*PairStore) } p.Pairs[a] = cpy + if p.matcher == nil { + p.matcher = make(map[key]*Pair) + } + for x := range cpy.Available { + p.matcher[key{ + Symbol: cpy.Available[x].Base.Lower().String() + cpy.Available[x].Quote.Lower().String(), + Asset: a}] = &cpy.Available[x] + } p.mutex.Unlock() return nil } @@ -85,6 +115,14 @@ func (p *PairsManager) Store(a asset.Item, ps *PairStore) error { // Delete deletes a map entry based on the supplied asset type func (p *PairsManager) Delete(a asset.Item) { p.mutex.Lock() + vals, ok := p.Pairs[a] + if !ok { + p.mutex.Unlock() + return + } + for x := range vals.Available { + delete(p.matcher, key{Symbol: vals.Available[x].Base.Lower().String() + vals.Available[x].Quote.Lower().String(), Asset: a}) + } delete(p.Pairs, a) p.mutex.Unlock() } @@ -187,6 +225,15 @@ func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) error pairStore.Enabled = cpy } else { pairStore.Available = cpy + + if p.matcher == nil { + p.matcher = make(map[key]*Pair) + } + for x := range pairStore.Available { + p.matcher[key{ + Symbol: pairStore.Available[x].Base.Lower().String() + pairStore.Available[x].Quote.Lower().String(), + Asset: a}] = &pairStore.Available[x] + } } return nil @@ -285,14 +332,14 @@ func (p *PairsManager) EnablePair(a asset.Item, pair Pair) error { return nil } -// IsAssetPairEnabled checks if a pair is enabled for an enabled asset type -func (p *PairsManager) IsAssetPairEnabled(a asset.Item, pair Pair) error { +// IsPairEnabled checks if a pair is enabled for an enabled asset type +func (p *PairsManager) IsPairEnabled(pair Pair, a asset.Item) (bool, error) { if !a.IsValid() { - return fmt.Errorf("%s %w", a, asset.ErrNotSupported) + return false, fmt.Errorf("%s %w", a, asset.ErrNotSupported) } if pair.IsEmpty() { - return ErrCurrencyPairEmpty + return false, ErrCurrencyPairEmpty } p.mutex.RLock() @@ -300,20 +347,12 @@ func (p *PairsManager) IsAssetPairEnabled(a asset.Item, pair Pair) error { pairStore, err := p.getPairStoreRequiresLock(a) if err != nil { - return err + return false, err } - if pairStore.AssetEnabled == nil { - return fmt.Errorf("%s %w", a, ErrAssetIsNil) + return false, fmt.Errorf("%s %w", a, ErrAssetIsNil) } - if !*pairStore.AssetEnabled { - return fmt.Errorf("%s %w", a, asset.ErrNotEnabled) - } - if !pairStore.Enabled.Contains(pair, true) { - return fmt.Errorf("%s %w", pair, ErrPairNotFound) - } - - return nil + return *pairStore.AssetEnabled && pairStore.Enabled.Contains(pair, true), nil } // IsAssetEnabled checks to see if an asset is enabled diff --git a/currency/manager_test.go b/currency/manager_test.go index 66a2dd29f29..dbd81f10a66 100644 --- a/currency/manager_test.go +++ b/currency/manager_test.go @@ -92,6 +92,48 @@ func TestGet(t *testing.T) { } } +func TestPairsManagerMatch(t *testing.T) { + t.Parallel() + + p := &PairsManager{} + + _, err := p.Match("", 1337) + if !errors.Is(err, ErrSymbolStringEmpty) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrSymbolStringEmpty) + } + + _, err = p.Match("sillyBilly", 1337) + if !errors.Is(err, errPairMatcherIsNil) { + t.Fatalf("received: '%v' but expected: '%v'", err, errPairMatcherIsNil) + } + + p = initTest(t) + + _, err = p.Match("sillyBilly", 1337) + if !errors.Is(err, ErrPairNotFound) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrPairNotFound) + } + + _, err = p.Match("sillyBilly", asset.Spot) + if !errors.Is(err, ErrPairNotFound) { + t.Fatalf("received: '%v' but expected: '%v'", err, ErrPairNotFound) + } + + whatIgot, err := p.Match("bTCuSD", asset.Spot) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + whatIwant, err := NewPairFromString("btc-usd") + if err != nil { + t.Fatal(err) + } + + if !whatIgot.Equal(whatIwant) { + t.Fatal("expected btc-usd") + } +} + func TestStore(t *testing.T) { t.Parallel() availPairs, err := NewPairsFromStrings([]string{"BTC-USD", "LTC-USD"}) @@ -531,43 +573,64 @@ func TestUnmarshalMarshal(t *testing.T) { } } -func TestIsAssetPairEnabled(t *testing.T) { +func TestIsPairEnabled(t *testing.T) { t.Parallel() pm := initTest(t) cp := NewPairWithDelimiter("BTC", "USD", "-") - err := pm.IsAssetPairEnabled(asset.Spot, cp) + enabled, err := pm.IsPairEnabled(cp, asset.Spot) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if !enabled { + t.Fatal("expected pair to be enabled") + } + + enabled, err = pm.IsPairEnabled(NewPair(SAFE, MOONRISE), asset.Spot) if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } - err = pm.IsAssetPairEnabled(asset.Futures, cp) - if !errors.Is(err, asset.ErrNotEnabled) { - t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotEnabled) + if enabled { + t.Fatal("expected pair to be disabled") + } + + enabled, err = pm.IsPairEnabled(cp, asset.Futures) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if enabled { + t.Fatal("expected pair to be disabled because asset type is not enabled") } cp = NewPairWithDelimiter("XRP", "DOGE", "-") - err = pm.IsAssetPairEnabled(asset.Spot, cp) - if !errors.Is(err, ErrPairNotFound) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrPairNotFound) + enabled, err = pm.IsPairEnabled(cp, asset.Spot) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if enabled { + t.Fatal("expected pair to be disabled because pair not found in enabled list") } - err = pm.IsAssetPairEnabled(asset.PerpetualSwap, cp) + _, err = pm.IsPairEnabled(cp, asset.PerpetualSwap) if !errors.Is(err, ErrAssetNotFound) { t.Fatalf("received: '%v' but expected: '%v'", err, ErrAssetNotFound) } - err = pm.IsAssetPairEnabled(asset.Item(1337), cp) + _, err = pm.IsPairEnabled(cp, asset.Item(1337)) if !errors.Is(err, asset.ErrNotSupported) { t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported) } pm.Pairs[asset.PerpetualSwap] = &PairStore{} - err = pm.IsAssetPairEnabled(asset.PerpetualSwap, cp) + _, err = pm.IsPairEnabled(cp, asset.PerpetualSwap) if !errors.Is(err, ErrAssetIsNil) { t.Fatalf("received: '%v' but expected: '%v'", err, ErrAssetIsNil) } - err = pm.IsAssetPairEnabled(asset.PerpetualSwap, EMPTYPAIR) + _, err = pm.IsPairEnabled(EMPTYPAIR, asset.PerpetualSwap) if !errors.Is(err, ErrCurrencyPairEmpty) { t.Fatalf("received: '%v' but expected: '%v'", err, ErrCurrencyPairEmpty) } diff --git a/currency/manager_types.go b/currency/manager_types.go index 2b94d7cad0b..3382cb1a4e9 100644 --- a/currency/manager_types.go +++ b/currency/manager_types.go @@ -8,13 +8,14 @@ import ( // PairsManager manages asset pairs type PairsManager struct { - BypassConfigFormatUpgrades bool `json:"bypassConfigFormatUpgrades"` - RequestFormat *PairFormat `json:"requestFormat,omitempty"` - ConfigFormat *PairFormat `json:"configFormat,omitempty"` - UseGlobalFormat bool `json:"useGlobalFormat,omitempty"` - LastUpdated int64 `json:"lastUpdated,omitempty"` - Pairs FullStore `json:"pairs"` - mutex sync.RWMutex `json:"-"` + BypassConfigFormatUpgrades bool `json:"bypassConfigFormatUpgrades"` + RequestFormat *PairFormat `json:"requestFormat,omitempty"` + ConfigFormat *PairFormat `json:"configFormat,omitempty"` + UseGlobalFormat bool `json:"useGlobalFormat,omitempty"` + LastUpdated int64 `json:"lastUpdated,omitempty"` + Pairs FullStore `json:"pairs"` + matcher map[key]*Pair + mutex sync.RWMutex } // FullStore holds all supported asset types with the enabled and available @@ -37,3 +38,9 @@ type PairFormat struct { Separator string `json:"separator,omitempty"` Index string `json:"index,omitempty"` } + +// key is used to store the asset type and symbol in a map +type key struct { + Symbol string + Asset asset.Item +} diff --git a/currency/pair.go b/currency/pair.go index 975eeb9704f..fe6c1716f39 100644 --- a/currency/pair.go +++ b/currency/pair.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strings" + "unicode" ) var errCannotCreatePair = errors.New("cannot create currency pair") @@ -100,23 +101,17 @@ func NewPairFromString(currencyPair string) (Pair, error) { errCannotCreatePair, currencyPair) } - var delimiter string - pairStrings := []string{currencyPair} - for x := range delimiters { - if strings.Contains(pairStrings[0], delimiters[x]) { - values := strings.SplitN(pairStrings[0], delimiters[x], 2) - if delimiter != "" { - values[1] += delimiter + pairStrings[1] - pairStrings = values - } else { - pairStrings = values - } - delimiter = delimiters[x] + + for x := range currencyPair { + if unicode.IsPunct(rune(currencyPair[x])) { + return Pair{ + Base: NewCode(currencyPair[:x]), + Delimiter: string(currencyPair[x]), + Quote: NewCode(currencyPair[x+1:]), + }, nil } } - if delimiter != "" { - return Pair{Base: NewCode(pairStrings[0]), Delimiter: delimiter, Quote: NewCode(pairStrings[1])}, nil - } + return NewPairFromStrings(currencyPair[0:3], currencyPair[3:]) } diff --git a/currency/pair_methods.go b/currency/pair_methods.go index 7add2bece6c..90689e1cfcc 100644 --- a/currency/pair_methods.go +++ b/currency/pair_methods.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "unicode" ) // EMPTYFORMAT defines an empty pair format @@ -45,8 +46,21 @@ func (p *Pair) UnmarshalJSON(d []byte) error { return nil } - *p, err = NewPairFromString(pair) - return err + // Check if pair is in the format of BTC-USD + for x := range pair { + if unicode.IsPunct(rune(pair[x])) { + p.Base = NewCode(pair[:x]) + p.Delimiter = string(pair[x]) + p.Quote = NewCode(pair[x+1:]) + return nil + } + } + + // NOTE: Pair string could be in format DUSKUSDT (Kucoin) which will be + // incorrectly converted to DUS-KUSDT, ELKRW (Bithumb) which will convert + // converted to ELK-RW and HTUSDT (Lbank) which will be incorrectly + // converted to HTU-SDT. + return fmt.Errorf("%w from %s cannot ensure pair is in correct format, please use exchange method MatchSymbolWithAvailablePairs", errCannotCreatePair, pair) } // MarshalJSON conforms type to the marshaler interface diff --git a/currency/pair_types.go b/currency/pair_types.go index 2db8832ffe3..a1086d48c21 100644 --- a/currency/pair_types.go +++ b/currency/pair_types.go @@ -1,6 +1,8 @@ package currency // Pair holds currency pair information +// NOTE: UnmarshalJSON allows string conversion to Pair type but only if there +// is a delimiter present in the string, otherwise it will return an error. type Pair struct { Delimiter string `json:"delimiter,omitempty"` Base Code `json:"base,omitempty"` diff --git a/currency/pairs.go b/currency/pairs.go index abf8beeecf8..887b46fcbc9 100644 --- a/currency/pairs.go +++ b/currency/pairs.go @@ -303,7 +303,8 @@ func (p Pairs) GetRandomPair() (Pair, error) { } // DeriveFrom matches symbol string to the available pairs list when no -// delimiter is supplied. +// delimiter is supplied. WARNING: This is not optimised and should only be used +// for one off processes. func (p Pairs) DeriveFrom(symbol string) (Pair, error) { if len(p) == 0 { return EMPTYPAIR, ErrCurrencyPairsEmpty diff --git a/engine/rpcserver.go b/engine/rpcserver.go index e2637ca0374..d7f27c68ff4 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -78,6 +78,7 @@ var ( errGRPCShutdownSignalIsNil = errors.New("cannot shutdown, gRPC shutdown channel is nil") errInvalidStrategy = errors.New("invalid strategy") errSpecificPairNotEnabled = errors.New("specified pair is not enabled") + errPairNotEnabled = errors.New("pair is not enabled") ) // RPCServer struct @@ -4686,15 +4687,20 @@ func (s *RPCServer) GetFundingRates(ctx context.Context, r *gctrpc.GetFundingRat return nil, err } - pairs, err := exch.GetEnabledPairs(a) + cp, err := exch.MatchSymbolWithAvailablePairs(r.Pair.Base+r.Pair.Quote, a, false) if err != nil { return nil, err } - cp, err := pairs.DeriveFrom(r.Pair.Base + r.Pair.Quote) + + pairs, err := exch.GetEnabledPairs(a) if err != nil { return nil, err } + if !pairs.Contains(cp, true) { + return nil, fmt.Errorf("%w %v", errPairNotEnabled, cp) + } + funding, err := exch.GetFundingRates(ctx, &fundingrate.RatesRequest{ Asset: a, Pair: cp, @@ -4779,14 +4785,20 @@ func (s *RPCServer) GetLatestFundingRate(ctx context.Context, r *gctrpc.GetLates return nil, fmt.Errorf("%s %w", a, futures.ErrNotFuturesAsset) } - pairs, err := exch.GetEnabledPairs(a) + cp, err := exch.MatchSymbolWithAvailablePairs(r.Pair.Base+r.Pair.Quote, a, false) if err != nil { return nil, err } - cp, err := pairs.DeriveFrom(r.Pair.Base + r.Pair.Quote) + + pairs, err := exch.GetEnabledPairs(a) if err != nil { return nil, err } + + if !pairs.Contains(cp, true) { + return nil, fmt.Errorf("%w %v", errPairNotEnabled, cp) + } + funding, err := exch.GetLatestFundingRate(ctx, &fundingrate.LatestRateRequest{ Asset: a, Pair: cp, diff --git a/engine/rpcserver_test.go b/engine/rpcserver_test.go index a1a9823c738..522c66eee2f 100644 --- a/engine/rpcserver_test.go +++ b/engine/rpcserver_test.go @@ -2927,19 +2927,26 @@ func TestGetFundingRates(t *testing.T) { } b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) - b.CurrencyPairs.Pairs[asset.Futures] = ¤cy.PairStore{ + err = b.CurrencyPairs.Store(asset.Futures, ¤cy.PairStore{ AssetEnabled: convert.BoolPtr(true), RequestFormat: ¤cy.PairFormat{Delimiter: "-"}, ConfigFormat: ¤cy.PairFormat{Delimiter: "-"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, + }) + if err != nil { + t.Fatal(err) } - b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ + + err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ AssetEnabled: convert.BoolPtr(true), ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, + }) + if err != nil { + t.Fatal(err) } b.Features.Supports.FuturesCapabilities.FundingRates = true fakeExchange := fExchange{ @@ -3028,19 +3035,25 @@ func TestGetLatestFundingRate(t *testing.T) { } b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) - b.CurrencyPairs.Pairs[asset.Futures] = ¤cy.PairStore{ + err = b.CurrencyPairs.Store(asset.Futures, ¤cy.PairStore{ AssetEnabled: convert.BoolPtr(true), RequestFormat: ¤cy.PairFormat{Delimiter: "-"}, ConfigFormat: ¤cy.PairFormat{Delimiter: "-"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, + }) + if err != nil { + t.Fatal(err) } - b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ + err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ AssetEnabled: convert.BoolPtr(true), ConfigFormat: ¤cy.PairFormat{Delimiter: "/"}, RequestFormat: ¤cy.PairFormat{Delimiter: "/"}, Available: currency.Pairs{cp}, Enabled: currency.Pairs{cp}, + }) + if err != nil { + t.Fatal(err) } fakeExchange := fExchange{ IBotExchange: exch, diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 830d3c58d72..c7bd983b069 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -356,20 +356,20 @@ func (b *Bitfinex) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) // UpdateTickers updates the ticker for all currency pairs of a given asset type func (b *Bitfinex) UpdateTickers(ctx context.Context, a asset.Item) error { - enabled, err := b.GetEnabledPairs(a) - if err != nil { - return err - } - tickerNew, err := b.GetTickerBatch(ctx) if err != nil { return err } for key, val := range tickerNew { - pair, err := enabled.DeriveFrom(strings.Replace(key, ":", "", 1)[1:]) + pair, enabled, err := b.MatchSymbolCheckEnabled(key[1:], a, true) if err != nil { - // GetTickerBatch returns all pairs in call across all asset types. + if !errors.Is(err, currency.ErrPairNotFound) { + return err + } + } + + if !enabled { continue } diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index 9c6016816cf..fc4e7c634b9 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -1098,6 +1098,7 @@ func TestGetHistoricTrades(t *testing.T) { } func TestUpdateTicker(t *testing.T) { + t.Parallel() cp := currency.NewPair(currency.ETH, currency.USD) _, err := b.UpdateTicker(context.Background(), cp, asset.PerpetualContract) if err != nil { diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 2660fd6d165..2e73a118fd3 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -364,11 +364,7 @@ func (b *Bitmex) UpdateTickers(ctx context.Context, a asset.Item) error { return err } - enabled, err := b.GetEnabledPairs(a) - if err != nil { - return err - } - + var enabled bool instruments: for j := range tick { var pair currency.Pair @@ -377,7 +373,7 @@ instruments: if tick[j].Typ != futuresID { continue instruments } - pair, err = enabled.DeriveFrom(tick[j].Symbol) + pair, enabled, err = b.MatchSymbolCheckEnabled(tick[j].Symbol, a, false) case asset.Index: switch tick[j].Typ { case bitMEXBasketIndexID, @@ -392,23 +388,27 @@ instruments: // contain an underscore. Calling DeriveFrom will then error and // the instruments will be missed. tick[j].Symbol = strings.Replace(tick[j].Symbol, currency.UnderscoreDelimiter, "", 1) - pair, err = enabled.DeriveFrom(tick[j].Symbol) + pair, enabled, err = b.MatchSymbolCheckEnabled(tick[j].Symbol, a, false) case asset.PerpetualContract: if tick[j].Typ != perpetualContractID { continue instruments } - pair, err = enabled.DeriveFrom(tick[j].Symbol) + pair, enabled, err = b.MatchSymbolCheckEnabled(tick[j].Symbol, a, false) case asset.Spot: if tick[j].Typ != spotID { continue instruments } tick[j].Symbol = strings.Replace(tick[j].Symbol, currency.UnderscoreDelimiter, "", 1) - pair, err = enabled.DeriveFrom(tick[j].Symbol) + pair, enabled, err = b.MatchSymbolCheckEnabled(tick[j].Symbol, a, false) } + if err != nil { if !errors.Is(err, currency.ErrPairNotFound) { return err } + } + + if !enabled { continue } diff --git a/exchanges/bitstamp/bitstamp_type_convert.go b/exchanges/bitstamp/bitstamp_type_convert.go index e0af07f1c0d..94af150ffe0 100644 --- a/exchanges/bitstamp/bitstamp_type_convert.go +++ b/exchanges/bitstamp/bitstamp_type_convert.go @@ -44,13 +44,22 @@ func (t *microTimestamp) UnmarshalJSON(data []byte) error { return err } + if strconv.IntSize == 32 && len(s) >= 10 { + i, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + *t = microTimestamp(time.UnixMicro(i)) + return nil + } + + // Has Fast path optimisation when int == 64 i, err := strconv.Atoi(s) if err != nil { return err } *t = microTimestamp(time.UnixMicro(int64(i))) - return nil } diff --git a/exchanges/bybit/bybit_test.go b/exchanges/bybit/bybit_test.go index 8be27d7ae2a..ed8e68fb930 100644 --- a/exchanges/bybit/bybit_test.go +++ b/exchanges/bybit/bybit_test.go @@ -2042,7 +2042,7 @@ func TestUpdateTicker(t *testing.T) { var pairs currency.Pairs if mockTests { var pair2 currency.Pair - pair2, err = currency.NewPairFromString("BTCUSD-U23") + pair2, err = currency.NewPairFromString("BTCUSD-Z23") if err != nil { t.Fatal(err) } @@ -2230,7 +2230,7 @@ func TestGetHistoricCandles(t *testing.T) { } var pair2 currency.Pair if mockTests { - pair2, err = currency.NewPairFromString("BTCUSD-U23") + pair2, err = currency.NewPairFromString("BTCUSD-Z23") if err != nil { t.Fatal(err) } @@ -2292,7 +2292,7 @@ func TestGetHistoricCandlesExtended(t *testing.T) { } var pair2 currency.Pair if mockTests { - pair2, err = currency.NewPairFromString("BTCUSD-U23") + pair2, err = currency.NewPairFromString("BTCUSD-Z23") if err != nil { t.Fatal(err) } diff --git a/exchanges/bybit/bybit_websocket.go b/exchanges/bybit/bybit_websocket.go index 182910cfdf2..4f7ee986fc5 100644 --- a/exchanges/bybit/bybit_websocket.go +++ b/exchanges/bybit/bybit_websocket.go @@ -266,11 +266,15 @@ func (by *Bybit) wsHandleData(respRaw []byte) error { if err != nil { return err } - p, err := by.extractCurrencyPair(data.OBData.Symbol, asset.Spot) + p, enabled, err := by.MatchSymbolCheckEnabled(data.OBData.Symbol, asset.Spot, false) if err != nil { return err } + if !enabled { + return nil + } + err = by.wsUpdateOrderbook(&data.OBData, p, asset.Spot) if err != nil { return err @@ -286,11 +290,15 @@ func (by *Bybit) wsHandleData(respRaw []byte) error { return err } - p, err := by.extractCurrencyPair(data.Parameters.Symbol, asset.Spot) + p, enabled, err := by.MatchSymbolCheckEnabled(data.Parameters.Symbol, asset.Spot, false) if err != nil { return err } + if !enabled { + return nil + } + side := order.Sell if data.TradeData.Side { side = order.Buy @@ -313,11 +321,15 @@ func (by *Bybit) wsHandleData(respRaw []byte) error { return err } - p, err := by.extractCurrencyPair(data.Ticker.Symbol, asset.Spot) + p, enabled, err := by.MatchSymbolCheckEnabled(data.Ticker.Symbol, asset.Spot, false) if err != nil { return err } + if !enabled { + return nil + } + by.Websocket.DataHandler <- &ticker.Price{ ExchangeName: by.Name, Bid: data.Ticker.Bid.Float64(), @@ -334,11 +346,15 @@ func (by *Bybit) wsHandleData(respRaw []byte) error { return err } - p, err := by.extractCurrencyPair(data.Kline.Symbol, asset.Spot) + p, enabled, err := by.MatchSymbolCheckEnabled(data.Kline.Symbol, asset.Spot, false) if err != nil { return err } + if !enabled { + return nil + } + by.Websocket.DataHandler <- stream.KlineData{ Pair: p, AssetType: asset.Spot, @@ -425,11 +441,15 @@ func (by *Bybit) wsHandleData(respRaw []byte) error { } } - p, err := by.extractCurrencyPair(data[j].Symbol, asset.Spot) + p, enabled, err := by.MatchSymbolCheckEnabled(data[j].Symbol, asset.Spot, false) if err != nil { return err } + if !enabled { + continue + } + by.Websocket.DataHandler <- order.Detail{ Price: data[j].Price.Float64(), Amount: data[j].Quantity.Float64(), @@ -475,11 +495,15 @@ func (by *Bybit) wsHandleData(respRaw []byte) error { } } - p, err := by.extractCurrencyPair(data[j].Symbol, asset.Spot) + p, enabled, err := by.MatchSymbolCheckEnabled(data[j].Symbol, asset.Spot, false) if err != nil { return err } + if !enabled { + continue + } + by.Websocket.DataHandler <- &order.Detail{ Exchange: by.Name, OrderID: data[j].OrderID, diff --git a/exchanges/bybit/bybit_wrapper.go b/exchanges/bybit/bybit_wrapper.go index 56afbfd9298..bfff780cca8 100644 --- a/exchanges/bybit/bybit_wrapper.go +++ b/exchanges/bybit/bybit_wrapper.go @@ -2,6 +2,7 @@ package bybit import ( "context" + "errors" "fmt" "sort" "strconv" @@ -416,16 +417,6 @@ func (by *Bybit) UpdateTradablePairs(ctx context.Context, forceUpdate bool) erro // UpdateTickers updates the ticker for all currency pairs of a given asset type func (by *Bybit) UpdateTickers(ctx context.Context, assetType asset.Item) error { - avail, err := by.GetAvailablePairs(assetType) - if err != nil { - return err - } - - enabled, err := by.GetEnabledPairs(assetType) - if err != nil { - return err - } - switch assetType { case asset.Spot: ticks, err := by.GetTickersV5(ctx, "spot", "", "") @@ -434,17 +425,19 @@ func (by *Bybit) UpdateTickers(ctx context.Context, assetType asset.Item) error } for x := range ticks.List { - pair, err := avail.DeriveFrom(ticks.List[x].Symbol) + pair, enabled, err := by.MatchSymbolCheckEnabled(ticks.List[x].Symbol, assetType, false) if err != nil { // These symbols below do not have a spot market but are in fact // perpetuals. if ticks.List[x].Symbol == "ZECUSDT" || ticks.List[x].Symbol == "DASHUSDT" { continue } - return err + if !errors.Is(err, currency.ErrPairNotFound) { + return err + } } - if !enabled.Contains(pair, true) { + if !enabled { continue } @@ -465,6 +458,11 @@ func (by *Bybit) UpdateTickers(ctx context.Context, assetType asset.Item) error } } case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.Futures: + enabled, err := by.GetEnabledPairs(assetType) + if err != nil { + return err + } + tick, err := by.GetFuturesSymbolPriceTicker(ctx, currency.EMPTYPAIR) if err != nil { return err @@ -480,7 +478,8 @@ func (by *Bybit) UpdateTickers(ctx context.Context, assetType asset.Item) error if tick[y].Symbol != formattedPair.String() { continue } - cp, err := by.extractCurrencyPair(tick[y].Symbol, assetType) + // Don't need to check if this pair is enabled due to call above. + cp, err := by.MatchSymbolWithAvailablePairs(tick[y].Symbol, assetType, false) if err != nil { return err } @@ -501,6 +500,11 @@ func (by *Bybit) UpdateTickers(ctx context.Context, assetType asset.Item) error } } case asset.USDCMarginedFutures: + enabled, err := by.GetEnabledPairs(assetType) + if err != nil { + return err + } + for x := range enabled { formattedPair, err := by.FormatExchangeCurrency(enabled[x], assetType) if err != nil { @@ -512,7 +516,8 @@ func (by *Bybit) UpdateTickers(ctx context.Context, assetType asset.Item) error return err } - cp, err := by.extractCurrencyPair(tick.Symbol, assetType) + // Don't need to check if this pair is enabled due to call above. + cp, err := by.MatchSymbolWithAvailablePairs(tick.Symbol, assetType, false) if err != nil { return err } @@ -551,10 +556,13 @@ func (by *Bybit) UpdateTicker(ctx context.Context, p currency.Pair, assetType as } for y := range tick { - cp, err := by.extractCurrencyPair(tick[y].Symbol, assetType) + cp, enabled, err := by.MatchSymbolCheckEnabled(tick[y].Symbol, assetType, false) if err != nil { return nil, err } + if !enabled { + continue + } err = ticker.ProcessTicker(&ticker.Price{ Last: tick[y].LastPrice.Float64(), High: tick[y].HighPrice.Float64(), @@ -572,7 +580,6 @@ func (by *Bybit) UpdateTicker(ctx context.Context, p currency.Pair, assetType as return nil, err } } - case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.Futures: tick, err := by.GetFuturesSymbolPriceTicker(ctx, formattedPair) if err != nil { @@ -580,10 +587,13 @@ func (by *Bybit) UpdateTicker(ctx context.Context, p currency.Pair, assetType as } for y := range tick { - cp, err := by.extractCurrencyPair(tick[y].Symbol, assetType) + cp, enabled, err := by.MatchSymbolCheckEnabled(tick[y].Symbol, assetType, false) if err != nil { return nil, err } + if !enabled { + continue + } err = ticker.ProcessTicker(&ticker.Price{ Last: tick[y].LastPrice.Float64(), High: tick[y].HighPrice24h.Float64(), @@ -606,10 +616,13 @@ func (by *Bybit) UpdateTicker(ctx context.Context, p currency.Pair, assetType as return nil, err } - cp, err := by.extractCurrencyPair(tick.Symbol, assetType) + cp, enabled, err := by.MatchSymbolCheckEnabled(tick.Symbol, assetType, false) if err != nil { return nil, err } + if !enabled { + return nil, fmt.Errorf("%v %v not enabled", formattedPair, assetType) + } err = ticker.ProcessTicker(&ticker.Price{ Last: tick.LastPrice.Float64(), High: tick.High24h.Float64(), @@ -2097,30 +2110,12 @@ func (by *Bybit) GetServerTime(ctx context.Context, a asset.Item) (time.Time, er return time.Time{}, fmt.Errorf("%s %w", a, asset.ErrNotSupported) } -func (by *Bybit) extractCurrencyPair(symbol string, item asset.Item) (currency.Pair, error) { - pairs, err := by.CurrencyPairs.GetPairs(item, true) - if err != nil { - return currency.EMPTYPAIR, err - } - pair, err := pairs.DeriveFrom(symbol) - if err != nil { - return currency.EMPTYPAIR, err - } - return pair, nil -} - // UpdateOrderExecutionLimits sets exchange executions for a required asset type func (by *Bybit) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { - avail, err := by.GetAvailablePairs(a) - if err != nil { - return err - } - var limits []order.MinMaxLevel switch a { case asset.Spot: - var pairsData []PairData - pairsData, err = by.GetAllSpotPairs(ctx) + pairsData, err := by.GetAllSpotPairs(ctx) if err != nil { return err } @@ -2128,11 +2123,15 @@ func (by *Bybit) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) e limits = make([]order.MinMaxLevel, 0, len(pairsData)) for x := range pairsData { var pair currency.Pair - pair, err = avail.DeriveFrom(pairsData[x].Name) + var enabled bool + pair, enabled, err = by.MatchSymbolCheckEnabled(pairsData[x].Name, a, false) if err != nil { log.Warnf(log.ExchangeSys, "%s unable to load limits for %v, pair data missing", by.Name, pairsData[x].Name) continue } + if !enabled { + continue + } limits = append(limits, order.MinMaxLevel{ Asset: a, diff --git a/exchanges/bybit/bybit_ws_cfutures.go b/exchanges/bybit/bybit_ws_cfutures.go index ec63ada3e0e..39ca9e296da 100644 --- a/exchanges/bybit/bybit_ws_cfutures.go +++ b/exchanges/bybit/bybit_ws_cfutures.go @@ -215,6 +215,7 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { if wsType, ok := multiStreamData["type"].(string); ok { switch topics[0] { case wsOrder25, wsOrder200: + var enabled bool switch wsType { case wsOperationSnapshot: var response WsFuturesOrderbook @@ -224,11 +225,15 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { } var p currency.Pair - p, err = by.extractCurrencyPair(response.OBData[0].Symbol, asset.CoinMarginedFutures) + p, enabled, err = by.MatchSymbolCheckEnabled(response.OBData[0].Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } + if !enabled { + return nil + } + err = by.processOrderbook(response.OBData, response.Type, p, @@ -236,7 +241,6 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { if err != nil { return err } - case wsOperationDelta: var response WsCoinDeltaOrderbook err = json.Unmarshal(respRaw, &response) @@ -246,11 +250,15 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { if len(response.OBData.Delete) > 0 { var p currency.Pair - p, err = by.extractCurrencyPair(response.OBData.Delete[0].Symbol, asset.CoinMarginedFutures) + p, enabled, err = by.MatchSymbolCheckEnabled(response.OBData.Delete[0].Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } + if !enabled { + return nil + } + err = by.processOrderbook(response.OBData.Delete, wsOrderbookActionDelete, p, @@ -262,11 +270,15 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { if len(response.OBData.Update) > 0 { var p currency.Pair - p, err = by.extractCurrencyPair(response.OBData.Update[0].Symbol, asset.CoinMarginedFutures) + p, enabled, err = by.MatchSymbolCheckEnabled(response.OBData.Update[0].Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } + if !enabled { + return nil + } + err = by.processOrderbook(response.OBData.Update, wsOrderbookActionUpdate, p, @@ -278,11 +290,15 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { if len(response.OBData.Insert) > 0 { var p currency.Pair - p, err = by.extractCurrencyPair(response.OBData.Insert[0].Symbol, asset.CoinMarginedFutures) + p, enabled, err = by.MatchSymbolCheckEnabled(response.OBData.Insert[0].Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } + if !enabled { + return nil + } + err = by.processOrderbook(response.OBData.Insert, wsOrderbookActionInsert, p, @@ -308,7 +324,7 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { trades := make([]trade.Data, len(response.TradeData)) for i := range response.TradeData { var p currency.Pair - p, err = by.extractCurrencyPair(response.TradeData[0].Symbol, asset.CoinMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.TradeData[0].Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } @@ -344,7 +360,7 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { } var p currency.Pair - p, err = by.extractCurrencyPair(topics[len(topics)-1], asset.CoinMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(topics[len(topics)-1], asset.CoinMarginedFutures, false) if err != nil { return err } @@ -382,7 +398,7 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { } var p currency.Pair - p, err = by.extractCurrencyPair(response.Ticker.Symbol, asset.CoinMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Ticker.Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } @@ -411,7 +427,7 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { if len(response.Data.Delete) > 0 { for x := range response.Data.Delete { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data.Delete[x].Symbol, asset.CoinMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data.Delete[x].Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } @@ -435,7 +451,7 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { if len(response.Data.Update) > 0 { for x := range response.Data.Update { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data.Update[x].Symbol, asset.CoinMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data.Update[x].Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } @@ -459,7 +475,7 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { if len(response.Data.Insert) > 0 { for x := range response.Data.Insert { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data.Insert[x].Symbol, asset.CoinMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data.Insert[x].Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } @@ -510,7 +526,7 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { for i := range response.Data { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data[i].Symbol, asset.CoinMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data[i].Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } @@ -566,7 +582,7 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { } for x := range response.Data { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data[x].Symbol, asset.CoinMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data[x].Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } @@ -628,7 +644,7 @@ func (by *Bybit) wsCoinHandleData(respRaw []byte) error { } for x := range response.Data { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data[x].Symbol, asset.CoinMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data[x].Symbol, asset.CoinMarginedFutures, false) if err != nil { return err } diff --git a/exchanges/bybit/bybit_ws_futures.go b/exchanges/bybit/bybit_ws_futures.go index 8eb6fe5132c..d5f2ef84599 100644 --- a/exchanges/bybit/bybit_ws_futures.go +++ b/exchanges/bybit/bybit_ws_futures.go @@ -176,7 +176,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { } var p currency.Pair - p, err = by.extractCurrencyPair(response.OBData[0].Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.OBData[0].Symbol, asset.Futures, false) if err != nil { return err } @@ -198,7 +198,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { if len(response.OBData.Delete) > 0 { var p currency.Pair - p, err = by.extractCurrencyPair(response.OBData.Delete[0].Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.OBData.Delete[0].Symbol, asset.Futures, false) if err != nil { return err } @@ -213,7 +213,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { if len(response.OBData.Update) > 0 { var p currency.Pair - p, err = by.extractCurrencyPair(response.OBData.Update[0].Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.OBData.Update[0].Symbol, asset.Futures, false) if err != nil { return err } @@ -229,7 +229,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { if len(response.OBData.Insert) > 0 { var p currency.Pair - p, err = by.extractCurrencyPair(response.OBData.Insert[0].Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.OBData.Insert[0].Symbol, asset.Futures, false) if err != nil { return err } @@ -261,7 +261,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { trades := make([]trade.Data, len(response.TradeData)) for i := range response.TradeData { var p currency.Pair - p, err = by.extractCurrencyPair(response.TradeData[0].Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.TradeData[0].Symbol, asset.Futures, false) if err != nil { return err } @@ -297,7 +297,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { } var p currency.Pair - p, err = by.extractCurrencyPair(topics[len(topics)-1], asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(topics[len(topics)-1], asset.Futures, false) if err != nil { return err } @@ -327,7 +327,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { } var p currency.Pair - p, err = by.extractCurrencyPair(response.Ticker.Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.Ticker.Symbol, asset.Futures, false) if err != nil { return err } @@ -356,7 +356,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { if len(response.Data.Delete) > 0 { for x := range response.Data.Delete { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data.Delete[x].Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data.Delete[x].Symbol, asset.Futures, false) if err != nil { return err } @@ -380,7 +380,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { if len(response.Data.Update) > 0 { for x := range response.Data.Update { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data.Update[x].Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data.Update[x].Symbol, asset.Futures, false) if err != nil { return err } @@ -404,7 +404,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { if len(response.Data.Insert) > 0 { for x := range response.Data.Insert { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data.Insert[x].Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data.Insert[x].Symbol, asset.Futures, false) if err != nil { return err } @@ -455,7 +455,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { for i := range response.Data { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data[i].Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data[i].Symbol, asset.Futures, false) if err != nil { return err } @@ -509,7 +509,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { } for x := range response.Data { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data[x].Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data[x].Symbol, asset.Futures, false) if err != nil { return err } @@ -571,7 +571,7 @@ func (by *Bybit) wsFuturesHandleData(respRaw []byte) error { } for x := range response.Data { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data[x].Symbol, asset.Futures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data[x].Symbol, asset.Futures, false) if err != nil { return err } diff --git a/exchanges/bybit/bybit_ws_ufutures.go b/exchanges/bybit/bybit_ws_ufutures.go index 2a42ec18aaa..73f7fd0e457 100644 --- a/exchanges/bybit/bybit_ws_ufutures.go +++ b/exchanges/bybit/bybit_ws_ufutures.go @@ -184,7 +184,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { } var p currency.Pair - p, err = by.extractCurrencyPair(response.Data.OBData[0].Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data.OBData[0].Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } @@ -206,7 +206,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { if len(response.OBData.Delete) > 0 { var p currency.Pair - p, err = by.extractCurrencyPair(response.OBData.Delete[0].Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.OBData.Delete[0].Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } @@ -222,7 +222,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { if len(response.OBData.Update) > 0 { var p currency.Pair - p, err = by.extractCurrencyPair(response.OBData.Update[0].Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.OBData.Update[0].Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } @@ -238,7 +238,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { if len(response.OBData.Insert) > 0 { var p currency.Pair - p, err = by.extractCurrencyPair(response.OBData.Insert[0].Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.OBData.Insert[0].Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } @@ -268,7 +268,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { trades := make([]trade.Data, len(response.TradeData)) for i := range response.TradeData { var p currency.Pair - p, err = by.extractCurrencyPair(response.TradeData[0].Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.TradeData[0].Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } @@ -303,7 +303,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { } var p currency.Pair - p, err = by.extractCurrencyPair(topics[len(topics)-1], asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(topics[len(topics)-1], asset.USDTMarginedFutures, false) if err != nil { return err } @@ -333,7 +333,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { } var p currency.Pair - p, err = by.extractCurrencyPair(response.Ticker.Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Ticker.Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } @@ -362,7 +362,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { if len(response.Data.Delete) > 0 { for x := range response.Data.Delete { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data.Delete[x].Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data.Delete[x].Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } @@ -386,7 +386,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { if len(response.Data.Update) > 0 { for x := range response.Data.Update { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data.Update[x].Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data.Update[x].Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } @@ -410,7 +410,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { if len(response.Data.Insert) > 0 { for x := range response.Data.Insert { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data.Insert[x].Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data.Insert[x].Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } @@ -461,7 +461,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { for i := range response.Data { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data[i].Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data[i].Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } @@ -515,7 +515,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { } for x := range response.Data { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data[x].Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data[x].Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } @@ -577,7 +577,7 @@ func (by *Bybit) wsUSDTHandleData(respRaw []byte) error { } for x := range response.Data { var p currency.Pair - p, err = by.extractCurrencyPair(response.Data[x].Symbol, asset.USDTMarginedFutures) + p, err = by.MatchSymbolWithAvailablePairs(response.Data[x].Symbol, asset.USDTMarginedFutures, false) if err != nil { return err } diff --git a/exchanges/coinut/coinut_types.go b/exchanges/coinut/coinut_types.go index abd7400ead4..f935aa17a46 100644 --- a/exchanges/coinut/coinut_types.go +++ b/exchanges/coinut/coinut_types.go @@ -125,8 +125,8 @@ type OrderResponse struct { // Commission holds trade commission structure type Commission struct { - Currency currency.Pair `json:"currency"` - Amount float64 `json:"amount,string"` + Currency string `json:"currency"` + Amount float64 `json:"amount,string"` } // OrderFilledResponse contains order filled response @@ -545,8 +545,8 @@ type WsOrderData struct { // WsOrderFilledCommissionData ws response data type WsOrderFilledCommissionData struct { - Amount float64 `json:"amount,string"` - Currency currency.Pair `json:"currency"` + Amount float64 `json:"amount,string"` + Currency string `json:"currency"` } // WsOrderRejectedResponse ws response @@ -594,8 +594,8 @@ type WsTradeHistoryResponse struct { // WsTradeHistoryCommissionData ws response data type WsTradeHistoryCommissionData struct { - Amount float64 `json:"amount,string"` - Currency currency.Pair `json:"currency"` + Amount float64 `json:"amount,string"` + Currency string `json:"currency"` } // WsTradeHistoryTradeData ws response data diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 94ccfc0beb9..a2bf0f3287c 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" "time" + "unicode" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" @@ -1731,3 +1732,38 @@ func (b *Base) SetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ m func (b *Base) GetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ order.Side) (float64, error) { return -1, common.ErrNotYetImplemented } + +// MatchSymbolWithAvailablePairs returns a currency pair based on the supplied +// symbol and asset type. If the string is expected to have a delimiter this +// will attempt to screen it out. +func (b *Base) MatchSymbolWithAvailablePairs(symbol string, a asset.Item, hasDelimiter bool) (currency.Pair, error) { + if hasDelimiter { + for x := range symbol { + if unicode.IsPunct(rune(symbol[x])) { + symbol = symbol[:x] + symbol[x+1:] + break + } + } + } + return b.CurrencyPairs.Match(symbol, a) +} + +// MatchSymbolCheckEnabled returns a currency pair based on the supplied symbol +// and asset type against the available pairs list. If the string is expected to +// have a delimiter this will attempt to screen it out. It will also check if +// the pair is enabled. +func (b *Base) MatchSymbolCheckEnabled(symbol string, a asset.Item, hasDelimiter bool) (pair currency.Pair, enabled bool, err error) { + pair, err = b.MatchSymbolWithAvailablePairs(symbol, a, hasDelimiter) + if err != nil { + return pair, false, err + } + + enabled, err = b.IsPairEnabled(pair, a) + return +} + +// IsPairEnabled checks if a pair is enabled for an enabled asset type. +// TODO: Optimisation map for enabled pair matching, instead of linear traversal. +func (b *Base) IsPairEnabled(pair currency.Pair, a asset.Item) (bool, error) { + return b.CurrencyPairs.IsPairEnabled(pair, a) +} diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index fed98ae7e4a..6c772da0d84 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -3089,3 +3089,139 @@ func TestGetStandardConfig(t *testing.T) { t.Fatalf("received: '%v' but expected: '%v'", cfg.WebsocketTrafficTimeout, config.DefaultWebsocketTrafficTimeout) } } + +func TestMatchSymbolWithAvailablePairs(t *testing.T) { + t.Parallel() + b := Base{Name: "test"} + whatIWant := currency.NewPair(currency.BTC, currency.USDT) + err := b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ + AssetEnabled: convert.BoolPtr(true), + Available: []currency.Pair{whatIWant}}) + if err != nil { + t.Fatal(err) + } + + _, err = b.MatchSymbolWithAvailablePairs("sillBillies", asset.Futures, false) + if !errors.Is(err, currency.ErrPairNotFound) { + t.Fatalf("received: '%v' but expected: '%v'", err, currency.ErrPairNotFound) + } + + whatIGot, err := b.MatchSymbolWithAvailablePairs("btcusdT", asset.Spot, false) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if !whatIGot.Equal(whatIWant) { + t.Fatalf("received: '%v' but expected: '%v'", whatIGot, whatIWant) + } + + whatIGot, err = b.MatchSymbolWithAvailablePairs("btc-usdT", asset.Spot, true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if !whatIGot.Equal(whatIWant) { + t.Fatalf("received: '%v' but expected: '%v'", whatIGot, whatIWant) + } +} + +func TestMatchSymbolCheckEnabled(t *testing.T) { + t.Parallel() + b := Base{Name: "test"} + whatIWant := currency.NewPair(currency.BTC, currency.USDT) + availButNoEnabled := currency.NewPair(currency.BTC, currency.AUD) + err := b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ + AssetEnabled: convert.BoolPtr(true), + Available: []currency.Pair{whatIWant, availButNoEnabled}, + Enabled: []currency.Pair{whatIWant}, + }) + if err != nil { + t.Fatal(err) + } + + _, _, err = b.MatchSymbolCheckEnabled("sillBillies", asset.Futures, false) + if !errors.Is(err, currency.ErrPairNotFound) { + t.Fatalf("received: '%v' but expected: '%v'", err, currency.ErrPairNotFound) + } + + whatIGot, enabled, err := b.MatchSymbolCheckEnabled("btcusdT", asset.Spot, false) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if !enabled { + t.Fatal("expected true") + } + + if !whatIGot.Equal(whatIWant) { + t.Fatalf("received: '%v' but expected: '%v'", whatIGot, whatIWant) + } + + whatIGot, enabled, err = b.MatchSymbolCheckEnabled("btc-usdT", asset.Spot, true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if !whatIGot.Equal(whatIWant) { + t.Fatalf("received: '%v' but expected: '%v'", whatIGot, whatIWant) + } + + if !enabled { + t.Fatal("expected true") + } + + whatIGot, enabled, err = b.MatchSymbolCheckEnabled("btc-AUD", asset.Spot, true) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if !whatIGot.Equal(availButNoEnabled) { + t.Fatalf("received: '%v' but expected: '%v'", whatIGot, whatIWant) + } + + if enabled { + t.Fatal("expected false") + } +} + +func TestIsPairEnabled(t *testing.T) { + t.Parallel() + b := Base{Name: "test"} + whatIWant := currency.NewPair(currency.BTC, currency.USDT) + availButNoEnabled := currency.NewPair(currency.BTC, currency.AUD) + err := b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ + AssetEnabled: convert.BoolPtr(true), + Available: []currency.Pair{whatIWant, availButNoEnabled}, + Enabled: []currency.Pair{whatIWant}, + }) + if err != nil { + t.Fatal(err) + } + + enabled, err := b.IsPairEnabled(currency.NewPair(currency.AAA, currency.CYC), asset.Spot) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if enabled { + t.Fatal("expected false") + } + + enabled, err = b.IsPairEnabled(availButNoEnabled, asset.Spot) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if enabled { + t.Fatal("expected false") + } + + enabled, err = b.IsPairEnabled(whatIWant, asset.Spot) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if !enabled { + t.Fatal("expected true") + } +} diff --git a/exchanges/exmo/exmo_test.go b/exchanges/exmo/exmo_test.go index d373f202bf2..d7e02d2aca9 100644 --- a/exchanges/exmo/exmo_test.go +++ b/exchanges/exmo/exmo_test.go @@ -48,22 +48,6 @@ func TestMain(m *testing.M) { e.API.AuthenticatedSupport = true e.SetCredentials(APIKey, APISecret, "", "", "", "") - - err = e.UpdateTradablePairs(context.Background(), false) - if err != nil { - log.Fatal("Exmo UpdateTradablePairs error", err) - } - - avail, err := e.GetAvailablePairs(asset.Spot) - if err != nil { - log.Fatal("Exmo GetAvailablePairs error", err) - } - - err = e.CurrencyPairs.StorePairs(asset.Spot, avail, true) - if err != nil { - log.Fatal("Exmo StorePairs error", err) - } - os.Exit(m.Run()) } @@ -513,18 +497,19 @@ func TestUpdateTicker(t *testing.T) { func TestUpdateTickers(t *testing.T) { t.Parallel() + err := e.UpdateTickers(context.Background(), asset.Spot) if err != nil { t.Error(err) } - avail, err := e.GetAvailablePairs(asset.Spot) + enabled, err := e.GetEnabledPairs(asset.Spot) if err != nil { t.Fatal(err) } - for x := range avail { - _, err := ticker.GetTicker(e.Name, avail[x], asset.Spot) + for x := range enabled { + _, err := ticker.GetTicker(e.Name, enabled[x], asset.Spot) if err != nil { t.Error(err) } diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index a56cff47081..dbed8c21cd9 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -206,28 +206,21 @@ func (e *EXMO) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error // UpdateTickers updates the ticker for all currency pairs of a given asset type func (e *EXMO) UpdateTickers(ctx context.Context, a asset.Item) error { - avail, err := e.GetAvailablePairs(a) - if err != nil { - return err - } - - enabled, err := e.GetEnabledPairs(a) - if err != nil { - return err - } - result, err := e.GetTicker(ctx) if err != nil { return err } + var enabled bool for symbol, tick := range result { var pair currency.Pair - pair, err = avail.DeriveFrom(strings.Replace(symbol, "_", "", 1)) + pair, enabled, err = e.MatchSymbolCheckEnabled(symbol, asset.Spot, true) if err != nil { - return err + if !errors.Is(err, currency.ErrPairNotFound) { + return err + } } - if !enabled.Contains(pair, true) { + if !enabled { continue } err = ticker.ProcessTicker(&ticker.Price{ diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 1076e20336d..973e5568c3f 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -2112,11 +2112,6 @@ func (g *Gateio) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) e return fmt.Errorf("%s %w", a, asset.ErrNotSupported) } - avail, err := g.GetAvailablePairs(a) - if err != nil { - return err - } - var limits []order.MinMaxLevel switch a { case asset.Spot: @@ -2132,7 +2127,7 @@ func (g *Gateio) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) e continue } var pair currency.Pair - pair, err = avail.DeriveFrom(strings.ReplaceAll(pairsData[x].ID, "_", "")) + pair, err = g.MatchSymbolWithAvailablePairs(pairsData[x].ID, a, true) if err != nil { return err } diff --git a/exchanges/gemini/gemini_types.go b/exchanges/gemini/gemini_types.go index 7736e5dc59a..b12c7c098c7 100644 --- a/exchanges/gemini/gemini_types.go +++ b/exchanges/gemini/gemini_types.go @@ -31,17 +31,17 @@ type Ticker struct { // TickerV2 holds returned ticker data from the exchange type TickerV2 struct { - Ask float64 `json:"ask,string"` - Bid float64 `json:"bid,string"` - Changes []string `json:"changes"` - Close float64 `json:"close,string"` - High float64 `json:"high,string"` - Low float64 `json:"low,string"` - Open float64 `json:"open,string"` - Message string `json:"message,omitempty"` - Reason string `json:"reason,omitempty"` - Result string `json:"result,omitempty"` - Symbol currency.Pair `json:"symbol"` + Ask float64 `json:"ask,string"` + Bid float64 `json:"bid,string"` + Changes []string `json:"changes"` + Close float64 `json:"close,string"` + High float64 `json:"high,string"` + Low float64 `json:"low,string"` + Open float64 `json:"open,string"` + Message string `json:"message,omitempty"` + Reason string `json:"reason,omitempty"` + Result string `json:"result,omitempty"` + Symbol string `json:"symbol"` } // Orderbook contains orderbook information for both bid and ask side diff --git a/exchanges/hitbtc/hitbtc_test.go b/exchanges/hitbtc/hitbtc_test.go index 46f989fa956..2c6b044d2ac 100644 --- a/exchanges/hitbtc/hitbtc_test.go +++ b/exchanges/hitbtc/hitbtc_test.go @@ -186,23 +186,18 @@ func TestUpdateTicker(t *testing.T) { } func TestUpdateTickers(t *testing.T) { - avail, err := h.GetAvailablePairs(asset.Spot) + err := h.UpdateTickers(context.Background(), asset.Spot) if err != nil { - t.Fatal(err) + t.Error(err) } - err = h.CurrencyPairs.StorePairs(asset.Spot, avail, true) + enabled, err := h.GetEnabledPairs(asset.Spot) if err != nil { t.Fatal(err) } - err = h.UpdateTickers(context.Background(), asset.Spot) - if err != nil { - t.Error(err) - } - - for j := range avail { - _, err = h.FetchTicker(context.Background(), avail[j], asset.Spot) + for j := range enabled { + _, err = h.FetchTicker(context.Background(), enabled[j], asset.Spot) if err != nil { t.Error(err) } diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 1e12f44c1c9..300ed1b1608 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -291,9 +291,9 @@ func (h *HitBTC) FetchTradablePairs(ctx context.Context, _ asset.Item) (currency pairs := make([]currency.Pair, len(symbols)) for x := range symbols { - quote := strings.Replace(symbols[x].ID, symbols[x].BaseCurrency, "", 1) + index := strings.Index(symbols[x].ID, symbols[x].QuoteCurrency) var pair currency.Pair - pair, err = currency.NewPairFromStrings(symbols[x].BaseCurrency, quote) + pair, err = currency.NewPairFromStrings(symbols[x].ID[:index], symbols[x].ID[index:]) if err != nil { return nil, err } @@ -322,23 +322,18 @@ func (h *HitBTC) UpdateTickers(ctx context.Context, a asset.Item) error { if err != nil { return err } - avail, err := h.GetAvailablePairs(a) - if err != nil { - return err - } - - enabled, err := h.GetEnabledPairs(a) - if err != nil { - return err - } for x := range tick { - pair, err := avail.DeriveFrom(tick[x].Symbol) + var pair currency.Pair + var enabled bool + pair, enabled, err = h.MatchSymbolCheckEnabled(tick[x].Symbol, a, false) if err != nil { - return err + if !errors.Is(err, currency.ErrPairNotFound) { + return err + } } - if !enabled.Contains(pair, true) { + if !enabled { continue } diff --git a/exchanges/interfaces.go b/exchanges/interfaces.go index 658d096aff8..bbaf824153d 100644 --- a/exchanges/interfaces.go +++ b/exchanges/interfaces.go @@ -97,6 +97,18 @@ type IBotExchange interface { CurrencyStateManagement FuturesManagement MarginManagement + + // MatchSymbolWithAvailablePairs returns a currency pair based on the supplied + // symbol and asset type. If the string is expected to have a delimiter this + // will attempt to screen it out. + MatchSymbolWithAvailablePairs(symbol string, a asset.Item, hasDelimiter bool) (currency.Pair, error) + // MatchSymbolCheckEnabled returns a currency pair based on the supplied symbol + // and asset type against the available pairs list. If the string is expected to + // have a delimiter this will attempt to screen it out. It will also check if + // the pair is enabled. + MatchSymbolCheckEnabled(symbol string, a asset.Item, hasDelimiter bool) (pair currency.Pair, enabled bool, err error) + // IsPairEnabled checks if a pair is enabled for an enabled asset type + IsPairEnabled(pair currency.Pair, a asset.Item) (bool, error) } // OrderManagement defines functionality for order management diff --git a/exchanges/okx/okx.go b/exchanges/okx/okx.go index bb5fc88c30e..7d7fafd7a08 100644 --- a/exchanges/okx/okx.go +++ b/exchanges/okx/okx.go @@ -4328,10 +4328,18 @@ func (ok *Okx) GetAssetsFromInstrumentTypeOrID(instType, instrumentID string) ([ switch { case len(splitSymbol) == 2: resp := make([]asset.Item, 0, 2) - if err := ok.CurrencyPairs.IsAssetPairEnabled(asset.Spot, pair); err == nil { + enabled, err := ok.IsPairEnabled(pair, asset.Spot) + if err != nil { + return nil, err + } + if enabled { resp = append(resp, asset.Spot) } - if err := ok.CurrencyPairs.IsAssetPairEnabled(asset.Margin, pair); err == nil { + enabled, err = ok.IsPairEnabled(pair, asset.Margin) + if err != nil { + return nil, err + } + if enabled { resp = append(resp, asset.Margin) } if len(resp) > 0 { @@ -4340,15 +4348,27 @@ func (ok *Okx) GetAssetsFromInstrumentTypeOrID(instType, instrumentID string) ([ case len(splitSymbol) > 2: switch splitSymbol[len(splitSymbol)-1] { case "SWAP", "swap": - if err := ok.CurrencyPairs.IsAssetPairEnabled(asset.PerpetualSwap, pair); err == nil { + enabled, err := ok.IsPairEnabled(pair, asset.PerpetualSwap) + if err != nil { + return nil, err + } + if enabled { return []asset.Item{asset.PerpetualSwap}, nil } case "C", "P", "c", "p": - if err := ok.CurrencyPairs.IsAssetPairEnabled(asset.Options, pair); err == nil { + enabled, err := ok.IsPairEnabled(pair, asset.Options) + if err != nil { + return nil, err + } + if enabled { return []asset.Item{asset.Options}, nil } default: - if err := ok.CurrencyPairs.IsAssetPairEnabled(asset.Futures, pair); err == nil { + enabled, err := ok.IsPairEnabled(pair, asset.Futures) + if err != nil { + return nil, err + } + if enabled { return []asset.Item{asset.Futures}, nil } } From f8e943ea8e424c30f5aa029c605b41e4fe554446 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 17:29:58 +1100 Subject: [PATCH 14/40] build(deps): bump bufbuild/buf-setup-action from 1.27.0 to 1.27.1 (#1378) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.27.0 to 1.27.1. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.27.0...v1.27.1) --- updated-dependencies: - dependency-name: bufbuild/buf-setup-action 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> --- .github/workflows/proto-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/proto-lint.yml b/.github/workflows/proto-lint.yml index 2671fca6f0f..d0162e0e274 100644 --- a/.github/workflows/proto-lint.yml +++ b/.github/workflows/proto-lint.yml @@ -23,7 +23,7 @@ jobs: go install google.golang.org/protobuf/cmd/protoc-gen-go go install google.golang.org/grpc/cmd/protoc-gen-go-grpc - - uses: bufbuild/buf-setup-action@v1.27.0 + - uses: bufbuild/buf-setup-action@v1.27.1 - name: buf generate working-directory: ./gctrpc From 2ea3083468a8dfa9928012df7e9886bd5891c241 Mon Sep 17 00:00:00 2001 From: Scott Date: Mon, 23 Oct 2023 17:06:25 +1000 Subject: [PATCH 15/40] exchanges/kraken,bittrex,gemini: Resolve Kraken panic, lint corrections, Bittrex batch tickers, set Gemini order limits and update tradable pairs (#1372) * fix kraken, batch bittrex, fix lint * surprise gemini! * thought this happened automatically * fix before shazbert sees * fixes annoying atoi bug * rm futures from gemini * lint * bittrex UpdatedAt, gemini Limits, stats relook * STATS used HARDEN!(improve stats package) * Whoopsies in your Daisies * rm RWMutex, json stringeroo * fixes additional index issues :laughing: :sob: --- exchanges/bittrex/bittrex.go | 9 +- exchanges/bittrex/bittrex_test.go | 20 + exchanges/bittrex/bittrex_types.go | 9 +- exchanges/bittrex/bittrex_wrapper.go | 34 +- exchanges/gemini/gemini.go | 17 +- exchanges/gemini/gemini_test.go | 39 + exchanges/gemini/gemini_types.go | 16 + exchanges/gemini/gemini_wrapper.go | 64 +- exchanges/kraken/kraken_wrapper.go | 7 +- exchanges/order/order_test.go | 11 +- exchanges/stats/stats.go | 135 ++- exchanges/stats/stats_test.go | 109 +- exchanges/stats/stats_types.go | 29 + testdata/http_mock/gemini/gemini.json | 1476 +++++++++++++++++++++++++ 14 files changed, 1855 insertions(+), 120 deletions(-) create mode 100644 exchanges/stats/stats_types.go diff --git a/exchanges/bittrex/bittrex.go b/exchanges/bittrex/bittrex.go index 7394676d090..3cc0f93c6b1 100644 --- a/exchanges/bittrex/bittrex.go +++ b/exchanges/bittrex/bittrex.go @@ -36,6 +36,7 @@ const ( getMarkets = "/markets" getMarketSummaries = "/markets/summaries" getTicker = "/markets/%s/ticker" + getTickers = "/markets/tickers" getMarketSummary = "/markets/%s/summary" getMarketTrades = "/markets/%s/trades" getOrderbook = "/markets/%s/orderbook?depth=%s" @@ -87,8 +88,14 @@ func (b *Bittrex) GetTicker(ctx context.Context, marketName string) (TickerData, return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getTicker, marketName), &resp, nil) } +// GetTickers returns bittrex tickers +func (b *Bittrex) GetTickers(ctx context.Context) ([]TickerData, error) { + var resp []TickerData + return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, getTickers, &resp, nil) +} + // GetMarketSummaries is used to get the last 24 hour summary of all active -// exchanges +// currencies func (b *Bittrex) GetMarketSummaries(ctx context.Context) ([]MarketSummaryData, error) { var resp []MarketSummaryData return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, getMarketSummaries, &resp, nil) diff --git a/exchanges/bittrex/bittrex_test.go b/exchanges/bittrex/bittrex_test.go index 75ff9677072..347f2440f18 100644 --- a/exchanges/bittrex/bittrex_test.go +++ b/exchanges/bittrex/bittrex_test.go @@ -725,3 +725,23 @@ func TestGetHistoricCandlesExtended(t *testing.T) { t.Fatal(err) } } + +func TestGetTickers(t *testing.T) { + t.Parallel() + _, err := b.GetTickers(context.Background()) + if err != nil { + t.Error(err) + } +} + +func TestUpdateTickers(t *testing.T) { + t.Parallel() + err := b.UpdateTickers(context.Background(), asset.Spot) + if err != nil { + t.Error(err) + } + err = b.UpdateTickers(context.Background(), asset.Futures) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatal(err) + } +} diff --git a/exchanges/bittrex/bittrex_types.go b/exchanges/bittrex/bittrex_types.go index a1c18af164b..da217efdcea 100644 --- a/exchanges/bittrex/bittrex_types.go +++ b/exchanges/bittrex/bittrex_types.go @@ -72,10 +72,11 @@ type MarketData struct { // TickerData stores ticker data type TickerData struct { - Symbol string `json:"symbol"` - LastTradeRate float64 `json:"lastTradeRate,string"` - BidRate float64 `json:"bidRate,string"` - AskRate float64 `json:"askRate,string"` + Symbol string `json:"symbol"` + LastTradeRate float64 `json:"lastTradeRate,string"` + BidRate float64 `json:"bidRate,string"` + AskRate float64 `json:"askRate,string"` + UpdatedAt time.Time `json:"updatedAt"` } // TradeData stores trades data diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index b64c712cbdb..3b2e65c77c3 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "sort" + "strings" "sync" "time" @@ -87,6 +88,7 @@ func (b *Bittrex) SetDefaults() { Websocket: true, RESTCapabilities: protocol.Features{ TickerFetching: true, + TickerBatching: true, KlineFetching: true, TradeFetching: true, OrderbookFetching: true, @@ -297,8 +299,36 @@ func (b *Bittrex) UpdateTradablePairs(ctx context.Context, forceUpdate bool) err } // UpdateTickers updates the ticker for all currency pairs of a given asset type -func (b *Bittrex) UpdateTickers(_ context.Context, _ asset.Item) error { - return common.ErrFunctionNotSupported +func (b *Bittrex) UpdateTickers(ctx context.Context, a asset.Item) error { + if a != asset.Spot { + return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + } + tickers, err := b.GetTickers(ctx) + if err != nil { + return err + } + summaries, err := b.GetMarketSummaries(ctx) + if err != nil { + return err + } + for x := range tickers { + for y := range summaries { + if !strings.EqualFold(summaries[y].Symbol, tickers[x].Symbol) { + continue + } + var pair currency.Pair + pair, err = currency.NewPairFromString(tickers[x].Symbol) + if err != nil { + return err + } + tickerPrice := b.constructTicker(tickers[x], &summaries[y], pair, a) + err = ticker.ProcessTicker(tickerPrice) + if err != nil { + return err + } + } + } + return nil } // UpdateTicker updates and returns the ticker for a currency pair diff --git a/exchanges/gemini/gemini.go b/exchanges/gemini/gemini.go index 3dc99059cbf..f650ebf43fd 100644 --- a/exchanges/gemini/gemini.go +++ b/exchanges/gemini/gemini.go @@ -24,7 +24,7 @@ const ( geminiAPIVersion = "1" geminiSymbols = "symbols" - geminiTicker = "pubticker" + geminiSymbolDetails = "symbols/details" geminiAuction = "auction" geminiAuctionHistory = "history" geminiOrderbook = "book" @@ -62,6 +62,21 @@ func (g *Gemini) GetSymbols(ctx context.Context) ([]string, error) { return symbols, g.SendHTTPRequest(ctx, exchange.RestSpot, path, &symbols) } +// GetSymbolDetails returns extra symbol details +// use symbol "all" to get everything +func (g *Gemini) GetSymbolDetails(ctx context.Context, symbol string) ([]SymbolDetails, error) { + if symbol == "all" { + var details []SymbolDetails + return details, g.SendHTTPRequest(ctx, exchange.RestSpot, "/v"+geminiAPIVersion+"/"+geminiSymbolDetails+"/"+symbol, &details) + } + var details SymbolDetails + err := g.SendHTTPRequest(ctx, exchange.RestSpot, "/v"+geminiAPIVersion+"/"+geminiSymbolDetails+"/"+symbol, &details) + if err != nil { + return nil, err + } + return []SymbolDetails{details}, nil +} + // GetTicker returns information about recent trading activity for the symbol func (g *Gemini) GetTicker(ctx context.Context, currencyPair string) (TickerV2, error) { ticker := TickerV2{} diff --git a/exchanges/gemini/gemini_test.go b/exchanges/gemini/gemini_test.go index 954b7f47dc9..c0b79a459aa 100644 --- a/exchanges/gemini/gemini_test.go +++ b/exchanges/gemini/gemini_test.go @@ -1259,3 +1259,42 @@ func TestGetOrderInfo(t *testing.T) { t.Error(err) } } + +func TestGetSymbolDetails(t *testing.T) { + t.Parallel() + _, err := g.GetSymbolDetails(context.Background(), "all") + if err != nil { + t.Error(err) + } + _, err = g.GetSymbolDetails(context.Background(), "btcusd") + if err != nil { + t.Error(err) + } +} + +func TestSetExchangeOrderExecutionLimits(t *testing.T) { + t.Parallel() + err := g.UpdateOrderExecutionLimits(context.Background(), asset.Spot) + if err != nil { + t.Fatal(err) + } + err = g.UpdateOrderExecutionLimits(context.Background(), asset.Futures) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatal(err) + } + + availPairs, err := g.GetAvailablePairs(asset.Spot) + if err != nil { + t.Fatal(err) + } + for x := range availPairs { + var limit order.MinMaxLevel + limit, err = g.GetOrderExecutionLimits(asset.Spot, availPairs[x]) + if err != nil { + t.Fatal(err, availPairs[x]) + } + if limit == (order.MinMaxLevel{}) { + t.Fatal("exchange limit should be loaded") + } + } +} diff --git a/exchanges/gemini/gemini_types.go b/exchanges/gemini/gemini_types.go index b12c7c098c7..da7c76c95ed 100644 --- a/exchanges/gemini/gemini_types.go +++ b/exchanges/gemini/gemini_types.go @@ -1,6 +1,7 @@ package gemini import ( + "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/currency" ) @@ -29,6 +30,21 @@ type Ticker struct { } } +// SymbolDetails contains additional symbol details +type SymbolDetails struct { + Symbol string `json:"symbol"` + BaseCurrency string `json:"base_currency"` + QuoteCurrency string `json:"quote_currency"` + TickSize float64 `json:"tick_size"` + QuoteIncrement float64 `json:"quote_increment"` + MinOrderSize convert.StringToFloat64 `json:"min_order_size"` + Status string `json:"status"` + WrapEnabled bool `json:"wrap_enabled"` + ProductType string `json:"product_type"` + ContractType string `json:"contract_type"` + ContractPriceCurrency string `json:"contract_price_currency"` +} + // TickerV2 holds returned ticker data from the exchange type TickerV2 struct { Ask float64 `json:"ask,string"` diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index 54c41053ea4..897e55cef9e 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -7,6 +7,7 @@ import ( "net/url" "sort" "strconv" + "strings" "sync" "time" @@ -259,7 +260,12 @@ func (g *Gemini) Run(ctx context.Context) { } } } - + if err := g.UpdateOrderExecutionLimits(ctx, asset.Spot); err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to set exchange order execution limits. Err: %v", + g.Name, + err) + } if !g.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { return } @@ -278,26 +284,26 @@ func (g *Gemini) FetchTradablePairs(ctx context.Context, a asset.Item) (currency return nil, asset.ErrNotSupported } - symbols, err := g.GetSymbols(ctx) + details, err := g.GetSymbolDetails(ctx, "all") if err != nil { return nil, err } - - pairs := make([]currency.Pair, len(symbols)) - for x := range symbols { - var pair currency.Pair - switch len(symbols[x]) { - case 8: - pair, err = currency.NewPairFromStrings(symbols[x][0:5], symbols[x][5:]) - case 7: - pair, err = currency.NewPairFromStrings(symbols[x][0:4], symbols[x][4:]) - default: - pair, err = currency.NewPairFromStrings(symbols[x][0:3], symbols[x][3:]) + pairs := make([]currency.Pair, 0, len(details)) + for i := range details { + status := strings.ToLower(details[i].Status) + if status != "open" && status != "limit_only" { + continue + } + if !strings.EqualFold(details[i].ContractType, "vanilla") { + // TODO: add support for futures + continue } + + cp, err := currency.NewPairFromStrings(details[i].BaseCurrency, details[i].Symbol[len(details[i].BaseCurrency):]) if err != nil { return nil, err } - pairs[x] = pair + pairs = append(pairs, cp) } return pairs, nil } @@ -918,3 +924,33 @@ func (g *Gemini) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair, func (g *Gemini) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits sets exchange executions for a required asset type +func (g *Gemini) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { + if a != asset.Spot { + return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + } + details, err := g.GetSymbolDetails(ctx, "all") + if err != nil { + return fmt.Errorf("cannot update exchange execution limits: %w", err) + } + resp := make([]order.MinMaxLevel, 0, len(details)) + for i := range details { + status := strings.ToLower(details[i].Status) + if status != "open" && status != "limit_only" { + continue + } + cp, err := currency.NewPairFromStrings(details[i].BaseCurrency, details[i].QuoteCurrency) + if err != nil { + return err + } + resp = append(resp, order.MinMaxLevel{ + Pair: cp, + Asset: a, + AmountStepIncrementSize: details[i].TickSize, + MinimumBaseAmount: details[i].MinOrderSize.Float64(), + QuoteStepIncrementSize: details[i].QuoteIncrement, + }) + } + return g.LoadLimits(resp) +} diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 93b670a14d4..6e1fc8efa38 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -1727,12 +1727,15 @@ func (k *Kraken) GetFuturesContractDetails(ctx context.Context, item asset.Item) } else { underlyingStr = underlyingBase[1] } - usdIndex := strings.Index(underlyingStr, "usd") + usdIndex := strings.LastIndex(strings.ToLower(underlyingStr), "usd") + if usdIndex <= 0 { + log.Warnf(log.ExchangeSys, "%v unable to find USD index in %v to process contract", k.Name, underlyingStr) + continue + } underlying, err = currency.NewPairFromStrings(underlyingStr[0:usdIndex], underlyingStr[usdIndex:]) if err != nil { return nil, err } - var s, e time.Time if result.Instruments[i].OpeningDate != "" { s, err = time.Parse(time.RFC3339, result.Instruments[i].OpeningDate) diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index c4fb0a14a0e..c533e272330 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -1511,9 +1511,14 @@ func TestMatchFilter(t *testing.T) { } // specific tests for num, tt := range tests { - if tt.o.MatchFilter(&tt.f) != tt.expectedResult { - t.Errorf("tests[%v] failed", num) - } + num := num + tt := tt + t.Run(fmt.Sprintf("%v", num), func(t *testing.T) { + t.Parallel() + if tt.o.MatchFilter(&tt.f) != tt.expectedResult { + t.Errorf("tests[%v] failed", num) + } + }) } } diff --git a/exchanges/stats/stats.go b/exchanges/stats/stats.go index be9bf3e7ab3..417acc7864c 100644 --- a/exchanges/stats/stats.go +++ b/exchanges/stats/stats.go @@ -8,49 +8,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) -// Item holds various fields for storing currency pair stats -type Item struct { - Exchange string - Pair currency.Pair - AssetType asset.Item - Price float64 - Volume float64 -} - -// Items var array -var Items []Item - -// ByPrice allows sorting by price -type ByPrice []Item - -func (b ByPrice) Len() int { - return len(b) -} - -func (b ByPrice) Less(i, j int) bool { - return b[i].Price < b[j].Price -} - -func (b ByPrice) Swap(i, j int) { - b[i], b[j] = b[j], b[i] -} - -// ByVolume allows sorting by volume -type ByVolume []Item - -func (b ByVolume) Len() int { - return len(b) -} - -func (b ByVolume) Less(i, j int) bool { - return b[i].Volume < b[j].Volume -} - -func (b ByVolume) Swap(i, j int) { - b[i], b[j] = b[j], b[i] -} - -// Add adds or updates the item stats +// Add adds or updates the Item stats func Add(exchange string, p currency.Pair, a asset.Item, price, volume float64) error { if exchange == "" || a == asset.Empty || @@ -82,13 +40,14 @@ func Add(exchange string, p currency.Pair, a asset.Item, price, volume float64) return nil } -// Append adds or updates the item stats for a specific -// currency pair and asset type +// Append adds the Item stats for a specific currency pair and asset type +// if it doesn't exist func Append(exchange string, p currency.Pair, a asset.Item, price, volume float64) { - if AlreadyExists(exchange, p, a, price, volume) { + statMutex.Lock() + defer statMutex.Unlock() + if alreadyExistsRequiresLock(exchange, p, a, price, volume) { return } - i := Item{ Exchange: exchange, Pair: p, @@ -97,59 +56,95 @@ func Append(exchange string, p currency.Pair, a asset.Item, price, volume float6 Volume: volume, } - Items = append(Items, i) + items = append(items, i) } -// AlreadyExists checks to see if item info already exists -// for a specific currency pair and asset type -func AlreadyExists(exchange string, p currency.Pair, assetType asset.Item, price, volume float64) bool { - for i := range Items { - if Items[i].Exchange == exchange && - Items[i].Pair.EqualIncludeReciprocal(p) && - Items[i].AssetType == assetType { - Items[i].Price, Items[i].Volume = price, volume +// alreadyExistsRequiresLock checks to see if Item info already exists +// requires a locking beforehand because of globals +func alreadyExistsRequiresLock(exchange string, p currency.Pair, assetType asset.Item, price, volume float64) bool { + for i := range items { + if items[i].Exchange == exchange && + items[i].Pair.EqualIncludeReciprocal(p) && + items[i].AssetType == assetType { + items[i].Price, items[i].Volume = price, volume return true } } return false } -// SortExchangesByVolume sorts item info by volume for a specific +// AlreadyExists checks to see if Item info already exists +// for a specific currency pair and asset type +func AlreadyExists(exchange string, p currency.Pair, assetType asset.Item, price, volume float64) bool { + statMutex.Lock() + defer statMutex.Unlock() + return alreadyExistsRequiresLock(exchange, p, assetType, price, volume) +} + +// SortExchangesByVolume sorts Item info by volume for a specific // currency pair and asset type. Reverse will reverse the order from lowest to // highest func SortExchangesByVolume(p currency.Pair, assetType asset.Item, reverse bool) []Item { var result []Item - for x := range Items { - if Items[x].Pair.EqualIncludeReciprocal(p) && - Items[x].AssetType == assetType { - result = append(result, Items[x]) + statMutex.Lock() + defer statMutex.Unlock() + for x := range items { + if items[x].Pair.EqualIncludeReciprocal(p) && + items[x].AssetType == assetType { + result = append(result, items[x]) } } if reverse { - sort.Sort(sort.Reverse(ByVolume(result))) + sort.Sort(sort.Reverse(byVolume(result))) } else { - sort.Sort(ByVolume(result)) + sort.Sort(byVolume(result)) } return result } -// SortExchangesByPrice sorts item info by volume for a specific +// SortExchangesByPrice sorts Item info by volume for a specific // currency pair and asset type. Reverse will reverse the order from lowest to // highest func SortExchangesByPrice(p currency.Pair, assetType asset.Item, reverse bool) []Item { var result []Item - for x := range Items { - if Items[x].Pair.EqualIncludeReciprocal(p) && - Items[x].AssetType == assetType { - result = append(result, Items[x]) + statMutex.Lock() + defer statMutex.Unlock() + for x := range items { + if items[x].Pair.EqualIncludeReciprocal(p) && + items[x].AssetType == assetType { + result = append(result, items[x]) } } if reverse { - sort.Sort(sort.Reverse(ByPrice(result))) + sort.Sort(sort.Reverse(byPrice(result))) } else { - sort.Sort(ByPrice(result)) + sort.Sort(byPrice(result)) } return result } + +func (b byPrice) Len() int { + return len(b) +} + +func (b byPrice) Less(i, j int) bool { + return b[i].Price < b[j].Price +} + +func (b byPrice) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} + +func (b byVolume) Len() int { + return len(b) +} + +func (b byVolume) Less(i, j int) bool { + return b[i].Volume < b[j].Volume +} + +func (b byVolume) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} diff --git a/exchanges/stats/stats_test.go b/exchanges/stats/stats_test.go index 2b0b81798cd..6630d4fe9ac 100644 --- a/exchanges/stats/stats_test.go +++ b/exchanges/stats/stats_test.go @@ -12,11 +12,12 @@ const ( ) func TestLenByPrice(t *testing.T) { + t.Parallel() p, err := currency.NewPairFromStrings("BTC", "USD") if err != nil { t.Fatal(err) } - Items = []Item{ + localItems := []Item{ { Exchange: testExchange, Pair: p, @@ -26,17 +27,18 @@ func TestLenByPrice(t *testing.T) { }, } - if ByPrice.Len(Items) < 1 { + if byPrice.Len(localItems) < 1 { t.Error("stats LenByPrice() length not correct.") } } func TestLessByPrice(t *testing.T) { + t.Parallel() p, err := currency.NewPairFromStrings("BTC", "USD") if err != nil { t.Fatal(err) } - Items = []Item{ + localItems := []Item{ { Exchange: "alphapoint", Pair: p, @@ -53,20 +55,21 @@ func TestLessByPrice(t *testing.T) { }, } - if !ByPrice.Less(Items, 1, 0) { + if !byPrice.Less(localItems, 1, 0) { t.Error("stats LessByPrice() incorrect return.") } - if ByPrice.Less(Items, 0, 1) { + if byPrice.Less(localItems, 0, 1) { t.Error("stats LessByPrice() incorrect return.") } } func TestSwapByPrice(t *testing.T) { + t.Parallel() p, err := currency.NewPairFromStrings("BTC", "USD") if err != nil { t.Fatal(err) } - Items = []Item{ + localItems := []Item{ { Exchange: "bitstamp", Pair: p, @@ -83,37 +86,97 @@ func TestSwapByPrice(t *testing.T) { }, } - ByPrice.Swap(Items, 0, 1) - if Items[0].Exchange != "bitfinex" || Items[1].Exchange != "bitstamp" { + byPrice.Swap(localItems, 0, 1) + if localItems[0].Exchange != "bitfinex" || localItems[1].Exchange != "bitstamp" { t.Error("stats SwapByPrice did not swap values.") } } func TestLenByVolume(t *testing.T) { - if ByVolume.Len(Items) != 2 { + t.Parallel() + p, err := currency.NewPairFromStrings("BTC", "USD") + if err != nil { + t.Fatal(err) + } + localItems := []Item{ + { + Exchange: "bitstamp", + Pair: p, + AssetType: asset.Spot, + Price: 1324, + Volume: 5, + }, + { + Exchange: "bitfinex", + Pair: p, + AssetType: asset.Spot, + Price: 7863, + Volume: 20, + }, + } + + if byVolume.Len(localItems) != 2 { t.Error("stats lenByVolume did not swap values.") } } func TestLessByVolume(t *testing.T) { - if !ByVolume.Less(Items, 1, 0) { - t.Error("stats LessByVolume() incorrect return.") + t.Parallel() + p, err := currency.NewPairFromStrings("BTC", "USD") + if err != nil { + t.Fatal(err) + } + localItems := []Item{ + { + Exchange: "bitstamp", + Pair: p, + AssetType: asset.Spot, + Price: 1324, + Volume: 5, + }, + { + Exchange: "bitfinex", + Pair: p, + AssetType: asset.Spot, + Price: 7863, + Volume: 20, + }, } - if ByVolume.Less(Items, 0, 1) { - t.Error("stats LessByVolume() incorrect return.") + if !byVolume.Less(localItems, 0, 1) { + t.Error("localItems[0].Volume should be less than localItems[1].Volume") } } func TestSwapByVolume(t *testing.T) { - ByPrice.Swap(Items, 0, 1) - - if Items[1].Exchange != "bitfinex" || Items[0].Exchange != "bitstamp" { + t.Parallel() + p, err := currency.NewPairFromStrings("BTC", "USD") + if err != nil { + t.Fatal(err) + } + localItems := []Item{ + { + Exchange: "bitstamp", + Pair: p, + AssetType: asset.Spot, + Price: 1324, + Volume: 5, + }, + { + Exchange: "bitfinex", + Pair: p, + AssetType: asset.Spot, + Price: 7863, + Volume: 20, + }, + } + byVolume.Swap(localItems, 0, 1) + if localItems[0].Exchange != "bitfinex" || localItems[1].Exchange != "bitstamp" { t.Error("stats SwapByVolume did not swap values.") } } func TestAdd(t *testing.T) { - Items = Items[:0] + items = items[:0] p, err := currency.NewPairFromStrings("BTC", "USD") if err != nil { t.Fatal(err) @@ -123,7 +186,7 @@ func TestAdd(t *testing.T) { t.Fatal(err) } - if len(Items) < 1 { + if len(items) < 1 { t.Error("stats Add did not add exchange info.") } @@ -132,7 +195,7 @@ func TestAdd(t *testing.T) { t.Fatal("error cannot be nil") } - if len(Items) != 1 { + if len(items) != 1 { t.Error("stats Add did not add exchange info.") } @@ -142,7 +205,7 @@ func TestAdd(t *testing.T) { t.Fatal(err) } - if Items[1].Pair.String() != "XBTUSD" { + if items[1].Pair.String() != "XBTUSD" { t.Fatal("stats Add did not add exchange info.") } @@ -156,7 +219,7 @@ func TestAdd(t *testing.T) { t.Fatal(err) } - if Items[2].Pair.String() != "ETHUSD" { + if items[2].Pair.String() != "ETHUSD" { t.Fatal("stats Add did not add exchange info.") } } @@ -167,12 +230,12 @@ func TestAppend(t *testing.T) { t.Fatal(err) } Append("sillyexchange", p, asset.Spot, 1234, 45) - if len(Items) < 2 { + if len(items) < 2 { t.Error("stats AppendResults did not add exchange values.") } Append("sillyexchange", p, asset.Spot, 1234, 45) - if len(Items) == 3 { + if len(items) == 3 { t.Error("stats AppendResults added exchange values") } } diff --git a/exchanges/stats/stats_types.go b/exchanges/stats/stats_types.go new file mode 100644 index 00000000000..623565d5b78 --- /dev/null +++ b/exchanges/stats/stats_types.go @@ -0,0 +1,29 @@ +package stats + +import ( + "sync" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +var ( + // items holds stat items + items []Item + statMutex sync.Mutex +) + +// Item holds various fields for storing currency pair stats +type Item struct { + Exchange string + Pair currency.Pair + AssetType asset.Item + Price float64 + Volume float64 +} + +// byPrice allows sorting by price +type byPrice []Item + +// byVolume allows sorting by volume +type byVolume []Item diff --git a/testdata/http_mock/gemini/gemini.json b/testdata/http_mock/gemini/gemini.json index 97e06e59256..1045764e111 100644 --- a/testdata/http_mock/gemini/gemini.json +++ b/testdata/http_mock/gemini/gemini.json @@ -2191,6 +2191,1482 @@ } ] }, + "/v1/symbols/details/all": { + "GET": [ + { + "data": [ + { + "base_currency": "AAVE", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "AAVEUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ALI", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "2", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.000001, + "status": "open", + "symbol": "ALIUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "AMP", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "10", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "AMPUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ANKR", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "ANKRUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "APE", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.02", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.001, + "status": "open", + "symbol": "APEUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "API3", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.03", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.001, + "status": "open", + "symbol": "API3USD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ATOM", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.001, + "status": "open", + "symbol": "ATOMUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "AVAX", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.00499999", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.001, + "status": "open", + "symbol": "AVAXUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "AXS", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.003", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "AXSUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "BAT", + "contract_price_currency": "BTC", + "contract_type": "vanilla", + "min_order_size": "1", + "product_type": "spot", + "quote_currency": "BTC", + "quote_increment": 1e-8, + "status": "limit_only", + "symbol": "BATBTC", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "BAT", + "contract_price_currency": "ETH", + "contract_type": "vanilla", + "min_order_size": "1", + "product_type": "spot", + "quote_currency": "ETH", + "quote_increment": 1e-7, + "status": "limit_only", + "symbol": "BATETH", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "BAT", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "BATUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "BCH", + "contract_price_currency": "BTC", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "BTC", + "quote_increment": 0.00001, + "status": "open", + "symbol": "BCHBTC", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "BCH", + "contract_price_currency": "ETH", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "ETH", + "quote_increment": 0.0001, + "status": "limit_only", + "symbol": "BCHETH", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "BCH", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "BCHUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "BTC", + "contract_price_currency": "DAI", + "contract_type": "vanilla", + "min_order_size": "0.00001", + "product_type": "spot", + "quote_currency": "DAI", + "quote_increment": 0.01, + "status": "limit_only", + "symbol": "BTCDAI", + "tick_size": 1e-8, + "wrap_enabled": false + }, + { + "base_currency": "BTC", + "contract_price_currency": "EUR", + "contract_type": "vanilla", + "min_order_size": "0.00001", + "product_type": "spot", + "quote_currency": "EUR", + "quote_increment": 0.01, + "status": "open", + "symbol": "BTCEUR", + "tick_size": 1e-8, + "wrap_enabled": false + }, + { + "base_currency": "BTC", + "contract_price_currency": "GBP", + "contract_type": "vanilla", + "min_order_size": "0.00001", + "product_type": "spot", + "quote_currency": "GBP", + "quote_increment": 0.01, + "status": "open", + "symbol": "BTCGBP", + "tick_size": 1e-8, + "wrap_enabled": false + }, + { + "base_currency": "BTC", + "contract_price_currency": "GUSD", + "contract_type": "vanilla", + "min_order_size": "0.00001", + "product_type": "spot", + "quote_currency": "GUSD", + "quote_increment": 0.01, + "status": "open", + "symbol": "BTCGUSD", + "tick_size": 1e-8, + "wrap_enabled": false + }, + { + "base_currency": "BTC", + "contract_price_currency": "GUSD", + "contract_type": "linear", + "min_order_size": "0.0001", + "product_type": "swap", + "quote_currency": "GUSD", + "quote_increment": 0.5, + "status": "open", + "symbol": "BTCGUSDPERP", + "tick_size": 0.0001, + "wrap_enabled": false + }, + { + "base_currency": "BTC", + "contract_price_currency": "SGD", + "contract_type": "vanilla", + "min_order_size": "0.00001", + "product_type": "spot", + "quote_currency": "SGD", + "quote_increment": 0.01, + "status": "open", + "symbol": "BTCSGD", + "tick_size": 1e-8, + "wrap_enabled": false + }, + { + "base_currency": "BTC", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.00001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "BTCUSD", + "tick_size": 1e-8, + "wrap_enabled": false + }, + { + "base_currency": "BTC", + "contract_price_currency": "USDT", + "contract_type": "vanilla", + "min_order_size": "0.00001", + "product_type": "spot", + "quote_currency": "USDT", + "quote_increment": 0.01, + "status": "open", + "symbol": "BTCUSDT", + "tick_size": 1e-8, + "wrap_enabled": false + }, + { + "base_currency": "CHZ", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.5", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "CHZUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "COMP", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "COMPUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "CRV", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "CRVUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "CTX", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.002", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "CTXUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "CUBE", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "CUBEUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "DAI", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "DAIUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "DOGE", + "contract_price_currency": "BTC", + "contract_type": "vanilla", + "min_order_size": "1", + "product_type": "spot", + "quote_currency": "BTC", + "quote_increment": 1e-9, + "status": "open", + "symbol": "DOGEBTC", + "tick_size": 1e-8, + "wrap_enabled": false + }, + { + "base_currency": "DOGE", + "contract_price_currency": "ETH", + "contract_type": "vanilla", + "min_order_size": "1", + "product_type": "spot", + "quote_currency": "ETH", + "quote_increment": 1e-8, + "status": "open", + "symbol": "DOGEETH", + "tick_size": 1e-8, + "wrap_enabled": false + }, + { + "base_currency": "DOGE", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "DOGEUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "DOT", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "DOTUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "EFIL", + "contract_price_currency": "FIL", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "FIL", + "quote_increment": 0.0001, + "status": "closed", + "symbol": "EFILFIL", + "tick_size": 0.000001, + "wrap_enabled": true + }, + { + "base_currency": "ELON", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "60000", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 1e-9, + "status": "open", + "symbol": "ELONUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ENS", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.002", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.001, + "status": "open", + "symbol": "ENSUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ERN", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.05", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "ERNUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ETH", + "contract_price_currency": "BTC", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "BTC", + "quote_increment": 0.00001, + "status": "open", + "symbol": "ETHBTC", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ETH", + "contract_price_currency": "DAI", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "DAI", + "quote_increment": 0.01, + "status": "limit_only", + "symbol": "ETHDAI", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ETH", + "contract_price_currency": "EUR", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "EUR", + "quote_increment": 0.01, + "status": "open", + "symbol": "ETHEUR", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ETH", + "contract_price_currency": "GBP", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "GBP", + "quote_increment": 0.01, + "status": "open", + "symbol": "ETHGBP", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ETH", + "contract_price_currency": "GUSD", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "GUSD", + "quote_increment": 0.01, + "status": "open", + "symbol": "ETHGUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ETH", + "contract_price_currency": "GUSD", + "contract_type": "linear", + "min_order_size": "0.001", + "product_type": "swap", + "quote_currency": "GUSD", + "quote_increment": 0.05, + "status": "open", + "symbol": "ETHGUSDPERP", + "tick_size": 0.001, + "wrap_enabled": false + }, + { + "base_currency": "ETH", + "contract_price_currency": "SGD", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "SGD", + "quote_increment": 0.01, + "status": "open", + "symbol": "ETHSGD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ETH", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "ETHUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ETH", + "contract_price_currency": "USDT", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "USDT", + "quote_increment": 0.01, + "status": "open", + "symbol": "ETHUSDT", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "FET", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "FETUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "FIL", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "FILUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "FTM", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.03", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "FTMUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "GALA", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.4", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "GALAUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "GAL", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.04", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "GALUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "GMT", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "GMTUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "GRT", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "GRTUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "GUSD", + "contract_price_currency": "GBP", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "GBP", + "quote_increment": 0.001, + "status": "open", + "symbol": "GUSDGBP", + "tick_size": 0.0001, + "wrap_enabled": false + }, + { + "base_currency": "GUSD", + "contract_price_currency": "SGD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "SGD", + "quote_increment": 0.001, + "status": "open", + "symbol": "GUSDSGD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "GUSD", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "closed", + "symbol": "GUSDUSD", + "tick_size": 0.01, + "wrap_enabled": true + }, + { + "base_currency": "HNT", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.04", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "HNTUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "IMX", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "IMXUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "INJ", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "INJUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "IOTX", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "3", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.000001, + "status": "open", + "symbol": "IOTXUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "JAM", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "10", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 1e-7, + "status": "open", + "symbol": "JAMUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "LDO", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.02", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.001, + "status": "open", + "symbol": "LDOUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "LINK", + "contract_price_currency": "BTC", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "BTC", + "quote_increment": 1e-8, + "status": "limit_only", + "symbol": "LINKBTC", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "LINK", + "contract_price_currency": "ETH", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "ETH", + "quote_increment": 1e-7, + "status": "limit_only", + "symbol": "LINKETH", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "LINK", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "LINKUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "LPT", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "LPTUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "LRC", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "LRCUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "LTC", + "contract_price_currency": "BCH", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "BCH", + "quote_increment": 0.0001, + "status": "open", + "symbol": "LTCBCH", + "tick_size": 0.00001, + "wrap_enabled": false + }, + { + "base_currency": "LTC", + "contract_price_currency": "BTC", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "BTC", + "quote_increment": 1e-7, + "status": "open", + "symbol": "LTCBTC", + "tick_size": 0.00001, + "wrap_enabled": false + }, + { + "base_currency": "LTC", + "contract_price_currency": "ETH", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "ETH", + "quote_increment": 0.00001, + "status": "open", + "symbol": "LTCETH", + "tick_size": 0.00001, + "wrap_enabled": false + }, + { + "base_currency": "LTC", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "LTCUSD", + "tick_size": 0.00001, + "wrap_enabled": false + }, + { + "base_currency": "LUNA", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 1e-8, + "status": "limit_only", + "symbol": "LUNAUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "MANA", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "MANAUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "MASK", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.001, + "status": "open", + "symbol": "MASKUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "MATIC", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "MATICUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "MKR", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "MKRUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "OXT", + "contract_price_currency": "BTC", + "contract_type": "vanilla", + "min_order_size": "1", + "product_type": "spot", + "quote_currency": "BTC", + "quote_increment": 1e-8, + "status": "limit_only", + "symbol": "OXTBTC", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "OXT", + "contract_price_currency": "ETH", + "contract_type": "vanilla", + "min_order_size": "1", + "product_type": "spot", + "quote_currency": "ETH", + "quote_increment": 1e-7, + "status": "limit_only", + "symbol": "OXTETH", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "OXT", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "OXTUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "PAXG", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.0001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "PAXGUSD", + "tick_size": 1e-8, + "wrap_enabled": false + }, + { + "base_currency": "PEPE", + "contract_price_currency": "GUSD", + "contract_type": "linear", + "min_order_size": "1", + "product_type": "swap", + "quote_currency": "GUSD", + "quote_increment": 1e-9, + "status": "open", + "symbol": "PEPEGUSDPERP", + "tick_size": 1, + "wrap_enabled": false + }, + { + "base_currency": "PEPE", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "1000", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 1e-9, + "status": "open", + "symbol": "PEPEUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "QNT", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.0004", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "QNTUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "QRDO", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.04", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "QRDOUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "RARE", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.001, + "status": "open", + "symbol": "RAREUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "REN", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "RENUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "RLY", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.2", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "RLYUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "RNDR", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.02", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.001, + "status": "open", + "symbol": "RNDRUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "SAMO", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "10", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 1e-7, + "status": "open", + "symbol": "SAMOUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "SAND", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "SANDUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "SHIB", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "1000", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 1e-9, + "status": "open", + "symbol": "SHIBUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "SKL", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "SKLUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "SNX", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "SNXUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "SOL", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.001, + "status": "open", + "symbol": "SOLUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "STORJ", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "STORJUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "SUSHI", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "SUSHIUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "TOKE", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.002", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.001, + "status": "open", + "symbol": "TOKEUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "UMA", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "UMAUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "UNI", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.01", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "UNIUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "USDC", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "USDCUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "USDT", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "USDTUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "XRP", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "XRPUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "XTZ", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.02", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.0001, + "status": "open", + "symbol": "XTZUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "YFI", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.00001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "YFIUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ZBC", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "3", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "ZBCUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ZEC", + "contract_price_currency": "BCH", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "BCH", + "quote_increment": 0.0001, + "status": "limit_only", + "symbol": "ZECBCH", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ZEC", + "contract_price_currency": "BTC", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "BTC", + "quote_increment": 1e-7, + "status": "limit_only", + "symbol": "ZECBTC", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ZEC", + "contract_price_currency": "ETH", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "ETH", + "quote_increment": 0.00001, + "status": "limit_only", + "symbol": "ZECETH", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ZEC", + "contract_price_currency": "LTC", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "LTC", + "quote_increment": 0.001, + "status": "limit_only", + "symbol": "ZECLTC", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ZEC", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "ZECUSD", + "tick_size": 0.000001, + "wrap_enabled": false + }, + { + "base_currency": "ZRX", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.1", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.00001, + "status": "open", + "symbol": "ZRXUSD", + "tick_size": 0.000001, + "wrap_enabled": false + } + ], + "queryString": "", + "bodyParams": "", + "headers": {} + } + ] + }, + "/v1/symbols/details/btcusd": { + "GET": [ + { + "data": { + "base_currency": "BTC", + "contract_price_currency": "USD", + "contract_type": "vanilla", + "min_order_size": "0.00001", + "product_type": "spot", + "quote_currency": "USD", + "quote_increment": 0.01, + "status": "open", + "symbol": "BTCUSD", + "tick_size": 1e-8, + "wrap_enabled": false + }, + "queryString": "", + "bodyParams": "", + "headers": {} + } + ] + }, "/v1/trades/BTCUSD": { "GET": [ { From 75111e08f650b868848fb5380524b2a5baaf1990 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:07:52 +1100 Subject: [PATCH 16/40] build(deps): bump google.golang.org/grpc from 1.58.3 to 1.59.0 (#1377) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.58.3 to 1.59.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.58.3...v1.59.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> --- go.mod | 2 +- go.sum | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index c1f19226b9b..16011cbeac1 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( golang.org/x/text v0.13.0 golang.org/x/time v0.3.0 google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb - google.golang.org/grpc v1.58.3 + google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 ) diff --git a/go.sum b/go.sum index ca6a0985b2b..bd7d17fe0b2 100644 --- a/go.sum +++ b/go.sum @@ -115,7 +115,7 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -658,8 +658,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= -google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From fe779b1aa8abc2c73da15064123b2a9a4df9e162 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Fri, 27 Oct 2023 16:30:01 +1100 Subject: [PATCH 17/40] engine/syncer: Stop orderbook fallover to REST when websocket connection is active (#1361) * sync manager: don't fall over to rest when websocket connection active * glorious: nits and fix bug --------- Co-authored-by: Ryan O'Hara-Reid --- engine/sync_manager.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/engine/sync_manager.go b/engine/sync_manager.go index 5c4d829f000..66dd63e1510 100644 --- a/engine/sync_manager.go +++ b/engine/sync_manager.go @@ -610,6 +610,12 @@ func (m *syncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExc e.SupportsREST() && time.Since(s.LastUpdated) > m.config.TimeoutWebsocket && time.Since(c.Created) > m.config.TimeoutWebsocket { + if w, err := e.GetWebsocket(); err == nil && w.IsConnected() { + // With an active websocket connection, we can assume the orderbook + // is being updated via the websocket connection, It could be very + // illiquid. There is no need to fall over to rest. + return + } // Downgrade to REST s.IsUsingWebsocket = false s.IsUsingREST = true From b8e3d8ef8b3b9b7fbe8a966802bd4d322549b5a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:13:32 +1100 Subject: [PATCH 18/40] build(deps): bump bufbuild/buf-setup-action from 1.27.1 to 1.27.2 (#1384) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.27.1 to 1.27.2. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.27.1...v1.27.2) --- updated-dependencies: - dependency-name: bufbuild/buf-setup-action 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> --- .github/workflows/proto-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/proto-lint.yml b/.github/workflows/proto-lint.yml index d0162e0e274..33bfed5a2c0 100644 --- a/.github/workflows/proto-lint.yml +++ b/.github/workflows/proto-lint.yml @@ -23,7 +23,7 @@ jobs: go install google.golang.org/protobuf/cmd/protoc-gen-go go install google.golang.org/grpc/cmd/protoc-gen-go-grpc - - uses: bufbuild/buf-setup-action@v1.27.1 + - uses: bufbuild/buf-setup-action@v1.27.2 - name: buf generate working-directory: ./gctrpc From dbe41a741220f98ec056e690bf234ba7746e6b71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:14:57 +1100 Subject: [PATCH 19/40] build(deps): bump actions/setup-node from 3 to 4 (#1383) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cb7c4817c23..f4ab5462ab4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -132,7 +132,7 @@ jobs: - uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '10.8.x' From f9437dbd087c92f61d0009d1cecd82c3fbd3dfa5 Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Thu, 2 Nov 2023 02:10:43 +0100 Subject: [PATCH 20/40] Bitfinex: Websocket subscription improvements (#1353) * Websockets: Add keys to websocket subscriptions * This switches all RO uses of the mutex to use a RLock method. * The mutex used for discrete field access has had scope drift from name 'connectionMutex' so rename to more appropriate fieldsMutex * The mutex used for Set/CanUseAuthEndpoints moves from the subscriptions endpoint to the fieldsMutex * Add GetSubscription by key * Expose stream.Matcher type * Bitfinex: Subscribe and Unsubscribe atomicly * Fix Auth failures ignored * This change makes it so that Subscribe and Unsubscribe wait for success ** Tells the DataHandler about errors ** Errors are returned to consumers * Subscribes concurrently to the channels * It also simplifies the chanId to stream mapping * Removes unable to locate chanID: %d errors which are just noise * Paves the way for unified channelSubscription id handling * Adds support for subId for Book subscriptions, which is more robust * Vastly simplifies what we need to test TestWsSubscribedResponse This test was working to ensure that the various fancy key parsing mechanisms all worked. Now that we use subId, we just need a thorough test of that * Expose Match.Set in order to capture websocket incoming data Can't see another way of doing this. Doesn't seem too bad * Allow tests to run with auth or WS These flags made it difficult to run the tests whilst working on websockets * Enable API auth and WS in testconfig This change minimises the changes requires for a full test run against live endpoints, so that new contributors have a clearer testing path. I cannot see any reason to turn WS off and Auth endpoints off when we're not going to run API tests without Creds being set, and we're not going to do live fire tests without canManipulateRealOrders * TestWsSubscribe and various fixes ** Enables the websocket for live non-authed integration tests by default ** Adds an integration test for subscriptions ** Changes the Ws tests to respect canManipulateRealOrders ** Uses WsConnect instead of setupWs; fixes seqNo config not sent for WS tests ** Allows api creds to live in config/testdata.json which might be less likely to accidentally commit, and less obtrusive * Bitfinex: Support period and timeframe for Candles * Fixes manual Subscribe() symbol or key formatting * Unifies handling of params for DefaultSubscriptions and manual subsrciptions * Bitfinex: Handle conf and info WS channel events * Bitfinex: Better tests for subscriptions * fixup! Websockets: Add keys to websocket subscriptions * fixup! Bitfinex: Subscribe and Unsubscribe atomicly * fixup! Websockets: Add keys to websocket subscriptions * Websockets: Add Pending subscription status Add a status tracker so that Sub/Unsub can prevent duplicates, and also fixes when first message comes before we have added the sub to the tracker * Websockets: Add State instead of pending This change allows more clarity about the current state and checks for specifically already Unsubing * Bitfinex: Fix first sub message maybe lost The only link we have between a sub req and the sub resp is the subID. And the only link we have between a sub message and the sub is the chanID. We can't derive a link using Pair or anything else. This meant that by sending the resp and its chanID down the IncomingData channel, we allowed the channel reader to maybe process the next message, the first message on the channel, before the runtime executed the switch back to subscribeToChan waiting on the chan. To fix this, we key initially on subId.(string), and then replace it with chanId.(int64) when we have it *inside* the wsHandleData so we know we've procedurally handled it before the next message. subscribeToChan is then free to remove the subId keyed Sub regardless of error or not If there's an error, we don't need to inline handling because there won't be any second update. Expands test coverage to make sure those subId keyed subscriptions are removed. * Websocket: Validate state in SetChanState * fixup! Bitfinex: Fix first sub message maybe lost * Websockets: Rename RemoveUnsuccessfulSubs Implementation doesn't imply Unsuccessful or need to. This change supports the registering of Pending subs * Bitfinex: Fix race in Tests --- docs/ADD_NEW_EXCHANGE.md | 2 +- exchanges/binance/binance_websocket.go | 2 +- exchanges/binanceus/binanceus_websocket.go | 2 +- exchanges/bitfinex/bitfinex.go | 7 +- exchanges/bitfinex/bitfinex_test.go | 398 +++++++----- exchanges/bitfinex/bitfinex_types.go | 10 +- exchanges/bitfinex/bitfinex_websocket.go | 581 ++++++++++-------- exchanges/bitfinex/bitfinex_wrapper.go | 1 - exchanges/bitfinex/testdata/getErrResp.json | 5 + exchanges/bitmex/bitmex_websocket.go | 2 +- exchanges/bitstamp/bitstamp_websocket.go | 2 +- exchanges/bittrex/bittrex_websocket.go | 2 +- exchanges/btcmarkets/btcmarkets_websocket.go | 2 +- exchanges/btse/btse_websocket.go | 2 +- exchanges/bybit/bybit_websocket.go | 2 +- exchanges/bybit/bybit_ws_cfutures.go | 2 +- exchanges/bybit/bybit_ws_futures.go | 2 +- exchanges/bybit/bybit_ws_ufutures.go | 2 +- .../coinbasepro/coinbasepro_websocket.go | 2 +- exchanges/coinut/coinut_websocket.go | 2 +- exchanges/gateio/gateio_websocket.go | 2 +- exchanges/gateio/gateio_ws_option.go | 2 +- exchanges/gemini/gemini_websocket.go | 2 +- exchanges/hitbtc/hitbtc_websocket.go | 2 +- exchanges/huobi/huobi_websocket.go | 4 +- exchanges/kraken/kraken_websocket.go | 4 +- exchanges/okcoin/okcoin_websocket.go | 6 +- exchanges/okx/okx_websocket.go | 6 +- exchanges/poloniex/poloniex_websocket.go | 4 +- exchanges/stream/stream_match.go | 28 +- exchanges/stream/stream_match_test.go | 4 +- exchanges/stream/stream_types.go | 21 +- exchanges/stream/websocket.go | 279 +++++---- exchanges/stream/websocket_connection.go | 2 +- exchanges/stream/websocket_test.go | 251 +++++--- exchanges/stream/websocket_types.go | 8 +- go.mod | 1 + go.sum | 2 + testdata/configtest.json | 6 +- 39 files changed, 1000 insertions(+), 664 deletions(-) create mode 100644 exchanges/bitfinex/testdata/getErrResp.json diff --git a/docs/ADD_NEW_EXCHANGE.md b/docs/ADD_NEW_EXCHANGE.md index 3abf11a28cb..f40c3f0844a 100644 --- a/docs/ADD_NEW_EXCHANGE.md +++ b/docs/ADD_NEW_EXCHANGE.md @@ -1095,7 +1095,7 @@ channels: continue } // When we have a successful unsubscription, we can alert our internal management system of the success. - f.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i]) + f.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i]) } if errs != nil { return errs diff --git a/exchanges/binance/binance_websocket.go b/exchanges/binance/binance_websocket.go index c544341ffe8..83084fabf00 100644 --- a/exchanges/binance/binance_websocket.go +++ b/exchanges/binance/binance_websocket.go @@ -621,7 +621,7 @@ func (b *Binance) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription return err } } - b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...) + b.Websocket.RemoveSubscriptions(channelsToUnsubscribe...) return nil } diff --git a/exchanges/binanceus/binanceus_websocket.go b/exchanges/binanceus/binanceus_websocket.go index b7c9557174a..b54a0ddf42f 100644 --- a/exchanges/binanceus/binanceus_websocket.go +++ b/exchanges/binanceus/binanceus_websocket.go @@ -614,7 +614,7 @@ func (bi *Binanceus) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscript return err } } - bi.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...) + bi.Websocket.RemoveSubscriptions(channelsToUnsubscribe...) return nil } diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index 617837c70d9..da1658b8c5e 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -21,7 +21,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) @@ -101,12 +100,16 @@ const ( bitfinexChecksumFlag = 131072 bitfinexWsSequenceFlag = 65536 + + // CandlesTimeframeKey configures the timeframe in stream.ChannelSubscription.Params + CandlesTimeframeKey = "_timeframe" + // CandlesPeriodKey configures the aggregated period in stream.ChannelSubscription.Params + CandlesPeriodKey = "_period" ) // Bitfinex is the overarching type across the bitfinex package type Bitfinex struct { exchange.Base - WebsocketSubdChannels map[int]*stream.ChannelSubscription } // GetPlatformStatus returns the Bifinex platform status diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index 42e4367d219..8e1ac1b3f27 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -1,16 +1,17 @@ package bitfinex import ( + "bufio" "context" "errors" "log" - "net/http" "os" + "strconv" "sync" "testing" "time" - "github.com/gorilla/websocket" + "github.com/buger/jsonparser" "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" @@ -27,7 +28,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) -// Please supply your own keys here to do better tests +// Please supply API keys here or in config/testdata.json to test authenticated endpoints const ( apiKey = "" apiSecret = "" @@ -35,7 +36,7 @@ const ( ) var b = &Bitfinex{} -var wsAuthExecuted bool +var wsConnected bool var btcusdPair = currency.NewPair(currency.BTC, currency.USD) func TestMain(m *testing.M) { @@ -54,9 +55,10 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("Bitfinex setup error", err) } - b.SetCredentials(apiKey, apiSecret, "", "", "", "") - if !b.Enabled || b.API.AuthenticatedSupport || - b.Verbose || b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 { + if apiKey != "" { + b.SetCredentials(apiKey, apiSecret, "", "", "", "") + } + if !b.Enabled || len(b.BaseCurrencies) < 1 { log.Fatal("Bitfinex Setup values not set correctly") } @@ -64,7 +66,6 @@ func TestMain(m *testing.M) { b.API.AuthenticatedSupport = true b.API.AuthenticatedWebsocketSupport = true } - b.WebsocketSubdChannels = make(map[int]*stream.ChannelSubscription) os.Exit(m.Run()) } @@ -858,7 +859,6 @@ func TestGetOrderHistory(t *testing.T) { // Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them // ---------------------------------------------------------------------------------------------------------------------------- - func TestSubmitOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders) @@ -911,7 +911,7 @@ func TestCancelExchangeOrder(t *testing.T) { } } -func TestCancelAllExchangeOrdera(t *testing.T) { +func TestCancelAllExchangeOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders) @@ -1046,56 +1046,131 @@ func TestGetDepositAddress(t *testing.T) { } } -func setupWs() { - var dialer websocket.Dialer - err := b.Websocket.AuthConn.Dial(&dialer, http.Header{}) - if err != nil { - log.Fatal(err) - } - go b.wsReadData(b.Websocket.AuthConn) - go b.WsDataHandler() -} - // TestWsAuth dials websocket, sends login request. func TestWsAuth(t *testing.T) { - if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport { - sharedtestvalues.SkipTestIfCredentialsUnset(t, b) + if !b.Websocket.IsEnabled() { + t.Skip(stream.WebsocketNotEnabled) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, b) + if !b.API.AuthenticatedWebsocketSupport { + t.Skip("Authentecated API support not enabled") + } + setupWs(t) + assert.True(t, b.Websocket.CanUseAuthenticatedEndpoints(), "CanUseAuthenticatedEndpoints should be turned on") + + var resp map[string]interface{} + catcher := func() (ok bool) { + select { + case v := <-b.Websocket.DataHandler: + resp, ok = v.(map[string]interface{}) + default: + } + return + } + + if assert.Eventually(t, catcher, sharedtestvalues.WebsocketResponseDefaultTimeout, time.Millisecond*10, "Auth response should arrive") { + assert.Equal(t, "auth", resp["event"], "event should be correct") + assert.Equal(t, "OK", resp["status"], "status should be correct") + assert.NotEmpty(t, resp["auth_id"], "status should be correct") } - runAuth(t) } -//nolint:gocritic // Only used as a testing helper function in this package -func runAuth(t *testing.T) { - t.Helper() - setupWs() - if err := b.WsSendAuth(context.Background()); err != nil { - t.Error(err) +// TestWsSubscribe tests Subscribe and Unsubscribe functionality +// See also TestSubscribeReq which covers key and symbol conversion +func TestWsSubscribe(t *testing.T) { + setupWs(t) + err := b.Subscribe([]stream.ChannelSubscription{{Channel: wsTicker, Currency: currency.NewPair(currency.BTC, currency.USD), Asset: asset.Spot}}) + assert.NoError(t, err, "Subrcribe should not error") + catcher := func() (ok bool) { + i := <-b.Websocket.DataHandler + _, ok = i.(*ticker.Price) + return } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resp := <-b.Websocket.DataHandler: - if logResponse, ok := resp.(map[string]interface{}); ok { - if logResponse["event"] != "auth" && logResponse["status"] != "OK" { - t.Error("expected successful login") + assert.Eventually(t, catcher, sharedtestvalues.WebsocketResponseDefaultTimeout, time.Millisecond*10, "Ticker response should arrive") + + subs, err := b.GetSubscriptions() + assert.NoError(t, err, "GetSubscriptions should not error") + assert.Len(t, subs, 1, "We should only have 1 subscription; subID subscription should have been Removed by subscribeToChan") + + err = b.Subscribe([]stream.ChannelSubscription{{Channel: wsTicker, Currency: currency.NewPair(currency.BTC, currency.USD), Asset: asset.Spot}}) + assert.ErrorIs(t, err, stream.ErrSubscriptionFailure, "Duplicate subscription should error correctly") + catcher = func() bool { + i := <-b.Websocket.DataHandler + if e, ok := i.(error); ok { + if assert.ErrorIs(t, e, stream.ErrSubscriptionFailure, "Error should go to DataHandler") { + assert.ErrorContains(t, e, "subscribe: dup (code: 10301)", "Error should contain message and code") + return true } - } else { - t.Error("Unexpected response") } - case <-timer.C: - t.Error("Have not received a response") + return false + } + assert.Eventually(t, catcher, sharedtestvalues.WebsocketResponseDefaultTimeout, time.Millisecond*10, "error response should arrive") + + subs, err = b.GetSubscriptions() + assert.NoError(t, err, "GetSubscriptions should not error") + assert.Len(t, subs, 1, "We should only have one subscription after an error attempt") + + err = b.Unsubscribe(subs) + assert.NoError(t, err, "Unsubscribing should not error") + + chanID, ok := subs[0].Key.(int) + assert.True(t, ok, "sub.Key should be an int") + + err = b.Unsubscribe(subs) + assert.ErrorIs(t, err, stream.ErrUnsubscribeFailure, "Unsubscribe should error") + assert.ErrorContains(t, err, strconv.Itoa(chanID), "Unsubscribe should contain correct chanId") + assert.ErrorContains(t, err, "unsubscribe: invalid (code: 10400)", "Unsubscribe should contain correct upstream error") + + err = b.Subscribe([]stream.ChannelSubscription{{ + Channel: wsTicker, + Currency: currency.NewPair(currency.BTC, currency.USD), + Asset: asset.Spot, + Params: map[string]interface{}{"key": "tBTCUSD"}, + }}) + assert.ErrorIs(t, err, stream.ErrSubscriptionFailure, "Trying to use a 'key' param should error ErrSubscriptionFailure") + assert.ErrorIs(t, err, errParamNotAllowed, "Trying to use a 'key' param should error errParamNotAllowed") +} + +// TestSubscribeReq tests the channel to request map marshalling +func TestSubscribeReq(t *testing.T) { + c := &stream.ChannelSubscription{ + Channel: wsCandles, + Currency: currency.NewPair(currency.BTC, currency.USD), + Asset: asset.MarginFunding, + Params: map[string]interface{}{ + CandlesPeriodKey: "30", + }, + } + r, err := subscribeReq(c) + assert.NoError(t, err, "subscribeReq should not error") + assert.Equal(t, "trade:1m:fBTCUSD:p30", r["key"], "key contain period and default timeframe") + + c.Params = map[string]interface{}{ + CandlesTimeframeKey: "15m", + } + r, err = subscribeReq(c) + assert.NoError(t, err, "subscribeReq should not error") + assert.Equal(t, "trade:15m:fBTCUSD", r["key"], "key should be contain specific timeframe and no period") + + c = &stream.ChannelSubscription{ + Channel: wsBook, + Currency: currency.NewPair(currency.BTC, currency.DOGE), + Asset: asset.Spot, } - timer.Stop() - wsAuthExecuted = true + r, err = subscribeReq(c) + assert.NoError(t, err, "subscribeReq should not error") + assert.Equal(t, "tBTC:DOGE", r["symbol"], "symbol should use colon delimiter if a currency is > 3 chars") + + c.Currency = currency.NewPair(currency.BTC, currency.LTC) + r, err = subscribeReq(c) + assert.NoError(t, err, "subscribeReq should not error") + assert.Equal(t, "tBTCLTC", r["symbol"], "symbol should not use colon delimiter if both currencies < 3 chars") } // TestWsPlaceOrder dials websocket, sends order request. func TestWsPlaceOrder(t *testing.T) { - if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport { - sharedtestvalues.SkipTestIfCredentialsUnset(t, b) - } - if !wsAuthExecuted { - runAuth(t) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders) + setupWs(t) _, err := b.WsNewOrder(&WsNewOrderRequest{ GroupID: 1, @@ -1111,12 +1186,8 @@ func TestWsPlaceOrder(t *testing.T) { // TestWsCancelOrder dials websocket, sends cancel request. func TestWsCancelOrder(t *testing.T) { - if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport { - sharedtestvalues.SkipTestIfCredentialsUnset(t, b) - } - if !wsAuthExecuted { - runAuth(t) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders) + setupWs(t) if err := b.WsCancelOrder(1234); err != nil { t.Error(err) } @@ -1124,12 +1195,8 @@ func TestWsCancelOrder(t *testing.T) { // TestWsCancelOrder dials websocket, sends modify request. func TestWsUpdateOrder(t *testing.T) { - if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport { - sharedtestvalues.SkipTestIfCredentialsUnset(t, b) - } - if !wsAuthExecuted { - runAuth(t) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders) + setupWs(t) err := b.WsModifyOrder(&WsUpdateOrderRequest{ OrderID: 1234, Price: -111, @@ -1142,12 +1209,8 @@ func TestWsUpdateOrder(t *testing.T) { // TestWsCancelAllOrders dials websocket, sends cancel all request. func TestWsCancelAllOrders(t *testing.T) { - if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport { - sharedtestvalues.SkipTestIfCredentialsUnset(t, b) - } - if !wsAuthExecuted { - runAuth(t) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders) + setupWs(t) if err := b.WsCancelAllOrders(); err != nil { t.Error(err) } @@ -1155,12 +1218,8 @@ func TestWsCancelAllOrders(t *testing.T) { // TestWsCancelAllOrders dials websocket, sends cancel all request. func TestWsCancelMultiOrders(t *testing.T) { - if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport { - sharedtestvalues.SkipTestIfCredentialsUnset(t, b) - } - if !wsAuthExecuted { - runAuth(t) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders) + setupWs(t) err := b.WsCancelMultiOrders([]int64{1, 2, 3, 4}) if err != nil { t.Error(err) @@ -1169,12 +1228,8 @@ func TestWsCancelMultiOrders(t *testing.T) { // TestWsNewOffer dials websocket, sends new offer request. func TestWsNewOffer(t *testing.T) { - if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport { - sharedtestvalues.SkipTestIfCredentialsUnset(t, b) - } - if !wsAuthExecuted { - runAuth(t) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders) + setupWs(t) err := b.WsNewOffer(&WsNewOfferRequest{ Type: order.Limit.String(), Symbol: "fBTC", @@ -1189,61 +1244,37 @@ func TestWsNewOffer(t *testing.T) { // TestWsCancelOffer dials websocket, sends cancel offer request. func TestWsCancelOffer(t *testing.T) { - if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport { - sharedtestvalues.SkipTestIfCredentialsUnset(t, b) - } - if !wsAuthExecuted { - runAuth(t) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders) + setupWs(t) if err := b.WsCancelOffer(1234); err != nil { t.Error(err) } } func TestWsSubscribedResponse(t *testing.T) { - b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.Spot, Currency: btcusdPair, Channel: wsTicker, Params: map[string]interface{}{"chanId": 224555}}) - if err := b.wsHandleData([]byte(`{"event":"subscribed","channel":"ticker","chanId":224555,"symbol":"tBTCUSD","pair":"BTCUSD"}`)); err != nil { - t.Error(err) - } - - // Spot Candles - b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.Spot, Currency: btcusdPair, Channel: wsCandles, Params: map[string]interface{}{"chanId": 224556}}) - if err := b.wsHandleData([]byte(`{"event":"subscribed","channel":"candles","chanId":224556,"key":"trade:1m:tBTCUSD"}`)); err != nil { - t.Error(err) + m, err := b.Websocket.Match.Set("subscribe:waiter1") + assert.NoError(t, err, "Setting a matcher should not error") + err = b.wsHandleData([]byte(`{"event":"subscribed","channel":"ticker","chanId":224555,"subId":"waiter1","symbol":"tBTCUSD","pair":"BTCUSD"}`)) + if assert.Error(t, err, "Should error if sub is not registered yet") { + assert.ErrorIs(t, err, stream.ErrSubscriptionFailure, "Should error SubFailure if sub isn't registered yet") + assert.ErrorIs(t, err, stream.ErrSubscriptionFailure, "Should error SubNotFound if sub isn't registered yet") + assert.ErrorContains(t, err, "waiter1", "Should error containing subID if") } - pair, err := currency.NewPairFromString("BTC:CNHT") - if err != nil { - t.Error(err) - } - b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.Spot, Currency: pair, Channel: wsCandles, Params: map[string]interface{}{"chanId": 224557}}) - pressXToJSON := `{"event":"subscribed","channel":"candles","chanId":224557,"key":"trade:1m:tBTC:CNHT"}` - if err = b.wsHandleData([]byte(pressXToJSON)); err != nil { - t.Error(err) - } - - // Margin Candles - pair, err = currency.NewPairFromString("BTC") - if err != nil { - t.Error(err) - } - b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.MarginFunding, Currency: pair, Channel: wsCandles, Params: map[string]interface{}{"chanId": 224558}}) - if e2 := b.wsHandleData([]byte(`{"event":"subscribed","channel":"candles","chanId":224558,"key":"trade:1m:fBTC:a30:p2:p30"}`)); e2 != nil { - t.Error(e2) - } - - pair, err = currency.NewPairFromString("USD") - if err != nil { - t.Error(err) - } - b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.MarginFunding, Currency: pair, Channel: wsCandles, Params: map[string]interface{}{"chanId": 224559}}) - if e2 := b.wsHandleData([]byte(`{"event":"subscribed","channel":"candles","chanId":224559,"key":"trade:1m:fUSD:p30"}`)); e2 != nil { - t.Error(e2) + b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Key: "waiter1"}) + err = b.wsHandleData([]byte(`{"event":"subscribed","channel":"ticker","chanId":224555,"subId":"waiter1","symbol":"tBTCUSD","pair":"BTCUSD"}`)) + assert.NoError(t, err, "wsHandleData should not error") + if assert.NotEmpty(t, m.C, "Matcher should have received a sub notification") { + msg := <-m.C + cID, err := jsonparser.GetInt(msg, "chanId") + assert.NoError(t, err, "Should get chanId from sub notification without error") + assert.EqualValues(t, 224555, cID, "Should get the correct chanId through the matcher notification") } + m.Cleanup() } func TestWsOrderBook(t *testing.T) { - b.WebsocketSubdChannels[23405] = &stream.ChannelSubscription{Asset: asset.Spot, Currency: btcusdPair, Channel: wsBook, Params: map[string]interface{}{"chanId": 23405}} + b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Key: 23405, Asset: asset.Spot, Currency: btcusdPair, Channel: wsBook}) pressXToJSON := `[23405,[[38334303613,9348.8,0.53],[38334308111,9348.8,5.98979404],[38331335157,9344.1,1.28965787],[38334302803,9343.8,0.08230094],[38334279092,9343,0.8],[38334307036,9342.938663676,0.8],[38332749107,9342.9,0.2],[38332277330,9342.8,0.85],[38329406786,9342,0.1432012],[38332841570,9341.947288638,0.3],[38332163238,9341.7,0.3],[38334303384,9341.6,0.324],[38332464840,9341.4,0.5],[38331935870,9341.2,0.5],[38334312082,9340.9,0.02126899],[38334261292,9340.8,0.26763],[38334138680,9340.625455254,0.12],[38333896802,9339.8,0.85],[38331627527,9338.9,1.57863959],[38334186713,9338.9,0.26769],[38334305819,9338.8,2.999],[38334211180,9338.75285796,3.999],[38334310699,9337.8,0.10679883],[38334307414,9337.5,1],[38334179822,9337.1,0.26773],[38334306600,9336.659955102,1.79],[38334299667,9336.6,1.1],[38334306452,9336.6,0.13979771],[38325672859,9336.3,1.25],[38334311646,9336.2,1],[38334258509,9336.1,0.37],[38334310592,9336,1.79],[38334310378,9335.6,1.43],[38334132444,9335.2,0.26777],[38331367325,9335,0.07],[38334310703,9335,0.10680562],[38334298209,9334.7,0.08757301],[38334304857,9334.456899462,0.291],[38334309940,9334.088390727,0.0725],[38334310377,9333.7,1.2868],[38334297615,9333.607784,0.1108],[38334095188,9333.3,0.26785],[38334228913,9332.7,0.40861186],[38334300526,9332.363996604,0.3884],[38334310701,9332.2,0.10680562],[38334303548,9332.005382871,0.07],[38334311798,9331.8,0.41285228],[38334301012,9331.7,1.7952],[38334089877,9331.4,0.2679],[38321942150,9331.2,0.2],[38334310670,9330,1.069],[38334063096,9329.6,0.26796],[38334310700,9329.4,0.10680562],[38334310404,9329.3,1],[38334281630,9329.1,6.57150597],[38334036864,9327.7,0.26801],[38334310702,9326.6,0.10680562],[38334311799,9326.1,0.50220625],[38334164163,9326,0.219638],[38334309722,9326,1.5],[38333051682,9325.8,0.26807],[38334302027,9325.7,0.75],[38334203435,9325.366592,0.32397696],[38321967613,9325,0.05],[38334298787,9324.9,0.3],[38334301719,9324.8,3.6227592],[38331316716,9324.763454646,0.71442],[38334310698,9323.8,0.10680562],[38334035499,9323.7,0.23431017],[38334223472,9322.670551788,0.42150603],[38334163459,9322.560399006,0.143967],[38321825171,9320.8,2],[38334075805,9320.467496148,0.30772633],[38334075800,9319.916732238,0.61457592],[38333682302,9319.7,0.0011],[38331323088,9319.116771762,0.12913],[38333677480,9319,0.0199],[38334277797,9318.6,0.89],[38325235155,9318.041088,1.20249],[38334310910,9317.82382938,1.79],[38334311811,9317.2,0.61079138],[38334311812,9317.2,0.71937652],[38333298214,9317.1,50],[38334306359,9317,1.79],[38325531545,9316.382823951,0.21263],[38333727253,9316.3,0.02316372],[38333298213,9316.1,45],[38333836479,9316,2.135],[38324520465,9315.9,2.7681],[38334307411,9315.5,1],[38330313617,9315.3,0.84455],[38334077770,9315.294024,0.01248397],[38334286663,9315.294024,1],[38325533762,9315.290315394,2.40498],[38334310018,9315.2,3],[38333682617,9314.6,0.0011],[38334304794,9314.6,0.76364676],[38334304798,9314.3,0.69242113],[38332915733,9313.8,0.0199],[38334084411,9312.8,1],[38334311893,9350.1,-1.015],[38334302734,9350.3,-0.26737],[38334300732,9350.8,-5.2],[38333957619,9351,-0.90677089],[38334300521,9351,-1.6457],[38334301600,9351.012829557,-0.0523],[38334308878,9351.7,-2.5],[38334299570,9351.921544,-0.1015],[38334279367,9352.1,-0.26732],[38334299569,9352.411802928,-0.4036],[38334202773,9353.4,-0.02139404],[38333918472,9353.7,-1.96412776],[38334278782,9354,-0.26731],[38334278606,9355,-1.2785],[38334302105,9355.439221251,-0.79191542],[38313897370,9355.569409242,-0.43363],[38334292995,9355.584296,-0.0979],[38334216989,9355.8,-0.03686414],[38333894025,9355.9,-0.26721],[38334293798,9355.936691952,-0.4311],[38331159479,9356,-0.4204022],[38333918888,9356.1,-1.10885563],[38334298205,9356.4,-0.20124428],[38328427481,9356.5,-0.1],[38333343289,9356.6,-0.41034213],[38334297205,9356.6,-0.08835018],[38334277927,9356.741101161,-0.0737],[38334311645,9356.8,-0.5],[38334309002,9356.9,-5],[38334309736,9357,-0.10680107],[38334306448,9357.4,-0.18645275],[38333693302,9357.7,-0.2672],[38332815159,9357.8,-0.0011],[38331239824,9358.2,-0.02],[38334271608,9358.3,-2.999],[38334311971,9358.4,-0.55],[38333919260,9358.5,-1.9972841],[38334265365,9358.5,-1.7841],[38334277960,9359,-3],[38334274601,9359.020969848,-3],[38326848839,9359.1,-0.84],[38334291080,9359.247048,-0.16199869],[38326848844,9359.4,-1.84],[38333680200,9359.6,-0.26713],[38331326606,9359.8,-0.84454],[38334309738,9359.8,-0.10680107],[38331314707,9359.9,-0.2],[38333919803,9360.9,-1.41177599],[38323651149,9361.33417827,-0.71442],[38333656906,9361.5,-0.26705],[38334035500,9361.5,-0.40861586],[38334091886,9362.4,-6.85940815],[38334269617,9362.5,-4],[38323629409,9362.545858872,-2.40497],[38334309737,9362.7,-0.10680107],[38334312380,9362.7,-3],[38325280830,9362.8,-1.75123],[38326622800,9362.8,-1.05145],[38333175230,9363,-0.0011],[38326848745,9363.2,-0.79],[38334308960,9363.206775564,-0.12],[38333920234,9363.3,-1.25318113],[38326848843,9363.4,-1.29],[38331239823,9363.4,-0.02],[38333209613,9363.4,-0.26719],[38334299964,9364,-0.05583123],[38323470224,9364.161816648,-0.12912],[38334284711,9365,-0.21346019],[38334299594,9365,-2.6757062],[38323211816,9365.073132585,-0.21262],[38334312456,9365.1,-0.11167861],[38333209612,9365.2,-0.26719],[38327770474,9365.3,-0.0073],[38334298788,9365.3,-0.3],[38334075803,9365.409831204,-0.30772637],[38334309740,9365.5,-0.10680107],[38326608767,9365.7,-2.76809],[38333920657,9365.7,-1.25848083],[38329594226,9366.6,-0.02587],[38334311813,9366.7,-4.72290945],[38316386301,9367.39258128,-2.37581],[38334302026,9367.4,-4.5],[38334228915,9367.9,-0.81725458],[38333921381,9368.1,-1.72213641],[38333175678,9368.2,-0.0011],[38334301150,9368.2,-2.654604],[38334297208,9368.3,-0.78036466],[38334309739,9368.3,-0.10680107],[38331227515,9368.7,-0.02],[38331184470,9369,-0.003975],[38334203436,9369.319616,-0.32397695],[38334269964,9369.7,-0.5],[38328386732,9370,-4.11759935],[38332719555,9370,-0.025],[38333921935,9370.5,-1.2224398],[38334258511,9370.5,-0.35],[38326848842,9370.8,-0.34],[38333985038,9370.9,-0.8551502],[38334283018,9370.9,-1],[38326848744,9371,-1.34]],5]` err := b.wsHandleData([]byte(pressXToJSON)) if err != nil { @@ -1260,7 +1291,7 @@ func TestWsOrderBook(t *testing.T) { } func TestWsTradeResponse(t *testing.T) { - b.WebsocketSubdChannels[18788] = &stream.ChannelSubscription{Asset: asset.Spot, Currency: btcusdPair, Channel: wsTrades} + b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.Spot, Currency: btcusdPair, Channel: wsTrades, Key: 18788}) pressXToJSON := `[18788,[[412685577,1580268444802,11.1998,176.3],[412685575,1580268444802,5,176.29952759],[412685574,1580268374717,1.99069999,176.41],[412685573,1580268374717,1.00930001,176.41],[412685572,1580268358760,0.9907,176.47],[412685571,1580268324362,0.5505,176.44],[412685570,1580268297270,-0.39040819,176.39],[412685568,1580268297270,-0.39780162,176.46475676],[412685567,1580268283470,-0.09,176.41],[412685566,1580268256536,-2.31310783,176.48],[412685565,1580268256536,-0.59669217,176.49],[412685564,1580268256536,-0.9902,176.49],[412685562,1580268194474,0.9902,176.55],[412685561,1580268186215,0.1,176.6],[412685560,1580268185964,-2.17096773,176.5],[412685559,1580268185964,-1.82903227,176.51],[412685558,1580268181215,2.098914,176.53],[412685557,1580268169844,16.7302,176.55],[412685556,1580268169844,3.25,176.54],[412685555,1580268155725,0.23576115,176.45],[412685553,1580268155725,3,176.44596249],[412685552,1580268155725,3.25,176.44],[412685551,1580268155725,5,176.44],[412685550,1580268155725,0.65830078,176.41],[412685549,1580268155725,0.45063807,176.41],[412685548,1580268153825,-0.67604704,176.39],[412685547,1580268145713,2.5883,176.41],[412685543,1580268087513,12.92927,176.33],[412685542,1580268087513,0.40083,176.33],[412685533,1580268005756,-0.17096773,176.32]]]` err := b.wsHandleData([]byte(pressXToJSON)) if err != nil { @@ -1269,7 +1300,7 @@ func TestWsTradeResponse(t *testing.T) { } func TestWsTickerResponse(t *testing.T) { - b.WebsocketSubdChannels[11534] = &stream.ChannelSubscription{Asset: asset.Spot, Currency: btcusdPair, Channel: wsTicker} + b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.Spot, Currency: btcusdPair, Channel: wsTicker, Key: 11534}) pressXToJSON := `[11534,[61.304,2228.36155358,61.305,1323.2442970500003,0.395,0.0065,61.371,50973.3020771,62.5,57.421]]` err := b.wsHandleData([]byte(pressXToJSON)) if err != nil { @@ -1279,7 +1310,7 @@ func TestWsTickerResponse(t *testing.T) { if err != nil { t.Error(err) } - b.WebsocketSubdChannels[123412] = &stream.ChannelSubscription{Asset: asset.Spot, Currency: pair, Channel: wsTicker} + b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.Spot, Currency: pair, Channel: wsTicker, Key: 123412}) pressXToJSON = `[123412,[61.304,2228.36155358,61.305,1323.2442970500003,0.395,0.0065,61.371,50973.3020771,62.5,57.421]]` err = b.wsHandleData([]byte(pressXToJSON)) if err != nil { @@ -1289,7 +1320,7 @@ func TestWsTickerResponse(t *testing.T) { if err != nil { t.Error(err) } - b.WebsocketSubdChannels[123413] = &stream.ChannelSubscription{Asset: asset.Spot, Currency: pair, Channel: wsTicker} + b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.Spot, Currency: pair, Channel: wsTicker, Key: 123413}) pressXToJSON = `[123413,[61.304,2228.36155358,61.305,1323.2442970500003,0.395,0.0065,61.371,50973.3020771,62.5,57.421]]` err = b.wsHandleData([]byte(pressXToJSON)) if err != nil { @@ -1299,7 +1330,7 @@ func TestWsTickerResponse(t *testing.T) { if err != nil { t.Error(err) } - b.WebsocketSubdChannels[123414] = &stream.ChannelSubscription{Asset: asset.Spot, Currency: pair, Channel: wsTicker} + b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.Spot, Currency: pair, Channel: wsTicker, Key: 123414}) pressXToJSON = `[123414,[61.304,2228.36155358,61.305,1323.2442970500003,0.395,0.0065,61.371,50973.3020771,62.5,57.421]]` err = b.wsHandleData([]byte(pressXToJSON)) if err != nil { @@ -1308,7 +1339,7 @@ func TestWsTickerResponse(t *testing.T) { } func TestWsCandleResponse(t *testing.T) { - b.WebsocketSubdChannels[343351] = &stream.ChannelSubscription{Asset: asset.Spot, Currency: btcusdPair, Channel: wsCandles} + b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.Spot, Currency: btcusdPair, Channel: wsCandles, Key: 343351}) pressXToJSON := `[343351,[[1574698260000,7379.785503,7383.8,7388.3,7379.785503,1.68829482]]]` err := b.wsHandleData([]byte(pressXToJSON)) if err != nil { @@ -1348,7 +1379,7 @@ func TestWsNotifications(t *testing.T) { } } -func TestWSFundingOfferSnapshotAndUpdate(t *testing.T) { +func TestWsFundingOfferSnapshotAndUpdate(t *testing.T) { pressXToJSON := `[0,"fos",[[41237920,"fETH",1573912039000,1573912039000,0.5,0.5,"LIMIT",null,null,0,"ACTIVE",null,null,null,0.0024,2,0,0,null,0,null]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) @@ -1360,7 +1391,7 @@ func TestWSFundingOfferSnapshotAndUpdate(t *testing.T) { } } -func TestWSFundingCreditSnapshotAndUpdate(t *testing.T) { +func TestWsFundingCreditSnapshotAndUpdate(t *testing.T) { pressXToJSON := `[0,"fcs",[[26223578,"fUST",1,1575052261000,1575296187000,350,0,"ACTIVE",null,null,null,0,30,1575052261000,1575293487000,0,0,null,0,null,0,"tBTCUST"],[26223711,"fUSD",-1,1575291961000,1575296187000,180,0,"ACTIVE",null,null,null,0.002,7,1575282446000,1575295587000,0,0,null,0,null,0,"tETHUSD"]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) @@ -1372,7 +1403,7 @@ func TestWSFundingCreditSnapshotAndUpdate(t *testing.T) { } } -func TestWSFundingLoanSnapshotAndUpdate(t *testing.T) { +func TestWsFundingLoanSnapshotAndUpdate(t *testing.T) { pressXToJSON := `[0,"fls",[[2995442,"fUSD",-1,1575291961000,1575295850000,820,0,"ACTIVE",null,null,null,0.002,7,1575282446000,1575295850000,0,0,null,0,null,0]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) @@ -1384,35 +1415,35 @@ func TestWSFundingLoanSnapshotAndUpdate(t *testing.T) { } } -func TestWSWalletSnapshot(t *testing.T) { +func TestWsWalletSnapshot(t *testing.T) { pressXToJSON := `[0,"ws",[["exchange","SAN",19.76,0,null,null,null]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) } } -func TestWSBalanceUpdate(t *testing.T) { +func TestWsBalanceUpdate(t *testing.T) { const pressXToJSON = `[0,"bu",[4131.85,4131.85]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) } } -func TestWSMarginInfoUpdate(t *testing.T) { +func TestWsMarginInfoUpdate(t *testing.T) { const pressXToJSON = `[0,"miu",["base",[-13.014640000000007,0,49331.70267297,49318.68803297,27]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) } } -func TestWSFundingInfoUpdate(t *testing.T) { +func TestWsFundingInfoUpdate(t *testing.T) { const pressXToJSON = `[0,"fiu",["sym","tETHUSD",[149361.09689202666,149639.26293509,830.0182168075556,895.0658432466332]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) } } -func TestWSFundingTrade(t *testing.T) { +func TestWsFundingTrade(t *testing.T) { pressXToJSON := `[0,"fte",[636854,"fUSD",1575282446000,41238905,-1000,0.002,7,null]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) @@ -1854,19 +1885,92 @@ func TestCancelMultipleOrdersV2(t *testing.T) { } } -func TestChanForSub(t *testing.T) { - t.Parallel() - p := currency.NewPairWithDelimiter("DOGE", "XLM", "-") - s, err := b.chanForSub(wsBook, asset.Spot, p) - assert.ErrorIs(t, err, errSubNotFound, "Correct error returned when stream when sub not found") - assert.Nil(t, s, "No stream returned when sub not found") +// TestGetErrResp unit tests the helper func getErrResp +func TestGetErrResp(t *testing.T) { + t.Parallel() + fixture, err := os.Open("testdata/getErrResp.json") + if !assert.NoError(t, err, "Opening fixture should not error") { + t.FailNow() + } + s := bufio.NewScanner(fixture) + seen := 0 + for s.Scan() { + testErr := b.getErrResp(s.Bytes()) + seen++ + switch seen { + case 1: // no event + assert.ErrorIs(t, testErr, errParsingWSField, "Message with no event Should get correct error type") + assert.ErrorContains(t, testErr, "'event'", "Message with no event error should contain missing field name") + assert.ErrorContains(t, testErr, "nightjar", "Message with no event error should contain the message") + case 2: // with {} for event + assert.NoError(t, testErr, "Message with '{}' for event field should not error") + case 3: // event != 'error' + assert.NoError(t, testErr, "Message with non-'error' event field should not error") + case 4: // event="error" + assert.ErrorIs(t, testErr, errUnknownError, "error without a message should throw unknown error") + assert.ErrorContains(t, testErr, "code: 0", "error without a code should throw code 0") + case 5: // Fully formatted + assert.ErrorContains(t, testErr, "redcoats", "message field should be in the error") + assert.ErrorContains(t, testErr, "code: 42", "code field should be in the error") + } + } + assert.NoError(t, s.Err(), "Fixture Scanner should not error") + assert.NoError(t, fixture.Close(), "Closing the fixture file should not error") +} + +// TestParallelChanOp unit tests the helper func parallelChanOp +func TestParallelChanOp(t *testing.T) { + t.Parallel() + c := []stream.ChannelSubscription{ + {Channel: "red"}, + {Channel: "blue"}, + {Channel: "violent"}, + {Channel: "spin"}, + {Channel: "charm"}, + } + run := make(chan struct{}, len(c)*2) + errC := make(chan error, 1) + go func() { + errC <- b.parallelChanOp(c, func(c *stream.ChannelSubscription) error { + time.Sleep(300 * time.Millisecond) + run <- struct{}{} + switch c.Channel { + case "spin", "violent": + return errors.New(c.Channel) + } + return nil + }) + }() + f := func(ct *assert.CollectT) { + if assert.Len(ct, errC, 1, "Should eventually have an error") { + err := <-errC + assert.ErrorContains(ct, err, "violent", "Should get a violent error") + assert.ErrorContains(ct, err, "spin", "Should get a spin error") + } + } + assert.EventuallyWithT(t, f, 500*time.Millisecond, 50*time.Millisecond, "ParallelChanOp should complete within 500ms not 5*300ms") + assert.Len(t, run, len(c), "Every channel was run to completion") +} - // Add a spare sub to ensure we don't get only-answer-is-right syndrome - b.Websocket.AddSuccessfulSubscriptions(stream.ChannelSubscription{Asset: asset.Spot, Currency: btcusdPair, Channel: wsTicker}) +// setupWs is a helper function to connect both auth and normal websockets +// It will skip the test if websockets are not enabled +// It's up to the test to skip if it requires creds, though +func setupWs(tb testing.TB) { + tb.Helper() + if !b.Websocket.IsEnabled() { + tb.Skip("Websocket not enabled") + } + if b.Websocket.IsConnected() { + return + } + if wsConnected { + return + } + // We don't use b.websocket.Connect() because it'd subscribe to channels + err := b.WsConnect() + if !assert.NoError(tb, err, "WsConnect should not error") { + tb.FailNow() + } - want := stream.ChannelSubscription{Asset: asset.Spot, Currency: p, Channel: wsBook} - b.Websocket.AddSuccessfulSubscriptions(want) - s, err = b.chanForSub(wsBook, asset.Spot, p) - assert.Nil(t, err, "No error returned when sub found") - assert.EqualValues(t, want, *s, "Correct Sub found") + wsConnected = true } diff --git a/exchanges/bitfinex/bitfinex_types.go b/exchanges/bitfinex/bitfinex_types.go index 665247038c9..e99a1171042 100644 --- a/exchanges/bitfinex/bitfinex_types.go +++ b/exchanges/bitfinex/bitfinex_types.go @@ -12,9 +12,11 @@ import ( var ( errSetCannotBeEmpty = errors.New("set cannot be empty") - errSubNotFound = errors.New("could not find matching subscription") errTypeAssert = errors.New("type assertion failed") errNoSeqNo = errors.New("no sequence number") + errUnknownError = errors.New("unknown error") + errParamNotAllowed = errors.New("param not allowed") + errParsingWSField = errors.New("error parsing WS field") ) // AccountV2Data stores account v2 data @@ -659,6 +661,12 @@ const ( wsTicker = "ticker" wsTrades = "trades" wsError = "error" + wsEventSubscribed = "subscribed" + wsEventUnsubscribed = "unsubscribed" + wsEventAuth = "auth" + wsEventError = "error" + wsEventConf = "conf" + wsEventInfo = "info" ) // WsAuthRequest container for WS auth request diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index 29a35727e5d..ac3667e0c2b 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -13,6 +13,7 @@ import ( "sync" "time" + "github.com/buger/jsonparser" "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" @@ -132,76 +133,31 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error { } switch d := result.(type) { case map[string]interface{}: - event := d["event"] - switch event { - case "subscribed": - chanID, ok := d["chanId"].(float64) - if !ok { - return errors.New("unable to type assert chanId") - } - channel, ok := d["channel"].(string) - if !ok { - return errors.New("unable to type assert channel") - } - symbol, ok := d["symbol"].(string) - if !ok { - key, ok := d["key"].(string) - if !ok { - return fmt.Errorf("subscribed to channel but no symbol or key: %v", channel) - } - if channel != wsCandles { - // status channel not implemented at all yet. - return fmt.Errorf("%v channel subscription keys: %w", channel, common.ErrNotYetImplemented) - } - var err error - symbol, err = symbolFromCandleKey(key) - if err != nil { - return err - } - } - if err := b.WsAddSubscriptionChannel(int(chanID), channel, symbol); err != nil { - return err - } - case "unsubscribed": - chanID, ok := d["chanId"].(float64) - if !ok { - return errors.New("unable to type assert chanId") - } - delete(b.WebsocketSubdChannels, int(chanID)) - case "auth": - status, ok := d["status"].(string) - if !ok { - return errors.New("unable to type assert status") - } - if status == "OK" { - b.Websocket.DataHandler <- d - } else if status == "fail" { - if code, ok := d["code"].(string); ok { - return fmt.Errorf("websocket unable to AUTH. Error code: %s", - code) - } - return errors.New("websocket unable to auth") - } - } + return b.handleWSEvent(respRaw) case []interface{}: - var chanID int - if f, ok := d[0].(float64); !ok { + chanIDFloat, ok := d[0].(float64) + if !ok { return common.GetTypeAssertError("float64", d[0], "chanID") - } else { //nolint:revive // using lexical variable requires else statement - chanID = int(f) } + chanID := int(chanIDFloat) eventType, hasEventType := d[1].(string) if chanID != 0 { - if c, ok := b.WebsocketSubdChannels[chanID]; ok { - return b.handleWSChannelUpdate(c, chanID, eventType, d) + if c := b.Websocket.GetSubscription(chanID); c != nil { + return b.handleWSChannelUpdate(c, eventType, d) + } + if b.Verbose { + log.Warnf(log.ExchangeSys, "%s %s; dropped WS message: %s", b.Name, stream.ErrSubscriptionNotFound, respRaw) } - return fmt.Errorf("unable to locate chanID: %d", chanID) + // We didn't have a mapping for this chanID; This probably means we have unsubscribed OR + // received our first message before processing the sub chanID + // In either case it's okay. No point in erroring because there's nothing we can do about it, and it happens often + return nil } if !hasEventType { - return errors.New("WS message without eventType or chanID") + return errors.New("WS message without eventType") } switch eventType { @@ -469,9 +425,107 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error { return nil } -func (b *Bitfinex) handleWSChannelUpdate(c *stream.ChannelSubscription, chanID int, eventType string, d []interface{}) error { +func (b *Bitfinex) handleWSEvent(respRaw []byte) error { + event, err := jsonparser.GetUnsafeString(respRaw, "event") + if err != nil { + return fmt.Errorf("%w 'event': %w from message: %s", errParsingWSField, err, respRaw) + } + switch event { + case wsEventSubscribed: + return b.handleWSSubscribed(respRaw) + case wsEventUnsubscribed: + chanID, err := jsonparser.GetUnsafeString(respRaw, "chanId") + if err != nil { + return fmt.Errorf("%w 'chanId': %w from message: %s", errParsingWSField, err, respRaw) + } + if !b.Websocket.Match.IncomingWithData("unsubscribe:"+chanID, respRaw) { + return fmt.Errorf("%v channel unsubscribe listener not found", chanID) + } + case wsEventError: + if subID, err := jsonparser.GetUnsafeString(respRaw, "subId"); err == nil { + if !b.Websocket.Match.IncomingWithData("subscribe:"+subID, respRaw) { + return fmt.Errorf("%v channel subscribe listener not found", subID) + } + } else if chanID, err := jsonparser.GetUnsafeString(respRaw, "chanId"); err == nil { + if !b.Websocket.Match.IncomingWithData("unsubscribe:"+chanID, respRaw) { + return fmt.Errorf("%v channel unsubscribe listener not found", chanID) + } + } else { + return fmt.Errorf("unknown channel error; Message: %s", respRaw) + } + case wsEventAuth: + status, err := jsonparser.GetUnsafeString(respRaw, "status") + if err != nil { + return fmt.Errorf("%w 'status': %w from message: %s", errParsingWSField, err, respRaw) + } + if status == "OK" { + var glob map[string]interface{} + if err := json.Unmarshal(respRaw, &glob); err != nil { + return fmt.Errorf("unable to Unmarshal auth resp; Error: %w Msg: %v", err, respRaw) + } + // TODO - Send a better value down the channel + b.Websocket.DataHandler <- glob + } else { + errCode, err := jsonparser.GetInt(respRaw, "code") + if err != nil { + log.Errorf(log.ExchangeSys, "%s %s 'code': %s from message: %s", b.Name, errParsingWSField, err, respRaw) + } + return fmt.Errorf("WS auth subscription error; Status: %s Error Code: %d", status, errCode) + } + case wsEventInfo: + // Nothing to do with info for now. + // version or platform.status might be useful in the future. + case wsEventConf: + status, err := jsonparser.GetUnsafeString(respRaw, "status") + if err != nil { + return fmt.Errorf("%w 'status': %w from message: %s", errParsingWSField, err, respRaw) + } + if status != "OK" { + return fmt.Errorf("WS configure channel error; Status: %s", status) + } + default: + return fmt.Errorf("unknown WS event msg: %s", respRaw) + } + + return nil +} + +// handleWSSubscribed parses a subscription response and registers the chanID key immediately, before updating subscribeToChan via IncomingWithData chan +// wsHandleData happens sequentially, so by rekeying on chanID immediately we ensure the first message is not dropped +func (b *Bitfinex) handleWSSubscribed(respRaw []byte) error { + subID, err := jsonparser.GetUnsafeString(respRaw, "subId") + if err != nil { + return fmt.Errorf("%w 'subId': %w from message: %s", errParsingWSField, err, respRaw) + } + + c := b.Websocket.GetSubscription(subID) + if c == nil { + return fmt.Errorf("%w: %w subID: %s", stream.ErrSubscriptionFailure, stream.ErrSubscriptionNotFound, subID) + } + + chanID, err := jsonparser.GetInt(respRaw, "chanId") + if err != nil { + return fmt.Errorf("%w: %w 'chanId': %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, errParsingWSField, err, c.Channel, c.Currency) + } + + // Note: chanID's int type avoids conflicts with the string type subID key because of the type difference + c.Key = int(chanID) + + // subscribeToChan removes the old subID keyed Subscription + b.Websocket.AddSuccessfulSubscriptions(*c) + + if b.Verbose { + log.Debugf(log.ExchangeSys, "%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n", b.Name, c.Channel, c.Currency, chanID) + } + if !b.Websocket.Match.IncomingWithData("subscribe:"+subID, respRaw) { + return fmt.Errorf("%v channel subscribe listener not found", subID) + } + return nil +} + +func (b *Bitfinex) handleWSChannelUpdate(c *stream.ChannelSubscription, eventType string, d []interface{}) error { if eventType == wsChecksum { - return b.handleWSChecksum(chanID, d) + return b.handleWSChecksum(c, d) } if eventType == wsHeartbeat { @@ -480,7 +534,7 @@ func (b *Bitfinex) handleWSChannelUpdate(c *stream.ChannelSubscription, chanID i switch c.Channel { case wsBook: - return b.handleWSBookUpdate(c, chanID, d) + return b.handleWSBookUpdate(c, d) case wsCandles: return b.handleWSCandleUpdate(c, d) case wsTicker: @@ -492,7 +546,7 @@ func (b *Bitfinex) handleWSChannelUpdate(c *stream.ChannelSubscription, chanID i return fmt.Errorf("%s unhandled channel update: %s", b.Name, c.Channel) } -func (b *Bitfinex) handleWSChecksum(chanID int, d []interface{}) error { +func (b *Bitfinex) handleWSChecksum(c *stream.ChannelSubscription, d []interface{}) error { var token int if f, ok := d[2].(float64); !ok { return common.GetTypeAssertError("float64", d[2], "checksum") @@ -509,6 +563,11 @@ func (b *Bitfinex) handleWSChecksum(chanID int, d []interface{}) error { seqNo = int64(f) } + chanID, ok := c.Key.(int) + if !ok { + return common.GetTypeAssertError("int", c.Key, "ChanID") // Should be impossible + } + cMtx.Lock() checksumStore[chanID] = &checksum{ Token: token, @@ -518,7 +577,7 @@ func (b *Bitfinex) handleWSChecksum(chanID int, d []interface{}) error { return nil } -func (b *Bitfinex) handleWSBookUpdate(c *stream.ChannelSubscription, chanID int, d []interface{}) error { +func (b *Bitfinex) handleWSBookUpdate(c *stream.ChannelSubscription, d []interface{}) error { var newOrderbook []WebsocketBook obSnapBundle, ok := d[1].([]interface{}) if !ok { @@ -604,7 +663,7 @@ func (b *Bitfinex) handleWSBookUpdate(c *stream.ChannelSubscription, chanID int, Amount: amountRate}) } - if err := b.WsUpdateOrderbook(c.Currency, c.Asset, newOrderbook, chanID, int64(sequenceNo), fundingRate); err != nil { + if err := b.WsUpdateOrderbook(c, c.Currency, c.Asset, newOrderbook, int64(sequenceNo), fundingRate); err != nil { return fmt.Errorf("updating orderbook error: %s", err) } @@ -1405,8 +1464,7 @@ func (b *Bitfinex) wsHandleOrder(data []interface{}) { b.Websocket.DataHandler <- &od } -// WsInsertSnapshot add the initial orderbook snapshot when subscribed to a -// channel +// WsInsertSnapshot add the initial orderbook snapshot when subscribed to a channel func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books []WebsocketBook, fundingRate bool) error { if len(books) == 0 { return errors.New("no orderbooks submitted") @@ -1450,7 +1508,7 @@ func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books // WsUpdateOrderbook updates the orderbook list, removing and adding to the // orderbook sides -func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book []WebsocketBook, channelID int, sequenceNo int64, fundingRate bool) error { +func (b *Bitfinex) WsUpdateOrderbook(c *stream.ChannelSubscription, p currency.Pair, assetType asset.Item, book []WebsocketBook, sequenceNo int64, fundingRate bool) error { orderbookUpdate := orderbook.Update{ Asset: assetType, Pair: p, @@ -1506,13 +1564,18 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book } } + chanID, ok := c.Key.(int) + if !ok { + return common.GetTypeAssertError("int", c.Key, "ChanID") // Should be impossible + } + cMtx.Lock() - checkme := checksumStore[channelID] + checkme := checksumStore[chanID] if checkme == nil { cMtx.Unlock() return b.Websocket.Orderbook.Update(&orderbookUpdate) } - checksumStore[channelID] = nil + checksumStore[chanID] = nil cMtx.Unlock() if checkme.Sequence+1 == sequenceNo { @@ -1528,9 +1591,7 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book if err = validateCRC32(ob, checkme.Token); err != nil { log.Errorf(log.WebsocketMgr, "%s websocket orderbook update error, will resubscribe orderbook: %v", b.Name, err) - if suberr := b.resubOrderbook(p, assetType); suberr != nil { - log.Errorf(log.ExchangeSys, "%s error resubscribing orderbook: %v", b.Name, suberr) - } + b.resubOrderbook(c) return err } } @@ -1540,37 +1601,22 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book // resubOrderbook resubscribes the orderbook after a consistency error, probably a failed checksum, // which forces a fresh snapshot. If we don't do this the orderbook will keep erroring and drifting. -func (b *Bitfinex) resubOrderbook(p currency.Pair, assetType asset.Item) error { - if err := b.Websocket.Orderbook.FlushOrderbook(p, assetType); err != nil { - return err +// Flushing the orderbook happens immediately, but the ReSub itself is a go routine to avoid blocking the WS data channel +func (b *Bitfinex) resubOrderbook(c *stream.ChannelSubscription) { + if err := b.Websocket.Orderbook.FlushOrderbook(c.Currency, c.Asset); err != nil { + log.Errorf(log.ExchangeSys, "%s error flushing orderbook: %v", b.Name, err) } - c, err := b.chanForSub(wsBook, assetType, p) - if err != nil { - return err - } - return b.Websocket.ResubscribeToChannel(c) -} - -// chanForSub returns an existing channel subscription for a given channel/asset/pair -func (b *Bitfinex) chanForSub(cName string, assetType asset.Item, pair currency.Pair) (*stream.ChannelSubscription, error) { - want := &stream.ChannelSubscription{ - Channel: cName, - Currency: pair, - Asset: assetType, - } - subs := b.Websocket.GetSubscriptions() - for i := range subs { - if subs[i].Equal(want) { - return &subs[i], nil + // Resub will block so we have to do this in a goro + go func() { + if err := b.Websocket.ResubscribeToChannel(c); err != nil { + log.Errorf(log.ExchangeSys, "%s error resubscribing orderbook: %v", b.Name, err) } - } - return nil, errSubNotFound + }() } // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (b *Bitfinex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) { - var wsPairFormat = currency.PairFormat{Uppercase: true} var channels = []string{wsBook, wsTrades, wsTicker, wsCandles} var subscriptions []stream.ChannelSubscription @@ -1592,29 +1638,8 @@ func (b *Bitfinex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, params["len"] = "100" } - prefix := "t" - if assets[i] == asset.MarginFunding { - prefix = "f" - } - - needsDelimiter := enabledPairs[k].Len() > 6 - - var formattedPair string - if needsDelimiter { - formattedPair = enabledPairs[k].Format(currency.PairFormat{Uppercase: true, Delimiter: ":"}).String() - } else { - formattedPair = wsPairFormat.Format(enabledPairs[k]) - } - - if channels[j] == wsCandles { - // TODO: Add ability to select timescale && funding period - fundingPeriod := "" - if assets[i] == asset.MarginFunding { - fundingPeriod = ":p30" - } - params["key"] = "trade:1m:" + prefix + formattedPair + fundingPeriod - } else { - params["symbol"] = prefix + formattedPair + if channels[j] == wsCandles && assets[i] == asset.MarginFunding { + params[CandlesPeriodKey] = "30" } subscriptions = append(subscriptions, stream.ChannelSubscription{ @@ -1630,31 +1655,6 @@ func (b *Bitfinex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, return subscriptions, nil } -// Subscribe sends a websocket message to receive data from the channel -func (b *Bitfinex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error { - var errs error - for i := range channelsToSubscribe { - req := make(map[string]interface{}) - req["event"] = "subscribe" - req["channel"] = channelsToSubscribe[i].Channel - - for k, v := range channelsToSubscribe[i].Params { - // Resubscribing channels might already have this set - if k != "chanId" { - req[k] = v - } - } - - err := b.Websocket.Conn.SendJSONMessage(req) - if err != nil { - errs = common.AppendError(errs, err) - continue - } - b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i]) - } - return errs -} - // ConfigureWS to send checksums and sequence numbers func (b *Bitfinex) ConfigureWS() error { return b.Websocket.Conn.SendJSONMessage(map[string]interface{}{ @@ -1663,35 +1663,191 @@ func (b *Bitfinex) ConfigureWS() error { }) } -// Unsubscribe sends a websocket message to stop receiving data from the channel -func (b *Bitfinex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { +// Subscribe sends a websocket message to receive data from channels +func (b *Bitfinex) Subscribe(channels []stream.ChannelSubscription) error { + return b.parallelChanOp(channels, b.subscribeToChan) +} + +// Unsubscribe sends a websocket message to stop receiving data from channels +func (b *Bitfinex) Unsubscribe(channels []stream.ChannelSubscription) error { + return b.parallelChanOp(channels, b.unsubscribeFromChan) +} + +// parallelChanOp performs a single method call in parallel across streams and waits to return any errors +func (b *Bitfinex) parallelChanOp(channels []stream.ChannelSubscription, m func(*stream.ChannelSubscription) error) error { + wg := sync.WaitGroup{} + wg.Add(len(channels)) + errC := make(chan error, len(channels)) + + for i := range channels { + go func(c *stream.ChannelSubscription) { + defer wg.Done() + if err := m(c); err != nil { + errC <- err + } + }(&channels[i]) + } + + wg.Wait() + close(errC) + var errs error - for i := range channelsToUnsubscribe { - idAny, ok := channelsToUnsubscribe[i].Params["chanId"] - if !ok { - errs = common.AppendError(errs, fmt.Errorf("cannot unsubscribe from a channel without an id")) - continue - } - chanID, ok := idAny.(int) - if !ok { - errs = common.AppendError(errs, fmt.Errorf("chanId is not an int")) - continue - } + for err := range errC { + errs = common.AppendError(errs, err) + } + + return errs +} + +// subscribeToChan handles a single subscription and parses the result +// on success it adds the subscription to the websocket +func (b *Bitfinex) subscribeToChan(c *stream.ChannelSubscription) error { + req, err := subscribeReq(c) + if err != nil { + return fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Currency) + } + + // subId is a single round-trip identifier that provides linking sub requests to chanIDs + // Although docs only mention subId for wsBook, it works for all chans + subID := strconv.FormatInt(b.Websocket.Conn.GenerateMessageID(false), 10) + req["subId"] = subID + + // Add a temporary Key so we can find this Sub when we get the resp without delay or context switch + // Otherwise we might drop the first messages after the subscribed resp + c.Key = subID // Note subID string type avoids conflicts with later chanID key + + c.State = stream.ChannelSubscribing + err = b.Websocket.AddSubscription(c) + if err != nil { + return fmt.Errorf("%w Channel: %s Pair: %s Error: %w", stream.ErrSubscriptionFailure, c.Channel, c.Currency, err) + } + + // Always remove the temporary subscription keyed by subID + defer b.Websocket.RemoveSubscriptions(*c) + + respRaw, err := b.Websocket.Conn.SendMessageReturnResponse("subscribe:"+subID, req) + if err != nil { + return fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Currency) + } + + if err = b.getErrResp(respRaw); err != nil { + wErr := fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Currency) + b.Websocket.DataHandler <- wErr + return wErr + } + + return nil +} - req := map[string]interface{}{ - "event": "unsubscribe", - "chanId": chanID, +// subscribeReq returns a map of request params for subscriptions +func subscribeReq(c *stream.ChannelSubscription) (map[string]interface{}, error) { + req := map[string]interface{}{ + "event": "subscribe", + "channel": c.Channel, + } + + for k, v := range c.Params { + switch k { + case CandlesPeriodKey, CandlesTimeframeKey: + // Skip these internal Params + case "key", "symbol": + // Ensure user's Params aren't silently overwritten + return nil, fmt.Errorf("%s %w", k, errParamNotAllowed) + default: + req[k] = v } + } - err := b.Websocket.Conn.SendJSONMessage(req) - if err != nil { - errs = common.AppendError(errs, err) - continue + prefix := "t" + if c.Asset == asset.MarginFunding { + prefix = "f" + } + + needsDelimiter := c.Currency.Len() > 6 + + var formattedPair string + if needsDelimiter { + formattedPair = c.Currency.Format(currency.PairFormat{Uppercase: true, Delimiter: ":"}).String() + } else { + formattedPair = currency.PairFormat{Uppercase: true}.Format(c.Currency) + } + + if c.Channel == wsCandles { + timeframe := "1m" + if t, ok := c.Params[CandlesTimeframeKey]; ok { + if timeframe, ok = t.(string); !ok { + return nil, common.GetTypeAssertError("string", t, "Subscription.CandlesTimeframeKey") + } } - // We do this before the unsubscribed event comes back so we can subscribe again when called from ResubcribeToChannel - b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i]) + fundingPeriod := "" + if p, ok := c.Params[CandlesPeriodKey]; ok { + s, cOk := p.(string) + if !cOk { + return nil, common.GetTypeAssertError("string", p, "Subscription.CandlesPeriodKey") + } + fundingPeriod = ":p" + s + } + req["key"] = "trade:" + timeframe + ":" + prefix + formattedPair + fundingPeriod + } else { + req["symbol"] = prefix + formattedPair } - return errs + + return req, nil +} + +// unsubscribeFromChan sends a websocket message to stop receiving data from a channel +func (b *Bitfinex) unsubscribeFromChan(c *stream.ChannelSubscription) error { + chanID, ok := c.Key.(int) + if !ok { + return common.GetTypeAssertError("int", c.Key, "chanID") + } + + req := map[string]interface{}{ + "event": "unsubscribe", + "chanId": chanID, + } + + respRaw, err := b.Websocket.Conn.SendMessageReturnResponse("unsubscribe:"+strconv.Itoa(chanID), req) + if err != nil { + return err + } + + if err := b.getErrResp(respRaw); err != nil { + wErr := fmt.Errorf("%w from ChanId: %v; %w", stream.ErrUnsubscribeFailure, chanID, err) + b.Websocket.DataHandler <- wErr + return wErr + } + + b.Websocket.RemoveSubscriptions(*c) + + return nil +} + +// getErrResp takes a json response string and looks for an error event type +// If found it parses the error code and message as a wrapped error and returns it +// It might log parsing errors about the nature of the error +// If the error message is not defined it will return a wrapped errUnknownError +func (b *Bitfinex) getErrResp(resp []byte) error { + event, err := jsonparser.GetUnsafeString(resp, "event") + if err != nil { + return fmt.Errorf("%w 'event': %w from message: %s", errParsingWSField, err, resp) + } + if event != "error" { + return nil + } + errCode, err := jsonparser.GetInt(resp, "code") + if err != nil { + log.Errorf(log.ExchangeSys, "%s %s 'code': %s from message: %s", b.Name, errParsingWSField, err, resp) + } + + var apiErr error + if msg, e2 := jsonparser.GetString(resp, "msg"); e2 != nil { + log.Errorf(log.ExchangeSys, "%s %s 'msg': %s from message: %s", b.Name, errParsingWSField, e2, resp) + apiErr = errUnknownError + } else { + apiErr = errors.New(msg) + } + return fmt.Errorf("%w (code: %d)", apiErr, errCode) } // WsSendAuth sends a authenticated event payload @@ -1726,56 +1882,6 @@ func (b *Bitfinex) WsSendAuth(ctx context.Context) error { return nil } -// WsAddSubscriptionChannel adds a confirmed channel subscription mapping from id to original params -func (b *Bitfinex) WsAddSubscriptionChannel(chanID int, channel, symbol string) error { - assetType, pair, err := assetPairFromSymbol(symbol) - if err != nil { - return err - } - - var c *stream.ChannelSubscription - s := b.Websocket.GetSubscriptions() - for i := range s { - if strings.EqualFold(s[i].Channel, channel) && s[i].Currency.Equal(pair) && s[i].Asset == assetType { - c = &s[i] - break - } - } - - if c == nil { - log.Errorf(log.ExchangeSys, - "%s Could not find an existing channel subscription: %s Pair: %s ChannelID: %d Asset: %s\n", - b.Name, - channel, - pair, - chanID, - assetType) - c = &stream.ChannelSubscription{ - Channel: channel, - Currency: pair, - Asset: assetType, - } - } - - if c.Params == nil { - c.Params = map[string]interface{}{} - } - - c.Params["chanId"] = chanID - - b.WebsocketSubdChannels[chanID] = c - - if b.Verbose { - log.Debugf(log.ExchangeSys, - "%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n", - b.Name, - channel, - pair, - chanID) - } - return nil -} - // WsNewOrder authenticated new order request func (b *Bitfinex) WsNewOrder(data *WsNewOrderRequest) (string, error) { data.CustomID = b.Websocket.AuthConn.GenerateMessageID(false) @@ -2087,42 +2193,3 @@ subSort: break } } - -func assetPairFromSymbol(symbol string) (asset.Item, currency.Pair, error) { - assetType := asset.Spot - - if symbol == "" { - return assetType, currency.EMPTYPAIR, nil - } - - switch symbol[0] { - case 'f': - assetType = asset.MarginFunding - case 't': - assetType = asset.Spot - default: - return assetType, currency.EMPTYPAIR, fmt.Errorf("unknown pair prefix: %v", symbol[0]) - } - - pair, err := currency.NewPairFromString(symbol[1:]) - - return assetType, pair, err -} - -// symbolFromCandleKey extracts the symbol or pair from a subscribed channel key -// e.g. trade:1h:tBTC, trade:1h:tBTC:CNHT, trade:1m:fBTC:p30 and trade:1m:fBTC:a30:p2:p30 -func symbolFromCandleKey(key string) (string, error) { - parts := strings.Split(key, ":") - if len(parts) < 3 { - return "", fmt.Errorf("subscription key has too few parts, need 3: %v", key) - } - parts = parts[2:] - if parts[0][0] == 'f' { - // Margin Funding subscription has one currency, and suffixes - return parts[0], nil - } - if len(parts) > 2 { - return "", fmt.Errorf("subscription key has too many parts for trade types: %v", key) - } - return strings.Join(parts, ":"), nil -} diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index c7bd983b069..4c0aafb5194 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -60,7 +60,6 @@ func (b *Bitfinex) SetDefaults() { b.Name = "Bitfinex" b.Enabled = true b.Verbose = true - b.WebsocketSubdChannels = make(map[int]*stream.ChannelSubscription) b.API.CredentialsValidator.RequiresKey = true b.API.CredentialsValidator.RequiresSecret = true diff --git a/exchanges/bitfinex/testdata/getErrResp.json b/exchanges/bitfinex/testdata/getErrResp.json new file mode 100644 index 00000000000..795d9f565b8 --- /dev/null +++ b/exchanges/bitfinex/testdata/getErrResp.json @@ -0,0 +1,5 @@ +{"bird": "great eared nightjar", "you_are_welcome":true} +{"event": {}} +{"event": "sneezegasm"} +{"event": "error"} +{"event": "error", "msg":"redcoats", "code":42} diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index b245c8aca3f..a90144de29d 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -657,7 +657,7 @@ func (b *Bitmex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) if err != nil { return err } - b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...) + b.Websocket.RemoveSubscriptions(channelsToUnsubscribe...) return nil } diff --git a/exchanges/bitstamp/bitstamp_websocket.go b/exchanges/bitstamp/bitstamp_websocket.go index e555f526eff..586f9eabc41 100644 --- a/exchanges/bitstamp/bitstamp_websocket.go +++ b/exchanges/bitstamp/bitstamp_websocket.go @@ -317,7 +317,7 @@ func (b *Bitstamp) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscriptio errs = common.AppendError(errs, err) continue } - b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i]) + b.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i]) } return errs } diff --git a/exchanges/bittrex/bittrex_websocket.go b/exchanges/bittrex/bittrex_websocket.go index 565b455763b..3cb4613793a 100644 --- a/exchanges/bittrex/bittrex_websocket.go +++ b/exchanges/bittrex/bittrex_websocket.go @@ -363,7 +363,7 @@ func (b *Bittrex) unsubscribeSlice(channelsToUnsubscribe []stream.ChannelSubscri errs = common.AppendError(errs, errors.New("unable to unsubscribe from "+channels[i]+" - error code "+response.Response[i].ErrorCode)) continue } - b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i]) + b.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i]) } return errs } diff --git a/exchanges/btcmarkets/btcmarkets_websocket.go b/exchanges/btcmarkets/btcmarkets_websocket.go index 1c91f3ada2b..62d0f7f8d10 100644 --- a/exchanges/btcmarkets/btcmarkets_websocket.go +++ b/exchanges/btcmarkets/btcmarkets_websocket.go @@ -429,7 +429,7 @@ func (b *BTCMarkets) Unsubscribe(subs []stream.ChannelSubscription) error { if err != nil { return err } - b.Websocket.RemoveSuccessfulUnsubscriptions(subs...) + b.Websocket.RemoveSubscriptions(subs...) return nil } diff --git a/exchanges/btse/btse_websocket.go b/exchanges/btse/btse_websocket.go index 8a1b0daed86..12071bc91c3 100644 --- a/exchanges/btse/btse_websocket.go +++ b/exchanges/btse/btse_websocket.go @@ -411,6 +411,6 @@ func (b *BTSE) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) e if err != nil { return err } - b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...) + b.Websocket.RemoveSubscriptions(channelsToUnsubscribe...) return nil } diff --git a/exchanges/bybit/bybit_websocket.go b/exchanges/bybit/bybit_websocket.go index 4f7ee986fc5..3843c710d34 100644 --- a/exchanges/bybit/bybit_websocket.go +++ b/exchanges/bybit/bybit_websocket.go @@ -170,7 +170,7 @@ func (by *Bybit) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) errs = common.AppendError(errs, err) continue } - by.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i]) + by.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i]) } return errs } diff --git a/exchanges/bybit/bybit_ws_cfutures.go b/exchanges/bybit/bybit_ws_cfutures.go index 39ca9e296da..c9b8cd77407 100644 --- a/exchanges/bybit/bybit_ws_cfutures.go +++ b/exchanges/bybit/bybit_ws_cfutures.go @@ -166,7 +166,7 @@ func (by *Bybit) UnsubscribeCoin(channelsToUnsubscribe []stream.ChannelSubscript errs = common.AppendError(errs, err) continue } - by.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i]) + by.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i]) } return errs } diff --git a/exchanges/bybit/bybit_ws_futures.go b/exchanges/bybit/bybit_ws_futures.go index d5f2ef84599..8f3c1d8956d 100644 --- a/exchanges/bybit/bybit_ws_futures.go +++ b/exchanges/bybit/bybit_ws_futures.go @@ -118,7 +118,7 @@ func (by *Bybit) UnsubscribeFutures(channelsToUnsubscribe []stream.ChannelSubscr errs = common.AppendError(errs, err) continue } - by.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i]) + by.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i]) } return errs } diff --git a/exchanges/bybit/bybit_ws_ufutures.go b/exchanges/bybit/bybit_ws_ufutures.go index 73f7fd0e457..bcf17f92d60 100644 --- a/exchanges/bybit/bybit_ws_ufutures.go +++ b/exchanges/bybit/bybit_ws_ufutures.go @@ -123,7 +123,7 @@ func (by *Bybit) UnsubscribeUSDT(channelsToUnsubscribe []stream.ChannelSubscript errs = common.AppendError(errs, err) continue } - by.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i]) + by.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i]) } if errs != nil { return errs diff --git a/exchanges/coinbasepro/coinbasepro_websocket.go b/exchanges/coinbasepro/coinbasepro_websocket.go index 286af6f18b9..3be31a7cd77 100644 --- a/exchanges/coinbasepro/coinbasepro_websocket.go +++ b/exchanges/coinbasepro/coinbasepro_websocket.go @@ -481,6 +481,6 @@ unsubscriptions: if err != nil { return err } - c.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...) + c.Websocket.RemoveSubscriptions(channelsToUnsubscribe...) return nil } diff --git a/exchanges/coinut/coinut_websocket.go b/exchanges/coinut/coinut_websocket.go index 5704f065cea..5f689d9b988 100644 --- a/exchanges/coinut/coinut_websocket.go +++ b/exchanges/coinut/coinut_websocket.go @@ -684,7 +684,7 @@ func (c *COINUT) Unsubscribe(channelToUnsubscribe []stream.ChannelSubscription) channelToUnsubscribe[i].Channel)) continue } - c.Websocket.RemoveSuccessfulUnsubscriptions(channelToUnsubscribe[i]) + c.Websocket.RemoveSubscriptions(channelToUnsubscribe[i]) } return errs } diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index 282d2dcfffc..e345c7f0a91 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -712,7 +712,7 @@ func (g *Gateio) handleSubscription(event string, channelsToSubscribe []stream.C if payloads[k].Event == "subscribe" { g.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[k]) } else { - g.Websocket.RemoveSuccessfulUnsubscriptions(channelsToSubscribe[k]) + g.Websocket.RemoveSubscriptions(channelsToSubscribe[k]) } } } diff --git a/exchanges/gateio/gateio_ws_option.go b/exchanges/gateio/gateio_ws_option.go index 26867e77404..e299a2c4e40 100644 --- a/exchanges/gateio/gateio_ws_option.go +++ b/exchanges/gateio/gateio_ws_option.go @@ -331,7 +331,7 @@ func (g *Gateio) handleOptionsSubscription(event string, channelsToSubscribe []s if payloads[k].Event == "subscribe" { g.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[k]) } else { - g.Websocket.RemoveSuccessfulUnsubscriptions(channelsToSubscribe[k]) + g.Websocket.RemoveSubscriptions(channelsToSubscribe[k]) } } } diff --git a/exchanges/gemini/gemini_websocket.go b/exchanges/gemini/gemini_websocket.go index 1ea3f5cdbaf..bc35ffe2bd5 100644 --- a/exchanges/gemini/gemini_websocket.go +++ b/exchanges/gemini/gemini_websocket.go @@ -171,7 +171,7 @@ func (g *Gemini) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) return err } - g.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...) + g.Websocket.RemoveSubscriptions(channelsToUnsubscribe...) return nil } diff --git a/exchanges/hitbtc/hitbtc_websocket.go b/exchanges/hitbtc/hitbtc_websocket.go index accc7cf13be..8c7b367769d 100644 --- a/exchanges/hitbtc/hitbtc_websocket.go +++ b/exchanges/hitbtc/hitbtc_websocket.go @@ -558,7 +558,7 @@ func (h *HitBTC) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) errs = common.AppendError(errs, err) continue } - h.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i]) + h.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i]) } if errs != nil { return errs diff --git a/exchanges/huobi/huobi_websocket.go b/exchanges/huobi/huobi_websocket.go index 3f9fcb7f49e..0f9101a068a 100644 --- a/exchanges/huobi/huobi_websocket.go +++ b/exchanges/huobi/huobi_websocket.go @@ -606,7 +606,7 @@ func (h *HUOBI) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) errs = common.AppendError(errs, err) continue } - h.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i]) + h.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i]) continue } err := h.Websocket.Conn.SendJSONMessage(WsRequest{ @@ -616,7 +616,7 @@ func (h *HUOBI) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) errs = common.AppendError(errs, err) continue } - h.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i]) + h.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i]) } if errs != nil { return errs diff --git a/exchanges/kraken/kraken_websocket.go b/exchanges/kraken/kraken_websocket.go index 4b422751170..719785b888e 100644 --- a/exchanges/kraken/kraken_websocket.go +++ b/exchanges/kraken/kraken_websocket.go @@ -1347,7 +1347,7 @@ channels: errs = common.AppendError(errs, err) continue } - k.Websocket.RemoveSuccessfulUnsubscriptions(unsubs[i].Channels...) + k.Websocket.RemoveSubscriptions(unsubs[i].Channels...) continue } @@ -1356,7 +1356,7 @@ channels: errs = common.AppendError(errs, err) continue } - k.Websocket.RemoveSuccessfulUnsubscriptions(unsubs[i].Channels...) + k.Websocket.RemoveSubscriptions(unsubs[i].Channels...) } return errs } diff --git a/exchanges/okcoin/okcoin_websocket.go b/exchanges/okcoin/okcoin_websocket.go index a029757fe8f..1d058501d64 100644 --- a/exchanges/okcoin/okcoin_websocket.go +++ b/exchanges/okcoin/okcoin_websocket.go @@ -926,9 +926,9 @@ func (o *Okcoin) handleSubscriptions(operation string, subs []stream.ChannelSubs if operation == "unsubscribe" { if authenticatedChannelSubscription { - o.Websocket.RemoveSuccessfulUnsubscriptions(authChannels...) + o.Websocket.RemoveSubscriptions(authChannels...) } else { - o.Websocket.RemoveSuccessfulUnsubscriptions(channels...) + o.Websocket.RemoveSubscriptions(channels...) } } else { if authenticatedChannelSubscription { @@ -968,7 +968,7 @@ func (o *Okcoin) handleSubscriptions(operation string, subs []stream.ChannelSubs } } if operation == "unsubscribe" { - o.Websocket.RemoveSuccessfulUnsubscriptions(channels...) + o.Websocket.RemoveSubscriptions(channels...) } else { o.Websocket.AddSuccessfulSubscriptions(channels...) } diff --git a/exchanges/okx/okx_websocket.go b/exchanges/okx/okx_websocket.go index dee1cd884f7..71c05a82cb1 100644 --- a/exchanges/okx/okx_websocket.go +++ b/exchanges/okx/okx_websocket.go @@ -478,7 +478,7 @@ func (ok *Okx) handleSubscription(operation string, subscriptions []stream.Chann return err } if operation == operationUnsubscribe { - ok.Websocket.RemoveSuccessfulUnsubscriptions(channels...) + ok.Websocket.RemoveSubscriptions(channels...) } else { ok.Websocket.AddSuccessfulSubscriptions(channels...) } @@ -500,7 +500,7 @@ func (ok *Okx) handleSubscription(operation string, subscriptions []stream.Chann return err } if operation == operationUnsubscribe { - ok.Websocket.RemoveSuccessfulUnsubscriptions(channels...) + ok.Websocket.RemoveSubscriptions(channels...) } else { ok.Websocket.AddSuccessfulSubscriptions(channels...) } @@ -529,7 +529,7 @@ func (ok *Okx) handleSubscription(operation string, subscriptions []stream.Chann if operation == operationUnsubscribe { channels = append(channels, authChannels...) - ok.Websocket.RemoveSuccessfulUnsubscriptions(channels...) + ok.Websocket.RemoveSubscriptions(channels...) } else { channels = append(channels, authChannels...) ok.Websocket.AddSuccessfulSubscriptions(channels...) diff --git a/exchanges/poloniex/poloniex_websocket.go b/exchanges/poloniex/poloniex_websocket.go index 718507949ae..2a32b060f72 100644 --- a/exchanges/poloniex/poloniex_websocket.go +++ b/exchanges/poloniex/poloniex_websocket.go @@ -639,7 +639,7 @@ channels: errs = common.AppendError(errs, err) continue channels } - p.Websocket.RemoveSuccessfulUnsubscriptions(unsub[i]) + p.Websocket.RemoveSubscriptions(unsub[i]) continue channels case strings.EqualFold(strconv.FormatInt(wsTickerDataID, 10), unsub[i].Channel): @@ -652,7 +652,7 @@ channels: errs = common.AppendError(errs, err) continue } - p.Websocket.RemoveSuccessfulUnsubscriptions(unsub[i]) + p.Websocket.RemoveSubscriptions(unsub[i]) } if errs != nil { return errs diff --git a/exchanges/stream/stream_match.go b/exchanges/stream/stream_match.go index 431bdba9384..dc2d46b3ac2 100644 --- a/exchanges/stream/stream_match.go +++ b/exchanges/stream/stream_match.go @@ -5,7 +5,7 @@ import ( "sync" ) -// NewMatch returns a new matcher +// NewMatch returns a new Match func NewMatch() *Match { return &Match{ m: make(map[interface{}]chan []byte), @@ -21,13 +21,20 @@ type Match struct { mu sync.Mutex } +// Matcher defines a payload matching return mechanism +type Matcher struct { + C chan []byte + sig interface{} + m *Match +} + // Incoming matches with request, disregarding the returned payload func (m *Match) Incoming(signature interface{}) bool { return m.IncomingWithData(signature, nil) } // IncomingWithData matches with requests and takes in the returned payload, to -// be processed outside of a stream processing routine +// be processed outside of a stream processing routine and returns true if a handler was found func (m *Match) IncomingWithData(signature interface{}, data []byte) bool { m.mu.Lock() defer m.mu.Unlock() @@ -44,35 +51,28 @@ func (m *Match) IncomingWithData(signature interface{}, data []byte) bool { return false } -// Sets the signature response channel for incoming data -func (m *Match) set(signature interface{}) (matcher, error) { +// Set the signature response channel for incoming data +func (m *Match) Set(signature interface{}) (Matcher, error) { var ch chan []byte m.mu.Lock() if _, ok := m.m[signature]; ok { m.mu.Unlock() - return matcher{}, errors.New("signature collision") + return Matcher{}, errors.New("signature collision") } // This is buffered so we don't need to wait for receiver. ch = make(chan []byte, 1) m.m[signature] = ch m.mu.Unlock() - return matcher{ + return Matcher{ C: ch, sig: signature, m: m, }, nil } -// matcher defines a payload matching return mechanism -type matcher struct { - C chan []byte - sig interface{} - m *Match -} - // Cleanup closes underlying channel and deletes signature from map -func (m *matcher) Cleanup() { +func (m *Matcher) Cleanup() { m.m.mu.Lock() close(m.C) delete(m.m.m, m.sig) diff --git a/exchanges/stream/stream_match_test.go b/exchanges/stream/stream_match_test.go index 5ab57f7c6f2..2659122535f 100644 --- a/exchanges/stream/stream_match_test.go +++ b/exchanges/stream/stream_match_test.go @@ -18,12 +18,12 @@ func TestMatch(t *testing.T) { t.Fatal("should not be able to match") } - m, err := nm.set("hello") + m, err := nm.Set("hello") if err != nil { t.Fatal(err) } - _, err = nm.set("hello") + _, err = nm.Set("hello") if err == nil { t.Fatal("error cannot be nil as this collision cannot occur") } diff --git a/exchanges/stream/stream_types.go b/exchanges/stream/stream_types.go index 053e5688a30..de2b16ae9d3 100644 --- a/exchanges/stream/stream_types.go +++ b/exchanges/stream/stream_types.go @@ -31,12 +31,31 @@ type Response struct { Raw []byte } -// ChannelSubscription container for streaming subscriptions +// DefaultChannelKey is the fallback key for AddSuccessfulSubscriptions +type DefaultChannelKey struct { + Channel string + Currency currency.Pair + Asset asset.Item +} + +// ChannelState tracks the status of a subscription channel +type ChannelState uint8 + +const ( + ChannelStateUnknown ChannelState = iota // ChannelStateUnknown means subscription state is not registered, but doesn't imply Inactive + ChannelSubscribing // ChannelSubscribing means channel is in the process of subscribing + ChannelSubscribed // ChannelSubscribed means the channel has finished a successful and acknowledged subscription + ChannelUnsubscribing // ChannelUnsubscribing means the channel has started to unsubscribe, but not yet confirmed +) + +// ChannelSubscription container for streaming subscription channels type ChannelSubscription struct { + Key any Channel string Currency currency.Pair Asset asset.Item Params map[string]interface{} + State ChannelState } // ConnectionSetup defines variables for an individual stream connection diff --git a/exchanges/stream/websocket.go b/exchanges/stream/websocket.go index 11f692a11d8..8a4e088b5aa 100644 --- a/exchanges/stream/websocket.go +++ b/exchanges/stream/websocket.go @@ -5,7 +5,6 @@ import ( "fmt" "net" "net/url" - "strings" "sync" "time" @@ -25,8 +24,16 @@ const ( ) var ( + // ErrSubscriptionNotFound defines an error when a subscription is not found + ErrSubscriptionNotFound = errors.New("subscription not found") + // ErrSubscribedAlready defines an error when a channel is already subscribed + ErrSubscribedAlready = errors.New("duplicate subscription") // ErrSubscriptionFailure defines an error when a subscription fails ErrSubscriptionFailure = errors.New("subscription failure") + // ErrUnsubscribeFailure defines an error when a unsubscribe fails + ErrUnsubscribeFailure = errors.New("unsubscribe failure") + // ErrChannelInStateAlready defines an error when a subscription channel is already in a new state + ErrChannelInStateAlready = errors.New("channel already in state") // ErrAlreadyDisabled is returned when you double-disable the websocket ErrAlreadyDisabled = errors.New("websocket already disabled") // ErrNotConnected defines an error when websocket is not connected @@ -52,7 +59,8 @@ var ( errSubscriptionsExceedsLimit = errors.New("subscriptions exceeds limit") errInvalidMaxSubscriptions = errors.New("max subscriptions cannot be less than 0") errNoSubscriptionsSupplied = errors.New("no subscriptions supplied") - errChannelSubscriptionAlreadySubscribed = errors.New("channel subscription already subscribed") + errChannelAlreadySubscribed = errors.New("channel already subscribed") + errInvalidChannelState = errors.New("invalid Channel state") ) var globalReporter Reporter @@ -373,9 +381,9 @@ func (w *Websocket) connectionMonitor() error { if w.checkAndSetMonitorRunning() { return errAlreadyRunning } - w.connectionMutex.RLock() + w.fieldMutex.RLock() delay := w.connectionMonitorDelay - w.connectionMutex.RUnlock() + w.fieldMutex.RUnlock() go func() { timer := time.NewTimer(delay) @@ -477,7 +485,7 @@ func (w *Websocket) Shutdown() error { // flush any subscriptions from last connection if needed w.subscriptionMutex.Lock() - w.subscriptions = nil + w.subscriptions = subscriptionMap{} w.subscriptionMutex.Unlock() close(w.ShutdownC) @@ -537,7 +545,7 @@ func (w *Websocket) FlushChannels() error { if len(newsubs) != 0 { // Purge subscription list as there will be conflicts w.subscriptionMutex.Lock() - w.subscriptions = nil + w.subscriptions = subscriptionMap{} w.subscriptionMutex.Unlock() return w.SubscribeToChannels(newsubs) } @@ -629,73 +637,73 @@ func (w *Websocket) trafficMonitor() { } func (w *Websocket) setConnectedStatus(b bool) { - w.connectionMutex.Lock() + w.fieldMutex.Lock() w.connected = b - w.connectionMutex.Unlock() + w.fieldMutex.Unlock() } // IsConnected returns status of connection func (w *Websocket) IsConnected() bool { - w.connectionMutex.RLock() - defer w.connectionMutex.RUnlock() + w.fieldMutex.RLock() + defer w.fieldMutex.RUnlock() return w.connected } func (w *Websocket) setConnectingStatus(b bool) { - w.connectionMutex.Lock() + w.fieldMutex.Lock() w.connecting = b - w.connectionMutex.Unlock() + w.fieldMutex.Unlock() } // IsConnecting returns status of connecting func (w *Websocket) IsConnecting() bool { - w.connectionMutex.RLock() - defer w.connectionMutex.RUnlock() + w.fieldMutex.RLock() + defer w.fieldMutex.RUnlock() return w.connecting } func (w *Websocket) setEnabled(b bool) { - w.connectionMutex.Lock() + w.fieldMutex.Lock() w.enabled = b - w.connectionMutex.Unlock() + w.fieldMutex.Unlock() } // IsEnabled returns status of enabled func (w *Websocket) IsEnabled() bool { - w.connectionMutex.RLock() - defer w.connectionMutex.RUnlock() + w.fieldMutex.RLock() + defer w.fieldMutex.RUnlock() return w.enabled } func (w *Websocket) setInit(b bool) { - w.connectionMutex.Lock() + w.fieldMutex.Lock() w.Init = b - w.connectionMutex.Unlock() + w.fieldMutex.Unlock() } // IsInit returns status of init func (w *Websocket) IsInit() bool { - w.connectionMutex.RLock() - defer w.connectionMutex.RUnlock() + w.fieldMutex.RLock() + defer w.fieldMutex.RUnlock() return w.Init } func (w *Websocket) setTrafficMonitorRunning(b bool) { - w.connectionMutex.Lock() + w.fieldMutex.Lock() w.trafficMonitorRunning = b - w.connectionMutex.Unlock() + w.fieldMutex.Unlock() } // IsTrafficMonitorRunning returns status of the traffic monitor func (w *Websocket) IsTrafficMonitorRunning() bool { - w.connectionMutex.RLock() - defer w.connectionMutex.RUnlock() + w.fieldMutex.RLock() + defer w.fieldMutex.RUnlock() return w.trafficMonitorRunning } func (w *Websocket) checkAndSetMonitorRunning() (alreadyRunning bool) { - w.connectionMutex.Lock() - defer w.connectionMutex.Unlock() + w.fieldMutex.Lock() + defer w.fieldMutex.Unlock() if w.connectionMonitorRunning { return true } @@ -704,28 +712,28 @@ func (w *Websocket) checkAndSetMonitorRunning() (alreadyRunning bool) { } func (w *Websocket) setConnectionMonitorRunning(b bool) { - w.connectionMutex.Lock() + w.fieldMutex.Lock() w.connectionMonitorRunning = b - w.connectionMutex.Unlock() + w.fieldMutex.Unlock() } // IsConnectionMonitorRunning returns status of connection monitor func (w *Websocket) IsConnectionMonitorRunning() bool { - w.connectionMutex.RLock() - defer w.connectionMutex.RUnlock() + w.fieldMutex.RLock() + defer w.fieldMutex.RUnlock() return w.connectionMonitorRunning } func (w *Websocket) setDataMonitorRunning(b bool) { - w.connectionMutex.Lock() + w.fieldMutex.Lock() w.dataMonitorRunning = b - w.connectionMutex.Unlock() + w.fieldMutex.Unlock() } // IsDataMonitorRunning returns status of data monitor func (w *Websocket) IsDataMonitorRunning() bool { - w.connectionMutex.RLock() - defer w.connectionMutex.RUnlock() + w.fieldMutex.RLock() + defer w.fieldMutex.RUnlock() return w.dataMonitorRunning } @@ -862,52 +870,44 @@ func (w *Websocket) GetName() string { // GetChannelDifference finds the difference between the subscribed channels // and the new subscription list when pairs are disabled or enabled. func (w *Websocket) GetChannelDifference(genSubs []ChannelSubscription) (sub, unsub []ChannelSubscription) { - w.subscriptionMutex.Lock() - defer w.subscriptionMutex.Unlock() - -oldsubs: - for x := range w.subscriptions { - for y := range genSubs { - if w.subscriptions[x].Equal(&genSubs[y]) { - continue oldsubs - } + w.subscriptionMutex.RLock() + unsubMap := make(map[any]ChannelSubscription, len(w.subscriptions)) + for k, c := range w.subscriptions { + unsubMap[k] = *c + } + w.subscriptionMutex.RUnlock() + + for i := range genSubs { + key := genSubs[i].EnsureKeyed() + if _, ok := unsubMap[key]; ok { + delete(unsubMap, key) // If it's in both then we remove it from the unsubscribe list + } else { + sub = append(sub, genSubs[i]) // If it's in genSubs but not existing subs we want to subscribe } - unsub = append(unsub, w.subscriptions[x]) } -newsubs: - for x := range genSubs { - for y := range w.subscriptions { - if genSubs[x].Equal(&w.subscriptions[y]) { - continue newsubs - } - } - sub = append(sub, genSubs[x]) + for _, c := range unsubMap { + unsub = append(unsub, c) } + return } // UnsubscribeChannels unsubscribes from a websocket channel func (w *Websocket) UnsubscribeChannels(channels []ChannelSubscription) error { if len(channels) == 0 { - return fmt.Errorf("%s websocket: channels not populated cannot remove", - w.exchangeName) + return fmt.Errorf("%s websocket: %w", w.exchangeName, errNoSubscriptionsSupplied) } - w.subscriptionMutex.Lock() + w.subscriptionMutex.RLock() -channels: - for x := range channels { - for y := range w.subscriptions { - if channels[x].Equal(&w.subscriptions[y]) { - continue channels - } + for i := range channels { + key := channels[i].EnsureKeyed() + if _, ok := w.subscriptions[key]; !ok { + w.subscriptionMutex.RUnlock() + return fmt.Errorf("%s websocket: %w: %+v", w.exchangeName, ErrSubscriptionNotFound, channels[i]) } - w.subscriptionMutex.Unlock() - return fmt.Errorf("%s websocket: subscription not found in list: %+v", - w.exchangeName, - channels[x]) } - w.subscriptionMutex.Unlock() + w.subscriptionMutex.RUnlock() return w.Unsubscriber(channels) } @@ -922,69 +922,138 @@ func (w *Websocket) ResubscribeToChannel(subscribedChannel *ChannelSubscription) // SubscribeToChannels appends supplied channels to channelsToSubscribe func (w *Websocket) SubscribeToChannels(channels []ChannelSubscription) error { - err := w.checkSubscriptions(channels) - if err != nil { + if err := w.checkSubscriptions(channels); err != nil { return fmt.Errorf("%s websocket: %w", w.exchangeName, common.AppendError(ErrSubscriptionFailure, err)) } - err = w.Subscriber(channels) - if err != nil { + if err := w.Subscriber(channels); err != nil { return fmt.Errorf("%s websocket: %w", w.exchangeName, common.AppendError(ErrSubscriptionFailure, err)) } return nil } +// AddSubscription adds a subscription to the subscription lists +// Unlike AddSubscriptions this method will error if the subscription already exists +func (w *Websocket) AddSubscription(c *ChannelSubscription) error { + w.subscriptionMutex.Lock() + defer w.subscriptionMutex.Unlock() + if w.subscriptions == nil { + w.subscriptions = subscriptionMap{} + } + key := c.EnsureKeyed() + if _, ok := w.subscriptions[key]; ok { + return ErrSubscribedAlready + } + + n := *c // Fresh copy; we don't want to use the pointer we were given and allow encapsulation/locks to be bypassed + w.subscriptions[key] = &n + + return nil +} + +// SetSubscriptionState sets an existing subscription state +// returns an error if the subscription is not found, or the new state is already set +func (w *Websocket) SetSubscriptionState(c *ChannelSubscription, state ChannelState) error { + w.subscriptionMutex.Lock() + defer w.subscriptionMutex.Unlock() + if w.subscriptions == nil { + w.subscriptions = subscriptionMap{} + } + key := c.EnsureKeyed() + p, ok := w.subscriptions[key] + if !ok { + return ErrSubscriptionNotFound + } + if state == p.State { + return ErrChannelInStateAlready + } + if state > ChannelUnsubscribing { + return errInvalidChannelState + } + p.State = state + return nil +} + // AddSuccessfulSubscriptions adds subscriptions to the subscription lists that // has been successfully subscribed func (w *Websocket) AddSuccessfulSubscriptions(channels ...ChannelSubscription) { w.subscriptionMutex.Lock() - w.subscriptions = append(w.subscriptions, channels...) - w.subscriptionMutex.Unlock() + defer w.subscriptionMutex.Unlock() + if w.subscriptions == nil { + w.subscriptions = subscriptionMap{} + } + for _, cN := range channels { + c := cN // cN is an iteration var; Not safe to make a pointer to + key := c.EnsureKeyed() + c.State = ChannelSubscribed + w.subscriptions[key] = &c + } } -// RemoveSuccessfulUnsubscriptions removes subscriptions from the subscription -// list that has been successfulling unsubscribed -func (w *Websocket) RemoveSuccessfulUnsubscriptions(channels ...ChannelSubscription) { +// RemoveSubscriptions removes subscriptions from the subscription list +func (w *Websocket) RemoveSubscriptions(channels ...ChannelSubscription) { w.subscriptionMutex.Lock() defer w.subscriptionMutex.Unlock() - for x := range channels { - for y := range w.subscriptions { - if channels[x].Equal(&w.subscriptions[y]) { - w.subscriptions[y] = w.subscriptions[len(w.subscriptions)-1] - w.subscriptions[len(w.subscriptions)-1] = ChannelSubscription{} - w.subscriptions = w.subscriptions[:len(w.subscriptions)-1] - break - } + if w.subscriptions == nil { + w.subscriptions = subscriptionMap{} + } + for i := range channels { + key := channels[i].EnsureKeyed() + delete(w.subscriptions, key) + } +} + +// EnsureKeyed sets the default key on a channel if it doesn't have one +// Returns key for convenience +func (c *ChannelSubscription) EnsureKeyed() any { + if c.Key == nil { + c.Key = DefaultChannelKey{ + Channel: c.Channel, + Asset: c.Asset, + Currency: c.Currency, } } + return c.Key } -// Equal two WebsocketChannelSubscription to determine equality -func (w *ChannelSubscription) Equal(s *ChannelSubscription) bool { - return strings.EqualFold(w.Channel, s.Channel) && - w.Currency.Equal(s.Currency) +// GetSubscription returns a pointer to a copy of the subscription at the key provided +// returns nil if no subscription is at that key or the key is nil +func (w *Websocket) GetSubscription(key any) *ChannelSubscription { + if key == nil || w == nil || w.subscriptions == nil { + return nil + } + w.subscriptionMutex.RLock() + defer w.subscriptionMutex.RUnlock() + if s, ok := w.subscriptions[key]; ok { + c := *s + return &c + } + return nil } -// GetSubscriptions returns a copied list of subscriptions -// and is a private member that cannot be manipulated +// GetSubscriptions returns a new slice of the subscriptions func (w *Websocket) GetSubscriptions() []ChannelSubscription { - w.subscriptionMutex.Lock() - defer w.subscriptionMutex.Unlock() - return append(w.subscriptions[:0:0], w.subscriptions...) + w.subscriptionMutex.RLock() + defer w.subscriptionMutex.RUnlock() + subs := make([]ChannelSubscription, 0, len(w.subscriptions)) + for _, c := range w.subscriptions { + subs = append(subs, *c) + } + return subs } // SetCanUseAuthenticatedEndpoints sets canUseAuthenticatedEndpoints val in // a thread safe manner func (w *Websocket) SetCanUseAuthenticatedEndpoints(val bool) { - w.subscriptionMutex.Lock() - defer w.subscriptionMutex.Unlock() + w.fieldMutex.Lock() + defer w.fieldMutex.Unlock() w.canUseAuthenticatedEndpoints = val } // CanUseAuthenticatedEndpoints gets canUseAuthenticatedEndpoints val in // a thread safe manner func (w *Websocket) CanUseAuthenticatedEndpoints() bool { - w.subscriptionMutex.Lock() - defer w.subscriptionMutex.Unlock() + w.fieldMutex.RLock() + defer w.fieldMutex.RUnlock() return w.canUseAuthenticatedEndpoints } @@ -1018,8 +1087,8 @@ func (w *Websocket) checkSubscriptions(subs []ChannelSubscription) error { return errNoSubscriptionsSupplied } - w.subscriptionMutex.Lock() - defer w.subscriptionMutex.Unlock() + w.subscriptionMutex.RLock() + defer w.subscriptionMutex.RUnlock() if w.MaxSubscriptionsPerConnection > 0 && len(w.subscriptions)+len(subs) > w.MaxSubscriptionsPerConnection { return fmt.Errorf("%w: current subscriptions: %v, incoming subscriptions: %v, max subscriptions per connection: %v - please reduce enabled pairs", @@ -1029,12 +1098,12 @@ func (w *Websocket) checkSubscriptions(subs []ChannelSubscription) error { w.MaxSubscriptionsPerConnection) } - for x := range subs { - for y := range w.subscriptions { - if subs[x].Equal(&w.subscriptions[y]) { - return fmt.Errorf("%w for %+v", errChannelSubscriptionAlreadySubscribed, subs[x]) - } + for i := range subs { + key := subs[i].EnsureKeyed() + if _, ok := w.subscriptions[key]; ok { + return fmt.Errorf("%w for %+v", errChannelAlreadySubscribed, subs[i]) } } + return nil } diff --git a/exchanges/stream/websocket_connection.go b/exchanges/stream/websocket_connection.go index b5b98ced22b..0bb1e660412 100644 --- a/exchanges/stream/websocket_connection.go +++ b/exchanges/stream/websocket_connection.go @@ -22,7 +22,7 @@ import ( // SendMessageReturnResponse will send a WS message to the connection and wait // for response func (w *WebsocketConnection) SendMessageReturnResponse(signature, request interface{}) ([]byte, error) { - m, err := w.Match.set(signature) + m, err := w.Match.Set(signature) if err != nil { return nil, err } diff --git a/exchanges/stream/websocket_test.go b/exchanges/stream/websocket_test.go index 2f68e5ba3a1..e362a31e16c 100644 --- a/exchanges/stream/websocket_test.go +++ b/exchanges/stream/websocket_test.go @@ -9,6 +9,7 @@ import ( "fmt" "net" "net/http" + "sort" "strconv" "strings" "sync" @@ -16,8 +17,10 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" ) @@ -52,6 +55,10 @@ type testResponse struct { RequestID int64 `json:"reqid,omitempty"` } +type testSubKey struct { + Mood string +} + var defaultSetup = &WebsocketSetup{ ExchangeConfig: &config.Exchange{ Features: &config.FeaturesConfig{ @@ -71,9 +78,9 @@ var defaultSetup = &WebsocketSetup{ GenerateSubscriptions: func() ([]ChannelSubscription, error) { return []ChannelSubscription{ {Channel: "TestSub"}, - {Channel: "TestSub2"}, - {Channel: "TestSub3"}, - {Channel: "TestSub4"}, + {Channel: "TestSub2", Key: "purple"}, + {Channel: "TestSub3", Key: testSubKey{"mauve"}}, + {Channel: "TestSub4", Key: 42}, }, nil }, Features: &protocol.Features{Subscribe: true, Unsubscribe: true}, @@ -495,62 +502,48 @@ func TestWebsocket(t *testing.T) { func TestSubscribeUnsubscribe(t *testing.T) { t.Parallel() ws := *New() - err := ws.Setup(defaultSetup) - if err != nil { - t.Fatal(err) - } + assert.NoError(t, ws.Setup(defaultSetup), "WS Setup should not error") fnSub := func(subs []ChannelSubscription) error { ws.AddSuccessfulSubscriptions(subs...) return nil } fnUnsub := func(unsubs []ChannelSubscription) error { - ws.RemoveSuccessfulUnsubscriptions(unsubs...) + ws.RemoveSubscriptions(unsubs...) return nil } ws.Subscriber = fnSub ws.Unsubscriber = fnUnsub - err = ws.UnsubscribeChannels(nil) - if err == nil { - t.Fatal("error cannot be nil") - } - - // Generate test sub subs, err := ws.GenerateSubs() - if err != nil { - t.Fatal(err) - } - - // unsub when no subscribed channel - err = ws.UnsubscribeChannels(subs) - if err == nil { - t.Fatal("error cannot be nil") - } - - err = ws.SubscribeToChannels(subs) - if err != nil { - t.Fatal(err) - } - - // subscribe when already subscribed - err = ws.SubscribeToChannels(subs) - if err == nil { - t.Fatal("error cannot be nil") - } - - // subscribe to nothing - err = ws.SubscribeToChannels(nil) - if err == nil { - t.Fatal("error cannot be nil") - } - - err = ws.UnsubscribeChannels(subs) - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err, "Generating test subscriptions should not error") + assert.ErrorIs(t, ws.UnsubscribeChannels(nil), errNoSubscriptionsSupplied, "Unsubscribing from nil should error") + assert.ErrorIs(t, ws.UnsubscribeChannels(subs), ErrSubscriptionNotFound, "Unsubscribing should error when not subscribed") + assert.Nil(t, ws.GetSubscription(42), "GetSubscription on empty internal map should return") + assert.NoError(t, ws.SubscribeToChannels(subs), "Basic Subscribing should not error") + assert.Len(t, ws.GetSubscriptions(), 4, "Should have 4 subscriptions") + byDefKey := ws.GetSubscription(DefaultChannelKey{Channel: "TestSub"}) + if assert.NotNil(t, byDefKey, "GetSubscription by default key should find a channel") { + assert.Equal(t, "TestSub", byDefKey.Channel, "GetSubscription by default key should return a pointer a copy of the right channel") + assert.NotSame(t, byDefKey, ws.subscriptions["TestSub"], "GetSubscription returns a fresh pointer") + } + if assert.NotNil(t, ws.GetSubscription("purple"), "GetSubscription by string key should find a channel") { + assert.Equal(t, "TestSub2", ws.GetSubscription("purple").Channel, "GetSubscription by string key should return a pointer a copy of the right channel") + } + if assert.NotNil(t, ws.GetSubscription(testSubKey{"mauve"}), "GetSubscription by type key should find a channel") { + assert.Equal(t, "TestSub3", ws.GetSubscription(testSubKey{"mauve"}).Channel, "GetSubscription by type key should return a pointer a copy of the right channel") + } + if assert.NotNil(t, ws.GetSubscription(42), "GetSubscription by int key should find a channel") { + assert.Equal(t, "TestSub4", ws.GetSubscription(42).Channel, "GetSubscription by int key should return a pointer a copy of the right channel") + } + assert.Nil(t, ws.GetSubscription(nil), "GetSubscription by nil should return nil") + assert.Nil(t, ws.GetSubscription(45), "GetSubscription by invalid key should return nil") + assert.ErrorIs(t, ws.SubscribeToChannels(subs), errChannelAlreadySubscribed, "Subscribe should error when already subscribed") + assert.ErrorIs(t, ws.SubscribeToChannels(nil), errNoSubscriptionsSupplied, "Subscribe to nil should error") + assert.NoError(t, ws.UnsubscribeChannels(subs), "Unsubscribing should not error") } +// TestResubscribe tests Resubscribing to existing subscriptions func TestResubscribe(t *testing.T) { t.Parallel() ws := *New() @@ -558,41 +551,66 @@ func TestResubscribe(t *testing.T) { wackedOutSetup := *defaultSetup wackedOutSetup.MaxWebsocketSubscriptionsPerConnection = -1 err := ws.Setup(&wackedOutSetup) - if !errors.Is(err, errInvalidMaxSubscriptions) { - t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidMaxSubscriptions) - } + assert.ErrorIs(t, err, errInvalidMaxSubscriptions, "Invalid MaxWebsocketSubscriptionsPerConnection should error") err = ws.Setup(defaultSetup) - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err, "WS Setup should not error") fnSub := func(subs []ChannelSubscription) error { ws.AddSuccessfulSubscriptions(subs...) return nil } fnUnsub := func(unsubs []ChannelSubscription) error { - ws.RemoveSuccessfulUnsubscriptions(unsubs...) + ws.RemoveSubscriptions(unsubs...) return nil } ws.Subscriber = fnSub ws.Unsubscriber = fnUnsub channel := []ChannelSubscription{{Channel: "resubTest"}} - err = ws.ResubscribeToChannel(&channel[0]) - if err == nil { - t.Fatal("error cannot be nil") - } - err = ws.SubscribeToChannels(channel) - if err != nil { - t.Fatal(err) - } + assert.ErrorIs(t, ws.ResubscribeToChannel(&channel[0]), ErrSubscriptionNotFound, "Resubscribe should error when channel isn't subscribed yet") + assert.NoError(t, ws.SubscribeToChannels(channel), "Subscribe should not error") + assert.NoError(t, ws.ResubscribeToChannel(&channel[0]), "Resubscribe should not error now the channel is subscribed") +} - err = ws.ResubscribeToChannel(&channel[0]) - if err != nil { - t.Fatal("error cannot be nil") - } +// TestSubscriptionState tests Subscription state changes +func TestSubscriptionState(t *testing.T) { + t.Parallel() + ws := New() + + c := &ChannelSubscription{Key: 42, Channel: "Gophers", State: ChannelSubscribing} + assert.ErrorIs(t, ws.SetSubscriptionState(c, ChannelUnsubscribing), ErrSubscriptionNotFound, "Setting an imaginary sub should error") + + assert.NoError(t, ws.AddSubscription(c), "Adding first subscription should not error") + found := ws.GetSubscription(42) + assert.NotNil(t, found, "Should find the subscription") + assert.Equal(t, ChannelSubscribing, found.State, "Subscription should be Subscribing") + assert.ErrorIs(t, ws.AddSubscription(c), ErrSubscribedAlready, "Adding an already existing sub should error") + assert.ErrorIs(t, ws.SetSubscriptionState(c, ChannelSubscribing), ErrChannelInStateAlready, "Setting Same state should error") + assert.ErrorIs(t, ws.SetSubscriptionState(c, ChannelUnsubscribing+1), errInvalidChannelState, "Setting an invalid state should error") + + ws.AddSuccessfulSubscriptions(*c) + found = ws.GetSubscription(42) + assert.NotNil(t, found, "Should find the subscription") + assert.Equal(t, found.State, ChannelSubscribed, "Subscription should be subscribed state") + + assert.NoError(t, ws.SetSubscriptionState(c, ChannelUnsubscribing), "Setting Unsub state should not error") + found = ws.GetSubscription(42) + assert.Equal(t, found.State, ChannelUnsubscribing, "Subscription should be unsubscribing state") +} + +// TestRemoveSubscriptions tests removing a subscription +func TestRemoveSubscriptions(t *testing.T) { + t.Parallel() + ws := New() + + c := &ChannelSubscription{Key: 42, Channel: "Unite!"} + assert.NoError(t, ws.AddSubscription(c), "Adding first subscription should not error") + assert.NotNil(t, ws.GetSubscription(42), "Added subscription should be findable") + + ws.RemoveSubscriptions(*c) + assert.Nil(t, ws.GetSubscription(42), "Remove should have removed the sub") } // TestConnectionMonitorNoConnection logic test @@ -618,18 +636,64 @@ func TestConnectionMonitorNoConnection(t *testing.T) { } } -// TestSliceCopyDoesntImpactBoth logic test +// TestGetSubscription logic test +func TestGetSubscription(t *testing.T) { + t.Parallel() + assert.Nil(t, (*Websocket).GetSubscription(nil, "imaginary"), "GetSubscription on a nil Websocket should return nil") + assert.Nil(t, (&Websocket{}).GetSubscription("empty"), "GetSubscription on a Websocket with no sub map should return nil") + w := Websocket{ + subscriptions: subscriptionMap{ + 42: { + Channel: "hello3", + }, + }, + } + assert.Nil(t, w.GetSubscription(43), "GetSubscription with an invalid key should return nil") + c := w.GetSubscription(42) + if assert.NotNil(t, c, "GetSubscription with an valid key should return a channel") { + assert.Equal(t, "hello3", c.Channel, "GetSubscription should return the correct channel details") + } +} + +// TestGetSubscriptions logic test func TestGetSubscriptions(t *testing.T) { t.Parallel() w := Websocket{ - subscriptions: []ChannelSubscription{ - { + subscriptions: subscriptionMap{ + 42: { Channel: "hello3", }, }, } - if !strings.EqualFold("hello3", w.GetSubscriptions()[0].Channel) { - t.Error("Subscriptions was not copied properly") + assert.Equal(t, "hello3", w.GetSubscriptions()[0].Channel, "GetSubscriptions should return the correct channel details") +} + +// TestEnsureKeyed logic test +func TestEnsureKeyed(t *testing.T) { + t.Parallel() + c := ChannelSubscription{ + Channel: "candles", + Asset: asset.Spot, + Currency: currency.NewPair(currency.BTC, currency.USDT), + } + k1, ok := c.EnsureKeyed().(DefaultChannelKey) + if assert.True(t, ok, "EnsureKeyed should return a DefaultChannelKey") { + assert.Exactly(t, k1, c.Key, "EnsureKeyed should set the same key") + assert.Equal(t, k1.Channel, c.Channel, "DefaultChannelKey channel should be correct") + assert.Equal(t, k1.Asset, c.Asset, "DefaultChannelKey asset should be correct") + assert.Equal(t, k1.Currency, c.Currency, "DefaultChannelKey currency should be correct") + } + type platypus string + c = ChannelSubscription{ + Key: platypus("Gerald"), + Channel: "orderbook", + Asset: asset.Margin, + Currency: currency.NewPair(currency.ETH, currency.USDC), + } + k2, ok := c.EnsureKeyed().(platypus) + if assert.True(t, ok, "EnsureKeyed should return a platypus") { + assert.Exactly(t, k2, c.Key, "EnsureKeyed should set the same key") + assert.EqualValues(t, "Gerald", k2, "key should have the correct value") } } @@ -1024,15 +1088,10 @@ func TestGetChannelDifference(t *testing.T) { }, } subs, unsubs := web.GetChannelDifference(newChans) - if len(subs) != 3 { - t.Fatal("error mismatch") - } - - if len(unsubs) != 0 { - t.Fatal("error mismatch") - } + assert.Len(t, subs, 3, "Should get the correct number of subs") + assert.Len(t, unsubs, 0, "Should get the correct number of unsubs") - web.subscriptions = subs + web.AddSuccessfulSubscriptions(subs...) flushedSubs := []ChannelSubscription{ { @@ -1041,12 +1100,8 @@ func TestGetChannelDifference(t *testing.T) { } subs, unsubs = web.GetChannelDifference(flushedSubs) - if len(subs) != 0 { - t.Fatal("error mismatch") - } - if len(unsubs) != 2 { - t.Fatal("error mismatch") - } + assert.Len(t, subs, 0, "Should get the correct number of subs") + assert.Len(t, unsubs, 2, "Should get the correct number of unsubs") flushedSubs = []ChannelSubscription{ { @@ -1058,11 +1113,13 @@ func TestGetChannelDifference(t *testing.T) { } subs, unsubs = web.GetChannelDifference(flushedSubs) - if len(subs) != 1 { - t.Fatal("error mismatch") + if assert.Len(t, subs, 1, "Should get the correct number of subs") { + assert.Equal(t, subs[0].Channel, "Test4", "Should subscribe to the right channel") } - if len(unsubs) != 2 { - t.Fatal("error mismatch") + if assert.Len(t, unsubs, 2, "Should get the correct number of unsubs") { + sort.Slice(unsubs, func(i, j int) bool { return unsubs[i].Channel <= unsubs[j].Channel }) + assert.Equal(t, unsubs[0].Channel, "Test1", "Should unsubscribe from the right channels") + assert.Equal(t, unsubs[1].Channel, "Test3", "Should unsubscribe from the right channels") } } @@ -1177,9 +1234,7 @@ func TestFlushChannels(t *testing.T) { if err != nil { t.Fatal(err) } - web.subscriptionMutex.Lock() - web.subscriptions = subs - web.subscriptionMutex.Unlock() + web.AddSuccessfulSubscriptions(subs...) err = web.FlushChannels() if err != nil { t.Fatal(err) @@ -1199,12 +1254,14 @@ func TestFlushChannels(t *testing.T) { t.Fatal(err) } web.subscriptionMutex.Lock() - web.subscriptions = []ChannelSubscription{ - { + web.subscriptions = subscriptionMap{ + 41: { + Key: 41, Channel: "match channel", Currency: currency.NewPair(currency.BTC, currency.AUD), }, - { + 42: { + Key: 42, Channel: "unsub channel", Currency: currency.NewPair(currency.THETA, currency.USDT), }, @@ -1416,10 +1473,10 @@ func TestCheckSubscriptions(t *testing.T) { ws.MaxSubscriptionsPerConnection = 2 - ws.subscriptions = []ChannelSubscription{{Channel: "test"}} - err = ws.checkSubscriptions([]ChannelSubscription{{Channel: "test"}}) - if !errors.Is(err, errChannelSubscriptionAlreadySubscribed) { - t.Fatalf("received: %v, but expected: %v", err, errChannelSubscriptionAlreadySubscribed) + ws.subscriptions = subscriptionMap{42: {Key: 42, Channel: "test"}} + err = ws.checkSubscriptions([]ChannelSubscription{{Key: 42, Channel: "test"}}) + if !errors.Is(err, errChannelAlreadySubscribed) { + t.Fatalf("received: %v, but expected: %v", err, errChannelAlreadySubscribed) } err = ws.checkSubscriptions([]ChannelSubscription{{}}) diff --git a/exchanges/stream/websocket_types.go b/exchanges/stream/websocket_types.go index 640fb74a5bb..c00298d8982 100644 --- a/exchanges/stream/websocket_types.go +++ b/exchanges/stream/websocket_types.go @@ -22,6 +22,8 @@ const ( UnhandledMessage = " - Unhandled websocket message: " ) +type subscriptionMap map[any]*ChannelSubscription + // Websocket defines a return type for websocket connections via the interface // wrapper for routine processing type Websocket struct { @@ -43,11 +45,11 @@ type Websocket struct { runningURLAuth string exchangeName string m sync.Mutex - connectionMutex sync.RWMutex + fieldMutex sync.RWMutex connector func() error - subscriptionMutex sync.Mutex - subscriptions []ChannelSubscription + subscriptionMutex sync.RWMutex + subscriptions subscriptionMap Subscribe chan []ChannelSubscription Unsubscribe chan []ChannelSubscription diff --git a/go.mod b/go.mod index 16011cbeac1..383113a35c7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/thrasher-corp/gocryptotrader go 1.20 require ( + github.com/buger/jsonparser v1.1.1 github.com/d5/tengo/v2 v2.16.1 github.com/gofrs/uuid v4.4.0+incompatible github.com/gorilla/mux v1.8.0 diff --git a/go.sum b/go.sum index bd7d17fe0b2..352fa627856 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= diff --git a/testdata/configtest.json b/testdata/configtest.json index 2bc79f4dd45..faf82a108f9 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -543,8 +543,8 @@ } }, "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, + "authenticatedSupport": true, + "authenticatedWebsocketApiSupport": true, "endpoints": { "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", @@ -571,7 +571,7 @@ }, "enabled": { "autoPairUpdates": true, - "websocketAPI": false + "websocketAPI": true } }, "bankAccounts": [ From 70690d9a047e26833f27d0596eedc8ce2755fb38 Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 3 Nov 2023 10:01:32 +1000 Subject: [PATCH 21/40] futures: Implement GetLatestFundingRates across exchanges (#1339) * adds funding rate implementations and improvements * merge fixes x1 * lint * kucoin funding rates func make * migrate sync-manager to keys * some kucoin work * adds some kucoin wrapper funcs * ehhh, todo * kucoin position * start of orders * adds the kucoin tests yay * multiplier * nits, EWS includes order limits * NotYetImplemented, IsPerp improvements, cleaning * lint, test fix, huobi time * fixes issues, improves testing * fixes linters I WRECKED * local lint but remote lint, lint, lint, lint * fixes err * skip CI * lint * Supported rates, binance endpoints * fixes weird mocktest problems * no, CZ is invalid * fixes some new EWS test errors --- backtester/data/kline/live/live_test.go | 4 +- cmd/exchange_template/wrapper_file.tmpl | 7 + cmd/exchange_wrapper_issues/main.go | 6 +- .../exchange_wrapper_standards_test.go | 49 +- common/key/key.go | 6 + currency/code_types.go | 3 + currency/manager.go | 2 + engine/engine.go | 4 +- engine/helpers.go | 2 +- engine/helpers_test.go | 40 +- engine/order_manager.go | 2 +- engine/rpcserver.go | 29 +- engine/rpcserver_test.go | 38 +- engine/sync_manager.go | 154 +- engine/sync_manager_test.go | 44 +- engine/sync_manager_types.go | 20 +- engine/websocketroutine_manager.go | 4 + engine/websocketroutine_manager_test.go | 14 +- engine/websocketroutine_manager_types.go | 1 + exchanges/alphapoint/alphapoint_wrapper.go | 6 + exchanges/binance/binance_cfutures.go | 8 + exchanges/binance/binance_test.go | 48 +- exchanges/binance/binance_ufutures.go | 7 + exchanges/binance/binance_wrapper.go | 302 ++- exchanges/binance/ufutures_types.go | 10 + exchanges/binanceus/binanceus_wrapper.go | 6 + exchanges/bitfinex/bitfinex_wrapper.go | 24 +- exchanges/bitflyer/bitflyer_wrapper.go | 6 + exchanges/bithumb/bithumb_wrapper.go | 6 + exchanges/bitmex/bitmex.go | 20 +- exchanges/bitmex/bitmex_test.go | 59 + exchanges/bitmex/bitmex_wrapper.go | 101 +- exchanges/bitstamp/bitstamp_wrapper.go | 6 + exchanges/bittrex/bittrex_wrapper.go | 6 + exchanges/btcmarkets/btcmarkets_wrapper.go | 6 + exchanges/btse/btse_test.go | 58 + exchanges/btse/btse_wrapper.go | 72 + exchanges/bybit/bybit_test.go | 19 + exchanges/bybit/bybit_wrapper.go | 12 + exchanges/coinbasepro/coinbasepro_wrapper.go | 6 + exchanges/coinut/coinut_wrapper.go | 6 + exchanges/exchange.go | 14 +- exchanges/exchange_test.go | 14 +- exchanges/exchange_types.go | 17 +- exchanges/exmo/exmo_wrapper.go | 9 + exchanges/fundingrate/fundingrate_types.go | 9 +- exchanges/futures/contract.go | 3 + exchanges/futures/futures.go | 10 +- exchanges/futures/futures_test.go | 10 +- exchanges/futures/futures_types.go | 11 +- exchanges/gateio/gateio_test.go | 38 +- exchanges/gateio/gateio_wrapper.go | 105 + exchanges/gemini/gemini_wrapper.go | 6 + exchanges/hitbtc/hitbtc_wrapper.go | 6 + exchanges/huobi/cfutures_types.go | 10 +- exchanges/huobi/huobi_cfutures.go | 20 +- exchanges/huobi/huobi_test.go | 63 +- exchanges/huobi/huobi_wrapper.go | 103 + exchanges/interfaces.go | 5 +- exchanges/itbit/itbit_wrapper.go | 6 + exchanges/kraken/kraken_test.go | 62 + exchanges/kraken/kraken_wrapper.go | 101 +- exchanges/kucoin/kucoin_futures_types.go | 2 +- exchanges/kucoin/kucoin_test.go | 144 +- exchanges/kucoin/kucoin_wrapper.go | 400 +++- exchanges/lbank/lbank_wrapper.go | 6 + exchanges/okcoin/okcoin_wrapper.go | 6 + exchanges/okx/okx_test.go | 12 +- exchanges/okx/okx_wrapper.go | 58 +- exchanges/order/limits.go | 32 +- exchanges/order/limits_test.go | 16 +- exchanges/order/order_test.go | 8 +- exchanges/poloniex/poloniex_wrapper.go | 10 +- exchanges/protocol/features.go | 60 +- exchanges/sharedtestvalues/customex.go | 11 + exchanges/yobit/yobit_wrapper.go | 6 + exchanges/zb/zb_wrapper.go | 6 + testdata/http_mock/binance/binance.json | 1985 ++++++++++++++++- 78 files changed, 4084 insertions(+), 523 deletions(-) diff --git a/backtester/data/kline/live/live_test.go b/backtester/data/kline/live/live_test.go index 089200877fc..428e4fa5d43 100644 --- a/backtester/data/kline/live/live_test.go +++ b/backtester/data/kline/live/live_test.go @@ -73,9 +73,7 @@ func TestLoadTrades(t *testing.T) { ConfigFormat: pFormat, } var data *gctkline.Item - // start is 10 mins in the past to ensure there are some trades to pull from the exchange - start := time.Now().Add(-time.Minute * 10) - data, err = LoadData(context.Background(), start, exch, common.DataTrade, interval.Duration(), cp, currency.EMPTYPAIR, a, true) + data, err = LoadData(context.Background(), time.Now().Add(-interval.Duration()*60), exch, common.DataTrade, interval.Duration(), cp, currency.EMPTYPAIR, a, true) if err != nil { t.Fatal(err) } diff --git a/cmd/exchange_template/wrapper_file.tmpl b/cmd/exchange_template/wrapper_file.tmpl index a29585ab842..20cd464cf7b 100644 --- a/cmd/exchange_template/wrapper_file.tmpl +++ b/cmd/exchange_template/wrapper_file.tmpl @@ -14,6 +14,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -547,4 +548,10 @@ func ({{.Variable}} *{{.CapitalName}}) GetFuturesContractDetails(context.Context return nil, common.ErrNotYetImplemented } +// GetLatestFundingRates returns the latest funding rates data +func ({{.Variable}} *{{.CapitalName}}) GetLatestFundingRates(_ context.Context, _ *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrNotYetImplemented +} + + {{end}} diff --git a/cmd/exchange_wrapper_issues/main.go b/cmd/exchange_wrapper_issues/main.go index dc6bbdb3fca..c7fefc7cba6 100644 --- a/cmd/exchange_wrapper_issues/main.go +++ b/cmd/exchange_wrapper_issues/main.go @@ -515,14 +515,14 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) Response: jsonifyInterface([]interface{}{""}), }) - fundingRateRequest := &fundingrate.RatesRequest{ + fundingRateRequest := &fundingrate.HistoricalRatesRequest{ Asset: assetTypes[i], Pair: p, StartDate: time.Now().Add(-time.Hour), EndDate: time.Now(), } - var fundingRateResponse *fundingrate.Rates - fundingRateResponse, err = e.GetFundingRates(context.TODO(), fundingRateRequest) + var fundingRateResponse *fundingrate.HistoricalRates + fundingRateResponse, err = e.GetHistoricalFundingRates(context.TODO(), fundingRateRequest) msg = "" if err != nil { msg = err.Error() diff --git a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go index 995c40ecce4..d60abaa711e 100644 --- a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go +++ b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/margin" @@ -110,6 +111,7 @@ func setupExchange(ctx context.Context, t *testing.T, name string, cfg *config.C // Add +1 to len to verify that exchanges can handle requests with unset pairs and assets assetPairs := make([]assetPair, 0, len(assets)+1) +assets: for j := range assets { var pairs currency.Pairs pairs, err = b.CurrencyPairs.GetPairs(assets[j], false) @@ -132,6 +134,12 @@ func setupExchange(ctx context.Context, t *testing.T, name string, cfg *config.C if err != nil { t.Fatalf("Cannot setup %v asset %v FormatExchangeCurrency %v", name, assets[j], err) } + for x := range unsupportedAssets { + if assets[j] == unsupportedAssets[x] { + // this asset cannot handle disrupt formatting + continue assets + } + } p, err = disruptFormatting(t, p) if err != nil { t.Fatalf("Cannot setup %v asset %v disruptFormatting %v", name, assets[j], err) @@ -281,6 +289,7 @@ var ( positionChangeRequestParam = reflect.TypeOf((**margin.PositionChangeRequest)(nil)).Elem() positionSummaryRequestParam = reflect.TypeOf((**futures.PositionSummaryRequest)(nil)).Elem() positionsRequestParam = reflect.TypeOf((**futures.PositionsRequest)(nil)).Elem() + latestRateRequest = reflect.TypeOf((**fundingrate.LatestRateRequest)(nil)).Elem() ) // generateMethodArg determines the argument type and returns a pre-made @@ -322,8 +331,8 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr case argGenerator.MethodInputType.AssignableTo(feeBuilderParam): input = reflect.ValueOf(&exchange.FeeBuilder{ FeeType: exchange.OfflineTradeFee, - Amount: 1337, - PurchasePrice: 1337, + Amount: 150, + PurchasePrice: 150, Pair: argGenerator.AssetParams.Pair, }) case argGenerator.MethodInputType.AssignableTo(currencyPairParam): @@ -425,7 +434,7 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr Side: order.Buy, Pair: argGenerator.AssetParams.Pair, AssetType: argGenerator.AssetParams.Asset, - Price: 1337, + Price: 150, Amount: 1, ClientID: "1337", ClientOrderID: "13371337", @@ -438,7 +447,7 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr Side: order.Buy, Pair: argGenerator.AssetParams.Pair, AssetType: argGenerator.AssetParams.Asset, - Price: 1337, + Price: 150, Amount: 1, ClientOrderID: "13371337", OrderID: "1337", @@ -482,8 +491,8 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr Pair: argGenerator.AssetParams.Pair, Asset: argGenerator.AssetParams.Asset, MarginType: margin.Isolated, - OriginalAllocatedMargin: 1337, - NewAllocatedMargin: 1338, + OriginalAllocatedMargin: 150, + NewAllocatedMargin: 151, }) case argGenerator.MethodInputType.AssignableTo(positionSummaryRequestParam): input = reflect.ValueOf(&futures.PositionSummaryRequest{ @@ -502,9 +511,15 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr case argGenerator.MethodInputType.AssignableTo(orderSideParam): input = reflect.ValueOf(order.Long) case argGenerator.MethodInputType.AssignableTo(int64Param): - input = reflect.ValueOf(1337) + input = reflect.ValueOf(150) case argGenerator.MethodInputType.AssignableTo(float64Param): - input = reflect.ValueOf(13.37) + input = reflect.ValueOf(150.0) + case argGenerator.MethodInputType.AssignableTo(latestRateRequest): + input = reflect.ValueOf(&fundingrate.LatestRateRequest{ + Asset: argGenerator.AssetParams.Asset, + Pair: argGenerator.AssetParams.Pair, + IncludePredictedRate: true, + }) default: input = reflect.Zero(argGenerator.MethodInputType) } @@ -533,10 +548,7 @@ var excludedMethodNames = map[string]struct{}{ "FlushWebsocketChannels": {}, // Unnecessary websocket test "UnsubscribeToWebsocketChannels": {}, // Unnecessary websocket test "SubscribeToWebsocketChannels": {}, // Unnecessary websocket test - "GetOrderExecutionLimits": {}, // Not widely supported/implemented feature "UpdateCurrencyStates": {}, // Not widely supported/implemented feature - "UpdateOrderExecutionLimits": {}, // Not widely supported/implemented feature - "CheckOrderExecutionLimits": {}, // Not widely supported/implemented feature "CanTradePair": {}, // Not widely supported/implemented feature "CanTrade": {}, // Not widely supported/implemented feature "CanWithdraw": {}, // Not widely supported/implemented feature @@ -548,7 +560,7 @@ var excludedMethodNames = map[string]struct{}{ "GetCollateralCurrencyForContract": {}, "GetCurrencyForRealisedPNL": {}, "GetFuturesPositions": {}, - "GetFundingRates": {}, + "GetHistoricalFundingRates": {}, "IsPerpetualFutureCurrency": {}, "GetMarginRatesHistory": {}, "CalculatePNL": {}, @@ -563,7 +575,6 @@ var excludedMethodNames = map[string]struct{}{ "GetLeverage": {}, "SetMarginType": {}, "ChangePositionMargin": {}, - "GetLatestFundingRate": {}, } // blockedCIExchanges are exchanges that are not able to be tested on CI @@ -572,6 +583,12 @@ var blockedCIExchanges = []string{ "bybit", // bybit API is banned from executing within the US where github Actions is ran } +// unsupportedAssets contains assets that cannot handle +// normal processing for testing. This is to be used very sparingly +var unsupportedAssets = []asset.Item{ + asset.Index, +} + var unsupportedExchangeNames = []string{ "testexch", "alphapoint", @@ -592,6 +609,7 @@ var cryptoChainPerExchange = map[string]string{ // acceptable errors do not throw test errors, see below for why var acceptableErrors = []error{ common.ErrFunctionNotSupported, // Shows API cannot perform function and developer has recognised this + common.ErrNotYetImplemented, // Shows API can perform function but developer has not implemented it yet asset.ErrNotSupported, // Shows that valid and invalid asset types are handled request.ErrAuthRequestFailed, // We must set authenticated requests properly in order to understand and better handle auth failures order.ErrUnsupportedOrderType, // Should be returned if an ordertype like ANY is requested and the implementation knows to throw this specific error @@ -605,6 +623,11 @@ var acceptableErrors = []error{ deposit.ErrAddressNotFound, // Is thrown when an address is not found due to the exchange requiring valid API keys futures.ErrNotFuturesAsset, // Is thrown when a futures function receives a non-futures asset currency.ErrSymbolStringEmpty, // Is thrown when a symbol string is empty for blank MatchSymbol func checks + futures.ErrNotPerpetualFuture, // Is thrown when a futures function receives a non-perpetual future + order.ErrExchangeLimitNotLoaded, // Is thrown when the limits aren't loaded for a particular exchange, asset, pair + order.ErrCannotValidateAsset, // Is thrown when attempting to get order limits from an asset that is not yet loaded + order.ErrCannotValidateBaseCurrency, // Is thrown when attempting to get order limits from an base currency that is not yet loaded + order.ErrCannotValidateQuoteCurrency, // Is thrown when attempting to get order limits from an quote currency that is not yet loaded } // warningErrors will t.Log(err) when thrown to diagnose things, but not necessarily suggest diff --git a/common/key/key.go b/common/key/key.go index 96d2210b597..921fe59a8f1 100644 --- a/common/key/key.go +++ b/common/key/key.go @@ -15,6 +15,12 @@ type ExchangePairAsset struct { Asset asset.Item } +// ExchangeAsset is a unique map key signature for exchange and asset +type ExchangeAsset struct { + Exchange string + Asset asset.Item +} + // PairAsset is a unique map key signature for currency pair and asset type PairAsset struct { Base *currency.Item diff --git a/currency/code_types.go b/currency/code_types.go index 7aec9871539..db3d167996b 100644 --- a/currency/code_types.go +++ b/currency/code_types.go @@ -230,6 +230,7 @@ var ( SSC = NewCode("SSC") SHOW = NewCode("SHOW") SPF = NewCode("SPF") + PF = NewCode("PF") SNC = NewCode("SNC") SWFTC = NewCode("SWFTC") TRA = NewCode("TRA") @@ -3018,6 +3019,8 @@ var ( SWAP = NewCode("SWAP") PI = NewCode("PI") FI = NewCode("FI") + USDM = NewCode("USDM") + USDTM = NewCode("USDTM") stables = Currencies{ USDT, diff --git a/currency/manager.go b/currency/manager.go index 22e68bd3f76..00a45835835 100644 --- a/currency/manager.go +++ b/currency/manager.go @@ -17,6 +17,8 @@ var ( ErrAssetAlreadyEnabled = errors.New("asset already enabled") // ErrPairAlreadyEnabled returns when enabling a pair that is already enabled ErrPairAlreadyEnabled = errors.New("pair already enabled") + // ErrPairNotEnabled returns when looking for a pair that is not enabled + ErrPairNotEnabled = errors.New("pair not enabled") // ErrPairNotFound is returned when a currency pair is not found ErrPairNotFound = errors.New("pair not found") // ErrAssetIsNil is an error when the asset has not been populated by the diff --git a/engine/engine.go b/engine/engine.go index fcc221c2253..d279afa8088 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -36,7 +36,7 @@ type Engine struct { apiServer *apiServerManager CommunicationsManager *CommunicationManager connectionManager *connectionManager - currencyPairSyncer *syncManager + currencyPairSyncer *SyncManager DatabaseManager *DatabaseConnectionManager DepositAddressManager *DepositAddressManager eventManager *eventManager @@ -513,7 +513,7 @@ func (bot *Engine) Start() error { bot.Settings.SyncWorkersCount != config.DefaultSyncerWorkers { cfg.NumWorkers = bot.Settings.SyncWorkersCount } - if s, err := setupSyncManager( + if s, err := SetupSyncManager( &cfg, bot.ExchangeManager, &bot.Config.RemoteControl, diff --git a/engine/helpers.go b/engine/helpers.go index 6634b83dc67..4e915e37d66 100644 --- a/engine/helpers.go +++ b/engine/helpers.go @@ -225,7 +225,7 @@ func (bot *Engine) SetSubsystem(subSystemName string, enable bool) error { bot.Settings.SyncWorkersCount != config.DefaultSyncerWorkers { cfg.NumWorkers = bot.Settings.SyncWorkersCount } - bot.currencyPairSyncer, err = setupSyncManager( + bot.currencyPairSyncer, err = SetupSyncManager( &cfg, bot.ExchangeManager, &bot.Config.RemoteControl, diff --git a/engine/helpers_test.go b/engine/helpers_test.go index 590214bc13c..56c3dd5798e 100644 --- a/engine/helpers_test.go +++ b/engine/helpers_test.go @@ -9,13 +9,11 @@ import ( "crypto/x509/pkix" "encoding/pem" "errors" - "fmt" "math/big" "net" "os" "path/filepath" "strings" - "sync" "testing" "time" @@ -1378,33 +1376,23 @@ func TestNewExchangeByNameWithDefaults(t *testing.T) { if !errors.Is(err, ErrExchangeNotFound) { t.Fatalf("received: '%v' but expected: '%v'", err, ErrExchangeNotFound) } - - ch := make(chan error, len(exchange.Exchanges)) - wg := sync.WaitGroup{} for x := range exchange.Exchanges { - wg.Add(1) - go func(x int) { - defer wg.Done() - exch, err := NewExchangeByNameWithDefaults(context.Background(), exchange.Exchanges[x]) + name := exchange.Exchanges[x] + t.Run(name, func(t *testing.T) { + t.Parallel() + if isCITest() && common.StringDataContains(blockedCIExchanges, name) { + t.Skipf("skipping %s due to CI test restrictions", name) + } + if common.StringDataContains(unsupportedDefaultConfigExchanges, name) { + t.Skipf("skipping %s unsupported", name) + } + exch, err := NewExchangeByNameWithDefaults(context.Background(), name) if err != nil { - ch <- err - return + t.Error(err) } - - if !strings.EqualFold(exch.GetName(), exchange.Exchanges[x]) { - ch <- fmt.Errorf("received: '%v' but expected: '%v'", exch.GetName(), exchange.Exchanges[x]) + if !strings.EqualFold(exch.GetName(), name) { + t.Errorf("received: '%v' but expected: '%v'", exch.GetName(), name) } - }(x) - } - wg.Wait() - -outta: - for { - select { - case err := <-ch: - t.Error(err) - default: - break outta - } + }) } } diff --git a/engine/order_manager.go b/engine/order_manager.go index 8224a3d3178..32ed6f15fb5 100644 --- a/engine/order_manager.go +++ b/engine/order_manager.go @@ -807,7 +807,7 @@ func (m *OrderManager) processFuturesPositions(exch exchange.IBotExchange, posit if !isPerp { return nil } - frp, err := exch.GetFundingRates(context.TODO(), &fundingrate.RatesRequest{ + frp, err := exch.GetHistoricalFundingRates(context.TODO(), &fundingrate.HistoricalRatesRequest{ Asset: position.Asset, Pair: position.Pair, StartDate: position.Orders[0].Date, diff --git a/engine/rpcserver.go b/engine/rpcserver.go index d7f27c68ff4..f2824c9eac2 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -4463,8 +4463,8 @@ func (s *RPCServer) GetFuturesPositionsSummary(ctx context.Context, r *gctrpc.Ge if !stats.AverageOpenPrice.IsZero() { positionStats.AverageOpenPrice = stats.AverageOpenPrice.String() } - if !stats.PositionPNL.IsZero() { - positionStats.RecentPnl = stats.PositionPNL.String() + if !stats.UnrealisedPNL.IsZero() { + positionStats.RecentPnl = stats.UnrealisedPNL.String() } if !stats.MaintenanceMarginFraction.IsZero() { positionStats.MarginFraction = stats.MaintenanceMarginFraction.String() @@ -4701,7 +4701,7 @@ func (s *RPCServer) GetFundingRates(ctx context.Context, r *gctrpc.GetFundingRat return nil, fmt.Errorf("%w %v", errPairNotEnabled, cp) } - funding, err := exch.GetFundingRates(ctx, &fundingrate.RatesRequest{ + funding, err := exch.GetHistoricalFundingRates(ctx, &fundingrate.HistoricalRatesRequest{ Asset: a, Pair: cp, StartDate: start, @@ -4799,7 +4799,7 @@ func (s *RPCServer) GetLatestFundingRate(ctx context.Context, r *gctrpc.GetLates return nil, fmt.Errorf("%w %v", errPairNotEnabled, cp) } - funding, err := exch.GetLatestFundingRate(ctx, &fundingrate.LatestRateRequest{ + fundingRates, err := exch.GetLatestFundingRates(ctx, &fundingrate.LatestRateRequest{ Asset: a, Pair: cp, IncludePredictedRate: r.IncludePredicted, @@ -4807,27 +4807,30 @@ func (s *RPCServer) GetLatestFundingRate(ctx context.Context, r *gctrpc.GetLates if err != nil { return nil, err } + if len(fundingRates) != 1 { + return nil, fmt.Errorf("expected 1 funding rate, received %v", len(fundingRates)) + } var response gctrpc.GetLatestFundingRateResponse fundingData := &gctrpc.FundingData{ Exchange: r.Exchange, Asset: r.Asset, Pair: &gctrpc.CurrencyPair{ - Delimiter: funding.Pair.Delimiter, - Base: funding.Pair.Base.String(), - Quote: funding.Pair.Quote.String(), + Delimiter: fundingRates[0].Pair.Delimiter, + Base: fundingRates[0].Pair.Base.String(), + Quote: fundingRates[0].Pair.Quote.String(), }, LatestRate: &gctrpc.FundingRate{ - Date: funding.LatestRate.Time.Format(common.SimpleTimeFormatWithTimezone), - Rate: funding.LatestRate.Rate.String(), + Date: fundingRates[0].LatestRate.Time.Format(common.SimpleTimeFormatWithTimezone), + Rate: fundingRates[0].LatestRate.Rate.String(), }, } - if !funding.TimeOfNextRate.IsZero() { - fundingData.TimeOfNextRate = funding.TimeOfNextRate.Format(common.SimpleTimeFormatWithTimezone) + if !fundingRates[0].TimeOfNextRate.IsZero() { + fundingData.TimeOfNextRate = fundingRates[0].TimeOfNextRate.Format(common.SimpleTimeFormatWithTimezone) } if r.IncludePredicted { fundingData.UpcomingRate = &gctrpc.FundingRate{ - Date: funding.PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone), - Rate: funding.PredictedUpcomingRate.Rate.String(), + Date: fundingRates[0].PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone), + Rate: fundingRates[0].PredictedUpcomingRate.Rate.String(), } } response.Rate = fundingData diff --git a/engine/rpcserver_test.go b/engine/rpcserver_test.go index 522c66eee2f..3b2dd8ce263 100644 --- a/engine/rpcserver_test.go +++ b/engine/rpcserver_test.go @@ -70,7 +70,7 @@ func (f fExchange) GetFuturesPositionSummary(context.Context, *futures.PositionS MarkPrice: leet, CurrentSize: leet, AverageOpenPrice: leet, - PositionPNL: leet, + UnrealisedPNL: leet, MaintenanceMarginFraction: leet, FreeCollateral: leet, TotalCollateral: leet, @@ -141,29 +141,31 @@ func (f fExchange) GetFuturesPositionOrders(_ context.Context, req *futures.Posi return resp, nil } -func (f fExchange) GetLatestFundingRate(_ context.Context, request *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) { +func (f fExchange) GetLatestFundingRates(_ context.Context, request *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { leet := decimal.NewFromInt(1337) - return &fundingrate.LatestRateResponse{ - Exchange: f.GetName(), - Asset: request.Asset, - Pair: request.Pair, - LatestRate: fundingrate.Rate{ - Time: time.Now(), - Rate: leet, - Payment: leet, - }, - PredictedUpcomingRate: fundingrate.Rate{ - Time: time.Now(), - Rate: leet, - Payment: leet, + return []fundingrate.LatestRateResponse{ + { + Exchange: f.GetName(), + Asset: request.Asset, + Pair: request.Pair, + LatestRate: fundingrate.Rate{ + Time: time.Now(), + Rate: leet, + Payment: leet, + }, + PredictedUpcomingRate: fundingrate.Rate{ + Time: time.Now(), + Rate: leet, + Payment: leet, + }, + TimeOfNextRate: time.Now(), }, - TimeOfNextRate: time.Now(), }, nil } -func (f fExchange) GetFundingRates(_ context.Context, request *fundingrate.RatesRequest) (*fundingrate.Rates, error) { +func (f fExchange) GetHistoricalFundingRates(_ context.Context, request *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { leet := decimal.NewFromInt(1337) - return &fundingrate.Rates{ + return &fundingrate.HistoricalRates{ Exchange: f.GetName(), Asset: request.Asset, Pair: request.Pair, diff --git a/engine/sync_manager.go b/engine/sync_manager.go index 66dd63e1510..52dd075e6d7 100644 --- a/engine/sync_manager.go +++ b/engine/sync_manager.go @@ -11,6 +11,7 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -39,8 +40,8 @@ var ( errCouldNotSyncNewData = errors.New("could not sync new data") ) -// setupSyncManager starts a new CurrencyPairSyncer -func setupSyncManager(c *config.SyncManagerConfig, exchangeManager iExchangeManager, remoteConfig *config.RemoteControlConfig, websocketRoutineManagerEnabled bool) (*syncManager, error) { +// SetupSyncManager creates a new CurrencyPairSyncer +func SetupSyncManager(c *config.SyncManagerConfig, exchangeManager iExchangeManager, remoteConfig *config.RemoteControlConfig, websocketRoutineManagerEnabled bool) (*SyncManager, error) { if c == nil { return nil, fmt.Errorf("%T %w", c, common.ErrNilPointer) } @@ -79,15 +80,15 @@ func setupSyncManager(c *config.SyncManagerConfig, exchangeManager iExchangeMana return nil, fmt.Errorf("%T %w", c.PairFormatDisplay, common.ErrNilPointer) } - s := &syncManager{ + s := &SyncManager{ config: *c, remoteConfig: remoteConfig, exchangeManager: exchangeManager, websocketRoutineManagerEnabled: websocketRoutineManagerEnabled, fiatDisplayCurrency: c.FiatDisplayCurrency, format: *c.PairFormatDisplay, - tickerBatchLastRequested: make(map[string]time.Time), - currencyPairs: make(map[currencyPairKey]*currencyPairSyncAgent), + tickerBatchLastRequested: make(map[key.ExchangeAsset]time.Time), + currencyPairs: make(map[key.ExchangePairAsset]*currencyPairSyncAgent), } log.Debugf(log.SyncMgr, @@ -102,12 +103,12 @@ func setupSyncManager(c *config.SyncManagerConfig, exchangeManager iExchangeMana } // IsRunning safely checks whether the subsystem is running -func (m *syncManager) IsRunning() bool { +func (m *SyncManager) IsRunning() bool { return m != nil && atomic.LoadInt32(&m.started) == 1 } // Start runs the subsystem -func (m *syncManager) Start() error { +func (m *SyncManager) Start() error { if m == nil { return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem) } @@ -175,10 +176,11 @@ func (m *syncManager) Start() error { continue } for i := range enabledPairs { - k := currencyPairKey{ - AssetType: assetTypes[y], - Exchange: exchangeName, - Pair: enabledPairs[i].Format(currency.PairFormat{Uppercase: true}), + k := key.ExchangePairAsset{ + Asset: assetTypes[y], + Exchange: exchangeName, + Base: enabledPairs[i].Base.Item, + Quote: enabledPairs[i].Quote.Item, } if e := m.get(k); e != nil { continue @@ -235,7 +237,7 @@ func (m *syncManager) Start() error { } // Stop shuts down the exchange currency pair syncer -func (m *syncManager) Stop() error { +func (m *SyncManager) Stop() error { if m == nil { return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem) } @@ -248,22 +250,23 @@ func (m *syncManager) Stop() error { return nil } -func (m *syncManager) get(k currencyPairKey) *currencyPairSyncAgent { +func (m *SyncManager) get(k key.ExchangePairAsset) *currencyPairSyncAgent { m.mux.Lock() defer m.mux.Unlock() return m.currencyPairs[k] } -func newCurrencyPairSyncAgent(k currencyPairKey) *currencyPairSyncAgent { +func newCurrencyPairSyncAgent(k key.ExchangePairAsset) *currencyPairSyncAgent { return ¤cyPairSyncAgent{ - currencyPairKey: k, - Created: time.Now(), - locks: make([]sync.Mutex, SyncItemTrade+1), - trackers: make([]*syncBase, SyncItemTrade+1), + Key: k, + Pair: currency.NewPair(k.Base.Currency(), k.Quote.Currency()), + Created: time.Now(), + locks: make([]sync.Mutex, SyncItemTrade+1), + trackers: make([]*syncBase, SyncItemTrade+1), } } -func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent { +func (m *SyncManager) add(k key.ExchangePairAsset, s syncBase) *currencyPairSyncAgent { m.mux.Lock() defer m.mux.Unlock() @@ -288,7 +291,9 @@ func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent if m.config.Verbose { log.Debugf(log.SyncMgr, "%s: Added ticker sync item %v: using websocket: %v using REST: %v", - c.Exchange, m.FormatCurrency(c.Pair).String(), c.trackers[SyncItemTicker].IsUsingWebsocket, + c.Key.Exchange, + m.FormatCurrency(c.Pair), + c.trackers[SyncItemTicker].IsUsingWebsocket, c.trackers[SyncItemTicker].IsUsingREST) } if atomic.LoadInt32(&m.initSyncCompleted) != 1 { @@ -301,7 +306,9 @@ func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent if m.config.Verbose { log.Debugf(log.SyncMgr, "%s: Added orderbook sync item %v: using websocket: %v using REST: %v", - c.Exchange, m.FormatCurrency(c.Pair).String(), c.trackers[SyncItemOrderbook].IsUsingWebsocket, + k.Exchange, + m.FormatCurrency(c.Pair), + c.trackers[SyncItemOrderbook].IsUsingWebsocket, c.trackers[SyncItemOrderbook].IsUsingREST) } if atomic.LoadInt32(&m.initSyncCompleted) != 1 { @@ -314,7 +321,9 @@ func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent if m.config.Verbose { log.Debugf(log.SyncMgr, "%s: Added trade sync item %v: using websocket: %v using REST: %v", - c.Exchange, m.FormatCurrency(c.Pair).String(), c.trackers[SyncItemTrade].IsUsingWebsocket, + k.Exchange, + m.FormatCurrency(c.Pair), + c.trackers[SyncItemTrade].IsUsingWebsocket, c.trackers[SyncItemTrade].IsUsingREST) } if atomic.LoadInt32(&m.initSyncCompleted) != 1 { @@ -324,7 +333,7 @@ func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent } if m.currencyPairs == nil { - m.currencyPairs = make(map[currencyPairKey]*currencyPairSyncAgent) + m.currencyPairs = make(map[key.ExchangePairAsset]*currencyPairSyncAgent) } m.currencyPairs[k] = c @@ -332,9 +341,9 @@ func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent return c } -// WebsocketUpdate notifies the syncManager to change the last updated time for a exchange asset pair +// WebsocketUpdate notifies the SyncManager to change the last updated time for a exchange asset pair // And set IsUsingWebsocket to true. It should be used externally only from websocket updaters -func (m *syncManager) WebsocketUpdate(exchangeName string, p currency.Pair, a asset.Item, syncType syncItemType, err error) error { +func (m *SyncManager) WebsocketUpdate(exchangeName string, p currency.Pair, a asset.Item, syncType syncItemType, err error) error { if m == nil { return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem) } @@ -362,15 +371,22 @@ func (m *syncManager) WebsocketUpdate(exchangeName string, p currency.Pair, a as return fmt.Errorf("%v %w", syncType, errUnknownSyncItem) } - k := currencyPairKey{ - AssetType: a, - Exchange: exchangeName, - Pair: p.Format(currency.PairFormat{Uppercase: true}), + k := key.ExchangePairAsset{ + Asset: a, + Exchange: exchangeName, + Base: p.Base.Item, + Quote: p.Quote.Item, } c, exists := m.currencyPairs[k] if !exists { - return fmt.Errorf("%w for %s %s %s %s", errCouldNotSyncNewData, k.Exchange, k.Pair, k.AssetType, syncType) + return fmt.Errorf("%w for %s %s %s %s %s", + errCouldNotSyncNewData, + k.Exchange, + k.Base, + k.Quote, + k.Asset, + syncType) } c.locks[syncType].Lock() @@ -387,9 +403,9 @@ func (m *syncManager) WebsocketUpdate(exchangeName string, p currency.Pair, a as if m.config.LogSwitchProtocolEvents { log.Warnf(log.SyncMgr, "%s %s %s: %s Websocket re-enabled, switching from rest to websocket", - c.Exchange, + k.Exchange, m.FormatCurrency(c.Pair), - strings.ToUpper(c.AssetType.String()), + strings.ToUpper(k.Asset.String()), syncType, ) } @@ -398,8 +414,8 @@ func (m *syncManager) WebsocketUpdate(exchangeName string, p currency.Pair, a as return m.update(c, syncType, err) } -// update notifies the syncManager to change the last updated time for a exchange asset pair -func (m *syncManager) update(c *currencyPairSyncAgent, syncType syncItemType, err error) error { +// update notifies the SyncManager to change the last updated time for a exchange asset pair +func (m *SyncManager) update(c *currencyPairSyncAgent, syncType syncItemType, err error) error { if syncType < SyncItemTicker || syncType > SyncItemTrade { return fmt.Errorf("%v %w", syncType, errUnknownSyncItem) } @@ -416,7 +432,7 @@ func (m *syncManager) update(c *currencyPairSyncAgent, syncType syncItemType, er removedCounter++ if m.config.LogInitialSyncEvents { log.Debugf(log.SyncMgr, "%s %s sync complete %v [%d/%d].", - c.Exchange, + c.Key.Exchange, syncType, m.FormatCurrency(c.Pair), removedCounter, @@ -428,7 +444,7 @@ func (m *syncManager) update(c *currencyPairSyncAgent, syncType syncItemType, er return nil } -func (m *syncManager) worker() { +func (m *SyncManager) worker() { cleanup := func() { log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer worker shutting down.") @@ -492,10 +508,11 @@ func (m *syncManager) worker() { return } - k := currencyPairKey{ - AssetType: assetTypes[y], - Exchange: exchangeName, - Pair: enabledPairs[i].Format(currency.PairFormat{Uppercase: true}), + k := key.ExchangePairAsset{ + Asset: assetTypes[y], + Exchange: exchangeName, + Base: enabledPairs[i].Base.Item, + Quote: enabledPairs[i].Quote.Item, } c := m.get(k) if c == nil { @@ -521,7 +538,7 @@ func (m *syncManager) worker() { } } -func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchange) { +func (m *SyncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchange) { if !c.locks[SyncItemTicker].TryLock() { return } @@ -541,9 +558,9 @@ func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchan if m.config.LogSwitchProtocolEvents { log.Warnf(log.SyncMgr, "%s %s %s: No ticker update after %s, switching from websocket to rest", - c.Exchange, + c.Key.Exchange, m.FormatCurrency(c.Pair), - strings.ToUpper(c.AssetType.String()), + strings.ToUpper(c.Key.Asset.String()), m.config.TimeoutWebsocket, ) } @@ -555,9 +572,15 @@ func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchan if e.SupportsRESTTickerBatchUpdates() { m.mux.Lock() - batchLastDone, ok := m.tickerBatchLastRequested[e.GetName()] + batchLastDone, ok := m.tickerBatchLastRequested[key.ExchangeAsset{ + Exchange: c.Key.Exchange, + Asset: c.Key.Asset, + }] if !ok { - m.tickerBatchLastRequested[exchangeName] = time.Time{} + m.tickerBatchLastRequested[key.ExchangeAsset{ + Exchange: c.Key.Exchange, + Asset: c.Key.Asset, + }] = time.Time{} } m.mux.Unlock() @@ -566,11 +589,14 @@ func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchan if m.config.Verbose { log.Debugf(log.SyncMgr, "Initialising %s REST ticker batching", exchangeName) } - err = e.UpdateTickers(context.TODO(), c.AssetType) + err = e.UpdateTickers(context.TODO(), c.Key.Asset) if err == nil { - result, err = e.FetchTicker(context.TODO(), c.Pair, c.AssetType) + result, err = e.FetchTicker(context.TODO(), c.Pair, c.Key.Asset) } - m.tickerBatchLastRequested[exchangeName] = time.Now() + m.tickerBatchLastRequested[key.ExchangeAsset{ + Exchange: c.Key.Exchange, + Asset: c.Key.Asset, + }] = time.Now() m.mux.Unlock() } else { if m.config.Verbose { @@ -578,17 +604,17 @@ func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchan } result, err = e.FetchTicker(context.TODO(), c.Pair, - c.AssetType) + c.Key.Asset) } } else { result, err = e.UpdateTicker(context.TODO(), c.Pair, - c.AssetType) + c.Key.Asset) } m.PrintTickerSummary(result, "REST", err) if err == nil { if m.remoteConfig.WebsocketRPC.Enabled { - relayWebsocketEvent(result, "ticker_update", c.AssetType.String(), exchangeName) + relayWebsocketEvent(result, "ticker_update", c.Key.Asset.String(), exchangeName) } } updateErr := m.update(c, SyncItemTicker, err) @@ -598,7 +624,7 @@ func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchan } } -func (m *syncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExchange) { +func (m *SyncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExchange) { if !c.locks[SyncItemOrderbook].TryLock() { return } @@ -622,9 +648,9 @@ func (m *syncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExc if m.config.LogSwitchProtocolEvents { log.Warnf(log.SyncMgr, "%s %s %s: No orderbook update after %s, switching from websocket to rest", - c.Exchange, - m.FormatCurrency(c.Pair).String(), - strings.ToUpper(c.AssetType.String()), + c.Key.Exchange, + m.FormatCurrency(c.Pair), + strings.ToUpper(c.Key.Asset.String()), m.config.TimeoutWebsocket, ) } @@ -633,11 +659,11 @@ func (m *syncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExc if s.IsUsingREST && time.Since(s.LastUpdated) > m.config.TimeoutREST { result, err := e.UpdateOrderbook(context.TODO(), c.Pair, - c.AssetType) + c.Key.Asset) m.PrintOrderbookSummary(result, "REST", err) if err == nil { if m.remoteConfig.WebsocketRPC.Enabled { - relayWebsocketEvent(result, "orderbook_update", c.AssetType.String(), e.GetName()) + relayWebsocketEvent(result, "orderbook_update", c.Key.Asset.String(), e.GetName()) } } updateErr := m.update(c, SyncItemOrderbook, err) @@ -647,7 +673,7 @@ func (m *syncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExc } } -func (m *syncManager) syncTrades(c *currencyPairSyncAgent) { +func (m *SyncManager) syncTrades(c *currencyPairSyncAgent) { if !c.locks[SyncItemTrade].TryLock() { return } @@ -703,7 +729,7 @@ func printConvertCurrencyFormat(origPrice float64, origCurrency, displayCurrency } // PrintTickerSummary outputs the ticker results -func (m *syncManager) PrintTickerSummary(result *ticker.Price, protocol string, err error) { +func (m *SyncManager) PrintTickerSummary(result *ticker.Price, protocol string, err error) { if m == nil || atomic.LoadInt32(&m.started) == 0 { return } @@ -777,11 +803,11 @@ func (m *syncManager) PrintTickerSummary(result *ticker.Price, protocol string, // FormatCurrency is a method that formats and returns a currency pair // based on the user currency display preferences -func (m *syncManager) FormatCurrency(p currency.Pair) currency.Pair { +func (m *SyncManager) FormatCurrency(cp currency.Pair) string { if m == nil || atomic.LoadInt32(&m.started) == 0 { - return p + return "" } - return p.Format(m.format) + return m.format.Format(cp) } const ( @@ -789,7 +815,7 @@ const ( ) // PrintOrderbookSummary outputs orderbook results -func (m *syncManager) PrintOrderbookSummary(result *orderbook.Base, protocol string, err error) { +func (m *SyncManager) PrintOrderbookSummary(result *orderbook.Base, protocol string, err error) { if m == nil || atomic.LoadInt32(&m.started) == 0 { return } @@ -863,7 +889,7 @@ func (m *syncManager) PrintOrderbookSummary(result *orderbook.Base, protocol str // WaitForInitialSync allows for a routine to wait for an initial sync to be // completed without exposing the underlying type. This needs to be called in a // separate routine. -func (m *syncManager) WaitForInitialSync() error { +func (m *SyncManager) WaitForInitialSync() error { if m == nil { return fmt.Errorf("sync manager %w", ErrNilSubsystem) } diff --git a/engine/sync_manager_test.go b/engine/sync_manager_test.go index adf49e761d6..950be921b6c 100644 --- a/engine/sync_manager_test.go +++ b/engine/sync_manager_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -15,42 +16,42 @@ import ( func TestSetupSyncManager(t *testing.T) { t.Parallel() - _, err := setupSyncManager(nil, nil, nil, false) + _, err := SetupSyncManager(nil, nil, nil, false) if !errors.Is(err, common.ErrNilPointer) { t.Errorf("error '%v', expected '%v'", err, common.ErrNilPointer) } - _, err = setupSyncManager(&config.SyncManagerConfig{}, nil, nil, false) + _, err = SetupSyncManager(&config.SyncManagerConfig{}, nil, nil, false) if !errors.Is(err, errNoSyncItemsEnabled) { t.Errorf("error '%v', expected '%v'", err, errNoSyncItemsEnabled) } - _, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, nil, nil, false) + _, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, nil, nil, false) if !errors.Is(err, errNilExchangeManager) { t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager) } - _, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, &ExchangeManager{}, nil, false) + _, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, &ExchangeManager{}, nil, false) if !errors.Is(err, errNilConfig) { t.Errorf("error '%v', expected '%v'", err, errNilConfig) } - _, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + _, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, currency.ErrCurrencyCodeEmpty) { t.Errorf("error '%v', expected '%v'", err, currency.ErrCurrencyCodeEmpty) } - _, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.BTC}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + _, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.BTC}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, currency.ErrFiatDisplayCurrencyIsNotFiat) { t.Errorf("error '%v', expected '%v'", err, currency.ErrFiatDisplayCurrencyIsNotFiat) } - _, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + _, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, common.ErrNilPointer) { t.Errorf("error '%v', expected '%v'", err, common.ErrNilPointer) } - m, err := setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + m, err := SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -61,7 +62,7 @@ func TestSetupSyncManager(t *testing.T) { func TestSyncManagerStart(t *testing.T) { t.Parallel() - m, err := setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + m, err := SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -96,7 +97,7 @@ func TestSyncManagerStart(t *testing.T) { func TestSyncManagerStop(t *testing.T) { t.Parallel() - var m *syncManager + var m *SyncManager err := m.Stop() if !errors.Is(err, ErrNilSubsystem) { t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) @@ -112,7 +113,7 @@ func TestSyncManagerStop(t *testing.T) { if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } - m, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) + m, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -150,7 +151,7 @@ func TestPrintConvertCurrencyFormat(t *testing.T) { func TestPrintTickerSummary(t *testing.T) { t.Parallel() - var m *syncManager + var m *SyncManager m.PrintTickerSummary(&ticker.Price{}, "REST", nil) em := NewExchangeManager() @@ -163,7 +164,7 @@ func TestPrintTickerSummary(t *testing.T) { if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } - m, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) + m, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -192,7 +193,7 @@ func TestPrintTickerSummary(t *testing.T) { func TestPrintOrderbookSummary(t *testing.T) { t.Parallel() - var m *syncManager + var m *SyncManager m.PrintOrderbookSummary(nil, "REST", nil) em := NewExchangeManager() @@ -205,7 +206,7 @@ func TestPrintOrderbookSummary(t *testing.T) { if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } - m, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) + m, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -242,13 +243,13 @@ func TestRelayWebsocketEvent(t *testing.T) { } func TestWaitForInitialSync(t *testing.T) { - var m *syncManager + var m *SyncManager err := m.WaitForInitialSync() if !errors.Is(err, ErrNilSubsystem) { t.Fatalf("received %v, but expected: %v", err, ErrNilSubsystem) } - m = &syncManager{} + m = &SyncManager{} err = m.WaitForInitialSync() if !errors.Is(err, ErrSubSystemNotStarted) { t.Fatalf("received %v, but expected: %v", err, ErrSubSystemNotStarted) @@ -263,13 +264,13 @@ func TestWaitForInitialSync(t *testing.T) { func TestSyncManagerWebsocketUpdate(t *testing.T) { t.Parallel() - var m *syncManager + var m *SyncManager err := m.WebsocketUpdate("", currency.EMPTYPAIR, 1, 47, nil) if !errors.Is(err, ErrNilSubsystem) { t.Fatalf("received %v, but expected: %v", err, ErrNilSubsystem) } - m = &syncManager{} + m = &SyncManager{} err = m.WebsocketUpdate("", currency.EMPTYPAIR, 1, 47, nil) if !errors.Is(err, ErrSubSystemNotStarted) { t.Fatalf("received %v, but expected: %v", err, ErrSubSystemNotStarted) @@ -314,9 +315,8 @@ func TestSyncManagerWebsocketUpdate(t *testing.T) { t.Fatalf("received %v, but expected: %v", err, errCouldNotSyncNewData) } - m.add(currencyPairKey{ - AssetType: asset.Spot, - Pair: currency.EMPTYPAIR.Format(currency.PairFormat{Uppercase: true}), + m.add(key.ExchangePairAsset{ + Asset: asset.Spot, }, syncBase{}) m.initSyncWG.Add(3) // orderbook match diff --git a/engine/sync_manager_types.go b/engine/sync_manager_types.go index aed9509ca78..468078323b3 100644 --- a/engine/sync_manager_types.go +++ b/engine/sync_manager_types.go @@ -4,9 +4,9 @@ import ( "sync" "time" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) // syncBase stores information @@ -18,23 +18,17 @@ type syncBase struct { NumErrors int } -// currencyPairKey is the map key for the sync agents -type currencyPairKey struct { - Exchange string - AssetType asset.Item - Pair currency.Pair -} - // currencyPairSyncAgent stores the sync agent info type currencyPairSyncAgent struct { - currencyPairKey + Key key.ExchangePairAsset + Pair currency.Pair Created time.Time trackers []*syncBase locks []sync.Mutex } -// syncManager stores the exchange currency pair syncer object -type syncManager struct { +// SyncManager stores the exchange currency pair syncer object +type SyncManager struct { initSyncCompleted int32 initSyncStarted int32 started int32 @@ -47,8 +41,8 @@ type syncManager struct { initSyncWG sync.WaitGroup inService sync.WaitGroup - currencyPairs map[currencyPairKey]*currencyPairSyncAgent - tickerBatchLastRequested map[string]time.Time + currencyPairs map[key.ExchangePairAsset]*currencyPairSyncAgent + tickerBatchLastRequested map[key.ExchangeAsset]time.Time remoteConfig *config.RemoteControlConfig config config.SyncManagerConfig diff --git a/engine/websocketroutine_manager.go b/engine/websocketroutine_manager.go index 6494b5e1542..bec886bebaa 100644 --- a/engine/websocketroutine_manager.go +++ b/engine/websocketroutine_manager.go @@ -242,6 +242,10 @@ func (m *WebsocketRoutineManager) websocketDataHandler(exchName string, data int } m.syncer.PrintTickerSummary(&d[x], "websocket", err) } + case order.Detail, + ticker.Price, + orderbook.Depth: + return errUseAPointer case stream.KlineData: if m.verbose { log.Infof(log.WebsocketMgr, "%s websocket %s %s kline updated %+v", diff --git a/engine/websocketroutine_manager_test.go b/engine/websocketroutine_manager_test.go index 6428d47e870..e082193bda4 100644 --- a/engine/websocketroutine_manager_test.go +++ b/engine/websocketroutine_manager_test.go @@ -26,17 +26,17 @@ func TestWebsocketRoutineManagerSetup(t *testing.T) { if !errors.Is(err, errNilCurrencyPairSyncer) { t.Errorf("error '%v', expected '%v'", err, errNilCurrencyPairSyncer) } - _, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, nil, false) + _, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, nil, false) if !errors.Is(err, errNilCurrencyConfig) { t.Errorf("error '%v', expected '%v'", err, errNilCurrencyConfig) } - _, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{}, true) + _, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, ¤cy.Config{}, true) if !errors.Is(err, errNilCurrencyPairFormat) { t.Errorf("error '%v', expected '%v'", err, errNilCurrencyPairFormat) } - m, err := setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) + m, err := setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -55,7 +55,7 @@ func TestWebsocketRoutineManagerStart(t *testing.T) { Uppercase: false, Delimiter: "-", }} - m, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, cfg, true) + m, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, cfg, true) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -75,7 +75,7 @@ func TestWebsocketRoutineManagerIsRunning(t *testing.T) { t.Error("expected false") } - m, err := setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) + m, err := setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -102,7 +102,7 @@ func TestWebsocketRoutineManagerStop(t *testing.T) { t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) } - m, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) + m, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -146,7 +146,7 @@ func TestWebsocketRoutineManagerHandleData(t *testing.T) { Uppercase: false, Delimiter: "-", }} - m, err := setupWebsocketRoutineManager(em, om, &syncManager{}, cfg, true) + m, err := setupWebsocketRoutineManager(em, om, &SyncManager{}, cfg, true) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } diff --git a/engine/websocketroutine_manager_types.go b/engine/websocketroutine_manager_types.go index 17780f26c87..0aa4236e59a 100644 --- a/engine/websocketroutine_manager_types.go +++ b/engine/websocketroutine_manager_types.go @@ -14,6 +14,7 @@ var ( errNilWebsocketDataHandlerFunction = errors.New("websocket data handler function is nil") errNilWebsocket = errors.New("websocket is nil") errRoutineManagerNotStarted = errors.New("websocket routine manager not started") + errUseAPointer = errors.New("could not process, pass to websocket routine manager as a pointer") ) const ( diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index 4cc7964a34a..604afc0bd7c 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -14,6 +14,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -482,3 +483,8 @@ func (a *Alphapoint) GetHistoricCandlesExtended(_ context.Context, _ currency.Pa func (a *Alphapoint) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (a *Alphapoint) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/binance/binance_cfutures.go b/exchanges/binance/binance_cfutures.go index a346663e5ab..8e919b3f659 100644 --- a/exchanges/binance/binance_cfutures.go +++ b/exchanges/binance/binance_cfutures.go @@ -31,6 +31,7 @@ const ( cfuturesContinuousKline = "/dapi/v1/continuousKlines?" cfuturesIndexKline = "/dapi/v1/indexPriceKlines?" cfuturesMarkPriceKline = "/dapi/v1/markPriceKlines?" + cfuturesFundingRateInfo = "/dapi/v1/fundingInfo?" cfuturesMarkPrice = "/dapi/v1/premiumIndex?" cfuturesFundingRateHistory = "/dapi/v1/fundingRate?" cfuturesTickerPriceStats = "/dapi/v1/ticker/24hr?" @@ -236,6 +237,13 @@ func (b *Binance) GetIndexAndMarkPrice(ctx context.Context, symbol, pair string) return resp, b.SendHTTPRequest(ctx, exchange.RestCoinMargined, cfuturesMarkPrice+params.Encode(), cFuturesIndexMarkPriceRate, &resp) } +// GetFundingRateInfo returns extra details about funding rates +func (b *Binance) GetFundingRateInfo(ctx context.Context) ([]FundingRateInfoResponse, error) { + params := url.Values{} + var resp []FundingRateInfoResponse + return resp, b.SendHTTPRequest(ctx, exchange.RestCoinMargined, cfuturesFundingRateInfo+params.Encode(), uFuturesDefaultRate, &resp) +} + // GetFuturesKlineData gets futures kline data for CoinMarginedFutures, func (b *Binance) GetFuturesKlineData(ctx context.Context, symbol currency.Pair, interval string, limit int64, startTime, endTime time.Time) ([]FuturesCandleStick, error) { params := url.Values{} diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index 8cc074b0cc8..e4c53d99be7 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" @@ -2819,7 +2820,7 @@ func TestUpdateOrderExecutionLimits(t *testing.T) { func TestGetFundingRates(t *testing.T) { t.Parallel() s, e := getTime() - _, err := b.GetFundingRates(context.Background(), &fundingrate.RatesRequest{ + _, err := b.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ Asset: asset.USDTMarginedFutures, Pair: currency.NewPair(currency.BTC, currency.USDT), StartDate: s, @@ -2831,7 +2832,7 @@ func TestGetFundingRates(t *testing.T) { t.Error(err) } - _, err = b.GetFundingRates(context.Background(), &fundingrate.RatesRequest{ + _, err = b.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ Asset: asset.USDTMarginedFutures, Pair: currency.NewPair(currency.BTC, currency.USDT), StartDate: s, @@ -2842,7 +2843,7 @@ func TestGetFundingRates(t *testing.T) { t.Error(err) } - r := &fundingrate.RatesRequest{ + r := &fundingrate.HistoricalRatesRequest{ Asset: asset.USDTMarginedFutures, Pair: currency.NewPair(currency.BTC, currency.USDT), StartDate: s, @@ -2851,7 +2852,7 @@ func TestGetFundingRates(t *testing.T) { if sharedtestvalues.AreAPICredentialsSet(b) { r.IncludePayments = true } - _, err = b.GetFundingRates(context.Background(), r) + _, err = b.GetHistoricalFundingRates(context.Background(), r) if err != nil { t.Error(err) } @@ -2861,37 +2862,36 @@ func TestGetFundingRates(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.GetFundingRates(context.Background(), r) + _, err = b.GetHistoricalFundingRates(context.Background(), r) if err != nil { t.Error(err) } } -func TestGetLatestFundingRate(t *testing.T) { +func TestGetLatestFundingRates(t *testing.T) { t.Parallel() - _, err := b.GetLatestFundingRate(context.Background(), &fundingrate.LatestRateRequest{ + cp := currency.NewPair(currency.BTC, currency.USDT) + _, err := b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ Asset: asset.USDTMarginedFutures, - Pair: currency.NewPair(currency.BTC, currency.USDT), + Pair: cp, IncludePredictedRate: true, }) if !errors.Is(err, common.ErrFunctionNotSupported) { t.Error(err) } - _, err = b.GetLatestFundingRate(context.Background(), &fundingrate.LatestRateRequest{ + err = b.CurrencyPairs.EnablePair(asset.USDTMarginedFutures, cp) + if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) { + t.Fatal(err) + } + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ Asset: asset.USDTMarginedFutures, - Pair: currency.NewPair(currency.BTC, currency.USDT), + Pair: cp, }) if err != nil { t.Error(err) } - - cp, err := currency.NewPairFromString("BTCUSD_PERP") - if err != nil { - t.Error(err) - } - _, err = b.GetLatestFundingRate(context.Background(), &fundingrate.LatestRateRequest{ + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ Asset: asset.CoinMarginedFutures, - Pair: cp, }) if err != nil { t.Error(err) @@ -3418,14 +3418,24 @@ func TestGetFuturesContractDetails(t *testing.T) { if !errors.Is(err, asset.ErrNotSupported) { t.Error(err) } - _, err = b.GetFuturesContractDetails(context.Background(), asset.USDTMarginedFutures) if !errors.Is(err, nil) { t.Error(err) } - _, err = b.GetFuturesContractDetails(context.Background(), asset.CoinMarginedFutures) if !errors.Is(err, nil) { t.Error(err) } } + +func TestGetFundingRateInfo(t *testing.T) { + t.Parallel() + _, err := b.GetFundingRateInfo(context.Background()) + assert.NoError(t, err) +} + +func TestUGetFundingRateInfo(t *testing.T) { + t.Parallel() + _, err := b.UGetFundingRateInfo(context.Background()) + assert.NoError(t, err) +} diff --git a/exchanges/binance/binance_ufutures.go b/exchanges/binance/binance_ufutures.go index 6643529a07e..9ff95dd230e 100644 --- a/exchanges/binance/binance_ufutures.go +++ b/exchanges/binance/binance_ufutures.go @@ -30,6 +30,7 @@ const ( ufuturesKlineData = "/fapi/v1/klines?" ufuturesMarkPrice = "/fapi/v1/premiumIndex?" ufuturesFundingRateHistory = "/fapi/v1/fundingRate?" + ufuturesFundingRateInfo = "/fapi/v1/fundingInfo?" ufuturesTickerPriceStats = "/fapi/v1/ticker/24hr?" ufuturesSymbolPriceTicker = "/fapi/v1/ticker/price?" ufuturesSymbolOrderbook = "/fapi/v1/ticker/bookTicker?" @@ -379,6 +380,12 @@ func (b *Binance) UGetMarkPrice(ctx context.Context, symbol currency.Pair) ([]UM return resp, nil } +// UGetFundingRateInfo returns extra details about funding rates +func (b *Binance) UGetFundingRateInfo(ctx context.Context) ([]FundingRateInfoResponse, error) { + var resp []FundingRateInfoResponse + return resp, b.SendHTTPRequest(ctx, exchange.RestUSDTMargined, ufuturesFundingRateInfo, uFuturesDefaultRate, &resp) +} + // UGetFundingHistory gets funding history for USDTMarginedFutures func (b *Binance) UGetFundingHistory(ctx context.Context, symbol currency.Pair, limit int64, startTime, endTime time.Time) ([]FundingRateHistory, error) { var resp []FundingRateHistory diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index dedc56f2b2e..c6577f19cc5 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -149,6 +149,7 @@ func (b *Binance) SetDefaults() { MultiChainDeposits: true, MultiChainWithdrawals: true, HasAssetTypeAccountSegregation: true, + FundingRateFetching: true, }, WebsocketCapabilities: protocol.Features{ TradeFetching: true, @@ -161,6 +162,7 @@ func (b *Binance) SetDefaults() { GetOrders: true, Subscribe: true, Unsubscribe: true, + FundingRateFetching: false, // supported but not implemented // TODO when multi-websocket support added }, WithdrawPermissions: exchange.AutoWithdrawCrypto | exchange.NoFiatWithdrawals, @@ -169,11 +171,17 @@ func (b *Binance) SetDefaults() { Intervals: true, }, FuturesCapabilities: exchange.FuturesCapabilities{ - Positions: true, - Leverage: true, - CollateralMode: true, - FundingRates: true, - FundingRateFrequency: kline.EightHour.Duration(), + Positions: true, + Leverage: true, + CollateralMode: true, + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.FourHour: true, + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.USDTMarginedFutures: true, + }, }, }, Enabled: exchange.FeaturesEnabled{ @@ -1985,7 +1993,7 @@ func (b *Binance) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) err = fmt.Errorf("%w %v", asset.ErrNotSupported, a) } if err != nil { - return fmt.Errorf("cannot update exchange execution limits: %v", err) + return fmt.Errorf("cannot update exchange execution limits: %w", err) } return b.LoadLimits(limits) } @@ -2072,57 +2080,148 @@ func (b *Binance) GetServerTime(ctx context.Context, ai asset.Item) (time.Time, return time.Time{}, fmt.Errorf("%s %w", ai, asset.ErrNotSupported) } -// GetLatestFundingRate returns the latest funding rate for a given asset and currency -func (b *Binance) GetLatestFundingRate(ctx context.Context, r *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) { +// GetLatestFundingRates returns the latest funding rates data +func (b *Binance) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { if r == nil { return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) } if r.IncludePredictedRate { return nil, fmt.Errorf("%w IncludePredictedRate", common.ErrFunctionNotSupported) } - format, err := b.GetPairFormat(r.Asset, true) - if err != nil { - return nil, err - } - fPair := r.Pair.Format(format) - pairRate := fundingrate.LatestRateResponse{ - Exchange: b.Name, - Asset: r.Asset, - Pair: fPair, + fPair := r.Pair + var err error + if !fPair.IsEmpty() { + var format currency.PairFormat + format, err = b.GetPairFormat(r.Asset, true) + if err != nil { + return nil, err + } + fPair = r.Pair.Format(format) } + switch r.Asset { case asset.USDTMarginedFutures: var mp []UMarkPrice - mp, err = b.UGetMarkPrice(ctx, r.Pair) + var fri []FundingRateInfoResponse + fri, err = b.UGetFundingRateInfo(ctx) if err != nil { return nil, err } - pairRate.TimeOfNextRate = time.UnixMilli(mp[len(mp)-1].NextFundingTime) - pairRate.LatestRate = fundingrate.Rate{ - Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency), - Rate: decimal.NewFromFloat(mp[len(mp)-1].LastFundingRate), + + mp, err = b.UGetMarkPrice(ctx, fPair) + if err != nil { + return nil, err + } + resp := make([]fundingrate.LatestRateResponse, 0, len(mp)) + for i := range mp { + var cp currency.Pair + var isEnabled bool + cp, isEnabled, err = b.MatchSymbolCheckEnabled(mp[i].Symbol, r.Asset, true) + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return nil, err + } + if !isEnabled { + continue + } + var isPerp bool + isPerp, err = b.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + var fundingRateFrequency int64 + for x := range fri { + if fri[x].Symbol != mp[i].Symbol { + continue + } + fundingRateFrequency = fri[x].FundingIntervalHours + break + } + nft := time.UnixMilli(mp[i].NextFundingTime) + rate := fundingrate.LatestRateResponse{ + TimeChecked: time.Now(), + Exchange: b.Name, + Asset: r.Asset, + Pair: cp, + LatestRate: fundingrate.Rate{ + Time: time.UnixMilli(mp[i].Time).Truncate(time.Hour * time.Duration(fundingRateFrequency)), + Rate: decimal.NewFromFloat(mp[i].LastFundingRate), + }, + } + if nft.Year() == rate.TimeChecked.Year() { + rate.TimeOfNextRate = nft + } + resp = append(resp, rate) + } + if len(resp) == 0 { + return nil, fmt.Errorf("%w %v %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair) } + return resp, nil case asset.CoinMarginedFutures: var mp []IndexMarkPrice mp, err = b.GetIndexAndMarkPrice(ctx, fPair.String(), "") if err != nil { return nil, err } - pairRate.TimeOfNextRate = time.UnixMilli(mp[len(mp)-1].NextFundingTime) - pairRate.LatestRate = fundingrate.Rate{ - Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency), - Rate: mp[len(mp)-1].LastFundingRate.Decimal(), + var fri []FundingRateInfoResponse + fri, err = b.GetFundingRateInfo(ctx) + if err != nil { + return nil, err } - default: - return nil, fmt.Errorf("%s %w", r.Asset, asset.ErrNotSupported) + + resp := make([]fundingrate.LatestRateResponse, 0, len(mp)) + for i := range mp { + var cp currency.Pair + cp, err = currency.NewPairFromString(mp[i].Symbol) + if err != nil { + return nil, err + } + var isPerp bool + isPerp, err = b.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + var fundingRateFrequency int64 + for x := range fri { + if fri[x].Symbol != mp[i].Symbol { + continue + } + fundingRateFrequency = fri[x].FundingIntervalHours + break + } + nft := time.UnixMilli(mp[i].NextFundingTime) + rate := fundingrate.LatestRateResponse{ + TimeChecked: time.Now(), + Exchange: b.Name, + Asset: r.Asset, + Pair: cp, + LatestRate: fundingrate.Rate{ + Time: time.UnixMilli(mp[i].Time).Truncate(time.Duration(fundingRateFrequency) * time.Hour), + Rate: mp[i].LastFundingRate.Decimal(), + }, + } + if nft.Year() == rate.TimeChecked.Year() { + rate.TimeOfNextRate = nft + } + resp = append(resp, rate) + } + if len(resp) == 0 { + return nil, fmt.Errorf("%w %v %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair) + } + return resp, nil } - return &pairRate, nil + return nil, fmt.Errorf("%s %w", r.Asset, asset.ErrNotSupported) } -// GetFundingRates returns funding rates for a given asset and currency for a time period -func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesRequest) (*fundingrate.Rates, error) { +// GetHistoricalFundingRates returns funding rates for a given asset and currency for a time period +func (b *Binance) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { if r == nil { - return nil, fmt.Errorf("%w RatesRequest", common.ErrNilPointer) + return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) } if r.IncludePredictedRate { return nil, fmt.Errorf("%w GetFundingRates IncludePredictedRate", common.ErrFunctionNotSupported) @@ -2138,7 +2237,7 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque return nil, err } fPair := r.Pair.Format(format) - pairRate := fundingrate.Rates{ + pairRate := fundingrate.HistoricalRates{ Exchange: b.Name, Asset: r.Asset, Pair: fPair, @@ -2149,6 +2248,20 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque case asset.USDTMarginedFutures: requestLimit := 1000 sd := r.StartDate + var fri []FundingRateInfoResponse + fri, err = b.UGetFundingRateInfo(ctx) + if err != nil { + return nil, err + } + var fundingRateFrequency int64 + fps := fPair.String() + for x := range fri { + if fri[x].Symbol != fps { + continue + } + fundingRateFrequency = fri[x].FundingIntervalHours + break + } for { var frh []FundingRateHistory frh, err = b.UGetFundingHistory(ctx, fPair, int64(requestLimit), sd, r.EndDate) @@ -2172,7 +2285,7 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque return nil, err } pairRate.LatestRate = fundingrate.Rate{ - Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency), + Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(time.Duration(fundingRateFrequency) * time.Hour), Rate: decimal.NewFromFloat(mp[len(mp)-1].LastFundingRate), } pairRate.TimeOfNextRate = time.UnixMilli(mp[len(mp)-1].NextFundingTime) @@ -2185,7 +2298,7 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque for j := range income { for x := range pairRate.FundingRates { tt := time.UnixMilli(income[j].Time) - tt = tt.Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency) + tt = tt.Truncate(time.Duration(fundingRateFrequency) * time.Hour) if !tt.Equal(pairRate.FundingRates[x].Time) { continue } @@ -2201,6 +2314,20 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque case asset.CoinMarginedFutures: requestLimit := 1000 sd := r.StartDate + var fri []FundingRateInfoResponse + fri, err = b.GetFundingRateInfo(ctx) + if err != nil { + return nil, err + } + var fundingRateFrequency int64 + fps := fPair.String() + for x := range fri { + if fri[x].Symbol != fps { + continue + } + fundingRateFrequency = fri[x].FundingIntervalHours + break + } for { var frh []FundingRateHistory frh, err = b.FuturesGetFundingHistory(ctx, fPair, int64(requestLimit), sd, r.EndDate) @@ -2224,7 +2351,7 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque return nil, err } pairRate.LatestRate = fundingrate.Rate{ - Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency), + Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(time.Duration(fundingRateFrequency) * time.Hour), Rate: mp[len(mp)-1].LastFundingRate.Decimal(), } pairRate.TimeOfNextRate = time.UnixMilli(mp[len(mp)-1].NextFundingTime) @@ -2237,7 +2364,7 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque for j := range income { for x := range pairRate.FundingRates { tt := time.UnixMilli(income[j].Timestamp) - tt = tt.Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency) + tt = tt.Truncate(time.Duration(fundingRateFrequency) * time.Hour) if !tt.Equal(pairRate.FundingRates[x].Time) { continue } @@ -2259,14 +2386,10 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque // IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future func (b *Binance) IsPerpetualFutureCurrency(a asset.Item, cp currency.Pair) (bool, error) { if a == asset.CoinMarginedFutures { - if cp.Quote.Equal(currency.PERP) { - return true, nil - } + return cp.Quote.Equal(currency.PERP), nil } if a == asset.USDTMarginedFutures { - if cp.Quote.Equal(currency.USDT) || cp.Quote.Equal(currency.BUSD) { - return true, nil - } + return cp.Quote.Equal(currency.USDT) || cp.Quote.Equal(currency.BUSD), nil } return false, nil } @@ -2406,7 +2529,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po var leverage, maintenanceMargin, initialMargin, liquidationPrice, markPrice, positionSize, collateralTotal, collateralUsed, collateralAvailable, - pnl, openPrice, isolatedMargin float64 + unrealisedPNL, openPrice, isolatedMargin float64 for i := range ai.Positions { if ai.Positions[i].Symbol != fPair.String() { @@ -2469,7 +2592,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po } collateralTotal = collateralAsset.WalletBalance collateralAvailable = collateralAsset.AvailableBalance - pnl = collateralAsset.UnrealizedProfit + unrealisedPNL = collateralAsset.UnrealizedProfit c = currency.NewCode(collateralAsset.Asset) if marginType == margin.Multi { isolatedMargin = collateralAsset.CrossUnPnl @@ -2482,7 +2605,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po collateralTotal = ai.TotalWalletBalance collateralUsed = ai.TotalWalletBalance - ai.AvailableBalance collateralAvailable = ai.AvailableBalance - pnl = accountPosition.UnrealisedProfit + unrealisedPNL = accountPosition.UnrealisedProfit } var maintenanceMarginFraction decimal.Decimal @@ -2496,8 +2619,9 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po return nil, err } var relevantPosition *UPositionInformationV2 + fps := fPair.String() for i := range positionsInfo { - if positionsInfo[i].Symbol != fPair.String() { + if positionsInfo[i].Symbol != fps { continue } relevantPosition = &positionsInfo[i] @@ -2522,7 +2646,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po MarkPrice: decimal.NewFromFloat(markPrice), CurrentSize: decimal.NewFromFloat(positionSize), AverageOpenPrice: decimal.NewFromFloat(openPrice), - PositionPNL: decimal.NewFromFloat(pnl), + UnrealisedPNL: decimal.NewFromFloat(unrealisedPNL), MaintenanceMarginFraction: maintenanceMarginFraction, FreeCollateral: decimal.NewFromFloat(collateralAvailable), TotalCollateral: decimal.NewFromFloat(collateralTotal), @@ -2540,8 +2664,9 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po pnl, openPrice, isolatedMargin float64 var accountPosition *FuturesAccountInformationPosition + fps := fPair.String() for i := range ai.Positions { - if ai.Positions[i].Symbol != fPair.String() { + if ai.Positions[i].Symbol != fps { continue } accountPosition = &ai.Positions[i] @@ -2594,7 +2719,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po } var relevantPosition *FuturesPositionInformation for i := range positionsInfo { - if positionsInfo[i].Symbol != fPair.String() { + if positionsInfo[i].Symbol != fps { continue } relevantPosition = &positionsInfo[i] @@ -2642,7 +2767,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po MarkPrice: decimal.NewFromFloat(markPrice), CurrentSize: decimal.NewFromFloat(positionSize), AverageOpenPrice: decimal.NewFromFloat(openPrice), - PositionPNL: decimal.NewFromFloat(pnl), + UnrealisedPNL: decimal.NewFromFloat(pnl), MaintenanceMarginFraction: mmf, FreeCollateral: decimal.NewFromFloat(collateralAvailable), TotalCollateral: tc, @@ -2873,18 +2998,31 @@ func (b *Binance) GetFuturesContractDetails(ctx context.Context, item asset.Item } switch item { case asset.USDTMarginedFutures: + fri, err := b.UGetFundingRateInfo(ctx) + if err != nil { + return nil, err + } + ei, err := b.UExchangeInfo(ctx) if err != nil { return nil, err } resp := make([]futures.Contract, 0, len(ei.Symbols)) for i := range ei.Symbols { + var fundingRateFloor, fundingRateCeil decimal.Decimal + for j := range fri { + if fri[j].Symbol != ei.Symbols[i].Symbol { + continue + } + fundingRateFloor = fri[j].AdjustedFundingRateFloor.Decimal() + fundingRateCeil = fri[j].AdjustedFundingRateCap.Decimal() + break + } var cp currency.Pair cp, err = currency.NewPairFromStrings(ei.Symbols[i].BaseAsset, ei.Symbols[i].Symbol[len(ei.Symbols[i].BaseAsset):]) if err != nil { return nil, err } - var ct futures.ContractType var ed time.Time if cp.Quote.Equal(currency.USDT) || cp.Quote.Equal(currency.BUSD) { @@ -2894,27 +3032,43 @@ func (b *Binance) GetFuturesContractDetails(ctx context.Context, item asset.Item ed = ei.Symbols[i].DeliveryDate.Time() } resp = append(resp, futures.Contract{ - Exchange: b.Name, - Name: cp, - Underlying: currency.NewPair(currency.NewCode(ei.Symbols[i].BaseAsset), currency.NewCode(ei.Symbols[i].QuoteAsset)), - Asset: item, - SettlementType: futures.Linear, - StartDate: ei.Symbols[i].OnboardDate.Time(), - EndDate: ed, - IsActive: ei.Symbols[i].Status == "TRADING", - Status: ei.Symbols[i].Status, - MarginCurrency: currency.NewCode(ei.Symbols[i].MarginAsset), - Type: ct, + Exchange: b.Name, + Name: cp, + Underlying: currency.NewPair(currency.NewCode(ei.Symbols[i].BaseAsset), currency.NewCode(ei.Symbols[i].QuoteAsset)), + Asset: item, + SettlementType: futures.Linear, + StartDate: ei.Symbols[i].OnboardDate.Time(), + EndDate: ed, + IsActive: ei.Symbols[i].Status == "TRADING", + Status: ei.Symbols[i].Status, + MarginCurrency: currency.NewCode(ei.Symbols[i].MarginAsset), + Type: ct, + FundingRateFloor: fundingRateFloor, + FundingRateCeiling: fundingRateCeil, }) } return resp, nil case asset.CoinMarginedFutures: + fri, err := b.GetFundingRateInfo(ctx) + if err != nil { + return nil, err + } ei, err := b.FuturesExchangeInfo(ctx) if err != nil { return nil, err } + resp := make([]futures.Contract, 0, len(ei.Symbols)) for i := range ei.Symbols { + var fundingRateFloor, fundingRateCeil decimal.Decimal + for j := range fri { + if fri[j].Symbol != ei.Symbols[i].Symbol { + continue + } + fundingRateFloor = fri[j].AdjustedFundingRateFloor.Decimal() + fundingRateCeil = fri[j].AdjustedFundingRateCap.Decimal() + break + } var cp currency.Pair cp, err = currency.NewPairFromString(ei.Symbols[i].Symbol) if err != nil { @@ -2930,16 +3084,18 @@ func (b *Binance) GetFuturesContractDetails(ctx context.Context, item asset.Item ed = ei.Symbols[i].DeliveryDate.Time() } resp = append(resp, futures.Contract{ - Exchange: b.Name, - Name: cp, - Underlying: currency.NewPair(currency.NewCode(ei.Symbols[i].BaseAsset), currency.NewCode(ei.Symbols[i].QuoteAsset)), - Asset: item, - StartDate: ei.Symbols[i].OnboardDate.Time(), - EndDate: ed, - IsActive: ei.Symbols[i].ContractStatus == "TRADING", - MarginCurrency: currency.NewCode(ei.Symbols[i].MarginAsset), - SettlementType: futures.Inverse, - Type: ct, + Exchange: b.Name, + Name: cp, + Underlying: currency.NewPair(currency.NewCode(ei.Symbols[i].BaseAsset), currency.NewCode(ei.Symbols[i].QuoteAsset)), + Asset: item, + StartDate: ei.Symbols[i].OnboardDate.Time(), + EndDate: ed, + IsActive: ei.Symbols[i].ContractStatus == "TRADING", + MarginCurrency: currency.NewCode(ei.Symbols[i].MarginAsset), + SettlementType: futures.Inverse, + Type: ct, + FundingRateFloor: fundingRateFloor, + FundingRateCeiling: fundingRateCeil, }) } return resp, nil diff --git a/exchanges/binance/ufutures_types.go b/exchanges/binance/ufutures_types.go index dbd66dda1d0..aca55dbf9c4 100644 --- a/exchanges/binance/ufutures_types.go +++ b/exchanges/binance/ufutures_types.go @@ -3,6 +3,7 @@ package binance import ( "time" + "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/currency" ) @@ -81,6 +82,15 @@ type UMarkPrice struct { Time int64 `json:"time"` } +// FundingRateInfoResponse stores funding rate info +type FundingRateInfoResponse struct { + Symbol string `json:"symbol"` + AdjustedFundingRateCap convert.StringToFloat64 `json:"adjustedFundingRateCap"` + AdjustedFundingRateFloor convert.StringToFloat64 `json:"adjustedFundingRateFloor"` + FundingIntervalHours int64 `json:"fundingIntervalHours"` + Disclaimer bool `json:"disclaimer"` +} + // FundingRateHistory stores funding rate history type FundingRateHistory struct { Symbol string `json:"symbol"` diff --git a/exchanges/binanceus/binanceus_wrapper.go b/exchanges/binanceus/binanceus_wrapper.go index fdd05cecdf5..4dd257791bd 100644 --- a/exchanges/binanceus/binanceus_wrapper.go +++ b/exchanges/binanceus/binanceus_wrapper.go @@ -16,6 +16,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -992,3 +993,8 @@ func (bi *Binanceus) GetAvailableTransferChains(ctx context.Context, cryptocurre func (bi *Binanceus) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (bi *Binanceus) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 4c0aafb5194..28a4d7612f2 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -18,6 +18,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -125,6 +126,7 @@ func (b *Bitfinex) SetDefaults() { MultiChainDeposits: true, MultiChainWithdrawals: true, MultiChainDepositRequiresChainSet: true, + FundingRateFetching: true, }, WebsocketCapabilities: protocol.Features{ AccountBalance: true, @@ -150,6 +152,15 @@ func (b *Bitfinex) SetDefaults() { DateRanges: true, Intervals: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.Margin: true, + }, + }, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, @@ -362,12 +373,9 @@ func (b *Bitfinex) UpdateTickers(ctx context.Context, a asset.Item) error { for key, val := range tickerNew { pair, enabled, err := b.MatchSymbolCheckEnabled(key[1:], a, true) - if err != nil { - if !errors.Is(err, currency.ErrPairNotFound) { - return err - } + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return err } - if !enabled { continue } @@ -1297,3 +1305,9 @@ func (b *Bitfinex) GetServerTime(_ context.Context, _ asset.Item) (time.Time, er func (b *Bitfinex) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bitfinex) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + // TODO: Add futures support for Bitfinex + return nil, common.ErrNotYetImplemented +} diff --git a/exchanges/bitflyer/bitflyer_wrapper.go b/exchanges/bitflyer/bitflyer_wrapper.go index 6908aa1aff6..e93f8c7e664 100644 --- a/exchanges/bitflyer/bitflyer_wrapper.go +++ b/exchanges/bitflyer/bitflyer_wrapper.go @@ -16,6 +16,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -504,3 +505,8 @@ func (b *Bitflyer) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair func (b *Bitflyer) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bitflyer) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index daccf5eee24..1ea333f64ae 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -19,6 +19,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/currencystate" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -919,3 +920,8 @@ func (b *Bithumb) GetServerTime(_ context.Context, _ asset.Item) (time.Time, err func (b *Bithumb) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bithumb) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/bitmex/bitmex.go b/exchanges/bitmex/bitmex.go index 8ebe9f5a08c..a618b94b021 100644 --- a/exchanges/bitmex/bitmex.go +++ b/exchanges/bitmex/bitmex.go @@ -232,11 +232,21 @@ func (b *Bitmex) GetAccountExecutionTradeHistory(ctx context.Context, params *Ge func (b *Bitmex) GetFullFundingHistory(ctx context.Context, symbol, count, filter, columns, start string, reverse bool, startTime, endTime time.Time) ([]Funding, error) { var fundingHistory []Funding params := url.Values{} - params.Set("symbol", symbol) - params.Set("count", count) - params.Set("filter", filter) - params.Set("columns", columns) - params.Set("start", start) + if symbol != "" { + params.Set("symbol", symbol) + } + if count != "" { + params.Set("count", count) + } + if filter != "" { + params.Set("filter", filter) + } + if columns != "" { + params.Set("columns", columns) + } + if !startTime.IsZero() { + params.Set("start", start) + } params.Set("reverse", "true") if !reverse { params.Set("reverse", "false") diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index fc4e7c634b9..5e5c928a662 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -17,6 +17,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" @@ -1256,3 +1257,61 @@ func TestGetFuturesContractDetails(t *testing.T) { t.Error(err) } } + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.USDTMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USDT), + IncludePredictedRate: true, + }) + if !errors.Is(err, common.ErrFunctionNotSupported) { + t.Error(err) + } + + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.BTC, currency.KLAY), + }) + if !errors.Is(err, futures.ErrNotPerpetualFuture) { + t.Error(err) + } + + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.PerpetualContract, + }) + if err != nil { + t.Error(err) + } + + cp, err := currency.NewPairFromString("ETHUSD") + if err != nil { + t.Error(err) + } + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.PerpetualContract, + Pair: cp, + }) + if err != nil { + t.Error(err) + } +} + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + isPerp, err := b.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.BTC, currency.USD)) + if err != nil { + t.Error(err) + } + if isPerp { + t.Error("expected false") + } + + isPerp, err = b.IsPerpetualFutureCurrency(asset.PerpetualContract, currency.NewPair(currency.BTC, currency.USD)) + if err != nil { + t.Error(err) + } + if !isPerp { + t.Error("expected true") + } +} diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 2e73a118fd3..95819f57b27 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -123,6 +123,7 @@ func (b *Bitmex) SetDefaults() { CryptoWithdrawal: true, TradeFee: true, CryptoWithdrawalFee: true, + FundingRateFetching: true, }, WebsocketCapabilities: protocol.Features{ TradeFetching: true, @@ -134,6 +135,16 @@ func (b *Bitmex) SetDefaults() { DeadMansSwitch: true, GetOrders: true, GetOrder: true, + FundingRateFetching: false, // supported but not implemented // TODO when multi-websocket support added + }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.PerpetualContract: true, + }, }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | exchange.WithdrawCryptoWithEmail | @@ -402,12 +413,9 @@ instruments: pair, enabled, err = b.MatchSymbolCheckEnabled(tick[j].Symbol, a, false) } - if err != nil { - if !errors.Is(err, currency.ErrPairNotFound) { - return err - } + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return err } - if !enabled { continue } @@ -1241,3 +1249,86 @@ func (b *Bitmex) GetFuturesContractDetails(ctx context.Context, item asset.Item) } return resp, nil } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bitmex) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + + if r.IncludePredictedRate { + return nil, fmt.Errorf("%w IncludePredictedRate", common.ErrFunctionNotSupported) + } + + count := "1" + if r.Pair.IsEmpty() { + count = "500" + } else { + isPerp, err := b.IsPerpetualFutureCurrency(r.Asset, r.Pair) + if err != nil { + return nil, err + } + if !isPerp { + return nil, fmt.Errorf("%w %v %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair) + } + } + + format, err := b.GetPairFormat(r.Asset, true) + if err != nil { + return nil, err + } + fPair := format.Format(r.Pair) + rates, err := b.GetFullFundingHistory(ctx, fPair, count, "", "", "", true, time.Time{}, time.Time{}) + if err != nil { + return nil, err + } + + resp := make([]fundingrate.LatestRateResponse, 0, len(rates)) + // Bitmex returns historical rates from this endpoint, we only want the latest + latestRateSymbol := make(map[string]bool) + for i := range rates { + if _, ok := latestRateSymbol[rates[i].Symbol]; ok { + continue + } + latestRateSymbol[rates[i].Symbol] = true + var nr time.Time + nr, err = time.Parse(time.RFC3339, rates[i].FundingInterval) + if err != nil { + return nil, err + } + var cp currency.Pair + var isEnabled bool + cp, isEnabled, err = b.MatchSymbolCheckEnabled(rates[i].Symbol, r.Asset, false) + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return nil, err + } + if !isEnabled { + continue + } + var isPerp bool + isPerp, err = b.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + resp = append(resp, fundingrate.LatestRateResponse{ + Exchange: b.Name, + Asset: r.Asset, + Pair: cp, + LatestRate: fundingrate.Rate{ + Time: rates[i].Timestamp, + Rate: decimal.NewFromFloat(rates[i].FundingRate), + }, + TimeOfNextRate: rates[i].Timestamp.Add(time.Duration(nr.Hour()) * time.Hour), + TimeChecked: time.Now(), + }) + } + return resp, nil +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (b *Bitmex) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) { + return a == asset.PerpetualContract, nil +} diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 13863077661..8e0ad553050 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -17,6 +17,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1029,3 +1030,8 @@ func (b *Bitstamp) GetServerTime(_ context.Context, _ asset.Item) (time.Time, er func (b *Bitstamp) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bitstamp) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index 3b2e65c77c3..36f7623ac7f 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -16,6 +16,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1095,3 +1096,8 @@ func (b *Bittrex) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair, func (b *Bittrex) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bittrex) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index 36e71ab9fdc..c530edcb9d3 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -18,6 +18,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1224,3 +1225,8 @@ func convertToKlineCandle(candle *[6]string) (kline.Candle, error) { func (b *BTCMarkets) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *BTCMarkets) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/btse/btse_test.go b/exchanges/btse/btse_test.go index 76c8c9f58c8..726f0283c19 100644 --- a/exchanges/btse/btse_test.go +++ b/exchanges/btse/btse_test.go @@ -16,6 +16,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -968,3 +969,60 @@ func TestGetFuturesContractDetails(t *testing.T) { t.Error(err) } } + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.USDTMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USDT), + IncludePredictedRate: true, + }) + if !errors.Is(err, asset.ErrNotSupported) { + t.Error(err) + } + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + }) + if err != nil { + t.Error(err) + } + + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: testFUTURESPair, + }) + if err != nil { + t.Error(err) + } +} + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + isPerp, err := b.IsPerpetualFutureCurrency(asset.CoinMarginedFutures, currency.NewPair(currency.BTC, currency.USD)) + if err != nil { + t.Error(err) + } + if isPerp { + t.Error("expected false") + } + + isPerp, err = b.IsPerpetualFutureCurrency(asset.Futures, testFUTURESPair) + if err != nil { + t.Error(err) + } + if !isPerp { + t.Error("expected true") + } + + cp, err := currency.NewPairFromString(testSPOTPair) + if err != nil { + t.Error(err) + } + isPerp, err = b.IsPerpetualFutureCurrency(asset.Futures, cp) + if err != nil { + t.Error(err) + } + if isPerp { + t.Error("expected false") + } +} diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index cba211de111..677374f8a1b 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -114,6 +114,7 @@ func (b *BTSE) SetDefaults() { FiatDepositFee: true, FiatWithdrawalFee: true, CryptoWithdrawalFee: true, + FundingRateFetching: true, }, WebsocketCapabilities: protocol.Features{ OrderbookFetching: true, @@ -128,6 +129,15 @@ func (b *BTSE) SetDefaults() { DateRanges: true, Intervals: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.OneHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.Futures: true, + }, + }, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, @@ -1219,3 +1229,65 @@ func (b *BTSE) GetFuturesContractDetails(ctx context.Context, item asset.Item) ( } return resp, nil } + +// GetLatestFundingRates returns the latest funding rates data +func (b *BTSE) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) + } + if r.IncludePredictedRate { + return nil, fmt.Errorf("%w IncludePredictedRate", common.ErrFunctionNotSupported) + } + + format, err := b.GetPairFormat(r.Asset, true) + if err != nil { + return nil, err + } + fPair := format.Format(r.Pair) + rates, err := b.GetMarketSummary(ctx, fPair, false) + if err != nil { + return nil, err + } + + resp := make([]fundingrate.LatestRateResponse, 0, len(rates)) + for i := range rates { + var cp currency.Pair + var isEnabled bool + cp, isEnabled, err = b.MatchSymbolCheckEnabled(rates[i].Symbol, r.Asset, true) + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return nil, err + } + if !isEnabled { + continue + } + var isPerp bool + isPerp, err = b.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + tt := time.Now().Truncate(time.Hour) + resp = append(resp, fundingrate.LatestRateResponse{ + Exchange: b.Name, + Asset: r.Asset, + Pair: cp, + LatestRate: fundingrate.Rate{ + Time: time.Now().Truncate(time.Hour), + Rate: decimal.NewFromFloat(rates[i].FundingRate), + }, + TimeOfNextRate: tt.Add(time.Hour), + TimeChecked: time.Now(), + }) + } + return resp, nil +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (b *BTSE) IsPerpetualFutureCurrency(a asset.Item, p currency.Pair) (bool, error) { + return a == asset.Futures && p.Quote.Equal(currency.PFC), nil +} diff --git a/exchanges/bybit/bybit_test.go b/exchanges/bybit/bybit_test.go index ed8e68fb930..a2cb789169d 100644 --- a/exchanges/bybit/bybit_test.go +++ b/exchanges/bybit/bybit_test.go @@ -3584,3 +3584,22 @@ func TestGetContractLength(t *testing.T) { t.Error("expected semi annually") } } + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + enabled, err := b.GetEnabledPairs(asset.Futures) + if err != nil { + t.Fatal(err) + } + for x := range enabled { + isPerp, err := b.IsPerpetualFutureCurrency(asset.Futures, enabled[x]) + if err != nil { + t.Fatal(err) + } + if enabled[x].Quote.Equal(currency.PFC) && !isPerp { + t.Error("expected true") + } else if !enabled[x].Quote.Equal(currency.PFC) && isPerp { + t.Error("expected false") + } + } +} diff --git a/exchanges/bybit/bybit_wrapper.go b/exchanges/bybit/bybit_wrapper.go index bfff780cca8..01ee925bd8b 100644 --- a/exchanges/bybit/bybit_wrapper.go +++ b/exchanges/bybit/bybit_wrapper.go @@ -17,6 +17,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -2416,3 +2417,14 @@ func getContractLength(contractLength time.Duration) (futures.ContractType, erro } return ct, nil } + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (by *Bybit) IsPerpetualFutureCurrency(a asset.Item, p currency.Pair) (bool, error) { + return a == asset.Futures && p.Quote.Equal(currency.PFC), nil +} + +// GetLatestFundingRates returns the latest funding rates data +func (by *Bybit) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + // TODO: implement with v5 API upgrade + return nil, common.ErrNotYetImplemented +} diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index 4e76dfa75ec..3aa525b5d2b 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -15,6 +15,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -981,6 +982,11 @@ func (c *CoinbasePro) GetServerTime(ctx context.Context, _ asset.Item) (time.Tim return st.ISO, nil } +// GetLatestFundingRates returns the latest funding rates data +func (c *CoinbasePro) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} + // GetFuturesContractDetails returns all contracts from the exchange by asset type func (c *CoinbasePro) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 2f61aeede16..228a1845eea 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -17,6 +17,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1199,3 +1200,8 @@ func (c *COINUT) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair, func (c *COINUT) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (c *COINUT) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/exchange.go b/exchanges/exchange.go index a2bf0f3287c..b617efb9b55 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -1672,11 +1672,6 @@ func (b *Base) GetFuturesPositionSummary(context.Context, *futures.PositionSumma return nil, common.ErrNotYetImplemented } -// GetFundingPaymentDetails returns funding payment details for a future for a specific time period -func (b *Base) GetFundingPaymentDetails(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) { - return nil, common.ErrNotYetImplemented -} - // GetFuturesPositions returns futures positions for all currencies func (b *Base) GetFuturesPositions(context.Context, *futures.PositionsRequest) ([]futures.PositionDetails, error) { return nil, common.ErrNotYetImplemented @@ -1687,13 +1682,8 @@ func (b *Base) GetFuturesPositionOrders(context.Context, *futures.PositionsReque return nil, common.ErrNotYetImplemented } -// GetLatestFundingRate returns the latest funding rate based on request data -func (b *Base) GetLatestFundingRate(context.Context, *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) { - return nil, common.ErrNotYetImplemented -} - -// GetFundingRates returns funding rates based on request data -func (b *Base) GetFundingRates(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) { +// GetHistoricalFundingRates returns historical funding rates for a future +func (b *Base) GetHistoricalFundingRates(context.Context, *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { return nil, common.ErrNotYetImplemented } diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index 6c772da0d84..23603fe611d 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -2521,18 +2521,10 @@ func TestGetFuturesPositions(t *testing.T) { } } -func TestGetFundingPaymentDetails(t *testing.T) { +func TestGetHistoricalFundingRates(t *testing.T) { t.Parallel() var b Base - if _, err := b.GetFundingPaymentDetails(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) { - t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented) - } -} - -func TestGetFundingRate(t *testing.T) { - t.Parallel() - var b Base - if _, err := b.GetLatestFundingRate(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) { + if _, err := b.GetHistoricalFundingRates(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) { t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented) } } @@ -2540,7 +2532,7 @@ func TestGetFundingRate(t *testing.T) { func TestGetFundingRates(t *testing.T) { t.Parallel() var b Base - if _, err := b.GetFundingRates(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) { + if _, err := b.GetHistoricalFundingRates(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) { t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented) } } diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go index c5a1f7486e3..f70cc538205 100644 --- a/exchanges/exchange_types.go +++ b/exchanges/exchange_types.go @@ -177,14 +177,15 @@ type FeaturesSupported struct { // FuturesCapabilities stores the exchange's futures capabilities type FuturesCapabilities struct { - FundingRates bool - Positions bool - OrderManagerPositionTracking bool - Collateral bool - CollateralMode bool - Leverage bool - MaximumFundingRateHistory time.Duration - FundingRateFrequency time.Duration + FundingRates bool + MaximumFundingRateHistory time.Duration + SupportedFundingRateFrequencies map[kline.Interval]bool + Positions bool + OrderManagerPositionTracking bool + Collateral bool + CollateralMode bool + Leverage bool + FundingRateBatching map[asset.Item]bool } // MarginCapabilities stores the exchange's margin capabilities diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index dbed8c21cd9..91d29ec7e05 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -17,6 +17,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -206,6 +207,9 @@ func (e *EXMO) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error // UpdateTickers updates the ticker for all currency pairs of a given asset type func (e *EXMO) UpdateTickers(ctx context.Context, a asset.Item) error { + if !e.SupportsAsset(a) { + return fmt.Errorf("%w: %v", asset.ErrNotSupported, a) + } result, err := e.GetTicker(ctx) if err != nil { return err @@ -831,3 +835,8 @@ func (e *EXMO) GetAvailableTransferChains(ctx context.Context, cryptocurrency cu func (e *EXMO) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (e *EXMO) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/fundingrate/fundingrate_types.go b/exchanges/fundingrate/fundingrate_types.go index b8709e73d19..a8376deb9cf 100644 --- a/exchanges/fundingrate/fundingrate_types.go +++ b/exchanges/fundingrate/fundingrate_types.go @@ -12,8 +12,8 @@ import ( // ErrFundingRateOutsideLimits is returned when a funding rate is outside the allowed date range var ErrFundingRateOutsideLimits = errors.New("funding rate outside limits") -// RatesRequest is used to request funding rate details for a position -type RatesRequest struct { +// HistoricalRatesRequest is used to request funding rate details for a position +type HistoricalRatesRequest struct { Asset asset.Item Pair currency.Pair // PaymentCurrency is an optional parameter depending on exchange API @@ -30,8 +30,8 @@ type RatesRequest struct { RespectHistoryLimits bool } -// Rates is used to return funding rate details for a position -type Rates struct { +// HistoricalRates is used to return funding rate details for a position +type HistoricalRates struct { Exchange string Asset asset.Item Pair currency.Pair @@ -60,6 +60,7 @@ type LatestRateResponse struct { LatestRate Rate PredictedUpcomingRate Rate TimeOfNextRate time.Time + TimeChecked time.Time } // Rate holds details for an individual funding rate diff --git a/exchanges/futures/contract.go b/exchanges/futures/contract.go index 8fbf5f492aa..8b890412b58 100644 --- a/exchanges/futures/contract.go +++ b/exchanges/futures/contract.go @@ -3,6 +3,7 @@ package futures import ( "time" + "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -26,6 +27,8 @@ type Contract struct { Multiplier float64 MaxLeverage float64 LatestRate fundingrate.Rate + FundingRateFloor decimal.Decimal + FundingRateCeiling decimal.Decimal } // ContractSettlementType holds the various style of contracts offered by futures exchanges diff --git a/exchanges/futures/futures.go b/exchanges/futures/futures.go index e8e71eefbe4..76cb8cf358f 100644 --- a/exchanges/futures/futures.go +++ b/exchanges/futures/futures.go @@ -134,7 +134,7 @@ func (c *PositionController) GetPositionsForExchange(exch string, item asset.Ite } // TrackFundingDetails applies funding rate details to a tracked position -func (c *PositionController) TrackFundingDetails(d *fundingrate.Rates) error { +func (c *PositionController) TrackFundingDetails(d *fundingrate.HistoricalRates) error { if c == nil { return fmt.Errorf("position controller %w", common.ErrNilPointer) } @@ -459,7 +459,7 @@ func (m *MultiPositionTracker) TrackNewOrder(d *order.Detail) error { } // TrackFundingDetails applies funding rate details to a tracked position -func (m *MultiPositionTracker) TrackFundingDetails(d *fundingrate.Rates) error { +func (m *MultiPositionTracker) TrackFundingDetails(d *fundingrate.HistoricalRates) error { if m == nil { return fmt.Errorf("multi-position tracker %w", common.ErrNilPointer) } @@ -579,7 +579,7 @@ func (p *PositionTracker) GetStats() *Position { if p.fundingRateDetails != nil { frs := make([]fundingrate.Rate, len(p.fundingRateDetails.FundingRates)) copy(frs, p.fundingRateDetails.FundingRates) - pos.FundingRates = fundingrate.Rates{ + pos.FundingRates = fundingrate.HistoricalRates{ Exchange: p.fundingRateDetails.Exchange, Asset: p.fundingRateDetails.Asset, Pair: p.fundingRateDetails.Pair, @@ -687,7 +687,7 @@ func (p *PositionTracker) GetLatestPNLSnapshot() (PNLResult, error) { } // TrackFundingDetails sets funding rates to a position -func (p *PositionTracker) TrackFundingDetails(d *fundingrate.Rates) error { +func (p *PositionTracker) TrackFundingDetails(d *fundingrate.HistoricalRates) error { if p == nil { return fmt.Errorf("position tracker %w", common.ErrNilPointer) } @@ -715,7 +715,7 @@ func (p *PositionTracker) TrackFundingDetails(d *fundingrate.Rates) error { return fmt.Errorf("%w for timeframe %v %v %v %v-%v", ErrNoPositionsFound, p.exchange, p.asset, p.contractPair, d.StartDate, d.EndDate) } if p.fundingRateDetails == nil { - p.fundingRateDetails = &fundingrate.Rates{ + p.fundingRateDetails = &fundingrate.HistoricalRates{ Exchange: d.Exchange, Asset: d.Asset, Pair: d.Pair, diff --git a/exchanges/futures/futures_test.go b/exchanges/futures/futures_test.go index 4a598c557b4..d595efbbff6 100644 --- a/exchanges/futures/futures_test.go +++ b/exchanges/futures/futures_test.go @@ -541,7 +541,7 @@ func TestGetStats(t *testing.T) { } p.exchange = testExchange - p.fundingRateDetails = &fundingrate.Rates{ + p.fundingRateDetails = &fundingrate.HistoricalRates{ FundingRates: []fundingrate.Rate{ {}, }, @@ -1274,7 +1274,7 @@ func TestPCTrackFundingDetails(t *testing.T) { } p := currency.NewPair(currency.BTC, currency.PERP) - rates := &fundingrate.Rates{ + rates := &fundingrate.HistoricalRates{ Asset: asset.Futures, Pair: p, } @@ -1341,7 +1341,7 @@ func TestMPTTrackFundingDetails(t *testing.T) { } cp := currency.NewPair(currency.BTC, currency.PERP) - rates := &fundingrate.Rates{ + rates := &fundingrate.HistoricalRates{ Asset: asset.Futures, Pair: cp, } @@ -1351,7 +1351,7 @@ func TestMPTTrackFundingDetails(t *testing.T) { } mpt.exchange = testExchange - rates = &fundingrate.Rates{ + rates = &fundingrate.HistoricalRates{ Exchange: testExchange, Asset: asset.Futures, Pair: cp, @@ -1410,7 +1410,7 @@ func TestPTTrackFundingDetails(t *testing.T) { } cp := currency.NewPair(currency.BTC, currency.PERP) - rates := &fundingrate.Rates{ + rates := &fundingrate.HistoricalRates{ Exchange: testExchange, Asset: asset.Futures, Pair: cp, diff --git a/exchanges/futures/futures_types.go b/exchanges/futures/futures_types.go index 291472d8655..4f73e90d4c2 100644 --- a/exchanges/futures/futures_types.go +++ b/exchanges/futures/futures_types.go @@ -156,7 +156,7 @@ type PositionTracker struct { shortPositions []order.Detail longPositions []order.Detail pnlHistory []PNLResult - fundingRateDetails *fundingrate.Rates + fundingRateDetails *fundingrate.HistoricalRates } // PositionTrackerSetup contains all required fields to @@ -264,7 +264,7 @@ type Position struct { CloseDate time.Time Orders []order.Detail PNLHistory []PNLResult - FundingRates fundingrate.Rates + FundingRates fundingrate.HistoricalRates } // PositionSummaryRequest is used to request a summary of an open position @@ -333,7 +333,8 @@ type PositionSummary struct { CollateralMode collateral.Mode // The currency in which the values are quoted against. Isn't always pair.Quote // eg BTC-USDC-230929's quote in GCT is 230929, but the currency should be USDC - Currency currency.Code + Currency currency.Code + StartDate time.Time AvailableEquity decimal.Decimal CashBalance decimal.Decimal @@ -345,6 +346,7 @@ type PositionSummary struct { NotionalLeverage decimal.Decimal TotalEquity decimal.Decimal StrategyEquity decimal.Decimal + MarginBalance decimal.Decimal IsolatedMargin decimal.Decimal NotionalSize decimal.Decimal @@ -359,7 +361,8 @@ type PositionSummary struct { ContractMultiplier decimal.Decimal ContractSettlementType ContractSettlementType AverageOpenPrice decimal.Decimal - PositionPNL decimal.Decimal + UnrealisedPNL decimal.Decimal + RealisedPNL decimal.Decimal MaintenanceMarginFraction decimal.Decimal FreeCollateral decimal.Decimal TotalCollateral decimal.Decimal diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 0db0ff436c8..33d4be1a200 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -3154,7 +3155,17 @@ func getFirstTradablePairOfAssets() { if err != nil { log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.Futures) } - futuresTradablePair = enabledPairs[len(enabledPairs)-1] + + if len(enabledPairs) == 0 { + var availPairs currency.Pairs + availPairs, err = g.GetAvailablePairs(asset.Futures) + if err != nil { + log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.Futures) + } + futuresTradablePair = availPairs[len(availPairs)-1] + } else { + futuresTradablePair = enabledPairs[len(enabledPairs)-1] + } enabledPairs, err = g.GetEnabledPairs(asset.Options) if err != nil { log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.Options) @@ -3419,3 +3430,28 @@ func TestGetFuturesContractDetails(t *testing.T) { t.Error(err) } } + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := g.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.USDTMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USDT), + IncludePredictedRate: true, + }) + if !errors.Is(err, asset.ErrNotSupported) { + t.Error(err) + } + _, err = g.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.BTC, currency.USD), + }) + if err != nil { + t.Error(err) + } + _, err = g.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + }) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 973e5568c3f..e0cc7dce434 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -94,6 +94,8 @@ func (g *Gateio) SetDefaults() { CryptoWithdrawalFee: true, MultiChainDeposits: true, MultiChainWithdrawals: true, + PredictedFundingRate: true, + FundingRateFetching: true, }, WebsocketCapabilities: protocol.Features{ TickerFetching: true, @@ -112,6 +114,16 @@ func (g *Gateio) SetDefaults() { Kline: kline.ExchangeCapabilitiesSupported{ Intervals: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.FourHour: true, + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.Futures: true, + }, + }, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, @@ -2155,3 +2167,96 @@ func (g *Gateio) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) e return g.LoadLimits(limits) } + +// GetLatestFundingRates returns the latest funding rates data +func (g *Gateio) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) + } + + if !r.Pair.IsEmpty() { + resp := make([]fundingrate.LatestRateResponse, 1) + fPair, err := g.FormatExchangeCurrency(r.Pair, r.Asset) + if err != nil { + return nil, err + } + var settle string + settle, err = g.getSettlementFromCurrency(fPair, true) + if err != nil { + return nil, err + } + contract, err := g.GetSingleContract(ctx, settle, fPair.String()) + if err != nil { + return nil, err + } + resp[0] = contractToFundingRate(g.Name, r.Asset, fPair, contract, r.IncludePredictedRate) + return resp, nil + } + + var resp []fundingrate.LatestRateResponse + settleCurrencies := []string{"btc", "usdt", "usd"} + pairs, err := g.GetEnabledPairs(asset.Futures) + if err != nil { + return nil, err + } + + for i := range settleCurrencies { + contracts, err := g.GetAllFutureContracts(ctx, settleCurrencies[i]) + if err != nil { + return nil, err + } + for j := range contracts { + p := strings.ToUpper(contracts[j].Name) + if !g.IsValidPairString(p) { + continue + } + cp, err := currency.NewPairFromString(p) + if err != nil { + return nil, err + } + if !pairs.Contains(cp, false) { + continue + } + var isPerp bool + isPerp, err = g.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + resp = append(resp, contractToFundingRate(g.Name, r.Asset, cp, &contracts[j], r.IncludePredictedRate)) + } + } + + return resp, nil +} + +func contractToFundingRate(name string, item asset.Item, fPair currency.Pair, contract *FuturesContract, includeUpcomingRate bool) fundingrate.LatestRateResponse { + resp := fundingrate.LatestRateResponse{ + Exchange: name, + Asset: item, + Pair: fPair, + LatestRate: fundingrate.Rate{ + Time: contract.FundingNextApply.Time().Add(-time.Duration(contract.FundingInterval) * time.Second), + Rate: contract.FundingRate.Decimal(), + }, + TimeOfNextRate: contract.FundingNextApply.Time(), + TimeChecked: time.Now(), + } + if includeUpcomingRate { + resp.PredictedUpcomingRate = fundingrate.Rate{ + Time: contract.FundingNextApply.Time(), + Rate: contract.FundingRateIndicative.Decimal(), + } + } + return resp +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (g *Gateio) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) { + return a == asset.Futures, nil +} diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index 897e55cef9e..c278c5eb8e1 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -18,6 +18,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -954,3 +955,8 @@ func (g *Gemini) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) e } return g.LoadLimits(resp) } + +// GetLatestFundingRates returns the latest funding rates data +func (g *Gemini) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 300ed1b1608..cdb52bd3c63 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -17,6 +17,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -993,3 +994,8 @@ func (h *HitBTC) GetHistoricCandlesExtended(ctx context.Context, pair currency.P func (h *HitBTC) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (h *HitBTC) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/huobi/cfutures_types.go b/exchanges/huobi/cfutures_types.go index 42cd7858f84..d2d84d0c1f8 100644 --- a/exchanges/huobi/cfutures_types.go +++ b/exchanges/huobi/cfutures_types.go @@ -543,6 +543,12 @@ type LiquidationOrdersData struct { } `json:"data"` } +// SwapFundingRatesResponse holds funding rates and data response +type SwapFundingRatesResponse struct { + Response + Data []FundingRatesData `json:"data"` +} + // FundingRatesData stores funding rates data type FundingRatesData struct { EstimatedRate float64 `json:"estimated_rate,string"` @@ -550,8 +556,8 @@ type FundingRatesData struct { ContractCode string `json:"contractCode"` Symbol string `json:"symbol"` FeeAsset string `json:"fee_asset"` - FundingTime string `json:"fundingTime"` - NextFundingTime string `json:"next_funding_time"` + FundingTime int64 `json:"fundingTime,string"` + NextFundingTime int64 `json:"next_funding_time,string"` } // HistoricalFundingRateData stores historical funding rates for perpetuals diff --git a/exchanges/huobi/huobi_cfutures.go b/exchanges/huobi/huobi_cfutures.go index f94cfb5e9dd..0477c8a13f2 100644 --- a/exchanges/huobi/huobi_cfutures.go +++ b/exchanges/huobi/huobi_cfutures.go @@ -20,6 +20,7 @@ const ( // Coin Margined Swap (perpetual futures) endpoints huobiSwapMarkets = "/swap-api/v1/swap_contract_info" huobiSwapFunding = "/swap-api/v1/swap_funding_rate" + huobiSwapBatchFunding = "/swap-api/v1/swap_batch_funding_rate" huobiSwapIndexPriceInfo = "/swap-api/v1/swap_index" huobiSwapPriceLimitation = "/swap-api/v1/swap_price_limit" huobiSwapOpenInterestInfo = "/swap-api/v1/swap_open_interest" @@ -344,8 +345,8 @@ func (h *HUOBI) GetLiquidationOrders(ctx context.Context, contract currency.Pair return resp, h.SendHTTPRequest(ctx, exchange.RestFutures, path, &resp) } -// GetHistoricalFundingRates gets historical funding rates for perpetual futures -func (h *HUOBI) GetHistoricalFundingRates(ctx context.Context, code currency.Pair, pageSize, pageIndex int64) (HistoricalFundingRateData, error) { +// GetHistoricalFundingRatesForPair gets historical funding rates for perpetual futures +func (h *HUOBI) GetHistoricalFundingRatesForPair(ctx context.Context, code currency.Pair, pageSize, pageIndex int64) (HistoricalFundingRateData, error) { var resp HistoricalFundingRateData codeValue, err := h.FormatSymbol(code, asset.CoinMarginedFutures) if err != nil { @@ -982,15 +983,15 @@ func (h *HUOBI) GetSwapMarkets(ctx context.Context, contract currency.Pair) ([]S Data []SwapMarketsData `json:"data"` } var result response - err := h.SendHTTPRequest(ctx, exchange.RestFutures, huobiSwapMarkets+vals.Encode(), &result) + err := h.SendHTTPRequest(ctx, exchange.RestFutures, huobiSwapMarkets+"?"+vals.Encode(), &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Data, err } -// GetSwapFundingRates gets funding rates data -func (h *HUOBI) GetSwapFundingRates(ctx context.Context, contract currency.Pair) (FundingRatesData, error) { +// GetSwapFundingRate gets funding rate data for one currency +func (h *HUOBI) GetSwapFundingRate(ctx context.Context, contract currency.Pair) (FundingRatesData, error) { vals := url.Values{} codeValue, err := h.FormatSymbol(contract, asset.CoinMarginedFutures) if err != nil { @@ -1002,9 +1003,16 @@ func (h *HUOBI) GetSwapFundingRates(ctx context.Context, contract currency.Pair) Data FundingRatesData `json:"data"` } var result response - err = h.SendHTTPRequest(ctx, exchange.RestFutures, huobiSwapFunding+vals.Encode(), &result) + err = h.SendHTTPRequest(ctx, exchange.RestFutures, huobiSwapFunding+"?"+vals.Encode(), &result) if result.ErrorMessage != "" { return FundingRatesData{}, errors.New(result.ErrorMessage) } return result.Data, err } + +// GetSwapFundingRates gets funding rates data +func (h *HUOBI) GetSwapFundingRates(ctx context.Context) (SwapFundingRatesResponse, error) { + var result SwapFundingRatesResponse + err := h.SendHTTPRequest(ctx, exchange.RestFutures, huobiSwapBatchFunding, &result) + return result, err +} diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index 94d217f5050..0cb4716a623 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -18,6 +18,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -975,7 +976,7 @@ func TestGetHistoricalFundingRates(t *testing.T) { if err != nil { t.Error(err) } - _, err = h.GetHistoricalFundingRates(context.Background(), cp, 0, 0) + _, err = h.GetHistoricalFundingRatesForPair(context.Background(), cp, 0, 0) if err != nil { t.Error(err) } @@ -2780,3 +2781,63 @@ func TestGetFuturesContractDetails(t *testing.T) { t.Error(err) } } + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := h.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.USDTMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USD), + IncludePredictedRate: true, + }) + if !errors.Is(err, asset.ErrNotSupported) { + t.Error(err) + } + + _, err = h.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.CoinMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USD), + IncludePredictedRate: true, + }) + if err != nil { + t.Error(err) + } + + err = h.CurrencyPairs.EnablePair(asset.CoinMarginedFutures, currency.NewPair(currency.BTC, currency.USD)) + if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) { + t.Fatal(err) + } + _, err = h.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.CoinMarginedFutures, + IncludePredictedRate: true, + }) + if err != nil { + t.Error(err) + } +} + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + is, err := h.IsPerpetualFutureCurrency(asset.Binary, currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Error(err) + } + if is { + t.Error("expected false") + } + + is, err = h.IsPerpetualFutureCurrency(asset.CoinMarginedFutures, currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Error(err) + } + if !is { + t.Error("expected true") + } +} + +func TestGetSwapFundingRates(t *testing.T) { + t.Parallel() + _, err := h.GetSwapFundingRates(context.Background()) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 3e818d0c65f..110098752a4 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" @@ -17,6 +18,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -122,6 +124,8 @@ func (h *HUOBI) SetDefaults() { MultiChainDeposits: true, MultiChainWithdrawals: true, HasAssetTypeAccountSegregation: true, + FundingRateFetching: true, + PredictedFundingRate: true, }, WebsocketCapabilities: protocol.Features{ KlineFetching: true, @@ -135,12 +139,23 @@ func (h *HUOBI) SetDefaults() { GetOrder: true, GetOrders: true, TickerFetching: true, + FundingRateFetching: false, // supported but not implemented // TODO when multi-websocket support added + }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup | exchange.NoFiatWithdrawals, Kline: kline.ExchangeCapabilitiesSupported{ Intervals: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.CoinMarginedFutures: true, + }, + }, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, @@ -2196,3 +2211,91 @@ func (h *HUOBI) GetFuturesContractDetails(ctx context.Context, item asset.Item) } return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item) } + +// GetLatestFundingRates returns the latest funding rates data +func (h *HUOBI) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + if r.Asset != asset.CoinMarginedFutures { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) + } + + var rates []FundingRatesData + if r.Pair.IsEmpty() { + batchRates, err := h.GetSwapFundingRates(ctx) + if err != nil { + return nil, err + } + rates = batchRates.Data + } else { + rateResp, err := h.GetSwapFundingRate(ctx, r.Pair) + if err != nil { + return nil, err + } + rates = append(rates, rateResp) + } + resp := make([]fundingrate.LatestRateResponse, 0, len(rates)) + for i := range rates { + if rates[i].ContractCode == "" { + // formatting to match documentation + rates[i].ContractCode = rates[i].Symbol + "-USD" + } + cp, isEnabled, err := h.MatchSymbolCheckEnabled(rates[i].ContractCode, r.Asset, true) + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return nil, err + } + if !isEnabled { + continue + } + var isPerp bool + isPerp, err = h.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + var ft, nft time.Time + nft = time.UnixMilli(rates[i].NextFundingTime) + ft = time.UnixMilli(rates[i].FundingTime) + var fri time.Duration + if len(h.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { + // can infer funding rate interval from the only funding rate frequency defined + for k := range h.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { + fri = k.Duration() + } + } + if rates[i].FundingTime == 0 { + ft = nft.Add(-fri) + } + if ft.After(time.Now()) { + ft = ft.Add(-fri) + nft = nft.Add(-fri) + } + rate := fundingrate.LatestRateResponse{ + Exchange: h.Name, + Asset: r.Asset, + Pair: r.Pair, + LatestRate: fundingrate.Rate{ + Time: ft, + Rate: decimal.NewFromFloat(rates[i].FundingRate), + }, + TimeOfNextRate: nft, + TimeChecked: time.Now(), + } + if r.IncludePredictedRate { + rate.PredictedUpcomingRate = fundingrate.Rate{ + Time: rate.TimeOfNextRate, + Rate: decimal.NewFromFloat(rates[i].EstimatedRate), + } + } + resp = append(resp, rate) + } + return resp, nil +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (h *HUOBI) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) { + return a == asset.CoinMarginedFutures, nil +} diff --git a/exchanges/interfaces.go b/exchanges/interfaces.go index bbaf824153d..48067c7ea44 100644 --- a/exchanges/interfaces.go +++ b/exchanges/interfaces.go @@ -161,8 +161,9 @@ type FuturesManagement interface { ScaleCollateral(ctx context.Context, calculator *futures.CollateralCalculator) (*collateral.ByCurrency, error) GetPositionSummary(context.Context, *futures.PositionSummaryRequest) (*futures.PositionSummary, error) CalculateTotalCollateral(context.Context, *futures.TotalCollateralCalculator) (*futures.TotalCollateralResponse, error) - GetFundingRates(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) - GetLatestFundingRate(context.Context, *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) + GetFuturesPositions(context.Context, *futures.PositionsRequest) ([]futures.PositionDetails, error) + GetHistoricalFundingRates(context.Context, *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) + GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) IsPerpetualFutureCurrency(asset.Item, currency.Pair) (bool, error) GetCollateralCurrencyForContract(asset.Item, currency.Pair) (currency.Code, asset.Item, error) diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index 253e54ed7fa..fbcc7fc554f 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -16,6 +16,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -698,3 +699,8 @@ func (i *ItBit) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair, _ func (i *ItBit) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (i *ItBit) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index 03a9552840c..b405813ff56 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -21,6 +21,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -2199,3 +2200,64 @@ func TestGetFuturesContractDetails(t *testing.T) { t.Error(err) } } + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := k.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.USDTMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USD), + IncludePredictedRate: true, + }) + if !errors.Is(err, asset.ErrNotSupported) { + t.Error(err) + } + + _, err = k.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + }) + if !errors.Is(err, nil) { + t.Error(err) + } + + cp := currency.NewPair(currency.PF, currency.NewCode("XBTUSD")) + cp.Delimiter = "_" + err = k.CurrencyPairs.EnablePair(asset.Futures, cp) + if !errors.Is(err, nil) { + t.Fatal(err) + } + _, err = k.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: cp, + IncludePredictedRate: true, + }) + if err != nil { + t.Error(err) + } +} + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + is, err := k.IsPerpetualFutureCurrency(asset.Binary, currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Error(err) + } + if is { + t.Error("expected false") + } + + is, err = k.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Error(err) + } + if is { + t.Error("expected false") + } + + is, err = k.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.PF, currency.NewCode("XBTUSD"))) + if err != nil { + t.Error(err) + } + if !is { + t.Error("expected true") + } +} diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 6e1fc8efa38..ec17c453ad1 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/config" @@ -18,6 +19,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -131,20 +133,23 @@ func (k *Kraken) SetDefaults() { MultiChainDeposits: true, MultiChainWithdrawals: true, HasAssetTypeAccountSegregation: true, + FundingRateFetching: true, + PredictedFundingRate: true, }, WebsocketCapabilities: protocol.Features{ - TickerFetching: true, - TradeFetching: true, - KlineFetching: true, - OrderbookFetching: true, - Subscribe: true, - Unsubscribe: true, - MessageCorrelation: true, - SubmitOrder: true, - CancelOrder: true, - CancelOrders: true, - GetOrders: true, - GetOrder: true, + TickerFetching: true, + TradeFetching: true, + KlineFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + MessageCorrelation: true, + SubmitOrder: true, + CancelOrder: true, + CancelOrders: true, + GetOrders: true, + GetOrder: true, + FundingRateFetching: false, // has capability but is not supported // TODO when multi-websocket support added }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup | exchange.WithdrawCryptoWith2FA | @@ -154,6 +159,15 @@ func (k *Kraken) SetDefaults() { DateRanges: true, Intervals: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.FourHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.Futures: true, + }, + }, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, @@ -1779,3 +1793,66 @@ func (k *Kraken) GetFuturesContractDetails(ctx context.Context, item asset.Item) } return resp, nil } + +// GetLatestFundingRates returns the latest funding rates data +func (k *Kraken) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) + } + if !r.Pair.IsEmpty() { + _, isEnabled, err := k.MatchSymbolCheckEnabled(r.Pair.String(), r.Asset, r.Pair.Delimiter != "") + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return nil, err + } + if !isEnabled { + return nil, fmt.Errorf("%w %v", currency.ErrPairNotEnabled, r.Pair) + } + } + + t, err := k.GetFuturesTickers(ctx) + if err != nil { + return nil, err + } + resp := make([]fundingrate.LatestRateResponse, 0, len(t.Tickers)) + for i := range t.Tickers { + pair, err := currency.NewPairFromString(t.Tickers[i].Symbol) + if err != nil { + return nil, err + } + if !r.Pair.IsEmpty() && !r.Pair.Equal(pair) { + continue + } + var isPerp bool + isPerp, err = k.IsPerpetualFutureCurrency(r.Asset, pair) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + rate := fundingrate.LatestRateResponse{ + Exchange: k.Name, + Asset: r.Asset, + Pair: pair, + LatestRate: fundingrate.Rate{ + Rate: decimal.NewFromFloat(t.Tickers[i].FundingRate), + }, + TimeChecked: time.Now(), + } + if r.IncludePredictedRate { + rate.PredictedUpcomingRate = fundingrate.Rate{ + Rate: decimal.NewFromFloat(t.Tickers[i].FundingRatePrediction), + } + } + resp = append(resp, rate) + } + return resp, nil +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (k *Kraken) IsPerpetualFutureCurrency(a asset.Item, cp currency.Pair) (bool, error) { + return cp.Base.Equal(currency.PF) && a == asset.Futures, nil +} diff --git a/exchanges/kucoin/kucoin_futures_types.go b/exchanges/kucoin/kucoin_futures_types.go index 5599e692fad..7d8304eb88e 100644 --- a/exchanges/kucoin/kucoin_futures_types.go +++ b/exchanges/kucoin/kucoin_futures_types.go @@ -251,7 +251,7 @@ type FuturesPosition struct { ADLRankingPercentile float64 `json:"delevPercentage"` OpeningTimestamp convert.ExchangeTime `json:"openingTimestamp"` CurrentTimestamp convert.ExchangeTime `json:"currentTimestamp"` - CurrentQty int64 `json:"currentQty"` + CurrentQty float64 `json:"currentQty"` CurrentCost float64 `json:"currentCost"` // Current position value CurrentComm float64 `json:"currentComm"` // Current commission UnrealisedCost float64 `json:"unrealisedCost"` diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index 2a27edad20f..a8ddc4cc63b 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -9,11 +9,14 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/margin" @@ -30,9 +33,6 @@ const ( apiSecret = "" passPhrase = "" canManipulateRealOrders = false - - assetNotEnabled = "asset %v not enabled" - spotAndMarginAssetNotEnabled = "neither spot nor margin asset is enabled" ) var ( @@ -2424,15 +2424,137 @@ func TestSeedLocalCache(t *testing.T) { func TestGetFuturesContractDetails(t *testing.T) { t.Parallel() _, err := ku.GetFuturesContractDetails(context.Background(), asset.Spot) - if !errors.Is(err, futures.ErrNotFuturesAsset) { - t.Error(err) - } + assert.ErrorIs(t, err, futures.ErrNotFuturesAsset) _, err = ku.GetFuturesContractDetails(context.Background(), asset.USDTMarginedFutures) - if !errors.Is(err, asset.ErrNotSupported) { - t.Error(err) - } + assert.ErrorIs(t, err, asset.ErrNotSupported) _, err = ku.GetFuturesContractDetails(context.Background(), asset.Futures) - if !errors.Is(err, nil) { - t.Error(err) + assert.NoError(t, err) +} + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := ku.GetLatestFundingRates(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + + req := &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.BTC, currency.USD), } + _, err = ku.GetLatestFundingRates(context.Background(), req) + assert.ErrorIs(t, err, futures.ErrNotPerpetualFuture) + + req = &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.XBT, currency.USDTM), + } + resp, err := ku.GetLatestFundingRates(context.Background(), req) + assert.NoError(t, err) + assert.Len(t, resp, 1) + + req = &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: currency.EMPTYPAIR, + } + resp, err = ku.GetLatestFundingRates(context.Background(), req) + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + is, err := ku.IsPerpetualFutureCurrency(asset.Spot, currency.EMPTYPAIR) + assert.NoError(t, err) + assert.False(t, is) + is, err = ku.IsPerpetualFutureCurrency(asset.Futures, currency.EMPTYPAIR) + assert.NoError(t, err) + assert.False(t, is) + is, err = ku.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.XBT, currency.EOS)) + assert.NoError(t, err) + assert.False(t, is) + is, err = ku.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.XBT, currency.USDTM)) + assert.NoError(t, err) + assert.True(t, is) + is, err = ku.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.XBT, currency.USDM)) + assert.NoError(t, err) + assert.True(t, is) +} + +func TestChangePositionMargin(t *testing.T) { + t.Parallel() + _, err := ku.ChangePositionMargin(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + + req := &margin.PositionChangeRequest{} + _, err = ku.ChangePositionMargin(context.Background(), req) + assert.ErrorIs(t, err, futures.ErrNotFuturesAsset) + + req.Asset = asset.Futures + _, err = ku.ChangePositionMargin(context.Background(), req) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + req.Pair = currency.NewPair(currency.XBT, currency.USDTM) + _, err = ku.ChangePositionMargin(context.Background(), req) + assert.ErrorIs(t, err, margin.ErrMarginTypeUnsupported) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + req.MarginType = margin.Isolated + _, err = ku.ChangePositionMargin(context.Background(), req) + assert.Error(t, err) + + req.NewAllocatedMargin = 1337 + _, err = ku.ChangePositionMargin(context.Background(), req) + assert.ErrorIs(t, err, nil) +} + +func TestGetFuturesPositionSummary(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesPositionSummary(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + + req := &futures.PositionSummaryRequest{} + _, err = ku.GetFuturesPositionSummary(context.Background(), req) + assert.ErrorIs(t, err, futures.ErrNotPerpetualFuture) + + req.Asset = asset.Futures + _, err = ku.GetFuturesPositionSummary(context.Background(), req) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + req.Pair = currency.NewPair(currency.XBT, currency.USDTM) + _, err = ku.GetFuturesPositionSummary(context.Background(), req) + assert.ErrorIs(t, err, nil) +} + +func TestGetFuturesPositionOrders(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesPositionOrders(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + + req := &futures.PositionsRequest{} + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, futures.ErrNotPerpetualFuture) + + req.Asset = asset.Futures + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + req.Pairs = currency.Pairs{ + currency.NewPair(currency.XBT, currency.USDTM), + } + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, common.ErrDateUnset) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + req.EndDate = time.Now() + req.StartDate = req.EndDate.Add(-time.Hour * 24 * 7) + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, nil) + + req.StartDate = req.EndDate.Add(-time.Hour * 24 * 30) + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, futures.ErrOrderHistoryTooLarge) + + req.RespectOrderHistoryLimits = true + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, nil) } diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index 80be1b26b90..10e777125ee 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -16,6 +16,7 @@ import ( 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/collateral" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" @@ -119,6 +120,20 @@ func (ku *Kucoin) SetDefaults() { KlineFetching: true, GetOrder: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + Positions: true, + Leverage: true, + CollateralMode: true, + FundingRates: true, + MaximumFundingRateHistory: kline.ThreeMonth.Duration(), + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.Futures: true, + }, + }, + MaximumOrderHistory: kline.OneDay.Duration() * 7, WithdrawPermissions: exchange.AutoWithdrawCrypto, }, Enabled: exchange.FeaturesEnabled{ @@ -890,11 +905,8 @@ func (ku *Kucoin) GetOrderInfo(ctx context.Context, orderID string, pair currenc if err != nil { return nil, err } - enabledPairs, err := ku.GetEnabledPairs(asset.Futures) - if err != nil { - return nil, err - } - nPair, err := enabledPairs.DeriveFrom(orderDetail.Symbol) + var nPair currency.Pair + nPair, err = ku.MatchSymbolWithAvailablePairs(orderDetail.Symbol, assetType, true) if err != nil { return nil, err } @@ -1066,22 +1078,19 @@ func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M if err != nil { return nil, err } - var enabledPairs currency.Pairs - enabledPairs, err = ku.GetEnabledPairs(asset.Futures) - if err != nil { - return nil, err - } for x := range futuresOrders.Items { if !futuresOrders.Items[x].IsActive { continue } - dPair, err := enabledPairs.DeriveFrom(futuresOrders.Items[x].Symbol) + var dPair currency.Pair + var isEnabled bool + dPair, isEnabled, err = ku.MatchSymbolCheckEnabled(futuresOrders.Items[x].Symbol, getOrdersRequest.AssetType, true) if err != nil { - if errors.Is(err, currency.ErrPairNotFound) { - continue - } return nil, err } + if !isEnabled { + continue + } for i := range getOrdersRequest.Pairs { if !getOrdersRequest.Pairs[i].Equal(dPair) { continue @@ -1125,8 +1134,6 @@ func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M if err != nil { return nil, err } - var enabledPairs currency.Pairs - enabledPairs, err = ku.GetEnabledPairs(asset.Futures) if err != nil { return nil, err } @@ -1134,13 +1141,15 @@ func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M if !spotOrders.Items[x].IsActive { continue } - dPair, err := enabledPairs.DeriveFrom(spotOrders.Items[x].Symbol) + var dPair currency.Pair + var isEnabled bool + dPair, isEnabled, err = ku.MatchSymbolCheckEnabled(spotOrders.Items[x].Symbol, getOrdersRequest.AssetType, true) if err != nil { - if errors.Is(err, currency.ErrPairNotFound) { - continue - } return nil, err } + if !isEnabled { + continue + } if len(getOrdersRequest.Pairs) > 0 && !getOrdersRequest.Pairs.Contains(dPair, true) { continue } @@ -1220,24 +1229,20 @@ func (ku *Kucoin) GetOrderHistory(ctx context.Context, getOrdersRequest *order.M } } } - var enabledPairs currency.Pairs - enabledPairs, err = ku.GetEnabledPairs(asset.Futures) - if err != nil { - return nil, err - } orders = make(order.FilteredOrders, 0, len(futuresOrders.Items)) for i := range orders { orderSide, err = order.StringToOrderSide(futuresOrders.Items[i].Side) if err != nil { return nil, err } - pair, err = enabledPairs.DeriveFrom(futuresOrders.Items[i].Symbol) + var isEnabled bool + pair, isEnabled, err = ku.MatchSymbolCheckEnabled(futuresOrders.Items[i].Symbol, getOrdersRequest.AssetType, true) if err != nil { - if errors.Is(err, currency.ErrPairNotFound) { - continue - } return nil, err } + if !isEnabled { + continue + } oType, err = order.StringToOrderType(futuresOrders.Items[i].OrderType) if err != nil { log.Errorf(log.ExchangeSys, "%s %v", ku.Name, err) @@ -1540,7 +1545,14 @@ func (ku *Kucoin) GetFuturesContractDetails(ctx context.Context, item asset.Item if contracts[i].IsInverse { contractSettlementType = futures.Inverse } - timeOfCurrentFundingRate := time.Now().Add((time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond) - time.Hour*8).Truncate(time.Hour).UTC() + var fri time.Duration + if len(ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { + // can infer funding rate interval from the only funding rate frequency defined + for k := range ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { + fri = k.Duration() + } + } + timeOfCurrentFundingRate := time.Now().Add((time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond) - fri).Truncate(time.Hour).UTC() resp[i] = futures.Contract{ Exchange: ku.Name, Name: cp, @@ -1564,3 +1576,331 @@ func (ku *Kucoin) GetFuturesContractDetails(ctx context.Context, item asset.Item } return resp, nil } + +// GetLatestFundingRates returns the latest funding rates data +func (ku *Kucoin) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + var fri time.Duration + if len(ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { + // can infer funding rate interval from the only funding rate frequency defined + for k := range ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { + fri = k.Duration() + } + } + if r.Pair.IsEmpty() { + contracts, err := ku.GetFuturesOpenContracts(ctx) + if err != nil { + return nil, err + } + if r.IncludePredictedRate { + log.Warnf(log.ExchangeSys, "%s predicted rate for all currencies requires an additional %v requests", ku.Name, len(contracts)) + } + timeChecked := time.Now() + resp := make([]fundingrate.LatestRateResponse, 0, len(contracts)) + for i := range contracts { + timeOfNextFundingRate := time.Now().Add(time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond).Truncate(time.Hour).UTC() + var cp currency.Pair + cp, err = currency.NewPairFromStrings(contracts[i].BaseCurrency, contracts[i].Symbol[len(contracts[i].BaseCurrency):]) + if err != nil { + return nil, err + } + var isPerp bool + isPerp, err = ku.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + + rate := fundingrate.LatestRateResponse{ + Exchange: ku.Name, + Asset: r.Asset, + Pair: cp, + LatestRate: fundingrate.Rate{ + Time: timeOfNextFundingRate.Add(-fri), + Rate: decimal.NewFromFloat(contracts[i].FundingFeeRate), + }, + TimeOfNextRate: timeOfNextFundingRate, + TimeChecked: timeChecked, + } + if r.IncludePredictedRate { + var fr *FuturesFundingRate + fr, err = ku.GetFuturesCurrentFundingRate(ctx, contracts[i].Symbol) + if err != nil { + return nil, err + } + rate.PredictedUpcomingRate = fundingrate.Rate{ + Time: timeOfNextFundingRate, + Rate: decimal.NewFromFloat(fr.PredictedValue), + } + } + resp = append(resp, rate) + } + return resp, nil + } + resp := make([]fundingrate.LatestRateResponse, 1) + is, err := ku.IsPerpetualFutureCurrency(r.Asset, r.Pair) + if err != nil { + return nil, err + } + if !is { + return nil, fmt.Errorf("%w %s %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair) + } + fPair, err := ku.FormatExchangeCurrency(r.Pair, r.Asset) + if err != nil { + return nil, err + } + var fr *FuturesFundingRate + fr, err = ku.GetFuturesCurrentFundingRate(ctx, fPair.String()) + if err != nil { + return nil, err + } + rate := fundingrate.LatestRateResponse{ + Exchange: ku.Name, + Asset: r.Asset, + Pair: r.Pair, + LatestRate: fundingrate.Rate{ + Time: fr.TimePoint.Time(), + Rate: decimal.NewFromFloat(fr.Value), + }, + TimeOfNextRate: fr.TimePoint.Time().Add(fri).Truncate(time.Hour).UTC(), + TimeChecked: time.Now(), + } + if r.IncludePredictedRate { + rate.PredictedUpcomingRate = fundingrate.Rate{ + Time: rate.TimeOfNextRate, + Rate: decimal.NewFromFloat(fr.PredictedValue), + } + } + resp[0] = rate + return resp, nil +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (ku *Kucoin) IsPerpetualFutureCurrency(a asset.Item, cp currency.Pair) (bool, error) { + return a == asset.Futures && (cp.Quote.Equal(currency.USDTM) || cp.Quote.Equal(currency.USDM)), nil +} + +// GetHistoricalFundingRates returns funding rates for a given asset and currency for a time period +func (ku *Kucoin) GetHistoricalFundingRates(_ context.Context, _ *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { + return nil, common.ErrFunctionNotSupported +} + +// GetLeverage gets the account's initial leverage for the asset type and pair +func (ku *Kucoin) GetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ order.Side) (float64, error) { + return -1, fmt.Errorf("%w leverage is set during order placement, view orders to view leverage", common.ErrFunctionNotSupported) +} + +// SetLeverage sets the account's initial leverage for the asset type and pair +func (ku *Kucoin) SetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ float64, _ order.Side) error { + return fmt.Errorf("%w leverage is set during order placement", common.ErrFunctionNotSupported) +} + +// SetMarginType sets the default margin type for when opening a new position +func (ku *Kucoin) SetMarginType(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type) error { + return fmt.Errorf("%w must be set via website", common.ErrFunctionNotSupported) +} + +// SetCollateralMode sets the collateral type for your account +func (ku *Kucoin) SetCollateralMode(_ context.Context, _ asset.Item, _ collateral.Mode) error { + return fmt.Errorf("%w must be set via website", common.ErrFunctionNotSupported) +} + +// GetCollateralMode returns the collateral type for your account +func (ku *Kucoin) GetCollateralMode(_ context.Context, _ asset.Item) (collateral.Mode, error) { + return collateral.UnknownMode, fmt.Errorf("%w only via website", common.ErrFunctionNotSupported) +} + +// ChangePositionMargin will modify a position/currencies margin parameters +func (ku *Kucoin) ChangePositionMargin(ctx context.Context, r *margin.PositionChangeRequest) (*margin.PositionChangeResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", futures.ErrNotFuturesAsset, r.Asset) + } + if r.Pair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + if r.MarginType != margin.Isolated { + return nil, fmt.Errorf("%w %v", margin.ErrMarginTypeUnsupported, r.MarginType) + } + fPair, err := ku.FormatExchangeCurrency(r.Pair, r.Asset) + if err != nil { + return nil, err + } + + resp, err := ku.AddMargin(ctx, fPair.String(), fmt.Sprintf("%s%v%v", r.Pair, r.NewAllocatedMargin, time.Now().Unix()), r.NewAllocatedMargin) + if err != nil { + return nil, err + } + if resp == nil { + return nil, fmt.Errorf("%s - %s", ku.Name, "no response received") + } + return &margin.PositionChangeResponse{ + Exchange: ku.Name, + Pair: r.Pair, + Asset: r.Asset, + AllocatedMargin: resp.PosMargin, + MarginType: r.MarginType, + }, nil +} + +// GetFuturesPositionSummary returns position summary details for an active position +func (ku *Kucoin) GetFuturesPositionSummary(ctx context.Context, r *futures.PositionSummaryRequest) (*futures.PositionSummary, error) { + if r == nil { + return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset) + } + if r.Pair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + fPair, err := ku.FormatExchangeCurrency(r.Pair, r.Asset) + if err != nil { + return nil, err + } + pos, err := ku.GetFuturesPosition(ctx, fPair.String()) + if err != nil { + return nil, err + } + marginType := margin.Isolated + if pos.CrossMode { + marginType = margin.Multi + } + contracts, err := ku.GetFuturesContractDetails(ctx, r.Asset) + if err != nil { + return nil, err + } + var multiplier, contractSize float64 + var settlementType futures.ContractSettlementType + for i := range contracts { + if !contracts[i].Name.Equal(fPair) { + continue + } + multiplier = contracts[i].Multiplier + contractSize = multiplier * pos.CurrentQty + settlementType = contracts[i].SettlementType + } + + ao, err := ku.GetFuturesAccountOverview(ctx, fPair.String()) + if err != nil { + return nil, err + } + + return &futures.PositionSummary{ + Pair: r.Pair, + Asset: r.Asset, + MarginType: marginType, + CollateralMode: collateral.MultiMode, + Currency: currency.NewCode(pos.SettleCurrency), + StartDate: pos.OpeningTimestamp.Time(), + AvailableEquity: decimal.NewFromFloat(ao.AccountEquity), + MarginBalance: decimal.NewFromFloat(ao.MarginBalance), + NotionalSize: decimal.NewFromFloat(pos.MarkValue), + Leverage: decimal.NewFromFloat(pos.RealLeverage), + MaintenanceMarginRequirement: decimal.NewFromFloat(pos.MaintMarginReq), + InitialMarginRequirement: decimal.NewFromFloat(pos.PosInit), + EstimatedLiquidationPrice: decimal.NewFromFloat(pos.LiquidationPrice), + CollateralUsed: decimal.NewFromFloat(pos.PosCost), + MarkPrice: decimal.NewFromFloat(pos.MarkPrice), + CurrentSize: decimal.NewFromFloat(pos.CurrentQty), + ContractSize: decimal.NewFromFloat(contractSize), + ContractMultiplier: decimal.NewFromFloat(multiplier), + ContractSettlementType: settlementType, + AverageOpenPrice: decimal.NewFromFloat(pos.AvgEntryPrice), + UnrealisedPNL: decimal.NewFromFloat(pos.UnrealisedPnl), + RealisedPNL: decimal.NewFromFloat(pos.RealisedPnl), + MaintenanceMarginFraction: decimal.NewFromFloat(pos.MaintMarginReq), + FreeCollateral: decimal.NewFromFloat(ao.AvailableBalance), + TotalCollateral: decimal.NewFromFloat(ao.AccountEquity), + FrozenBalance: decimal.NewFromFloat(ao.FrozenFunds), + }, nil +} + +// GetFuturesPositionOrders returns the orders for futures positions +func (ku *Kucoin) GetFuturesPositionOrders(ctx context.Context, r *futures.PositionsRequest) ([]futures.PositionResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset) + } + if len(r.Pairs) == 0 { + return nil, currency.ErrCurrencyPairEmpty + } + err := common.StartEndTimeCheck(r.StartDate, r.EndDate) + if err != nil { + return nil, err + } + if !r.EndDate.IsZero() && r.EndDate.Sub(r.StartDate) > ku.Features.Supports.MaximumOrderHistory { + if r.RespectOrderHistoryLimits { + r.StartDate = time.Now().Add(-ku.Features.Supports.MaximumOrderHistory) + } else { + return nil, fmt.Errorf("%w max lookup %v", futures.ErrOrderHistoryTooLarge, time.Now().Add(-ku.Features.Supports.MaximumOrderHistory)) + } + } + contracts, err := ku.GetFuturesContractDetails(ctx, r.Asset) + if err != nil { + return nil, err + } + resp := make([]futures.PositionResponse, len(r.Pairs)) + for x := range r.Pairs { + var multiplier float64 + fPair, err := ku.FormatExchangeCurrency(r.Pairs[x], r.Asset) + if err != nil { + return nil, err + } + for i := range contracts { + if !contracts[i].Name.Equal(fPair) { + continue + } + multiplier = contracts[i].Multiplier + } + + positionOrders, err := ku.GetFuturesOrders(ctx, "", fPair.String(), "", "", r.StartDate, r.EndDate) + if err != nil { + return nil, err + } + resp[x].Orders = make([]order.Detail, len(positionOrders.Items)) + for y := range positionOrders.Items { + side, err := order.StringToOrderSide(positionOrders.Items[y].Side) + if err != nil { + return nil, err + } + oType, err := order.StringToOrderType(positionOrders.Items[y].OrderType) + if err != nil { + return nil, fmt.Errorf("asset type: %v err: %w", r.Asset, err) + } + oStatus, err := order.StringToOrderStatus(positionOrders.Items[y].Status) + if err != nil { + return nil, fmt.Errorf("asset type: %v err: %w", r.Asset, err) + } + resp[x].Orders[y] = order.Detail{ + Leverage: positionOrders.Items[y].Leverage, + Price: positionOrders.Items[y].Price, + Amount: positionOrders.Items[y].Size * multiplier, + ContractAmount: positionOrders.Items[y].Size, + ExecutedAmount: positionOrders.Items[y].FilledSize, + RemainingAmount: positionOrders.Items[y].Size - positionOrders.Items[y].FilledSize, + CostAsset: currency.NewCode(positionOrders.Items[y].SettleCurrency), + Exchange: ku.Name, + OrderID: positionOrders.Items[y].ID, + ClientOrderID: positionOrders.Items[y].ClientOid, + Type: oType, + Side: side, + Status: oStatus, + AssetType: asset.Futures, + Date: positionOrders.Items[y].CreatedAt.Time(), + CloseTime: positionOrders.Items[y].EndAt.Time(), + LastUpdated: positionOrders.Items[y].UpdatedAt.Time(), + Pair: fPair, + } + } + } + return resp, nil +} diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index e607becc36d..aae7e58fa2c 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -16,6 +16,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1025,3 +1026,8 @@ func (l *Lbank) GetStatus(status int64) order.Status { func (l *Lbank) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (l *Lbank) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go index 3eb1474f5a5..e8152deea3d 100644 --- a/exchanges/okcoin/okcoin_wrapper.go +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -15,6 +15,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1261,3 +1262,8 @@ func (o *Okcoin) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) e func (o *Okcoin) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (o *Okcoin) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index 6e9f452d032..9fe7e19d46d 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -3290,7 +3290,7 @@ func TestGetLatestFundingRate(t *testing.T) { if err != nil { t.Error(err) } - _, err = ok.GetLatestFundingRate(contextGenerate(), &fundingrate.LatestRateRequest{ + _, err = ok.GetLatestFundingRates(contextGenerate(), &fundingrate.LatestRateRequest{ Asset: asset.PerpetualSwap, Pair: cp, IncludePredictedRate: true, @@ -3300,13 +3300,13 @@ func TestGetLatestFundingRate(t *testing.T) { } } -func TestGetFundingRates(t *testing.T) { +func TestGetHistoricalFundingRates(t *testing.T) { t.Parallel() cp, err := currency.NewPairFromString("BTC-USD-SWAP") if err != nil { t.Error(err) } - r := &fundingrate.RatesRequest{ + r := &fundingrate.HistoricalRatesRequest{ Asset: asset.PerpetualSwap, Pair: cp, PaymentCurrency: currency.USDT, @@ -3317,19 +3317,19 @@ func TestGetFundingRates(t *testing.T) { if sharedtestvalues.AreAPICredentialsSet(ok) { r.IncludePayments = true } - _, err = ok.GetFundingRates(contextGenerate(), r) + _, err = ok.GetHistoricalFundingRates(contextGenerate(), r) if err != nil { t.Error(err) } r.StartDate = time.Now().Add(-time.Hour * 24 * 120) - _, err = ok.GetFundingRates(contextGenerate(), r) + _, err = ok.GetHistoricalFundingRates(contextGenerate(), r) if !errors.Is(err, fundingrate.ErrFundingRateOutsideLimits) { t.Error(err) } r.RespectHistoryLimits = true - _, err = ok.GetFundingRates(contextGenerate(), r) + _, err = ok.GetHistoricalFundingRates(contextGenerate(), r) if err != nil { t.Error(err) } diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index da66dad4742..97cc55795d6 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -111,6 +111,8 @@ func (ok *Okx) SetDefaults() { DepositHistory: true, WithdrawalHistory: true, ModifyOrder: true, + FundingRateFetching: true, + PredictedFundingRate: true, }, WebsocketCapabilities: protocol.Features{ TickerFetching: true, @@ -135,7 +137,9 @@ func (ok *Okx) SetDefaults() { CollateralMode: true, FundingRates: true, MaximumFundingRateHistory: kline.ThreeMonth.Duration(), - FundingRateFrequency: kline.EightHour.Duration(), + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.EightHour: true, + }, }, }, Enabled: exchange.FeaturesEnabled{ @@ -1546,7 +1550,7 @@ func (ok *Okx) getInstrumentsForOptions(ctx context.Context) ([]Instrument, erro // getInstrumentsForAsset returns the instruments for an asset type func (ok *Okx) getInstrumentsForAsset(ctx context.Context, a asset.Item) ([]Instrument, error) { if !ok.SupportsAsset(a) { - return nil, fmt.Errorf("asset type of %s is not supported by %s", a, ok.Name) + return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, a) } var instType string @@ -1568,43 +1572,58 @@ func (ok *Okx) getInstrumentsForAsset(ctx context.Context, a asset.Item) ([]Inst }) } -// GetLatestFundingRate returns the latest funding rate for a given asset and currency -func (ok *Okx) GetLatestFundingRate(ctx context.Context, r *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) { +// GetLatestFundingRates returns the latest funding rates data +func (ok *Okx) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { if r == nil { return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) } + if r.Asset != asset.PerpetualSwap { + return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset) + } + if r.Pair.IsEmpty() { + return nil, fmt.Errorf("%w, pair required", currency.ErrCurrencyPairEmpty) + } format, err := ok.GetPairFormat(r.Asset, true) if err != nil { return nil, err } fPair := r.Pair.Format(format) pairRate := fundingrate.LatestRateResponse{ - Exchange: ok.Name, - Asset: r.Asset, - Pair: fPair, + TimeChecked: time.Now(), + Exchange: ok.Name, + Asset: r.Asset, + Pair: fPair, } fr, err := ok.GetSingleFundingRate(ctx, fPair.String()) if err != nil { return nil, err } + var fri time.Duration + if len(ok.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { + // can infer funding rate interval from the only funding rate frequency defined + for k := range ok.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { + fri = k.Duration() + } + } pairRate.LatestRate = fundingrate.Rate{ - Time: fr.FundingTime.Time(), + // okx funding rate is settlement time, not when it started + Time: fr.FundingTime.Time().Add(-fri), Rate: fr.FundingRate.Decimal(), } if r.IncludePredictedRate { pairRate.TimeOfNextRate = fr.NextFundingTime.Time() pairRate.PredictedUpcomingRate = fundingrate.Rate{ - Time: fr.NextFundingTime.Time(), + Time: fr.NextFundingTime.Time().Add(-fri), Rate: fr.NextFundingRate.Decimal(), } } - return &pairRate, nil + return []fundingrate.LatestRateResponse{pairRate}, nil } -// GetFundingRates returns funding rates for a given asset and currency for a time period -func (ok *Okx) GetFundingRates(ctx context.Context, r *fundingrate.RatesRequest) (*fundingrate.Rates, error) { +// GetHistoricalFundingRates returns funding rates for a given asset and currency for a time period +func (ok *Okx) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { if r == nil { - return nil, fmt.Errorf("%w RatesRequest", common.ErrNilPointer) + return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) } requestLimit := 100 sd := r.StartDate @@ -1625,7 +1644,7 @@ func (ok *Okx) GetFundingRates(ctx context.Context, r *fundingrate.RatesRequest) return nil, err } fPair := r.Pair.Format(format) - pairRate := fundingrate.Rates{ + pairRate := fundingrate.HistoricalRates{ Exchange: ok.Name, Asset: r.Asset, Pair: fPair, @@ -1693,6 +1712,13 @@ func (ok *Okx) GetFundingRates(ctx context.Context, r *fundingrate.RatesRequest) if sd.Equal(r.EndDate) || sd.After(r.EndDate) { break } + var fri time.Duration + if len(ok.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { + // can infer funding rate interval from the only funding rate frequency defined + for k := range ok.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { + fri = k.Duration() + } + } var billDetails []BillsDetailResponse billDetails, err = billDetailsFunc(ctx, &BillsDetailQueryParameter{ InstrumentType: ok.GetInstrumentTypeFromAssetItem(r.Asset), @@ -1706,7 +1732,7 @@ func (ok *Okx) GetFundingRates(ctx context.Context, r *fundingrate.RatesRequest) return nil, err } for i := range billDetails { - if index, okay := mti[billDetails[i].Timestamp.Time().Truncate(ok.Features.Supports.FuturesCapabilities.FundingRateFrequency).Unix()]; okay { + if index, okay := mti[billDetails[i].Timestamp.Time().Truncate(fri).Unix()]; okay { pairRate.FundingRates[index].Payment = billDetails[i].ProfitAndLoss.Decimal() continue } @@ -1945,7 +1971,7 @@ func (ok *Okx) GetFuturesPositionSummary(ctx context.Context, req *futures.Posit ContractMultiplier: decimal.NewFromFloat(multiplier), ContractSettlementType: contractSettlementType, AverageOpenPrice: positionSummary.AveragePrice.Decimal(), - PositionPNL: positionSummary.UPNL.Decimal(), + UnrealisedPNL: positionSummary.UPNL.Decimal(), MaintenanceMarginFraction: positionSummary.MarginRatio.Decimal(), FreeCollateral: freeCollateral, TotalCollateral: totalCollateral, diff --git a/exchanges/order/limits.go b/exchanges/order/limits.go index 4a18251005d..ae6ef8d0100 100644 --- a/exchanges/order/limits.go +++ b/exchanges/order/limits.go @@ -42,17 +42,19 @@ var ( // ErrMarketAmountExceedsStep is when the amount is not divisible by its // step for a market order ErrMarketAmountExceedsStep = errors.New("market order amount exceeds step limit") - - errCannotValidateAsset = errors.New("cannot check limit, asset not loaded") - errCannotValidateBaseCurrency = errors.New("cannot check limit, base currency not loaded") - errCannotValidateQuoteCurrency = errors.New("cannot check limit, quote currency not loaded") - errExchangeLimitAsset = errors.New("exchange limits not found for asset") - errExchangeLimitBase = errors.New("exchange limits not found for base currency") - errExchangeLimitQuote = errors.New("exchange limits not found for quote currency") - errCannotLoadLimit = errors.New("cannot load limit, levels not supplied") - errInvalidPriceLevels = errors.New("invalid price levels, cannot load limits") - errInvalidAmountLevels = errors.New("invalid amount levels, cannot load limits") - errInvalidQuoteLevels = errors.New("invalid quote levels, cannot load limits") + // ErrCannotValidateAsset is thrown when the asset is not loaded + ErrCannotValidateAsset = errors.New("cannot check limit, asset not loaded") + // ErrCannotValidateBaseCurrency is thrown when the base currency is not loaded + ErrCannotValidateBaseCurrency = errors.New("cannot check limit, base currency not loaded") + // ErrCannotValidateQuoteCurrency is thrown when the quote currency is not loaded + ErrCannotValidateQuoteCurrency = errors.New("cannot check limit, quote currency not loaded") + + errExchangeLimitBase = errors.New("exchange limits not found for base currency") + errExchangeLimitQuote = errors.New("exchange limits not found for quote currency") + errCannotLoadLimit = errors.New("cannot load limit, levels not supplied") + errInvalidPriceLevels = errors.New("invalid price levels, cannot load limits") + errInvalidAmountLevels = errors.New("invalid amount levels, cannot load limits") + errInvalidQuoteLevels = errors.New("invalid quote levels, cannot load limits") ) // ExecutionLimits defines minimum and maximum values in relation to @@ -171,7 +173,7 @@ func (e *ExecutionLimits) GetOrderExecutionLimits(a asset.Item, cp currency.Pair m1, ok := e.m[a] if !ok { - return MinMaxLevel{}, fmt.Errorf("%w %v", errExchangeLimitAsset, a) + return MinMaxLevel{}, fmt.Errorf("%w %v", ErrCannotValidateAsset, a) } m2, ok := m1[cp.Base.Item] @@ -200,17 +202,17 @@ func (e *ExecutionLimits) CheckOrderExecutionLimits(a asset.Item, cp currency.Pa m1, ok := e.m[a] if !ok { - return errCannotValidateAsset + return ErrCannotValidateAsset } m2, ok := m1[cp.Base.Item] if !ok { - return errCannotValidateBaseCurrency + return ErrCannotValidateBaseCurrency } limit, ok := m2[cp.Quote.Item] if !ok { - return errCannotValidateQuoteCurrency + return ErrCannotValidateQuoteCurrency } err := limit.Conforms(price, amount, orderType) diff --git a/exchanges/order/limits_test.go b/exchanges/order/limits_test.go index b743b27b41f..072c9c5ac4c 100644 --- a/exchanges/order/limits_test.go +++ b/exchanges/order/limits_test.go @@ -166,8 +166,8 @@ func TestGetOrderExecutionLimits(t *testing.T) { } _, err = e.GetOrderExecutionLimits(asset.Futures, ltcusd) - if !errors.Is(err, errExchangeLimitAsset) { - t.Fatalf("expected error %v but received %v", errExchangeLimitAsset, err) + if !errors.Is(err, ErrCannotValidateAsset) { + t.Fatalf("expected error %v but received %v", ErrCannotValidateAsset, err) } _, err = e.GetOrderExecutionLimits(asset.Spot, ltcusd) @@ -218,18 +218,18 @@ func TestCheckLimit(t *testing.T) { } err = e.CheckOrderExecutionLimits(asset.Futures, ltcusd, 1337, 1337, Limit) - if !errors.Is(err, errCannotValidateAsset) { - t.Fatalf("expected error %v but received %v", errCannotValidateAsset, err) + if !errors.Is(err, ErrCannotValidateAsset) { + t.Fatalf("expected error %v but received %v", ErrCannotValidateAsset, err) } err = e.CheckOrderExecutionLimits(asset.Spot, ltcusd, 1337, 1337, Limit) - if !errors.Is(err, errCannotValidateBaseCurrency) { - t.Fatalf("expected error %v but received %v", errCannotValidateBaseCurrency, err) + if !errors.Is(err, ErrCannotValidateBaseCurrency) { + t.Fatalf("expected error %v but received %v", ErrCannotValidateBaseCurrency, err) } err = e.CheckOrderExecutionLimits(asset.Spot, btcltc, 1337, 1337, Limit) - if !errors.Is(err, errCannotValidateQuoteCurrency) { - t.Fatalf("expected error %v but received %v", errCannotValidateQuoteCurrency, err) + if !errors.Is(err, ErrCannotValidateQuoteCurrency) { + t.Fatalf("expected error %v but received %v", ErrCannotValidateQuoteCurrency, err) } err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 9, Limit) diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index c533e272330..544ae9c23d7 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -1417,7 +1417,7 @@ func TestMatchFilter(t *testing.T) { if err != nil { t.Fatal(err) } - filters := map[int]Filter{ + filters := map[int]*Filter{ 0: {}, 1: {Exchange: "Binance"}, 2: {InternalOrderID: id}, @@ -1460,13 +1460,13 @@ func TestMatchFilter(t *testing.T) { // empty filter tests emptyFilter := filters[0] for _, o := range orders { - if !o.MatchFilter(&emptyFilter) { + if !o.MatchFilter(emptyFilter) { t.Error("empty filter should match everything") } } tests := map[int]struct { - f Filter + f *Filter o Detail expectedResult bool }{ @@ -1515,7 +1515,7 @@ func TestMatchFilter(t *testing.T) { tt := tt t.Run(fmt.Sprintf("%v", num), func(t *testing.T) { t.Parallel() - if tt.o.MatchFilter(&tt.f) != tt.expectedResult { + if tt.o.MatchFilter(tt.f) != tt.expectedResult { t.Errorf("tests[%v] failed", num) } }) diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index ee0efc0c139..01f6073468e 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -17,6 +17,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1133,5 +1134,12 @@ func (p *Poloniex) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, // GetFuturesContractDetails returns all contracts from the exchange by asset type func (p *Poloniex) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { - return nil, common.ErrFunctionNotSupported + // TODO: implement with API upgrade + return nil, common.ErrNotYetImplemented +} + +// GetLatestFundingRates returns the latest funding rates data +func (p *Poloniex) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + // TODO: implement with API upgrade + return nil, common.ErrNotYetImplemented } diff --git a/exchanges/protocol/features.go b/exchanges/protocol/features.go index eaa0f231beb..2d12a992f1e 100644 --- a/exchanges/protocol/features.go +++ b/exchanges/protocol/features.go @@ -3,35 +3,37 @@ package protocol // Features holds all variables for the exchanges supported features // for a protocol (e.g REST or Websocket) type Features struct { - TickerBatching bool `json:"tickerBatching,omitempty"` - AutoPairUpdates bool `json:"autoPairUpdates,omitempty"` - AccountBalance bool `json:"accountBalance,omitempty"` - CryptoDeposit bool `json:"cryptoDeposit,omitempty"` - CryptoWithdrawal bool `json:"cryptoWithdrawal,omitempty"` - FiatWithdraw bool `json:"fiatWithdraw,omitempty"` - GetOrder bool `json:"getOrder,omitempty"` - GetOrders bool `json:"getOrders,omitempty"` - CancelOrders bool `json:"cancelOrders,omitempty"` - CancelOrder bool `json:"cancelOrder,omitempty"` - SubmitOrder bool `json:"submitOrder,omitempty"` - SubmitOrders bool `json:"submitOrders,omitempty"` - ModifyOrder bool `json:"modifyOrder,omitempty"` - DepositHistory bool `json:"depositHistory,omitempty"` - WithdrawalHistory bool `json:"withdrawalHistory,omitempty"` - TradeHistory bool `json:"tradeHistory,omitempty"` - UserTradeHistory bool `json:"userTradeHistory,omitempty"` - TradeFee bool `json:"tradeFee,omitempty"` - FiatDepositFee bool `json:"fiatDepositFee,omitempty"` - FiatWithdrawalFee bool `json:"fiatWithdrawalFee,omitempty"` - CryptoDepositFee bool `json:"cryptoDepositFee,omitempty"` - CryptoWithdrawalFee bool `json:"cryptoWithdrawalFee,omitempty"` - TickerFetching bool `json:"tickerFetching,omitempty"` - KlineFetching bool `json:"klineFetching,omitempty"` - TradeFetching bool `json:"tradeFetching,omitempty"` - OrderbookFetching bool `json:"orderbookFetching,omitempty"` - AccountInfo bool `json:"accountInfo,omitempty"` - FiatDeposit bool `json:"fiatDeposit,omitempty"` - DeadMansSwitch bool `json:"deadMansSwitch,omitempty"` + TickerBatching bool `json:"tickerBatching,omitempty"` + AutoPairUpdates bool `json:"autoPairUpdates,omitempty"` + AccountBalance bool `json:"accountBalance,omitempty"` + CryptoDeposit bool `json:"cryptoDeposit,omitempty"` + CryptoWithdrawal bool `json:"cryptoWithdrawal,omitempty"` + FiatWithdraw bool `json:"fiatWithdraw,omitempty"` + GetOrder bool `json:"getOrder,omitempty"` + GetOrders bool `json:"getOrders,omitempty"` + CancelOrders bool `json:"cancelOrders,omitempty"` + CancelOrder bool `json:"cancelOrder,omitempty"` + SubmitOrder bool `json:"submitOrder,omitempty"` + SubmitOrders bool `json:"submitOrders,omitempty"` + ModifyOrder bool `json:"modifyOrder,omitempty"` + DepositHistory bool `json:"depositHistory,omitempty"` + WithdrawalHistory bool `json:"withdrawalHistory,omitempty"` + TradeHistory bool `json:"tradeHistory,omitempty"` + UserTradeHistory bool `json:"userTradeHistory,omitempty"` + TradeFee bool `json:"tradeFee,omitempty"` + FiatDepositFee bool `json:"fiatDepositFee,omitempty"` + FiatWithdrawalFee bool `json:"fiatWithdrawalFee,omitempty"` + CryptoDepositFee bool `json:"cryptoDepositFee,omitempty"` + CryptoWithdrawalFee bool `json:"cryptoWithdrawalFee,omitempty"` + TickerFetching bool `json:"tickerFetching,omitempty"` + KlineFetching bool `json:"klineFetching,omitempty"` + TradeFetching bool `json:"tradeFetching,omitempty"` + OrderbookFetching bool `json:"orderbookFetching,omitempty"` + AccountInfo bool `json:"accountInfo,omitempty"` + FiatDeposit bool `json:"fiatDeposit,omitempty"` + DeadMansSwitch bool `json:"deadMansSwitch,omitempty"` + FundingRateFetching bool `json:"fundingRateFetching"` + PredictedFundingRate bool `json:"predictedFundingRate,omitempty"` // FullPayloadSubscribe flushes and changes full subscription on websocket // connection by subscribing with full default stream channel list FullPayloadSubscribe bool `json:"fullPayloadSubscribe,omitempty"` diff --git a/exchanges/sharedtestvalues/customex.go b/exchanges/sharedtestvalues/customex.go index d55571ce32d..776c233c231 100644 --- a/exchanges/sharedtestvalues/customex.go +++ b/exchanges/sharedtestvalues/customex.go @@ -12,6 +12,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -360,6 +361,16 @@ func (c *CustomEx) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) e return nil } +// GetHistoricalFundingRates returns funding rates for a given asset and currency for a time period +func (c *CustomEx) GetHistoricalFundingRates(_ context.Context, _ *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { + return nil, nil +} + +// GetLatestFundingRates returns the latest funding rates data +func (c *CustomEx) GetLatestFundingRates(_ context.Context, _ *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, nil +} + // GetFuturesContractDetails returns all contracts from the exchange by asset type func (c *CustomEx) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index 8a11c53a1b7..7ec55d8729c 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -17,6 +17,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -766,3 +767,8 @@ func (y *Yobit) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, err func (y *Yobit) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (y *Yobit) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index 0cebb6c1bc6..d3a923ace84 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -16,6 +16,7 @@ import ( "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1152,3 +1153,8 @@ func (z *ZB) GetAvailableTransferChains(ctx context.Context, cryptocurrency curr func (z *ZB) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (z *ZB) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/testdata/http_mock/binance/binance.json b/testdata/http_mock/binance/binance.json index fd48e28f602..12451766743 100644 --- a/testdata/http_mock/binance/binance.json +++ b/testdata/http_mock/binance/binance.json @@ -262367,9 +262367,19 @@ } ] }, + "/dapi/v1/fundingInfo": { + "GET": [ + { + "data": null, + "queryString": "", + "bodyParams": "", + "headers": {} + } + ] + }, "/dapi/v1/fundingRate": { "GET": [ - { + { "data": [ { "fundingRate": "0.00010000", @@ -262397,7 +262407,7 @@ "symbol": "BTCUSD_PERP" } ], - "queryString": "endTime=1580515200000&limit=1000&startTime=1577836800000&symbol=BTCUSD_PERP", + "queryString": "endTime=1580515200000\u0026limit=1000\u0026startTime=1577836800000\u0026symbol=BTCUSD_PERP", "bodyParams": "", "headers": {} }, @@ -263394,6 +263404,695 @@ "queryString": "symbol=BTCUSD_PERP", "bodyParams": "", "headers": {} + }, + { + "data": [ + { + "estimatedSettlePrice": "5.98305943", + "indexPrice": "5.99608046", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "6.10235122", + "nextFundingTime": 0, + "pair": "LINKUSD", + "symbol": "LINKUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "4.10673771", + "indexPrice": "4.12005486", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "4.12235138", + "nextFundingTime": 0, + "pair": "DOTUSD", + "symbol": "DOTUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "3.10597019", + "indexPrice": "3.11371069", + "interestRate": "0.00010000", + "lastFundingRate": "0.00004207", + "markPrice": "3.11300000", + "nextFundingTime": 1694419200000, + "pair": "FILUSD", + "symbol": "FILUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.49424516", + "indexPrice": "0.49509765", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "0.50212503", + "nextFundingTime": 0, + "pair": "XRPUSD", + "symbol": "XRPUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.14866513", + "indexPrice": "0.14904614", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00000660", + "markPrice": "0.14890000", + "nextFundingTime": 1694419200000, + "pair": "GMTUSD", + "symbol": "GMTUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.67023180", + "indexPrice": "0.67136966", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00026801", + "markPrice": "0.66920000", + "nextFundingTime": 1694419200000, + "pair": "XTZUSD", + "symbol": "XTZUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "189.76387568", + "indexPrice": "190.27248939", + "interestRate": "0.00010000", + "lastFundingRate": "0.00001624", + "markPrice": "190.22000000", + "nextFundingTime": 1694419200000, + "pair": "BCHUSD", + "symbol": "BCHUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "189.76387568", + "indexPrice": "190.27248939", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "189.47489159", + "nextFundingTime": 0, + "pair": "BCHUSD", + "symbol": "BCHUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.49424516", + "indexPrice": "0.49509765", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "0.49654169", + "nextFundingTime": 0, + "pair": "XRPUSD", + "symbol": "XRPUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "5.98305943", + "indexPrice": "5.99608046", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00000394", + "markPrice": "5.99171788", + "nextFundingTime": 1694419200000, + "pair": "LINKUSD", + "symbol": "LINKUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "25709.96183509", + "indexPrice": "25735.28694227", + "interestRate": "0.00010000", + "lastFundingRate": "0.00004887", + "markPrice": "25720.70740797", + "nextFundingTime": 1694419200000, + "pair": "BTCUSD", + "symbol": "BTCUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.49424516", + "indexPrice": "0.49509765", + "interestRate": "0.00010000", + "lastFundingRate": "0.00003679", + "markPrice": "0.49492225", + "nextFundingTime": 1694419200000, + "pair": "XRPUSD", + "symbol": "XRPUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "5.98305943", + "indexPrice": "5.99608046", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "6.01514011", + "nextFundingTime": 0, + "pair": "LINKUSD", + "symbol": "LINKUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1.53426890", + "indexPrice": "1.53619621", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00020772", + "markPrice": "1.53431719", + "nextFundingTime": 1694419200000, + "pair": "RUNEUSD", + "symbol": "RUNEUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "25709.96183509", + "indexPrice": "25735.28694227", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "26092.24351908", + "nextFundingTime": 0, + "pair": "BTCUSD", + "symbol": "BTCUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.05675151", + "indexPrice": "0.05689040", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.05688260", + "nextFundingTime": 1694419200000, + "pair": "CHZUSD", + "symbol": "CHZUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "211.67251057", + "indexPrice": "211.60154982", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "210.38483368", + "nextFundingTime": 0, + "pair": "BNBUSD", + "symbol": "BNBUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0", + "indexPrice": "0.01482385", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.01473491", + "nextFundingTime": 1694419200000, + "pair": "GALAUSD", + "symbol": "GALAUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.07828221", + "indexPrice": "0.07836312", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.07831000", + "nextFundingTime": 1694419200000, + "pair": "TRXUSD", + "symbol": "TRXUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.09169686", + "indexPrice": "0.09192646", + "interestRate": "0.00010000", + "lastFundingRate": "0.00007248", + "markPrice": "0.09181226", + "nextFundingTime": 1694419200000, + "pair": "ALGOUSD", + "symbol": "ALGOUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "211.67251057", + "indexPrice": "211.60154982", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "206.85230590", + "nextFundingTime": 0, + "pair": "BNBUSD", + "symbol": "BNBUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.06110966", + "indexPrice": "0.06126664", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.06125000", + "nextFundingTime": 1694419200000, + "pair": "DOGEUSD", + "symbol": "DOGEUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "9.40639668", + "indexPrice": "9.42874590", + "interestRate": "0.00010000", + "lastFundingRate": "0.00004538", + "markPrice": "9.41973781", + "nextFundingTime": 1694419200000, + "pair": "AVAXUSD", + "symbol": "AVAXUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.29313952", + "indexPrice": "0.29371279", + "interestRate": "0.00010000", + "lastFundingRate": "0.00008715", + "markPrice": "0.29352266", + "nextFundingTime": 1694419200000, + "pair": "SANDUSD", + "symbol": "SANDUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.51907941", + "indexPrice": "0.52034250", + "interestRate": "0.00010000", + "lastFundingRate": "0.00007855", + "markPrice": "0.52000000", + "nextFundingTime": 1694419200000, + "pair": "MATICUSD", + "symbol": "MATICUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.55917817", + "indexPrice": "0.56026570", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.56000000", + "nextFundingTime": 1694419200000, + "pair": "EOSUSD", + "symbol": "EOSUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "61.08722760", + "indexPrice": "61.21094078", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "61.81475791", + "nextFundingTime": 0, + "pair": "LTCUSD", + "symbol": "LTCUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "6.62118246", + "indexPrice": "6.63098588", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00012913", + "markPrice": "6.62200000", + "nextFundingTime": 1694419200000, + "pair": "ATOMUSD", + "symbol": "ATOMUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "25709.96183509", + "indexPrice": "25735.28694227", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "25737.81185242", + "nextFundingTime": 0, + "pair": "BTCUSD", + "symbol": "BTCUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.59839140", + "indexPrice": "0.60008085", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.59980785", + "nextFundingTime": 1694419200000, + "pair": "THETAUSD", + "symbol": "THETAUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "61.08722760", + "indexPrice": "61.21094078", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "61.22534124", + "nextFundingTime": 0, + "pair": "LTCUSD", + "symbol": "LTCUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0", + "indexPrice": "0.01555103", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.01549787", + "nextFundingTime": 1694419200000, + "pair": "ZILUSD", + "symbol": "ZILUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1.25837753", + "indexPrice": "1.26388236", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00003978", + "markPrice": "1.26341023", + "nextFundingTime": 1694419200000, + "pair": "OPUSD", + "symbol": "OPUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.13324903", + "indexPrice": "0.13388503", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00011125", + "markPrice": "0.13378063", + "nextFundingTime": 1694419200000, + "pair": "XLMUSD", + "symbol": "XLMUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0", + "indexPrice": "0.01517998", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.01517132", + "nextFundingTime": 1694419200000, + "pair": "VETUSD", + "symbol": "VETUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.24783468", + "indexPrice": "0.24801487", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "0.24844096", + "nextFundingTime": 0, + "pair": "ADAUSD", + "symbol": "ADAUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "15.03458674", + "indexPrice": "15.04121219", + "interestRate": "0.00010000", + "lastFundingRate": "0.00001197", + "markPrice": "15.03200000", + "nextFundingTime": 1694419200000, + "pair": "ETCUSD", + "symbol": "ETCUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "4.21483460", + "indexPrice": "4.22193353", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "4.21825776", + "nextFundingTime": 1694419200000, + "pair": "UNIUSD", + "symbol": "UNIUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "23.34740865", + "indexPrice": "23.38262705", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00013508", + "markPrice": "23.36900000", + "nextFundingTime": 1694419200000, + "pair": "EGLDUSD", + "symbol": "EGLDUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "142.62518042", + "indexPrice": "143.16242243", + "interestRate": "0.00010000", + "lastFundingRate": "0.00009462", + "markPrice": "143.12000000", + "nextFundingTime": 1694419200000, + "pair": "XMRUSD", + "symbol": "XMRUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "61.08722760", + "indexPrice": "61.21094078", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00005797", + "markPrice": "61.18000000", + "nextFundingTime": 1694419200000, + "pair": "LTCUSD", + "symbol": "LTCUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.27873276", + "indexPrice": "0.27952235", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00007630", + "markPrice": "0.27940000", + "nextFundingTime": 1694419200000, + "pair": "MANAUSD", + "symbol": "MANAUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.19218924", + "indexPrice": "0.19249999", + "interestRate": "0.00010000", + "lastFundingRate": "0.00004105", + "markPrice": "0.19240000", + "nextFundingTime": 1694419200000, + "pair": "FTMUSD", + "symbol": "FTMUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.16099506", + "indexPrice": "0.16151141", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.16150000", + "nextFundingTime": 1694419200000, + "pair": "ICXUSD", + "symbol": "ICXUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "4.10673771", + "indexPrice": "4.12005486", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00010056", + "markPrice": "4.11700000", + "nextFundingTime": 1694419200000, + "pair": "DOTUSD", + "symbol": "DOTUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "53.69550437", + "indexPrice": "53.78489019", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00002744", + "markPrice": "53.69982355", + "nextFundingTime": 1694419200000, + "pair": "AAVEUSD", + "symbol": "AAVEUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1610.47089411", + "indexPrice": "1612.50073389", + "interestRate": "0.00010000", + "lastFundingRate": "0.00004075", + "markPrice": "1611.64669108", + "nextFundingTime": 1694419200000, + "pair": "ETHUSD", + "symbol": "ETHUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1.21414412", + "indexPrice": "1.20942310", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "1.20939523", + "nextFundingTime": 1694419200000, + "pair": "APEUSD", + "symbol": "APEUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "189.76387568", + "indexPrice": "190.27248939", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "186.54308604", + "nextFundingTime": 0, + "pair": "BCHUSD", + "symbol": "BCHUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1610.47089411", + "indexPrice": "1612.50073389", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "1630.17757997", + "nextFundingTime": 0, + "pair": "ETHUSD", + "symbol": "ETHUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.24783468", + "indexPrice": "0.24801487", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.24790000", + "nextFundingTime": 1694419200000, + "pair": "ADAUSD", + "symbol": "ADAUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "211.67251057", + "indexPrice": "211.60154982", + "interestRate": "0", + "lastFundingRate": "0", + "markPrice": "211.67497257", + "nextFundingTime": 1694419200000, + "pair": "BNBUSD", + "symbol": "BNBUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "18.27181500", + "indexPrice": "18.35939604", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00051634", + "markPrice": "18.33565485", + "nextFundingTime": 1694419200000, + "pair": "SOLUSD", + "symbol": "SOLUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.52198053", + "indexPrice": "0.52291416", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00022361", + "markPrice": "0.52231323", + "nextFundingTime": 1694419200000, + "pair": "KNCUSD", + "symbol": "KNCUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.24783468", + "indexPrice": "0.24801487", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "0.24992013", + "nextFundingTime": 0, + "pair": "ADAUSD", + "symbol": "ADAUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "7.41016438", + "indexPrice": "7.42285423", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "7.41697383", + "nextFundingTime": 1694419200000, + "pair": "ENSUSD", + "symbol": "ENSUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1610.47089411", + "indexPrice": "1612.50073389", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "1613.47385775", + "nextFundingTime": 0, + "pair": "ETHUSD", + "symbol": "ETHUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.03827183", + "indexPrice": "0.03839887", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.03838000", + "nextFundingTime": 1694419200000, + "pair": "ROSEUSD", + "symbol": "ROSEUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "5.11847228", + "indexPrice": "5.12624707", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00025858", + "markPrice": "5.12474383", + "nextFundingTime": 1694419200000, + "pair": "APTUSD", + "symbol": "APTUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "4.34382753", + "indexPrice": "4.36395226", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00052514", + "markPrice": "4.35735921", + "nextFundingTime": 1694419200000, + "pair": "AXSUSD", + "symbol": "AXSUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1.13703668", + "indexPrice": "1.13905831", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00021479", + "markPrice": "1.13800000", + "nextFundingTime": 1694419200000, + "pair": "NEARUSD", + "symbol": "NEARUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "4.10673771", + "indexPrice": "4.12005486", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "4.08623194", + "nextFundingTime": 0, + "pair": "DOTUSD", + "symbol": "DOTUSD_231229", + "time": 1694401588000 + } + ], + "queryString": "", + "bodyParams": "", + "headers": {} } ] }, @@ -299843,6 +300542,1221 @@ } ] }, + "/fapi/v1/fundingInfo": { + "GET": [ + { + "data": [ + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "BLZUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GTCUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "LPTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "UNFIUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "TRBUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "PERPUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "HIFIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ARKUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "IMXUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "FRONTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "FLMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GLMRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ENJUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BICOUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "OGNUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BNTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "STMXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "FOOTBALLUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DEFIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BTCDOMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BLUEBIRDUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "QNTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ALGOUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RNDRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "FLOWUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "1000XECUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "FXSUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "WOOUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ARUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ASTRUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BATUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LRCUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ENSUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SEIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CVXUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RVNUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ANTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "JASMYUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "AUDIOUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SSVUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ICXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "TUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "1000FLOKIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LUNA2USDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "IOTXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "PENDLEUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BANDUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ONEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "MAGICUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CKBUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SKLUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GALUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DGBUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ACHUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ZENUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "UMAUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "API3USDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "EDUUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "NMRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "JOEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "IDUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LQTYUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "XVSUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RADUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "HFTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RDNTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "NKNUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "OXTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "DODOXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LINAUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "MAVUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BNXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "XVGUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "CYBERUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "YGGUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ARKMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SPELLUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ARPAUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CTKUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "HOOKUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ALICEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "TRUUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "AGLDUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BELUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "COMBOUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "TLMUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BAKEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LEVERUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ATAUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "KEYUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "IDEXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "MDTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "REEFUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LITUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "PHBUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "AMBUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "STORJUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "HBARUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ICPUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GRTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "EGLDUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "STXUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RUNEUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "THETAUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "INJUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SNXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "KAVAUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CHZUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ZECUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "IOTAUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CRVUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "KLAYUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "MINAUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "1000LUNCUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "COMPUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DASHUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GMXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ROSEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "1INCHUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ZILUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "1000PEPEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SFPUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "XEMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "QTUMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "AGIXUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CELOUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "OCEANUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "FETUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ANKRUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GMTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "YFIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "HOTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "WAVESUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BALUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BLURUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "KSMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SXPUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ZRXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SUSHIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ONTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "IOSTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "TOMOUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "KNCUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RSRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "STGUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CTSIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CELRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "MTLUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "C98USDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RLCUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CHRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "HIGHUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ALPHAUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "OMGUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DENTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "COTIUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RENUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DUSKUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "PEOPLEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DARUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "STRAXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "LOOMUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "BIGTIMEUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "BONDUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "ORBSUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "STPTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "WAXPUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "BSVUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "RIFUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "POLYXUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "GASUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "POWRUSDT" + } + ], + "queryString": "", + "bodyParams": "", + "headers": {} + } + ] + }, "/fapi/v1/fundingRate": { "GET": [ { @@ -299869,30 +301783,6 @@ "bodyParams": "", "headers": {} }, - { - "data": [ - { - "fundingRate": "0.00010000", - "fundingTime": 1602979200006, - "symbol": "BTCUSDT" - } - ], - "queryString": "limit=1\u0026symbol=BTCUSDT", - "bodyParams": "", - "headers": {} - }, - { - "data": [ - { - "fundingRate": "0.00010000", - "fundingTime": 1602979200006, - "symbol": "LTCUSDT" - } - ], - "queryString": "endTime=1580515200000\u0026limit=1\u0026startTime=1577836800000\u0026symbol=LTCUSDT", - "bodyParams": "", - "headers": {} - }, { "data": [ { @@ -300882,17 +302772,40 @@ "queryString": "endTime=1580515200000\u0026limit=1000\u0026startTime=1577836800000\u0026symbol=BTCUSDT", "bodyParams": "", "headers": {} - } - ] - }, - "/fapi/v1/historicalTrades": { - "GET": [ + }, { - "data": { - "code": -2014, - "msg": "API-key format invalid." - }, - "queryString": "limit=5\u0026symbol=BTCUSDT", + "data": [ + { + "fundingRate": "0.00010000", + "fundingTime": 1698220800000, + "symbol": "LTCUSDT" + } + ], + "queryString": "endTime=1698719354537\u0026limit=1\u0026startTime=1698200954537\u0026symbol=LTCUSDT", + "bodyParams": "", + "headers": {} + }, + { + "data": [ + { + "fundingRate": "0.00010000", + "fundingTime": 1698710400001, + "symbol": "BTCUSDT" + } + ], + "queryString": "limit=1\u0026symbol=BTCUSDT", + "bodyParams": "", + "headers": {} + }, + { + "data": [ + { + "fundingRate": "0.00010000", + "fundingTime": 1602979200006, + "symbol": "LTCUSDT" + } + ], + "queryString": "endTime=1580515200000\u0026limit=1\u0026startTime=1577836800000\u0026symbol=LTCUSDT", "bodyParams": "", "headers": {} } From 9df23018501a2ed174ef6b7153472863c74c4b75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:11:19 +1100 Subject: [PATCH 22/40] build(deps): bump github.com/mattn/go-sqlite3 from 1.14.17 to 1.14.18 (#1391) Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.17 to 1.14.18. - [Release notes](https://github.com/mattn/go-sqlite3/releases) - [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.17...v1.14.18) --- updated-dependencies: - dependency-name: github.com/mattn/go-sqlite3 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> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 383113a35c7..91e8e25c09b 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 github.com/lib/pq v1.10.9 - github.com/mattn/go-sqlite3 v1.14.17 + github.com/mattn/go-sqlite3 v1.14.18 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 github.com/shopspring/decimal v1.3.1 diff --git a/go.sum b/go.sum index 352fa627856..b898ac45e34 100644 --- a/go.sum +++ b/go.sum @@ -221,8 +221,8 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= +github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= From 7f0ec8e954842555b5f72fd2aac461325a45f77e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:12:37 +1100 Subject: [PATCH 23/40] build(deps): bump golang.org/x/time from 0.3.0 to 0.4.0 (#1387) Bumps [golang.org/x/time](https://github.com/golang/time) from 0.3.0 to 0.4.0. - [Commits](https://github.com/golang/time/compare/v0.3.0...v0.4.0) --- updated-dependencies: - dependency-name: golang.org/x/time 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> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 91e8e25c09b..9c558e56b49 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( golang.org/x/crypto v0.14.0 golang.org/x/net v0.17.0 golang.org/x/text v0.13.0 - golang.org/x/time v0.3.0 + golang.org/x/time v0.4.0 google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 diff --git a/go.sum b/go.sum index b898ac45e34..c8be56d67cf 100644 --- a/go.sum +++ b/go.sum @@ -514,8 +514,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= +golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From f39821d8df1b594943565ab7cfe540a3384a1d62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:16:41 +1100 Subject: [PATCH 24/40] build(deps): bump github.com/gorilla/websocket from 1.5.0 to 1.5.1 (#1388) Bumps [github.com/gorilla/websocket](https://github.com/gorilla/websocket) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/gorilla/websocket/releases) - [Commits](https://github.com/gorilla/websocket/compare/v1.5.0...v1.5.1) --- updated-dependencies: - dependency-name: github.com/gorilla/websocket 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> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9c558e56b49..b60ec174a09 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/d5/tengo/v2 v2.16.1 github.com/gofrs/uuid v4.4.0+incompatible github.com/gorilla/mux v1.8.0 - github.com/gorilla/websocket v1.5.0 + github.com/gorilla/websocket v1.5.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 diff --git a/go.sum b/go.sum index c8be56d67cf..566869bc257 100644 --- a/go.sum +++ b/go.sum @@ -180,8 +180,8 @@ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= From b05e1f2826f007d3daff640a7ed900624bafb751 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:26:08 +1100 Subject: [PATCH 25/40] build(deps): bump github.com/gorilla/mux from 1.8.0 to 1.8.1 (#1390) Bumps [github.com/gorilla/mux](https://github.com/gorilla/mux) from 1.8.0 to 1.8.1. - [Release notes](https://github.com/gorilla/mux/releases) - [Commits](https://github.com/gorilla/mux/compare/v1.8.0...v1.8.1) --- updated-dependencies: - dependency-name: github.com/gorilla/mux 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> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b60ec174a09..138b42996b3 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/buger/jsonparser v1.1.1 github.com/d5/tengo/v2 v2.16.1 github.com/gofrs/uuid v4.4.0+incompatible - github.com/gorilla/mux v1.8.0 + github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 diff --git a/go.sum b/go.sum index 566869bc257..a92aa92c6fd 100644 --- a/go.sum +++ b/go.sum @@ -177,8 +177,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= From 0dd37b255f7cfb6574f5d69d8117c0d1d9d778f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:35:36 +1100 Subject: [PATCH 26/40] build(deps): bump golang.org/x/text from 0.13.0 to 0.14.0 (#1389) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.13.0 to 0.14.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.13.0...v0.14.0) --- updated-dependencies: - dependency-name: golang.org/x/text 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> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 138b42996b3..622f969b203 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/volatiletech/null v8.0.0+incompatible golang.org/x/crypto v0.14.0 golang.org/x/net v0.17.0 - golang.org/x/text v0.13.0 + golang.org/x/text v0.14.0 golang.org/x/time v0.4.0 google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb google.golang.org/grpc v1.59.0 diff --git a/go.sum b/go.sum index a92aa92c6fd..0a1930dd793 100644 --- a/go.sum +++ b/go.sum @@ -509,8 +509,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 0520cd7a6365163da88a9d5278d803aeaa9b1ef0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:49:24 +1100 Subject: [PATCH 27/40] build(deps): bump github.com/grpc-ecosystem/grpc-gateway/v2 (#1396) Bumps [github.com/grpc-ecosystem/grpc-gateway/v2](https://github.com/grpc-ecosystem/grpc-gateway) from 2.18.0 to 2.18.1. - [Release notes](https://github.com/grpc-ecosystem/grpc-gateway/releases) - [Changelog](https://github.com/grpc-ecosystem/grpc-gateway/blob/main/.goreleaser.yml) - [Commits](https://github.com/grpc-ecosystem/grpc-gateway/compare/v2.18.0...v2.18.1) --- 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> --- go.mod | 8 ++++---- go.sum | 25 ++++++++++++------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 622f969b203..0c30f9a0c4d 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.18 @@ -27,7 +27,7 @@ require ( golang.org/x/net v0.17.0 golang.org/x/text v0.14.0 golang.org/x/time v0.4.0 - google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb + google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 ) @@ -60,8 +60,8 @@ require ( golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect + google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0a1930dd793..2eadc9c95a6 100644 --- a/go.sum +++ b/go.sum @@ -17,14 +17,14 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= +cloud.google.com/go v0.110.9 h1:e7ITSqGFFk4rbz/JFIqZh3G4VEHguhAL4BQcFlWtU68= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= +cloud.google.com/go/compute v1.23.2 h1:nWEMDhgbBkBJjfpVySqU4jgWdc22PLR0o4vEexZHers= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= @@ -117,7 +117,6 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -158,7 +157,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -187,8 +186,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDa github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -444,7 +443,7 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -637,12 +636,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA= -google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= -google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= -google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA= +google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 h1:I6WNifs6pF9tNdSob2W24JtyxIYjzFB9qDlpUC76q+U= +google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 h1:AB/lmRny7e2pLhFEYIbl5qkDAUt2h0ZRO4wGPhZf+ik= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= From 9279c7983e6252a3eef060653c49d90c24dcb89b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:58:18 +1100 Subject: [PATCH 28/40] build(deps): bump golang.org/x/net from 0.17.0 to 0.18.0 (#1398) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.17.0 to 0.18.0. - [Commits](https://github.com/golang/net/compare/v0.17.0...v0.18.0) --- updated-dependencies: - dependency-name: golang.org/x/net 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> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 0c30f9a0c4d..b7671d6faf3 100644 --- a/go.mod +++ b/go.mod @@ -23,8 +23,8 @@ require ( github.com/thrasher-corp/sqlboiler v1.0.1-0.20191001234224-71e17f37a85e github.com/urfave/cli/v2 v2.25.7 github.com/volatiletech/null v8.0.0+incompatible - golang.org/x/crypto v0.14.0 - golang.org/x/net v0.17.0 + golang.org/x/crypto v0.15.0 + golang.org/x/net v0.18.0 golang.org/x/text v0.14.0 golang.org/x/time v0.4.0 google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 @@ -58,7 +58,7 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.14.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect diff --git a/go.sum b/go.sum index 2eadc9c95a6..6443002e57c 100644 --- a/go.sum +++ b/go.sum @@ -360,8 +360,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -432,8 +432,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -497,8 +497,8 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 0083ae7c9d801d89534165bb2d466861aa7b9aa4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:00:41 +1100 Subject: [PATCH 29/40] build(deps): bump bufbuild/buf-setup-action from 1.27.2 to 1.28.0 (#1399) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.27.2 to 1.28.0. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.27.2...v1.28.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> --- .github/workflows/proto-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/proto-lint.yml b/.github/workflows/proto-lint.yml index 33bfed5a2c0..9be0029a6cf 100644 --- a/.github/workflows/proto-lint.yml +++ b/.github/workflows/proto-lint.yml @@ -23,7 +23,7 @@ jobs: go install google.golang.org/protobuf/cmd/protoc-gen-go go install google.golang.org/grpc/cmd/protoc-gen-go-grpc - - uses: bufbuild/buf-setup-action@v1.27.2 + - uses: bufbuild/buf-setup-action@v1.28.0 - name: buf generate working-directory: ./gctrpc From 340558b8eac2abac95be0bf414c10710abbbe1c2 Mon Sep 17 00:00:00 2001 From: Bea <103050835+Beadko@users.noreply.github.com> Date: Tue, 14 Nov 2023 07:25:10 +0700 Subject: [PATCH 30/40] Bitfinex: replace errTypeAssert with common.GetTypeAssertError (#1386) * Bitfinex: Replace errTypeAssert with common.GetTypeAssertError * Bitfinex/common.GetTypeAssertError description to match the documentation * Bitfinex/Correct typos, fix type in finalResp error * Bitfinex: Comments amended from.Symbol to .Data * Bitfinex: fix the typo * Bitfinex: fix the comments --- exchanges/bitfinex/bitfinex.go | 267 +++++++++++++++------------------ 1 file changed, 124 insertions(+), 143 deletions(-) diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index da1658b8c5e..a3d092ddcb2 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -136,29 +136,24 @@ func (b *Bitfinex) GetPlatformStatus(ctx context.Context) (int, error) { func baseMarginInfo(data []interface{}) (MarginInfoV2, error) { var resp MarginInfoV2 - tempData, ok := data[1].([]interface{}) + marginInfo, ok := data[1].([]any) if !ok { - return resp, fmt.Errorf("%w", errTypeAssert) + return resp, common.GetTypeAssertError("[]any", data[1], "MarginInfo") } - resp.UserPNL, ok = tempData[0].(float64) - if !ok { - return resp, fmt.Errorf("%w for UserPNL", errTypeAssert) + if resp.UserPNL, ok = marginInfo[0].(float64); !ok { + return resp, common.GetTypeAssertError("float64", marginInfo[0], "UserPNL") } - resp.UserSwaps, ok = tempData[1].(float64) - if !ok { - return resp, fmt.Errorf("%w for UserSwaps", errTypeAssert) + if resp.UserSwaps, ok = marginInfo[1].(float64); !ok { + return resp, common.GetTypeAssertError("float64", marginInfo[1], "UserSwaps") } - resp.MarginBalance, ok = tempData[2].(float64) - if !ok { - return resp, fmt.Errorf("%w for MarginBalance", errTypeAssert) + if resp.MarginBalance, ok = marginInfo[2].(float64); !ok { + return resp, common.GetTypeAssertError("float64", marginInfo[2], "MarginBalance") } - resp.MarginNet, ok = tempData[3].(float64) - if !ok { - return resp, fmt.Errorf("%w for MarginNet", errTypeAssert) + if resp.MarginNet, ok = marginInfo[3].(float64); !ok { + return resp, common.GetTypeAssertError("float64", marginInfo[3], "MarginNet") } - resp.MarginMin, ok = tempData[4].(float64) - if !ok { - return resp, fmt.Errorf("%w for MarginMin", errTypeAssert) + if resp.MarginMin, ok = marginInfo[4].(float64); !ok { + return resp, common.GetTypeAssertError("float64", marginInfo[4], "MarginMin") } return resp, nil } @@ -167,37 +162,32 @@ func symbolMarginInfo(data []interface{}) ([]MarginInfoV2, error) { resp := make([]MarginInfoV2, len(data)) for x := range data { var tempResp MarginInfoV2 - tempData, ok := data[x].([]interface{}) + marginInfo, ok := data[x].([]any) if !ok { - return nil, fmt.Errorf("%w for all sym", errTypeAssert) + return nil, common.GetTypeAssertError("[]any", data[x], "MarginInfo") } var check bool - tempResp.Symbol, check = tempData[1].(string) - if !check { - return nil, fmt.Errorf("%w for symbol data", errTypeAssert) + if tempResp.Symbol, check = marginInfo[1].(string); !check { + return nil, common.GetTypeAssertError("string", marginInfo[1], "Symbol") } - tempFloatData, check := tempData[2].([]interface{}) + pairMarginInfo, check := marginInfo[2].([]any) if !check { - return nil, fmt.Errorf("%w for symbol data", errTypeAssert) + return nil, common.GetTypeAssertError("[]any", marginInfo[2], "MarginInfo.Data") } - if len(tempFloatData) < 4 { + if len(pairMarginInfo) < 4 { return nil, errors.New("invalid data received") } - tempResp.TradableBalance, ok = tempFloatData[0].(float64) - if !ok { - return nil, fmt.Errorf("%w for TradableBalance", errTypeAssert) + if tempResp.TradableBalance, ok = pairMarginInfo[0].(float64); !ok { + return nil, common.GetTypeAssertError("float64", pairMarginInfo[0], "MarginInfo.Data.TradableBalance") } - tempResp.GrossBalance, ok = tempFloatData[1].(float64) - if !ok { - return nil, fmt.Errorf("%w for GrossBalance", errTypeAssert) + if tempResp.GrossBalance, ok = pairMarginInfo[1].(float64); !ok { + return nil, common.GetTypeAssertError("float64", pairMarginInfo[1], "MarginInfo.Data.GlossBalance") } - tempResp.BestAskAmount, ok = tempFloatData[2].(float64) - if !ok { - return nil, fmt.Errorf("%w for BestAskAmount", errTypeAssert) + if tempResp.BestAskAmount, ok = pairMarginInfo[2].(float64); !ok { + return nil, common.GetTypeAssertError("float64", pairMarginInfo[2], "MarginInfo.Data.BestAskAmount") } - tempResp.BestBidAmount, ok = tempFloatData[3].(float64) - if !ok { - return nil, fmt.Errorf("%w for BestBidAmount", errTypeAssert) + if tempResp.BestBidAmount, ok = pairMarginInfo[3].(float64); !ok { + return nil, common.GetTypeAssertError("float64", pairMarginInfo[3], "MarginInfo.Data.BestBidAmount") } resp[x] = tempResp } @@ -207,32 +197,27 @@ func symbolMarginInfo(data []interface{}) ([]MarginInfoV2, error) { func defaultMarginV2Info(data []interface{}) (MarginInfoV2, error) { var resp MarginInfoV2 var ok bool - resp.Symbol, ok = data[1].(string) - if !ok { - return resp, fmt.Errorf("%w for symbol", errTypeAssert) + if resp.Symbol, ok = data[1].(string); !ok { + return resp, common.GetTypeAssertError("string", data[1], "Symbol") } - tempData, check := data[2].([]interface{}) + marginInfo, check := data[2].([]any) if !check { - return resp, fmt.Errorf("%w for symbol data", errTypeAssert) + return resp, common.GetTypeAssertError("[]any", data[2], "MarginInfo.Data") } - if len(tempData) < 4 { + if len(marginInfo) < 4 { return resp, errors.New("invalid data received") } - resp.TradableBalance, ok = tempData[0].(float64) - if !ok { - return resp, fmt.Errorf("%w for TradableBalance", errTypeAssert) + if resp.TradableBalance, ok = marginInfo[0].(float64); !ok { + return resp, common.GetTypeAssertError("float64", marginInfo[0], "MarginInfo.Data.TradableBalance") } - resp.GrossBalance, ok = tempData[1].(float64) - if !ok { - return resp, fmt.Errorf("%w for GrossBalance", errTypeAssert) + if resp.GrossBalance, ok = marginInfo[1].(float64); !ok { + return resp, common.GetTypeAssertError("float64", marginInfo[1], "MarginInfo.Data.GrossBalance") } - resp.BestAskAmount, ok = tempData[2].(float64) - if !ok { - return resp, fmt.Errorf("%w for BestAskAmount", errTypeAssert) + if resp.BestAskAmount, ok = marginInfo[2].(float64); !ok { + return resp, common.GetTypeAssertError("float64", marginInfo[2], "MarginInfo.Data.BestAskAmount") } - resp.BestBidAmount, ok = tempData[3].(float64) - if !ok { - return resp, fmt.Errorf("%w for BestBidAmount", errTypeAssert) + if resp.BestBidAmount, ok = marginInfo[3].(float64); !ok { + return resp, common.GetTypeAssertError("float64", marginInfo[3], "MarginInfo.Data.BestBidAmount") } return resp, nil } @@ -291,11 +276,11 @@ func (b *Bitfinex) GetV2MarginFunding(ctx context.Context, symbol, amount string } avgRate, ok := resp[0].(float64) if !ok { - return response, fmt.Errorf("%v - %v: %w for rate", b.Name, symbol, errTypeAssert) + return response, common.GetTypeAssertError("float64", resp[0], "MarketAveragePrice.PriceOrRate") } avgAmount, ok := resp[1].(float64) if !ok { - return response, fmt.Errorf("%v - %v: %w for amount", b.Name, symbol, errTypeAssert) + return response, common.GetTypeAssertError("float64", resp[1], "MarketAveragePrice.Amount") } response.Symbol = symbol response.RateAverage = avgRate @@ -320,15 +305,15 @@ func (b *Bitfinex) GetV2FundingInfo(ctx context.Context, key string) (MarginFund } sym, ok := resp[0].(string) if !ok { - return response, fmt.Errorf("%v GetV2FundingInfo: %w for sym", b.Name, errTypeAssert) + return response, common.GetTypeAssertError("string", resp[0], "FundingInfo.sym") } symbol, ok := resp[1].(string) if !ok { - return response, fmt.Errorf("%v GetV2FundingInfo: %w for symbol", b.Name, errTypeAssert) + return response, common.GetTypeAssertError("string", resp[1], "FundingInfo.Symbol") } - fundingData, ok := resp[2].([]interface{}) + fundingData, ok := resp[2].([]any) if !ok { - return response, fmt.Errorf("%v GetV2FundingInfo: %w for fundingData", b.Name, errTypeAssert) + return response, common.GetTypeAssertError("[]any", resp[2], "FundingInfo.FundingRateOrDuration") } response.Sym = sym response.Symbol = symbol @@ -369,27 +354,27 @@ func (b *Bitfinex) GetAccountInfoV2(ctx context.Context) (AccountV2Data, error) var tempString string var tempFloat float64 if tempFloat, ok = data[0].(float64); !ok { - return resp, fmt.Errorf("%v GetAccountInfoV2: %w for id", b.Name, errTypeAssert) + return resp, common.GetTypeAssertError("float64", data[0], "AccountInfo.AccountID") } resp.ID = int64(tempFloat) if tempString, ok = data[1].(string); !ok { - return resp, fmt.Errorf("%v GetAccountInfoV2: %w for email", b.Name, errTypeAssert) + return resp, common.GetTypeAssertError("string", data[1], "AccountInfo.AccountEmail") } resp.Email = tempString if tempString, ok = data[2].(string); !ok { - return resp, fmt.Errorf("%v GetAccountInfoV2: %w for username", b.Name, errTypeAssert) + return resp, common.GetTypeAssertError("string", data[2], "AccountInfo.AccountUsername") } resp.Username = tempString if tempFloat, ok = data[3].(float64); !ok { - return resp, fmt.Errorf("%v GetAccountInfoV2: %w for accountcreate", b.Name, errTypeAssert) + return resp, common.GetTypeAssertError("float64", data[3], "AccountInfo.Account.MTSAccountCreate") } resp.MTSAccountCreate = int64(tempFloat) if tempFloat, ok = data[4].(float64); !ok { - return resp, fmt.Errorf("%v GetAccountInfoV2: %w failed for verified", b.Name, errTypeAssert) + return resp, common.GetTypeAssertError("float64", data[4], "AccountInfo.AccountVerified") } resp.Verified = int64(tempFloat) if tempString, ok = data[7].(string); !ok { - return resp, fmt.Errorf("%v GetAccountInfoV2: %w for timezone", b.Name, errTypeAssert) + return resp, common.GetTypeAssertError("string", data[7], "AccountInfo.AccountTimezone") } resp.Timezone = tempString return resp, nil @@ -409,26 +394,26 @@ func (b *Bitfinex) GetV2Balances(ctx context.Context) ([]WalletDataV2, error) { } resp := make([]WalletDataV2, len(data)) for x := range data { - wType, ok := data[x][0].(string) + walletType, ok := data[x][0].(string) if !ok { - return resp, fmt.Errorf("%v GetV2Balances: %w for walletType", b.Name, errTypeAssert) + return resp, common.GetTypeAssertError("string", data[x][0], "Wallets.WalletType") } - curr, ok := data[x][1].(string) + currency, ok := data[x][1].(string) if !ok { - return resp, fmt.Errorf("%v GetV2Balances: %w for currency", b.Name, errTypeAssert) + return resp, common.GetTypeAssertError("string", data[x][1], "Wallets.Currency") } - bal, ok := data[x][2].(float64) + balance, ok := data[x][2].(float64) if !ok { - return resp, fmt.Errorf("%v GetV2Balances: %w for balance", b.Name, errTypeAssert) + return resp, common.GetTypeAssertError("float64", data[x][2], "Wallets.WalletBalance") } unsettledInterest, ok := data[x][3].(float64) if !ok { - return resp, fmt.Errorf("%v GetV2Balances: %w for unsettledInterest", b.Name, errTypeAssert) + return resp, common.GetTypeAssertError("float64", data[x][3], "Wallets.UnsettledInterest") } resp[x] = WalletDataV2{ - WalletType: wType, - Currency: curr, - Balance: bal, + WalletType: walletType, + Currency: currency, + Balance: balance, UnsettledInterest: unsettledInterest, } } @@ -596,34 +581,34 @@ func (b *Bitfinex) GetDerivativeStatusInfo(ctx context.Context, keys, startTime, var response DerivativeDataResponse var ok bool if response.Key, ok = result[z][0].(string); !ok { - return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for Key", b.Name, errTypeAssert) + return finalResp, common.GetTypeAssertError("string", result[z][0], "DerivativesStatus.Key") } if response.MTS, ok = result[z][1].(float64); !ok { - return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for MTS", b.Name, errTypeAssert) + return finalResp, common.GetTypeAssertError("float64", result[z][1], "DerivativesStatus.MTS") } if response.DerivPrice, ok = result[z][3].(float64); !ok { - return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for DerivPrice", b.Name, errTypeAssert) + return finalResp, common.GetTypeAssertError("float64", result[z][3], "DerivativesStatus.DerivPrice") } if response.SpotPrice, ok = result[z][4].(float64); !ok { - return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for SpotPrice", b.Name, errTypeAssert) + return finalResp, common.GetTypeAssertError("float64", result[z][4], "DerivativesStatus.SpotPrice") } if response.InsuranceFundBalance, ok = result[z][6].(float64); !ok { - return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for Insurance fund balance", b.Name, errTypeAssert) + return finalResp, common.GetTypeAssertError("float64", result[z][6], "DerivativesStatus.InsuranceFundBalance") } if response.NextFundingEventTS, ok = result[z][8].(float64); !ok { - return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for NextFundingEventTS", b.Name, errTypeAssert) + return finalResp, common.GetTypeAssertError("float64", result[z][8], "DerivativesStatus.NextFundingEventMTS") } if response.NextFundingAccrued, ok = result[z][9].(float64); !ok { - return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for NextFundingAccrued", b.Name, errTypeAssert) + return finalResp, common.GetTypeAssertError("float64", result[z][9], "DerivativesStatus.NextFundingAccrued") } if response.NextFundingStep, ok = result[z][10].(float64); !ok { - return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for NextFundingStep", b.Name, errTypeAssert) + return finalResp, common.GetTypeAssertError("float64", result[z][10], "DerivativesStatus.NextFundingStep") } if response.CurrentFunding, ok = result[z][12].(float64); !ok { - return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for CurrentFunding", b.Name, errTypeAssert) + return finalResp, common.GetTypeAssertError("float64", result[z][12], "DerivativesStatus.CurrentFunding") } if response.MarkPrice, ok = result[z][15].(float64); !ok { - return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for MarkPrice", b.Name, errTypeAssert) + return finalResp, common.GetTypeAssertError("float64", result[z][15], "DerivativesStatus.MarkPrice") } switch t := result[z][18].(type) { @@ -632,11 +617,7 @@ func (b *Bitfinex) GetDerivativeStatusInfo(ctx context.Context, keys, startTime, case nil: break // OpenInterest will default to 0 default: - return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for OpenInterest. Type received: %v", - b.Name, - errTypeAssert, - t, - ) + return finalResp, common.GetTypeAssertError(" float64|nil", t, "DerivativesStatus.OpenInterest") } finalResp[z] = response } @@ -659,58 +640,58 @@ func (b *Bitfinex) GetTickerBatch(ctx context.Context) (map[string]Ticker, error for x := range response { symbol, ok := response[x][0].(string) if !ok { - return nil, common.GetTypeAssertError("string", response[x][0], "symbol") + return nil, common.GetTypeAssertError("string", response[x][0], "Ticker.Symbol") } var t Ticker if len(response[x]) > 11 { if t.FlashReturnRate, ok = response[x][1].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][1], "FlashReturnRate") + return nil, common.GetTypeAssertError("float64", response[x][1], "Ticker.Data.FlashReturnRate") } if t.Bid, ok = response[x][2].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][2], "bid") + return nil, common.GetTypeAssertError("float64", response[x][2], "Ticker.Data.Bid") } var bidPeriod float64 bidPeriod, ok = response[x][3].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", response[x][3], "bidPeriod") + return nil, common.GetTypeAssertError("float64", response[x][3], "Ticker.Data.BidPeriod") } t.BidPeriod = int64(bidPeriod) if t.BidSize, ok = response[x][4].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][4], "bidSize") + return nil, common.GetTypeAssertError("float64", response[x][4], "Ticker.Data.BidSize") } if t.Ask, ok = response[x][5].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][5], "ask") + return nil, common.GetTypeAssertError("float64", response[x][5], "Ticker.Data.Ask") } var askPeriod float64 askPeriod, ok = response[x][6].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", response[x][6], "askPeriod") + return nil, common.GetTypeAssertError("float64", response[x][6], "Ticker.Data.AskPeriod") } t.AskPeriod = int64(askPeriod) if t.AskSize, ok = response[x][7].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][7], "askSize") + return nil, common.GetTypeAssertError("float64", response[x][7], "Ticker.Data.AskSize") } if t.DailyChange, ok = response[x][8].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][8], "dailyChange") + return nil, common.GetTypeAssertError("float64", response[x][8], "Ticker.Data.DailyChange") } if t.DailyChangePerc, ok = response[x][9].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][9], "dailyChangePerc") + return nil, common.GetTypeAssertError("float64", response[x][9], "Ticker.Data.DailyChangePercentage") } if t.Last, ok = response[x][10].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][10], "last") + return nil, common.GetTypeAssertError("float64", response[x][10], "Ticker.Data.LastPrice") } if t.Volume, ok = response[x][11].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][11], "volume") + return nil, common.GetTypeAssertError("float64", response[x][11], "Ticker.Data.DailyVolume") } if t.High, ok = response[x][12].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][12], "high") + return nil, common.GetTypeAssertError("float64", response[x][12], "Ticker.Data.DailyHigh") } if t.Low, ok = response[x][13].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][13], "low") + return nil, common.GetTypeAssertError("float64", response[x][13], "Ticker.Data.DailyLow") } if t.FFRAmountAvailable, ok = response[x][16].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][16], "FFRAmountAvailable") + return nil, common.GetTypeAssertError("float64", response[x][16], "Ticker.Data.FFRAmountAvailable") } tickers[symbol] = t @@ -718,34 +699,34 @@ func (b *Bitfinex) GetTickerBatch(ctx context.Context) (map[string]Ticker, error } if t.Bid, ok = response[x][1].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][1], "bid") + return nil, common.GetTypeAssertError("float64", response[x][1], "Tickers.Symbol.Bid") } if t.BidSize, ok = response[x][2].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][2], "bid size") + return nil, common.GetTypeAssertError("float64", response[x][2], "Tickers.Symbol.BidSize") } if t.Ask, ok = response[x][3].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][3], "ask") + return nil, common.GetTypeAssertError("float64", response[x][3], "Tickers.Symbol.Ask") } if t.AskSize, ok = response[x][4].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][4], "ask size") + return nil, common.GetTypeAssertError("float64", response[x][4], "Tickers.Symbol.AskSize") } if t.DailyChange, ok = response[x][5].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][5], "daily change") + return nil, common.GetTypeAssertError("float64", response[x][5], "Tickers.Symbol.DailyChange") } if t.DailyChangePerc, ok = response[x][6].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][6], "daily change percent") + return nil, common.GetTypeAssertError("float64", response[x][6], "Tickers.Symbol.DailyChangeRelative") } if t.Last, ok = response[x][7].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][7], "last") + return nil, common.GetTypeAssertError("float64", response[x][7], "Tickers.Symbol.LastPrice") } if t.Volume, ok = response[x][8].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][8], "volume") + return nil, common.GetTypeAssertError("float64", response[x][8], "Tickers.Symbol.DailyVolume") } if t.High, ok = response[x][9].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][9], "high") + return nil, common.GetTypeAssertError("float64", response[x][9], "Tickers.Symbol.DailyHigh") } if t.Low, ok = response[x][10].(float64); !ok { - return nil, common.GetTypeAssertError("float64", response[x][10], "low") + return nil, common.GetTypeAssertError("float64", response[x][10], "Tickers.Symbol.DailyLow") } tickers[symbol] = t } @@ -1704,97 +1685,97 @@ func (b *Bitfinex) CancelMultipleOrdersV2(ctx context.Context, orderID, clientOr case 0: f, ok := cancelledOrderFields[z].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "ID") + return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.OrderID") } cancelledOrder.OrderID = strconv.FormatFloat(f, 'f', -1, 64) case 1: f, ok := cancelledOrderFields[z].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "GID") + return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.GroupOrderID") } cancelledOrder.GroupOrderID = strconv.FormatFloat(f, 'f', -1, 64) case 2: f, ok := cancelledOrderFields[z].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CID") + return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.ClientOrderID") } cancelledOrder.ClientOrderID = strconv.FormatFloat(f, 'f', -1, 64) case 3: f, ok := cancelledOrderFields[z].(string) if !ok { - return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "SYMBOL") + return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "CancelOrders.Symbol") } cancelledOrder.Symbol = f case 4: f, ok := cancelledOrderFields[z].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "MTS_CREATE") + return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.MTSOfCreation") } cancelledOrder.CreatedTime = time.UnixMilli(int64(f)) case 5: f, ok := cancelledOrderFields[z].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "MTS_UPDATE") + return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.MTSOfLastUpdate") } cancelledOrder.UpdatedTime = time.UnixMilli(int64(f)) case 6: f, ok := cancelledOrderFields[z].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "AMOUNT") + return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.Amount") } cancelledOrder.Amount = f case 7: f, ok := cancelledOrderFields[z].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "AMOUNT_ORIG") + return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.OriginalAmount") } cancelledOrder.OriginalAmount = f case 8: f, ok := cancelledOrderFields[z].(string) if !ok { - return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "TYPE") + return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "CancelOrders.OrderType") } cancelledOrder.OrderType = f case 9: f, ok := cancelledOrderFields[z].(string) if !ok { - return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "TYPE_PREV") + return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "CancelOrders.PreviousOrderType") } cancelledOrder.OriginalOrderType = f case 12: f, ok := cancelledOrderFields[z].(string) if !ok { - return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "FLAGS") + return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "CancelOrders.SumOfOrderFlags") } cancelledOrder.OrderFlags = f case 13: f, ok := cancelledOrderFields[z].(string) if !ok { - return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "ORDER_STATUS") + return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "CancelOrders.OrderStatuses") } cancelledOrder.OrderStatus = f case 16: f, ok := cancelledOrderFields[z].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "PRICE") + return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.Price") } cancelledOrder.Price = f case 17: f, ok := cancelledOrderFields[z].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "PRICE_AVG") + return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.AveragePrice") } cancelledOrder.AveragePrice = f case 18: f, ok := cancelledOrderFields[z].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "PRICE_TRAILING") + return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.TrailingPrice") } cancelledOrder.TrailingPrice = f case 19: f, ok := cancelledOrderFields[z].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "PRICE_AUX_LIMIT") + return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.AuxiliaryLimitPrice") } cancelledOrder.AuxLimitPrice = f } @@ -1976,53 +1957,53 @@ func (b *Bitfinex) GetMovementHistory(ctx context.Context, symbol, method string var id float64 id, ok = response[i][j].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", response[i][j], "ID") + return nil, common.GetTypeAssertError("float64", response[i][j], "Movements.Id") } move.ID = int64(id) case 1: move.Currency, ok = response[i][j].(string) if !ok { - return nil, common.GetTypeAssertError("string", response[i][j], "CURRENCY") + return nil, common.GetTypeAssertError("string", response[i][j], "Movements.Currency") } case 5: move.TimestampCreated, ok = response[i][j].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", response[i][j], "MTS_STARTED") + return nil, common.GetTypeAssertError("float64", response[i][j], "Movements.MovementStartedAt") } case 6: move.Timestamp, ok = response[i][j].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", response[i][j], "MTS_UPDATED") + return nil, common.GetTypeAssertError("float64", response[i][j], "Movements.MovementLastUpdated") } case 9: move.Status, ok = response[i][j].(string) if !ok { - return nil, common.GetTypeAssertError("string", response[i][j], "STATUS") + return nil, common.GetTypeAssertError("string", response[i][j], "Movements.CurrentStatus") } case 12: move.Amount, ok = response[i][j].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", response[i][j], "AMOUNT") + return nil, common.GetTypeAssertError("float64", response[i][j], "Movements.AmountOfFundsMoved") } case 13: move.Fee, ok = response[i][j].(float64) if !ok { - return nil, common.GetTypeAssertError("float64", response[i][j], "FEE") + return nil, common.GetTypeAssertError("float64", response[i][j], "Movements.FeesApplied") } case 16: move.Address, ok = response[i][j].(string) if !ok { - return nil, common.GetTypeAssertError("string", response[i][j], "DESTINATION_ADDRESS") + return nil, common.GetTypeAssertError("string", response[i][j], "Movements.DestinationAddress") } case 20: move.TxID, ok = response[i][j].(string) if !ok { - return nil, common.GetTypeAssertError("string", response[i][j], "TRANSACTION_ID") + return nil, common.GetTypeAssertError("string", response[i][j], "Movements.TransactionId") } case 21: move.Description, ok = response[i][j].(string) if !ok { - return nil, common.GetTypeAssertError("string", response[i][j], "WITHDRAW_TRANSACTION_NOTE") + return nil, common.GetTypeAssertError("string", response[i][j], "Movements.WithdrawTransactionNote") } } } From a13a6c3248cd0672507c2bfb4767bbcfae40cf31 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Tue, 14 Nov 2023 11:34:38 +1100 Subject: [PATCH 31/40] Kucoin: Add order execution limits (#1369) * cmd/exchange_template: Add wrapper function * exchanges: force UpdateOrderExecutionLimits to wrappers as this is important for order deployment * kucoin: Add spot order execution limits * linter: fix * glorious: nits * kucoin: change from name to symbol for correct fee fetching and order deployment * WHAAAAAAAT says Vitalik * kucoin: Add futures limit support, fix insertion of non margin tradable pairs, update naming and add comments. * kucoin: implement master branch changes * Update exchanges/kucoin/kucoin_test.go Co-authored-by: Scott * kucoin/test: update commentary --------- Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott --- cmd/exchange_template/wrapper_file.tmpl | 4 + config/config.go | 1 - exchanges/alphapoint/alphapoint_wrapper.go | 5 ++ exchanges/binanceus/binanceus_wrapper.go | 5 ++ exchanges/bitflyer/bitflyer_wrapper.go | 5 ++ exchanges/bitmex/bitmex_wrapper.go | 5 ++ exchanges/bittrex/bittrex_wrapper.go | 5 ++ exchanges/btse/btse_wrapper.go | 5 ++ exchanges/coinbasepro/coinbasepro_wrapper.go | 5 ++ exchanges/coinut/coinut_wrapper.go | 5 ++ exchanges/exchange.go | 5 -- exchanges/exchange_test.go | 8 -- exchanges/exmo/exmo_wrapper.go | 5 ++ exchanges/hitbtc/hitbtc_wrapper.go | 5 ++ exchanges/huobi/huobi_wrapper.go | 5 ++ exchanges/itbit/itbit_wrapper.go | 5 ++ exchanges/kucoin/kucoin.go | 7 +- exchanges/kucoin/kucoin_test.go | 37 ++++++++- exchanges/kucoin/kucoin_types.go | 1 - exchanges/kucoin/kucoin_wrapper.go | 81 +++++++++++++++++++- exchanges/lbank/lbank_wrapper.go | 5 ++ exchanges/poloniex/poloniex_wrapper.go | 5 ++ exchanges/yobit/yobit_wrapper.go | 5 ++ exchanges/zb/zb_wrapper.go | 5 ++ 24 files changed, 202 insertions(+), 22 deletions(-) diff --git a/cmd/exchange_template/wrapper_file.tmpl b/cmd/exchange_template/wrapper_file.tmpl index 20cd464cf7b..5e2a6b42c33 100644 --- a/cmd/exchange_template/wrapper_file.tmpl +++ b/cmd/exchange_template/wrapper_file.tmpl @@ -553,5 +553,9 @@ func ({{.Variable}} *{{.CapitalName}}) GetLatestFundingRates(_ context.Context, return nil, common.ErrNotYetImplemented } +// UpdateOrderExecutionLimits updates order execution limits +func ({{.Variable}} *{{.CapitalName}}) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} {{end}} diff --git a/config/config.go b/config/config.go index 4b7b5827dcd..61a34401910 100644 --- a/config/config.go +++ b/config/config.go @@ -1817,7 +1817,6 @@ func (c *Config) LoadConfig(configPath string, dryrun bool) error { if err != nil { return fmt.Errorf(ErrFailureOpeningConfig, configPath, err) } - return c.CheckConfig() } diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index 604afc0bd7c..946030e0875 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -488,3 +488,8 @@ func (a *Alphapoint) GetFuturesContractDetails(context.Context, asset.Item) ([]f func (a *Alphapoint) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (a *Alphapoint) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/binanceus/binanceus_wrapper.go b/exchanges/binanceus/binanceus_wrapper.go index 4dd257791bd..72f8e0c17e2 100644 --- a/exchanges/binanceus/binanceus_wrapper.go +++ b/exchanges/binanceus/binanceus_wrapper.go @@ -998,3 +998,8 @@ func (bi *Binanceus) GetFuturesContractDetails(context.Context, asset.Item) ([]f func (bi *Binanceus) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (bi *Binanceus) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/bitflyer/bitflyer_wrapper.go b/exchanges/bitflyer/bitflyer_wrapper.go index e93f8c7e664..919380653cc 100644 --- a/exchanges/bitflyer/bitflyer_wrapper.go +++ b/exchanges/bitflyer/bitflyer_wrapper.go @@ -510,3 +510,8 @@ func (b *Bitflyer) GetFuturesContractDetails(context.Context, asset.Item) ([]fut func (b *Bitflyer) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (b *Bitflyer) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 95819f57b27..19de9175d2c 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -1332,3 +1332,8 @@ func (b *Bitmex) GetLatestFundingRates(ctx context.Context, r *fundingrate.Lates func (b *Bitmex) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) { return a == asset.PerpetualContract, nil } + +// UpdateOrderExecutionLimits updates order execution limits +func (b *Bitmex) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index 36f7623ac7f..ea62bc2d3cd 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -1101,3 +1101,8 @@ func (b *Bittrex) GetFuturesContractDetails(context.Context, asset.Item) ([]futu func (b *Bittrex) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (b *Bittrex) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index 677374f8a1b..46e378175ea 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -1291,3 +1291,8 @@ func (b *BTSE) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestR func (b *BTSE) IsPerpetualFutureCurrency(a asset.Item, p currency.Pair) (bool, error) { return a == asset.Futures && p.Quote.Equal(currency.PFC), nil } + +// UpdateOrderExecutionLimits updates order execution limits +func (b *BTSE) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index 3aa525b5d2b..eb93463ea15 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -991,3 +991,8 @@ func (c *CoinbasePro) GetLatestFundingRates(context.Context, *fundingrate.Latest func (c *CoinbasePro) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (c *CoinbasePro) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 228a1845eea..f5261333f4f 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -1205,3 +1205,8 @@ func (c *COINUT) GetFuturesContractDetails(context.Context, asset.Item) ([]futur func (c *COINUT) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (c *COINUT) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/exchange.go b/exchanges/exchange.go index b617efb9b55..6804b6aa30f 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -1417,11 +1417,6 @@ func getURLTypeFromString(ep string) (URL, error) { } } -// UpdateOrderExecutionLimits updates order execution limits this is overridable -func (b *Base) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { - return common.ErrNotYetImplemented -} - // DisableAssetWebsocketSupport disables websocket functionality for the // supplied asset item. In the case that websocket functionality has not yet // been implemented for that specific asset type. This is a base method to diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index 23603fe611d..8be48ebe182 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -2453,14 +2453,6 @@ func TestUpdateCurrencyStates(t *testing.T) { } } -func TestUpdateOrderExecutionLimits(t *testing.T) { - t.Parallel() - var b Base - if err := b.UpdateOrderExecutionLimits(context.Background(), asset.Spot); !errors.Is(err, common.ErrNotYetImplemented) { - t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented) - } -} - func TestSetTradeFeedStatus(t *testing.T) { t.Parallel() b := Base{ diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index 91d29ec7e05..e052d554a5a 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -840,3 +840,8 @@ func (e *EXMO) GetFuturesContractDetails(context.Context, asset.Item) ([]futures func (e *EXMO) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (e *EXMO) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index cdb52bd3c63..8b13bdbb291 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -999,3 +999,8 @@ func (h *HitBTC) GetFuturesContractDetails(context.Context, asset.Item) ([]futur func (h *HitBTC) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (h *HitBTC) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 110098752a4..fd614c42983 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -2299,3 +2299,8 @@ func (h *HUOBI) GetLatestFundingRates(ctx context.Context, r *fundingrate.Latest func (h *HUOBI) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) { return a == asset.CoinMarginedFutures, nil } + +// UpdateOrderExecutionLimits updates order execution limits +func (h *HUOBI) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index fbcc7fc554f..62181469443 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -704,3 +704,8 @@ func (i *ItBit) GetFuturesContractDetails(context.Context, asset.Item) ([]future func (i *ItBit) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (i *ItBit) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/kucoin/kucoin.go b/exchanges/kucoin/kucoin.go index 0995e854ea9..52cdc3db016 100644 --- a/exchanges/kucoin/kucoin.go +++ b/exchanges/kucoin/kucoin.go @@ -138,10 +138,11 @@ const ( ) // GetSymbols gets pairs details on the exchange -func (ku *Kucoin) GetSymbols(ctx context.Context, ccy string) ([]SymbolInfo, error) { +// For market details see endpoint: https://www.kucoin.com/docs/rest/spot-trading/market-data/get-market-list +func (ku *Kucoin) GetSymbols(ctx context.Context, market string) ([]SymbolInfo, error) { params := url.Values{} - if ccy != "" { - params.Set("market", ccy) + if market != "" { + params.Set("market", market) } var resp []SymbolInfo return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, common.EncodeURLValues(kucoinGetSymbols, params), &resp) diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index a8ddc4cc63b..625e755c741 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -81,14 +81,17 @@ func TestMain(m *testing.M) { // Spot asset test cases starts from here func TestGetSymbols(t *testing.T) { t.Parallel() - _, err := ku.GetSymbols(context.Background(), "") + symbols, err := ku.GetSymbols(context.Background(), "") if err != nil { t.Error("GetSymbols() error", err) } - _, err = ku.GetSymbols(context.Background(), currency.BTC.String()) + assert.NotEmpty(t, symbols, "should return all available spot/margin symbols") + // Using market string reduces the scope of what is returned. + symbols, err = ku.GetSymbols(context.Background(), "ETF") if err != nil { t.Error("GetSymbols() error", err) } + assert.NotEmpty(t, symbols, "should return all available ETF symbols") } func TestGetTicker(t *testing.T) { @@ -2558,3 +2561,33 @@ func TestGetFuturesPositionOrders(t *testing.T) { _, err = ku.GetFuturesPositionOrders(context.Background(), req) assert.ErrorIs(t, err, nil) } + +func TestUpdateOrderExecutionLimits(t *testing.T) { + t.Parallel() + + err := ku.UpdateOrderExecutionLimits(context.Background(), asset.Binary) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("Received %v, expected %v", err, asset.ErrNotSupported) + } + + assets := []asset.Item{asset.Spot, asset.Futures, asset.Margin} + for x := range assets { + err = ku.UpdateOrderExecutionLimits(context.Background(), assets[x]) + if !errors.Is(err, nil) { + t.Fatalf("received %v, expected %v", err, nil) + } + + enabled, err := ku.GetEnabledPairs(assets[x]) + if err != nil { + t.Fatal(err) + } + + for y := range enabled { + lim, err := ku.GetOrderExecutionLimits(assets[x], enabled[y]) + if err != nil { + t.Fatalf("%v %s %v", err, enabled[y], assets[x]) + } + assert.NotEmpty(t, lim, "limit cannot be empty") + } + } +} diff --git a/exchanges/kucoin/kucoin_types.go b/exchanges/kucoin/kucoin_types.go index 130e3aa320b..bed7d7afb76 100644 --- a/exchanges/kucoin/kucoin_types.go +++ b/exchanges/kucoin/kucoin_types.go @@ -33,7 +33,6 @@ var ( errSizeOrFundIsRequired = errors.New("at least one required among size and funds") errInvalidLeverage = errors.New("invalid leverage value") errInvalidClientOrderID = errors.New("invalid client order ID") - errCurrencyPairNotEnabled = errors.New("currency pair not enabled") subAccountRegExp = regexp.MustCompile("^[a-zA-Z0-9]{7-32}$") subAccountPassphraseRegExp = regexp.MustCompile("^[a-zA-Z0-9]{7-24}$") diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index 10e777125ee..6df942bb1e4 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -249,6 +249,16 @@ func (ku *Kucoin) Run(ctx context.Context) { ku.PrintEnabledPairs() } + assetTypes := ku.GetAssetTypes(false) + for i := range assetTypes { + if err := ku.UpdateOrderExecutionLimits(ctx, assetTypes[i]); err != nil && !errors.Is(err, common.ErrNotYetImplemented) { + log.Errorf(log.ExchangeSys, + "%s failed to set exchange order execution limits. Err: %v", + ku.Name, + err) + } + } + if !ku.GetEnabledFeatures().AutoPairUpdates { return } @@ -294,10 +304,12 @@ func (ku *Kucoin) FetchTradablePairs(ctx context.Context, assetType asset.Item) } pairs := make(currency.Pairs, 0, len(myPairs)) for x := range myPairs { - if !myPairs[x].EnableTrading { + if !myPairs[x].EnableTrading || (assetType == asset.Margin && !myPairs[x].IsMarginEnabled) { continue } - cp, err = currency.NewPairFromString(strings.ToUpper(myPairs[x].Name)) + // Symbol field must be used to generate pair as this is the symbol + // to fetch data from the API. e.g. BSV-USDT name is BCHSV-USDT as symbol. + cp, err = currency.NewPairFromString(strings.ToUpper(myPairs[x].Symbol)) if err != nil { return nil, err } @@ -1904,3 +1916,68 @@ func (ku *Kucoin) GetFuturesPositionOrders(ctx context.Context, r *futures.Posit } return resp, nil } + +// UpdateOrderExecutionLimits updates order execution limits +func (ku *Kucoin) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { + if !ku.SupportsAsset(a) { + return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + } + + var limits []order.MinMaxLevel + switch a { + case asset.Spot, asset.Margin: + symbols, err := ku.GetSymbols(ctx, "") + if err != nil { + return err + } + limits = make([]order.MinMaxLevel, 0, len(symbols)) + for x := range symbols { + if a == asset.Margin && !symbols[x].IsMarginEnabled { + continue + } + pair, enabled, err := ku.MatchSymbolCheckEnabled(symbols[x].Symbol, a, true) + if err != nil { + return err + } + if !enabled { + continue + } + limits = append(limits, order.MinMaxLevel{ + Pair: pair, + Asset: a, + AmountStepIncrementSize: symbols[x].BaseIncrement, + QuoteStepIncrementSize: symbols[x].QuoteIncrement, + PriceStepIncrementSize: symbols[x].PriceIncrement, + MinimumBaseAmount: symbols[x].BaseMinSize, + MaximumBaseAmount: symbols[x].BaseMaxSize, + MinimumQuoteAmount: symbols[x].QuoteMinSize, + MaximumQuoteAmount: symbols[x].QuoteMaxSize, + }) + } + case asset.Futures: + contract, err := ku.GetFuturesOpenContracts(ctx) + if err != nil { + return err + } + limits = make([]order.MinMaxLevel, 0, len(contract)) + for x := range contract { + pair, enabled, err := ku.MatchSymbolCheckEnabled(contract[x].Symbol, a, false) + if err != nil { + return err + } + if !enabled { + continue + } + limits = append(limits, order.MinMaxLevel{ + Pair: pair, + Asset: a, + AmountStepIncrementSize: contract[x].LotSize, + QuoteStepIncrementSize: contract[x].TickSize, + MaximumBaseAmount: contract[x].MaxOrderQty, + MaximumQuoteAmount: contract[x].MaxPrice, + }) + } + } + + return ku.LoadLimits(limits) +} diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index aae7e58fa2c..156fc51c5cc 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -1031,3 +1031,8 @@ func (l *Lbank) GetFuturesContractDetails(context.Context, asset.Item) ([]future func (l *Lbank) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (l *Lbank) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 01f6073468e..55eaf13be52 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -1143,3 +1143,8 @@ func (p *Poloniex) GetLatestFundingRates(context.Context, *fundingrate.LatestRat // TODO: implement with API upgrade return nil, common.ErrNotYetImplemented } + +// UpdateOrderExecutionLimits updates order execution limits +func (p *Poloniex) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index 7ec55d8729c..bb9e1472485 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -772,3 +772,8 @@ func (y *Yobit) GetFuturesContractDetails(context.Context, asset.Item) ([]future func (y *Yobit) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (y *Yobit) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index d3a923ace84..7018a624803 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -1158,3 +1158,8 @@ func (z *ZB) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.C func (z *ZB) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { return nil, common.ErrFunctionNotSupported } + +// UpdateOrderExecutionLimits updates order execution limits +func (z *ZB) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { + return common.ErrNotYetImplemented +} From f65a582cd6b5a1ce12989860ae696d5595067855 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Tue, 14 Nov 2023 15:07:35 +1100 Subject: [PATCH 32/40] kucoin: fix spot order unmarshal bug (#1380) * kucoin: fix unmarshal bug * kucoin: Add time in force handling * kucoin: fix test * thrasher: nits * kucoin_test: shift skip check down for coverage, rm market testing, change buy price to reduce chance of instant match * kucoin: fix nits and force usage of uuid to rm conflicts --------- Co-authored-by: shazbert --- exchanges/kucoin/kucoin.go | 11 ++++++---- exchanges/kucoin/kucoin_test.go | 33 ++++++++++++++++-------------- exchanges/kucoin/kucoin_types.go | 2 +- exchanges/kucoin/kucoin_wrapper.go | 30 ++++++++++++++++++++------- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/exchanges/kucoin/kucoin.go b/exchanges/kucoin/kucoin.go index 52cdc3db016..8bd3494fdde 100644 --- a/exchanges/kucoin/kucoin.go +++ b/exchanges/kucoin/kucoin.go @@ -815,6 +815,7 @@ func (ku *Kucoin) GetServiceStatus(ctx context.Context) (*ServiceStatus, error) // Note: use this only for SPOT trades func (ku *Kucoin) PostOrder(ctx context.Context, arg *SpotOrderParam) (string, error) { if arg.ClientOrderID == "" { + // NOTE: 128 bit max length character string. UUID recommended. return "", errInvalidClientOrderID } if arg.Side == "" { @@ -841,11 +842,13 @@ func (ku *Kucoin) PostOrder(ctx context.Context, arg *SpotOrderParam) (string, e default: return "", fmt.Errorf("%w %s", order.ErrTypeIsInvalid, arg.OrderType) } - resp := struct { - OrderID string `json:"orderId"` + var resp struct { + Data struct { + OrderID string `json:"orderId"` + } `json:"data"` Error - }{} - return resp.OrderID, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, placeOrderEPL, http.MethodPost, kucoinPostOrder, &arg, &resp) + } + return resp.Data.OrderID, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, placeOrderEPL, http.MethodPost, kucoinPostOrder, &arg, &resp) } // PostMarginOrder used to place two types of margin orders: limit and market diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index 625e755c741..0b96beda74d 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" @@ -563,7 +564,6 @@ func TestGetServiceStatus(t *testing.T) { func TestPostOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) // default order type is limit _, err := ku.PostOrder(context.Background(), &SpotOrderParam{ @@ -571,43 +571,46 @@ func TestPostOrder(t *testing.T) { if !errors.Is(err, errInvalidClientOrderID) { t.Errorf("PostOrder() expected %v, but found %v", errInvalidClientOrderID, err) } + + customID, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + _, err = ku.PostOrder(context.Background(), &SpotOrderParam{ - ClientOrderID: "5bd6e9286d99522a52e458de", Symbol: spotTradablePair, + ClientOrderID: customID.String(), Symbol: spotTradablePair, OrderType: ""}) if !errors.Is(err, order.ErrSideIsInvalid) { t.Errorf("PostOrder() expected %v, but found %v", order.ErrSideIsInvalid, err) } _, err = ku.PostOrder(context.Background(), &SpotOrderParam{ - ClientOrderID: "5bd6e9286d99522a52e458de", Symbol: currency.EMPTYPAIR, + ClientOrderID: customID.String(), Symbol: currency.EMPTYPAIR, Size: 0.1, Side: "buy", Price: 234565}) if !errors.Is(err, currency.ErrCurrencyPairEmpty) { t.Errorf("PostOrder() expected %v, but found %v", currency.ErrCurrencyPairEmpty, err) } _, err = ku.PostOrder(context.Background(), &SpotOrderParam{ - ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", + ClientOrderID: customID.String(), Side: "buy", Symbol: spotTradablePair, OrderType: "limit", Size: 0.1}) if !errors.Is(err, errInvalidPrice) { t.Errorf("PostOrder() expected %v, but found %v", errInvalidPrice, err) } _, err = ku.PostOrder(context.Background(), &SpotOrderParam{ - ClientOrderID: "5bd6e9286d99522a52e458de", Symbol: spotTradablePair, Side: "buy", + ClientOrderID: customID.String(), Symbol: spotTradablePair, Side: "buy", OrderType: "limit", Price: 234565}) if !errors.Is(err, errInvalidSize) { t.Errorf("PostOrder() expected %v, but found %v", errInvalidSize, err) } - _, err = ku.PostOrder(context.Background(), &SpotOrderParam{ - ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", - Symbol: spotTradablePair, OrderType: "limit", Size: 0.1, Price: 234565}) - if err != nil { - t.Error("PostOrder() error", err) - } - // market order + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) _, err = ku.PostOrder(context.Background(), &SpotOrderParam{ - ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", - Symbol: spotTradablePair, - OrderType: "market", Remark: "remark", Size: 0.1}) + ClientOrderID: customID.String(), + Side: "buy", + Symbol: spotTradablePair, + OrderType: "limit", + Size: 0.005, + Price: 1000}) if err != nil { t.Error("PostOrder() error", err) } diff --git a/exchanges/kucoin/kucoin_types.go b/exchanges/kucoin/kucoin_types.go index bed7d7afb76..af5a1faea65 100644 --- a/exchanges/kucoin/kucoin_types.go +++ b/exchanges/kucoin/kucoin_types.go @@ -32,7 +32,7 @@ var ( errMissingOrderbookSequence = errors.New("missing orderbook sequence") errSizeOrFundIsRequired = errors.New("at least one required among size and funds") errInvalidLeverage = errors.New("invalid leverage value") - errInvalidClientOrderID = errors.New("invalid client order ID") + errInvalidClientOrderID = errors.New("no client order ID supplied, this endpoint requires a UUID or similar string") subAccountRegExp = regexp.MustCompile("^[a-zA-Z0-9]{7-32}$") subAccountPassphraseRegExp = regexp.MustCompile("^[a-zA-Z0-9]{7-24}$") diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index 6df942bb1e4..a5a1975a032 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -761,13 +761,29 @@ func (ku *Kucoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm } return s.DeriveSubmitResponse(o) case asset.Spot: - if s.ClientID != "" && s.ClientOrderID == "" { - s.ClientOrderID = s.ClientID + timeInForce := "" + if s.Type == order.Limit { + switch { + case s.FillOrKill: + timeInForce = "FOK" + case s.ImmediateOrCancel: + timeInForce = "IOC" + case s.PostOnly: + default: + timeInForce = "GTC" + } } o, err := ku.PostOrder(ctx, &SpotOrderParam{ - ClientOrderID: s.ClientOrderID, Side: sideString, - Symbol: s.Pair, OrderType: s.Type.Lower(), Size: s.Amount, - Price: s.Price, PostOnly: s.PostOnly, Hidden: s.Hidden}) + ClientOrderID: s.ClientOrderID, + Side: sideString, + Symbol: s.Pair, + OrderType: s.Type.Lower(), + Size: s.Amount, + Price: s.Price, + PostOnly: s.PostOnly, + Hidden: s.Hidden, + TimeInForce: timeInForce, + }) if err != nil { return nil, err } @@ -1936,7 +1952,7 @@ func (ku *Kucoin) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) continue } pair, enabled, err := ku.MatchSymbolCheckEnabled(symbols[x].Symbol, a, true) - if err != nil { + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { return err } if !enabled { @@ -1962,7 +1978,7 @@ func (ku *Kucoin) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) limits = make([]order.MinMaxLevel, 0, len(contract)) for x := range contract { pair, enabled, err := ku.MatchSymbolCheckEnabled(contract[x].Symbol, a, false) - if err != nil { + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { return err } if !enabled { From dff0581890d1a3ca7c38a889512df5e459c19e0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:58:49 +1100 Subject: [PATCH 33/40] build(deps): bump bufbuild/buf-setup-action from 1.28.0 to 1.28.1 (#1402) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.28.0 to 1.28.1. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.28.0...v1.28.1) --- updated-dependencies: - dependency-name: bufbuild/buf-setup-action 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> --- .github/workflows/proto-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/proto-lint.yml b/.github/workflows/proto-lint.yml index 9be0029a6cf..eabf3baaf26 100644 --- a/.github/workflows/proto-lint.yml +++ b/.github/workflows/proto-lint.yml @@ -23,7 +23,7 @@ jobs: go install google.golang.org/protobuf/cmd/protoc-gen-go go install google.golang.org/grpc/cmd/protoc-gen-go-grpc - - uses: bufbuild/buf-setup-action@v1.28.0 + - uses: bufbuild/buf-setup-action@v1.28.1 - name: buf generate working-directory: ./gctrpc From 06024fcb569c3638e5c8f3ef85aa6e7a66990e34 Mon Sep 17 00:00:00 2001 From: Bea <103050835+Beadko@users.noreply.github.com> Date: Tue, 21 Nov 2023 07:28:38 +0700 Subject: [PATCH 34/40] Kucoin: Fix ProcessMarketSnapshot and add a test (#1392) * Kucoin: Fix ProcessMarketSnapshot and add a test * Kucoin: ProcessMarketSnapshot: move the check before the ticker * Kucoin: remove time.sleep from the test, add if statement to processmarketSnapshot * Kucoin: ProcessMarketSnapshot to send margin, spot and both margin and spot pair data * Kucoin: range over listOfAssetsCurrencyPairEnabledFor which returns a slice * Kucoin: linter fix and pointers placed * Kucoin: removed AssetWebsocketSupport and ku.CurrencyPairs.IsAssetEnabled, linter error fixed, comment amended --- exchanges/kucoin/kucoin_test.go | 64 +++++ exchanges/kucoin/kucoin_websocket.go | 236 +++++++----------- exchanges/kucoin/kucoin_wrapper.go | 36 +-- .../kucoin/testdata/wsMarketSnapshot.json | 3 + testdata/configtest.json | 4 +- 5 files changed, 168 insertions(+), 175 deletions(-) create mode 100644 exchanges/kucoin/testdata/wsMarketSnapshot.json diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index 0b96beda74d..9694507fbbc 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -24,7 +24,9 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" + "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/portfolio/withdraw" ) @@ -2415,6 +2417,68 @@ func TestProcessOrderbook(t *testing.T) { } } +func TestProcessMarketSnapshot(t *testing.T) { + t.Parallel() + n := new(Kucoin) + sharedtestvalues.TestFixtureToDataHandler(t, ku, n, "testdata/wsMarketSnapshot.json", n.wsHandleData) + seen := 0 + for reading := true; reading; { + select { + default: + reading = false + case resp := <-n.GetBase().Websocket.DataHandler: + seen++ + switch v := resp.(type) { + case *ticker.Price: + switch seen { + // spot only + case 1: + assert.Equal(t, time.UnixMilli(1698740324415), v.LastUpdated, "datetime") + assert.Equal(t, 0.00001402100000000000, v.High, "high") + assert.Equal(t, 0.000012508, v.Last, "lastTradedPrice") + assert.Equal(t, 0.00001129200000000000, v.Low, "low") + assert.Equal(t, currency.NewPairWithDelimiter("XMR", "BTC", "-"), v.Pair, "symbol") + assert.Equal(t, 28474.47280000000000000000, v.Volume, "volume") + assert.Equal(t, 0.37038038297340000000, v.QuoteVolume, "volValue") + // margin only + case 2: + assert.Equal(t, time.UnixMilli(1698740324483), v.LastUpdated, "datetime") + assert.Equal(t, 0.00000039450000000000, v.High, "high") + assert.Equal(t, 0.0000003897, v.Last, "lastTradedPrice") + assert.Equal(t, 0.00000034200000000000, v.Low, "low") + assert.Equal(t, currency.NewPairWithDelimiter("MTV", "BTC", "-"), v.Pair, "symbol") + assert.Equal(t, 316078.69700000000000000000, v.Volume, "volume") + assert.Equal(t, 0.11768519138877000000, v.QuoteVolume, "volValue") + // both margin and spot + case 3: + assert.Equal(t, time.UnixMilli(1698740324437), v.LastUpdated, "datetime") + assert.Equal(t, 0.00008486000000000000, v.High, "high") + assert.Equal(t, 0.00008318, v.Last, "lastTradedPrice") + assert.Equal(t, 0.00007152000000000000, v.Low, "low") + assert.Equal(t, currency.NewPairWithDelimiter("BTC", "USDT", "-"), v.Pair, "symbol") + assert.Equal(t, 17062.45450000000000000000, v.Volume, "volume") + assert.Equal(t, 1.33076678861000000000, v.QuoteVolume, "volValue") + } + case error: + t.Error(v) + default: + t.Errorf("Got unexpected data: %T %v", v, v) + } + } + } + assert.Equal(t, 4, seen, "Number of messages") +} + +func TestSubscribeMarketSnapshot(t *testing.T) { + t.Parallel() + setupWS() + s := []stream.ChannelSubscription{ + {Channel: marketTickerSnapshotForCurrencyChannel, + Currency: currency.Pair{Base: currency.BTC}}, + } + err := ku.Subscribe(s) + assert.NoError(t, err, "Subscribe to MarketSnapshot should not error") +} func TestSeedLocalCache(t *testing.T) { t.Parallel() pair, err := currency.NewPairFromString("ETH-USDT") diff --git a/exchanges/kucoin/kucoin_websocket.go b/exchanges/kucoin/kucoin_websocket.go index a9a1b480f10..a8fbfdc2b7d 100644 --- a/exchanges/kucoin/kucoin_websocket.go +++ b/exchanges/kucoin/kucoin_websocket.go @@ -232,7 +232,7 @@ func (ku *Kucoin) wsHandleData(respData []byte) error { return ku.processTicker(resp.Data, instruments) case strings.HasPrefix(marketTickerSnapshotChannel, topicInfo[0]) || strings.HasPrefix(marketTickerSnapshotForCurrencyChannel, topicInfo[0]): - return ku.processMarketSnapshot(resp.Data, topicInfo[1]) + return ku.processMarketSnapshot(resp.Data) case strings.HasPrefix(marketOrderbookLevel2Channels, topicInfo[0]): return ku.processOrderbookWithDepth(respData, topicInfo[1]) case strings.HasPrefix(marketOrderbookLevel2to5Channel, topicInfo[0]), @@ -677,30 +677,24 @@ func (ku *Kucoin) processOrderChangeEvent(respData []byte) error { if err != nil { return err } - orderChange := order.Detail{ - Price: response.Price, - Amount: response.Size, - ExecutedAmount: response.FilledSize, - RemainingAmount: response.RemainSize, - Exchange: ku.Name, - OrderID: response.OrderID, - ClientOrderID: response.ClientOid, - Type: oType, - Side: side, - Status: oStatus, - AssetType: asset.Spot, - Date: response.OrderTime.Time(), - LastUpdated: response.Timestamp.Time(), - Pair: pair, - } - assetPairEnabled := ku.listOfAssetsCurrencyPairEnabledFor(pair) - if assetPairEnabled[asset.Spot] && ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil { - ku.Websocket.DataHandler <- &orderChange - } - if assetPairEnabled[asset.Margin] && ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { - marginOrderChange := orderChange - marginOrderChange.AssetType = asset.Margin - ku.Websocket.DataHandler <- &marginOrderChange + // TODO should amend this function as we need to know the order asset type when we call it + for _, assetType := range ku.listOfAssetsCurrencyPairEnabledFor(pair) { + ku.Websocket.DataHandler <- &order.Detail{ + Price: response.Price, + Amount: response.Size, + ExecutedAmount: response.FilledSize, + RemainingAmount: response.RemainSize, + Exchange: ku.Name, + OrderID: response.OrderID, + ClientOrderID: response.ClientOid, + Type: oType, + Side: side, + Status: oStatus, + AssetType: assetType, + Date: response.OrderTime.Time(), + LastUpdated: response.Timestamp.Time(), + Pair: pair, + } } return nil } @@ -724,26 +718,17 @@ func (ku *Kucoin) processTradeData(respData []byte, instrument string) error { if err != nil { return err } - tradeData := trade.Data{ - CurrencyPair: pair, - Timestamp: response.Time.Time(), - Price: response.Price, - Amount: response.Size, - Side: side, - Exchange: ku.Name, - TID: response.TradeID, - AssetType: asset.Spot, - } - assetPairEnabled := ku.listOfAssetsCurrencyPairEnabledFor(pair) - if assetPairEnabled[asset.Spot] && ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil { - err = ku.Websocket.Trade.Update(saveTradeData, tradeData) - if err != nil { - return err - } - } - if assetPairEnabled[asset.Margin] && ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { - tradeData.AssetType = asset.Margin - err := ku.Websocket.Trade.Update(saveTradeData, tradeData) + for _, assetType := range ku.listOfAssetsCurrencyPairEnabledFor(pair) { + err = ku.Websocket.Trade.Update(saveTradeData, trade.Data{ + CurrencyPair: pair, + Timestamp: response.Time.Time(), + Price: response.Price, + Amount: response.Size, + Side: side, + Exchange: ku.Name, + TID: response.TradeID, + AssetType: assetType, + }) if err != nil { return err } @@ -761,26 +746,19 @@ func (ku *Kucoin) processTicker(respData []byte, instrument string) error { if err != nil { return err } - spotTickerPrice := ticker.Price{ - AssetType: asset.Spot, - Last: response.Price, - LastUpdated: response.Timestamp.Time(), - ExchangeName: ku.Name, - Pair: pair, - Ask: response.BestAsk, - Bid: response.BestBid, - AskSize: response.BestAskSize, - BidSize: response.BestBidSize, - Volume: response.Size, - } - assetEnabledPairs := ku.listOfAssetsCurrencyPairEnabledFor(pair) - if assetEnabledPairs[asset.Spot] && ku.AssetWebsocketSupport.IsAssetWebsocketSupported(asset.Spot) && ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil { - ku.Websocket.DataHandler <- &spotTickerPrice - } - if assetEnabledPairs[asset.Margin] && ku.AssetWebsocketSupport.IsAssetWebsocketSupported(asset.Margin) && ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { - marginTickerPrice := spotTickerPrice - marginTickerPrice.AssetType = asset.Margin - ku.Websocket.DataHandler <- &marginTickerPrice + for _, assetType := range ku.listOfAssetsCurrencyPairEnabledFor(pair) { + ku.Websocket.DataHandler <- &ticker.Price{ + AssetType: assetType, + Last: response.Price, + LastUpdated: response.Timestamp.Time(), + ExchangeName: ku.Name, + Pair: pair, + Ask: response.BestAsk, + Bid: response.BestBid, + AskSize: response.BestAskSize, + BidSize: response.BestBidSize, + Volume: response.Size, + } } return nil } @@ -799,26 +777,20 @@ func (ku *Kucoin) processCandlesticks(respData []byte, instrument, intervalStrin if err != nil { return err } - candlestickData := stream.KlineData{ - Timestamp: response.Time.Time(), - Pair: pair, - AssetType: asset.Spot, - Exchange: ku.Name, - StartTime: resp.Candles.StartTime, - Interval: intervalString, - OpenPrice: resp.Candles.OpenPrice, - ClosePrice: resp.Candles.ClosePrice, - HighPrice: resp.Candles.HighPrice, - LowPrice: resp.Candles.LowPrice, - Volume: resp.Candles.TransactionVolume, - } - assetEnabledPairs := ku.listOfAssetsCurrencyPairEnabledFor(pair) - if assetEnabledPairs[asset.Spot] && ku.AssetWebsocketSupport.IsAssetWebsocketSupported(asset.Spot) && ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil { - ku.Websocket.DataHandler <- candlestickData - } - if assetEnabledPairs[asset.Margin] && ku.AssetWebsocketSupport.IsAssetWebsocketSupported(asset.Margin) && ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { - candlestickData.AssetType = asset.Margin - ku.Websocket.DataHandler <- candlestickData + for _, assetType := range ku.listOfAssetsCurrencyPairEnabledFor(pair) { + ku.Websocket.DataHandler <- stream.KlineData{ + Timestamp: response.Time.Time(), + Pair: pair, + AssetType: assetType, + Exchange: ku.Name, + StartTime: resp.Candles.StartTime, + Interval: intervalString, + OpenPrice: resp.Candles.OpenPrice, + ClosePrice: resp.Candles.ClosePrice, + HighPrice: resp.Candles.HighPrice, + LowPrice: resp.Candles.LowPrice, + Volume: resp.Candles.TransactionVolume, + } } return nil } @@ -836,28 +808,15 @@ func (ku *Kucoin) processOrderbookWithDepth(respData []byte, instrument string) return err } var init bool - assetEnabledPairs := ku.listOfAssetsCurrencyPairEnabledFor(pair) - if assetEnabledPairs[asset.Spot] && ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil { - init, err = ku.UpdateLocalBuffer(result.Result, asset.Spot) + for _, assetType := range ku.listOfAssetsCurrencyPairEnabledFor(pair) { + init, err = ku.UpdateLocalBuffer(result.Result, assetType) if err != nil { if init { return nil } return fmt.Errorf("%v - UpdateLocalCache for asset type: %v error: %s", ku.Name, - asset.Spot, - err) - } - } - if assetEnabledPairs[asset.Margin] && ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { - init, err = ku.UpdateLocalBuffer(result.Result, asset.Margin) - if err != nil { - if init { - return nil - } - return fmt.Errorf("%v - UpdateLocalCache for asset type: %v error: %s", - ku.Name, - asset.Margin, + assetType, err) } } @@ -913,65 +872,45 @@ func (ku *Kucoin) processOrderbook(respData []byte, symbol string) error { return err } var init bool - assetEnabledPairs := ku.listOfAssetsCurrencyPairEnabledFor(pair) - if assetEnabledPairs[asset.Spot] && ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil { - init, err = ku.UpdateLocalBuffer(response, asset.Spot) - if err != nil { - if init { - return nil - } - return fmt.Errorf("%v - UpdateLocalCache for asset type %v error: %s", - ku.Name, - asset.Spot, - err) - } - } - if assetEnabledPairs[asset.Margin] && ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { - init, err = ku.UpdateLocalBuffer(response, asset.Margin) + for _, assetType := range ku.listOfAssetsCurrencyPairEnabledFor(pair) { + init, err = ku.UpdateLocalBuffer(response, assetType) if err != nil { if init { return nil } return fmt.Errorf("%v - UpdateLocalCache for asset type %v error: %s", ku.Name, - asset.Margin, + assetType, err) } } return nil } -func (ku *Kucoin) processMarketSnapshot(respData []byte, instrument string) error { +func (ku *Kucoin) processMarketSnapshot(respData []byte) error { response := WsSpotTicker{} err := json.Unmarshal(respData, &response) if err != nil { return err } - pair, err := currency.NewPairFromString(instrument) + pair, err := currency.NewPairFromString(response.Data.Symbol) if err != nil { return err } - spotTickerPrice := ticker.Price{ - ExchangeName: ku.Name, - AssetType: asset.Spot, - Last: response.Data.LastTradedPrice, - Pair: pair, - Low: response.Data.Low, - High: response.Data.High, - QuoteVolume: response.Data.VolValue, - Volume: response.Data.Vol, - Open: response.Data.Open, - Close: response.Data.Close, - LastUpdated: response.Data.Datetime.Time(), - } - assetEnabledPairs := ku.listOfAssetsCurrencyPairEnabledFor(pair) - if assetEnabledPairs[asset.Spot] && ku.AssetWebsocketSupport.IsAssetWebsocketSupported(asset.Spot) && ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil { - ku.Websocket.DataHandler <- &spotTickerPrice - } - if assetEnabledPairs[asset.Margin] && ku.AssetWebsocketSupport.IsAssetWebsocketSupported(asset.Margin) && ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { - marginTickerPrice := spotTickerPrice - marginTickerPrice.AssetType = asset.Margin - ku.Websocket.DataHandler <- &marginTickerPrice + for _, assetType := range ku.listOfAssetsCurrencyPairEnabledFor(pair) { + ku.Websocket.DataHandler <- &ticker.Price{ + ExchangeName: ku.Name, + AssetType: assetType, + Last: response.Data.LastTradedPrice, + Pair: pair, + Low: response.Data.Low, + High: response.Data.High, + QuoteVolume: response.Data.VolValue, + Volume: response.Data.Vol, + Open: response.Data.Open, + Close: response.Data.Close, + LastUpdated: response.Data.Datetime.Time(), + } } return nil } @@ -1897,16 +1836,13 @@ func (o *orderbookManager) stopNeedsFetchingBook(pair currency.Pair, assetType a return nil } -func (ku *Kucoin) listOfAssetsCurrencyPairEnabledFor(cp currency.Pair) map[asset.Item]bool { - assetTypes := ku.CurrencyPairs.GetAssetTypes(true) - // we need this all asset types on the map even if their value is false - assetPairEnabled := map[asset.Item]bool{asset.Spot: false, asset.Futures: false, asset.Margin: false} - for i := range assetTypes { - pairs, err := ku.GetEnabledPairs(assetTypes[i]) - if err != nil { - continue +func (ku *Kucoin) listOfAssetsCurrencyPairEnabledFor(cp currency.Pair) []asset.Item { + assets := []asset.Item{} + for _, a := range ku.CurrencyPairs.GetAssetTypes(true) { + pairs, err := ku.GetEnabledPairs(a) + if err == nil && pairs.Contains(cp, true) { + assets = append(assets, a) } - assetPairEnabled[assetTypes[i]] = pairs.Contains(cp, true) } - return assetPairEnabled + return assets } diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index a5a1975a032..98906dd5514 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -405,29 +405,19 @@ func (ku *Kucoin) UpdateTickers(ctx context.Context, assetType asset.Item) error if !pairs.Contains(pair, true) { continue } - tick := ticker.Price{ - Last: ticks.Tickers[t].Last, - High: ticks.Tickers[t].High, - Low: ticks.Tickers[t].Low, - Volume: ticks.Tickers[t].Volume, - Ask: ticks.Tickers[t].Sell, - Bid: ticks.Tickers[t].Buy, - Pair: pair, - ExchangeName: ku.Name, - AssetType: assetType, - LastUpdated: ticks.Time.Time(), - } - assetEnabledPairs := ku.listOfAssetsCurrencyPairEnabledFor(pair) - if assetEnabledPairs[asset.Spot] && ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil { - err = ticker.ProcessTicker(&tick) - if err != nil { - return err - } - } - if assetEnabledPairs[asset.Margin] && ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { - marginTick := tick - marginTick.AssetType = asset.Margin - err = ticker.ProcessTicker(&marginTick) + for _, assetType := range ku.listOfAssetsCurrencyPairEnabledFor(pair) { + err = ticker.ProcessTicker(&ticker.Price{ + Last: ticks.Tickers[t].Last, + High: ticks.Tickers[t].High, + Low: ticks.Tickers[t].Low, + Volume: ticks.Tickers[t].Volume, + Ask: ticks.Tickers[t].Sell, + Bid: ticks.Tickers[t].Buy, + Pair: pair, + ExchangeName: ku.Name, + AssetType: assetType, + LastUpdated: ticks.Time.Time(), + }) if err != nil { return err } diff --git a/exchanges/kucoin/testdata/wsMarketSnapshot.json b/exchanges/kucoin/testdata/wsMarketSnapshot.json new file mode 100644 index 00000000000..51261e57009 --- /dev/null +++ b/exchanges/kucoin/testdata/wsMarketSnapshot.json @@ -0,0 +1,3 @@ +{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324504,"data":{"averagePrice":0.00001164,"baseCurrency":"XMR","board":0,"buy":0.00001252,"changePrice":0.00000104800000000000,"changeRate":0.0914,"close":0.000012508,"datetime":1698740324415,"high":0.00001402100000000000,"lastTradedPrice":0.000012508,"low":0.00001129200000000000,"makerCoefficient":2.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"BTC","marketChange1h":{"changeRate":0,"high":0,"low":0,"open":0,"vol":0,"volValue":0},"marketChange24h":{"changePrice":0.00000104800000000000,"changeRate":0.0914,"high":0.00001402100000000000,"low":0.00001129200000000000,"open":0.00001146000000000000,"vol":28474.47280000000000000000,"volValue":0.37038038297340000000},"marketChange4h":{"changePrice":0.00000009600000000000,"changeRate":0.0077,"high":0.00001308400000000000,"low":0.00001241200000000000,"open":0.00001241200000000000,"vol":7090.00000000000000000000,"volValue":0.08885800028840000000},"markets":["BTC"],"open":0.00001146000000000000,"quoteCurrency":"BTC","sell":0.000013191,"sort":100,"symbol":"XMR-BTC","symbolCode":"XMR-BTC","takerCoefficient":2.000000,"takerFeeRate":0.001,"trading":true,"vol":28474.47280000000000000000,"volValue":0.37038038297340000000}}} +{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324488,"data":{"averagePrice":0.00000037,"baseCurrency":"MTV","board":0,"buy":0.0000003641,"changePrice":0.00000004770000000000,"changeRate":0.1394,"close":0.0000003897,"datetime":1698740324483,"high":0.00000039450000000000,"lastTradedPrice":0.0000003897,"low":0.00000034200000000000,"makerCoefficient":2.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"BTC","marketChange1h":{"changeRate":0,"high":0,"low":0,"open":0,"vol":0,"volValue":0},"marketChange24h":{"changePrice":0.00000004770000000000,"changeRate":0.1394,"high":0.00000039450000000000,"low":0.00000034200000000000,"open":0.00000034200000000000,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000},"marketChange4h":{"changePrice":0.00000003290000000000,"changeRate":0.0922,"high":0.00000038970000000000,"low":0.00000035680000000000,"open":0.00000035680000000000,"vol":2309.46880000000000000000,"volValue":0.00089999999136000000},"markets":["BTC"],"open":0.00000034200000000000,"quoteCurrency":"BTC","sell":0.0000004022,"sort":100,"symbol":"MTV-BTC","symbolCode":"MTV-BTC","takerCoefficient":2.000000,"takerFeeRate":0.001,"trading":true,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000}}} +{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324508,"data":{"averagePrice":0.00007307,"baseCurrency":"BTC","board":0,"buy":0.00008388,"changePrice":0.00001166000000000000,"changeRate":0.1630,"close":0.00008318,"datetime":1698740324437,"high":0.00008486000000000000,"lastTradedPrice":0.00008318,"low":0.00007152000000000000,"makerCoefficient":1.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"USDT","marketChange1h":{"changePrice":-0.00000116000000000000,"changeRate":-0.0137,"high":0.00008434000000000000,"low":0.00008318000000000000,"open":0.00008434000000000000,"vol":189.33430000000000000000,"volValue":0.01578748292300000000},"marketChange24h":{"changePrice":0.00001166000000000000,"changeRate":0.1630,"high":0.00008486000000000000,"low":0.00007152000000000000,"open":0.00007152000000000000,"vol":17062.45450000000000000000,"volValue":1.33076678861000000000},"marketChange4h":{"changePrice":0.00000143000000000000,"changeRate":0.0174,"high":0.00008486000000000000,"low":0.00008175000000000000,"open":0.00008175000000000000,"vol":1752.55690000000000000000,"volValue":0.14543003812900000000},"markets":["BTC"],"open":0.00007152000000000000,"quoteCurrency":"USDT","sell":0.00008421,"sort":100,"symbol":"BTC-USDT","symbolCode":"BTC-USDT","takerCoefficient":1.000000,"takerFeeRate":0.001,"trading":true,"vol":17062.45450000000000000000,"volValue":1.33076678861000000000}}} diff --git a/testdata/configtest.json b/testdata/configtest.json index faf82a108f9..bfaacb5707f 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -1996,7 +1996,7 @@ "pairs": { "spot": { "assetEnabled": true, - "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC", + "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,XMR-BTC", "available": "BTC-USDT,MHC-ETH,MHC-BTC,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MHC-USDT,XMR-BTC,XMR-ETH,RIF-BTC,MTV-BTC,MTV-ETH,CRO-BTC,MTV-USDT,KMD-BTC,KMD-USDT,RFOX-USDT,TEL-USDT,TT-USDT,AERGO-USDT,XMR-USDT,TRX-KCS,ATOM-BTC,ATOM-ETH,ATOM-USDT,ATOM-KCS,ETN-USDT,FTM-USDT,TOMO-USDT,VSYS-USDT,OCEAN-BTC,OCEAN-ETH,CHR-BTC,CHR-USDT,FX-BTC,FX-ETH,NIM-BTC,NIM-ETH,COTI-BTC,COTI-USDT,NRG-ETH,BNB-BTC,BNB-USDT,JAR-BTC,JAR-USDT,ALGO-BTC,ALGO-ETH,ALGO-USDT,XEM-BTC,XEM-USDT,CIX100-USDT,XTZ-BTC,XTZ-USDT,ZEC-BTC,ZEC-USDT,ADA-BTC,ADA-USDT,REV-USDT,WXT-BTC,WXT-USDT,FORESTPLUS-BTC,FORESTPLUS-USDT,BOLT-BTC,BOLT-USDT,ARPA-USDT,CHZ-BTC,CHZ-USDT,DAPPT-BTC,DAPPT-USDT,NOIA-BTC,NOIA-USDT,WIN-BTC,WIN-USDT,DERO-BTC,DERO-USDT,BTT-USDT,EOSC-USDT,ENQ-BTC,ENQ-USDT,ONE-BTC,ONE-USDT,TOKO-BTC,TOKO-USDT,VID-BTC,VID-USDT,LUNA-USDT,SXP-BTC,SXP-USDT,AKRO-BTC,AKRO-USDT,ROOBEE-BTC,WIN-TRX,MAP-BTC,MAP-USDT,AMPL-BTC,AMPL-USDT,DAG-USDT,POL-USDT,ARX-USDT,NWC-BTC,NWC-USDT,BEPRO-BTC,BEPRO-USDT,VRA-BTC,VRA-USDT,KSM-BTC,KSM-USDT,DASH-USDT,SUTER-USDT,ACOIN-USDT,SUTER-BTC,SENSO-USDT,PRE-BTC,XDB-USDT,SYLO-USDT,WOM-USDT,SENSO-BTC,DGB-USDT,LYXE-USDT,LYXE-ETH,XDB-BTC,STX-BTC,STX-USDT,XSR-USDT,COMP-USDT,CRO-USDT,KAI-USDT,KAI-BTC,WEST-BTC,WEST-USDT,EWT-BTC,WAVES-USDT,WAVES-BTC,ORN-USDT,AMPL-ETH,BNS-USDT,MKR-USDT,SUKU-BTC,MLK-BTC,MLK-USDT,JST-USDT,KAI-ETH,SUKU-USDT,DIA-USDT,DIA-BTC,LINK-BTC,LINK-USDT,DOT-USDT,DOT-BTC,SHA-BTC,SHA-USDT,EWT-USDT,USDJ-USDT,EFX-BTC,CKB-BTC,CKB-USDT,UMA-USDT,ALEPH-USDT,VELO-USDT,SUN-USDT,BUY-USDT,YFI-USDT,OXEN-USDT,UNI-USDT,UOS-USDT,UOS-BTC,NIM-USDT,DEGO-USDT,DEGO-ETH,UDOO-ETH,RFUEL-USDT,FIL-USDT,UBX-ETH,REAP-USDT,AAVE-USDT,AAVE-BTC,TONE-BTC,TONE-ETH,ELF-ETH,AERGO-BTC,IOST-ETH,KCS-USDT,SNX-ETH,TOMO-ETH,KCS-ETH,DRGN-BTC,WAN-ETH,NULS-ETH,AXPR-ETH,POWR-BTC,QTUM-BTC,MANA-BTC,TEL-BTC,XYO-ETH,AXPR-BTC,ETN-BTC,COV-ETH,VET-BTC,KCS-BTC,CAPP-ETH,ONT-BTC,DRGN-ETH,DAG-ETH,TOMO-BTC,WAN-BTC,KNC-ETH,CRPT-ETH,LTC-USDT,BAX-ETH,BSV-USDT,DENT-ETH,AION-ETH,LYM-ETH,TRAC-ETH,ENJ-BTC,WAXP-BTC,DGB-BTC,ELA-BTC,ZIL-BTC,BSV-BTC,XLM-USDT,IOTX-ETH,SOUL-BTC,DOCK-BTC,AMB-ETH,TRX-BTC,XRP-TUSD,NULS-BTC,ETH-DAI,LSK-BTC,GMB-ETH,GMB-BTC,NEO-ETH,OMG-ETH,BTC-TUSD,KAT-USDT,KNC-BTC,ELF-BTC,MANA-ETH,ETC-USDT,ONT-ETH,MKR-BTC,KAT-BTC,XRP-USDC,XYO-BTC,SNT-ETH,ZRX-BTC,LOOM-ETH,AION-BTC,POWR-ETH,OLT-ETH,OLT-BTC,SNT-BTC,TRAC-BTC,XLM-ETH,ETH-USDT,BSV-ETH,TRX-ETH,ETN-ETH,AOA-USDT,BCD-BTC,DENT-BTC,DOCK-ETH,KEY-BTC,EOS-KCS,XLM-BTC,ADB-ETH,TIME-ETH,CVC-BTC,LSK-ETH,QKC-BTC,AMB-BTC,USDT-TUSD,ETC-ETH,XRP-BTC,NEO-KCS,SNX-USDT,CRPT-BTC,IOTX-BTC,LTC-ETH,XRP-KCS,ADB-BTC,LTC-KCS,TEL-ETH,DCR-ETH,LYM-USDT,USDT-USDC,ETH-USDC,DAG-BTC,AVA-BTC,BTC-USDT,WAXP-ETH,XRP-USDT,KEY-ETH,VET-ETH,FTM-BTC,USDT-DAI,QKC-ETH,ETH-BTC,MAN-BTC,CPC-ETH,TRX-USDT,BTC-DAI,ONT-USDT,DASH-ETH,BAX-BTC,AVA-ETH,LOOM-BTC,MVP-BTC,MKR-ETH,COV-BTC,CPC-BTC,REQ-ETH,EOS-BTC,LTC-BTC,XRP-ETH,CAPP-BTC,FTM-ETH,BCD-ETH,ZRX-ETH,DGB-ETH,VET-USDT,REQ-BTC,UTK-BTC,PLAY-BTC,UTK-ETH,SNX-BTC,MVP-ETH,NEO-BTC,SOUL-ETH,NEO-USDT,ELA-ETH,OMG-BTC,TIME-BTC,AOA-BTC,ETC-BTC,DCR-BTC,BTC-USDC,ENJ-ETH,IOST-BTC,DASH-BTC,EOS-USDT,EOS-ETH,ZIL-ETH,ETH-TUSD,GAS-BTC,LYM-BTC,BCH-BTC,VSYS-BTC,BCH-USDT,MKR-DAI,SOLVE-BTC,GRIN-BTC,GRIN-USDT,UQC-BTC,UQC-ETH,OPCT-BTC,OPCT-ETH,PRE-USDT,SHR-BTC,SHR-USDT,UBXT-USDT,ROSE-USDT,USDC-USDT,CTI-USDT,CTI-ETH,ETH2-ETH,BUX-BTC,XHV-USDT,PLU-USDT,GRT-USDT,CAS-BTC,CAS-USDT,MSWAP-BTC,MSWAP-USDT,GOM2-BTC,GOM2-USDT,REVV-BTC,REVV-USDT,LON-USDT,1INCH-USDT,LOC-USDT,API3-USDT,UNFI-USDT,HTR-USDT,FRONT-USDT,FRONT-BTC,WBTC-BTC,WBTC-ETH,MIR-USDT,LTC-USDC,BCH-USDC,HYDRA-USDT,DFI-USDT,DFI-BTC,CRV-USDT,SUSHI-USDT,FRM-USDT,EOS-USDC,BSV-USDC,ZEN-USDT,CUDOS-USDT,ADA-USDC,REN-USDT,LRC-USDT,LINK-USDC,KLV-USDT,KLV-BTC,BOA-USDT,THETA-USDT,QNT-USDT,BAT-USDT,DOGE-USDT,DOGE-USDC,DAO-USDT,STRONG-USDT,TRIAS-USDT,TRIAS-BTC,DOGE-BTC,MITX-BTC,MITX-USDT,CAKE-USDT,ORAI-USDT,ZEE-USDT,LTX-USDT,LTX-BTC,MASK-USDT,KLV-TRX,IDEA-USDT,PHA-USDT,PHA-ETH,BCH-KCS,SRK-USDT,SRK-BTC,ADA-KCS,HTR-BTC,BSV-KCS,DOT-KCS,LINK-KCS,MIR-KCS,BNB-KCS,XLM-KCS,VET-KCS,SWINGBY-USDT,SWINGBY-BTC,XHV-BTC,DASH-KCS,UNI-KCS,AAVE-KCS,DOGE-KCS,ZEC-KCS,XTZ-KCS,GRT-KCS,ALGO-KCS,EWT-KCS,GAS-USDT,AVAX-USDT,AVAX-BTC,KRL-BTC,KRL-USDT,POLK-USDT,POLK-BTC,ENJ-USDT,MANA-USDT,RNDR-USDT,RNDR-BTC,RLY-USDT,ANC-USDT,SKEY-USDT,LAYER-USDT,TARA-USDT,TARA-ETH,IOST-USDT,DYP-USDT,DYP-ETH,XYM-USDT,XYM-BTC,PCX-USDT,PCX-BTC,ORBS-USDT,ORBS-BTC,BTC3L-USDT,BTC3S-USDT,ETH3L-USDT,ETH3S-USDT,ANKR-USDT,DSLA-USDT,DSLA-BTC,SAND-USDT,VAI-USDT,XCUR-USDT,XCUR-BTC,FLUX-USDT,OMG-USDT,ZIL-USDT,DODO-USDT,MAN-USDT,BAX-USDT,BOSON-USDT,BOSON-ETH,PUNDIX-USDT,PUNDIX-BTC,WAXP-USDT,HT-USDT,PDEX-USDT,LABS-USDT,LABS-ETH,GMB-USDT,PHNX-USDT,PHNX-BTC,HAI-USDT,EQZ-USDT,FORTH-USDT,HORD-USDT,CGG-USDT,UBX-USDT,GHX-USDT,TCP-USDT,STND-USDT,STND-ETH,TOWER-USDT,TOWER-BTC,ACE-USDT,LOCG-USDT,CARD-USDT,FLY-USDT,CWS-USDT,XDC-USDT,XDC-ETH,STRK-BTC,STRK-ETH,SHIB-USDT,POLX-USDT,KDA-USDT,KDA-BTC,ICP-USDT,ICP-BTC,STC-USDT,STC-BTC,GOVI-USDT,GOVI-BTC,FKX-USDT,CELO-USDT,CELO-BTC,CUSD-USDT,CUSD-BTC,FCL-USDT,MATIC-USDT,MATIC-BTC,ELA-USDT,CRPT-USDT,OPCT-USDT,OGN-USDT,OGN-BTC,OUSD-USDT,OUSD-BTC,TLOS-USDT,TLOS-BTC,YOP-USDT,YOP-ETH,GLQ-USDT,GLQ-BTC,MXC-USDT,ERSDL-USDT,HOTCROSS-USDT,ADA3L-USDT,ADA3S-USDT,HYVE-USDT,HYVE-BTC,DAPPX-USDT,KONO-USDT,PRQ-USDT,MAHA-USDT,MAHA-BTC,FEAR-USDT,PYR-USDT,PYR-BTC,PROM-USDT,PROM-BTC,GLCH-USDT,UNO-USDT,ALBT-USDT,ALBT-ETH,XCAD-USDT,EOS3L-USDT,EOS3S-USDT,BCH3L-USDT,BCH3S-USDT,ELON-USDT,APL-USDT,FCL-ETH,VEED-USDT,VEED-BTC,DIVI-USDT,PDEX-BTC,JUP-USDT,JUP-ETH,POLS-USDT,POLS-BTC,LPOOL-USDT,LPOOL-BTC,LSS-USDT,VET3L-USDT,VET3S-USDT,LTC3L-USDT,LTC3S-USDT,ABBC-USDT,ABBC-BTC,KOK-USDT,ROSN-USDT,DORA-USDT,DORA-BTC,ZCX-USDT,ZCX-BTC,NORD-USDT,GMEE-USDT,SFUND-USDT,XAVA-USDT,AI-USDT,ALPACA-USDT,IOI-USDT,NFT-USDT,NFT-TRX,MNST-USDT,MEM-USDT,AGIX-USDT,AGIX-BTC,AGIX-ETH,CQT-USDT,AIOZ-USDT,MARSH-USDT,HAPI-USDT,MODEFI-USDT,MODEFI-BTC,YFDAI-USDT,YFDAI-BTC,GENS-USDT,FORM-USDT,ARRR-USDT,ARRR-BTC,TOKO-KCS,EXRD-USDT,NGM-USDT,LPT-USDT,STMX-USDT,ASD-USDT,BOND-USDT,HAI-BTC,SOUL-USDT,2CRZ-USDT,NEAR-USDT,NEAR-BTC,DFYN-USDT,OOE-USDT,CFG-USDT,CFG-BTC,AXS-USDT,CLV-USDT,ROUTE-USDT,KAR-USDT,EFX-USDT,XDC-BTC,SHFT-USDT,PMON-USDT,DPET-USDT,ERG-USDT,ERG-BTC,SOL-USDT,SLP-USDT,LITH-USDT,LITH-ETH,XCH-USDT,HAKA-USDT,LAYER-BTC,MTL-USDT,MTL-BTC,IOTX-USDT,GALA-USDT,REQ-USDT,TXA-USDT,TXA-USDC,CIRUS-USDT,QI-USDT,QI-BTC,ODDZ-USDT,PNT-USDT,PNT-BTC,XPR-USDT,XPR-BTC,TRIBE-USDT,SHFT-BTC,MOVR-USDT,MOVR-ETH,WOO-USDT,WILD-USDT,QRDO-USDT,QRDO-ETH,SDN-USDT,SDN-ETH,MAKI-USDT,MAKI-BTC,REP-USDT,REP-BTC,REP-ETH,BNT-USDT,BNT-BTC,BNT-ETH,OXT-USDT,OXT-BTC,OXT-ETH,BAL-USDT,BAL-BTC,BAL-ETH,STORJ-USDT,STORJ-BTC,STORJ-ETH,YGG-USDT,NDAU-USDT,SDAO-USDT,SDAO-ETH,XRP3L-USDT,XRP3S-USDT,SKL-USDT,SKL-BTC,NMR-USDT,NMR-BTC,IXS-USDT,TRB-USDT,TRB-BTC,DYDX-USDT,XYO-USDT,GTC-USDT,GTC-BTC,EQX-USDT,EQX-BTC,RLC-USDT,RLC-BTC,XPRT-USDT,EGLD-USDT,EGLD-BTC,HBAR-USDT,HBAR-BTC,DOGE3L-USDT,DOGE3S-USDT,FLOW-USDT,FLOW-BTC,NKN-USDT,NKN-BTC,PBX-USDT,SOL3L-USDT,SOL3S-USDT,MLN-USDT,MLN-BTC,XNL-USDT,SOLVE-USDT,WNCG-USDT,WNCG-BTC,DMTR-USDT,LINK3L-USDT,LINK3S-USDT,DOT3L-USDT,DOT3S-USDT,CTSI-USDT,CTSI-BTC,ALICE-USDT,ALICE-BTC,ALICE-ETH,OPUL-USDT,ILV-USDT,BAND-USDT,BAND-BTC,FTT-USDT,FTT-BTC,DVPN-USDT,SKU-USDT,SKU-BTC,EDG-USDT,SLIM-USDT,TLM-USDT,TLM-BTC,TLM-ETH,DEXE-USDT,DEXE-BTC,DEXE-ETH,MATTER-USDT,CUDOS-BTC,RUNE-USDT,RUNE-BTC,RMRK-USDT,BMON-USDT,C98-USDT,BLOK-USDT,SOLR-USDT,ATOM3L-USDT,ATOM3S-USDT,UNI3L-USDT,UNI3S-USDT,WSIENNA-USDT,PUSH-USDT,PUSH-BTC,FORM-ETH,NTVRK-USDT,NTVRK-USDC,AXS3L-USDT,AXS3S-USDT,FTM3L-USDT,FTM3S-USDT,FLAME-USDT,AGLD-USDT,NAKA-USDT,YLD-USDT,TONE-USDT,REEF-USDT,REEF-BTC,TIDAL-USDT,TVK-USDT,TVK-BTC,INJ-USDT,INJ-BTC,BNB3L-USDT,BNB3S-USDT,MATIC3L-USDT,MATIC3S-USDT,NFTB-USDT,VEGA-USDT,VEGA-ETH,ALPHA-USDT,ALPHA-BTC,BADGER-USDT,BADGER-BTC,UNO-BTC,ZKT-USDT,AR-USDT,AR-BTC,XVS-USDT,XVS-BTC,JASMY-USDT,PERP-USDT,PERP-BTC,GHST-USDT,GHST-BTC,SCLP-USDT,SCLP-BTC,SUPER-USDT,SUPER-BTC,CPOOL-USDT,HERO-USDT,BASIC-USDT,XED-USDT,XED-BTC,AURY-USDT,SWASH-USDT,LTO-USDT,LTO-BTC,BUX-USDT,MTRG-USDT,DREAMS-USDT,SHIB-DOGE,QUICK-USDT,QUICK-BTC,TRU-USDT,TRU-BTC,WRX-USDT,WRX-BTC,TKO-USDT,TKO-BTC,SUSHI3L-USDT,SUSHI3S-USDT,NEAR3L-USDT,NEAR3S-USDT,DATA-USDT,DATA-BTC,NORD-BTC,ISP-USDT,CERE-USDT,SHILL-USDT,HEGIC-USDT,HEGIC-BTC,ERN-USDT,ERN-BTC,FTG-USDT,PAXG-USDT,PAXG-BTC,AUDIO-USDT,AUDIO-BTC,ENS-USDT,AAVE3L-USDT,AAVE3S-USDT,SAND3L-USDT,SAND3S-USDT,XTM-USDT,MNW-USDT,FXS-USDT,FXS-BTC,ATA-USDT,ATA-BTC,VXV-USDT,LRC-BTC,LRC-ETH,DPR-USDT,CWAR-USDT,CWAR-BTC,FLUX-BTC,EDG-BTC,PBR-USDT,WNXM-USDT,WNXM-BTC,ANT-USDT,ANT-BTC,COV-USDT,SWP-USDT,TWT-USDT,TWT-BTC,OM-USDT,OM-BTC,ADX-USDT,AVAX3L-USDT,AVAX3S-USDT,MANA3L-USDT,MANA3S-USDT,GLM-USDT,GLM-BTC,BAKE-USDT,BAKE-BTC,BAKE-ETH,NUM-USDT,VLX-USDT,VLX-BTC,TRADE-USDT,TRADE-BTC,1EARTH-USDT,MONI-USDT,LIKE-USDT,MFT-USDT,MFT-BTC,LIT-USDT,LIT-BTC,KAVA-USDT,SFP-USDT,SFP-BTC,BURGER-USDT,BURGER-BTC,ILA-USDT,CREAM-USDT,CREAM-BTC,RSR-USDT,RSR-BTC,BUY-BTC,IMX-USDT,GODS-USDT,KMA-USDT,SRM-USDT,SRM-BTC,POLC-USDT,XTAG-USDT,MNET-USDT,NGC-USDT,HARD-USDT,GALAX3L-USDT,GALAX3S-USDT,UNIC-USDT,POND-USDT,POND-BTC,VR-USDT,EPIK-USDT,NGL-USDT,NGL-BTC,KDON-USDT,PEL-USDT,CIRUS-ETH,LINA-USDT,LINA-BTC,KLAY-USDT,KLAY-BTC,CREDI-USDT,TRVL-USDT,LACE-USDT,LACE-ETH,ARKER-USDT,BONDLY-USDT,BONDLY-ETH,XEC-USDT,HEART-USDT,HEART-BTC,UNB-USDT,GAFI-USDT,KOL-USDT,KOL-ETH,H3RO3S-USDT,FALCONS-USDT,UFO-USDT,CHMB-USDT,GEEQ-USDT,ORC-USDT,RACEFI-USDT,PEOPLE-USDT,ADS-USDT,ADS-BTC,OCEAN-USDT,SOS-USDT,WHALE-USDT,TIME-USDT,CWEB-USDT,IOTA-USDT,IOTA-BTC,OOKI-USDT,OOKI-BTC,HNT-USDT,HNT-BTC,GGG-USDT,POWR-USDT,REVU-USDT,CLH-USDT,PLGR-USDT,GLMR-USDT,GLMR-BTC,LOVE-USDT,CTC-USDT,CTC-BTC,GARI-USDT,FRR-USDT,ASTR-USDT,ASTR-BTC,ERTHA-USDT,FCON-USDT,ACA-USDT,ACA-BTC,MTS-USDT,ROAR-USDT,HBB-USDT,SURV-USDT,CVX-USDT,AMP-USDT,ACT-USDT,MJT-USDT,MJT-KCS,SHX-USDT,SHX-BTC,STARLY-USDT,ONSTON-USDT,RANKER-USDT,WMT-USDT,XNO-USDT,XNO-BTC,MARS4-USDT,TFUEL-USDT,TFUEL-BTC,METIS-USDT,LAVAX-USDT,WAL-USDT,BULL-USDT,SON-USDT,MELOS-USDT,APE-USDT,GMT-USDT,BICO-USDT,STG-USDT,LMR-USDT,LMR-BTC,LOKA-USDT,URUS-USDT,JAM-USDT,JAM-ETH,BNC-USDT,LBP-USDT,CFX-USDT,LOOKS-USDT,XCN-USDT,XCN-BTC,KP3R-USDT,TITAN-USDT,INDI-USDT,UPO-USDT,SPELL-USDT,SLCL-USDT,CEEK-USDT,VEMP-USDT,BETA-USDT,NHCT-USDT,ARNM-USDT,FRA-USDT,VISION-USDT,COCOS-USDT,ALPINE-USDT,BNX-USDT,ZBC-USDT,WOOP-USDT,T-USDT,NYM-USDT,VOXEL-USDT,VOXEL-ETH,PSTAKE-USDT,SPA-USDT,SPA-ETH,SYNR-USDT,DAR-USDT,DAR-BTC,MV-USDT,XDEFI-USDT,RACA-USDT,XWG-USDT,HAWK-USDT,TRVL-BTC,SWFTC-USDT,IDEX-USDT,BRWL-USDT,PLATO-USDT,TAUM-USDT,CELR-USDT,AURORA-USDT,POSI-USDT,COOHA-USDT,KNC-USDT,EPK-USDT,PLD-USDT,PSL-USDT,PKF-USDT,OVR-USDT,SYS-USDT,SYS-BTC,BRISE-USDT,DG-USDT,EPX-USDT,GST-USDT,PLY-USDT,GAL-USDT,BSW-USDT,FITFI-USDT,FSN-USDT,H2O-USDT,GMM-USDT,AKT-USDT,SIN-USDT,AUSD-USDT,BOBA-USDT,KARA-USDT,BFC-USDT,BIFI-USDT,DFA-USDT,KYL-USDT,FCD-USDT,MBL-USDT,CELT-USDT,DUSK-USDT,USDD-USDT,USDD-USDC,FITFI-USDC,MBOX-USDT,MBOX-BTC,APE-USDC,AVAX-USDC,SHIB-USDC,XCN-USDC,TRX-USDC,NEAR-USDC,MATIC-USDC,FTM-USDC,ZIL-USDC,SOL-USDC,MLS-USDT,AFK-USDT,AFK-USDC,ACH-USDT,SCRT-USDT,SCRT-BTC,APE3L-USDT,APE3S-USDT,STORE-USDT,STORE-ETH,GMT3L-USDT,GMT3S-USDT,CCD-USDT,DOSE-USDC,LUNC-USDT,LUNC-USDC,USTC-USDT,USTC-USDC,GMT-USDC,VRA-USDC,DOT-USDC,RUNE-USDC,ATOM-USDC,BNB-USDC,JASMY-USDC,KCS-USDC,KDA-USDC,ALGO-USDC,LUNA-USDC,OP-USDT,OP-USDC,JASMY3L-USDT,JASMY3S-USDT,EVER-USDT,MOOV-USDT,IHC-USDT,ICX-USDT,ICX-ETH,BTC-BRL,ETH-BRL,USDT-BRL,WELL-USDT,FORT-USDT,USDP-USDT,USDD-TRX,CSPR-USDT,CSPR-ETH,WEMIX-USDT,REV3L-USDT,OLE-USDT,LDO-USDT,LDO-USDC,CULT-USDT,SWFTC-USDC,FIDA-USDT,BUSD-USDT,RBP-USDT,SRBP-USDT,HIBAYC-USDT,BUSD-USDC,OGV-USDT,WOMBAT-USDT,HIPUNKS-USDT,FT-USDT,ETC-USDC,HIENS4-USDT,EGAME-USDT,EGAME-BTC,STEPWATCH-USDT,HISAND33-USDT,DC-USDT,NEER-USDT,RVN-USDT,HIENS3-USDT,MC-USDT,PEEL-USDT,PEEL-BTC,SDL-USDT,SDL-BTC,SWEAT-USDT,HIODBS-USDT,CMP-USDT,PIX-USDT,MPLX-USDT,HIDOODLES-USDT,ETHW-USDT,QUARTZ-USDT,ACQ-USDT,ACQ-USDC,AOG-USDT,HIMAYC-USDT,PRMX-USDT,RED-USDT,PUMLX-USDT,XETA-USDT,GEM-USDT,DERC-USDT,P00LS-USDT,P00LS-USDC,KICKS-USDT,TRIBL-USDT,GMX-USDT,HIOD-USDT,POKT-USDT,EFI-USDT,APT-USDT,BBC-USDT,EUL-USDT,TON-USDT,PIAS-USDT,HIMEEBITS-USDT,HISQUIGGLE-USDT,XCV-USDT,HFT-USDT,HFT-USDC,ECOX-USDT,AMB-USDT,AZERO-USDT,HIFIDENZA-USDT,BEAT-USDT", "requestFormat": { "uppercase": true, @@ -2009,7 +2009,7 @@ }, "margin": { "assetEnabled": true, - "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC", + "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MTV-BTC", "available": "BTC-USDT,MHC-ETH,MHC-BTC,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MHC-USDT,XMR-BTC,XMR-ETH,RIF-BTC,MTV-BTC,MTV-ETH,CRO-BTC,MTV-USDT,KMD-BTC,KMD-USDT,RFOX-USDT,TEL-USDT,TT-USDT,AERGO-USDT,XMR-USDT,TRX-KCS,ATOM-BTC,ATOM-ETH,ATOM-USDT,ATOM-KCS,ETN-USDT,FTM-USDT,TOMO-USDT,VSYS-USDT,OCEAN-BTC,OCEAN-ETH,CHR-BTC,CHR-USDT,FX-BTC,FX-ETH,NIM-BTC,NIM-ETH,COTI-BTC,COTI-USDT,NRG-ETH,BNB-BTC,BNB-USDT,JAR-BTC,JAR-USDT,ALGO-BTC,ALGO-ETH,ALGO-USDT,XEM-BTC,XEM-USDT,CIX100-USDT,XTZ-BTC,XTZ-USDT,ZEC-BTC,ZEC-USDT,ADA-BTC,ADA-USDT,REV-USDT,WXT-BTC,WXT-USDT,FORESTPLUS-BTC,FORESTPLUS-USDT,BOLT-BTC,BOLT-USDT,ARPA-USDT,CHZ-BTC,CHZ-USDT,DAPPT-BTC,DAPPT-USDT,NOIA-BTC,NOIA-USDT,WIN-BTC,WIN-USDT,DERO-BTC,DERO-USDT,BTT-USDT,EOSC-USDT,ENQ-BTC,ENQ-USDT,ONE-BTC,ONE-USDT,TOKO-BTC,TOKO-USDT,VID-BTC,VID-USDT,LUNA-USDT,SXP-BTC,SXP-USDT,AKRO-BTC,AKRO-USDT,ROOBEE-BTC,WIN-TRX,MAP-BTC,MAP-USDT,AMPL-BTC,AMPL-USDT,DAG-USDT,POL-USDT,ARX-USDT,NWC-BTC,NWC-USDT,BEPRO-BTC,BEPRO-USDT,VRA-BTC,VRA-USDT,KSM-BTC,KSM-USDT,DASH-USDT,SUTER-USDT,ACOIN-USDT,SUTER-BTC,SENSO-USDT,PRE-BTC,XDB-USDT,SYLO-USDT,WOM-USDT,SENSO-BTC,DGB-USDT,LYXE-USDT,LYXE-ETH,XDB-BTC,STX-BTC,STX-USDT,XSR-USDT,COMP-USDT,CRO-USDT,KAI-USDT,KAI-BTC,WEST-BTC,WEST-USDT,EWT-BTC,WAVES-USDT,WAVES-BTC,ORN-USDT,AMPL-ETH,BNS-USDT,MKR-USDT,SUKU-BTC,MLK-BTC,MLK-USDT,JST-USDT,KAI-ETH,SUKU-USDT,DIA-USDT,DIA-BTC,LINK-BTC,LINK-USDT,DOT-USDT,DOT-BTC,SHA-BTC,SHA-USDT,EWT-USDT,USDJ-USDT,EFX-BTC,CKB-BTC,CKB-USDT,UMA-USDT,ALEPH-USDT,VELO-USDT,SUN-USDT,BUY-USDT,YFI-USDT,OXEN-USDT,UNI-USDT,UOS-USDT,UOS-BTC,NIM-USDT,DEGO-USDT,DEGO-ETH,UDOO-ETH,RFUEL-USDT,FIL-USDT,UBX-ETH,REAP-USDT,AAVE-USDT,AAVE-BTC,TONE-BTC,TONE-ETH,ELF-ETH,AERGO-BTC,IOST-ETH,KCS-USDT,SNX-ETH,TOMO-ETH,KCS-ETH,DRGN-BTC,WAN-ETH,NULS-ETH,AXPR-ETH,POWR-BTC,QTUM-BTC,MANA-BTC,TEL-BTC,XYO-ETH,AXPR-BTC,ETN-BTC,COV-ETH,VET-BTC,KCS-BTC,CAPP-ETH,ONT-BTC,DRGN-ETH,DAG-ETH,TOMO-BTC,WAN-BTC,KNC-ETH,CRPT-ETH,LTC-USDT,BAX-ETH,BSV-USDT,DENT-ETH,AION-ETH,LYM-ETH,TRAC-ETH,ENJ-BTC,WAXP-BTC,DGB-BTC,ELA-BTC,ZIL-BTC,BSV-BTC,XLM-USDT,IOTX-ETH,SOUL-BTC,DOCK-BTC,AMB-ETH,TRX-BTC,XRP-TUSD,NULS-BTC,ETH-DAI,LSK-BTC,GMB-ETH,GMB-BTC,NEO-ETH,OMG-ETH,BTC-TUSD,KAT-USDT,KNC-BTC,ELF-BTC,MANA-ETH,ETC-USDT,ONT-ETH,MKR-BTC,KAT-BTC,XRP-USDC,XYO-BTC,SNT-ETH,ZRX-BTC,LOOM-ETH,AION-BTC,POWR-ETH,OLT-ETH,OLT-BTC,SNT-BTC,TRAC-BTC,XLM-ETH,ETH-USDT,BSV-ETH,TRX-ETH,ETN-ETH,AOA-USDT,BCD-BTC,DENT-BTC,DOCK-ETH,KEY-BTC,EOS-KCS,XLM-BTC,ADB-ETH,TIME-ETH,CVC-BTC,LSK-ETH,QKC-BTC,AMB-BTC,USDT-TUSD,ETC-ETH,XRP-BTC,NEO-KCS,SNX-USDT,CRPT-BTC,IOTX-BTC,LTC-ETH,XRP-KCS,ADB-BTC,LTC-KCS,TEL-ETH,DCR-ETH,LYM-USDT,USDT-USDC,ETH-USDC,DAG-BTC,AVA-BTC,BTC-USDT,WAXP-ETH,XRP-USDT,KEY-ETH,VET-ETH,FTM-BTC,USDT-DAI,QKC-ETH,ETH-BTC,MAN-BTC,CPC-ETH,TRX-USDT,BTC-DAI,ONT-USDT,DASH-ETH,BAX-BTC,AVA-ETH,LOOM-BTC,MVP-BTC,MKR-ETH,COV-BTC,CPC-BTC,REQ-ETH,EOS-BTC,LTC-BTC,XRP-ETH,CAPP-BTC,FTM-ETH,BCD-ETH,ZRX-ETH,DGB-ETH,VET-USDT,REQ-BTC,UTK-BTC,PLAY-BTC,UTK-ETH,SNX-BTC,MVP-ETH,NEO-BTC,SOUL-ETH,NEO-USDT,ELA-ETH,OMG-BTC,TIME-BTC,AOA-BTC,ETC-BTC,DCR-BTC,BTC-USDC,ENJ-ETH,IOST-BTC,DASH-BTC,EOS-USDT,EOS-ETH,ZIL-ETH,ETH-TUSD,GAS-BTC,LYM-BTC,BCH-BTC,VSYS-BTC,BCH-USDT,MKR-DAI,SOLVE-BTC,GRIN-BTC,GRIN-USDT,UQC-BTC,UQC-ETH,OPCT-BTC,OPCT-ETH,PRE-USDT,SHR-BTC,SHR-USDT,UBXT-USDT,ROSE-USDT,USDC-USDT,CTI-USDT,CTI-ETH,ETH2-ETH,BUX-BTC,XHV-USDT,PLU-USDT,GRT-USDT,CAS-BTC,CAS-USDT,MSWAP-BTC,MSWAP-USDT,GOM2-BTC,GOM2-USDT,REVV-BTC,REVV-USDT,LON-USDT,1INCH-USDT,LOC-USDT,API3-USDT,UNFI-USDT,HTR-USDT,FRONT-USDT,FRONT-BTC,WBTC-BTC,WBTC-ETH,MIR-USDT,LTC-USDC,BCH-USDC,HYDRA-USDT,DFI-USDT,DFI-BTC,CRV-USDT,SUSHI-USDT,FRM-USDT,EOS-USDC,BSV-USDC,ZEN-USDT,CUDOS-USDT,ADA-USDC,REN-USDT,LRC-USDT,LINK-USDC,KLV-USDT,KLV-BTC,BOA-USDT,THETA-USDT,QNT-USDT,BAT-USDT,DOGE-USDT,DOGE-USDC,DAO-USDT,STRONG-USDT,TRIAS-USDT,TRIAS-BTC,DOGE-BTC,MITX-BTC,MITX-USDT,CAKE-USDT,ORAI-USDT,ZEE-USDT,LTX-USDT,LTX-BTC,MASK-USDT,KLV-TRX,IDEA-USDT,PHA-USDT,PHA-ETH,BCH-KCS,SRK-USDT,SRK-BTC,ADA-KCS,HTR-BTC,BSV-KCS,DOT-KCS,LINK-KCS,MIR-KCS,BNB-KCS,XLM-KCS,VET-KCS,SWINGBY-USDT,SWINGBY-BTC,XHV-BTC,DASH-KCS,UNI-KCS,AAVE-KCS,DOGE-KCS,ZEC-KCS,XTZ-KCS,GRT-KCS,ALGO-KCS,EWT-KCS,GAS-USDT,AVAX-USDT,AVAX-BTC,KRL-BTC,KRL-USDT,POLK-USDT,POLK-BTC,ENJ-USDT,MANA-USDT,RNDR-USDT,RNDR-BTC,RLY-USDT,ANC-USDT,SKEY-USDT,LAYER-USDT,TARA-USDT,TARA-ETH,IOST-USDT,DYP-USDT,DYP-ETH,XYM-USDT,XYM-BTC,PCX-USDT,PCX-BTC,ORBS-USDT,ORBS-BTC,BTC3L-USDT,BTC3S-USDT,ETH3L-USDT,ETH3S-USDT,ANKR-USDT,DSLA-USDT,DSLA-BTC,SAND-USDT,VAI-USDT,XCUR-USDT,XCUR-BTC,FLUX-USDT,OMG-USDT,ZIL-USDT,DODO-USDT,MAN-USDT,BAX-USDT,BOSON-USDT,BOSON-ETH,PUNDIX-USDT,PUNDIX-BTC,WAXP-USDT,HT-USDT,PDEX-USDT,LABS-USDT,LABS-ETH,GMB-USDT,PHNX-USDT,PHNX-BTC,HAI-USDT,EQZ-USDT,FORTH-USDT,HORD-USDT,CGG-USDT,UBX-USDT,GHX-USDT,TCP-USDT,STND-USDT,STND-ETH,TOWER-USDT,TOWER-BTC,ACE-USDT,LOCG-USDT,CARD-USDT,FLY-USDT,CWS-USDT,XDC-USDT,XDC-ETH,STRK-BTC,STRK-ETH,SHIB-USDT,POLX-USDT,KDA-USDT,KDA-BTC,ICP-USDT,ICP-BTC,STC-USDT,STC-BTC,GOVI-USDT,GOVI-BTC,FKX-USDT,CELO-USDT,CELO-BTC,CUSD-USDT,CUSD-BTC,FCL-USDT,MATIC-USDT,MATIC-BTC,ELA-USDT,CRPT-USDT,OPCT-USDT,OGN-USDT,OGN-BTC,OUSD-USDT,OUSD-BTC,TLOS-USDT,TLOS-BTC,YOP-USDT,YOP-ETH,GLQ-USDT,GLQ-BTC,MXC-USDT,ERSDL-USDT,HOTCROSS-USDT,ADA3L-USDT,ADA3S-USDT,HYVE-USDT,HYVE-BTC,DAPPX-USDT,KONO-USDT,PRQ-USDT,MAHA-USDT,MAHA-BTC,FEAR-USDT,PYR-USDT,PYR-BTC,PROM-USDT,PROM-BTC,GLCH-USDT,UNO-USDT,ALBT-USDT,ALBT-ETH,XCAD-USDT,EOS3L-USDT,EOS3S-USDT,BCH3L-USDT,BCH3S-USDT,ELON-USDT,APL-USDT,FCL-ETH,VEED-USDT,VEED-BTC,DIVI-USDT,PDEX-BTC,JUP-USDT,JUP-ETH,POLS-USDT,POLS-BTC,LPOOL-USDT,LPOOL-BTC,LSS-USDT,VET3L-USDT,VET3S-USDT,LTC3L-USDT,LTC3S-USDT,ABBC-USDT,ABBC-BTC,KOK-USDT,ROSN-USDT,DORA-USDT,DORA-BTC,ZCX-USDT,ZCX-BTC,NORD-USDT,GMEE-USDT,SFUND-USDT,XAVA-USDT,AI-USDT,ALPACA-USDT,IOI-USDT,NFT-USDT,NFT-TRX,MNST-USDT,MEM-USDT,AGIX-USDT,AGIX-BTC,AGIX-ETH,CQT-USDT,AIOZ-USDT,MARSH-USDT,HAPI-USDT,MODEFI-USDT,MODEFI-BTC,YFDAI-USDT,YFDAI-BTC,GENS-USDT,FORM-USDT,ARRR-USDT,ARRR-BTC,TOKO-KCS,EXRD-USDT,NGM-USDT,LPT-USDT,STMX-USDT,ASD-USDT,BOND-USDT,HAI-BTC,SOUL-USDT,2CRZ-USDT,NEAR-USDT,NEAR-BTC,DFYN-USDT,OOE-USDT,CFG-USDT,CFG-BTC,AXS-USDT,CLV-USDT,ROUTE-USDT,KAR-USDT,EFX-USDT,XDC-BTC,SHFT-USDT,PMON-USDT,DPET-USDT,ERG-USDT,ERG-BTC,SOL-USDT,SLP-USDT,LITH-USDT,LITH-ETH,XCH-USDT,HAKA-USDT,LAYER-BTC,MTL-USDT,MTL-BTC,IOTX-USDT,GALA-USDT,REQ-USDT,TXA-USDT,TXA-USDC,CIRUS-USDT,QI-USDT,QI-BTC,ODDZ-USDT,PNT-USDT,PNT-BTC,XPR-USDT,XPR-BTC,TRIBE-USDT,SHFT-BTC,MOVR-USDT,MOVR-ETH,WOO-USDT,WILD-USDT,QRDO-USDT,QRDO-ETH,SDN-USDT,SDN-ETH,MAKI-USDT,MAKI-BTC,REP-USDT,REP-BTC,REP-ETH,BNT-USDT,BNT-BTC,BNT-ETH,OXT-USDT,OXT-BTC,OXT-ETH,BAL-USDT,BAL-BTC,BAL-ETH,STORJ-USDT,STORJ-BTC,STORJ-ETH,YGG-USDT,NDAU-USDT,SDAO-USDT,SDAO-ETH,XRP3L-USDT,XRP3S-USDT,SKL-USDT,SKL-BTC,NMR-USDT,NMR-BTC,IXS-USDT,TRB-USDT,TRB-BTC,DYDX-USDT,XYO-USDT,GTC-USDT,GTC-BTC,EQX-USDT,EQX-BTC,RLC-USDT,RLC-BTC,XPRT-USDT,EGLD-USDT,EGLD-BTC,HBAR-USDT,HBAR-BTC,DOGE3L-USDT,DOGE3S-USDT,FLOW-USDT,FLOW-BTC,NKN-USDT,NKN-BTC,PBX-USDT,SOL3L-USDT,SOL3S-USDT,MLN-USDT,MLN-BTC,XNL-USDT,SOLVE-USDT,WNCG-USDT,WNCG-BTC,DMTR-USDT,LINK3L-USDT,LINK3S-USDT,DOT3L-USDT,DOT3S-USDT,CTSI-USDT,CTSI-BTC,ALICE-USDT,ALICE-BTC,ALICE-ETH,OPUL-USDT,ILV-USDT,BAND-USDT,BAND-BTC,FTT-USDT,FTT-BTC,DVPN-USDT,SKU-USDT,SKU-BTC,EDG-USDT,SLIM-USDT,TLM-USDT,TLM-BTC,TLM-ETH,DEXE-USDT,DEXE-BTC,DEXE-ETH,MATTER-USDT,CUDOS-BTC,RUNE-USDT,RUNE-BTC,RMRK-USDT,BMON-USDT,C98-USDT,BLOK-USDT,SOLR-USDT,ATOM3L-USDT,ATOM3S-USDT,UNI3L-USDT,UNI3S-USDT,WSIENNA-USDT,PUSH-USDT,PUSH-BTC,FORM-ETH,NTVRK-USDT,NTVRK-USDC,AXS3L-USDT,AXS3S-USDT,FTM3L-USDT,FTM3S-USDT,FLAME-USDT,AGLD-USDT,NAKA-USDT,YLD-USDT,TONE-USDT,REEF-USDT,REEF-BTC,TIDAL-USDT,TVK-USDT,TVK-BTC,INJ-USDT,INJ-BTC,BNB3L-USDT,BNB3S-USDT,MATIC3L-USDT,MATIC3S-USDT,NFTB-USDT,VEGA-USDT,VEGA-ETH,ALPHA-USDT,ALPHA-BTC,BADGER-USDT,BADGER-BTC,UNO-BTC,ZKT-USDT,AR-USDT,AR-BTC,XVS-USDT,XVS-BTC,JASMY-USDT,PERP-USDT,PERP-BTC,GHST-USDT,GHST-BTC,SCLP-USDT,SCLP-BTC,SUPER-USDT,SUPER-BTC,CPOOL-USDT,HERO-USDT,BASIC-USDT,XED-USDT,XED-BTC,AURY-USDT,SWASH-USDT,LTO-USDT,LTO-BTC,BUX-USDT,MTRG-USDT,DREAMS-USDT,SHIB-DOGE,QUICK-USDT,QUICK-BTC,TRU-USDT,TRU-BTC,WRX-USDT,WRX-BTC,TKO-USDT,TKO-BTC,SUSHI3L-USDT,SUSHI3S-USDT,NEAR3L-USDT,NEAR3S-USDT,DATA-USDT,DATA-BTC,NORD-BTC,ISP-USDT,CERE-USDT,SHILL-USDT,HEGIC-USDT,HEGIC-BTC,ERN-USDT,ERN-BTC,FTG-USDT,PAXG-USDT,PAXG-BTC,AUDIO-USDT,AUDIO-BTC,ENS-USDT,AAVE3L-USDT,AAVE3S-USDT,SAND3L-USDT,SAND3S-USDT,XTM-USDT,MNW-USDT,FXS-USDT,FXS-BTC,ATA-USDT,ATA-BTC,VXV-USDT,LRC-BTC,LRC-ETH,DPR-USDT,CWAR-USDT,CWAR-BTC,FLUX-BTC,EDG-BTC,PBR-USDT,WNXM-USDT,WNXM-BTC,ANT-USDT,ANT-BTC,COV-USDT,SWP-USDT,TWT-USDT,TWT-BTC,OM-USDT,OM-BTC,ADX-USDT,AVAX3L-USDT,AVAX3S-USDT,MANA3L-USDT,MANA3S-USDT,GLM-USDT,GLM-BTC,BAKE-USDT,BAKE-BTC,BAKE-ETH,NUM-USDT,VLX-USDT,VLX-BTC,TRADE-USDT,TRADE-BTC,1EARTH-USDT,MONI-USDT,LIKE-USDT,MFT-USDT,MFT-BTC,LIT-USDT,LIT-BTC,KAVA-USDT,SFP-USDT,SFP-BTC,BURGER-USDT,BURGER-BTC,ILA-USDT,CREAM-USDT,CREAM-BTC,RSR-USDT,RSR-BTC,BUY-BTC,IMX-USDT,GODS-USDT,KMA-USDT,SRM-USDT,SRM-BTC,POLC-USDT,XTAG-USDT,MNET-USDT,NGC-USDT,HARD-USDT,GALAX3L-USDT,GALAX3S-USDT,UNIC-USDT,POND-USDT,POND-BTC,VR-USDT,EPIK-USDT,NGL-USDT,NGL-BTC,KDON-USDT,PEL-USDT,CIRUS-ETH,LINA-USDT,LINA-BTC,KLAY-USDT,KLAY-BTC,CREDI-USDT,TRVL-USDT,LACE-USDT,LACE-ETH,ARKER-USDT,BONDLY-USDT,BONDLY-ETH,XEC-USDT,HEART-USDT,HEART-BTC,UNB-USDT,GAFI-USDT,KOL-USDT,KOL-ETH,H3RO3S-USDT,FALCONS-USDT,UFO-USDT,CHMB-USDT,GEEQ-USDT,ORC-USDT,RACEFI-USDT,PEOPLE-USDT,ADS-USDT,ADS-BTC,OCEAN-USDT,SOS-USDT,WHALE-USDT,TIME-USDT,CWEB-USDT,IOTA-USDT,IOTA-BTC,OOKI-USDT,OOKI-BTC,HNT-USDT,HNT-BTC,GGG-USDT,POWR-USDT,REVU-USDT,CLH-USDT,PLGR-USDT,GLMR-USDT,GLMR-BTC,LOVE-USDT,CTC-USDT,CTC-BTC,GARI-USDT,FRR-USDT,ASTR-USDT,ASTR-BTC,ERTHA-USDT,FCON-USDT,ACA-USDT,ACA-BTC,MTS-USDT,ROAR-USDT,HBB-USDT,SURV-USDT,CVX-USDT,AMP-USDT,ACT-USDT,MJT-USDT,MJT-KCS,SHX-USDT,SHX-BTC,STARLY-USDT,ONSTON-USDT,RANKER-USDT,WMT-USDT,XNO-USDT,XNO-BTC,MARS4-USDT,TFUEL-USDT,TFUEL-BTC,METIS-USDT,LAVAX-USDT,WAL-USDT,BULL-USDT,SON-USDT,MELOS-USDT,APE-USDT,GMT-USDT,BICO-USDT,STG-USDT,LMR-USDT,LMR-BTC,LOKA-USDT,URUS-USDT,JAM-USDT,JAM-ETH,BNC-USDT,LBP-USDT,CFX-USDT,LOOKS-USDT,XCN-USDT,XCN-BTC,KP3R-USDT,TITAN-USDT,INDI-USDT,UPO-USDT,SPELL-USDT,SLCL-USDT,CEEK-USDT,VEMP-USDT,BETA-USDT,NHCT-USDT,ARNM-USDT,FRA-USDT,VISION-USDT,COCOS-USDT,ALPINE-USDT,BNX-USDT,ZBC-USDT,WOOP-USDT,T-USDT,NYM-USDT,VOXEL-USDT,VOXEL-ETH,PSTAKE-USDT,SPA-USDT,SPA-ETH,SYNR-USDT,DAR-USDT,DAR-BTC,MV-USDT,XDEFI-USDT,RACA-USDT,XWG-USDT,HAWK-USDT,TRVL-BTC,SWFTC-USDT,IDEX-USDT,BRWL-USDT,PLATO-USDT,TAUM-USDT,CELR-USDT,AURORA-USDT,POSI-USDT,COOHA-USDT,KNC-USDT,EPK-USDT,PLD-USDT,PSL-USDT,PKF-USDT,OVR-USDT,SYS-USDT,SYS-BTC,BRISE-USDT,DG-USDT,EPX-USDT,GST-USDT,PLY-USDT,GAL-USDT,BSW-USDT,FITFI-USDT,FSN-USDT,H2O-USDT,GMM-USDT,AKT-USDT,SIN-USDT,AUSD-USDT,BOBA-USDT,KARA-USDT,BFC-USDT,BIFI-USDT,DFA-USDT,KYL-USDT,FCD-USDT,MBL-USDT,CELT-USDT,DUSK-USDT,USDD-USDT,USDD-USDC,FITFI-USDC,MBOX-USDT,MBOX-BTC,APE-USDC,AVAX-USDC,SHIB-USDC,XCN-USDC,TRX-USDC,NEAR-USDC,MATIC-USDC,FTM-USDC,ZIL-USDC,SOL-USDC,MLS-USDT,AFK-USDT,AFK-USDC,ACH-USDT,SCRT-USDT,SCRT-BTC,APE3L-USDT,APE3S-USDT,STORE-USDT,STORE-ETH,GMT3L-USDT,GMT3S-USDT,CCD-USDT,DOSE-USDC,LUNC-USDT,LUNC-USDC,USTC-USDT,USTC-USDC,GMT-USDC,VRA-USDC,DOT-USDC,RUNE-USDC,ATOM-USDC,BNB-USDC,JASMY-USDC,KCS-USDC,KDA-USDC,ALGO-USDC,LUNA-USDC,OP-USDT,OP-USDC,JASMY3L-USDT,JASMY3S-USDT,EVER-USDT,MOOV-USDT,IHC-USDT,ICX-USDT,ICX-ETH,BTC-BRL,ETH-BRL,USDT-BRL,WELL-USDT,FORT-USDT,USDP-USDT,USDD-TRX,CSPR-USDT,CSPR-ETH,WEMIX-USDT,REV3L-USDT,OLE-USDT,LDO-USDT,LDO-USDC,CULT-USDT,SWFTC-USDC,FIDA-USDT,BUSD-USDT,RBP-USDT,SRBP-USDT,HIBAYC-USDT,BUSD-USDC,OGV-USDT,WOMBAT-USDT,HIPUNKS-USDT,FT-USDT,ETC-USDC,HIENS4-USDT,EGAME-USDT,EGAME-BTC,STEPWATCH-USDT,HISAND33-USDT,DC-USDT,NEER-USDT,RVN-USDT,HIENS3-USDT,MC-USDT,PEEL-USDT,PEEL-BTC,SDL-USDT,SDL-BTC,SWEAT-USDT,HIODBS-USDT,CMP-USDT,PIX-USDT,MPLX-USDT,HIDOODLES-USDT,ETHW-USDT,QUARTZ-USDT,ACQ-USDT,ACQ-USDC,AOG-USDT,HIMAYC-USDT,PRMX-USDT,RED-USDT,PUMLX-USDT,XETA-USDT,GEM-USDT,DERC-USDT,P00LS-USDT,P00LS-USDC,KICKS-USDT,TRIBL-USDT,GMX-USDT,HIOD-USDT,POKT-USDT,EFI-USDT,APT-USDT,BBC-USDT,EUL-USDT,TON-USDT,PIAS-USDT,HIMEEBITS-USDT,HISQUIGGLE-USDT,XCV-USDT,HFT-USDT,HFT-USDC,ECOX-USDT,AMB-USDT,AZERO-USDT,HIFIDENZA-USDT,BEAT-USDT", "requestFormat": { "uppercase": true, From 929957cbd65af88598561bc5296be1ae11ced1e9 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Tue, 21 Nov 2023 12:45:08 +1100 Subject: [PATCH 35/40] kucoin: Fix fetching fee rates for multiple pairs (#1370) * kucoin: Fix fetching fee rates for multiple pairs * Update exchanges/kucoin/kucoin_test.go Co-authored-by: Adrian Gallagher * NITS: WOW * glorious: nits --------- Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Adrian Gallagher --- exchanges/kucoin/kucoin.go | 11 +++++---- exchanges/kucoin/kucoin_test.go | 38 ++++++++++++++++++++++++++++-- exchanges/kucoin/kucoin_types.go | 9 ++++--- exchanges/kucoin/kucoin_wrapper.go | 2 +- 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/exchanges/kucoin/kucoin.go b/exchanges/kucoin/kucoin.go index 8bd3494fdde..2c71d13cc0a 100644 --- a/exchanges/kucoin/kucoin.go +++ b/exchanges/kucoin/kucoin.go @@ -1647,13 +1647,14 @@ func (ku *Kucoin) GetBasicFee(ctx context.Context, currencyType string) (*Fees, } // GetTradingFee get fee rate of trading pairs -func (ku *Kucoin) GetTradingFee(ctx context.Context, symbols string) ([]Fees, error) { - params := url.Values{} - if symbols != "" { - params.Set("symbols", symbols) +// WARNING: There is a limit of 10 currency pairs allowed to be requested per call. +func (ku *Kucoin) GetTradingFee(ctx context.Context, pairs currency.Pairs) ([]Fees, error) { + if len(pairs) == 0 { + return nil, currency.ErrCurrencyPairsEmpty } + path := kucoinTradingFee + "?symbols=" + pairs.Upper().Join() var resp []Fees - return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinTradingFee, params), nil, &resp) + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, path, nil, &resp) } // SendHTTPRequest sends an unauthenticated HTTP request diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index 9694507fbbc..e954fefcd75 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -1096,11 +1096,45 @@ func TestGetBasicFee(t *testing.T) { func TestGetTradingFee(t *testing.T) { t.Parallel() + + _, err := ku.GetTradingFee(context.Background(), nil) + if !errors.Is(err, currency.ErrCurrencyPairsEmpty) { + t.Fatalf("received %v, expected %v", err, currency.ErrCurrencyPairsEmpty) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) - _, err := ku.GetTradingFee(context.Background(), "BTC-USDT") + avail, err := ku.GetAvailablePairs(asset.Spot) if err != nil { - t.Error("GetTradingFee() error", err) + t.Fatal(err) + } + + pairs := currency.Pairs{avail[0]} + btcusdTradingFee, err := ku.GetTradingFee(context.Background(), pairs) + if !errors.Is(err, nil) { + t.Fatalf("received %v, expected %v", err, nil) + } + + if len(btcusdTradingFee) != 1 { + t.Error("GetTradingFee() error, expected 1 pair") + } + + // NOTE: Test below will error out from an external call as this will exceed + // the allowed pairs. If this does not error then this endpoint will allow + // more items to be requested. + pairs = append(pairs, avail[1:11]...) + _, err = ku.GetTradingFee(context.Background(), pairs) + if errors.Is(err, nil) { + t.Fatalf("received %v, expected %v", err, "code: 200000 message: symbols size invalid.") + } + + got, err := ku.GetTradingFee(context.Background(), pairs[:10]) + if !errors.Is(err, nil) { + t.Fatalf("received %v, expected %v", err, nil) + } + + if len(got) != 10 { + t.Error("GetTradingFee() error, expected 10 pairs") } } diff --git a/exchanges/kucoin/kucoin_types.go b/exchanges/kucoin/kucoin_types.go index af5a1faea65..45419bc01b3 100644 --- a/exchanges/kucoin/kucoin_types.go +++ b/exchanges/kucoin/kucoin_types.go @@ -56,11 +56,14 @@ func (e Error) GetError() error { return err } switch code { - case 200000, 200: + case 200: return nil - default: - return fmt.Errorf("code: %s message: %s", e.Code, e.Msg) + case 200000: + if e.Msg == "" { + return nil + } } + return fmt.Errorf("code: %s message: %s", e.Code, e.Msg) } // SymbolInfo stores symbol information diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index 98906dd5514..d9919ffee68 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -1351,7 +1351,7 @@ func (ku *Kucoin) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuil switch feeBuilder.FeeType { case exchange.CryptocurrencyWithdrawalFee, exchange.CryptocurrencyTradeFee: - fee, err := ku.GetTradingFee(ctx, feeBuilder.Pair.String()) + fee, err := ku.GetTradingFee(ctx, currency.Pairs{feeBuilder.Pair}) if err != nil { return 0, err } From 86e091028fc0aa19fc9bb457dc4b49401710217b Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Tue, 21 Nov 2023 02:47:02 +0100 Subject: [PATCH 36/40] Engine: Fix panic when exchange errors (#1400) BTSE is currently failing startup. This fix simply prevents that from panicing. --- engine/helpers_test.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/engine/helpers_test.go b/engine/helpers_test.go index 56c3dd5798e..3055e944a64 100644 --- a/engine/helpers_test.go +++ b/engine/helpers_test.go @@ -17,6 +17,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/common/file" @@ -1372,10 +1373,8 @@ func TestNewSupportedExchangeByName(t *testing.T) { func TestNewExchangeByNameWithDefaults(t *testing.T) { t.Parallel() - _, err := NewExchangeByNameWithDefaults(context.Background(), "meow") - if !errors.Is(err, ErrExchangeNotFound) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrExchangeNotFound) - } + _, err := NewExchangeByNameWithDefaults(context.Background(), "moarunlikelymeow") + assert.ErrorIs(t, err, ErrExchangeNotFound, "Invalid exchange name should error") for x := range exchange.Exchanges { name := exchange.Exchanges[x] t.Run(name, func(t *testing.T) { @@ -1387,11 +1386,8 @@ func TestNewExchangeByNameWithDefaults(t *testing.T) { t.Skipf("skipping %s unsupported", name) } exch, err := NewExchangeByNameWithDefaults(context.Background(), name) - if err != nil { - t.Error(err) - } - if !strings.EqualFold(exch.GetName(), name) { - t.Errorf("received: '%v' but expected: '%v'", exch.GetName(), name) + if assert.NoError(t, err, "NewExchangeByNameWithDefaults should not error") { + assert.Equal(t, name, strings.ToLower(exch.GetName()), "Should get correct exchange name") } }) } From 0fd433e86557b088e91470dd6e4172c275fb2a9c Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Wed, 22 Nov 2023 10:20:09 +1100 Subject: [PATCH 37/40] exchanges/bittrex,kucoin: Remove exchange implementation and fix minor test issue (#1403) * exchanges/Bittrex: Remove exchange implementation * Kucoin: Fix TestProcessMarketSnapshot after pair removal update * Kucoin: Fix race due to duplicate setupWS call Unleash your inner Max Verstappen * Kucoin: Actually test spot/margin market snapshot replication --- README.md | 1 - backtester/report/report_test.go | 4 +- cmd/apichecker/testupdates.json | 11 - cmd/apichecker/updates.json | 11 - .../exchanges_templates/bittrex.tmpl | 103 -- .../exchanges_trade_readme.tmpl | 1 - .../root_templates/root_readme.tmpl | 1 - .../exchange_wrapper_standards_test.go | 1 - config_example.json | 78 -- docs/ADD_NEW_EXCHANGE.md | 2 - docs/MULTICHAIN_TRANSFER_SUPPORT.md | 1 - docs/OHLCV.md | 1 - engine/helpers.go | 3 - exchanges/binance/binance.go | 2 +- exchanges/bitmex/bitmex.go | 2 +- exchanges/bittrex/README.md | 137 -- exchanges/bittrex/bittrex.go | 488 -------- exchanges/bittrex/bittrex_test.go | 747 ----------- exchanges/bittrex/bittrex_types.go | 305 ----- exchanges/bittrex/bittrex_websocket.go | 624 ---------- exchanges/bittrex/bittrex_wrapper.go | 1108 ----------------- exchanges/bittrex/bittrex_ws_orderbook.go | 437 ------- exchanges/kucoin/kucoin_test.go | 6 +- .../kucoin/testdata/wsMarketSnapshot.json | 2 +- exchanges/orderbook/orderbook_types.go | 4 +- exchanges/support.go | 1 - exchanges/trade/README.md | 1 - testdata/configtest.json | 80 +- testdata/exchangelist.csv | 1 - 29 files changed, 11 insertions(+), 4152 deletions(-) delete mode 100644 cmd/documentation/exchanges_templates/bittrex.tmpl delete mode 100644 exchanges/bittrex/README.md delete mode 100644 exchanges/bittrex/bittrex.go delete mode 100644 exchanges/bittrex/bittrex_test.go delete mode 100644 exchanges/bittrex/bittrex_types.go delete mode 100644 exchanges/bittrex/bittrex_websocket.go delete mode 100644 exchanges/bittrex/bittrex_wrapper.go delete mode 100644 exchanges/bittrex/bittrex_ws_orderbook.go diff --git a/README.md b/README.md index 31e90575d09..68c8bc5cf10 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | Bithumb | Yes | Yes | NA | | BitMEX | Yes | Yes | NA | | Bitstamp | Yes | Yes | No | -| Bittrex | Yes | Yes | NA | | BTCMarkets | Yes | Yes | NA | | BTSE | Yes | Yes | NA | | Bybit | Yes | Yes | NA | diff --git a/backtester/report/report_test.go b/backtester/report/report_test.go index 68da9d40d3f..4b25811007e 100644 --- a/backtester/report/report_test.go +++ b/backtester/report/report_test.go @@ -135,11 +135,11 @@ func TestGenerateReport(t *testing.T) { }, }, { - Exchange: "Bittrex", + Exchange: "Bitstamp", Asset: a, Pair: currency.NewPair(currency.BTC, currency.USD), Interval: gctkline.OneDay, - Watermark: "BITTREX - SPOT - BTC-USD - 1d", + Watermark: "BITSTAMP - SPOT - BTC-USD - 1d", Candles: []DetailedCandle{ { UnixMilli: time.Date(2020, 12, 12, 0, 0, 0, 0, time.UTC).UnixMilli(), diff --git a/cmd/apichecker/testupdates.json b/cmd/apichecker/testupdates.json index c15915b31d9..75a83e12163 100644 --- a/cmd/apichecker/testupdates.json +++ b/cmd/apichecker/testupdates.json @@ -72,17 +72,6 @@ }, "Disabled": false }, - { - "Name": "Bittrex", - "CheckType": "GitHub Sha Check", - "Data": { - "GitHubData": { - "Repo": "Bittrex/bittrex.github.io", - "Sha": "fc1ea9c10c48aa82c4dc2c6be74887ef61b5b31b" - } - }, - "Disabled": false - }, { "Name": "Coinut", "CheckType": "GitHub Sha Check", diff --git a/cmd/apichecker/updates.json b/cmd/apichecker/updates.json index 49ada374d2e..fefb432726b 100644 --- a/cmd/apichecker/updates.json +++ b/cmd/apichecker/updates.json @@ -72,17 +72,6 @@ }, "Disabled": false }, - { - "Name": "Bittrex", - "CheckType": "GitHub Sha Check", - "Data": { - "GitHubData": { - "Repo": "Bittrex/bittrex.github.io", - "Sha": "fc1ea9c10c48aa82c4dc2c6be74887ef61b5b31b" - } - }, - "Disabled": false - }, { "Name": "Coinut", "CheckType": "GitHub Sha Check", diff --git a/cmd/documentation/exchanges_templates/bittrex.tmpl b/cmd/documentation/exchanges_templates/bittrex.tmpl deleted file mode 100644 index 968448f82d5..00000000000 --- a/cmd/documentation/exchanges_templates/bittrex.tmpl +++ /dev/null @@ -1,103 +0,0 @@ -{{define "exchanges bittrex" -}} -{{template "header" .}} -## Bittrex Exchange - -### Current Features - -+ REST Support - -### Notes - -- Bittrex used to have reversed market names: btc-ltc. The v3 API changed this to the more widely accepted format with first the base pair and then the quote pair: ltc-btc. -- Asset names and market names are not case sensitive. - -### 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() == "Bittrex" { - b = bot.Exchanges[i] - } -} - -// Public calls - wrapper functions - -// Fetches current ticker information -tick, err := b.FetchTicker() -if err != nil { - // Handle error -} - -// Fetches current orderbook information -ob, err := b.FetchOrderbook() -if err != nil { - // Handle error -} - -// Private calls - wrapper functions - make sure your APIKEY and APISECRET are -// set and AuthenticatedAPISupport is set to true - -// Fetches current account information -accountInfo, err := b.GetAccountInfo() -if err != nil { - // Handle error -} -``` - -+ If enabled via individually importing package, rudimentary example below: - -```go -// Public calls - -// Fetches current ticker information -ticker, err := b.GetTicker() -if err != nil { - // Handle error -} - -// Fetches current orderbook information -ob, err := b.GetOrderBook() -if err != nil { - // Handle error -} - -// Private calls - make sure your APIKEY and APISECRET are set and -// AuthenticatedAPISupport is set to true - -// GetUserInfo returns account info -accountInfo, err := b.GetUserInfo(...) -if err != nil { - // Handle error -} - -// Submits an order and the exchange and returns its tradeID -tradeID, err := b.Trade(...) -if err != nil { - // Handle error -} -``` - -### Please click GoDocs chevron above to view current GoDoc information for this package -{{template "contributions"}} -{{template "donations" .}} -{{end}} diff --git a/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl b/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl index 51b793145e1..cc0d02f2b5a 100644 --- a/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl +++ b/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl @@ -49,7 +49,6 @@ _b in this context is an `IBotExchange` implemented struct_ | Bithumb | Yes | Yes | No | | BitMEX | Yes | Yes | Yes | | Bitstamp | Yes | Yes | No | -| Bittrex | Yes | Yes | No | | BTCMarkets | Yes | Yes | No | | BTSE | Yes | Yes | No | | Bybit | Yes | Yes | Yes | diff --git a/cmd/documentation/root_templates/root_readme.tmpl b/cmd/documentation/root_templates/root_readme.tmpl index d1346219866..063bec6b47a 100644 --- a/cmd/documentation/root_templates/root_readme.tmpl +++ b/cmd/documentation/root_templates/root_readme.tmpl @@ -27,7 +27,6 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | Bithumb | Yes | Yes | NA | | BitMEX | Yes | Yes | NA | | Bitstamp | Yes | Yes | No | -| Bittrex | Yes | Yes | NA | | BTCMarkets | Yes | Yes | NA | | BTSE | Yes | Yes | NA | | Bybit | Yes | Yes | NA | diff --git a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go index d60abaa711e..a9ab33ed485 100644 --- a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go +++ b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go @@ -593,7 +593,6 @@ var unsupportedExchangeNames = []string{ "testexch", "alphapoint", "bitflyer", // Bitflyer has many "ErrNotYetImplemented, which is true, but not what we care to test for here - "bittrex", // the api is about to expire in March, and we haven't updated it yet "itbit", // itbit has no way of retrieving pair data "btse", // TODO rm once timeout issues resolved "poloniex", // outdated API // TODO rm once updated diff --git a/config_example.json b/config_example.json index a1544fda20a..fe8426d2e7a 100644 --- a/config_example.json +++ b/config_example.json @@ -956,84 +956,6 @@ } ] }, - { - "name": "Bittrex", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": true, - "delimiter": "-" - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "USDT-BTC", - "available": "BTC-LTC,BTC-DOGE,BTC-VTC,BTC-PPC,BTC-FTC,BTC-RDD,BTC-NXT,BTC-DASH,BTC-POT,BTC-BLK,BTC-EMC2,BTC-XMY,BTC-GRS,BTC-NLG,BTC-MONA,BTC-VRC,BTC-CURE,BTC-XMR,BTC-XDN,BTC-NAV,BTC-XST,BTC-AR,BTC-VIA,BTC-PINK,BTC-IOC,BTC-SYS,BTC-DGB,BTC-BURST,BTC-EXCL,BTC-BLOCK,BTC-BTS,BTC-XRP,BTC-GAME,BTC-NXS,BTC-GEO,BTC-FLO,BTC-MUE,BTC-XEM,BTC-SPHR,BTC-OK,BTC-AEON,BTC-ETH,BTC-EXP,BTC-XLM,USDT-BTC,BTC-FCT,BTC-MAID,BTC-SLS,BTC-RADS,BTC-DCR,BTC-XVG,BTC-PIVX,BTC-MEME,BTC-STEEM,BTC-LSK,BTC-WAVES,BTC-LBC,BTC-SBD,BTC-ETC,ETH-ETC,BTC-STRAT,BTC-REP,BTC-ARDR,BTC-XZC,BTC-NEO,BTC-ZEC,BTC-UBQ,BTC-KMD,BTC-SIB,BTC-ION,BTC-CRW,BTC-ARK,BTC-INCNT,BTC-GBYTE,BTC-GNT,BTC-EDG,BTC-MORE,ETH-GNT,ETH-REP,USDT-ETH,BTC-WINGS,BTC-RLC,BTC-GNO,BTC-GUP,ETH-GNO,BTC-HMQ,BTC-ANT,ETH-ANT,BTC-SC,ETH-BAT,BTC-BAT,BTC-ZEN,BTC-QRL,BTC-PTOY,BTC-BNT,ETH-BNT,BTC-NMR,ETH-NMR,ETH-LTC,ETH-XRP,BTC-SNT,ETH-SNT,BTC-DCT,BTC-XEL,BTC-MCO,ETH-MCO,BTC-ADT,BTC-PAY,ETH-PAY,BTC-MTL,BTC-STORJ,BTC-ADX,ETH-ADX,ETH-DASH,ETH-SC,ETH-ZEC,USDT-ZEC,USDT-LTC,USDT-ETC,USDT-XRP,BTC-OMG,ETH-OMG,BTC-CVC,ETH-CVC,BTC-PART,BTC-QTUM,ETH-QTUM,ETH-XMR,ETH-XEM,ETH-XLM,ETH-NEO,USDT-XMR,USDT-DASH,ETH-BCH,USDT-BCH,BTC-BCH,BTC-DNT,USDT-NEO,ETH-WAVES,ETH-STRAT,ETH-DGB,USDT-OMG,BTC-ADA,BTC-MANA,ETH-MANA,BTC-RCN,BTC-VIB,ETH-VIB,BTC-MER,BTC-POWR,ETH-POWR,ETH-ADA,BTC-ENG,ETH-ENG,USDT-ADA,USDT-XVG,BTC-UKG,ETH-UKG,BTC-IGNIS,BTC-SRN,ETH-SRN,BTC-WAXP,ETH-WAXP,BTC-ZRX,ETH-ZRX,BTC-VEE,BTC-TRX,ETH-TRX,BTC-TUSD,BTC-LRC,ETH-TUSD,BTC-DMT,ETH-DMT,USDT-TUSD,USDT-SC,USDT-TRX,BTC-STMX,ETH-STMX,BTC-AID,BTC-NGC,BTC-GTO,USDT-DCR,USD-BTC,USD-USDT,USD-TUSD,BTC-TUBE,BTC-CMCT,USD-ETH,BTC-NLC2,BTC-MFT,BTC-LOOM,BTC-RFR,USDT-DGB,BTC-RVN,USD-XRP,USD-ETC,BTC-BFT,BTC-GO,BTC-HYDRO,BTC-UPP,USD-ADA,USD-ZEC,USDT-DOGE,BTC-ENJ,BTC-MET,USD-LTC,USD-TRX,BTC-DTA,BTC-EDR,BTC-IHT,USD-BCH,BTC-XHV,USDT-ZRX,BTC-NPXS,BTC-PMA,USDT-BAT,USDT-RVN,BTC-PAL,USD-SC,BTC-PAX,BTC-ZIL,BTC-MOC,BTC-OST,BTC-SPC,BTC-MED,BTC-BSV,BTC-IOST,USDT-BSV,ETH-BSV,BTC-SOLVE,BTC-USDS,USDT-PMA,ETH-NPXS,USDT-NPXS,USD-ZRX,BTC-JNT,BTC-LBA,USD-BAT,USD-BSV,BTC-DENT,USD-USDS,BTC-DRGN,USD-PAX,BTC-VITE,BTC-IOTX,USD-DGB,BTC-BTM,BTC-ELF,BTC-QNT,BTC-BTU,USD-ZEN,BTC-SPND,BTC-BTT,BTC-NKN,USD-KMD,USDT-BTT,BTC-GRIN,BTC-CTXC,BTC-HXRO,BTC-META,USDT-GRIN,BTC-FSN,BTC-ANKR,USDT-XLM,BTC-TRAC,BTC-CRO,BTC-ONT,ETH-SOLVE,BTC-ONG,BTC-TTC,BTC-PTON,BTC-PI,ETH-ANKR,BTC-PLA,BTC-ART,BTC-ORBS,USDT-ENJ,BTC-VBK,BTC-BORA,BTC-CND,USDT-ONT,BTC-FX,ETH-FX,BTC-ATOM,USDT-ATOM,ETH-ATOM,BTC-OCEAN,USDT-OCEAN,BTC-BWX,BTC-VDX,USDT-VDX,ETH-VDX,BTC-COSM,BTC-LAMB,BTC-STPT,BTC-DAI,ETH-DAI,USDT-DAI,BTC-FNB,BTC-PROM,BTC-ABYSS,BTC-EOS,ETH-EOS,USDT-EOS,BTC-FXC,BTC-DUSK,BTC-URAC,BTC-BLOC,BTC-BRZ,BTC-TEMCO,BTC-SPIN,BTC-LUNA,BTC-CHR,BTC-TUDA,BTC-UTK,BTC-PXL,BTC-AKRO,BTC-TSHP,BTC-HEDG,BTC-MRPH,BTC-HBAR,ETH-HBAR,USD-HBAR,USDT-HBAR,BTC-PLG,BTC-VET,USDT-VET,BTC-SIX,BTC-WGP,BTC-APM,BTC-FLETA,USD-DCR,BTC-BLTV,BTC-HDAC,BTC-HYC,BTC-LINK,USD-EOS,BTC-APIX,BTC-XTZ,ETH-XTZ,USD-XTZ,USDT-XTZ,BTC-XTP,BTC-XSR,BTC-CTC,USD-ATOM,BTC-IOTA,ETH-LINK,USD-LINK,USDT-LINK,BTC-VRA,BTC-ABBC,BTC-FRSP,BTC-WICC,USDT-WICC,USDT-NMR,USD-DASH,USD-RVN,USD-DAI,BTC-VANY,BTC-BOA,BTC-CPC,BTC-CKB,USDT-CKB,BTC-MOF,USDT-MOF,USD-WAXP,USDT-WAXP,BTC-UPT,BTC-UPUSD,BTC-UPEUR,BTC-CVT,BTC-HBD,BTC-HIVE,USDT-CRO,BTC-SXP,BTC-ELAMA,BTC-STC,BTC-IRIS,USDT-IRIS,USDT-BOA,EUR-BTC,EUR-ETH,EUR-USDT,EUR-BSV,EUR-BCH,EUR-TRX,USDT-APM,USDT-HXRO,BTC-OGN,ETH-OGN,BTC-ALGO,BTC-OXT,USDT-OXT,BTC-ICX,BTC-USDC,ETH-USDC,USD-USDC,USDT-USDC,USDT-UPUSD,USDT-BRZ,BTC-XUC,BTC-MDT,USDT-MDT,BTC-REV,USDT-XUC,USDT-REV,BTC-UCT,USDT-UCT,BTC-YOU,USD-HIVE,USDT-HIVE,USD-ENJ,ETH-ENJ,BTC-HDAO,USDT-HDAO,BTC-DNA,USDT-DNA,USDT-SOLVE,BTC-CNTM,USDT-LBC,BTC-LOON,BTC-TNC,USDT-LOON,USD-ALGO,USDT-ALGO,BTC-UBT,ETH-UBT,BTC-DEP,USDT-DEP,EUR-USD,BTC-CELO,ETH-CELO,USD-CELO,USDT-CELO,USDT-CNTM,BTC-VID,BTC-HNS,ETH-HNS,USDT-HNS,BTC-PHNX,BTC-UTI,USD-SOLVE,BTC-4ART,USDT-4ART,BTC-VLX,USDT-VLX,ETH-MET,ETH-TRAC,USDT-TRAC,BTC-ME,BTC-DAWN,BTC-KDA,USDT-KDA" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": false, - "websocketCapabilities": {} - }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ - { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, { "name": "BTSE", "enabled": true, diff --git a/docs/ADD_NEW_EXCHANGE.md b/docs/ADD_NEW_EXCHANGE.md index f40c3f0844a..554fef0dc60 100644 --- a/docs/ADD_NEW_EXCHANGE.md +++ b/docs/ADD_NEW_EXCHANGE.md @@ -202,7 +202,6 @@ Yes means supported, No means not yet implemented and NA means protocol unsuppor | Bithumb | Yes | NA | NA | | BitMEX | Yes | Yes | NA | | Bitstamp | Yes | Yes | No | -| Bittrex | Yes | Yes | NA | | BTCMarkets | Yes | No | NA | | BTSE | Yes | Yes | NA | | COINUT | Yes | Yes | NA | @@ -233,7 +232,6 @@ var Exchanges = []string{ "bithumb", "bitmex", "bitstamp", - "bittrex", "btc markets", "btse", "coinbasepro", diff --git a/docs/MULTICHAIN_TRANSFER_SUPPORT.md b/docs/MULTICHAIN_TRANSFER_SUPPORT.md index ccb335967ad..1a6feb4448b 100644 --- a/docs/MULTICHAIN_TRANSFER_SUPPORT.md +++ b/docs/MULTICHAIN_TRANSFER_SUPPORT.md @@ -50,7 +50,6 @@ $ ./gctcli withdrawcryptofunds --exchange=binance --currency=USDT --address=TJU9 | Bithumb | No | No | | | BitMEX | No | No | Supports BTC only | | Bitstamp | No | No | | -| Bittrex | No | No | NA | | BTCMarkets | No | No| NA | | BTSE | No | No | Only through website | | Bybit | Yes | Yes | | diff --git a/docs/OHLCV.md b/docs/OHLCV.md index 2565fe9604f..fce445262cd 100644 --- a/docs/OHLCV.md +++ b/docs/OHLCV.md @@ -73,7 +73,6 @@ A helper tool [cmd/dbseed](../cmd/dbseed/README.md) has been created for assisti | Bitmex | | | Bitstamp | Y | | BTC Markets | Y | -| Bittrex | | | BTSE | Y | | Bybit | Y | | Coinbase Pro | Y | diff --git a/engine/helpers.go b/engine/helpers.go index 4e915e37d66..f5010f21e93 100644 --- a/engine/helpers.go +++ b/engine/helpers.go @@ -34,7 +34,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/bithumb" "github.com/thrasher-corp/gocryptotrader/exchanges/bitmex" "github.com/thrasher-corp/gocryptotrader/exchanges/bitstamp" - "github.com/thrasher-corp/gocryptotrader/exchanges/bittrex" "github.com/thrasher-corp/gocryptotrader/exchanges/btcmarkets" "github.com/thrasher-corp/gocryptotrader/exchanges/btse" "github.com/thrasher-corp/gocryptotrader/exchanges/bybit" @@ -1010,8 +1009,6 @@ func NewSupportedExchangeByName(name string) (exchange.IBotExchange, error) { return new(bitmex.Bitmex), nil case "bitstamp": return new(bitstamp.Bitstamp), nil - case "bittrex": - return new(bittrex.Bittrex), nil case "btc markets": return new(btcmarkets.BTCMarkets), nil case "btse": diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index d3a40cd80bf..e1423d9e950 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -976,7 +976,7 @@ func (b *Binance) getMultiplier(ctx context.Context, isMaker bool) (float64, err return multiplier, nil } -// calculateTradingFee returns the fee for trading any currency on Bittrex +// calculateTradingFee returns the fee for trading any currency on Binance func calculateTradingFee(purchasePrice, amount, multiplier float64) float64 { return (multiplier / 100) * purchasePrice * amount } diff --git a/exchanges/bitmex/bitmex.go b/exchanges/bitmex/bitmex.go index a618b94b021..13a76bd6daf 100644 --- a/exchanges/bitmex/bitmex.go +++ b/exchanges/bitmex/bitmex.go @@ -966,7 +966,7 @@ func getOfflineTradeFee(price, amount float64) float64 { return 0.000750 * price * amount } -// calculateTradingFee returns the fee for trading any currency on Bittrex +// calculateTradingFee returns the fee for trading any currency on Bitmex func calculateTradingFee(purchasePrice, amount float64, isMaker bool) float64 { var fee = 0.000750 if isMaker { diff --git a/exchanges/bittrex/README.md b/exchanges/bittrex/README.md deleted file mode 100644 index a52c48bfd7e..00000000000 --- a/exchanges/bittrex/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# GoCryptoTrader package Bittrex - - - - -[![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/bittrex) -[![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 bittrex 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) - -## Bittrex Exchange - -### Current Features - -+ REST Support - -### Notes - -- Bittrex used to have reversed market names: btc-ltc. The v3 API changed this to the more widely accepted format with first the base pair and then the quote pair: ltc-btc. -- Asset names and market names are not case sensitive. - -### 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() == "Bittrex" { - b = bot.Exchanges[i] - } -} - -// Public calls - wrapper functions - -// Fetches current ticker information -tick, err := b.FetchTicker() -if err != nil { - // Handle error -} - -// Fetches current orderbook information -ob, err := b.FetchOrderbook() -if err != nil { - // Handle error -} - -// Private calls - wrapper functions - make sure your APIKEY and APISECRET are -// set and AuthenticatedAPISupport is set to true - -// Fetches current account information -accountInfo, err := b.GetAccountInfo() -if err != nil { - // Handle error -} -``` - -+ If enabled via individually importing package, rudimentary example below: - -```go -// Public calls - -// Fetches current ticker information -ticker, err := b.GetTicker() -if err != nil { - // Handle error -} - -// Fetches current orderbook information -ob, err := b.GetOrderBook() -if err != nil { - // Handle error -} - -// Private calls - make sure your APIKEY and APISECRET are set and -// AuthenticatedAPISupport is set to true - -// GetUserInfo returns account info -accountInfo, err := b.GetUserInfo(...) -if err != nil { - // Handle error -} - -// Submits an order and the exchange and returns its tradeID -tradeID, err := b.Trade(...) -if err != nil { - // Handle error -} -``` - -### Please click GoDocs chevron above to view current GoDoc information for this package - -## Contribution - -Please feel free to submit any pull requests or suggest any desired features to be added. - -When submitting a PR, please abide by our coding guidelines: - -+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). -+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. -+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). -+ Pull requests need to be based on and opened against the `master` branch. - -## Donations - - - -If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: - -***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/exchanges/bittrex/bittrex.go b/exchanges/bittrex/bittrex.go deleted file mode 100644 index 3cc0f93c6b1..00000000000 --- a/exchanges/bittrex/bittrex.go +++ /dev/null @@ -1,488 +0,0 @@ -package bittrex - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/common/crypto" - "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/kline" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" -) - -// Bittrex is the overaching type across the bittrex methods -type Bittrex struct { - exchange.Base - WsSequenceOrders int64 - - obm *orderbookManager - tickerCache *TickerCache -} - -const ( - bittrexAPIRestURL = "https://api.bittrex.com/v3" - bittrexAPIDeprecatedURL = "https://bittrex.com/api/v1.1" - - // Public endpoints - getMarkets = "/markets" - getMarketSummaries = "/markets/summaries" - getTicker = "/markets/%s/ticker" - getTickers = "/markets/tickers" - getMarketSummary = "/markets/%s/summary" - getMarketTrades = "/markets/%s/trades" - getOrderbook = "/markets/%s/orderbook?depth=%s" - getRecentCandles = "/markets/%s/candles/%s/%s/recent" - getHistoricalCandles = "/markets/%s/candles/%s/%s/historical/%s" - getCurrencies = "/currencies" - - // Authenticated endpoints - getBalances = "/balances" - getBalance = "/balances/%s" - getDepositAddress = "/addresses/%s" - depositAddresses = "/addresses/" - getAllOpenOrders = "/orders/open" - getOpenOrders = "/orders/open?marketSymbol=%s" - getOrder = "/orders/%s" - getClosedOrders = "/orders/closed?marketSymbol=%s" - cancelOrder = "/orders/%s" - cancelOpenOrders = "/orders/open" - getClosedWithdrawals = "/withdrawals/closed" - getOpenWithdrawals = "/withdrawals/open" - submitWithdrawal = "/transfers" - getClosedDeposits = "/deposits/closed" - getOpenDeposits = "/deposits/open" - submitOrder = "/orders" - - // Other Consts - ratePeriod = time.Minute - rateLimit = 60 - orderbookDepth = 500 // ws uses REST snapshots and needs identical depths -) - -// GetMarkets is used to get the open and available trading markets at Bittrex -// along with other meta data. -func (b *Bittrex) GetMarkets(ctx context.Context) ([]MarketData, error) { - var resp []MarketData - return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, getMarkets, &resp, nil) -} - -// GetCurrencies is used to get all supported currencies at Bittrex -func (b *Bittrex) GetCurrencies(ctx context.Context) ([]CurrencyData, error) { - var resp []CurrencyData - return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, getCurrencies, &resp, nil) -} - -// GetTicker sends a public get request and returns current ticker information -// on the supplied currency. Example currency input param "ltc-btc". -func (b *Bittrex) GetTicker(ctx context.Context, marketName string) (TickerData, error) { - var resp TickerData - return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getTicker, marketName), &resp, nil) -} - -// GetTickers returns bittrex tickers -func (b *Bittrex) GetTickers(ctx context.Context) ([]TickerData, error) { - var resp []TickerData - return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, getTickers, &resp, nil) -} - -// GetMarketSummaries is used to get the last 24 hour summary of all active -// currencies -func (b *Bittrex) GetMarketSummaries(ctx context.Context) ([]MarketSummaryData, error) { - var resp []MarketSummaryData - return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, getMarketSummaries, &resp, nil) -} - -// GetMarketSummary is used to get the last 24 hour summary of all active -// exchanges by currency pair (ltc-btc). -func (b *Bittrex) GetMarketSummary(ctx context.Context, marketName string) (MarketSummaryData, error) { - var resp MarketSummaryData - return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getMarketSummary, marketName), &resp, nil) -} - -// GetOrderbook method returns current order book information by currency and depth. -// "marketSymbol" ie ltc-btc -// "depth" is either 1, 25 or 500. Server side, the depth defaults to 25. -func (b *Bittrex) GetOrderbook(ctx context.Context, marketName string, depth int64) (*OrderbookData, int64, error) { - strDepth := strconv.FormatInt(depth, 10) - - var resp OrderbookData - var sequence int64 - resultHeader := http.Header{} - err := b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getOrderbook, marketName, strDepth), &resp, &resultHeader) - if err != nil { - return nil, 0, err - } - sequence, err = strconv.ParseInt(resultHeader.Get("sequence"), 10, 64) - if err != nil { - return nil, 0, err - } - - return &resp, sequence, nil -} - -// GetMarketHistory retrieves the latest trades that have occurred for a specific market -func (b *Bittrex) GetMarketHistory(ctx context.Context, currency string) ([]TradeData, error) { - var resp []TradeData - return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getMarketTrades, currency), &resp, nil) -} - -// Order places an order -func (b *Bittrex) Order(ctx context.Context, marketName, side, orderType string, timeInForce TimeInForce, price, amount, ceiling float64) (OrderData, error) { - req := make(map[string]interface{}) - req["marketSymbol"] = marketName - req["direction"] = side - req["type"] = orderType - req["quantity"] = strconv.FormatFloat(amount, 'f', -1, 64) - if orderType == "CEILING_LIMIT" || orderType == "CEILING_MARKET" { - req["ceiling"] = strconv.FormatFloat(ceiling, 'f', -1, 64) - } - if orderType == "LIMIT" { - req["limit"] = strconv.FormatFloat(price, 'f', -1, 64) - } - if timeInForce != "" { - req["timeInForce"] = timeInForce - } else { - req["timeInForce"] = GoodTilCancelled - } - var resp OrderData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, submitOrder, nil, req, &resp, nil) -} - -// GetOpenOrders returns all orders that you currently have opened. -// A specific market can be requested for example "ltc-btc" -func (b *Bittrex) GetOpenOrders(ctx context.Context, marketName string) ([]OrderData, int64, error) { - var path string - if marketName == "" || marketName == " " { - path = getAllOpenOrders - } else { - path = fmt.Sprintf(getOpenOrders, marketName) - } - var resp []OrderData - var sequence int64 - resultHeader := http.Header{} - err := b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, &resp, &resultHeader) - if err != nil { - return nil, 0, err - } - sequence, err = strconv.ParseInt(resultHeader.Get("sequence"), 10, 64) - if err != nil { - return nil, 0, err - } - return resp, sequence, err -} - -// CancelExistingOrder is used to cancel a buy or sell order. -func (b *Bittrex) CancelExistingOrder(ctx context.Context, uuid string) (OrderData, error) { - var resp OrderData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, fmt.Sprintf(cancelOrder, uuid), nil, nil, &resp, nil) -} - -// CancelOpenOrders is used to cancel all open orders for a specific market -// Or cancel all orders for all markets if the parameter `markets` is set to "" -func (b *Bittrex) CancelOpenOrders(ctx context.Context, market string) ([]BulkCancelResultData, error) { - var resp []BulkCancelResultData - - params := url.Values{} - if market != "" { - params.Set("marketSymbol", market) - } - - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, cancelOpenOrders, params, nil, &resp, nil) -} - -// GetRecentCandles retrieves recent candles; -// Interval: MINUTE_1, MINUTE_5, HOUR_1, or DAY_1 -// Type: TRADE or MIDPOINT -func (b *Bittrex) GetRecentCandles(ctx context.Context, marketName, candleInterval, candleType string) ([]CandleData, error) { - var resp []CandleData - - return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getRecentCandles, marketName, candleType, candleInterval), &resp, nil) -} - -// GetHistoricalCandles retrieves recent candles -// Type: TRADE or MIDPOINT -func (b *Bittrex) GetHistoricalCandles(ctx context.Context, marketName, candleInterval, candleType string, year, month, day int) ([]CandleData, error) { - var resp []CandleData - - var start string - switch candleInterval { - case "MINUTE_1", "MINUTE_5": - // Retrieve full day - start = fmt.Sprintf("%d/%d/%d", year, month, day) - case "HOUR_1": - // Retrieve full month - start = fmt.Sprintf("%d/%d", year, month) - case "DAY_1": - // Retrieve full year - start = fmt.Sprintf("%d", year) - default: - return resp, fmt.Errorf("%w %v", kline.ErrUnsupportedInterval, candleInterval) - } - - return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getHistoricalCandles, marketName, candleType, candleInterval, start), &resp, nil) -} - -// GetBalances is used to retrieve all balances from your account -func (b *Bittrex) GetBalances(ctx context.Context) ([]BalanceData, error) { - var resp []BalanceData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getBalances, nil, nil, &resp, nil) -} - -// GetAccountBalanceByCurrency is used to retrieve the balance from your account -// for a specific currency. ie. "btc" or "ltc" -func (b *Bittrex) GetAccountBalanceByCurrency(ctx context.Context, currency string) (BalanceData, error) { - var resp BalanceData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(getBalance, currency), nil, nil, &resp, nil) -} - -// GetCryptoDepositAddresses is used to retrieve all deposit addresses -func (b *Bittrex) GetCryptoDepositAddresses(ctx context.Context) ([]AddressData, error) { - var resp []AddressData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, depositAddresses, nil, nil, &resp, nil) -} - -// GetCryptoDepositAddress is used to retrieve an address for a specific currency -func (b *Bittrex) GetCryptoDepositAddress(ctx context.Context, currency string) (AddressData, error) { - var resp AddressData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(getDepositAddress, currency), nil, nil, &resp, nil) -} - -// ProvisionNewDepositAddress provisions a new deposit address for a specific currency -func (b *Bittrex) ProvisionNewDepositAddress(ctx context.Context, currency string) (*ProvisionNewAddressData, error) { - req := make(map[string]interface{}, 1) - req["currencySymbol"] = currency - var resp ProvisionNewAddressData - return &resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, depositAddresses, nil, req, &resp, nil) -} - -// Withdraw is used to withdraw funds from your account. -func (b *Bittrex) Withdraw(ctx context.Context, currency, paymentID, address string, quantity float64) (WithdrawalData, error) { - req := make(map[string]interface{}) - req["currencySymbol"] = currency - req["quantity"] = strconv.FormatFloat(quantity, 'f', -1, 64) - req["cryptoAddress"] = address - if len(paymentID) > 0 { - req["cryptoAddressTag"] = paymentID - } - var resp WithdrawalData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, submitWithdrawal, nil, req, &resp, nil) -} - -// GetOrder is used to retrieve a single order by UUID. -func (b *Bittrex) GetOrder(ctx context.Context, uuid string) (OrderData, error) { - var resp OrderData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(getOrder, uuid), nil, nil, &resp, nil) -} - -// GetOrderHistoryForCurrency is used to retrieve your order history. If marketName -// is omitted it will return the entire order History. -func (b *Bittrex) GetOrderHistoryForCurrency(ctx context.Context, currency string) ([]OrderData, error) { - var resp []OrderData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(getClosedOrders, currency), nil, nil, &resp, nil) -} - -// GetClosedWithdrawals is used to retrieve your withdrawal history. -func (b *Bittrex) GetClosedWithdrawals(ctx context.Context) ([]WithdrawalData, error) { - var resp []WithdrawalData - - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getClosedWithdrawals, nil, nil, &resp, nil) -} - -// GetClosedWithdrawalsForCurrency is used to retrieve your withdrawal history for the specified currency. -func (b *Bittrex) GetClosedWithdrawalsForCurrency(ctx context.Context, currency string) ([]WithdrawalData, error) { - var resp []WithdrawalData - - params := url.Values{} - params.Set("currencySymbol", currency) - - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getClosedWithdrawals, params, nil, &resp, nil) -} - -// GetOpenWithdrawals is used to retrieve your withdrawal history. If currency -// omitted it will return the entire history -func (b *Bittrex) GetOpenWithdrawals(ctx context.Context) ([]WithdrawalData, error) { - var resp []WithdrawalData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOpenWithdrawals, nil, nil, &resp, nil) -} - -// GetClosedDeposits is used to retrieve your deposit history. -func (b *Bittrex) GetClosedDeposits(ctx context.Context) ([]DepositData, error) { - var resp []DepositData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getClosedDeposits, nil, nil, &resp, nil) -} - -// GetClosedDepositsForCurrency is used to retrieve your deposit history for the specified currency -func (b *Bittrex) GetClosedDepositsForCurrency(ctx context.Context, currency string) ([]DepositData, error) { - var resp []DepositData - - params := url.Values{} - params.Set("currencySymbol", currency) - - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getClosedDeposits, params, nil, &resp, nil) -} - -// GetClosedDepositsPaginated is used to retrieve your deposit history. -// The maximum page size is 200 and it defaults to 100. -// PreviousPageToken is the unique identifier of the item that the resulting -// query result should end before, in the sort order of the given endpoint. Used -// for traversing a paginated set in the reverse direction. -func (b *Bittrex) GetClosedDepositsPaginated(ctx context.Context, pageSize int, previousPageTokenOptional ...string) ([]DepositData, error) { - var resp []DepositData - - params := url.Values{} - params.Set("pageSize", strconv.Itoa(pageSize)) - - if len(previousPageTokenOptional) > 0 { - params.Set("previousPageToken", previousPageTokenOptional[0]) - } - - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getClosedDeposits, params, nil, &resp, nil) -} - -// GetOpenDeposits is used to retrieve your open deposits. -func (b *Bittrex) GetOpenDeposits(ctx context.Context) ([]DepositData, error) { - var resp []DepositData - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOpenDeposits, nil, nil, &resp, nil) -} - -// GetOpenDepositsForCurrency is used to retrieve your open deposits for the specified currency -func (b *Bittrex) GetOpenDepositsForCurrency(ctx context.Context, currency string) ([]DepositData, error) { - var resp []DepositData - - params := url.Values{} - params.Set("currencySymbol", currency) - - return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOpenDeposits, params, nil, &resp, nil) -} - -// SendHTTPRequest sends an unauthenticated HTTP request -func (b *Bittrex) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, result interface{}, resultHeader *http.Header) error { - endpoint, err := b.API.Endpoints.GetURL(ep) - if err != nil { - return err - } - item := &request.Item{ - Method: http.MethodGet, - Path: endpoint + path, - Result: result, - Verbose: b.Verbose, - HTTPDebugging: b.HTTPDebugging, - HTTPRecording: b.HTTPRecording, - HeaderResponse: resultHeader, - } - return b.SendPayload(ctx, request.Unset, func() (*request.Item, error) { return item, nil }, request.UnauthenticatedRequest) -} - -// SendAuthHTTPRequest sends an authenticated request -func (b *Bittrex) SendAuthHTTPRequest(ctx context.Context, ep exchange.URL, method, action string, params url.Values, data, result interface{}, resultHeader *http.Header) error { - creds, err := b.GetCredentials(ctx) - if err != nil { - return err - } - endpoint, err := b.API.Endpoints.GetURL(ep) - if err != nil { - return err - } - - newRequest := func() (*request.Item, error) { - ts := strconv.FormatInt(time.Now().UnixMilli(), 10) - path := common.EncodeURLValues(action, params) - - var body io.Reader - var hmac, payload []byte - var contentHash string - if data == nil { - payload = []byte("") - } else { - var err error - payload, err = json.Marshal(data) - if err != nil { - return nil, err - } - } - body = bytes.NewBuffer(payload) - hash, err := crypto.GetSHA512(payload) - if err != nil { - return nil, err - } - contentHash = crypto.HexEncodeToString(hash) - sigPayload := ts + endpoint + path + method + contentHash - hmac, err = crypto.GetHMAC(crypto.HashSHA512, - []byte(sigPayload), - []byte(creds.Secret)) - if err != nil { - return nil, err - } - - headers := make(map[string]string) - headers["Api-Key"] = creds.Key - headers["Api-Timestamp"] = ts - headers["Api-Content-Hash"] = contentHash - headers["Api-Signature"] = crypto.HexEncodeToString(hmac) - headers["Content-Type"] = "application/json" - headers["Accept"] = "application/json" - - return &request.Item{ - Method: method, - Path: endpoint + path, - Headers: headers, - Body: body, - Result: result, - Verbose: b.Verbose, - HTTPDebugging: b.HTTPDebugging, - HTTPRecording: b.HTTPRecording, - HeaderResponse: resultHeader, - }, nil - } - - return b.SendPayload(ctx, request.Unset, newRequest, request.AuthenticatedRequest) -} - -// GetFee returns an estimate of fee based on type of transaction -func (b *Bittrex) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { - var fee float64 - var err error - - switch feeBuilder.FeeType { - case exchange.CryptocurrencyTradeFee: - fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount) - case exchange.CryptocurrencyWithdrawalFee: - fee, err = b.GetWithdrawalFee(ctx, feeBuilder.Pair.Base) - case exchange.OfflineTradeFee: - fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount) - } - if fee < 0 { - fee = 0 - } - return fee, err -} - -// GetWithdrawalFee returns the fee for withdrawing from the exchange -func (b *Bittrex) GetWithdrawalFee(ctx context.Context, c currency.Code) (float64, error) { - var fee float64 - - currencies, err := b.GetCurrencies(ctx) - if err != nil { - return 0, err - } - for i := range currencies { - if currencies[i].Symbol == c.String() { - fee = currencies[i].TxFee - } - } - return fee, nil -} - -// calculateTradingFee returns the fee for trading any currency on Bittrex -func calculateTradingFee(price, amount float64) float64 { - return 0.0025 * price * amount -} diff --git a/exchanges/bittrex/bittrex_test.go b/exchanges/bittrex/bittrex_test.go deleted file mode 100644 index 347f2440f18..00000000000 --- a/exchanges/bittrex/bittrex_test.go +++ /dev/null @@ -1,747 +0,0 @@ -package bittrex - -import ( - "context" - "errors" - "log" - "os" - "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 you own test keys here to run better tests. -const ( - apiKey = "" - apiSecret = "" - canManipulateRealOrders = false - currPair = "BTC-USDT" - curr = "BTC" -) - -var b = &Bittrex{} - -func TestMain(m *testing.M) { - b.SetDefaults() - cfg := config.GetConfig() - err := cfg.LoadConfig("../../testdata/configtest.json", true) - if err != nil { - log.Fatal(err) - } - bConfig, err := cfg.GetExchangeConfig("Bittrex") - if err != nil { - log.Fatal(err) - } - bConfig.API.Credentials.Key = apiKey - bConfig.API.Credentials.Secret = apiSecret - bConfig.API.AuthenticatedSupport = true - - err = b.Setup(bConfig) - if err != nil { - log.Fatal(err) - } - - if !b.IsEnabled() || !b.API.AuthenticatedSupport || - b.Verbose || len(b.BaseCurrencies) < 1 { - log.Fatal("Bittrex Setup values not set correctly") - } - - var wg sync.WaitGroup - err = b.Start(context.Background(), &wg) - if err != nil { - log.Fatal(err) - } - wg.Wait() - - os.Exit(m.Run()) -} - -func TestGetMarkets(t *testing.T) { - t.Parallel() - _, err := b.GetMarkets(context.Background()) - if err != nil { - t.Error(err) - } -} - -func TestGetCurrencies(t *testing.T) { - t.Parallel() - _, err := b.GetCurrencies(context.Background()) - if err != nil { - t.Error(err) - } -} - -func TestGetTicker(t *testing.T) { - t.Parallel() - _, err := b.GetTicker(context.Background(), currPair) - if err != nil { - t.Error(err) - } -} - -func TestGetMarketSummaries(t *testing.T) { - t.Parallel() - _, err := b.GetMarketSummaries(context.Background()) - if err != nil { - t.Error(err) - } -} - -func TestGetMarketSummary(t *testing.T) { - t.Parallel() - _, err := b.GetMarketSummary(context.Background(), currPair) - if err != nil { - t.Error(err) - } -} - -func TestGetOrderbook(t *testing.T) { - t.Parallel() - - _, _, err := b.GetOrderbook(context.Background(), currPair, 500) - if err != nil { - t.Error(err) - } -} - -func TestGetMarketHistory(t *testing.T) { - t.Parallel() - - _, err := b.GetMarketHistory(context.Background(), currPair) - if err != nil { - t.Error(err) - } -} - -func TestGetRecentCandles(t *testing.T) { - t.Parallel() - - _, err := b.GetRecentCandles(context.Background(), - currPair, "HOUR_1", "MIDPOINT") - if err != nil { - t.Error(err) - } -} - -func TestGetHistoricalCandles(t *testing.T) { - t.Parallel() - - _, err := b.GetHistoricalCandles(context.Background(), - currPair, "MINUTE_5", "MIDPOINT", 2020, 12, 31) - if err != nil { - t.Error(err) - } - _, err = b.GetHistoricalCandles(context.Background(), - currPair, "MINUTE_5", "MIDPOINT", 2020, 12, 32) - if err == nil { - t.Error("invalid date should give an error") - } -} - -func TestOrder(t *testing.T) { - t.Parallel() - - _, err := b.Order(context.Background(), - currPair, order.Buy.String(), order.Limit.String(), "", 1, 1, 0.0) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetOpenOrders(t *testing.T) { - t.Parallel() - - _, _, err := b.GetOpenOrders(context.Background(), "") - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } - _, _, err = b.GetOpenOrders(context.Background(), currPair) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestCancelExistingOrder(t *testing.T) { - t.Parallel() - - _, err := b.CancelExistingOrder(context.Background(), "invalid-order") - if err == nil { - t.Error("Expected error") - } -} - -func TestGetAccountBalances(t *testing.T) { - t.Parallel() - - _, err := b.GetBalances(context.Background()) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetAccountBalanceByCurrency(t *testing.T) { - t.Parallel() - - _, err := b.GetAccountBalanceByCurrency(context.Background(), curr) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetOrder(t *testing.T) { - t.Parallel() - - _, err := b.GetOrder(context.Background(), "0cb4c4e4-bdc7-4e13-8c13-430e587d2cc1") - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } - _, err = b.GetOrder(context.Background(), "") - if sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetOrderHistoryForCurrency(t *testing.T) { - t.Parallel() - - _, err := b.GetOrderHistoryForCurrency(context.Background(), "") - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } - _, err = b.GetOrderHistoryForCurrency(context.Background(), currPair) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetClosedWithdrawals(t *testing.T) { - t.Parallel() - - _, err := b.GetClosedWithdrawals(context.Background()) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetClosedWithdrawalsForCurrency(t *testing.T) { - t.Parallel() - - _, err := b.GetClosedWithdrawalsForCurrency(context.Background(), curr) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetOpenWithdrawals(t *testing.T) { - t.Parallel() - - _, err := b.GetOpenWithdrawals(context.Background()) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetCryptoDepositAddresses(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, b) - _, err := b.GetCryptoDepositAddresses(context.Background()) - if err != nil { - t.Error(err) - } -} - -func TestProvisionNewDepositAddress(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, b) - _, err := b.ProvisionNewDepositAddress(context.Background(), currency.XRP.String()) - if err != nil { - t.Error(err) - } -} - -func TestGetClosedDeposits(t *testing.T) { - t.Parallel() - - _, err := b.GetClosedDeposits(context.Background()) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetClosedDepositsForCurrency(t *testing.T) { - t.Parallel() - - _, err := b.GetClosedDepositsForCurrency(context.Background(), curr) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetClosedDepositsPaginated(t *testing.T) { - t.Parallel() - - _, err := b.GetClosedDepositsPaginated(context.Background(), 100) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetOpenDeposits(t *testing.T) { - t.Parallel() - - _, err := b.GetOpenDeposits(context.Background()) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestGetOpenDepositsForCurrency(t *testing.T) { - t.Parallel() - - _, err := b.GetOpenDepositsForCurrency(context.Background(), curr) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Error(err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expected error") - } -} - -func TestWithdraw(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders) - _, err := b.Withdraw(context.Background(), - curr, "", core.BitcoinDonationAddress, 0.0009) - if err != nil { - t.Error(err) - } -} - -func setFeeBuilder() *exchange.FeeBuilder { - return &exchange.FeeBuilder{ - Amount: 1, - FeeType: exchange.CryptocurrencyTradeFee, - Pair: currency.NewPair(currency.BTC, currency.LTC), - PurchasePrice: 1, - } -} - -// TestGetFeeByTypeOfflineTradeFee logic test -func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { - var feeBuilder = setFeeBuilder() - _, err := b.GetFeeByType(context.Background(), feeBuilder) - if err != nil { - t.Fatal(err) - } - if !sharedtestvalues.AreAPICredentialsSet(b) { - if feeBuilder.FeeType != exchange.OfflineTradeFee { - t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) - } - } else { - if feeBuilder.FeeType != exchange.CryptocurrencyTradeFee { - t.Errorf("Expected %v, received %v", exchange.CryptocurrencyTradeFee, feeBuilder.FeeType) - } - } -} - -func TestGetFee(t *testing.T) { - var feeBuilder = setFeeBuilder() - // CryptocurrencyTradeFee Basic - if _, err := b.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // CryptocurrencyTradeFee High quantity - feeBuilder = setFeeBuilder() - feeBuilder.Amount = 1000 - feeBuilder.PurchasePrice = 1000 - if _, err := b.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // CryptocurrencyTradeFee IsMaker - feeBuilder = setFeeBuilder() - feeBuilder.IsMaker = true - if _, err := b.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // CryptocurrencyTradeFee Negative purchase price - feeBuilder = setFeeBuilder() - feeBuilder.PurchasePrice = -1000 - if _, err := b.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // CryptocurrencyWithdrawalFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if _, err := b.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // CryptocurrencyDepositFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CryptocurrencyDepositFee - if _, err := b.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // InternationalBankDepositFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.InternationalBankDepositFee - feeBuilder.FiatCurrency = currency.HKD - if _, err := b.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // InternationalBankWithdrawalFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee - feeBuilder.FiatCurrency = currency.HKD - if _, err := b.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } -} - -func TestFormatWithdrawPermissions(t *testing.T) { - expectedResult := exchange.AutoWithdrawCryptoWithAPIPermissionText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := b.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { - t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) - } -} - -func TestGetActiveOrders(t *testing.T) { - p, err := currency.NewPairFromString(currPair) - if err != nil { - t.Fatal(err) - } - - var getOrdersRequest = order.MultiOrderRequest{ - Type: order.AnyType, - Pairs: []currency.Pair{p}, - AssetType: asset.Spot, - Side: order.AnySide, - } - - getOrdersRequest.Pairs[0].Delimiter = currency.DashDelimiter - - _, err = b.GetActiveOrders(context.Background(), &getOrdersRequest) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Errorf("Could not get open orders: %s", err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expecting an error when no keys are set") - } -} - -func TestGetOrderHistory(t *testing.T) { - var getOrdersRequest = order.MultiOrderRequest{ - Type: order.AnyType, - AssetType: asset.Spot, - Side: order.AnySide, - } - - _, err := b.GetOrderHistory(context.Background(), &getOrdersRequest) - if err == nil { - t.Error("Expected: 'At least one currency is required to fetch order history'. received nil") - } - - getOrdersRequest.Pairs = []currency.Pair{ - currency.NewPair(currency.BTC, currency.USDT), - } - - _, err = b.GetOrderHistory(context.Background(), &getOrdersRequest) - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Errorf("Could not get order history: %s", err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expecting an error when no keys are set") - } -} - -// Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them -// ---------------------------------------------------------------------------------------------------------------------------- - -func TestSubmitOrder(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders) - - var orderSubmission = &order.Submit{ - Exchange: b.GetName(), - Pair: currency.Pair{ - Delimiter: currency.DashDelimiter, - Base: currency.BTC, - Quote: currency.LTC, - }, - Side: order.Buy, - Type: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", - AssetType: asset.Spot, - } - response, err := b.SubmitOrder(context.Background(), orderSubmission) - if sharedtestvalues.AreAPICredentialsSet(b) && (err != nil || response.Status != order.New) { - t.Errorf("Order failed to be placed: %v", err) - } else if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expecting an error when no keys are set") - } -} - -func TestCancelExchangeOrder(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders) - - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - var orderCancellation = &order.Cancel{ - OrderID: "1", - WalletAddress: core.BitcoinDonationAddress, - AccountID: "1", - Pair: currencyPair, - AssetType: asset.Spot, - } - - err := b.CancelOrder(context.Background(), orderCancellation) - if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expecting an error when no keys are set") - } - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Errorf("Could not cancel orders: %v", err) - } -} - -func TestCancelAllExchangeOrders(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders) - - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - var orderCancellation = &order.Cancel{ - OrderID: "1", - WalletAddress: core.BitcoinDonationAddress, - AccountID: "1", - Pair: currencyPair, - AssetType: asset.Spot, - } - - resp, err := b.CancelAllOrders(context.Background(), orderCancellation) - - if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expecting an error when no keys are set") - } - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Errorf("Could not cancel orders: %v", err) - } - - if len(resp.Status) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.Status)) - } -} - -func TestModifyOrder(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders) - _, err := b.ModifyOrder(context.Background(), - &order.Modify{AssetType: asset.Spot}) - if err == nil { - t.Error("Expected error") - } -} - -func TestWithdrawCryptocurrencyFunds(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders) - - withdrawCryptoRequest := withdraw.Request{ - Exchange: b.Name, - Amount: -1, - Currency: currency.BTC, - Description: "WITHDRAW IT ALL", - Crypto: withdraw.CryptoRequest{ - Address: core.BitcoinDonationAddress, - }, - } - - _, err := b.WithdrawCryptocurrencyFunds(context.Background(), &withdrawCryptoRequest) - if !sharedtestvalues.AreAPICredentialsSet(b) && err == nil { - t.Error("Expecting an error when no keys are set") - } - if sharedtestvalues.AreAPICredentialsSet(b) && err != nil { - t.Errorf("Withdraw failed to be placed: %v", err) - } -} - -func TestWithdrawFiat(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders) - - var withdrawFiatRequest = withdraw.Request{} - - _, err := b.WithdrawFiatFunds(context.Background(), &withdrawFiatRequest) - if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) - } -} - -func TestWithdrawInternationalBank(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, b, canManipulateRealOrders) - - var withdrawFiatRequest = withdraw.Request{} - - _, err := b.WithdrawFiatFundsToInternationalBank(context.Background(), - &withdrawFiatRequest) - if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) - } -} - -func TestGetDepositAddress(t *testing.T) { - if sharedtestvalues.AreAPICredentialsSet(b) { - _, err := b.GetDepositAddress(context.Background(), currency.XRP, "", "") - if err != nil { - t.Error(err) - } - } else { - _, err := b.GetDepositAddress(context.Background(), currency.BTC, "", "") - if err == nil { - t.Error("error cannot be nil") - } - } -} - -func TestGetRecentTrades(t *testing.T) { - t.Parallel() - currencyPair, err := currency.NewPairFromString(currPair) - if err != nil { - t.Fatal(err) - } - _, err = b.GetRecentTrades(context.Background(), currencyPair, asset.Spot) - if err != nil { - t.Error(err) - } -} - -func TestGetHistoricTrades(t *testing.T) { - t.Parallel() - currencyPair, err := currency.NewPairFromString(currPair) - if err != nil { - t.Fatal(err) - } - _, err = b.GetHistoricTrades(context.Background(), - currencyPair, asset.Spot, time.Now().Add(-time.Minute*15), time.Now()) - if err != nil && err != common.ErrFunctionNotSupported { - t.Fatal(err) - } -} - -func TestGetHistoricCandles(t *testing.T) { - t.Parallel() - pair, err := currency.NewPairFromString("btc-usdt") - if err != nil { - t.Fatal(err) - } - - start := time.Unix(1546300800, 0) - end := start.AddDate(0, 12, 0) - _, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, start, end) - if err != nil { - t.Fatal(err) - } - - end = time.Now() - start = end.AddDate(0, -12, 0) - _, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, start, end) - if err != nil { - t.Fatal(err) - } - - start = end.AddDate(0, 0, -30) - _, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneHour, start, end) - if err != nil { - t.Fatal(err) - } - - end = time.Now().Add(-kline.OneDay.Duration()) - start = end.AddDate(0, 0, -1).Add(time.Minute * 5) - _, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.FiveMin, start, end) - if err != nil { - t.Fatal(err) - } -} - -func TestGetHistoricCandlesExtended(t *testing.T) { - t.Parallel() - pair, err := currency.NewPairFromString("btc-usdt") - if err != nil { - t.Fatal(err) - } - start := time.Unix(1546300800, 0) - end := time.Unix(1577836799, 0) - _, err = b.GetHistoricCandlesExtended(context.Background(), pair, asset.Spot, kline.OneDay, start, end) - if !errors.Is(err, common.ErrFunctionNotSupported) { - t.Fatal(err) - } -} - -func TestGetTickers(t *testing.T) { - t.Parallel() - _, err := b.GetTickers(context.Background()) - if err != nil { - t.Error(err) - } -} - -func TestUpdateTickers(t *testing.T) { - t.Parallel() - err := b.UpdateTickers(context.Background(), asset.Spot) - if err != nil { - t.Error(err) - } - err = b.UpdateTickers(context.Background(), asset.Futures) - if !errors.Is(err, asset.ErrNotSupported) { - t.Fatal(err) - } -} diff --git a/exchanges/bittrex/bittrex_types.go b/exchanges/bittrex/bittrex_types.go deleted file mode 100644 index da217efdcea..00000000000 --- a/exchanges/bittrex/bittrex_types.go +++ /dev/null @@ -1,305 +0,0 @@ -package bittrex - -import ( - "sync" - "time" - - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/stream" -) - -// CancelOrderRequest holds request data for CancelOrder -type CancelOrderRequest struct { - OrderID int64 `json:"orderId,string"` -} - -// TimeInForce defines timeInForce types -type TimeInForce string - -// All order status types -const ( - GoodTilCancelled TimeInForce = "GOOD_TIL_CANCELLED" - ImmediateOrCancel TimeInForce = "IMMEDIATE_OR_CANCEL" - FillOrKill TimeInForce = "FILL_OR_KILL" - PostOnlyGoodTilCancelled TimeInForce = "POST_ONLY_GOOD_TIL_CANCELLED" - BuyNow TimeInForce = "BUY_NOW" -) - -// OrderData holds order data -type OrderData struct { - ID string `json:"id"` - MarketSymbol string `json:"marketSymbol"` - Direction string `json:"direction"` - Type string `json:"type"` - Quantity float64 `json:"quantity,string"` - Limit float64 `json:"limit,string"` - Ceiling float64 `json:"ceiling,string"` - TimeInForce string `json:"timeInForce"` - ClientOrderID string `json:"clientOrderId"` - FillQuantity float64 `json:"fillQuantity,string"` - Commission float64 `json:"commission,string"` - Proceeds float64 `json:"proceeds,string"` - Status string `json:"status"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - ClosedAt time.Time `json:"closedAt"` - OrderToCancel struct { - Type string `json:"type,string"` - ID string `json:"id,string"` - } `json:"orderToCancel"` -} - -// BulkCancelResultData holds the result of a bulk cancel action -type BulkCancelResultData struct { - ID string `json:"id"` - StatusCode string `json:"statusCode"` - Result OrderData `json:"result"` -} - -// MarketData stores market data -type MarketData struct { - Symbol string `json:"symbol"` - BaseCurrencySymbol string `json:"baseCurrencySymbol"` - QuoteCurrencySymbol string `json:"quoteCurrencySymbol"` - MinTradeSize float64 `json:"minTradeSize,string"` - Precision int32 `json:"precision"` - Status string `json:"status"` - CreatedAt time.Time `json:"createdAt"` - Notice string `json:"notice"` - ProhibitedIn []string `json:"prohibitedIn"` -} - -// TickerData stores ticker data -type TickerData struct { - Symbol string `json:"symbol"` - LastTradeRate float64 `json:"lastTradeRate,string"` - BidRate float64 `json:"bidRate,string"` - AskRate float64 `json:"askRate,string"` - UpdatedAt time.Time `json:"updatedAt"` -} - -// TradeData stores trades data -type TradeData struct { - ID string `json:"id"` - ExecutedAt time.Time `json:"executedAt"` - Quantity float64 `json:"quantity,string"` - Rate float64 `json:"rate,string"` - TakerSide string `json:"takerSide"` -} - -// MarketSummaryData stores market summary data -type MarketSummaryData struct { - Symbol string `json:"symbol"` - High float64 `json:"high,string"` - Low float64 `json:"low,string"` - Volume float64 `json:"volume,string"` - QuoteVolume float64 `json:"quoteVolume,string"` - PercentChange float64 `json:"percentChange,string"` - UpdatedAt time.Time `json:"updatedAt"` -} - -// OrderbookData holds the order book data -type OrderbookData struct { - Bid []OrderbookEntryData `json:"bid"` - Ask []OrderbookEntryData `json:"ask"` -} - -// OrderbookEntryData holds an order book entry -type OrderbookEntryData struct { - Quantity float64 `json:"quantity,string"` - Rate float64 `json:"rate,string"` -} - -// BalanceData holds balance data -type BalanceData struct { - CurrencySymbol string `json:"currencySymbol"` - Total float64 `json:"total,string"` - Available float64 `json:"available,string"` - UpdatedAt time.Time `json:"updatedAt"` -} - -// AddressData holds address data -// Status is REQUESTED or PROVISIONED -type AddressData struct { - Status string `json:"status"` - CurrencySymbol string `json:"currencySymbol"` - CryptoAddress string `json:"cryptoAddress"` - CryptoAddressTag string `json:"cryptoAddressTag"` -} - -// ProvisionNewAddressData holds the provision deposit data -// Status is REQUESTED -type ProvisionNewAddressData struct { - Status string `json:"status"` - CurrencySymbol string `json:"currencySymbol"` -} - -// CurrencyData holds currency data -// Status is ONLINE or OFFLINE -type CurrencyData struct { - Symbol string `json:"symbol"` - Name string `json:"name"` - CoinType string `json:"coinType"` - Status string `json:"status"` - MinConfirmations int32 `json:"minConfirmations"` - Notice string `json:"notice"` - TxFee float64 `json:"txFee,string"` - LogoURL string `json:"logoUrl"` - ProhibitedIn []string `json:"prohibitedIn"` -} - -// WithdrawalData holds withdrawal data -type WithdrawalData struct { - ID string `json:"id"` - CurrencySymbol string `json:"currencySymbol"` - Quantity float64 `json:"quantity,string"` - CryptoAddress string `json:"cryptoAddress"` - CryptoAddressTag string `json:"cryptoAddressTag"` - TxCost float64 `json:"txCost,string"` - TxID string `json:"txId"` - Status string `json:"status"` - CreatedAt time.Time `json:"createdAt"` - CompletedAt time.Time `json:"completedAt"` - ClientWithdrawalID string `json:"clientWithdrawalId"` -} - -// DepositData holds deposit data -type DepositData struct { - ID string `json:"id"` - CurrencySymbol string `json:"currencySymbol"` - Quantity float64 `json:"quantity,string"` - CryptoAddress string `json:"cryptoAddress"` - CryptoAddressTag string `json:"cryptoAddressTag"` - TxID string `json:"txId"` - Confirmations int32 `json:"confirmations"` - UpdatedAt time.Time `json:"updatedAt"` - CompletedAt time.Time `json:"completedAt"` - Status string `json:"status"` - Source string `json:"source"` -} - -// CandleData holds candle data -type CandleData struct { - StartsAt time.Time `json:"startsAt"` - Open float64 `json:"open,string"` - High float64 `json:"high,string"` - Low float64 `json:"low,string"` - Close float64 `json:"close,string"` - Volume float64 `json:"volume,string"` - QuoteVolume float64 `json:"quoteVolume,string"` -} - -// WsSignalRHandshakeData holds data for the SignalR websocket wrapper handshake -type WsSignalRHandshakeData struct { - URL string `json:"Url"` // Path to the SignalR endpoint - ConnectionToken string `json:"ConnectionToken"` // Connection token assigned by the server - ConnectionID string `json:"ConnectionId"` // The ID of the connection - KeepAliveTimeout float64 `json:"KeepAliveTimeout"` // Representing the amount of time to wait before sending a keep alive packet over an idle connection - DisconnectTimeout float64 `json:"DisconnectTimeout"` // Represents the amount of time to wait after a connection goes away before raising the disconnect event - ConnectionTimeout float64 `json:"ConnectionTimeout"` // Represents the amount of time to leave a connection open before timing out - TryWebSockets bool `json:"TryWebSockets"` // Whether the server supports websockets - ProtocolVersion string `json:"ProtocolVersion"` // The version of the protocol used for communication - TransportConnectTimeout float64 `json:"TransportConnectTimeout"` // The maximum amount of time the client should try to connect to the server using a given transport - LongPollDelay float64 `json:"LongPollDelay"` // The time to tell the browser to wait before reestablishing a long poll connection after data is sent from the server. -} - -// WsEventRequest holds data on websocket requests -type WsEventRequest struct { - Hub string `json:"H"` - Method string `json:"M"` - Arguments interface{} `json:"A"` - InvocationID int64 `json:"I"` -} - -// WsEventStatus holds data on the websocket event status -type WsEventStatus struct { - Success bool `json:"Success"` - ErrorCode string `json:"ErrorCode"` -} - -// WsEventResponse holds data on the websocket response -type WsEventResponse struct { - C string `json:"C"` - S int `json:"S"` - G string `json:"G"` - Response interface{} `json:"R"` - InvocationID int64 `json:"I,string"` - Message []struct { - Hub string `json:"H"` - Method string `json:"M"` - Arguments []string `json:"A"` - } `json:"M"` -} - -// WsSubscriptionResponse holds data on the websocket response -type WsSubscriptionResponse struct { - C string `json:"C"` - S int `json:"S"` - G string `json:"G"` - Response []WsEventStatus `json:"R"` - InvocationID int64 `json:"I,string"` - Message []struct { - Hub string `json:"H"` - Method string `json:"M"` - Arguments []string `json:"A"` - } `json:"M"` -} - -// WsAuthResponse holds data on the websocket response -type WsAuthResponse struct { - C string `json:"C"` - S int `json:"S"` - G string `json:"G"` - Response WsEventStatus `json:"R"` - InvocationID int64 `json:"I,string"` - Message []struct { - Hub string `json:"H"` - Method string `json:"M"` - Arguments []string `json:"A"` - } `json:"M"` -} - -// OrderbookUpdateMessage holds websocket orderbook update messages -type OrderbookUpdateMessage struct { - MarketSymbol string `json:"marketSymbol"` - Depth int `json:"depth"` - Sequence int64 `json:"sequence"` - BidDeltas []OrderbookEntryData `json:"bidDeltas"` - AskDeltas []OrderbookEntryData `json:"askDeltas"` -} - -// OrderUpdateMessage holds websocket order update messages -type OrderUpdateMessage struct { - AccountID string `json:"accountId"` - Sequence int `json:"int,string"` - Delta OrderData `json:"delta"` -} - -// WsPendingRequest holds pending requests -type WsPendingRequest struct { - WsEventRequest - ChannelsToSubscribe *[]stream.ChannelSubscription -} - -// 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 -} - -type update struct { - buffer chan *OrderbookUpdateMessage - fetchingBook bool - initialSync bool - needsFetchingBook bool -} - -// job defines a synchronisation job that tells a go routine to fetch an -// orderbook via the REST protocol -type job struct { - Pair currency.Pair -} diff --git a/exchanges/bittrex/bittrex_websocket.go b/exchanges/bittrex/bittrex_websocket.go deleted file mode 100644 index 3cb4613793a..00000000000 --- a/exchanges/bittrex/bittrex_websocket.go +++ /dev/null @@ -1,624 +0,0 @@ -package bittrex - -import ( - "bytes" - "compress/flate" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "sync" - "time" - - "github.com/gofrs/uuid" - "github.com/gorilla/websocket" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/common/crypto" - "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/exchanges/stream" - "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" - "github.com/thrasher-corp/gocryptotrader/log" -) - -const ( - bittrexAPIWSURL = "wss://socket-v3.bittrex.com/signalr" - bittrexAPIWSNegotiationsURL = "https://socket-v3.bittrex.com/signalr" - - bittrexWebsocketTimer = 13 * time.Second - wsTicker = "ticker" - wsOrderbook = "orderbook" - wsMarketSummary = "market_summary" - wsOrders = "order" - wsHeartbeat = "heartbeat" - authenticate = "Authenticate" - subscribe = "subscribe" - unsubscribe = "unsubscribe" - wsRateLimit = 50 - wsMessageRateLimit = 60 -) - -var defaultSpotSubscribedChannels = []string{ - // wsHeartbeat, - wsOrderbook, - wsTicker, - wsMarketSummary, -} - -var defaultSpotSubscribedChannelsAuth = []string{ - wsOrders, -} - -// TickerCache holds ticker and market summary data -// in order to combine them when processing data -type TickerCache struct { - MarketSummaries map[string]*MarketSummaryData - Tickers map[string]*TickerData - mu sync.RWMutex -} - -// WsConnect connects to a websocket feed -func (b *Bittrex) WsConnect() error { - if !b.Websocket.IsEnabled() || !b.IsEnabled() { - return errors.New(stream.WebsocketNotEnabled) - } - - var wsHandshakeData WsSignalRHandshakeData - err := b.WsSignalRHandshake(context.TODO(), &wsHandshakeData) - if err != nil { - return err - } - - var dialer websocket.Dialer - endpoint, err := b.API.Endpoints.GetURL(exchange.WebsocketSpot) - if err != nil { - return err - } - - params := url.Values{} - params.Set("clientProtocol", "1.5") - params.Set("transport", "webSockets") - params.Set("connectionToken", wsHandshakeData.ConnectionToken) - params.Set("connectionData", "[{name:\"c3\"}]") - params.Set("tid", "10") - - path := common.EncodeURLValues("/connect", params) - - err = b.Websocket.SetWebsocketURL(endpoint+path, false, false) - if err != nil { - return err - } - - err = b.Websocket.Conn.Dial(&dialer, http.Header{}) - if err != nil { - return err - } - // Can set up custom ping handler per websocket connection. - b.Websocket.Conn.SetupPingHandler(stream.PingHandler{ - MessageType: websocket.PingMessage, - Delay: bittrexWebsocketTimer, - }) - - // This reader routine is called prior to initiating a subscription for - // efficient processing. - b.Websocket.Wg.Add(1) - go b.wsReadData() - - b.setupOrderbookManager() - b.tickerCache = &TickerCache{ - MarketSummaries: make(map[string]*MarketSummaryData), - Tickers: make(map[string]*TickerData), - } - - if b.IsWebsocketAuthenticationSupported() { - err = b.WsAuth(context.TODO()) - if err != nil { - b.Websocket.DataHandler <- err - b.Websocket.SetCanUseAuthenticatedEndpoints(false) - } - } - return nil -} - -// WsSignalRHandshake requests the SignalR connection token over https -func (b *Bittrex) WsSignalRHandshake(ctx context.Context, result interface{}) error { - endpoint, err := b.API.Endpoints.GetURL(exchange.WebsocketSpotSupplementary) - if err != nil { - return err - } - path := "/negotiate?connectionData=[{name:\"c3\"}]&clientProtocol=1.5" - item := &request.Item{ - Method: http.MethodGet, - Path: endpoint + path, - Result: result, - Verbose: b.Verbose, - HTTPDebugging: b.HTTPDebugging, - HTTPRecording: b.HTTPRecording, - } - return b.SendPayload(ctx, request.Unset, func() (*request.Item, error) { - return item, nil - }, request.UnauthenticatedRequest) -} - -// WsAuth sends an authentication message to receive auth data -// Authentications expire after 10 minutes -func (b *Bittrex) WsAuth(ctx context.Context) error { - creds, err := b.GetCredentials(ctx) - if err != nil { - return err - } - // [apiKey, timestamp in ms, random uuid, signed payload] - apiKey := creds.Key - randomContent, err := uuid.NewV4() - if err != nil { - return err - } - timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) - hmac, err := crypto.GetHMAC( - crypto.HashSHA512, - []byte(timestamp+randomContent.String()), - []byte(creds.Secret), - ) - if err != nil { - return err - } - - signature := crypto.HexEncodeToString(hmac) - - req := WsEventRequest{ - Hub: "c3", - Method: authenticate, - InvocationID: b.Websocket.Conn.GenerateMessageID(false), - } - - arguments := make([]string, 0) - arguments = append(arguments, apiKey, timestamp, randomContent.String(), signature) - req.Arguments = arguments - - requestString, err := json.Marshal(req) - if err != nil { - return err - } - if b.Verbose { - log.Debugf(log.WebsocketMgr, "%s Sending JSON message - %s\n", b.Name, requestString) - } - - respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req) - if err != nil { - return err - } - var response WsAuthResponse - err = json.Unmarshal(respRaw, &response) - if err != nil { - log.Warnf(log.WebsocketMgr, "%s - Cannot unmarshal into WsAuthResponse (%s)\n", b.Name, string(respRaw)) - return err - } - if !response.Response.Success { - log.Warnf(log.WebsocketMgr, "%s - Unable to authenticate (%s)", b.Name, response.Response.ErrorCode) - b.Websocket.SetCanUseAuthenticatedEndpoints(false) - } - return nil -} - -// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be -// handled by ManageSubscriptions() -func (b *Bittrex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) { - var subscriptions []stream.ChannelSubscription - pairs, err := b.GetEnabledPairs(asset.Spot) - if err != nil { - return nil, err - } - - channels := defaultSpotSubscribedChannels - if b.IsWebsocketAuthenticationSupported() { - channels = append(channels, defaultSpotSubscribedChannelsAuth...) - } - - for i := range pairs { - pair, err := b.FormatExchangeCurrency(pairs[i], asset.Spot) - if err != nil { - return nil, err - } - for y := range channels { - var channel string - switch channels[y] { - case wsOrderbook: - channel = channels[y] + "_" + pair.String() + "_" + strconv.FormatInt(orderbookDepth, 10) - case wsTicker: - channel = channels[y] + "_" + pair.String() - case wsMarketSummary: - channel = channels[y] + "_" + pair.String() - default: - channel = channels[y] - } - subscriptions = append(subscriptions, - stream.ChannelSubscription{ - Channel: channel, - Currency: pair, - Asset: asset.Spot, - }) - } - } - - return subscriptions, nil -} - -// Subscribe sends a websocket message to receive data from the channel -func (b *Bittrex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error { - var x int - var errs error - for x = 0; x+wsMessageRateLimit < len(channelsToSubscribe); x += wsMessageRateLimit { - err := b.subscribeSlice(channelsToSubscribe[x : x+wsMessageRateLimit]) - if err != nil { - errs = common.AppendError(errs, err) - } - } - err := b.subscribeSlice(channelsToSubscribe[x:]) - if err != nil { - errs = common.AppendError(errs, err) - } - return errs -} - -func (b *Bittrex) subscribeSlice(channelsToSubscribe []stream.ChannelSubscription) error { - req := WsEventRequest{ - Hub: "c3", - Method: subscribe, - InvocationID: b.Websocket.Conn.GenerateMessageID(false), - } - - channels := make([]string, len(channelsToSubscribe)) - for i := range channelsToSubscribe { - channels[i] = channelsToSubscribe[i].Channel - } - arguments := make([][]string, 0) - arguments = append(arguments, channels) - req.Arguments = arguments - - requestString, err := json.Marshal(req) - if err != nil { - return err - } - if b.Verbose { - log.Debugf(log.WebsocketMgr, "%s - Sending JSON message - %s\n", b.Name, requestString) - } - respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req) - if err != nil { - return err - } - var response WsSubscriptionResponse - err = json.Unmarshal(respRaw, &response) - if err != nil { - return err - } - var errs error - for i := range response.Response { - if !response.Response[i].Success { - errs = common.AppendError(errs, errors.New("unable to subscribe to "+channels[i]+" - error code "+response.Response[i].ErrorCode)) - continue - } - b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i]) - } - return errs -} - -// Unsubscribe sends a websocket message to receive data from the channel -func (b *Bittrex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { - var x int - var errs error - for x = 0; x+wsMessageRateLimit < len(channelsToUnsubscribe); x += wsMessageRateLimit { - err := b.unsubscribeSlice(channelsToUnsubscribe[x : x+wsMessageRateLimit]) - if err != nil { - errs = common.AppendError(errs, err) - } - } - err := b.unsubscribeSlice(channelsToUnsubscribe[x:]) - if err != nil { - errs = common.AppendError(errs, err) - } - return errs -} - -func (b *Bittrex) unsubscribeSlice(channelsToUnsubscribe []stream.ChannelSubscription) error { - req := WsEventRequest{ - Hub: "c3", - Method: unsubscribe, - InvocationID: b.Websocket.Conn.GenerateMessageID(false), - } - - channels := make([]string, len(channelsToUnsubscribe)) - for i := range channelsToUnsubscribe { - channels[i] = channelsToUnsubscribe[i].Channel - } - arguments := make([][]string, 0) - arguments = append(arguments, channels) - req.Arguments = arguments - - requestString, err := json.Marshal(req) - if err != nil { - return err - } - if b.Verbose { - log.Debugf(log.WebsocketMgr, "%s - Sending JSON message - %s\n", b.Name, requestString) - } - respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(req.InvocationID, req) - if err != nil { - return err - } - var response WsSubscriptionResponse - err = json.Unmarshal(respRaw, &response) - if err != nil { - return err - } - var errs error - for i := range response.Response { - if !response.Response[i].Success { - errs = common.AppendError(errs, errors.New("unable to unsubscribe from "+channels[i]+" - error code "+response.Response[i].ErrorCode)) - continue - } - b.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i]) - } - return errs -} - -// wsReadData gets and passes on websocket messages for processing -func (b *Bittrex) wsReadData() { - defer b.Websocket.Wg.Done() - - for { - select { - case <-b.Websocket.ShutdownC: - return - default: - resp := b.Websocket.Conn.ReadMessage() - if resp.Raw == nil { - log.Warnf(log.WebsocketMgr, "%s Received empty message\n", b.Name) - return - } - - err := b.wsHandleData(resp.Raw) - if err != nil { - b.Websocket.DataHandler <- err - } - } - } -} - -func (b *Bittrex) wsDecodeMessage(encodedMessage string, v interface{}) error { - raw, err := crypto.Base64Decode(encodedMessage) - if err != nil { - return err - } - reader := flate.NewReader(bytes.NewBuffer(raw)) - message, err := io.ReadAll(reader) - if err != nil { - return err - } - if err = reader.Close(); err != nil { - log.Warnf(log.WebsocketMgr, "%s wsDecodeMessage: unable to close reader: %s", - b.Name, - err, - ) - } - return json.Unmarshal(message, v) -} - -func (b *Bittrex) wsHandleData(respRaw []byte) error { - var response WsEventResponse - err := json.Unmarshal(respRaw, &response) - if err != nil { - log.Warnf(log.WebsocketMgr, "%s Cannot unmarshal into eventResponse (%s)\n", b.Name, string(respRaw)) - return err - } - if response.Response != nil && response.InvocationID > 0 { - if b.Websocket.Match.IncomingWithData(response.InvocationID, respRaw) { - return nil - } - return errors.New("received response to unknown request") - } - - if response.Response == nil && len(response.Message) == 0 && response.C == "" { - if b.Verbose { - log.Warnf(log.WebsocketMgr, "%s Received keep-alive (%s)\n", b.Name, string(respRaw)) - } - return nil - } - for i := range response.Message { - switch response.Message[i].Method { - case "orderBook": - for j := range response.Message[i].Arguments { - var orderbookUpdate OrderbookUpdateMessage - err = b.wsDecodeMessage(response.Message[i].Arguments[j], &orderbookUpdate) - if err != nil { - return err - } - var init bool - init, err = b.UpdateLocalOBBuffer(&orderbookUpdate) - if err != nil { - if init { - return nil - } - return fmt.Errorf("%v - UpdateLocalCache error: %s", - b.Name, - err) - } - } - case "ticker": - for j := range response.Message[i].Arguments { - var tickerUpdate TickerData - err = b.wsDecodeMessage(response.Message[i].Arguments[j], &tickerUpdate) - if err != nil { - return err - } - err = b.WsProcessUpdateTicker(tickerUpdate) - if err != nil { - return err - } - } - case "marketSummary": - for j := range response.Message[i].Arguments { - var marketSummaryUpdate MarketSummaryData - err = b.wsDecodeMessage(response.Message[i].Arguments[j], &marketSummaryUpdate) - if err != nil { - return err - } - - err = b.WsProcessUpdateMarketSummary(&marketSummaryUpdate) - if err != nil { - return err - } - } - case "heartbeat": - if b.Verbose { - log.Warnf(log.WebsocketMgr, "%s Received heartbeat\n", b.Name) - } - case "authenticationExpiring": - if b.Verbose { - log.Debugf(log.WebsocketMgr, "%s - Re-authenticating.\n", b.Name) - } - err = b.WsAuth(context.TODO()) - if err != nil { - b.Websocket.DataHandler <- err - b.Websocket.SetCanUseAuthenticatedEndpoints(false) - } - case "order": - for j := range response.Message[i].Arguments { - var orderUpdate OrderUpdateMessage - err = b.wsDecodeMessage(response.Message[i].Arguments[j], &orderUpdate) - if err != nil { - return err - } - err = b.WsProcessUpdateOrder(&orderUpdate) - if err != nil { - return err - } - } - } - } - return nil -} - -// WsProcessUpdateTicker processes an update on the ticker -func (b *Bittrex) WsProcessUpdateTicker(tickerData TickerData) error { - pair, err := currency.NewPairFromString(tickerData.Symbol) - if err != nil { - return err - } - - tickerPrice, err := ticker.GetTicker(b.Name, pair, asset.Spot) - if err != nil { - b.tickerCache.mu.Lock() - defer b.tickerCache.mu.Unlock() - if b.tickerCache.MarketSummaries[tickerData.Symbol] != nil { - marketSummaryData := b.tickerCache.MarketSummaries[tickerData.Symbol] - tickerPrice = b.constructTicker(tickerData, marketSummaryData, pair, asset.Spot) - b.Websocket.DataHandler <- tickerPrice - return nil - } - b.tickerCache.Tickers[tickerData.Symbol] = &tickerData - return nil - } - - tickerPrice.Last = tickerData.LastTradeRate - tickerPrice.Bid = tickerData.BidRate - tickerPrice.Ask = tickerData.AskRate - - b.Websocket.DataHandler <- tickerPrice - - return nil -} - -// WsProcessUpdateMarketSummary processes an update on the ticker -func (b *Bittrex) WsProcessUpdateMarketSummary(marketSummaryData *MarketSummaryData) error { - pair, err := currency.NewPairFromString(marketSummaryData.Symbol) - if err != nil { - return err - } - - tickerPrice, err := ticker.GetTicker(b.Name, pair, asset.Spot) - if err != nil { - b.tickerCache.mu.Lock() - defer b.tickerCache.mu.Unlock() - if b.tickerCache.Tickers[marketSummaryData.Symbol] != nil { - tickerData := b.tickerCache.Tickers[marketSummaryData.Symbol] - tickerPrice = b.constructTicker(*tickerData, marketSummaryData, pair, asset.Spot) - b.Websocket.DataHandler <- tickerPrice - return nil - } - b.tickerCache.MarketSummaries[marketSummaryData.Symbol] = marketSummaryData - return nil - } - - tickerPrice.High = marketSummaryData.High - tickerPrice.Low = marketSummaryData.Low - tickerPrice.Volume = marketSummaryData.Volume - tickerPrice.QuoteVolume = marketSummaryData.QuoteVolume - tickerPrice.LastUpdated = marketSummaryData.UpdatedAt - - b.Websocket.DataHandler <- tickerPrice - - return nil -} - -// WsProcessUpdateOrder processes an update on the open orders -func (b *Bittrex) WsProcessUpdateOrder(data *OrderUpdateMessage) error { - orderType, err := order.StringToOrderType(data.Delta.Type) - if err != nil { - b.Websocket.DataHandler <- order.ClassificationError{ - Exchange: b.Name, - OrderID: data.Delta.ID, - Err: err, - } - } - orderSide, err := order.StringToOrderSide(data.Delta.Direction) - if err != nil { - b.Websocket.DataHandler <- order.ClassificationError{ - Exchange: b.Name, - OrderID: data.Delta.ID, - Err: err, - } - } - orderStatus, err := order.StringToOrderStatus(data.Delta.Status) - if err != nil { - b.Websocket.DataHandler <- order.ClassificationError{ - Exchange: b.Name, - OrderID: data.Delta.ID, - Err: err, - } - } - - pair, err := currency.NewPairFromString(data.Delta.MarketSymbol) - if err != nil { - b.Websocket.DataHandler <- order.ClassificationError{ - Exchange: b.Name, - OrderID: data.Delta.ID, - Err: err, - } - } - - b.Websocket.DataHandler <- &order.Detail{ - ImmediateOrCancel: data.Delta.TimeInForce == string(ImmediateOrCancel), - FillOrKill: data.Delta.TimeInForce == string(GoodTilCancelled), - PostOnly: data.Delta.TimeInForce == string(PostOnlyGoodTilCancelled), - Price: data.Delta.Limit, - Amount: data.Delta.Quantity, - RemainingAmount: data.Delta.Quantity - data.Delta.FillQuantity, - ExecutedAmount: data.Delta.FillQuantity, - Exchange: b.Name, - OrderID: data.Delta.ID, - Type: orderType, - Side: orderSide, - Status: orderStatus, - AssetType: asset.Spot, - Date: data.Delta.CreatedAt, - Pair: pair, - } - return nil -} diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go deleted file mode 100644 index ea62bc2d3cd..00000000000 --- a/exchanges/bittrex/bittrex_wrapper.go +++ /dev/null @@ -1,1108 +0,0 @@ -package bittrex - -import ( - "context" - "errors" - "fmt" - "sort" - "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/fundingrate" - "github.com/thrasher-corp/gocryptotrader/exchanges/futures" - "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" -) - -var ( - oneDay = time.Hour * 24 - oneMonth = oneDay * 31 - oneYear = oneDay * 366 -) - -// GetDefaultConfig returns a default exchange config -func (b *Bittrex) GetDefaultConfig(ctx context.Context) (*config.Exchange, error) { - b.SetDefaults() - exchCfg, err := b.GetStandardConfig() - if err != nil { - return nil, err - } - - err = b.SetupDefaults(exchCfg) - if err != nil { - return nil, err - } - - if b.Features.Supports.RESTCapabilities.AutoPairUpdates { - err = b.UpdateTradablePairs(ctx, true) - if err != nil { - return nil, err - } - } - return exchCfg, nil -} - -// SetDefaults sets the basic defaults for Bittrex -func (b *Bittrex) SetDefaults() { - b.Name = "Bittrex" - b.Enabled = true - b.Verbose = true - b.API.CredentialsValidator.RequiresKey = true - b.API.CredentialsValidator.RequiresSecret = true - - spot := currency.PairStore{ - RequestFormat: ¤cy.PairFormat{ - Uppercase: true, - Delimiter: currency.DashDelimiter, - }, - ConfigFormat: ¤cy.PairFormat{ - Uppercase: true, - Delimiter: currency.DashDelimiter, - }, - } - - err := b.StoreAssetPairFormat(asset.Spot, spot) - if err != nil { - log.Errorln(log.ExchangeSys, err) - } - - b.Features = exchange.Features{ - Supports: exchange.FeaturesSupported{ - REST: true, - Websocket: true, - RESTCapabilities: protocol.Features{ - TickerFetching: true, - TickerBatching: true, - KlineFetching: true, - TradeFetching: true, - OrderbookFetching: true, - AutoPairUpdates: true, - GetOrder: true, - GetOrders: true, - CancelOrder: true, - CancelOrders: true, - SubmitOrder: true, - DepositHistory: true, - WithdrawalHistory: true, - UserTradeHistory: true, - CryptoDeposit: true, - CryptoWithdrawal: true, - TradeFee: true, - CryptoWithdrawalFee: true, - }, - WebsocketCapabilities: protocol.Features{ - TickerFetching: true, - OrderbookFetching: true, - Subscribe: true, - Unsubscribe: true, - }, - WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | - exchange.NoFiatWithdrawals, - }, - Enabled: exchange.FeaturesEnabled{ - AutoPairUpdates: true, - Kline: kline.ExchangeCapabilitiesEnabled{ - Intervals: kline.DeployExchangeIntervals( - kline.IntervalCapacity{Interval: kline.OneMin, Capacity: 1440}, // 1m interval: candles for 1 day (0:00 - 23:59) - kline.IntervalCapacity{Interval: kline.FiveMin, Capacity: 288}, // 5m interval: candles for 1 day (0:00 - 23:55) - kline.IntervalCapacity{Interval: kline.OneHour, Capacity: 744}, // 1 hour interval: candles for 31 days (0:00 - 23:00) - kline.IntervalCapacity{Interval: kline.OneDay, Capacity: 366}, // 1 day interval: candles for 366 days - ), - }, - }, - } - - b.Requester, err = request.New(b.Name, - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), - request.WithLimiter(request.NewBasicRateLimit(ratePeriod, rateLimit))) - if err != nil { - log.Errorln(log.ExchangeSys, err) - } - - b.API.Endpoints = b.NewEndpoints() - - err = b.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{ - exchange.RestSpot: bittrexAPIRestURL, - exchange.WebsocketSpot: bittrexAPIWSURL, - exchange.WebsocketSpotSupplementary: bittrexAPIWSNegotiationsURL, - }) - if err != nil { - log.Errorln(log.ExchangeSys, err) - } - b.Websocket = stream.New() - b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit - b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout - b.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit -} - -// Setup takes in the supplied exchange configuration details and sets params -func (b *Bittrex) Setup(exch *config.Exchange) error { - err := exch.Validate() - if err != nil { - return err - } - if !exch.Enabled { - b.SetEnabled(false) - return nil - } - err = b.SetupDefaults(exch) - if err != nil { - return err - } - - wsRunningEndpoint, err := b.API.Endpoints.GetURL(exchange.WebsocketSpot) - if err != nil { - return err - } - - // Websocket details setup below - err = b.Websocket.Setup(&stream.WebsocketSetup{ - ExchangeConfig: exch, - DefaultURL: bittrexAPIWSURL, // Default ws endpoint so we can roll back via CLI if needed. - RunningURL: wsRunningEndpoint, - Connector: b.WsConnect, // Connector function outlined above. - Subscriber: b.Subscribe, // Subscriber function outlined above. - Unsubscriber: b.Unsubscribe, // Unsubscriber function outlined above. - GenerateSubscriptions: b.GenerateDefaultSubscriptions, // GenerateDefaultSubscriptions function outlined above. - Features: &b.Features.Supports.WebsocketCapabilities, // Defines the capabilities of the websocket outlined in supported features struct. This allows the websocket connection to be flushed appropriately if we have a pair/asset enable/disable change. This is outlined below. - OrderbookBufferConfig: buffer.Config{ - SortBuffer: true, - SortBufferByUpdateIDs: true, - }, - }) - if err != nil { - return err - } - // Sets up a new connection for the websocket, there are two separate connections denoted by the ConnectionSetup struct auth bool. - return b.Websocket.SetupNewConnection(stream.ConnectionSetup{ - ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - RateLimit: wsRateLimit, - // Authenticated bool sets if the connection is dedicated for an authenticated websocket stream which can be accessed from the Websocket field variable AuthConn e.g. f.Websocket.AuthConn - }) -} - -// GetServerTime returns the current exchange server time. -func (b *Bittrex) GetServerTime(_ context.Context, _ asset.Item) (time.Time, error) { - return time.Time{}, common.ErrFunctionNotSupported -} - -// Start starts the Bittrex go routine -func (b *Bittrex) Start(ctx context.Context, wg *sync.WaitGroup) error { - if wg == nil { - return fmt.Errorf("%T %w", wg, common.ErrNilPointer) - } - wg.Add(1) - go func() { - b.Run(ctx) - wg.Done() - }() - return nil -} - -// Run implements the Bittrex wrapper -func (b *Bittrex) Run(ctx context.Context) { - if b.Verbose { - log.Debugf(log.ExchangeSys, - "%s Websocket: %s.", - b.Name, - common.IsEnabled(b.Websocket.IsEnabled())) - b.PrintEnabledPairs() - } - - if !b.GetEnabledFeatures().AutoPairUpdates { - return - } - - err := b.UpdateTradablePairs(ctx, false) - if err != nil { - log.Errorf(log.ExchangeSys, - "%s failed to update tradable pairs. Err: %s", - b.Name, - err) - } - restURL, err := b.API.Endpoints.GetURL(exchange.RestSpot) - if err != nil { - log.Errorf(log.ExchangeSys, - "%s failed to check REST Spot URL. Err: %s", - b.Name, - err) - } - if restURL == bittrexAPIDeprecatedURL { - err = b.API.Endpoints.SetRunning(exchange.RestSpot.String(), bittrexAPIRestURL) - if err != nil { - log.Errorf(log.ExchangeSys, - "%s failed to update deprecated REST Spot URL. Err: %s", - b.Name, - err) - } - b.Config.API.Endpoints[exchange.RestSpot.String()] = bittrexAPIRestURL - log.Warnf(log.ExchangeSys, - "Deprecated %s REST URL updated from %s to %s", b.Name, bittrexAPIDeprecatedURL, bittrexAPIRestURL) - } -} - -// FetchTradablePairs returns a list of the exchanges tradable pairs -func (b *Bittrex) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) { - // Bittrex only supports spot trading - if !b.SupportsAsset(a) { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) - } - markets, err := b.GetMarkets(ctx) - if err != nil { - return nil, err - } - - pairs := make([]currency.Pair, 0, len(markets)) - for x := range markets { - if markets[x].Status != "ONLINE" { - continue - } - var pair currency.Pair - pair, err = currency.NewPairFromString(markets[x].Symbol) - if err != nil { - return nil, err - } - pairs = append(pairs, pair) - } - return pairs, nil -} - -// UpdateTradablePairs updates the exchanges available pairs and stores -// them in the exchanges config -func (b *Bittrex) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error { - pairs, err := b.FetchTradablePairs(ctx, asset.Spot) - if err != nil { - return err - } - err = b.UpdatePairs(pairs, asset.Spot, false, forceUpdate) - if err != nil { - return err - } - return b.EnsureOnePairEnabled() -} - -// UpdateTickers updates the ticker for all currency pairs of a given asset type -func (b *Bittrex) UpdateTickers(ctx context.Context, a asset.Item) error { - if a != asset.Spot { - return fmt.Errorf("%w %v", asset.ErrNotSupported, a) - } - tickers, err := b.GetTickers(ctx) - if err != nil { - return err - } - summaries, err := b.GetMarketSummaries(ctx) - if err != nil { - return err - } - for x := range tickers { - for y := range summaries { - if !strings.EqualFold(summaries[y].Symbol, tickers[x].Symbol) { - continue - } - var pair currency.Pair - pair, err = currency.NewPairFromString(tickers[x].Symbol) - if err != nil { - return err - } - tickerPrice := b.constructTicker(tickers[x], &summaries[y], pair, a) - err = ticker.ProcessTicker(tickerPrice) - if err != nil { - return err - } - } - } - return nil -} - -// UpdateTicker updates and returns the ticker for a currency pair -func (b *Bittrex) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) { - formattedPair, err := b.FormatExchangeCurrency(p, a) - if err != nil { - return nil, err - } - - t, err := b.GetTicker(ctx, formattedPair.String()) - if err != nil { - return nil, err - } - - s, err := b.GetMarketSummary(ctx, formattedPair.String()) - if err != nil { - return nil, err - } - - pair, err := currency.NewPairFromString(t.Symbol) - if err != nil { - return nil, err - } - - tickerPrice := b.constructTicker(t, &s, pair, a) - - err = ticker.ProcessTicker(tickerPrice) - if err != nil { - return nil, err - } - - return ticker.GetTicker(b.Name, p, a) -} - -// constructTicker constructs a ticker price from the underlying data -func (b *Bittrex) constructTicker(t TickerData, s *MarketSummaryData, pair currency.Pair, assetType asset.Item) *ticker.Price { - return &ticker.Price{ - Pair: pair, - Last: t.LastTradeRate, - Bid: t.BidRate, - Ask: t.AskRate, - High: s.High, - Low: s.Low, - Volume: s.Volume, - QuoteVolume: s.QuoteVolume, - LastUpdated: s.UpdatedAt, - AssetType: assetType, - ExchangeName: b.Name, - } -} - -// FetchTicker returns the ticker for a currency pair -func (b *Bittrex) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) { - resp, err := ticker.GetTicker(b.Name, p, assetType) - if err != nil { - return b.UpdateTicker(ctx, p, assetType) - } - return resp, nil -} - -// FetchOrderbook returns orderbook base on the currency pair -func (b *Bittrex) FetchOrderbook(ctx context.Context, c currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - resp, err := orderbook.Get(b.Name, c, assetType) - if err != nil { - return b.UpdateOrderbook(ctx, c, assetType) - } - return resp, nil -} - -// UpdateOrderbook updates and returns the orderbook for a currency pair -func (b *Bittrex) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - if p.IsEmpty() { - return nil, currency.ErrCurrencyPairEmpty - } - if err := b.CurrencyPairs.IsAssetEnabled(assetType); err != nil { - return nil, err - } - book := &orderbook.Base{ - Exchange: b.Name, - Pair: p, - Asset: assetType, - VerifyOrderbook: b.CanVerifyOrderbook, - } - - formattedPair, err := b.FormatExchangeCurrency(p, assetType) - if err != nil { - return book, err - } - - // Valid order book depths are 1, 25 and 500 - orderbookData, sequence, err := b.GetOrderbook(ctx, formattedPair.String(), orderbookDepth) - if err != nil { - return book, err - } - - book.LastUpdateID = sequence - book.Bids = make(orderbook.Items, len(orderbookData.Bid)) - book.Asks = make(orderbook.Items, len(orderbookData.Ask)) - - for x := range orderbookData.Bid { - book.Bids[x] = orderbook.Item{ - Amount: orderbookData.Bid[x].Quantity, - Price: orderbookData.Bid[x].Rate, - } - } - - for x := range orderbookData.Ask { - book.Asks[x] = orderbook.Item{ - Amount: orderbookData.Ask[x].Quantity, - Price: orderbookData.Ask[x].Rate, - } - } - err = book.Process() - if err != nil { - return book, err - } - - return orderbook.Get(b.Name, p, assetType) -} - -// UpdateAccountInfo retrieves balances for all enabled currencies -func (b *Bittrex) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var resp account.Holdings - balanceData, err := b.GetBalances(ctx) - if err != nil { - return resp, err - } - - currencies := make([]account.Balance, len(balanceData)) - for i := range balanceData { - currencies[i] = account.Balance{ - Currency: currency.NewCode(balanceData[i].CurrencySymbol), - Total: balanceData[i].Total, - Hold: balanceData[i].Total - balanceData[i].Available, - Free: balanceData[i].Available, - } - } - - resp.Accounts = append(resp.Accounts, account.SubAccount{ - AssetType: assetType, - Currencies: currencies, - }) - resp.Exchange = b.Name - - creds, err := b.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - return resp, account.Process(&resp, creds) -} - -// FetchAccountInfo retrieves balances for all enabled currencies -func (b *Bittrex) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - creds, err := b.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - resp, err := account.GetHoldings(b.Name, creds, assetType) - if err != nil { - return b.UpdateAccountInfo(ctx, assetType) - } - return resp, nil -} - -// GetAccountFundingHistory returns funding history, deposits and -// withdrawals -func (b *Bittrex) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) { - closedDepositData, err := b.GetClosedDeposits(ctx) - if err != nil { - return nil, err - } - openDepositData, err := b.GetOpenDeposits(ctx) - if err != nil { - return nil, err - } - closedWithdrawalData, err := b.GetClosedWithdrawals(ctx) - if err != nil { - return nil, err - } - openWithdrawalData, err := b.GetOpenWithdrawals(ctx) - if err != nil { - return nil, err - } - - depositData := make([]DepositData, 0, len(closedDepositData)+len(openDepositData)) - depositData = append(depositData, closedDepositData...) - depositData = append(depositData, openDepositData...) - - withdrawalData := make([]WithdrawalData, 0, len(closedWithdrawalData)+len(openWithdrawalData)) - withdrawalData = append(withdrawalData, closedWithdrawalData...) - withdrawalData = append(withdrawalData, openWithdrawalData...) - - resp := make([]exchange.FundingHistory, 0, len(depositData)+len(withdrawalData)) - for x := range depositData { - resp = append(resp, exchange.FundingHistory{ - ExchangeName: b.Name, - Status: depositData[x].Status, - Description: depositData[x].CryptoAddressTag, - Timestamp: depositData[x].UpdatedAt, - Currency: depositData[x].CurrencySymbol, - Amount: depositData[x].Quantity, - TransferType: "deposit", - CryptoToAddress: depositData[x].CryptoAddress, - CryptoTxID: depositData[x].TxID, - }) - } - for x := range withdrawalData { - resp = append(resp, exchange.FundingHistory{ - ExchangeName: b.Name, - Status: withdrawalData[x].Status, - Description: withdrawalData[x].CryptoAddressTag, - Timestamp: depositData[x].UpdatedAt, - Currency: withdrawalData[x].CurrencySymbol, - Amount: withdrawalData[x].Quantity, - Fee: withdrawalData[x].TxCost, - TransferType: "withdrawal", - CryptoToAddress: withdrawalData[x].CryptoAddress, - CryptoTxID: withdrawalData[x].TxID, - TransferID: withdrawalData[x].ID, - }) - } - return resp, nil -} - -// GetWithdrawalsHistory returns previous withdrawals data -func (b *Bittrex) GetWithdrawalsHistory(_ context.Context, _ currency.Code, _ asset.Item) (resp []exchange.WithdrawalHistory, err error) { - return nil, common.ErrFunctionNotSupported -} - -// GetRecentTrades returns the most recent trades for a currency and asset -func (b *Bittrex) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) { - formattedPair, err := b.FormatExchangeCurrency(p, assetType) - if err != nil { - return nil, err - } - - tradeData, err := b.GetMarketHistory(ctx, formattedPair.String()) - if err != nil { - return nil, err - } - - resp := make([]trade.Data, len(tradeData)) - for i := range tradeData { - var side order.Side - side, err = order.StringToOrderSide(tradeData[i].TakerSide) - if err != nil { - return nil, err - } - resp[i] = trade.Data{ - Exchange: b.Name, - TID: tradeData[i].ID, - CurrencyPair: formattedPair, - AssetType: assetType, - Side: side, - Price: tradeData[i].Rate, - Amount: tradeData[i].Quantity, - Timestamp: tradeData[i].ExecutedAt, - } - } - - err = b.AddTradesToBuffer(resp...) - if err != nil { - return nil, err - } - - sort.Sort(trade.ByDate(resp)) - return resp, nil -} - -// GetHistoricTrades returns historic trade data within the timeframe provided -// Bittrex only reports recent trades -func (b *Bittrex) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) { - return nil, common.ErrFunctionNotSupported -} - -// SubmitOrder submits a new order -func (b *Bittrex) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { - return nil, err - } - - if s.Side.IsShort() { - s.Side = order.Sell - } - - if s.Side.IsLong() { - s.Side = order.Buy - } - - formattedPair, err := b.FormatExchangeCurrency(s.Pair, s.AssetType) - if err != nil { - return nil, err - } - - orderData, err := b.Order(ctx, - formattedPair.String(), - s.Side.String(), - s.Type.String(), - GoodTilCancelled, - s.Price, - s.Amount, - 0.0) - if err != nil { - return nil, err - } - return s.DeriveSubmitResponse(orderData.ID) -} - -// ModifyOrder will allow of changing orderbook placement and limit to -// market conversion -func (b *Bittrex) ModifyOrder(_ context.Context, _ *order.Modify) (*order.ModifyResponse, error) { - return nil, common.ErrFunctionNotSupported -} - -// CancelOrder cancels an order by its corresponding ID number -func (b *Bittrex) CancelOrder(ctx context.Context, ord *order.Cancel) error { - if err := ord.Validate(ord.StandardCancel()); err != nil { - return err - } - _, err := b.CancelExistingOrder(ctx, ord.OrderID) - return err -} - -// CancelBatchOrders cancels an orders by their corresponding ID numbers -func (b *Bittrex) CancelBatchOrders(_ context.Context, _ []order.Cancel) (*order.CancelBatchResponse, error) { - return nil, common.ErrFunctionNotSupported -} - -// CancelAllOrders cancels all orders associated with a currency pair, or cancels all orders for all -// pairs if no pair was specified -func (b *Bittrex) CancelAllOrders(ctx context.Context, orderCancellation *order.Cancel) (order.CancelAllResponse, error) { - var pair string - if orderCancellation != nil { - formattedPair, err := b.FormatExchangeCurrency(orderCancellation.Pair, orderCancellation.AssetType) - if err != nil { - return order.CancelAllResponse{}, err - } - pair = formattedPair.String() - } - orderData, err := b.CancelOpenOrders(ctx, pair) - if err != nil { - return order.CancelAllResponse{}, err - } - - tempMap := make(map[string]string) - for x := range orderData { - if orderData[x].Result.Status == "CLOSED" { - tempMap[orderData[x].ID] = "Success" - } - } - resp := order.CancelAllResponse{ - Status: tempMap, - Count: int64(len(tempMap)), - } - return resp, nil -} - -// GetOrderInfo returns information on a current open order -func (b *Bittrex) GetOrderInfo(ctx context.Context, orderID string, _ currency.Pair, _ asset.Item) (*order.Detail, error) { - orderData, err := b.GetOrder(ctx, orderID) - if err != nil { - return nil, err - } - - return b.ConstructOrderDetail(&orderData) -} - -// ConstructOrderDetail constructs an order detail item from the underlying data -func (b *Bittrex) ConstructOrderDetail(orderData *OrderData) (*order.Detail, error) { - immediateOrCancel := false - if orderData.TimeInForce == string(ImmediateOrCancel) { - immediateOrCancel = true - } - - format, err := b.GetPairFormat(asset.Spot, false) - if err != nil { - return nil, err - } - orderPair, err := currency.NewPairDelimiter(orderData.MarketSymbol, - format.Delimiter) - if err != nil { - log.Errorf(log.ExchangeSys, - "Exchange %v Func %v Order %v Could not parse currency pair %v", - b.Name, - "GetActiveOrders", - orderData.ID, - err) - } - orderType, err := order.StringToOrderType(orderData.Type) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", b.Name, err) - } - - var orderStatus order.Status - switch orderData.Status { - case order.Open.String(): - switch orderData.FillQuantity { - case 0: - orderStatus = order.Open - default: - orderStatus = order.PartiallyFilled - } - case order.Closed.String(): - switch orderData.FillQuantity { - case 0: - orderStatus = order.Cancelled - case orderData.Quantity: - orderStatus = order.Filled - default: - orderStatus = order.PartiallyCancelled - } - } - - return &order.Detail{ - ImmediateOrCancel: immediateOrCancel, - Amount: orderData.Quantity, - ExecutedAmount: orderData.FillQuantity, - RemainingAmount: orderData.Quantity - orderData.FillQuantity, - Price: orderData.Limit, - Date: orderData.CreatedAt, - OrderID: orderData.ID, - Exchange: b.Name, - Type: orderType, - Pair: orderPair, - Status: orderStatus, - }, nil -} - -// GetDepositAddress returns a deposit address for a specified currency -func (b *Bittrex) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, _ string) (*deposit.Address, error) { - depositAddr, err := b.GetCryptoDepositAddress(ctx, cryptocurrency.String()) - if err != nil { - return nil, err - } - - return &deposit.Address{ - Address: depositAddr.CryptoAddress, - Tag: depositAddr.CryptoAddressTag, - }, nil -} - -// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is -// submitted -func (b *Bittrex) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { - if err := withdrawRequest.Validate(); err != nil { - return nil, err - } - result, err := b.Withdraw(ctx, - withdrawRequest.Currency.String(), - withdrawRequest.Crypto.AddressTag, - withdrawRequest.Crypto.Address, - withdrawRequest.Amount) - if err != nil { - return nil, err - } - return &withdraw.ExchangeResponse{ - Name: b.Name, - ID: result.ID, - Status: result.Status, - }, err -} - -// WithdrawFiatFunds returns a withdrawal ID when a -// withdrawal is submitted -func (b *Bittrex) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { - return nil, common.ErrFunctionNotSupported -} - -// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a -// withdrawal is submitted -func (b *Bittrex) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { - return nil, common.ErrFunctionNotSupported -} - -// GetActiveOrders retrieves any orders that are active/open -func (b *Bittrex) GetActiveOrders(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) { - err := req.Validate() - if err != nil { - return nil, err - } - - var currPair string - if len(req.Pairs) == 1 { - var formattedPair currency.Pair - formattedPair, err = b.FormatExchangeCurrency(req.Pairs[0], asset.Spot) - if err != nil { - return nil, err - } - currPair = formattedPair.String() - } - - format, err := b.GetPairFormat(asset.Spot, false) - if err != nil { - return nil, err - } - - orderData, sequence, err := b.GetOpenOrders(ctx, currPair) - if err != nil { - return nil, err - } - - resp := make([]order.Detail, 0, len(orderData)) - for i := range orderData { - var pair currency.Pair - pair, err = currency.NewPairDelimiter(orderData[i].MarketSymbol, - format.Delimiter) - if err != nil { - log.Errorf(log.ExchangeSys, - "Exchange %v Func %v Order %v Could not parse currency pair %v", - b.Name, - "GetActiveOrders", - orderData[i].ID, - err) - } - - var orderType order.Type - orderType, err = order.StringToOrderType(orderData[i].Type) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", b.Name, err) - } - - var orderSide order.Side - orderSide, err = order.StringToOrderSide(orderData[i].Direction) - if err != nil { - log.Errorf(log.ExchangeSys, "GetActiveOrders - %s - cannot get order side - %s\n", b.Name, err.Error()) - } - - resp = append(resp, order.Detail{ - Amount: orderData[i].Quantity, - RemainingAmount: orderData[i].Quantity - orderData[i].FillQuantity, - ExecutedAmount: orderData[i].FillQuantity, - Price: orderData[i].Limit, - Date: orderData[i].CreatedAt, - OrderID: orderData[i].ID, - Exchange: b.Name, - Type: orderType, - Side: orderSide, - Status: order.Active, - Pair: pair, - }) - } - b.WsSequenceOrders = sequence - return req.Filter(b.Name, resp), nil -} - -// GetOrderHistory retrieves account order information -// Can Limit response to specific order status -func (b *Bittrex) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) { - err := req.Validate() - if err != nil { - return nil, err - } - if len(req.Pairs) == 0 { - return nil, errors.New("at least one currency is required to fetch order history") - } - - format, err := b.GetPairFormat(asset.Spot, false) - if err != nil { - return nil, err - } - - var resp []order.Detail - for x := range req.Pairs { - var formattedPair currency.Pair - formattedPair, err = b.FormatExchangeCurrency(req.Pairs[x], req.AssetType) - if err != nil { - return nil, err - } - - var orderData []OrderData - orderData, err = b.GetOrderHistoryForCurrency(ctx, formattedPair.String()) - if err != nil { - return nil, err - } - - for i := range orderData { - var pair currency.Pair - pair, err = currency.NewPairDelimiter(orderData[i].MarketSymbol, - format.Delimiter) - if err != nil { - log.Errorf(log.ExchangeSys, - "Exchange %v Func %v Order %v Could not parse currency pair %v", - b.Name, - "GetOrderHistory", - orderData[i].ID, - err) - } - var orderType order.Type - orderType, err = order.StringToOrderType(orderData[i].Type) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", b.Name, err) - } - - var orderSide order.Side - orderSide, err = order.StringToOrderSide(orderData[i].Direction) - if err != nil { - log.Errorf(log.ExchangeSys, "GetActiveOrders - %s - cannot get order side - %s\n", b.Name, err.Error()) - } - - var orderStatus order.Status - orderStatus, err = order.StringToOrderStatus(orderData[i].Status) - if err != nil { - log.Errorf(log.ExchangeSys, "GetActiveOrders - %s - cannot get order status - %s\n", b.Name, err.Error()) - } - - detail := order.Detail{ - Amount: orderData[i].Quantity, - ExecutedAmount: orderData[i].FillQuantity, - RemainingAmount: orderData[i].Quantity - orderData[i].FillQuantity, - Price: orderData[i].Limit, - Date: orderData[i].CreatedAt, - CloseTime: orderData[i].ClosedAt, - OrderID: orderData[i].ID, - Exchange: b.Name, - Type: orderType, - Side: orderSide, - Status: orderStatus, - Fee: orderData[i].Commission, - Pair: pair, - } - detail.InferCostsAndTimes() - resp = append(resp, detail) - } - } - return req.Filter(b.Name, resp), nil -} - -// GetFeeByType returns an estimate of fee based on type of transaction -func (b *Bittrex) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { - if feeBuilder == nil { - return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) - } - if !b.AreCredentialsValid(ctx) && // Todo check connection status - feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { - feeBuilder.FeeType = exchange.OfflineTradeFee - } - return b.GetFee(ctx, feeBuilder) -} - -// ValidateAPICredentials validates current credentials used for wrapper -// functionality -func (b *Bittrex) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error { - _, err := b.UpdateAccountInfo(ctx, assetType) - return b.CheckTransientError(err) -} - -// FormatExchangeKlineInterval returns Interval to string -// Overrides Base function -func (b *Bittrex) FormatExchangeKlineInterval(in kline.Interval) string { - switch in { - case kline.OneMin: - return "MINUTE_1" - case kline.FiveMin: - return "MINUTE_5" - case kline.OneHour: - return "HOUR_1" - case kline.OneDay: - return "DAY_1" - default: - return "notfound" - } -} - -// GetHistoricCandles returns candles between a time period for a set time interval -// Candles set size returned by Bittrex depends on interval length: -// - 1m interval: candles for 1 day (0:00 - 23:59) -// - 5m interval: candles for 1 day (0:00 - 23:55) -// - 1 hour interval: candles for 31 days -// - 1 day interval: candles for 366 days -// This implementation rounds returns candles up to the next interval or to the end -// time (whichever comes first) -func (b *Bittrex) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := b.GetKlineRequest(pair, a, interval, start, end, false) - if err != nil { - return nil, err - } - - candleInterval := b.FormatExchangeKlineInterval(req.ExchangeInterval) - if candleInterval == "notfound" { - return nil, fmt.Errorf("%w %v", kline.ErrInvalidInterval, interval) - } - - year, month, day := req.End.Date() - curYear, curMonth, curDay := time.Now().Date() - var getHistoric, getRecent bool - switch req.ExchangeInterval { - case kline.OneMin, kline.FiveMin: - if time.Since(req.Start) > oneDay { - getHistoric = true - } - if year >= curYear && month >= curMonth && day >= curDay { - getRecent = true - } - case kline.OneHour: - if time.Since(req.Start) > oneMonth { - getHistoric = true - } - if year >= curYear && month >= curMonth { - getRecent = true - } - case kline.OneDay: - if time.Since(req.Start) > oneYear { - getHistoric = true - } - if year >= curYear { - getRecent = true - } - } - - if !getHistoric && !getRecent { - return nil, errors.New("start end time range cannot get historic or recent candles") - } - - var candleData []CandleData - if getHistoric { - historicData, err := b.GetHistoricalCandles(ctx, - req.RequestFormatted.String(), - candleInterval, - "TRADE", - start.Year(), - int(start.Month()), - start.Day()) - if err != nil { - return nil, err - } - candleData = append(candleData, historicData...) - } - - if getRecent { - recentData, err := b.GetRecentCandles(ctx, - req.RequestFormatted.String(), - candleInterval, - "TRADE") - if err != nil { - return nil, err - } - candleData = append(candleData, recentData...) - } - - timeSeries := make([]kline.Candle, 0, len(candleData)) - for x := range candleData { - if candleData[x].StartsAt.Before(req.Start) || candleData[x].StartsAt.After(req.End) { - continue - } - timeSeries = append(timeSeries, kline.Candle{ - Time: candleData[x].StartsAt, - Open: candleData[x].Open, - High: candleData[x].High, - Low: candleData[x].Low, - Close: candleData[x].Close, - Volume: candleData[x].Volume, - }) - } - return req.ProcessResponse(timeSeries) -} - -// GetHistoricCandlesExtended returns candles between a time period for a set time interval -func (b *Bittrex) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair, _ asset.Item, _ kline.Interval, _, _ time.Time) (*kline.Item, error) { - // TODO implement with API upgradeËœ - return nil, common.ErrFunctionNotSupported -} - -// GetFuturesContractDetails returns all contracts from the exchange by asset type -func (b *Bittrex) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { - return nil, common.ErrFunctionNotSupported -} - -// GetLatestFundingRates returns the latest funding rates data -func (b *Bittrex) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { - return nil, common.ErrFunctionNotSupported -} - -// UpdateOrderExecutionLimits updates order execution limits -func (b *Bittrex) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { - return common.ErrNotYetImplemented -} diff --git a/exchanges/bittrex/bittrex_ws_orderbook.go b/exchanges/bittrex/bittrex_ws_orderbook.go deleted file mode 100644 index 508ce092fdc..00000000000 --- a/exchanges/bittrex/bittrex_ws_orderbook.go +++ /dev/null @@ -1,437 +0,0 @@ -package bittrex - -import ( - "context" - "fmt" - - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/log" -) - -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 -) - -func (b *Bittrex) setupOrderbookManager() { - if b.obm == nil { - b.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 _, m1 := range b.obm.state { - for _, m2 := range m1 { - for _, update := range m2 { - update.initialSync = true - update.needsFetchingBook = true - } - } - } - } - - for i := 0; i < maxWSOrderbookWorkers; i++ { - // 10 workers for synchronising book - b.SynchroniseWebsocketOrderbook() - } -} - -// ProcessUpdateOB processes the websocket orderbook update -func (b *Bittrex) ProcessUpdateOB(pair currency.Pair, message *OrderbookUpdateMessage) error { - updateBids := make([]orderbook.Item, len(message.BidDeltas)) - for x := range message.BidDeltas { - updateBids[x] = orderbook.Item{ - Price: message.BidDeltas[x].Rate, - Amount: message.BidDeltas[x].Quantity, - } - } - updateAsks := make([]orderbook.Item, len(message.AskDeltas)) - for x := range message.AskDeltas { - updateAsks[x] = orderbook.Item{ - Price: message.AskDeltas[x].Rate, - Amount: message.AskDeltas[x].Quantity, - } - } - - return b.Websocket.Orderbook.Update(&orderbook.Update{ - Asset: asset.Spot, - Pair: pair, - UpdateID: message.Sequence, - Bids: updateBids, - Asks: updateAsks, - }) -} - -// UpdateLocalOBBuffer updates and returns the most recent iteration of the orderbook -func (b *Bittrex) UpdateLocalOBBuffer(update *OrderbookUpdateMessage) (bool, error) { - enabledPairs, err := b.GetEnabledPairs(asset.Spot) - if err != nil { - return false, err - } - - format, err := b.GetPairFormat(asset.Spot, true) - if err != nil { - return false, err - } - - currencyPair, err := currency.NewPairFromFormattedPairs(update.MarketSymbol, - enabledPairs, - format) - if err != nil { - return false, err - } - - err = b.obm.stageWsUpdate(update, currencyPair, asset.Spot) - if err != nil { - init, err2 := b.obm.checkIsInitialSync(currencyPair) - if err2 != nil { - return false, err2 - } - return init, err - } - - err = b.applyBufferUpdate(currencyPair) - if err != nil { - log.Errorf(log.WebsocketMgr, "%s websocket UpdateLocalOBBuffer: Could not apply buffer update\n", b.Name) - } - - return false, err -} - -// SeedLocalOBCache seeds depth data -func (b *Bittrex) SeedLocalOBCache(ctx context.Context, p currency.Pair) error { - ob, sequence, err := b.GetOrderbook(ctx, p.String(), orderbookDepth) - if err != nil { - return err - } - return b.SeedLocalCacheWithOrderBook(p, sequence, ob, orderbookDepth) -} - -// SeedLocalCacheWithOrderBook seeds the local orderbook cache -func (b *Bittrex) SeedLocalCacheWithOrderBook(p currency.Pair, sequence int64, orderbookNew *OrderbookData, maxDepth int) error { - newOrderBook := orderbook.Base{ - Pair: p, - Asset: asset.Spot, - Exchange: b.Name, - LastUpdateID: sequence, - VerifyOrderbook: b.CanVerifyOrderbook, - Bids: make(orderbook.Items, len(orderbookNew.Bid)), - Asks: make(orderbook.Items, len(orderbookNew.Ask)), - MaxDepth: maxDepth, - } - - for i := range orderbookNew.Bid { - newOrderBook.Bids[i] = orderbook.Item{ - Amount: orderbookNew.Bid[i].Quantity, - Price: orderbookNew.Bid[i].Rate, - } - } - for i := range orderbookNew.Ask { - newOrderBook.Asks[i] = orderbook.Item{ - Amount: orderbookNew.Ask[i].Quantity, - Price: orderbookNew.Ask[i].Rate, - } - } - - return b.Websocket.Orderbook.LoadSnapshot(&newOrderBook) -} - -// 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 (b *Bittrex) applyBufferUpdate(pair currency.Pair) error { - fetching, needsFetching, err := b.obm.handleFetchingBook(pair) - if err != nil { - return err - } - if fetching { - return nil - } - if needsFetching { - if b.Verbose { - log.Debugf(log.WebsocketMgr, "%s Orderbook: Fetching via REST\n", b.Name) - } - return b.obm.fetchBookViaREST(pair) - } - recent, err := b.Websocket.Orderbook.GetOrderbook(pair, asset.Spot) - if err != nil { - log.Errorf( - log.WebsocketMgr, - "%s error fetching recent orderbook when applying updates: %s\n", - b.Name, - err) - } - - if recent != nil { - err = b.obm.checkAndProcessUpdate(b.ProcessUpdateOB, pair, recent) - if err != nil { - log.Errorf( - log.WebsocketMgr, - "%s error processing update - initiating new orderbook sync via REST: %s\n", - b.Name, - err) - err = b.obm.setNeedsFetchingBook(pair) - if err != nil { - return err - } - } - } - return nil -} - -// SynchroniseWebsocketOrderbook synchronises full orderbook for currency pair -// asset -func (b *Bittrex) SynchroniseWebsocketOrderbook() { - b.Websocket.Wg.Add(1) - go func() { - defer b.Websocket.Wg.Done() - for { - select { - case <-b.Websocket.ShutdownC: - for { - select { - case <-b.obm.jobs: - default: - return - } - } - case j := <-b.obm.jobs: - err := b.processJob(j.Pair) - if err != nil { - log.Errorf(log.WebsocketMgr, - "%s processing websocket orderbook error %v", - b.Name, err) - } - } - } - }() -} - -// processJob fetches and processes orderbook updates -func (b *Bittrex) processJob(p currency.Pair) error { - err := b.SeedLocalOBCache(context.TODO(), p) - if err != nil { - return fmt.Errorf("%s %s seeding local cache for orderbook error: %v", - p, asset.Spot, err) - } - - err = b.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. - return b.applyBufferUpdate(p) -} - -// stageWsUpdate stages websocket update to roll through updates that need to -// be applied to a fetched orderbook via REST. -func (o *orderbookManager) stageWsUpdate(u *OrderbookUpdateMessage, 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{ - // 100ms update assuming we might have up to a 10 second delay. - // There could be a potential 100 updates for the currency. - buffer: make(chan *OrderbookUpdateMessage, maxWSUpdateBuffer), - fetchingBook: false, - initialSync: true, - needsFetchingBook: true, - } - m2[a] = state - } - - 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) - } -} - -// 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 -} - -// 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 -} - -// 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 -} - -// 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 -} - -// 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) - } -} - -func (o *orderbookManager) checkAndProcessUpdate(processor func(currency.Pair, *OrderbookUpdateMessage) 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, d) - if err != nil { - return fmt.Errorf("%s %s processing update error: %w", - pair, asset.Spot, err) - } - recent.LastUpdateID = d.Sequence - } - default: - break buffer - } - } - return nil -} - -// validate checks for correct update alignment -func (u *update) validate(updt *OrderbookUpdateMessage, recent *orderbook.Base) (bool, error) { - if updt.Sequence <= recent.LastUpdateID { - // Drop any event where u is <= lastUpdateId in the snapshot. - return false, nil - } - - id := recent.LastUpdateID + 1 - if u.initialSync { - // The first processed event should have U <= lastUpdateId+1 AND - // u >= lastUpdateId+1. - if updt.Sequence > id { - return false, fmt.Errorf("initial websocket orderbook sync failure for pair %s and asset %s", - recent.Pair, - asset.Spot) - } - u.initialSync = false - } else if updt.Sequence != id { - // While listening to the stream, each new event's U should be - // equal to the previous event's u+1. - return false, fmt.Errorf("websocket orderbook synchronisation failure for pair %s and asset %s", - recent.Pair, - asset.Spot) - } - return true, nil -} diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index e954fefcd75..8be7279da2b 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -73,6 +73,7 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal(err) } + request.MaxRequestJobs = 100 ku.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() ku.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() @@ -2480,11 +2481,11 @@ func TestProcessMarketSnapshot(t *testing.T) { assert.Equal(t, 0.00000039450000000000, v.High, "high") assert.Equal(t, 0.0000003897, v.Last, "lastTradedPrice") assert.Equal(t, 0.00000034200000000000, v.Low, "low") - assert.Equal(t, currency.NewPairWithDelimiter("MTV", "BTC", "-"), v.Pair, "symbol") + assert.Equal(t, currency.NewPairWithDelimiter("ETH", "BTC", "-"), v.Pair, "symbol") assert.Equal(t, 316078.69700000000000000000, v.Volume, "volume") assert.Equal(t, 0.11768519138877000000, v.QuoteVolume, "volValue") // both margin and spot - case 3: + case 3, 4: assert.Equal(t, time.UnixMilli(1698740324437), v.LastUpdated, "datetime") assert.Equal(t, 0.00008486000000000000, v.High, "high") assert.Equal(t, 0.00008318, v.Last, "lastTradedPrice") @@ -2505,7 +2506,6 @@ func TestProcessMarketSnapshot(t *testing.T) { func TestSubscribeMarketSnapshot(t *testing.T) { t.Parallel() - setupWS() s := []stream.ChannelSubscription{ {Channel: marketTickerSnapshotForCurrencyChannel, Currency: currency.Pair{Base: currency.BTC}}, diff --git a/exchanges/kucoin/testdata/wsMarketSnapshot.json b/exchanges/kucoin/testdata/wsMarketSnapshot.json index 51261e57009..a4ab30e4b3f 100644 --- a/exchanges/kucoin/testdata/wsMarketSnapshot.json +++ b/exchanges/kucoin/testdata/wsMarketSnapshot.json @@ -1,3 +1,3 @@ {"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324504,"data":{"averagePrice":0.00001164,"baseCurrency":"XMR","board":0,"buy":0.00001252,"changePrice":0.00000104800000000000,"changeRate":0.0914,"close":0.000012508,"datetime":1698740324415,"high":0.00001402100000000000,"lastTradedPrice":0.000012508,"low":0.00001129200000000000,"makerCoefficient":2.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"BTC","marketChange1h":{"changeRate":0,"high":0,"low":0,"open":0,"vol":0,"volValue":0},"marketChange24h":{"changePrice":0.00000104800000000000,"changeRate":0.0914,"high":0.00001402100000000000,"low":0.00001129200000000000,"open":0.00001146000000000000,"vol":28474.47280000000000000000,"volValue":0.37038038297340000000},"marketChange4h":{"changePrice":0.00000009600000000000,"changeRate":0.0077,"high":0.00001308400000000000,"low":0.00001241200000000000,"open":0.00001241200000000000,"vol":7090.00000000000000000000,"volValue":0.08885800028840000000},"markets":["BTC"],"open":0.00001146000000000000,"quoteCurrency":"BTC","sell":0.000013191,"sort":100,"symbol":"XMR-BTC","symbolCode":"XMR-BTC","takerCoefficient":2.000000,"takerFeeRate":0.001,"trading":true,"vol":28474.47280000000000000000,"volValue":0.37038038297340000000}}} -{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324488,"data":{"averagePrice":0.00000037,"baseCurrency":"MTV","board":0,"buy":0.0000003641,"changePrice":0.00000004770000000000,"changeRate":0.1394,"close":0.0000003897,"datetime":1698740324483,"high":0.00000039450000000000,"lastTradedPrice":0.0000003897,"low":0.00000034200000000000,"makerCoefficient":2.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"BTC","marketChange1h":{"changeRate":0,"high":0,"low":0,"open":0,"vol":0,"volValue":0},"marketChange24h":{"changePrice":0.00000004770000000000,"changeRate":0.1394,"high":0.00000039450000000000,"low":0.00000034200000000000,"open":0.00000034200000000000,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000},"marketChange4h":{"changePrice":0.00000003290000000000,"changeRate":0.0922,"high":0.00000038970000000000,"low":0.00000035680000000000,"open":0.00000035680000000000,"vol":2309.46880000000000000000,"volValue":0.00089999999136000000},"markets":["BTC"],"open":0.00000034200000000000,"quoteCurrency":"BTC","sell":0.0000004022,"sort":100,"symbol":"MTV-BTC","symbolCode":"MTV-BTC","takerCoefficient":2.000000,"takerFeeRate":0.001,"trading":true,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000}}} +{"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324488,"data":{"averagePrice":0.00000037,"baseCurrency":"ETH","board":0,"buy":0.0000003641,"changePrice":0.00000004770000000000,"changeRate":0.1394,"close":0.0000003897,"datetime":1698740324483,"high":0.00000039450000000000,"lastTradedPrice":0.0000003897,"low":0.00000034200000000000,"makerCoefficient":2.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"BTC","marketChange1h":{"changeRate":0,"high":0,"low":0,"open":0,"vol":0,"volValue":0},"marketChange24h":{"changePrice":0.00000004770000000000,"changeRate":0.1394,"high":0.00000039450000000000,"low":0.00000034200000000000,"open":0.00000034200000000000,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000},"marketChange4h":{"changePrice":0.00000003290000000000,"changeRate":0.0922,"high":0.00000038970000000000,"low":0.00000035680000000000,"open":0.00000035680000000000,"vol":2309.46880000000000000000,"volValue":0.00089999999136000000},"markets":["BTC"],"open":0.00000034200000000000,"quoteCurrency":"BTC","sell":0.0000004022,"sort":100,"symbol":"ETH-BTC","symbolCode":"ETH-BTC","takerCoefficient":2.000000,"takerFeeRate":0.001,"trading":true,"vol":316078.69700000000000000000,"volValue":0.11768519138877000000}}} {"type":"message","topic":"/market/snapshot:BTC","subject":"trade.snapshot","data":{"sequence":1698740324508,"data":{"averagePrice":0.00007307,"baseCurrency":"BTC","board":0,"buy":0.00008388,"changePrice":0.00001166000000000000,"changeRate":0.1630,"close":0.00008318,"datetime":1698740324437,"high":0.00008486000000000000,"lastTradedPrice":0.00008318,"low":0.00007152000000000000,"makerCoefficient":1.000000,"makerFeeRate":0.001,"marginTrade":false,"mark":0,"market":"USDT","marketChange1h":{"changePrice":-0.00000116000000000000,"changeRate":-0.0137,"high":0.00008434000000000000,"low":0.00008318000000000000,"open":0.00008434000000000000,"vol":189.33430000000000000000,"volValue":0.01578748292300000000},"marketChange24h":{"changePrice":0.00001166000000000000,"changeRate":0.1630,"high":0.00008486000000000000,"low":0.00007152000000000000,"open":0.00007152000000000000,"vol":17062.45450000000000000000,"volValue":1.33076678861000000000},"marketChange4h":{"changePrice":0.00000143000000000000,"changeRate":0.0174,"high":0.00008486000000000000,"low":0.00008175000000000000,"open":0.00008175000000000000,"vol":1752.55690000000000000000,"volValue":0.14543003812900000000},"markets":["BTC"],"open":0.00007152000000000000,"quoteCurrency":"USDT","sell":0.00008421,"sort":100,"symbol":"BTC-USDT","symbolCode":"BTC-USDT","takerCoefficient":1.000000,"takerFeeRate":0.001,"trading":true,"vol":17062.45450000000000000000,"volValue":1.33076678861000000000}}} diff --git a/exchanges/orderbook/orderbook_types.go b/exchanges/orderbook/orderbook_types.go index 229cdcd8b18..07b2a467036 100644 --- a/exchanges/orderbook/orderbook_types.go +++ b/exchanges/orderbook/orderbook_types.go @@ -106,8 +106,8 @@ type Base struct { // Checks if the orderbook needs ID alignment as well as price alignment IDAlignment bool // Determines if there is a max depth of orderbooks and after an append we - // should remove any items that are outside of this scope. Bittrex and - // Kraken utilise this field. + // should remove any items that are outside of this scope. Kraken utilises + // this field. MaxDepth int // ChecksumStringRequired defines if the checksum is built from the raw // string representations of the price and amount. This helps alleviate any diff --git a/exchanges/support.go b/exchanges/support.go index 65875fb54ff..e198fc4f5f3 100644 --- a/exchanges/support.go +++ b/exchanges/support.go @@ -21,7 +21,6 @@ var Exchanges = []string{ "bitflyer", "bitmex", "bitstamp", - "bittrex", "btc markets", "btse", "bybit", diff --git a/exchanges/trade/README.md b/exchanges/trade/README.md index ae3f2c40d0a..ef191f3eb06 100644 --- a/exchanges/trade/README.md +++ b/exchanges/trade/README.md @@ -67,7 +67,6 @@ _b in this context is an `IBotExchange` implemented struct_ | Bithumb | Yes | Yes | No | | BitMEX | Yes | Yes | Yes | | Bitstamp | Yes | Yes | No | -| Bittrex | Yes | Yes | No | | BTCMarkets | Yes | Yes | No | | BTSE | Yes | Yes | No | | Bybit | Yes | Yes | Yes | diff --git a/testdata/configtest.json b/testdata/configtest.json index bfaacb5707f..2348827f1a3 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -1021,84 +1021,6 @@ } ] }, - { - "name": "Bittrex", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": true, - "delimiter": "-" - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC-USDT", - "available": "1ECO-BTC,1ECO-USDT,1INCH-BTC,1INCH-ETH,1INCH-USD,1INCH-USDT,1PECO-BTC,1PECO-USDT,4ART-BTC,4ART-USDT,AAVE-BTC,AAVE-ETH,AAVE-EUR,AAVE-USD,AAVE-USDT,ABBC-BTC,ABYSS-BTC,ACH-USD,ACXT-USDT,ADA-BTC,ADA-ETH,ADA-EUR,ADA-USD,ADA-USDC,ADA-USDT,ADX-BTC,ADX-ETH,AGRS-BTC,AGRS-USDT,AGV-USDT,AKN-USDT,AKRO-BTC,AKT-BTC,AKTIO-USDT,AKT-USDT,ALGO-BTC,ALGO-USD,ALGO-USDT,ALTA-USDT,AMP-BTC,AMP-ETH,AMP-USD,AMP-USDT,ANKR-BTC,ANKR-ETH,ANKR-USD,ANT-BTC,ANT-ETH,ANTE-USDT,ANT-USD,APE-USD,APE-USDT,API3-USDT,APM-BTC,APM-USDT,APXP-USDT,AR-BTC,ARDR-BTC,ARDX-BTC,ARDX-ETH,ARDX-USDT,ARIA20-BTC,ARIA20-USD,ARK-BTC,ARTIC-USDT,ARTII-BTC,AR-USD,ARV-USDT,ARW-USDT,ASM-USDT,ATOM-BTC,ATOM-ETH,ATOM-USD,ATOM-USDT,ATRI-USDT,ATTR-USDT,AUDT-USDT,AVAX-BTC,AVAX-ETH,AVAX-USD,AVAX-USDT,AVT-ETH,AVT-USDT,AXS-USD,AXS-USDT,B2M-USDT,BAAS-BTC,BAAS-USDT,BADGER-USD,BAL-BTC,BAL-ETH,BAL-EUR,BAL-USD,BAL-USDT,BAND-BTC,BAND-ETH,BAND-EUR,BAND-USD,BAND-USDT,BAT-BTC,BAT-ETH,BAT-USD,BAT-USDT,BAX-ETH,BAX-USDT,BBC-BTC,BBC-USDT,BBF-USDT,BCH-BTC,BCH-ETH,BCH-EUR,BCH-USD,BCH-USDT,BEE-USDT,BERRY-USDT,BEST-USDT,BFC-BTC,BIFI-BTC,BIOT-USDT,BIST-USDT,BITCI-USDT,BLK-BTC,BLOCK-BTC,BLOCK-USDT,BMP-BTC,BMP-USDT,BNA-USDT,BNS-BTC,BNS-USDT,BNT-BTC,BNT-ETH,BNT-USD,BOA-BTC,BOA-USDT,BOND-ETH,BONDLY-BTC,BONDLY-ETH,BONDLY-USDT,BOND-USDT,BOSON-BTC,BOSON-USDT,BRZ-BTC,BRZ-USDT,BST-BTC,BST-USDT,BSV-BTC,BSV-ETH,BSV-EUR,BSV-USD,BSV-USDT,BTBS-USDT,BTC-EUR,BTC-USD,BTC-USDC,BTC-USDT,BTCV-BTC,BTCV-USDT,BTD-USDT,BTM-BTC,BTR-BTC,BTRST-USD,BTS-BTC,BTTOLD-BTC,BTTOLD-USDT,BTU-BTC,CADX-BTC,CAMP-BTC,CAMP-USDT,CAST-USDT,CBANK-USDT,CBC-BTC,CBC-USDT,CDEX-USDT,CEDS-USDT,CEL-BTC,CEL-ETH,CELO-BTC,CELO-ETH,CELO-EUR,CELO-USD,CELO-USDT,CEL-USD,CEL-USDT,CGT-BTC,CGT-USDT,CHNG-USDT,CIND-USDT,CIV-USDT,CKB-BTC,CKB-USD,CKB-USDT,CLI-USDT,CLT-BTC,CLT-USDT,CMCX-USDT,CND-BTC,CNTM-BTC,CNTM-USDT,COMP-BTC,COMP-ETH,COMP-USD,COMP-USDT,COT-BTC,COT-ETH,COT-USDT,COVN-BTC,COVN-USDT,CPC-BTC,CRFI-USDT,CRO-BTC,CRO-ETH,CRO-EUR,CRO-USD,CRO-USDT,CRTS-USDT,CRV-BTC,CRV-ETH,CRV-USDT,CRW-BTC,CRW-USDT,CSC-USDT,CTC-BTC,CTPL-USDT,CTXC-BTC,CUDOS-BTC,CUDOS-USDT,CURE-BTC,CURIO-USDT,CUSD-BTC,CUSD-ETH,CUSD-USDT,CVC-BTC,CVC-ETH,CWC-BTC,CWD-USDT,CWEB-USDT,CYCLUB-USDT,DAF-ETH,DAF-USDT,DAI-BTC,DAI-ETH,DAI-USD,DAI-USDT,DASH-BTC,DASH-ETH,DASH-USD,DASH-USDT,DAWN-BTC,DCR-BTC,DCR-USD,DCR-USDT,DECE-USDT,DEP-BTC,DEP-USDT,DEXA-USDT,DFCH-USDT,DFI-BTC,DFI-ETH,DFI-EUR,DFI-USDT,DGB-BTC,DGB-ETH,DGB-EUR,DGB-USD,DGB-USDT,DMR-ETH,DMR-USDT,DMT-BTC,DMT-ETH,DMTR-USDT,DNT-BTC,DOGE-BTC,DOGE-ETH,DOGE-EUR,DOGE-USD,DOGE-USDC,DOGE-USDT,DOT-BTC,DOT-ETH,DOT-EUR,DOT-USD,DOT-USDT,DRC-ETH,DRCM-USDT,DRE-USDT,DRGN-BTC,DST-USDT,DTX-ETH,DTX-USDT,DUSK-BTC,DUSK-USDT,DVI-BTC,DVI-USDT,DVK-USDT,EAC-USDT,ECELL-BTC,ECELL-USDT,ECOC-BTC,ECOC-USDT,EDR-USDT,ELA-BTC,ELAMA-BTC,ELA-USDT,ELT-USDT,EMC2-BTC,EMC2-ETH,ENG-BTC,ENG-ETH,ENJ-BTC,ENJ-ETH,ENJ-USD,ENJ-USDT,ENS-USDT,EOS-BTC,EOS-ETH,EOS-USD,EOS-USDT,EPTT-USDT,EQX-BTC,EQX-ETH,EQX-USDT,ETC-BTC,ETC-ETH,ETC-USD,ETC-USDT,ETHA-USDT,ETH-BTC,ETH-EUR,ETH-USD,ETH-USDC,ETH-USDT,ETHW-USD,ETHW-USDT,ETM-USDT,EWC-USDT,EXCL-BTC,EXE-BTC,EXE-USDT,EXP-BTC,EXVA-USDT,FCT2-BTC,FDM-USDT,FEVR-USDT,FIL-BTC,FIL-ETH,FIL-USD,FIL-USDT,FIRO-USDT,FIT-BTC,FLIXX-USDT,FLO-BTC,FLUX-USDT,FNX-BTC,FNX-USDT,FOL-BTC,FOL-USDT,FTC-BTC,FTC-USDT,FTM-USD,FTM-USDT,FUSE-USDT,FX-BTC,FX-ETH,FXS-USDT,FX-USDT,GALA-USD,GALA-USDT,GAME-BTC,GAME-USDT,GAZE-ETH,GAZE-USDT,GBIT-USDT,GBPT-BTC,GBPT-ETH,GBPT-USDC,GBYTE-BTC,GDT-USDT,GEO-BTC,GET-BTC,GET-USDT,GIGX-USDT,GLEEC-BTC,GLEEC-USDT,GLM-BTC,GLM-ETH,GMT-USDT,GNC-BTC,GNC-USDT,GNO-BTC,GNO-ETH,GNY-BTC,GO-BTC,GOLD-USDT,GPX-USDT,GPYX-BTC,GPYX-USDT,GRNC-USDT,GRS-BTC,GRT-BTC,GRT-ETH,GRT-EUR,GRT-USD,GRT-USDT,GST-BTC,GST-USDT,GTC-USD,GXC-BTC,GXC-USDT,HBAR-BTC,HBAR-ETH,HBAR-USD,HBAR-USDT,HBD-BTC,HDAO-BTC,HDAO-USDT,HEDG-BTC,HIVE-BTC,HIVE-USD,HIVE-USDT,HLM-USDT,HNS-BTC,HNS-ETH,HNS-USDT,HOTCROSS-USDT,HRTS-USD,HRTS-USDT,HTML-USDT,HXRO-BTC,HXRO-USDT,HYDRO-BTC,HYVE-USDT,ICA-USDT,ICX-BTC,IGNIS-BTC,IGNIS-USDT,INSTAR-BTC,INV-ETH,INX-BTC,INXT-BTC,INXT-USDT,INX-USDT,IOC-BTC,IOST-BTC,IOTA-BTC,IOTA-USD,IOTA-USDT,IOTX-BTC,IOTX-USD,IOTX-USDT,IQO-USDT,IQQ-BTC,IQQ-USDT,IQ-USDT,IRIS-BTC,IRIS-USDT,JAM-USDT,JASMY-ETH,JASMY-USDT,JGN-USDT,JOB-BTC,JOB-USDT,KAI-BTC,KAI-USDT,KBH-USDT,KDA-BTC,KDA-USD,KDA-USDT,KIN-USDT,KLAY-BTC,KLAY-USDT,KLEVA-USDT,KLV-BTC,KLV-USDT,KMD-BTC,KMD-USD,KMD-USDT,KNC-BTC,KNC-ETH,KNC-EUR,KNC-USD,KNC-USDT,KOK-BTC,KOK-USDT,KRRX-USDT,KSM-BTC,KSM-ETH,KSM-USD,KSM-USDT,KUSD-USDC,KUSD-USDT,LAND-USDT,LBC-BTC,LBC-ETH,LBC-USD,LBC-USDT,LBL-USDT,LGCY-USDT,LINK-BTC,LINK-ETH,LINK-USD,LINK-USDT,LOOM-BTC,LOON-BTC,LOON-USDT,LPNT-USDT,LPT-USDT,LRC-BTC,LRC-USD,LSK-BTC,LSK-USDT,LTC-BTC,LTC-ETH,LTC-USD,LTC-USDT,LUCY-BTC,LUCY-USDT,LWC-ETH,LWC-USDT,MAID-BTC,MANA-BTC,MANA-ETH,MANA-USD,MARS4-ETH,MARS4-USDT,MATIC-BTC,MATIC-ETH,MATIC-USD,MATIC-USDT,MCCX-USDT,MCH-USDT,MDC-BTC,MDT-BTC,MDT-USDT,ME-BTC,MED-BTC,MED-USDT,MEME-BTC,MER-BTC,METADIUM-BTC,MF1-USDT,MFT-BTC,MFT-USDT,MF-USDT,MIMO-BTC,MIM-USDT,MINE-USDT,MINT-ETH,MINT-USDT,MKR-BTC,MKR-ETH,MKR-USDT,MMAON-USDT,MNFT-ETH,MNFT-USDT,MNW-BTC,MNW-ETH,MNW-USDT,MODEX-ETH,MODEX-USDT,MOGX-USDT,MONA-BTC,MONA-USDT,MORE-BTC,MOR-USDT,MOV-USDT,MPC-USDT,MPT-USDT,MQL-USDT,MSB-USDT,MTC-BTC,MTL-BTC,MTRA-USDT,MTSP-USDT,MTS-USDT,MUE-BTC,MUNT-BTC,MVC-USDT,MYCE-BTC,MYCE-USDT,MYID-USDT,MYST-BTC,MYST-USDT,NAV-BTC,NDAU-USDT,NEO-BTC,NEO-ETH,NEO-USDT,NFTX-BTC,NFTX-ETH,NFTX-USDT,NGC-BTC,NIFT-USDT,NKCLC-USDT,NKN-BTC,NKN-USDT,NMR-BTC,NMR-ETH,NMR-USDT,NPT-USDT,NVT-BTC,NVT-USDT,NXS-BTC,NXT-BTC,O3-ETH,O3-USDT,OCEAN-BTC,OCEAN-USDT,OGN-BTC,OGN-ETH,OGT-BTC,OGT-USDT,OK-BTC,OMG-BTC,OMG-ETH,OMG-USD,OMG-USDT,ONG-BTC,ONSTON-USDT,ONT-BTC,ONT-USDT,ORBS-BTC,OXEN-BTC,OXEN-USDT,OXT-BTC,OXT-USDT,PANDO-USDT,PAR-BTC,PART-BTC,PAR-USDT,PAXG-USD,PAXG-USDT,PAY-BTC,PAY-ETH,PGX-USDT,PHNX-BTC,PHNX-USDT,PINK-USDT,PIST-USDT,PIVX-BTC,PKR-USDT,PKT-BTC,PKT-USDT,PLA-BTC,PLA-EUR,PLA-USD,PLA-USDT,PLAY-BTC,PMA-BTC,PMA-USDT,POLC-ETH,POLC-USDT,POLL-USDT,POPK-USDT,POT-BTC,POWR-BTC,POWR-ETH,PPAY-BTC,PPAY-USD,PPAY-USDT,PPC-BTC,PROM-BTC,PROM-USDT,PROS-USDT,PRO-USD,PRO-USDC,PRO-USDT,PRT-USDT,PTOY-BTC,PTOY-USDT,PUNDIX-BTC,PUNDIX-ETH,PUNDIX-USDT,PXP-USDT,PYR-BTC,PYR-USDT,QLC-BTC,QLC-USDT,QNT-BTC,QNT-ETH,QNT-USD,QRL-BTC,QTUM-BTC,QTUM-ETH,QTUM-USDT,R1-USDT,RAD-USD,RAMP-BTC,RAMP-ETH,RAMP-USDT,RDD-USDT,REAL-BTC,REAL-USDT,REN-BTC,RENBTC-BTC,RENBTC-ETH,RENBTC-EUR,RENBTC-USD,RENBTC-USDT,REN-ETH,REN-EUR,REN-USD,REN-USDT,REPV2-BTC,REPV2-ETH,REV-BTC,REV-USDT,REVV-BTC,REVV-USDT,RFOX-BTC,RFOX-USDT,RGT-BTC,RGT-ETH,RGT-USDT,RLC-BTC,RLC-USD,RLY-USD,RNB-BTC,RNDR-USDT,ROC-USDT,ROOK-BTC,ROOK-ETH,ROOK-USDT,RSR-BTC,RSR-USDT,RSV-USD,RSV-USDT,RTH-USDT,RVC-BTC,RVC-USDT,RVN-BTC,RVN-ETH,RVN-USD,RVN-USDT,SAND-BTC,SAND-USD,SAND-USDT,SATT-USDT,SBD-BTC,SBT-USDT,SC-BTC,SC-ETH,SC-USD,SC-USDT,SENSO-BTC,SENSO-ETH,SG-USDT,SHIB-USD,SHR-BTC,SHR-USDT,SHX-BTC,SHX-USDT,SIG-BTC,SIG-ETH,SIGNA-BTC,SIGNA-USDT,SIG-USDT,SIRS-BTC,SIRS-USDT,SKL-USD,SKL-USDT,SLICE-BTC,SLICE-USDT,SLS-BTC,SMARTCREDIT-USDT,SMBSWAP-BTC,SMBSWAP-ETH,SMBSWAP-USDT,SMG-USDT,SML-USDT,SNT-BTC,SNT-ETH,SNX-BTC,SNX-ETH,SNX-USD,SNX-USDT,SOL-BTC,SOL-EUR,SOL-USD,SOL-USDT,SOLVE-BTC,SOLVE-ETH,SOLVE-USD,SOLVE-USDT,SPC-BTC,SPC-USDT,SPHR-BTC,SPI-BTC,SPI-USDT,SPWN-ETH,SPWN-USDT,SRM-USDT,SRN-BTC,SRN-ETH,SSX-BTC,SSX-USDT,STCCOIN-BTC,STCCOIN-USDT,STEEM-BTC,STEEM-USDT,STMX-BTC,STMX-ETH,STMX-EUR,STMX-USD,STORJ-BTC,STORJ-USD,STPT-BTC,STRAX-BTC,STRAX-ETH,STRK-BTC,STRK-USD,STRK-USDT,SUKU-BTC,SUKU-USDT,SUSHI-BTC,SUSHI-ETH,SUSHI-EUR,SUSHI-USD,SUSHI-USDT,SXP-BTC,SYLO-USDT,SYS-BTC,SYS-USDT,TCR-USDT,TEA-BTC,TEA-USDT,TEDDY-USDT,TFBX-USDT,TFC-BTC,TFC-USDT,THC-BTC,TLM-USDT,TNC-BTC,TOKKI-USDT,TOM-USDT,TON-USD,TRAC-BTC,TRAC-ETH,TRAC-USDT,TRIX-USDT,TRX-BTC,TRX-ETH,TRX-EUR,TRX-USD,TRX-USDT,TRZ-USDT,TUSD-BTC,TUSD-ETH,TUSD-USD,TUSD-USDT,TXA-USDT,TYB-USDT,TYC-BTC,TYC-USDT,TZBTC-BTC,TZBTC-USDT,UBQ-BTC,UBQ-USDT,UBT-BTC,UBT-ETH,UBT-EUR,UMA-BTC,UMA-ETH,UMA-EUR,UMA-USD,UMA-USDT,UNI-BTC,UNI-ETH,UNI-EUR,UNI-USD,UNI-USDT,UNIX-BTC,UNIX-ETH,UNIX-USDT,UPCO2-BTC,UPCO2-USDT,UPEUR-BTC,UPP-BTC,UPT-BTC,UPUSD-BTC,UPUSD-USDT,UPXAU-BTC,UPXAU-USDT,UQC-BTC,UQC-USDT,URQA-BTC,URQA-USDT,USDC-BTC,USDC-ETH,USDC-EUR,USDC-USD,USDC-USDT,USD-EUR,USDN-BTC,USDN-USDT,USDP-BTC,USDP-USD,USDS-BTC,USDS-USD,USDT-EUR,USDT-USD,USDT-USDC,UTK-BTC,UZRS-USDT,VAL-BTC,VAL-USDT,VANY-BTC,VANY-USDT,VBK-BTC,VCK-USDT,VEE-BTC,VEMP-USDT,VET-BTC,VET-ETH,VET-USD,VET-USDT,VIA-BTC,VID-BTC,VIL-BTC,VITE-BTC,VLX-BTC,VLX-USDT,VOLTINU-USDT,VRA-BTC,VRA-ETH,VRA-USDT,VRC-BTC,VSP-ETH,VSP-USDT,VTC-BTC,VVT-USDT,WACME-ETH,WACME-USDT,WAVES-BTC,WAVES-ETH,WAVES-USDT,WAXE-USDT,WAXP-BTC,WAXP-ETH,WAXP-USD,WAXP-USDT,WBTC-BTC,WBTC-ETH,WBTC-USDT,WEC-USDT,WEMIX-USDT,WICC-BTC,WICC-USDT,WINGS-BTC,WSB-USDT,WSTRM-USDT,WXBTC-BTC,WXBTC-USDT,XAI-USDT,XBN-USDT,XCAD-USDT,XCF-BTC,XCF-USDT,XCN-BTC,XCN-ETH,XCN-EUR,XCN-USD,XCN-USDT,XDC-BTC,XDC-EUR,XDC-USDT,XDN-USDT,XELS-BTC,XELS-USDT,XEM-BTC,XEM-ETH,XEM-USD,XEM-USDT,XEP-USDT,XGOLD-USDT,XLM-BTC,XLM-ETH,XLM-EUR,XLM-USD,XLM-USDT,XMY-USDT,XRP-BTC,XRP-ETH,XRP-EUR,XRP-USD,XRP-USDC,XRP-USDT,XSILV-USDT,XST-BTC,XTP-BTC,XTZ-BTC,XTZ-ETH,XTZ-USD,XTZ-USDT,XVG-BTC,XVG-ETH,XVG-USDT,XWC-BTC,XWC-USDT,XYM-BTC,XYM-ETH,XYM-USDT,XYO-USD,XYO-USDT,YEFI-USDT,YFI-USD,YFL-ETH,YFL-USDT,YLD-BTC,YLD-USDT,YOU-USDT,ZEC-BTC,ZEC-ETH,ZEC-USD,ZEC-USDT,ZEN-BTC,ZEN-USD,ZEN-USDT,ZIL-BTC,ZILD-USDT,ZIL-USD,ZKP-ETH,ZKP-USDT,ZK-USDT,ZPTC-USDC,ZRX-BTC,ZRX-ETH,ZRX-USD,ZRX-USDT,ZUSD-USDT" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} - }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": true - } - }, - "bankAccounts": [ - { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, { "name": "BTSE", "enabled": true, @@ -2009,7 +1931,7 @@ }, "margin": { "assetEnabled": true, - "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MTV-BTC", + "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MTV-BTC,ETH-BTC", "available": "BTC-USDT,MHC-ETH,MHC-BTC,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MHC-USDT,XMR-BTC,XMR-ETH,RIF-BTC,MTV-BTC,MTV-ETH,CRO-BTC,MTV-USDT,KMD-BTC,KMD-USDT,RFOX-USDT,TEL-USDT,TT-USDT,AERGO-USDT,XMR-USDT,TRX-KCS,ATOM-BTC,ATOM-ETH,ATOM-USDT,ATOM-KCS,ETN-USDT,FTM-USDT,TOMO-USDT,VSYS-USDT,OCEAN-BTC,OCEAN-ETH,CHR-BTC,CHR-USDT,FX-BTC,FX-ETH,NIM-BTC,NIM-ETH,COTI-BTC,COTI-USDT,NRG-ETH,BNB-BTC,BNB-USDT,JAR-BTC,JAR-USDT,ALGO-BTC,ALGO-ETH,ALGO-USDT,XEM-BTC,XEM-USDT,CIX100-USDT,XTZ-BTC,XTZ-USDT,ZEC-BTC,ZEC-USDT,ADA-BTC,ADA-USDT,REV-USDT,WXT-BTC,WXT-USDT,FORESTPLUS-BTC,FORESTPLUS-USDT,BOLT-BTC,BOLT-USDT,ARPA-USDT,CHZ-BTC,CHZ-USDT,DAPPT-BTC,DAPPT-USDT,NOIA-BTC,NOIA-USDT,WIN-BTC,WIN-USDT,DERO-BTC,DERO-USDT,BTT-USDT,EOSC-USDT,ENQ-BTC,ENQ-USDT,ONE-BTC,ONE-USDT,TOKO-BTC,TOKO-USDT,VID-BTC,VID-USDT,LUNA-USDT,SXP-BTC,SXP-USDT,AKRO-BTC,AKRO-USDT,ROOBEE-BTC,WIN-TRX,MAP-BTC,MAP-USDT,AMPL-BTC,AMPL-USDT,DAG-USDT,POL-USDT,ARX-USDT,NWC-BTC,NWC-USDT,BEPRO-BTC,BEPRO-USDT,VRA-BTC,VRA-USDT,KSM-BTC,KSM-USDT,DASH-USDT,SUTER-USDT,ACOIN-USDT,SUTER-BTC,SENSO-USDT,PRE-BTC,XDB-USDT,SYLO-USDT,WOM-USDT,SENSO-BTC,DGB-USDT,LYXE-USDT,LYXE-ETH,XDB-BTC,STX-BTC,STX-USDT,XSR-USDT,COMP-USDT,CRO-USDT,KAI-USDT,KAI-BTC,WEST-BTC,WEST-USDT,EWT-BTC,WAVES-USDT,WAVES-BTC,ORN-USDT,AMPL-ETH,BNS-USDT,MKR-USDT,SUKU-BTC,MLK-BTC,MLK-USDT,JST-USDT,KAI-ETH,SUKU-USDT,DIA-USDT,DIA-BTC,LINK-BTC,LINK-USDT,DOT-USDT,DOT-BTC,SHA-BTC,SHA-USDT,EWT-USDT,USDJ-USDT,EFX-BTC,CKB-BTC,CKB-USDT,UMA-USDT,ALEPH-USDT,VELO-USDT,SUN-USDT,BUY-USDT,YFI-USDT,OXEN-USDT,UNI-USDT,UOS-USDT,UOS-BTC,NIM-USDT,DEGO-USDT,DEGO-ETH,UDOO-ETH,RFUEL-USDT,FIL-USDT,UBX-ETH,REAP-USDT,AAVE-USDT,AAVE-BTC,TONE-BTC,TONE-ETH,ELF-ETH,AERGO-BTC,IOST-ETH,KCS-USDT,SNX-ETH,TOMO-ETH,KCS-ETH,DRGN-BTC,WAN-ETH,NULS-ETH,AXPR-ETH,POWR-BTC,QTUM-BTC,MANA-BTC,TEL-BTC,XYO-ETH,AXPR-BTC,ETN-BTC,COV-ETH,VET-BTC,KCS-BTC,CAPP-ETH,ONT-BTC,DRGN-ETH,DAG-ETH,TOMO-BTC,WAN-BTC,KNC-ETH,CRPT-ETH,LTC-USDT,BAX-ETH,BSV-USDT,DENT-ETH,AION-ETH,LYM-ETH,TRAC-ETH,ENJ-BTC,WAXP-BTC,DGB-BTC,ELA-BTC,ZIL-BTC,BSV-BTC,XLM-USDT,IOTX-ETH,SOUL-BTC,DOCK-BTC,AMB-ETH,TRX-BTC,XRP-TUSD,NULS-BTC,ETH-DAI,LSK-BTC,GMB-ETH,GMB-BTC,NEO-ETH,OMG-ETH,BTC-TUSD,KAT-USDT,KNC-BTC,ELF-BTC,MANA-ETH,ETC-USDT,ONT-ETH,MKR-BTC,KAT-BTC,XRP-USDC,XYO-BTC,SNT-ETH,ZRX-BTC,LOOM-ETH,AION-BTC,POWR-ETH,OLT-ETH,OLT-BTC,SNT-BTC,TRAC-BTC,XLM-ETH,ETH-USDT,BSV-ETH,TRX-ETH,ETN-ETH,AOA-USDT,BCD-BTC,DENT-BTC,DOCK-ETH,KEY-BTC,EOS-KCS,XLM-BTC,ADB-ETH,TIME-ETH,CVC-BTC,LSK-ETH,QKC-BTC,AMB-BTC,USDT-TUSD,ETC-ETH,XRP-BTC,NEO-KCS,SNX-USDT,CRPT-BTC,IOTX-BTC,LTC-ETH,XRP-KCS,ADB-BTC,LTC-KCS,TEL-ETH,DCR-ETH,LYM-USDT,USDT-USDC,ETH-USDC,DAG-BTC,AVA-BTC,BTC-USDT,WAXP-ETH,XRP-USDT,KEY-ETH,VET-ETH,FTM-BTC,USDT-DAI,QKC-ETH,ETH-BTC,MAN-BTC,CPC-ETH,TRX-USDT,BTC-DAI,ONT-USDT,DASH-ETH,BAX-BTC,AVA-ETH,LOOM-BTC,MVP-BTC,MKR-ETH,COV-BTC,CPC-BTC,REQ-ETH,EOS-BTC,LTC-BTC,XRP-ETH,CAPP-BTC,FTM-ETH,BCD-ETH,ZRX-ETH,DGB-ETH,VET-USDT,REQ-BTC,UTK-BTC,PLAY-BTC,UTK-ETH,SNX-BTC,MVP-ETH,NEO-BTC,SOUL-ETH,NEO-USDT,ELA-ETH,OMG-BTC,TIME-BTC,AOA-BTC,ETC-BTC,DCR-BTC,BTC-USDC,ENJ-ETH,IOST-BTC,DASH-BTC,EOS-USDT,EOS-ETH,ZIL-ETH,ETH-TUSD,GAS-BTC,LYM-BTC,BCH-BTC,VSYS-BTC,BCH-USDT,MKR-DAI,SOLVE-BTC,GRIN-BTC,GRIN-USDT,UQC-BTC,UQC-ETH,OPCT-BTC,OPCT-ETH,PRE-USDT,SHR-BTC,SHR-USDT,UBXT-USDT,ROSE-USDT,USDC-USDT,CTI-USDT,CTI-ETH,ETH2-ETH,BUX-BTC,XHV-USDT,PLU-USDT,GRT-USDT,CAS-BTC,CAS-USDT,MSWAP-BTC,MSWAP-USDT,GOM2-BTC,GOM2-USDT,REVV-BTC,REVV-USDT,LON-USDT,1INCH-USDT,LOC-USDT,API3-USDT,UNFI-USDT,HTR-USDT,FRONT-USDT,FRONT-BTC,WBTC-BTC,WBTC-ETH,MIR-USDT,LTC-USDC,BCH-USDC,HYDRA-USDT,DFI-USDT,DFI-BTC,CRV-USDT,SUSHI-USDT,FRM-USDT,EOS-USDC,BSV-USDC,ZEN-USDT,CUDOS-USDT,ADA-USDC,REN-USDT,LRC-USDT,LINK-USDC,KLV-USDT,KLV-BTC,BOA-USDT,THETA-USDT,QNT-USDT,BAT-USDT,DOGE-USDT,DOGE-USDC,DAO-USDT,STRONG-USDT,TRIAS-USDT,TRIAS-BTC,DOGE-BTC,MITX-BTC,MITX-USDT,CAKE-USDT,ORAI-USDT,ZEE-USDT,LTX-USDT,LTX-BTC,MASK-USDT,KLV-TRX,IDEA-USDT,PHA-USDT,PHA-ETH,BCH-KCS,SRK-USDT,SRK-BTC,ADA-KCS,HTR-BTC,BSV-KCS,DOT-KCS,LINK-KCS,MIR-KCS,BNB-KCS,XLM-KCS,VET-KCS,SWINGBY-USDT,SWINGBY-BTC,XHV-BTC,DASH-KCS,UNI-KCS,AAVE-KCS,DOGE-KCS,ZEC-KCS,XTZ-KCS,GRT-KCS,ALGO-KCS,EWT-KCS,GAS-USDT,AVAX-USDT,AVAX-BTC,KRL-BTC,KRL-USDT,POLK-USDT,POLK-BTC,ENJ-USDT,MANA-USDT,RNDR-USDT,RNDR-BTC,RLY-USDT,ANC-USDT,SKEY-USDT,LAYER-USDT,TARA-USDT,TARA-ETH,IOST-USDT,DYP-USDT,DYP-ETH,XYM-USDT,XYM-BTC,PCX-USDT,PCX-BTC,ORBS-USDT,ORBS-BTC,BTC3L-USDT,BTC3S-USDT,ETH3L-USDT,ETH3S-USDT,ANKR-USDT,DSLA-USDT,DSLA-BTC,SAND-USDT,VAI-USDT,XCUR-USDT,XCUR-BTC,FLUX-USDT,OMG-USDT,ZIL-USDT,DODO-USDT,MAN-USDT,BAX-USDT,BOSON-USDT,BOSON-ETH,PUNDIX-USDT,PUNDIX-BTC,WAXP-USDT,HT-USDT,PDEX-USDT,LABS-USDT,LABS-ETH,GMB-USDT,PHNX-USDT,PHNX-BTC,HAI-USDT,EQZ-USDT,FORTH-USDT,HORD-USDT,CGG-USDT,UBX-USDT,GHX-USDT,TCP-USDT,STND-USDT,STND-ETH,TOWER-USDT,TOWER-BTC,ACE-USDT,LOCG-USDT,CARD-USDT,FLY-USDT,CWS-USDT,XDC-USDT,XDC-ETH,STRK-BTC,STRK-ETH,SHIB-USDT,POLX-USDT,KDA-USDT,KDA-BTC,ICP-USDT,ICP-BTC,STC-USDT,STC-BTC,GOVI-USDT,GOVI-BTC,FKX-USDT,CELO-USDT,CELO-BTC,CUSD-USDT,CUSD-BTC,FCL-USDT,MATIC-USDT,MATIC-BTC,ELA-USDT,CRPT-USDT,OPCT-USDT,OGN-USDT,OGN-BTC,OUSD-USDT,OUSD-BTC,TLOS-USDT,TLOS-BTC,YOP-USDT,YOP-ETH,GLQ-USDT,GLQ-BTC,MXC-USDT,ERSDL-USDT,HOTCROSS-USDT,ADA3L-USDT,ADA3S-USDT,HYVE-USDT,HYVE-BTC,DAPPX-USDT,KONO-USDT,PRQ-USDT,MAHA-USDT,MAHA-BTC,FEAR-USDT,PYR-USDT,PYR-BTC,PROM-USDT,PROM-BTC,GLCH-USDT,UNO-USDT,ALBT-USDT,ALBT-ETH,XCAD-USDT,EOS3L-USDT,EOS3S-USDT,BCH3L-USDT,BCH3S-USDT,ELON-USDT,APL-USDT,FCL-ETH,VEED-USDT,VEED-BTC,DIVI-USDT,PDEX-BTC,JUP-USDT,JUP-ETH,POLS-USDT,POLS-BTC,LPOOL-USDT,LPOOL-BTC,LSS-USDT,VET3L-USDT,VET3S-USDT,LTC3L-USDT,LTC3S-USDT,ABBC-USDT,ABBC-BTC,KOK-USDT,ROSN-USDT,DORA-USDT,DORA-BTC,ZCX-USDT,ZCX-BTC,NORD-USDT,GMEE-USDT,SFUND-USDT,XAVA-USDT,AI-USDT,ALPACA-USDT,IOI-USDT,NFT-USDT,NFT-TRX,MNST-USDT,MEM-USDT,AGIX-USDT,AGIX-BTC,AGIX-ETH,CQT-USDT,AIOZ-USDT,MARSH-USDT,HAPI-USDT,MODEFI-USDT,MODEFI-BTC,YFDAI-USDT,YFDAI-BTC,GENS-USDT,FORM-USDT,ARRR-USDT,ARRR-BTC,TOKO-KCS,EXRD-USDT,NGM-USDT,LPT-USDT,STMX-USDT,ASD-USDT,BOND-USDT,HAI-BTC,SOUL-USDT,2CRZ-USDT,NEAR-USDT,NEAR-BTC,DFYN-USDT,OOE-USDT,CFG-USDT,CFG-BTC,AXS-USDT,CLV-USDT,ROUTE-USDT,KAR-USDT,EFX-USDT,XDC-BTC,SHFT-USDT,PMON-USDT,DPET-USDT,ERG-USDT,ERG-BTC,SOL-USDT,SLP-USDT,LITH-USDT,LITH-ETH,XCH-USDT,HAKA-USDT,LAYER-BTC,MTL-USDT,MTL-BTC,IOTX-USDT,GALA-USDT,REQ-USDT,TXA-USDT,TXA-USDC,CIRUS-USDT,QI-USDT,QI-BTC,ODDZ-USDT,PNT-USDT,PNT-BTC,XPR-USDT,XPR-BTC,TRIBE-USDT,SHFT-BTC,MOVR-USDT,MOVR-ETH,WOO-USDT,WILD-USDT,QRDO-USDT,QRDO-ETH,SDN-USDT,SDN-ETH,MAKI-USDT,MAKI-BTC,REP-USDT,REP-BTC,REP-ETH,BNT-USDT,BNT-BTC,BNT-ETH,OXT-USDT,OXT-BTC,OXT-ETH,BAL-USDT,BAL-BTC,BAL-ETH,STORJ-USDT,STORJ-BTC,STORJ-ETH,YGG-USDT,NDAU-USDT,SDAO-USDT,SDAO-ETH,XRP3L-USDT,XRP3S-USDT,SKL-USDT,SKL-BTC,NMR-USDT,NMR-BTC,IXS-USDT,TRB-USDT,TRB-BTC,DYDX-USDT,XYO-USDT,GTC-USDT,GTC-BTC,EQX-USDT,EQX-BTC,RLC-USDT,RLC-BTC,XPRT-USDT,EGLD-USDT,EGLD-BTC,HBAR-USDT,HBAR-BTC,DOGE3L-USDT,DOGE3S-USDT,FLOW-USDT,FLOW-BTC,NKN-USDT,NKN-BTC,PBX-USDT,SOL3L-USDT,SOL3S-USDT,MLN-USDT,MLN-BTC,XNL-USDT,SOLVE-USDT,WNCG-USDT,WNCG-BTC,DMTR-USDT,LINK3L-USDT,LINK3S-USDT,DOT3L-USDT,DOT3S-USDT,CTSI-USDT,CTSI-BTC,ALICE-USDT,ALICE-BTC,ALICE-ETH,OPUL-USDT,ILV-USDT,BAND-USDT,BAND-BTC,FTT-USDT,FTT-BTC,DVPN-USDT,SKU-USDT,SKU-BTC,EDG-USDT,SLIM-USDT,TLM-USDT,TLM-BTC,TLM-ETH,DEXE-USDT,DEXE-BTC,DEXE-ETH,MATTER-USDT,CUDOS-BTC,RUNE-USDT,RUNE-BTC,RMRK-USDT,BMON-USDT,C98-USDT,BLOK-USDT,SOLR-USDT,ATOM3L-USDT,ATOM3S-USDT,UNI3L-USDT,UNI3S-USDT,WSIENNA-USDT,PUSH-USDT,PUSH-BTC,FORM-ETH,NTVRK-USDT,NTVRK-USDC,AXS3L-USDT,AXS3S-USDT,FTM3L-USDT,FTM3S-USDT,FLAME-USDT,AGLD-USDT,NAKA-USDT,YLD-USDT,TONE-USDT,REEF-USDT,REEF-BTC,TIDAL-USDT,TVK-USDT,TVK-BTC,INJ-USDT,INJ-BTC,BNB3L-USDT,BNB3S-USDT,MATIC3L-USDT,MATIC3S-USDT,NFTB-USDT,VEGA-USDT,VEGA-ETH,ALPHA-USDT,ALPHA-BTC,BADGER-USDT,BADGER-BTC,UNO-BTC,ZKT-USDT,AR-USDT,AR-BTC,XVS-USDT,XVS-BTC,JASMY-USDT,PERP-USDT,PERP-BTC,GHST-USDT,GHST-BTC,SCLP-USDT,SCLP-BTC,SUPER-USDT,SUPER-BTC,CPOOL-USDT,HERO-USDT,BASIC-USDT,XED-USDT,XED-BTC,AURY-USDT,SWASH-USDT,LTO-USDT,LTO-BTC,BUX-USDT,MTRG-USDT,DREAMS-USDT,SHIB-DOGE,QUICK-USDT,QUICK-BTC,TRU-USDT,TRU-BTC,WRX-USDT,WRX-BTC,TKO-USDT,TKO-BTC,SUSHI3L-USDT,SUSHI3S-USDT,NEAR3L-USDT,NEAR3S-USDT,DATA-USDT,DATA-BTC,NORD-BTC,ISP-USDT,CERE-USDT,SHILL-USDT,HEGIC-USDT,HEGIC-BTC,ERN-USDT,ERN-BTC,FTG-USDT,PAXG-USDT,PAXG-BTC,AUDIO-USDT,AUDIO-BTC,ENS-USDT,AAVE3L-USDT,AAVE3S-USDT,SAND3L-USDT,SAND3S-USDT,XTM-USDT,MNW-USDT,FXS-USDT,FXS-BTC,ATA-USDT,ATA-BTC,VXV-USDT,LRC-BTC,LRC-ETH,DPR-USDT,CWAR-USDT,CWAR-BTC,FLUX-BTC,EDG-BTC,PBR-USDT,WNXM-USDT,WNXM-BTC,ANT-USDT,ANT-BTC,COV-USDT,SWP-USDT,TWT-USDT,TWT-BTC,OM-USDT,OM-BTC,ADX-USDT,AVAX3L-USDT,AVAX3S-USDT,MANA3L-USDT,MANA3S-USDT,GLM-USDT,GLM-BTC,BAKE-USDT,BAKE-BTC,BAKE-ETH,NUM-USDT,VLX-USDT,VLX-BTC,TRADE-USDT,TRADE-BTC,1EARTH-USDT,MONI-USDT,LIKE-USDT,MFT-USDT,MFT-BTC,LIT-USDT,LIT-BTC,KAVA-USDT,SFP-USDT,SFP-BTC,BURGER-USDT,BURGER-BTC,ILA-USDT,CREAM-USDT,CREAM-BTC,RSR-USDT,RSR-BTC,BUY-BTC,IMX-USDT,GODS-USDT,KMA-USDT,SRM-USDT,SRM-BTC,POLC-USDT,XTAG-USDT,MNET-USDT,NGC-USDT,HARD-USDT,GALAX3L-USDT,GALAX3S-USDT,UNIC-USDT,POND-USDT,POND-BTC,VR-USDT,EPIK-USDT,NGL-USDT,NGL-BTC,KDON-USDT,PEL-USDT,CIRUS-ETH,LINA-USDT,LINA-BTC,KLAY-USDT,KLAY-BTC,CREDI-USDT,TRVL-USDT,LACE-USDT,LACE-ETH,ARKER-USDT,BONDLY-USDT,BONDLY-ETH,XEC-USDT,HEART-USDT,HEART-BTC,UNB-USDT,GAFI-USDT,KOL-USDT,KOL-ETH,H3RO3S-USDT,FALCONS-USDT,UFO-USDT,CHMB-USDT,GEEQ-USDT,ORC-USDT,RACEFI-USDT,PEOPLE-USDT,ADS-USDT,ADS-BTC,OCEAN-USDT,SOS-USDT,WHALE-USDT,TIME-USDT,CWEB-USDT,IOTA-USDT,IOTA-BTC,OOKI-USDT,OOKI-BTC,HNT-USDT,HNT-BTC,GGG-USDT,POWR-USDT,REVU-USDT,CLH-USDT,PLGR-USDT,GLMR-USDT,GLMR-BTC,LOVE-USDT,CTC-USDT,CTC-BTC,GARI-USDT,FRR-USDT,ASTR-USDT,ASTR-BTC,ERTHA-USDT,FCON-USDT,ACA-USDT,ACA-BTC,MTS-USDT,ROAR-USDT,HBB-USDT,SURV-USDT,CVX-USDT,AMP-USDT,ACT-USDT,MJT-USDT,MJT-KCS,SHX-USDT,SHX-BTC,STARLY-USDT,ONSTON-USDT,RANKER-USDT,WMT-USDT,XNO-USDT,XNO-BTC,MARS4-USDT,TFUEL-USDT,TFUEL-BTC,METIS-USDT,LAVAX-USDT,WAL-USDT,BULL-USDT,SON-USDT,MELOS-USDT,APE-USDT,GMT-USDT,BICO-USDT,STG-USDT,LMR-USDT,LMR-BTC,LOKA-USDT,URUS-USDT,JAM-USDT,JAM-ETH,BNC-USDT,LBP-USDT,CFX-USDT,LOOKS-USDT,XCN-USDT,XCN-BTC,KP3R-USDT,TITAN-USDT,INDI-USDT,UPO-USDT,SPELL-USDT,SLCL-USDT,CEEK-USDT,VEMP-USDT,BETA-USDT,NHCT-USDT,ARNM-USDT,FRA-USDT,VISION-USDT,COCOS-USDT,ALPINE-USDT,BNX-USDT,ZBC-USDT,WOOP-USDT,T-USDT,NYM-USDT,VOXEL-USDT,VOXEL-ETH,PSTAKE-USDT,SPA-USDT,SPA-ETH,SYNR-USDT,DAR-USDT,DAR-BTC,MV-USDT,XDEFI-USDT,RACA-USDT,XWG-USDT,HAWK-USDT,TRVL-BTC,SWFTC-USDT,IDEX-USDT,BRWL-USDT,PLATO-USDT,TAUM-USDT,CELR-USDT,AURORA-USDT,POSI-USDT,COOHA-USDT,KNC-USDT,EPK-USDT,PLD-USDT,PSL-USDT,PKF-USDT,OVR-USDT,SYS-USDT,SYS-BTC,BRISE-USDT,DG-USDT,EPX-USDT,GST-USDT,PLY-USDT,GAL-USDT,BSW-USDT,FITFI-USDT,FSN-USDT,H2O-USDT,GMM-USDT,AKT-USDT,SIN-USDT,AUSD-USDT,BOBA-USDT,KARA-USDT,BFC-USDT,BIFI-USDT,DFA-USDT,KYL-USDT,FCD-USDT,MBL-USDT,CELT-USDT,DUSK-USDT,USDD-USDT,USDD-USDC,FITFI-USDC,MBOX-USDT,MBOX-BTC,APE-USDC,AVAX-USDC,SHIB-USDC,XCN-USDC,TRX-USDC,NEAR-USDC,MATIC-USDC,FTM-USDC,ZIL-USDC,SOL-USDC,MLS-USDT,AFK-USDT,AFK-USDC,ACH-USDT,SCRT-USDT,SCRT-BTC,APE3L-USDT,APE3S-USDT,STORE-USDT,STORE-ETH,GMT3L-USDT,GMT3S-USDT,CCD-USDT,DOSE-USDC,LUNC-USDT,LUNC-USDC,USTC-USDT,USTC-USDC,GMT-USDC,VRA-USDC,DOT-USDC,RUNE-USDC,ATOM-USDC,BNB-USDC,JASMY-USDC,KCS-USDC,KDA-USDC,ALGO-USDC,LUNA-USDC,OP-USDT,OP-USDC,JASMY3L-USDT,JASMY3S-USDT,EVER-USDT,MOOV-USDT,IHC-USDT,ICX-USDT,ICX-ETH,BTC-BRL,ETH-BRL,USDT-BRL,WELL-USDT,FORT-USDT,USDP-USDT,USDD-TRX,CSPR-USDT,CSPR-ETH,WEMIX-USDT,REV3L-USDT,OLE-USDT,LDO-USDT,LDO-USDC,CULT-USDT,SWFTC-USDC,FIDA-USDT,BUSD-USDT,RBP-USDT,SRBP-USDT,HIBAYC-USDT,BUSD-USDC,OGV-USDT,WOMBAT-USDT,HIPUNKS-USDT,FT-USDT,ETC-USDC,HIENS4-USDT,EGAME-USDT,EGAME-BTC,STEPWATCH-USDT,HISAND33-USDT,DC-USDT,NEER-USDT,RVN-USDT,HIENS3-USDT,MC-USDT,PEEL-USDT,PEEL-BTC,SDL-USDT,SDL-BTC,SWEAT-USDT,HIODBS-USDT,CMP-USDT,PIX-USDT,MPLX-USDT,HIDOODLES-USDT,ETHW-USDT,QUARTZ-USDT,ACQ-USDT,ACQ-USDC,AOG-USDT,HIMAYC-USDT,PRMX-USDT,RED-USDT,PUMLX-USDT,XETA-USDT,GEM-USDT,DERC-USDT,P00LS-USDT,P00LS-USDC,KICKS-USDT,TRIBL-USDT,GMX-USDT,HIOD-USDT,POKT-USDT,EFI-USDT,APT-USDT,BBC-USDT,EUL-USDT,TON-USDT,PIAS-USDT,HIMEEBITS-USDT,HISQUIGGLE-USDT,XCV-USDT,HFT-USDT,HFT-USDC,ECOX-USDT,AMB-USDT,AZERO-USDT,HIFIDENZA-USDT,BEAT-USDT", "requestFormat": { "uppercase": true, diff --git a/testdata/exchangelist.csv b/testdata/exchangelist.csv index 92dfea9f25c..6425a47ea48 100644 --- a/testdata/exchangelist.csv +++ b/testdata/exchangelist.csv @@ -5,7 +5,6 @@ bitflyer, bithumb, bitmex, bitstamp, -bittrex, btc markets, btse, coinbasepro, From f23cc004fd8430f0837634a1cc5ef90503730a90 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Wed, 22 Nov 2023 10:52:26 +1100 Subject: [PATCH 38/40] Gateio: Implement GetHistoricalFundingRates wrapper func (#1385) * adds funding rate implementations and improvements * merge fixes x1 * lint * kucoin funding rates func make * migrate sync-manager to keys * some kucoin work * adds some kucoin wrapper funcs * ehhh, todo * kucoin position * start of orders * adds the kucoin tests yay * multiplier * nits, EWS includes order limits * NotYetImplemented, IsPerp improvements, cleaning * lint, test fix, huobi time * fixes issues, improves testing * fixes linters I WRECKED * local lint but remote lint, lint, lint, lint * fixes err * skip CI * lint * Supported rates, binance endpoints * fixes weird mocktest problems * no, CZ is invalid * fixes some new EWS test errors * gateio: Add func GetHistoricalFundingRates * glorious: nits * rm: print call * glorious: nits fix test --------- Co-authored-by: Scott Co-authored-by: Ryan O'Hara-Reid --- exchanges/fundingrate/fundingrate_types.go | 10 ++- exchanges/gateio/gateio_test.go | 83 ++++++++++++++++++++++ exchanges/gateio/gateio_wrapper.go | 83 ++++++++++++++++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) diff --git a/exchanges/fundingrate/fundingrate_types.go b/exchanges/fundingrate/fundingrate_types.go index a8376deb9cf..79508ef4901 100644 --- a/exchanges/fundingrate/fundingrate_types.go +++ b/exchanges/fundingrate/fundingrate_types.go @@ -9,8 +9,14 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) -// ErrFundingRateOutsideLimits is returned when a funding rate is outside the allowed date range -var ErrFundingRateOutsideLimits = errors.New("funding rate outside limits") +var ( + // ErrFundingRateOutsideLimits is returned when a funding rate is outside the allowed date range + ErrFundingRateOutsideLimits = errors.New("funding rate outside limits") + // ErrPaymentCurrencyCannotBeEmpty is returned when a payment currency is not set + ErrPaymentCurrencyCannotBeEmpty = errors.New("payment currency cannot be empty") + // ErrNoFundingRatesFound is returned when no funding rates are found + ErrNoFundingRatesFound = errors.New("no funding rates found") +) // HistoricalRatesRequest is used to request funding rate details for a position type HistoricalRatesRequest struct { diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 33d4be1a200..b1475f7e082 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/core" @@ -3455,3 +3456,85 @@ func TestGetLatestFundingRates(t *testing.T) { t.Error(err) } } + +func TestGetHistoricalFundingRates(t *testing.T) { + t.Parallel() + _, err := g.GetHistoricalFundingRates(context.Background(), nil) + if !errors.Is(err, common.ErrNilPointer) { + t.Fatalf("received: %v, expected: %v", err, common.ErrNilPointer) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{}) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("received: %v, expected: %v", err, asset.ErrNotSupported) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + }) + if !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Fatalf("received: %v, expected: %v", err, currency.ErrCurrencyPairEmpty) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + }) + if !errors.Is(err, fundingrate.ErrPaymentCurrencyCannotBeEmpty) { + t.Fatalf("received: %v, expected: %v", err, fundingrate.ErrPaymentCurrencyCannotBeEmpty) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + PaymentCurrency: currency.USDT, + IncludePayments: true, + IncludePredictedRate: true, + }) + if !errors.Is(err, common.ErrNotYetImplemented) { + t.Fatalf("received: %v, expected: %v", err, common.ErrNotYetImplemented) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + PaymentCurrency: currency.USDT, + IncludePredictedRate: true, + }) + if !errors.Is(err, common.ErrNotYetImplemented) { + t.Fatalf("received: %v, expected: %v", err, common.ErrNotYetImplemented) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + PaymentCurrency: currency.USDT, + StartDate: time.Now().Add(time.Hour * 16), + EndDate: time.Now(), + }) + if !errors.Is(err, common.ErrStartAfterEnd) { + t.Fatalf("received: %v, expected: %v", err, common.ErrStartAfterEnd) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + PaymentCurrency: currency.USDT, + StartDate: time.Now().Add(-time.Hour * 8008), + EndDate: time.Now(), + }) + if !errors.Is(err, fundingrate.ErrFundingRateOutsideLimits) { + t.Fatalf("received: %v, expected: %v", err, fundingrate.ErrFundingRateOutsideLimits) + } + + history, err := g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + PaymentCurrency: currency.USDT, + }) + if !errors.Is(err, nil) { + t.Fatalf("received: %v, expected: %v", err, nil) + } + + assert.NotEmpty(t, history, "should return values") +} diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index e0cc7dce434..0531d2133b9 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/config" @@ -2168,6 +2169,88 @@ func (g *Gateio) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) e return g.LoadLimits(limits) } +// GetHistoricalFundingRates returns historical funding rates for a futures contract +func (g *Gateio) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) + } + + if r.Pair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + + if !r.StartDate.IsZero() && !r.EndDate.IsZero() { + err := common.StartEndTimeCheck(r.StartDate, r.EndDate) + if err != nil { + return nil, err + } + } + + // NOTE: Opted to fail here as a misconfigured request will result in + // {"label":"CONTRACT_NOT_FOUND"} and rather not mutate request using + // quote currency as the settlement currency. + if r.PaymentCurrency.IsEmpty() { + return nil, fundingrate.ErrPaymentCurrencyCannotBeEmpty + } + + if r.IncludePayments { + return nil, fmt.Errorf("include payments %w", common.ErrNotYetImplemented) + } + + if r.IncludePredictedRate { + return nil, fmt.Errorf("include predicted rate %w", common.ErrNotYetImplemented) + } + + fPair, err := g.FormatExchangeCurrency(r.Pair, r.Asset) + if err != nil { + return nil, err + } + + records, err := g.GetFutureFundingRates(ctx, r.PaymentCurrency.String(), fPair, 1000) + if err != nil { + return nil, err + } + + if len(records) == 0 { + return nil, fundingrate.ErrNoFundingRatesFound + } + + if !r.StartDate.IsZero() && !r.RespectHistoryLimits && r.StartDate.Before(records[len(records)-1].Timestamp.Time()) { + return nil, fmt.Errorf("%w start date requested: %v last returned record: %v", fundingrate.ErrFundingRateOutsideLimits, r.StartDate, records[len(records)-1].Timestamp.Time()) + } + + fundingRates := make([]fundingrate.Rate, 0, len(records)) + for i := range records { + if (!r.EndDate.IsZero() && r.EndDate.Before(records[i].Timestamp.Time())) || + (!r.StartDate.IsZero() && r.StartDate.After(records[i].Timestamp.Time())) { + continue + } + + fundingRates = append(fundingRates, fundingrate.Rate{ + Rate: decimal.NewFromFloat(records[i].Rate.Float64()), + Time: records[i].Timestamp.Time(), + }) + } + + if len(fundingRates) == 0 { + return nil, fundingrate.ErrNoFundingRatesFound + } + + return &fundingrate.HistoricalRates{ + Exchange: g.Name, + Asset: r.Asset, + Pair: r.Pair, + FundingRates: fundingRates, + StartDate: fundingRates[len(fundingRates)-1].Time, + EndDate: fundingRates[0].Time, + LatestRate: fundingRates[0], + PaymentCurrency: r.PaymentCurrency, + }, nil +} + // GetLatestFundingRates returns the latest funding rates data func (g *Gateio) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { if r == nil { From 88182ec41400fad1a686981797b7c3828acbe204 Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Fri, 24 Nov 2023 03:50:01 +0100 Subject: [PATCH 39/40] Engine: Remove Default Forex Provider (#1395) * Currency: Do not use a default forex provider exchangerate.host now requires an API key. Instead of finding a new Free (for now) default, this change simply disables the currency exchange when nothing is enabled. * SyncManager: Report ?.?? for an unknown forex amount In a situation where we thought forex was available but we got an error, this avoids showing 0.00 when there was actually an error. * Currency: Tests for no default forex * Currency: Use mock provider for tests * Currency: Add API key to exchangerate.host * Currency: Remove Exchangerate.host Exchangerate.host was bought by apilayer, the old API deprecated, and replaced with a proxy to the apilayer api. We already have currencylayer support, so ther's no reason to keep exh. Worth noting: New ERH keys actually work on currencylayer * Currencies: Add test coverage for currency layer * fixup! Currency: Tests for no default forex Remove duplicate assignment Fixes [review comment](https://github.com/thrasher-corp/gocryptotrader/pull/1395#discussion_r1395178513) * fixup! Currency: Add API key to exchangerate.host Remove unused ErrVar Fixes [review comment](https://github.com/thrasher-corp/gocryptotrader/pull/1395#discussion_r1396647418) * fixup! Currency: Tests for no default forex Fix spelling of override in test Fixes [review comment](https://github.com/thrasher-corp/gocryptotrader/pull/1395#discussion_r1396701476) * fixup! SyncManager: Report ?.?? for an unknown forex amount Fix display of non-positive currency conversions. Fixes [review comment](https://github.com/thrasher-corp/gocryptotrader/pull/1395/files#r1398527134) --- CONTRIBUTORS | 6 +- README.md | 12 +- cmd/documentation/currency_templates/fx.tmpl | 1 - .../fx_exchangeratehost.tmpl | 35 --- config/config_types.go | 11 +- currency/conversion_test.go | 37 +-- currency/currency_types.go | 1 - currency/forexprovider/README.md | 1 - .../currencylayer/currencylayer_test.go | 176 ++++++----- .../currencylayer/currencylayer_types.go | 1 + .../forexprovider/exchangerate.host/README.md | 69 ----- .../exchangerate.host/exchangerate.go | 265 ---------------- .../exchangerate.host/exchangerate_test.go | 110 ------- .../exchangerate.host/exchangerate_types.go | 91 ------ currency/forexprovider/forexprovider.go | 30 -- currency/mock_provider_test.go | 40 +++ currency/storage.go | 81 ++--- currency/storage_test.go | 283 +++++------------- engine/engine.go | 6 +- engine/engine_types.go | 1 - engine/sync_manager.go | 19 +- main.go | 1 - 22 files changed, 287 insertions(+), 990 deletions(-) delete mode 100644 cmd/documentation/currency_templates/fx_exchangeratehost.tmpl delete mode 100644 currency/forexprovider/exchangerate.host/README.md delete mode 100644 currency/forexprovider/exchangerate.host/exchangerate.go delete mode 100644 currency/forexprovider/exchangerate.host/exchangerate_test.go delete mode 100644 currency/forexprovider/exchangerate.host/exchangerate_types.go create mode 100644 currency/mock_provider_test.go diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 907db75520a..1bd5d58401f 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -2,8 +2,8 @@ Thanks to the following contributors: thrasher- | https://github.com/thrasher- shazbert | https://github.com/shazbert -gloriousCode | https://github.com/gloriousCode dependabot[bot] | https://github.com/apps/dependabot +gloriousCode | https://github.com/gloriousCode dependabot-preview[bot] | https://github.com/apps/dependabot-preview xtda | https://github.com/xtda gbjk | https://github.com/gbjk @@ -13,13 +13,13 @@ vazha | https://github.com/vazha ydm | https://github.com/ydm ermalguni | https://github.com/ermalguni MadCozBadd | https://github.com/MadCozBadd +Beadko | https://github.com/Beadko vadimzhukck | https://github.com/vadimzhukck 140am | https://github.com/140am marcofranssen | https://github.com/marcofranssen geseq | https://github.com/geseq -Beadko | https://github.com/Beadko -TaltaM | https://github.com/TaltaM samuael | https://github.com/samuael +TaltaM | https://github.com/TaltaM dackroyd | https://github.com/dackroyd cranktakular | https://github.com/cranktakular khcchiu | https://github.com/khcchiu diff --git a/README.md b/README.md index 68c8bc5cf10..9a53d1cb0e3 100644 --- a/README.md +++ b/README.md @@ -144,25 +144,25 @@ Binaries will be published once the codebase reaches a stable condition. |User|Contribution Amount| |--|--| | [thrasher-](https://github.com/thrasher-) | 683 | -| [shazbert](https://github.com/shazbert) | 301 | -| [gloriousCode](https://github.com/gloriousCode) | 219 | -| [dependabot[bot]](https://github.com/apps/dependabot) | 207 | +| [shazbert](https://github.com/shazbert) | 313 | +| [dependabot[bot]](https://github.com/apps/dependabot) | 227 | +| [gloriousCode](https://github.com/gloriousCode) | 224 | | [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 88 | | [xtda](https://github.com/xtda) | 47 | -| [gbjk](https://github.com/gbjk) | 35 | +| [gbjk](https://github.com/gbjk) | 40 | | [lrascao](https://github.com/lrascao) | 27 | | [Rots](https://github.com/Rots) | 15 | | [vazha](https://github.com/vazha) | 15 | | [ydm](https://github.com/ydm) | 15 | | [ermalguni](https://github.com/ermalguni) | 14 | | [MadCozBadd](https://github.com/MadCozBadd) | 13 | +| [Beadko](https://github.com/Beadko) | 10 | | [vadimzhukck](https://github.com/vadimzhukck) | 10 | | [140am](https://github.com/140am) | 8 | | [marcofranssen](https://github.com/marcofranssen) | 8 | | [geseq](https://github.com/geseq) | 8 | -| [Beadko](https://github.com/Beadko) | 6 | +| [samuael](https://github.com/samuael) | 7 | | [TaltaM](https://github.com/TaltaM) | 6 | -| [samuael](https://github.com/samuael) | 6 | | [dackroyd](https://github.com/dackroyd) | 5 | | [cranktakular](https://github.com/cranktakular) | 5 | | [khcchiu](https://github.com/khcchiu) | 5 | diff --git a/cmd/documentation/currency_templates/fx.tmpl b/cmd/documentation/currency_templates/fx.tmpl index 14a0fba971c..6e9e55f1162 100644 --- a/cmd/documentation/currency_templates/fx.tmpl +++ b/cmd/documentation/currency_templates/fx.tmpl @@ -7,7 +7,6 @@ + Exchange Rates support + Fixer.io support + Open Exchange Rates support -+ ExchangeRate.host support ### Please click GoDocs chevron above to view current GoDoc information for this package {{template "contributions"}} diff --git a/cmd/documentation/currency_templates/fx_exchangeratehost.tmpl b/cmd/documentation/currency_templates/fx_exchangeratehost.tmpl deleted file mode 100644 index 24a9c900934..00000000000 --- a/cmd/documentation/currency_templates/fx_exchangeratehost.tmpl +++ /dev/null @@ -1,35 +0,0 @@ -{{define "currency forexprovider exchangerate.host" -}} -{{template "header" .}} -## Current Features for {{.Name}} - -+ Fetches up to date currency data from [ExchangeRate.host API]("https://exchangerate.host") - -### How to enable - -+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-currency-via-config-example) - -+ Individual package example below: -```go -import ( - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerate.host" -) - -var c exchangeratehost.ExchangeRateHost - -// Define configuration -newSettings := base.Settings{ - Name: "ExchangeRateHost", - // ... -} - -c.Setup(newSettings) - -rates, err := c.GetRates("USD", "EUR,AUD") -// Handle error -``` - -### Please click GoDocs chevron above to view current GoDoc information for this package -{{template "contributions"}} -{{template "donations" .}} -{{- end}} \ No newline at end of file diff --git a/config/config_types.go b/config/config_types.go index 657cabbe5c1..c69a942ed79 100644 --- a/config/config_types.go +++ b/config/config_types.go @@ -68,12 +68,11 @@ const ( // Constants here define unset default values displayed in the config.json // file const ( - APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API" - WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - DefaultUnsetAPIKey = "Key" - DefaultUnsetAPISecret = "Secret" - DefaultUnsetAccountPlan = "accountPlan" - DefaultForexProviderExchangeRatesAPI = "ExchangeRateHost" + APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API" + WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + DefaultUnsetAPIKey = "Key" + DefaultUnsetAPISecret = "Secret" + DefaultUnsetAccountPlan = "accountPlan" ) // Variables here are used for configuration diff --git a/currency/conversion_test.go b/currency/conversion_test.go index 189b18f2178..ca97648d4f2 100644 --- a/currency/conversion_test.go +++ b/currency/conversion_test.go @@ -2,32 +2,25 @@ package currency import ( "fmt" - "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestNewConversionFromString(t *testing.T) { - expected := "AUDUSD" - conv, err := NewConversionFromString(expected) - if err != nil { - t.Error(err) - } - if conv.String() != expected { - t.Errorf("NewConversion() error expected %s but received %s", - expected, - conv) - } - - newexpected := strings.ToLower(expected) - conv, err = NewConversionFromString(newexpected) - if err != nil { - t.Error(err) - } - if conv.String() != newexpected { - t.Errorf("NewConversion() error expected %s but received %s", - newexpected, - conv) - } + conv, err := NewConversionFromString("AUDUSD") + assert.NoError(t, err, "NewConversionFromString should not error") + assert.Equal(t, "AUDUSD", conv.String(), "Should provide correct conversion currency") + r, err := conv.GetRate() + assert.NoError(t, err, "GetRate should not error") + assert.Positive(t, r, "Should provide correct conversion rate") + + conv, err = NewConversionFromString("audusd") + assert.NoError(t, err, "NewConversionFromString should not error") + assert.Equal(t, "audusd", conv.String(), "Should provide correct conversion for lowercase") + r, err = conv.GetRate() + assert.NoError(t, err, "GetRate should not error") + assert.Positive(t, r, "Should provide correct conversion rate") } func TestNewConversionFromStrings(t *testing.T) { diff --git a/currency/currency_types.go b/currency/currency_types.go index de9d1aa464c..97575ed830e 100644 --- a/currency/currency_types.go +++ b/currency/currency_types.go @@ -34,7 +34,6 @@ type BotOverrides struct { ExchangeRates bool Fixer bool OpenExchangeRates bool - ExchangeRateHost bool } // CoinmarketcapSettings refers to settings diff --git a/currency/forexprovider/README.md b/currency/forexprovider/README.md index 25118167e2d..eca205a78ab 100644 --- a/currency/forexprovider/README.md +++ b/currency/forexprovider/README.md @@ -25,7 +25,6 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader + Exchange Rates support + Fixer.io support + Open Exchange Rates support -+ ExchangeRate.host support ### Please click GoDocs chevron above to view current GoDoc information for this package diff --git a/currency/forexprovider/currencylayer/currencylayer_test.go b/currency/forexprovider/currencylayer/currencylayer_test.go index 1105a3069b3..bbcb1f40b3d 100644 --- a/currency/forexprovider/currencylayer/currencylayer_test.go +++ b/currency/forexprovider/currencylayer/currencylayer_test.go @@ -1,137 +1,129 @@ package currencylayer import ( + "log" + "os" + "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" ) var c CurrencyLayer -// please set your API key here for due diligence testing NOTE be aware you will -// minimize your API calls using this test. -const ( - APIkey = "" - Apilevel = 0 +// Either set your API key here or in env var for integration testing +var ( + apiKey = "" + apiKeyLevel = 0 ) var isSet bool -func setup() error { - if !isSet { - defaultCfg := base.Settings{ - Name: "CurrencyLayer", - Enabled: true, - } - - if APIkey != "" { - defaultCfg.APIKey = APIkey - } - - if Apilevel > -2 && Apilevel < 4 { - defaultCfg.APIKeyLvl = Apilevel - } - - err := c.Setup(defaultCfg) - if err != nil { - return err - } - isSet = true +func TestMain(m *testing.M) { + if apiKey == "" { + apiKey = os.Getenv("CURRENCYLAYER_APIKEY") } - return nil -} -func areAPIKeysSet() bool { - return APIkey != "" && Apilevel != -1 -} + cfg := base.Settings{ + Name: "CurrencyLayer", + Enabled: true, + APIKeyLvl: apiKeyLevel, + APIKey: apiKey, + } -func TestGetRates(t *testing.T) { - err := setup() - if err != nil { - t.Skip("CurrencyLayer GetRates error", err) + if err := c.Setup(cfg); err != nil { + log.Fatal(err) } - _, err = c.GetRates("USD", "AUD") - if areAPIKeysSet() && err != nil { - t.Error("test error - currencylayer GetRates() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer GetRates() error cannot be nil") + os.Exit(m.Run()) +} + +func skipIfNoAPIKey(tb testing.TB) { + tb.Helper() + if apiKey == "" { + tb.Skip("No API Key configured. Set env var CURRENCYLAYER_APIKEY") } } +func TestNoAPIKey(t *testing.T) { + t.Parallel() + n := c + n.APIKey = "" + _, err := n.GetSupportedCurrencies() + assert.ErrorContains(t, err, "You have not supplied an API Access Key", "Should error APIKeyRequired") +} + func TestGetSupportedCurrencies(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer GetSupportedCurrencies error", err) + t.Parallel() + skipIfNoAPIKey(t) + currs, err := c.GetSupportedCurrencies() + if assert.NoError(t, err, "GetSupportedCurrencies should not error") { + assert.Contains(t, currs, "AUD", "AUD is a valid currency") + assert.Contains(t, currs, "USD", "USD is a valid currency") // Might fail in the near future } - _, err = c.GetSupportedCurrencies() - if areAPIKeysSet() && err != nil { - t.Error("test error - currencylayer GetSupportedCurrencies() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer GetSupportedCurrencies() error cannot be nil") +} + +func TestGetRates(t *testing.T) { + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.GetRates("USD", "AUD") + if assert.NoError(t, err, "GetRates should not error") { + assert.Contains(t, r, "USDAUD", "Should find a USDAUD rate") + assert.Positive(t, r["USDAUD"], "Rate should be positive") } } func TestGetliveData(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer GetliveData error", err) - } - _, err = c.GetliveData("AUD", "USD") - if areAPIKeysSet() && err != nil { - t.Error("test error - currencylayer GetliveData() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer GetliveData() error cannot be nil") + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.GetliveData("EUR", "GBP") + if assert.NoError(t, err, "GetliveData should not error") { + assert.Contains(t, r, "GBPEUR", "Should find rate") + assert.Positive(t, r["GBPEUR"], "Rate should be positive") } } func TestGetHistoricalData(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer GetHistoricalData error", err) - } - _, err = c.GetHistoricalData("2016-12-15", []string{"AUD"}, "USD") - if areAPIKeysSet() && err != nil { - t.Error("test error - currencylayer GetHistoricalData() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer GetHistoricalData() error cannot be nil") + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.GetHistoricalData("2022-09-26", []string{"USD"}, "EUR") + if assert.NoError(t, err, "GetHistoricalData should not error") { + assert.Contains(t, r, "EURUSD", "Should find rate") + assert.Equal(t, r["EURUSD"], 0.962232, "Rate should be exactly correct") } } func TestConvert(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer Convert error", err) - } - _, err = c.Convert("USD", "AUD", "", 1) - if areAPIKeysSet() && err != nil && c.APIKeyLvl >= AccountBasic { - t.Error("test error - currencylayer Convert() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer Convert() error cannot be nil") + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.Convert("USD", "AUD", "", 1) + if assert.NoError(t, err, "Convert should not error") { + assert.Positive(t, r, "Should get a positive rate") } } func TestQueryTimeFrame(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer QueryTimeFrame error", err) - } - _, err = c.QueryTimeFrame("2010-12-0", "2010-12-5", "USD", []string{"AUD"}) - if areAPIKeysSet() && err != nil && c.APIKeyLvl >= AccountPro { - t.Error("test error - currencylayer QueryTimeFrame() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer QueryTimeFrame() error cannot be nil") + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.QueryTimeFrame("2020-03-12", "2020-03-16", "USD", []string{"AUD"}) + if assert.NoError(t, err, "QueryTimeFrame should not error") { + assert.Len(t, r, 5, "Should get correct number of days") + a, ok := r["2020-03-16"].(map[string]any) + assert.True(t, ok, "Has final date entry") + assert.Equal(t, a["USDAUD"], 1.6397, "And it was a bad week") } } func TestQueryCurrencyChange(t *testing.T) { - err := setup() - if err != nil { - t.Fatal("CurrencyLayer QueryCurrencyChange() error", err) - } - _, err = c.QueryCurrencyChange("2010-12-0", "2010-12-5", "USD", []string{"AUD"}) - if areAPIKeysSet() && err != nil && c.APIKeyLvl == AccountEnterprise { - t.Error("test error - currencylayer QueryCurrencyChange() error", err) - } else if !areAPIKeysSet() && err == nil { - t.Error("test error - currencylayer QueryCurrencyChange() error cannot be nil") + t.Parallel() + skipIfNoAPIKey(t) + r, err := c.QueryCurrencyChange("2030-03-12", "2030-03-16", "USD", []string{"AUD"}) + switch { + case err != nil && strings.Contains(err.Error(), "insufficient API privileges, upgrade to basic to use this function"): + t.Skip("Upgrade to Basic API plan to test Currency Change") + case assert.NoError(t, err, "QueryCurrencyChange should not error"): + assert.Contains(t, r, "USDAUD", "Should find change") + assert.Positive(t, r["USDAUD"].Change, "Change should be positive") + assert.Positive(t, r["USDAUD"].ChangePCT, "Change PCT should be positive") } } diff --git a/currency/forexprovider/currencylayer/currencylayer_types.go b/currency/forexprovider/currencylayer/currencylayer_types.go index 43848c7bc98..6d6351fa52b 100644 --- a/currency/forexprovider/currencylayer/currencylayer_types.go +++ b/currency/forexprovider/currencylayer/currencylayer_types.go @@ -33,6 +33,7 @@ type CurrencyLayer struct { // Error Defines the response error if an error occurred type Error struct { Code int `json:"code"` + Type string `json:"type"` Info string `json:"info"` } diff --git a/currency/forexprovider/exchangerate.host/README.md b/currency/forexprovider/exchangerate.host/README.md deleted file mode 100644 index 114f59095ce..00000000000 --- a/currency/forexprovider/exchangerate.host/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# GoCryptoTrader package Exchangerate.host - - - - -[![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/currency/forexprovider/exchangerate.host) -[![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 exchangerate.host package is part of the GoCryptoTrader codebase. - -## This is still in active development - -You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). - -Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) - -## Current Features for exchangerate.host - -+ Fetches up to date currency data from [ExchangeRate.host API]("https://exchangerate.host") - -### How to enable - -+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-currency-via-config-example) - -+ Individual package example below: -```go -import ( - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerate.host" -) - -var c exchangeratehost.ExchangeRateHost - -// Define configuration -newSettings := base.Settings{ - Name: "ExchangeRateHost", - // ... -} - -c.Setup(newSettings) - -rates, err := c.GetRates("USD", "EUR,AUD") -// Handle error -``` - -### Please click GoDocs chevron above to view current GoDoc information for this package - -## Contribution - -Please feel free to submit any pull requests or suggest any desired features to be added. - -When submitting a PR, please abide by our coding guidelines: - -+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). -+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. -+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). -+ Pull requests need to be based on and opened against the `master` branch. - -## Donations - - - -If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: - -***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** \ No newline at end of file diff --git a/currency/forexprovider/exchangerate.host/exchangerate.go b/currency/forexprovider/exchangerate.host/exchangerate.go deleted file mode 100644 index 420b1213ebb..00000000000 --- a/currency/forexprovider/exchangerate.host/exchangerate.go +++ /dev/null @@ -1,265 +0,0 @@ -package exchangeratehost - -import ( - "context" - "errors" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" -) - -// A client for the exchangerate.host API. NOTE: The format and callback -// parameters aren't supported as they're not needed for this implementation. -// Furthermore, the source is set to "ECB" as default - -const ( - timeLayout = "2006-01-02" - exchangeRateHostURL = "https://api.exchangerate.host" -) - -var ( - // DefaultSource uses the ecb for forex rates - DefaultSource = "ecb" -) - -// Setup sets up the ExchangeRateHost config -func (e *ExchangeRateHost) Setup(config base.Settings) error { - e.Name = config.Name - e.Enabled = config.Enabled - e.Verbose = config.Verbose - e.PrimaryProvider = config.PrimaryProvider - var err error - e.Requester, err = request.New(e.Name, - common.NewHTTPClientWithTimeout(base.DefaultTimeOut)) - return err -} - -// GetLatestRates returns a list of forex rates based on the supplied params -func (e *ExchangeRateHost) GetLatestRates(baseCurrency, symbols string, amount float64, places int64, source string) (*LatestRates, error) { - v := url.Values{} - if baseCurrency != "" { - v.Set("base", baseCurrency) - } - - if symbols != "" { - v.Set("symbols", symbols) - } - - if amount != 0 { - v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - } - - if places != 0 { - v.Set("places", strconv.FormatInt(places, 10)) - } - - targetSource := DefaultSource - if source != "" { - targetSource = source - } - v.Set("source", targetSource) - - var l LatestRates - return &l, e.SendHTTPRequest("latest", v, &l) -} - -// ConvertCurrency converts a currency based on the supplied params -func (e *ExchangeRateHost) ConvertCurrency(from, to, baseCurrency, symbols, source string, date time.Time, amount float64, places int64) (*ConvertCurrency, error) { - v := url.Values{} - if from != "" { - v.Set("from", from) - } - if to != "" { - v.Set("to", to) - } - if !date.IsZero() { - v.Set("date", date.UTC().Format(timeLayout)) - } - if baseCurrency != "" { - v.Set("base", baseCurrency) - } - if symbols != "" { - v.Set("symbols", symbols) - } - if amount != 0 { - v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - } - if places != 0 { - v.Set("places", strconv.FormatInt(places, 10)) - } - targetSource := DefaultSource - if source != "" { - targetSource = source - } - v.Set("source", targetSource) - - var c ConvertCurrency - return &c, e.SendHTTPRequest("convert", v, &c) -} - -// GetHistoricalRates returns a list of historical rates based on the supplied params -func (e *ExchangeRateHost) GetHistoricalRates(date time.Time, baseCurrency, symbols string, amount float64, places int64, source string) (*HistoricRates, error) { - v := url.Values{} - if date.IsZero() { - date = time.Now() - } - fmtDate := date.UTC().Format(timeLayout) - v.Set("date", fmtDate) - if baseCurrency != "" { - v.Set("base", baseCurrency) - } - - if symbols != "" { - v.Set("symbols", symbols) - } - - if amount != 0 { - v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - } - - if places != 0 { - v.Set("places", strconv.FormatInt(places, 10)) - } - - targetSource := DefaultSource - if source != "" { - targetSource = source - } - v.Set("source", targetSource) - - var h HistoricRates - return &h, e.SendHTTPRequest(fmtDate, v, &h) -} - -// GetTimeSeries returns time series forex data based on the supplied params -func (e *ExchangeRateHost) GetTimeSeries(startDate, endDate time.Time, baseCurrency, symbols string, amount float64, places int64, source string) (*TimeSeries, error) { - if startDate.IsZero() || endDate.IsZero() { - return nil, errors.New("startDate and endDate must be set") - } - - if startDate.After(endDate) || startDate.Equal(endDate) { - return nil, errors.New("startDate and endDate must be set correctly") - } - - v := url.Values{} - v.Set("start_date", startDate.UTC().Format(timeLayout)) - v.Set("end_date", endDate.UTC().Format(timeLayout)) - - if baseCurrency != "" { - v.Set("base", baseCurrency) - } - - if symbols != "" { - v.Set("symbols", symbols) - } - - if amount != 0 { - v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - } - - if places != 0 { - v.Set("places", strconv.FormatInt(places, 10)) - } - - targetSource := DefaultSource - if source != "" { - targetSource = source - } - v.Set("source", targetSource) - - var t TimeSeries - return &t, e.SendHTTPRequest("timeseries", v, &t) -} - -// GetFluctuations returns a list of forex price fluctuations based on the supplied params -func (e *ExchangeRateHost) GetFluctuations(startDate, endDate time.Time, baseCurrency, symbols string, amount float64, places int64, source string) (*Fluctuations, error) { - if startDate.IsZero() || endDate.IsZero() { - return nil, errors.New("startDate and endDate must be set") - } - - if startDate.After(endDate) || startDate.Equal(endDate) { - return nil, errors.New("startDate and endDate must be set correctly") - } - - v := url.Values{} - v.Set("start_date", startDate.UTC().Format(timeLayout)) - v.Set("end_date", endDate.UTC().Format(timeLayout)) - - if baseCurrency != "" { - v.Set("base", baseCurrency) - } - - if symbols != "" { - v.Set("symbols", symbols) - } - - if amount != 0 { - v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - } - - if places != 0 { - v.Set("places", strconv.FormatInt(places, 10)) - } - - targetSource := DefaultSource - if source != "" { - targetSource = source - } - v.Set("source", targetSource) - - var f Fluctuations - return &f, e.SendHTTPRequest("fluctuation", v, &f) -} - -// GetSupportedSymbols returns a list of supported symbols -func (e *ExchangeRateHost) GetSupportedSymbols() (*SupportedSymbols, error) { - var s SupportedSymbols - return &s, e.SendHTTPRequest("symbols", url.Values{}, &s) -} - -// GetSupportedCurrencies returns a list of supported currencies -func (e *ExchangeRateHost) GetSupportedCurrencies() ([]string, error) { - s, err := e.GetSupportedSymbols() - if err != nil { - return nil, err - } - - symbols := make([]string, 0, len(s.Symbols)) - for x := range s.Symbols { - symbols = append(symbols, x) - } - return symbols, nil -} - -// GetRates returns the forex rates based on the supplied base currency and symbols -func (e *ExchangeRateHost) GetRates(baseCurrency, symbols string) (map[string]float64, error) { - l, err := e.GetLatestRates(baseCurrency, symbols, 0, 0, "") - if err != nil { - return nil, err - } - - rates := make(map[string]float64) - for k, v := range l.Rates { - rates[baseCurrency+k] = v - } - return rates, nil -} - -// SendHTTPRequest sends a typical get request -func (e *ExchangeRateHost) SendHTTPRequest(endpoint string, v url.Values, result interface{}) error { - path := common.EncodeURLValues(exchangeRateHostURL+"/"+endpoint, v) - item := &request.Item{ - Method: http.MethodGet, - Path: path, - Result: &result, - Verbose: e.Verbose, - } - return e.Requester.SendPayload(context.TODO(), request.Unset, func() (*request.Item, error) { - return item, nil - }, request.UnauthenticatedRequest) -} diff --git a/currency/forexprovider/exchangerate.host/exchangerate_test.go b/currency/forexprovider/exchangerate.host/exchangerate_test.go deleted file mode 100644 index c5535909956..00000000000 --- a/currency/forexprovider/exchangerate.host/exchangerate_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package exchangeratehost - -import ( - "log" - "os" - "testing" - "time" - - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" -) - -var ( - e ExchangeRateHost - testCurrencies = "USD,EUR,CZK" -) - -func TestMain(t *testing.M) { - err := e.Setup(base.Settings{ - Name: "ExchangeRateHost", - }) - if err != nil { - log.Fatal(err) - } - os.Exit(t.Run()) -} - -func TestGetLatestRates(t *testing.T) { - _, err := e.GetLatestRates("USD", testCurrencies, 1200, 2, "") - if err != nil { - t.Error(err) - } -} - -func TestConvertCurrency(t *testing.T) { - _, err := e.ConvertCurrency("USD", "EUR", "", testCurrencies, "", time.Now(), 1200, 2) - if err != nil { - t.Error(err) - } -} - -func TestGetHistoricRates(t *testing.T) { - _, err := e.GetHistoricalRates(time.Time{}, "AUD", testCurrencies, 1200, 2, "") - if err != nil { - t.Error(err) - } -} - -func TestGetTimeSeriesRates(t *testing.T) { - _, err := e.GetTimeSeries(time.Time{}, time.Now(), "USD", testCurrencies, 1200, 2, "") - if err == nil { - t.Error("empty start time show throw an error") - } - tmNow := time.Now() - _, err = e.GetTimeSeries(tmNow, tmNow, "USD", testCurrencies, 1200, 2, "") - if err == nil { - t.Error("equal times show throw an error") - } - tmStart := tmNow.AddDate(0, -3, 0) - _, err = e.GetTimeSeries(tmStart, tmNow, "USD", testCurrencies, 1200, 2, "") - if err != nil { - t.Error(err) - } -} - -func TestGetFluctuationData(t *testing.T) { - _, err := e.GetFluctuations(time.Time{}, time.Now(), "USD", testCurrencies, 1200, 2, "") - if err == nil { - t.Error("empty start time show throw an error") - } - tmNow := time.Now() - _, err = e.GetFluctuations(tmNow, tmNow, "USD", testCurrencies, 1200, 2, "") - if err == nil { - t.Error("equal times show throw an error") - } - tmStart := tmNow.AddDate(0, -3, 0) - _, err = e.GetFluctuations(tmStart, tmNow, "USD", testCurrencies, 1200, 2, "") - if err != nil { - t.Error(err) - } -} - -func TestGetSupportedSymbols(t *testing.T) { - r, err := e.GetSupportedSymbols() - if err != nil { - t.Fatal(err) - } - if _, ok := r.Symbols["AUD"]; !ok { - t.Error("should contain AUD") - } -} - -func TestGetGetSupportedCurrencies(t *testing.T) { - s, err := e.GetSupportedCurrencies() - if err != nil { - t.Fatal(err) - } - if len(s) == 0 { - t.Error("supported currencies should be greater than 0") - } -} - -func TestGetRates(t *testing.T) { - r, err := e.GetRates("USD", "") - if err != nil { - t.Fatal(err) - } - if rate := r["USDAUD"]; rate == 0 { - t.Error("rate of USDAUD should be set") - } -} diff --git a/currency/forexprovider/exchangerate.host/exchangerate_types.go b/currency/forexprovider/exchangerate.host/exchangerate_types.go deleted file mode 100644 index 0a56ac9838c..00000000000 --- a/currency/forexprovider/exchangerate.host/exchangerate_types.go +++ /dev/null @@ -1,91 +0,0 @@ -package exchangeratehost - -import ( - "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" -) - -// ExchangeRateHost stores the struct for the exchangerate.host API -type ExchangeRateHost struct { - base.Base - Requester *request.Requester -} - -// MessageOfTheDay stores the message of the day -type MessageOfTheDay struct { - Message string `json:"msg"` - DonationURL string `json:"url"` -} - -// LatestRates stores the latest forex rates -type LatestRates struct { - MessageOfTheDay MessageOfTheDay `json:"motd"` - Success bool `json:"success"` - Base string `json:"base"` - Date string `json:"date"` - Rates map[string]float64 `json:"rates"` -} - -// ConvertCurrency stores currency conversion data -type ConvertCurrency struct { - MessageOfTheDay MessageOfTheDay `json:"motd"` - Query struct { - From string `json:"from"` - To string `json:"to"` - Amount float64 `json:"amount"` - } `json:"query"` - Info struct { - Rate float64 `json:"rate"` - } `json:"info"` - Historical bool `json:"historical"` - Date string `json:"date"` - Result float64 `json:"result"` -} - -// HistoricRates stores the hostoric rates -type HistoricRates struct { - LatestRates - Historical bool `json:"historical"` -} - -// TimeSeries stores time series data -type TimeSeries struct { - MessageOfTheDay MessageOfTheDay `json:"motd"` - Success bool `json:"success"` - TimeSeries bool `json:"timeseries"` - Base string `json:"base"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - Rates map[string]map[string]float64 `json:"rates"` -} - -// Fluctuation stores an individual rate flucutation -type Fluctuation struct { - StartRate float64 `json:"start_rate"` - EndRate float64 `json:"end_rate"` - Change float64 `json:"change"` - ChangePercentage float64 `json:"change_pct"` -} - -// Fluctuations stores a collection of rate fluctuations -type Fluctuations struct { - MessageOfTheDay MessageOfTheDay `json:"motd"` - Success bool `json:"success"` - Flucutation bool `json:"fluctuation"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - Rates map[string]Fluctuation `json:"rate"` -} - -// Symbol stores an individual symbol -type Symbol struct { - Description string `json:"description"` - Code string `json:"code"` -} - -// SupportedSymbols store a collection of supported symbols -type SupportedSymbols struct { - MessageOfTheDay MessageOfTheDay `json:"motd"` - Success bool `json:"success"` - Symbols map[string]Symbol `json:"symbols"` -} diff --git a/currency/forexprovider/forexprovider.go b/currency/forexprovider/forexprovider.go index 393a01fabd4..0be87517c08 100644 --- a/currency/forexprovider/forexprovider.go +++ b/currency/forexprovider/forexprovider.go @@ -9,7 +9,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" currencyconverter "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencyconverterapi" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencylayer" - exchangeratehost "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerate.host" exchangerates "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangeratesapi.io" fixer "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/fixer.io" "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/openexchangerates" @@ -33,36 +32,9 @@ func GetSupportedForexProviders() []string { "ExchangeRates", "Fixer", "OpenExchangeRates", - "ExchangeRateHost", } } -// NewDefaultFXProvider returns the default forex provider (currencyconverterAPI) -func NewDefaultFXProvider() *ForexProviders { - handler := new(ForexProviders) - provider := new(exchangeratehost.ExchangeRateHost) - err := provider.Setup(base.Settings{ - PrimaryProvider: true, - Enabled: true, - Name: "ExchangeRateHost", - }) - if err != nil { - panic(err) - } - - currencies, _ := provider.GetSupportedCurrencies() - providerBase := base.Provider{ - Provider: provider, - SupportedCurrencies: currencies, - } - - handler.FXHandler = base.FXHandler{ - Primary: providerBase, - } - - return handler -} - // SetProvider sets provider to the FX handler func (f *ForexProviders) SetProvider(b base.IFXProvider) error { currencies, err := b.GetSupportedCurrencies() @@ -102,8 +74,6 @@ func StartFXService(fxProviders []base.Settings) (*ForexProviders, error) { provider = new(fixer.Fixer) case "OpenExchangeRates": provider = new(openexchangerates.OXR) - case "ExchangeRateHost": - provider = new(exchangeratehost.ExchangeRateHost) default: return nil, fmt.Errorf("%s %w", fxProviders[i].Name, errUnhandledForeignExchangeProvider) diff --git a/currency/mock_provider_test.go b/currency/mock_provider_test.go new file mode 100644 index 00000000000..3e1431ef8a5 --- /dev/null +++ b/currency/mock_provider_test.go @@ -0,0 +1,40 @@ +package currency + +import ( + "math/rand" + "strings" + + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider" + "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base" +) + +type MockProvider struct{} + +func newMockProvider() *forexprovider.ForexProviders { + p := &MockProvider{} + c, _ := p.GetSupportedCurrencies() + return &forexprovider.ForexProviders{ + FXHandler: base.FXHandler{ + Primary: base.Provider{ + Provider: p, + SupportedCurrencies: c, + }, + }, + } +} + +func (m *MockProvider) GetName() string { return "MockProvider" } +func (m *MockProvider) Setup(base.Settings) error { return nil } +func (m *MockProvider) IsEnabled() bool { return true } +func (m *MockProvider) IsPrimaryProvider() bool { return true } +func (m *MockProvider) GetSupportedCurrencies() ([]string, error) { + return storage.defaultFiatCurrencies.Strings(), nil +} +func (m *MockProvider) GetRates(baseCurrency, symbols string) (map[string]float64, error) { + c := map[string]float64{} + for _, s := range strings.Split(symbols, ",") { + // The year is 2027; The USD is nearly worthless. The world reserve currency is eggs. + c[baseCurrency+s] = 1 / (1 + rand.Float64()) //nolint:gosec // Doesn't need to be a strong random number + } + return c, nil +} diff --git a/currency/storage.go b/currency/storage.go index e43d7b35b32..930533f1a68 100644 --- a/currency/storage.go +++ b/currency/storage.go @@ -26,10 +26,9 @@ func init() { // CurrencyFileUpdateDelay defines the rate at which the currency.json file is // updated const ( - DefaultCurrencyFileDelay = 168 * time.Hour - DefaultForeignExchangeDelay = 1 * time.Minute - DefaultStorageFile = "currency.json" - DefaultForexProviderExchangeRatesAPI = "ExchangeRateHost" + DefaultCurrencyFileDelay = 168 * time.Hour + DefaultForeignExchangeDelay = 1 * time.Minute + DefaultStorageFile = "currency.json" ) var ( @@ -74,7 +73,12 @@ func (s *Storage) SetDefaults() { log.Errorf(log.Currency, "Currency Storage: Setting default cryptocurrencies error: %s", err) } s.SetupConversionRates() - s.fiatExchangeMarkets = forexprovider.NewDefaultFXProvider() + s.fiatExchangeMarkets = nil +} + +// ForexEnabled returns whether the currency system has any available forex providers enabled +func ForexEnabled() bool { + return storage.fiatExchangeMarkets != nil } // RunUpdater runs the foreign exchange updater service. This will set up a JSON @@ -103,6 +107,10 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *Config, filePath } s.mtx.Lock() + + // Ensure the forex provider is unset in cases we exit early + s.fiatExchangeMarkets = nil + s.shutdown = make(chan struct{}) s.baseCurrency = settings.FiatDisplayCurrency s.path = filepath.Join(filePath, DefaultStorageFile) @@ -132,26 +140,23 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *Config, filePath (settings.ForexProviders[i].Name == "CurrencyLayer" && overrides.CurrencyLayer) || (settings.ForexProviders[i].Name == "Fixer" && overrides.Fixer) || (settings.ForexProviders[i].Name == "OpenExchangeRates" && overrides.OpenExchangeRates) || - (settings.ForexProviders[i].Name == "ExchangeRates" && overrides.ExchangeRates) || - (settings.ForexProviders[i].Name == "ExchangeRateHost" && overrides.ExchangeRateHost) + (settings.ForexProviders[i].Name == "ExchangeRates" && overrides.ExchangeRates) if !enabled { continue } - if settings.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI { - if settings.ForexProviders[i].APIKey == "" || settings.ForexProviders[i].APIKey == "Key" { - log.Warnf(log.Currency, "%s forex provider API key not set, disabling. Please set this in your config.json file\n", - settings.ForexProviders[i].Name) - settings.ForexProviders[i].Enabled = false - settings.ForexProviders[i].PrimaryProvider = false - continue - } + if settings.ForexProviders[i].APIKey == "" || settings.ForexProviders[i].APIKey == "Key" { + log.Warnf(log.Currency, "%s forex provider API key not set, disabling. Please set this in your config.json file\n", + settings.ForexProviders[i].Name) + settings.ForexProviders[i].Enabled = false + settings.ForexProviders[i].PrimaryProvider = false + continue + } - if settings.ForexProviders[i].APIKeyLvl == -1 && settings.ForexProviders[i].Name != "ExchangeRates" { - log.Warnf(log.Currency, "%s APIKey level not set, functionality is limited. Please review this in your config.json file\n", - settings.ForexProviders[i].Name) - } + if settings.ForexProviders[i].APIKeyLvl == -1 && settings.ForexProviders[i].Name != "ExchangeRates" { + log.Warnf(log.Currency, "%s APIKey level not set, functionality is limited. Please review this in your config.json file\n", + settings.ForexProviders[i].Name) } if settings.ForexProviders[i].PrimaryProvider { @@ -166,23 +171,10 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *Config, filePath fxSettings = append(fxSettings, base.Settings(settings.ForexProviders[i])) } - if len(fxSettings) == 0 { - log.Warnln(log.Currency, "No foreign exchange providers enabled, setting default provider...") - for x := range settings.ForexProviders { - if settings.ForexProviders[x].Name != DefaultForexProviderExchangeRatesAPI { - continue - } - settings.ForexProviders[x].Enabled = true - settings.ForexProviders[x].PrimaryProvider = true - primaryProvider = true - log.Warnf(log.Currency, "No valid foreign exchange providers configured. Defaulting to %s.", DefaultForexProviderExchangeRatesAPI) - fxSettings = append(fxSettings, base.Settings(settings.ForexProviders[x])) - } - } - if len(fxSettings) == 0 { s.mtx.Unlock() - return errNoForeignExchangeProvidersEnabled + log.Warnln(log.Currency, "No foreign exchange providers enabled, currency conversion will not be available") + return nil } if !primaryProvider { @@ -313,8 +305,7 @@ func (s *Storage) ForeignExchangeUpdater() { log.Errorln(log.Currency, err) } - // Unlock main rate retrieval mutex so all routines waiting can get access - // to data + // Unlock main rate retrieval mutex so all routines waiting can get access to data s.mtx.Unlock() // Set tickers to client defined rates or defaults @@ -516,6 +507,9 @@ func (s *Storage) UpdateCurrencies() error { func (s *Storage) SeedForeignExchangeRatesByCurrencies(c Currencies) error { s.fxRates.mtx.Lock() defer s.fxRates.mtx.Unlock() + if s.fiatExchangeMarkets == nil { + return nil + } rates, err := s.fiatExchangeMarkets.GetCurrencyData(s.baseCurrency.String(), c.Strings()) if err != nil { @@ -526,6 +520,9 @@ func (s *Storage) SeedForeignExchangeRatesByCurrencies(c Currencies) error { // SeedForeignExchangeRate returns a singular exchange rate func (s *Storage) SeedForeignExchangeRate(from, to Code) (map[string]float64, error) { + if s.fiatExchangeMarkets == nil { + return nil, nil + } return s.fiatExchangeMarkets.GetCurrencyData(from.String(), []string{to.String()}) } @@ -546,6 +543,9 @@ func (s *Storage) GetDefaultForeignExchangeRates() (Conversions, error) { func (s *Storage) SeedDefaultForeignExchangeRates() error { s.fxRates.mtx.Lock() defer s.fxRates.mtx.Unlock() + if s.fiatExchangeMarkets == nil { + return errNoForeignExchangeProvidersEnabled + } rates, err := s.fiatExchangeMarkets.GetCurrencyData( s.defaultBaseCurrency.String(), s.defaultFiatCurrencies.Strings()) @@ -566,11 +566,13 @@ func (s *Storage) GetExchangeRates() (Conversions, error) { return s.fxRates.GetFullRates(), nil } -// SeedForeignExchangeRates seeds the foreign exchange rates from storage config -// currencies +// SeedForeignExchangeRates seeds the foreign exchange rates from storage config currencies func (s *Storage) SeedForeignExchangeRates() error { s.fxRates.mtx.Lock() defer s.fxRates.mtx.Unlock() + if s.fiatExchangeMarkets == nil { + return errNoForeignExchangeProvidersEnabled + } rates, err := s.fiatExchangeMarkets.GetCurrencyData(s.baseCurrency.String(), s.fiatCurrencies.Strings()) if err != nil { @@ -695,6 +697,9 @@ func (s *Storage) UpdateEnabledFiatCurrencies(c Currencies) { // ConvertCurrency for example converts $1 USD to the equivalent Japanese Yen // or vice versa. func (s *Storage) ConvertCurrency(amount float64, from, to Code) (float64, error) { + if s.fiatExchangeMarkets == nil { + return 0, errNoForeignExchangeProvidersEnabled + } if amount <= 0 { return 0, fmt.Errorf("%f %w", amount, errInvalidAmount) } diff --git a/currency/storage_test.go b/currency/storage_test.go index 8481ac4a792..a35b60dd91d 100644 --- a/currency/storage_test.go +++ b/currency/storage_test.go @@ -1,11 +1,11 @@ package currency import ( - "errors" "fmt" "os" "testing" + "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/database/testhelpers" ) @@ -17,6 +17,8 @@ func TestMain(m *testing.M) { os.Exit(1) } + storage.fiatExchangeMarkets = newMockProvider() + t := m.Run() err = os.RemoveAll(testhelpers.TempDir) @@ -30,219 +32,88 @@ func TestMain(m *testing.M) { func TestRunUpdater(t *testing.T) { var newStorage Storage - emptyMainConfig := Config{} - err := newStorage.RunUpdater(BotOverrides{}, &emptyMainConfig, "") - if err == nil { - t.Fatal("storage RunUpdater() error cannot be nil") - } - - mainConfig := Config{} - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "") - if !errors.Is(err, errFiatDisplayCurrencyUnset) { - t.Fatalf("received: '%v' but expected: '%v'", err, errFiatDisplayCurrencyUnset) - } + err := newStorage.RunUpdater(BotOverrides{}, &Config{}, "") + assert.ErrorIs(t, err, errFiatDisplayCurrencyUnset, "No currency should error correctly") - mainConfig.FiatDisplayCurrency = BTC - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "") - if !errors.Is(err, ErrFiatDisplayCurrencyIsNotFiat) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrFiatDisplayCurrencyIsNotFiat) - } + err = newStorage.RunUpdater(BotOverrides{}, &Config{FiatDisplayCurrency: BTC}, "") + assert.ErrorIs(t, err, ErrFiatDisplayCurrencyIsNotFiat, "Crypto currency should error as not fiat") - mainConfig.FiatDisplayCurrency = AUD - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "") - if !errors.Is(err, errNoFilePathSet) { - t.Fatalf("received: '%v' but expected: '%v'", err, errNoFilePathSet) - } + c := &Config{FiatDisplayCurrency: AUD} + err = newStorage.RunUpdater(BotOverrides{}, c, "") + assert.ErrorIs(t, err, errNoFilePathSet, "Should error with no path set") tempDir := testhelpers.TempDir + err = newStorage.RunUpdater(BotOverrides{}, c, tempDir) + assert.ErrorIs(t, err, errInvalidCurrencyFileUpdateDuration, "Should error invalid file update duration") - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, tempDir) - if !errors.Is(err, errInvalidCurrencyFileUpdateDuration) { - t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidCurrencyFileUpdateDuration) - } - - mainConfig.CurrencyFileUpdateDuration = DefaultCurrencyFileDelay - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, tempDir) - if !errors.Is(err, errInvalidForeignExchangeUpdateDuration) { - t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidForeignExchangeUpdateDuration) - } - - mainConfig.ForeignExchangeUpdateDuration = DefaultForeignExchangeDelay - err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, tempDir) - if !errors.Is(err, errNoForeignExchangeProvidersEnabled) { - t.Fatalf("received: '%v' but expected: '%v'", err, errNoForeignExchangeProvidersEnabled) - } - - settings := FXSettings{ - Name: "Fixer", - Enabled: true, - APIKey: "wo", - } - - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{Fixer: true}, &mainConfig, tempDir) - if errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, "an error") - } - - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - settings.Name = "CurrencyConverter" - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{CurrencyConverter: true}, &mainConfig, tempDir) - if errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, "an error") - } - - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - settings.Name = "CurrencyLayer" - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{CurrencyLayer: true}, &mainConfig, tempDir) - if errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, "an error") - } - - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - settings.Name = "OpenExchangeRates" - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{OpenExchangeRates: true}, &mainConfig, tempDir) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - settings.Name = "ExchangeRates" - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{ExchangeRates: true}, &mainConfig, tempDir) - if errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, "an error") - } - - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - settings.Name = "ExchangeRateHost" - mainConfig.ForexProviders = AllFXSettings{settings} - err = newStorage.RunUpdater(BotOverrides{ExchangeRateHost: true}, &mainConfig, tempDir) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - // old config where two providers enabled - oldDefault := FXSettings{ - Name: "ExchangeRates", - Enabled: true, - APIKey: "", // old default provider which did not need api keys. - PrimaryProvider: true, - } - other := FXSettings{ - Name: "OpenExchangeRates", - Enabled: true, - APIKey: "enabled-keys", // Has keys enabled and will fall over to primary - } - defaultProvider := FXSettings{ - // From config this will be included but not necessarily enabled. - Name: "ExchangeRateHost", - Enabled: false, - } - - mainConfig.ForexProviders = AllFXSettings{oldDefault, other, defaultProvider} - err = newStorage.RunUpdater(BotOverrides{ExchangeRates: true, OpenExchangeRates: true}, &mainConfig, tempDir) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - if mainConfig.ForexProviders[0].Enabled { - t.Fatal("old default provider should not be enabled due to unset keys") - } - - if mainConfig.ForexProviders[0].PrimaryProvider { - t.Fatal("old default provider should not be a primary provider anymore") - } - - if !mainConfig.ForexProviders[1].Enabled { - t.Fatal("open exchange rates provider with keys set should be enabled") - } - - if !mainConfig.ForexProviders[1].PrimaryProvider { - t.Fatal("open exchange rates provider with keys set should be set as primary provider") - } + c.CurrencyFileUpdateDuration = DefaultCurrencyFileDelay + err = newStorage.RunUpdater(BotOverrides{}, c, tempDir) + assert.ErrorIs(t, err, errInvalidForeignExchangeUpdateDuration, "Should error invalid forex update duration") - if mainConfig.ForexProviders[2].Enabled { - t.Fatal("actual default provider should not be enabled") - } + c.ForeignExchangeUpdateDuration = DefaultForeignExchangeDelay + err = newStorage.RunUpdater(BotOverrides{}, c, tempDir) - if mainConfig.ForexProviders[2].PrimaryProvider { - t.Fatal("actual default provider should not be designated as primary provider") - } + assert.NoError(t, err, "Storage should not error with no forex providers enabled") + assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers") // Proxy for testing ForexEnabled err = newStorage.Shutdown() - if err != nil { - t.Fatal(err) - } - - // old config where two providers enabled - oldDefault = FXSettings{ - Name: "ExchangeRates", - Enabled: true, - APIKey: "", // old default provider which did not need api keys. - PrimaryProvider: true, - } - other = FXSettings{ - Name: "OpenExchangeRates", - Enabled: true, - APIKey: "", // Has no keys enabled will fall over to new default provider. - } - - mainConfig.ForexProviders = AllFXSettings{oldDefault, other, defaultProvider} - err = newStorage.RunUpdater(BotOverrides{ExchangeRates: true, OpenExchangeRates: true}, &mainConfig, tempDir) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - if mainConfig.ForexProviders[0].Enabled { - t.Fatal("old default provider should not be enabled due to unset keys") - } - - if mainConfig.ForexProviders[0].PrimaryProvider { - t.Fatal("old default provider should not be a primary provider anymore") - } - - if mainConfig.ForexProviders[1].Enabled { - t.Fatal("open exchange rates provider with keys unset should not be enabled") - } - - if mainConfig.ForexProviders[1].PrimaryProvider { - t.Fatal("open exchange rates provider with keys unset should not be set as primary provider") - } - - if !mainConfig.ForexProviders[2].Enabled { - t.Fatal("actual default provider should not be disabled") - } + assert.NoError(t, err, "Shutdown should not error evne though it silently aborted the RunUpdater early") + + // Exchanges which reject a bad APIKey + for _, n := range []string{"Fixer", "CurrencyConverter", "CurrencyLayer", "ExchangeRates"} { + c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: ""}} + err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir) + assert.NoErrorf(t, err, "%s should not error and silently exit without running with no api keys", n) + assert.Falsef(t, c.ForexProviders[0].Enabled, "%s should not be marked enabled with no api keys", n) + assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers") + c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: "sudo shazam!"}} + err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir) + assert.Errorf(t, err, "%s should throw some provider originating error with a (hopefully) invalid api key", n) + assert.Truef(t, c.ForexProviders[0].Enabled, "%s should still be enabled after being chosen but failing", n) + assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled when provider errored during startup") + err = newStorage.Shutdown() + assert.NoError(t, err, "Shutdown should not error") + } + + // Exchanges which do not error with a bad APIKey on startup + for _, n := range []string{"OpenExchangeRates"} { + c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: ""}} + err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir) + assert.NoErrorf(t, err, "%s should not error and silently exit without running with no api keys", n) + assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers") + c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: "sudo shazam!"}} + err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir) + assert.NoError(t, err, "%s should not error on Setup with a bad apikey", n) + assert.NotNil(t, newStorage.fiatExchangeMarkets, "Forex should be enabled now we have a provider with a key") + err = newStorage.Shutdown() + assert.NoError(t, err, "Shutdown should not error") + } + + c.ForexProviders = AllFXSettings{ + {Name: "ExchangeRates"}, // Old Default + {Name: "OpenExchangeRates", APIKey: "shazam?"}, + } + + // Regression test for old defaults which were enabled when in settings and nothing else was enabled and configured + err = newStorage.RunUpdater(BotOverrides{}, c, tempDir) + assert.NoError(t, err, "RunUpdater should not error") + assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers") // Proxy for testing ForexEnabled + assert.False(t, c.ForexProviders[0].Enabled, "Old Default ExchangeRates should not have defaulted to enabled with no enabled overrides") +} - if !mainConfig.ForexProviders[2].PrimaryProvider { - t.Fatal("actual default provider should be designated as primary provider") - } +func overrideForProvider(n string) BotOverrides { + b := BotOverrides{} + switch n { + case "Fixer": + b.Fixer = true + case "CurrencyConverter": + b.CurrencyConverter = true + case "CurrencyLayer": + b.CurrencyLayer = true + case "OpenExchangeRates": + b.OpenExchangeRates = true + case "ExchangeRates": + b.ExchangeRates = true + } + return b } diff --git a/engine/engine.go b/engine/engine.go index d279afa8088..08bb6041569 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -173,7 +173,6 @@ func validateSettings(b *Engine, s *Settings, flagSet FlagSet) { flagSet.WithBool("exchangerates", &b.Settings.EnableExchangeRates, b.Config.Currency.ForexProviders.IsEnabled("exchangerates")) flagSet.WithBool("fixer", &b.Settings.EnableFixer, b.Config.Currency.ForexProviders.IsEnabled("fixer")) flagSet.WithBool("openexchangerates", &b.Settings.EnableOpenExchangeRates, b.Config.Currency.ForexProviders.IsEnabled("openexchangerates")) - flagSet.WithBool("exchangeratehost", &b.Settings.EnableExchangeRateHost, b.Config.Currency.ForexProviders.IsEnabled("exchangeratehost")) flagSet.WithBool("datahistorymanager", &b.Settings.EnableDataHistoryManager, b.Config.DataHistoryManager.Enabled) flagSet.WithBool("currencystatemanager", &b.Settings.EnableCurrencyStateManager, b.Config.CurrencyStateManager.Enabled != nil && *b.Config.CurrencyStateManager.Enabled) @@ -403,12 +402,11 @@ func (bot *Engine) Start() error { ExchangeRates: bot.Settings.EnableExchangeRates, Fixer: bot.Settings.EnableFixer, OpenExchangeRates: bot.Settings.EnableOpenExchangeRates, - ExchangeRateHost: bot.Settings.EnableExchangeRateHost, }, &bot.Config.Currency, bot.Settings.DataDir, ); err != nil { - gctlog.Errorf(gctlog.Global, "ExchangeSettings updater system failed to start %s", err) + gctlog.Errorf(gctlog.Global, "Currency Converter system failed to start %s", err) } if bot.Settings.EnableGRPC { @@ -677,7 +675,7 @@ func (bot *Engine) Stop() { err = currency.ShutdownStorageUpdater() if err != nil { - gctlog.Errorf(gctlog.Global, "ExchangeSettings storage system. Error: %v", err) + gctlog.Errorf(gctlog.Global, "Currency Converter unable to stop. Error: %v", err) } if !bot.Settings.EnableDryRun { diff --git a/engine/engine_types.go b/engine/engine_types.go index 211c7140edd..c84b940e6bb 100644 --- a/engine/engine_types.go +++ b/engine/engine_types.go @@ -80,7 +80,6 @@ type ForexSettings struct { EnableExchangeRates bool EnableFixer bool EnableOpenExchangeRates bool - EnableExchangeRateHost bool } // ExchangeTuningSettings defines settings related to an exchange diff --git a/engine/sync_manager.go b/engine/sync_manager.go index 52dd075e6d7..22db709520b 100644 --- a/engine/sync_manager.go +++ b/engine/sync_manager.go @@ -697,13 +697,15 @@ func printCurrencyFormat(price float64, displayCurrency currency.Code) string { } func printConvertCurrencyFormat(origPrice float64, origCurrency, displayCurrency currency.Code) string { - var conv float64 + var conv string if origPrice > 0 { - var err error - conv, err = currency.ConvertFiat(origPrice, origCurrency, displayCurrency) - if err != nil { - log.Errorf(log.SyncMgr, "Failed to convert currency: %s", err) + if convFloat, err := currency.ConvertFiat(origPrice, origCurrency, displayCurrency); err != nil { + conv = "?.??" + } else { + conv = fmt.Sprintf("%.2f", convFloat) } + } else { + conv = "0.00" } displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency) @@ -718,7 +720,7 @@ func printConvertCurrencyFormat(origPrice float64, origCurrency, displayCurrency err) } - return fmt.Sprintf("%s%.2f %s (%s%.2f %s)", + return fmt.Sprintf("%s%s %s (%s%.2f %s)", displaySymbol, conv, displayCurrency, @@ -755,7 +757,8 @@ func (m *SyncManager) PrintTickerSummary(result *ticker.Price, protocol string, return } - if result.Pair.Quote.IsFiatCurrency() && + if currency.ForexEnabled() && + result.Pair.Quote.IsFiatCurrency() && !result.Pair.Quote.Equal(m.fiatDisplayCurrency) && !m.fiatDisplayCurrency.IsEmpty() { origCurrency := result.Pair.Quote.Upper() @@ -854,7 +857,7 @@ func (m *SyncManager) PrintOrderbookSummary(result *orderbook.Base, protocol str var bidValueResult, askValueResult string switch { - case result.Pair.Quote.IsFiatCurrency() && !result.Pair.Quote.Equal(m.fiatDisplayCurrency) && !m.fiatDisplayCurrency.IsEmpty(): + case currency.ForexEnabled() && result.Pair.Quote.IsFiatCurrency() && !result.Pair.Quote.Equal(m.fiatDisplayCurrency) && !m.fiatDisplayCurrency.IsEmpty(): origCurrency := result.Pair.Quote.Upper() if bidsValue > 0 { bidValueResult = printConvertCurrencyFormat(bidsValue, origCurrency, m.fiatDisplayCurrency) diff --git a/main.go b/main.go index fd0900eef91..a6085d4f973 100644 --- a/main.go +++ b/main.go @@ -79,7 +79,6 @@ func main() { flag.BoolVar(&settings.EnableExchangeRates, "exchangerates", false, "overrides config and sets up foreign exchange exchangeratesapi.io") flag.BoolVar(&settings.EnableFixer, "fixer", false, "overrides config and sets up foreign exchange Fixer.io") flag.BoolVar(&settings.EnableOpenExchangeRates, "openexchangerates", false, "overrides config and sets up foreign exchange Open Exchange Rates") - flag.BoolVar(&settings.EnableExchangeRateHost, "exchangeratehost", false, "overrides config and sets up foreign exchange ExchangeRate.host") // Exchange tuning settings flag.BoolVar(&settings.EnableExchangeAutoPairUpdates, "exchangeautopairupdates", false, "enables automatic available currency pair updates for supported exchanges") From 0620ee034dcb7fb05f7ff91aac3f392e8735d097 Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Mon, 27 Nov 2023 23:38:37 +0100 Subject: [PATCH 40/40] Okx: Fix GetInsuranceFundInformation "" resp (#1405) * Okx: Fix GetInsuranceFundInformation "" resp When amount is "" this was erroring * Test: Standardise configtest.json Simple jq formatting of configtest git diff -w or github ignore whitespaces should show no changes. Any conflicts, just accept --ours and reformat it. Or not :o) --- exchanges/okx/okx_test.go | 12 +- exchanges/okx/okx_types.go | 10 +- testdata/configtest.json | 4714 ++++++++++++++++++------------------ 3 files changed, 2371 insertions(+), 2365 deletions(-) diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index 9fe7e19d46d..bacb1187d0d 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -359,12 +359,18 @@ func TestGetPublicUnderlyings(t *testing.T) { func TestGetInsuranceFundInformation(t *testing.T) { t.Parallel() - if _, err := ok.GetInsuranceFundInformation(contextGenerate(), &InsuranceFundInformationRequestParams{ + r, err := ok.GetInsuranceFundInformation(contextGenerate(), &InsuranceFundInformationRequestParams{ InstrumentType: "FUTURES", Underlying: "BTC-USDT", Limit: 2, - }); err != nil { - t.Error("Okx GetInsuranceFundInformation() error", err) + }) + assert.NoError(t, err, "GetInsuranceFundInformation should not error") + assert.Positive(t, r.Total, "Total should be positive") + assert.NotEmpty(t, r.Details, "Should have some details") + for _, d := range r.Details { + assert.Positive(t, d.Balance, "Balance should be positive") + assert.NotEmpty(t, d.Type, "Type should not be empty") + assert.Positive(t, d.Timestamp, "Timestamp should be positive") } } diff --git a/exchanges/okx/okx_types.go b/exchanges/okx/okx_types.go index 47509dcfe9a..e02bc1f37fc 100644 --- a/exchanges/okx/okx_types.go +++ b/exchanges/okx/okx_types.go @@ -505,11 +505,11 @@ type InsuranceFundInformation struct { // InsuranceFundInformationDetail represents an Insurance fund information item for a // single currency and type type InsuranceFundInformationDetail struct { - Amount float64 `json:"amt,string"` - Balance float64 `json:"balance,string"` - Currency string `json:"ccy"` - Timestamp okxUnixMilliTime `json:"ts"` - Type string `json:"type"` + Amount convert.StringToFloat64 `json:"amt"` + Balance convert.StringToFloat64 `json:"balance"` + Currency string `json:"ccy"` + Timestamp okxUnixMilliTime `json:"ts"` + Type string `json:"type"` } // SupportedCoinsData holds information about currencies supported by the trading data endpoints. diff --git a/testdata/configtest.json b/testdata/configtest.json index 2348827f1a3..fbe23f97896 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -1,422 +1,422 @@ { - "name": "Skynet", - "encryptConfig": -1, - "globalHTTPTimeout": 15000000000, - "database": { - "enabled": true, - "verbose": false, - "driver": "sqlite3", - "connectionDetails": { - "host": "", - "port": 0, - "username": "", - "password": "", - "database": "gocryptotrader.db", - "sslmode": "" - } - }, - "logging": { - "enabled": true, - "level": "INFO|DEBUG|WARN|ERROR", - "output": "console", - "fileSettings": { - "filename": "log.txt", - "rotate": false, - "maxsize": 100 - }, - "advancedSettings": { - "showLogSystemName": false, - "spacer": " | ", - "timeStampFormat": " 02/01/2006 15:04:05 ", - "headers": { - "info": "[INFO]", - "warn": "[WARN]", - "debug": "[DEBUG]", - "error": "[ERROR]" - } - } - }, - "connectionMonitor": { - "preferredDNSList": [ - "8.8.8.8", - "8.8.4.4", - "1.1.1.1", - "1.0.0.1" - ], - "preferredDomainList": [ - "www.google.com", - "www.cloudflare.com", - "www.facebook.com" - ], - "checkInterval": 1000000000 - }, - "profiler": { - "enabled": false, - "mutex_profile_fraction": 0 - }, - "ntpclient": { - "enabled": 0, - "pool": [ - "0.pool.ntp.org:123", - "pool.ntp.org:123" - ], - "allowedDifference": 50000000, - "allowedNegativeDifference": 50000000 - }, - "gctscript": { - "enabled": false, - "timeout": 30000000000, - "max_virtual_machines": 10, - "allow_imports": false, - "auto_load": null, - "verbose": false - }, - "currencyConfig": { - "forexProviders": [ - { - "name": "CurrencyConverter", - "enabled": false, - "verbose": false, - "restPollingDelay": 600, - "apiKey": "Key", - "apiKeyLvl": -1, - "primaryProvider": false - }, - { - "name": "CurrencyLayer", - "enabled": false, - "verbose": false, - "restPollingDelay": 600, - "apiKey": "Key", - "apiKeyLvl": -1, - "primaryProvider": false - }, - { - "name": "Fixer", - "enabled": false, - "verbose": false, - "restPollingDelay": 600, - "apiKey": "Key", - "apiKeyLvl": -1, - "primaryProvider": false - }, - { - "name": "OpenExchangeRates", - "enabled": false, - "verbose": false, - "restPollingDelay": 600, - "apiKey": "Key", - "apiKeyLvl": -1, - "primaryProvider": false - }, - { - "name": "ExchangeRates", + "name": "Skynet", + "encryptConfig": -1, + "globalHTTPTimeout": 15000000000, + "database": { "enabled": true, "verbose": false, - "restPollingDelay": 600, - "apiKey": "Key", - "apiKeyLvl": -1, - "primaryProvider": true - } - ], - "cryptocurrencyProvider": { - "name": "CoinMarketCap", - "enabled": false, - "verbose": false, - "apiKey": "Key", - "accountPlan": "accountPlan" - }, - "cryptocurrencies": "BTC,LTC,ETH,DOGE,DASH,XRP,XMR", - "currencyPairFormat": { - "uppercase": true, - "delimiter": "-" - }, - "fiatDisplayCurrency": "USD", - "currencyFileUpdateDuration": 0, - "foreignExchangeUpdateDuration": 0 - }, - "communications": { - "slack": { - "name": "Slack", - "enabled": false, - "verbose": false, - "targetChannel": "general", - "verificationToken": "testtest" + "driver": "sqlite3", + "connectionDetails": { + "host": "", + "port": 0, + "username": "", + "password": "", + "database": "gocryptotrader.db", + "sslmode": "" + } }, - "smsGlobal": { - "name": "SMSGlobal", - "from": "Skynet", - "enabled": true, - "verbose": false, - "username": "1234", - "password": "12334", - "contacts": [ - { - "name": "StyleGherkin", - "number": "1231424", - "enabled": true + "logging": { + "enabled": true, + "level": "INFO|DEBUG|WARN|ERROR", + "output": "console", + "fileSettings": { + "filename": "log.txt", + "rotate": false, + "maxsize": 100 + }, + "advancedSettings": { + "showLogSystemName": false, + "spacer": " | ", + "timeStampFormat": " 02/01/2006 15:04:05 ", + "headers": { + "info": "[INFO]", + "warn": "[WARN]", + "debug": "[DEBUG]", + "error": "[ERROR]" + } } - ] }, - "smtp": { - "name": "SMTP", - "enabled": false, - "verbose": false, - "host": "smtp.google.com", - "port": "537", - "accountName": "some", - "accountPassword": "password", - "from": "", - "recipientList": "lol123@gmail.com" + "connectionMonitor": { + "preferredDNSList": [ + "8.8.8.8", + "8.8.4.4", + "1.1.1.1", + "1.0.0.1" + ], + "preferredDomainList": [ + "www.google.com", + "www.cloudflare.com", + "www.facebook.com" + ], + "checkInterval": 1000000000 }, - "telegram": { - "name": "Telegram", - "enabled": false, - "verbose": false, - "verificationToken": "testest", - "authorisedClients": { - "user_example": 0 - } - } - }, - "remoteControl": { - "username": "admin", - "password": "Password", - "gRPC": { - "enabled": true, - "listenAddress": "localhost:9052", - "grpcProxyEnabled": true, - "grpcProxyListenAddress": "localhost:9053" + "profiler": { + "enabled": false, + "mutex_profile_fraction": 0 }, - "deprecatedRPC": { - "enabled": true, - "listenAddress": "localhost:9050" + "ntpclient": { + "enabled": 0, + "pool": [ + "0.pool.ntp.org:123", + "pool.ntp.org:123" + ], + "allowedDifference": 50000000, + "allowedNegativeDifference": 50000000 }, - "websocketRPC": { - "enabled": true, - "listenAddress": "localhost:9051", - "connectionLimit": 1, - "maxAuthFailures": 3, - "allowInsecureOrigin": true - } - }, - "portfolioAddresses": { - "addresses": [ - { - "Address": "1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy", - "CoinType": "BTC", - "Balance": 53000.01741264, - "Description": "", - "WhiteListed": false, - "ColdStorage": false, - "SupportedExchanges": "" - }, - { - "Address": "3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v", - "CoinType": "BTC", - "Balance": 107848.28963408, - "Description": "", - "WhiteListed": false, - "ColdStorage": false, - "SupportedExchanges": "" - }, - { - "Address": "LgY8ahfHRhvjVQC1zJnBhFMG5pCTMuKRqh", - "CoinType": "LTC", - "Balance": 0.03665026, - "Description": "", - "WhiteListed": false, - "ColdStorage": false, - "SupportedExchanges": "" - }, - { - "Address": "0xb794f5ea0ba39494ce839613fffba74279579268", - "CoinType": "ETH", - "Balance": 0.25555604051326, - "Description": "", - "WhiteListed": false, - "ColdStorage": false, - "SupportedExchanges": "" - } - ] - }, - "exchanges": [ - { - "name": "Okx", + "gctscript": { + "enabled": false, + "timeout": 30000000000, + "max_virtual_machines": 10, + "allow_imports": false, + "auto_load": null, + "verbose": false + }, + "currencyConfig": { + "forexProviders": [ + { + "name": "CurrencyConverter", + "enabled": false, + "verbose": false, + "restPollingDelay": 600, + "apiKey": "Key", + "apiKeyLvl": -1, + "primaryProvider": false + }, + { + "name": "CurrencyLayer", + "enabled": false, + "verbose": false, + "restPollingDelay": 600, + "apiKey": "Key", + "apiKeyLvl": -1, + "primaryProvider": false + }, + { + "name": "Fixer", + "enabled": false, + "verbose": false, + "restPollingDelay": 600, + "apiKey": "Key", + "apiKeyLvl": -1, + "primaryProvider": false + }, + { + "name": "OpenExchangeRates", + "enabled": false, + "verbose": false, + "restPollingDelay": 600, + "apiKey": "Key", + "apiKeyLvl": -1, + "primaryProvider": false + }, + { + "name": "ExchangeRates", "enabled": true, "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "baseCurrencies": "USD", - "currencyPairs": { - "bypassConfigFormatUpgrades": false, - "requestFormat": { - "uppercase": true, - "delimiter":"-" - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "assetTypes": [ - "futures", - "margin", - "option", - "perpetualswap", - "spot" - ], - "pairs": { - "futures": { - "assetEnabled": true, - "enabled": "BTC-USD-221007,BTC-USD-221014", - "available": "BTC-USD-221230,BTC-USD-230331,ETH-USD-221007,ETH-USD-221014,ETH-USD-221230,ETH-USD-230331,LTC-USD-221007,LTC-USD-221014,LTC-USD-221230,LTC-USD-230331,DOT-USD-221007,DOT-USD-221014,DOT-USD-221230,DOT-USD-230331,FIL-USD-221007,FIL-USD-221014,FIL-USD-221230,FIL-USD-230331,ADA-USD-221007,ADA-USD-221014,ADA-USD-221230,ADA-USD-230331,AVAX-USD-221007,AVAX-USD-221014,AVAX-USD-221230,BTC-USD-221007,BTC-USD-221014,BTC-USD-221230,BTC-USD-230331,ETH-USD-221007,ETH-USD-221014,ETH-USD-221230,ETH-USD-230331,LTC-USD-221007,LTC-USD-221014,LTC-USD-221230,LTC-USD-230331,DOT-USD-221007,DOT-USD-221014,DOT-USD-221230,DOT-USD-230331,FIL-USD-221007,FIL-USD-221014,FIL-USD-221230,FIL-USD-230331,ADA-USD-221007,ADA-USD-221014,ADA-USD-221230,ADA-USD-230331,AVAX-USD-221007,AVAX-USD-221014,AVAX-USD-221230,AVAX-USD-230331,BCH-USD-221007,BCH-USD-221014,BCH-USD-221230,BCH-USD-230331,EOS-USD-221007,EOS-USD-221014,EOS-USD-221230,EOS-USD-230331,ETC-USD-221007,ETC-USD-221014,ETC-USD-221230,ETC-USD-230331,LINK-USD-221007,LINK-USD-221014,LINK-USD-221230,LINK-USD-230331,SOL-USD-221007,SOL-USD-221014,SOL-USD-221230,SOL-USD-230331,TRX-USD-221007,TRX-USD-221014,TRX-USD-221230,TRX-USD-230331,XRP-USD-221007,XRP-USD-221014,XRP-USD-221230,XRP-USD-230331,BTC-USDT-221007,BTC-USDT-221014,BTC-USDT-221230,BTC-USDT-230331,ETH-USDT-221007,ETH-USDT-221014,ETH-USDT-221230,ETH-USDT-230331,LTC-USDT-221007,LTC-USDT-221014,LTC-USDT-221230,LTC-USDT-230331,DOT-USDT-221007,DOT-USDT-221014,DOT-USDT-221230,DOT-USDT-230331,FIL-USDT-221007,FIL-USDT-221014,FIL-USDT-221230,FIL-USDT-230331,ADA-USDT-221007,ADA-USDT-221014,ADA-USDT-221230,ADA-USDT-230331,BCH-USDT-221007,BCH-USDT-221014,BCH-USDT-221230,BCH-USDT-230331,EOS-USDT-221007,EOS-USDT-221014,EOS-USDT-221230,EOS-USDT-230331,ETC-USDT-221007,ETC-USDT-221014,ETC-USDT-221230,ETC-USDT-230331,LINK-USDT-221007,LINK-USDT-221014,LINK-USDT-221230,LINK-USDT-230331,TRX-USDT-221007,TRX-USDT-221014,TRX-USDT-221230,TRX-USDT-230331,XRP-USDT-221007,XRP-USDT-221014,XRP-USDT-221230,XRP-USDT-230331" - }, - "margin": { - "assetEnabled": true, - "enabled": "BTC-USDT,ETH-USDT,OKB-USDT", - "available": "LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,1INCH-USDT,AAVE-USDT,ADA-USDT,AGLD-USDT,AKITA-USDT,ALGO-USDT,ALPHA-USDT,BTC-USDT,ETH-USDT,OKB-USDT,LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,1INCH-USDT,AAVE-USDT,ADA-USDT,AGLD-USDT,AKITA-USDT,ALGO-USDT,ALPHA-USDT,ANT-USDT,APE-USDT,API3-USDT,ASTR-USDT,ATOM-USDT,AVAX-USDT,AXS-USDT,BABYDOGE-USDT,BADGER-USDT,BAL-USDT,BAND-USDT,BAT-USDT,BCH-USDT,BICO-USDT,BNT-USDT,BSV-USDT,BTM-USDT,BTT-USDT,BZZ-USDT,CELO-USDT,CEL-USDT,CELR-USDT,CFX-USDT,CHZ-USDT,CLV-USDT,COMP-USDT,CONV-USDT,CQT-USDT,CRO-USDT,CRV-USDT,CSPR-USDT,CVC-USDT,DASH-USDT,DOME-USDT,DORA-USDT,DYDX-USDT,EFI-USDT,EGLD-USDT,ELF-USDT,ENJ-USDT,ENS-USDT,EOS-USDT,ETC-USDT,FITFI-USDT,FLM-USDT,FLOW-USDT,FTM-USDT,GALA-USDT,GLMR-USDT,GMT-USDT,GODS-USDT,GRT-USDT,HBAR-USDT,HC-USDT,ICP-USDT,IMX-USDT,IOST-USDT,IOTA-USDT,JST-USDT,KAR-USDT,KISHU-USDT,KNC-USDT,KSM-USDT,LAMB-USDT,LAT-USDT,LINK-USDT,LON-USDT,LOOKS-USDT,LPT-USDT,LRC-USDT,MANA-USDT,MASK-USDT,MATIC-USDT,MINA-USDT,MKR-USDT,NAS-USDT,NEAR-USDT,NEO-USDT,NFT-USDT,NYM-USDT,OMG-USDT,ONT-USDT,OP-USDT,PEOPLE-USDT,PERP-USDT,QTUM-USDT,REN-USDT,RSR-USDT,RSS3-USDT,RVN-USDT,SAND-USDT,SC-USDT,SHIB-USDT,SKL-USDT,SLP-USDT,SNT-USDT,SNX-USDT,SOL-USDT,SOS-USDT,SRM-USDT,STARL-USDT,STORJ-USDT,SUSHI-USDT,SWEAT-USDT,SWRV-USDT,THETA-USDT,TORN-USDT,TRB-USDT,TRX-USDT,UMA-USDT,UMEE-USDT,UNI-USDT,USDC-USDT,VSYS-USDT,WAVES-USDT,WNCG-USDT,WNXM-USDT,XCH-USDT,XEM-USDT,XLM-USDT,XMR-USDT,XTZ-USDT,YFI-USDT,YFII-USDT,YGG-USDT,ZEC-USDT,ZEN-USDT,ZIL-USDT,ZRX-USDT,BTC-USDC,ETH-BTC,LTC-USDC,DOT-USDC,DOGE-USDC,LUNC-USDC,LUNA-USDC,XRP-USDC,ADA-USDC,ATOM-USDC,AVAX-USDC,NEAR-USDC,OP-USDC,SOL-USDC,OKB-BTC,LTC-BTC,DOT-BTC,DOGE-BTC,FIL-BTC,XRP-BTC,AAVE-BTC,ADA-BTC,ALGO-BTC,ANT-BTC,ATOM-BTC,AVAX-BTC,BADGER-BTC,BAT-BTC,BCH-BTC,BNT-BTC,BSV-BTC,BTM-BTC,CHZ-BTC,COMP-BTC,CRO-BTC,CRV-BTC,CVC-BTC,DASH-BTC,EGLD-BTC,ELF-BTC,ENJ-BTC,EOS-BTC,ETC-BTC,GRT-BTC,HBAR-BTC,HC-BTC,ICP-BTC,IOST-BTC,IOTA-BTC,KNC-BTC,KSM-BTC,LINK-BTC,LRC-BTC,MANA-BTC,MKR-BTC,NAS-BTC,NEAR-BTC,NEO-BTC,OMG-BTC,ONT-BTC,QTUM-BTC,REN-BTC,RSR-BTC,RVN-BTC,SNT-BTC,SOL-BTC,SRM-BTC,THETA-BTC,TRX-BTC,UNI-BTC,VSYS-BTC,WAVES-BTC,XCH-BTC,XEM-BTC,XLM-BTC,XMR-BTC,XTZ-BTC,ZEC-BTC,ZIL-BTC,ZRX-BTC" - }, - "option": { - "assetEnabled": true, - "enabled": "BTC-USD-220930-28000-P,BTC-USD-220930-30000-C", - "available": "BTC-USD-220930-30000-P,BTC-USD-220930-32000-C,BTC-USD-220930-32000-P,BTC-USD-220930-34000-C,BTC-USD-220930-34000-P,BTC-USD-220930-35000-C,BTC-USD-220930-35000-P,BTC-USD-220930-36000-C,BTC-USD-220930-36000-P,BTC-USD-220930-40000-C,BTC-USD-220930-40000-P,BTC-USD-220930-45000-C,BTC-USD-220930-45000-P,BTC-USD-220930-50000-C,BTC-USD-220930-50000-P,BTC-USD-220930-55000-C,BTC-USD-220930-55000-P,BTC-USD-220930-60000-C,BTC-USD-220930-60000-P,BTC-USD-220930-65000-C,BTC-USD-220930-65000-P,BTC-USD-220930-70000-C,BTC-USD-220930-70000-P,BTC-USD-220930-80000-C,BTC-USD-220930-80000-P,BTC-USD-220930-90000-C,BTC-USD-220930-90000-P,BTC-USD-220930-100000-C,BTC-USD-220930-100000-P,BTC-USD-220930-120000-C,BTC-USD-220930-120000-P,BTC-USD-221007-13000-C,BTC-USD-221007-13000-P,BTC-USD-221007-14000-C,BTC-USD-221007-14000-P,BTC-USD-221007-15000-C,BTC-USD-221007-15000-P,BTC-USD-221007-16000-C,BTC-USD-221007-16000-P,BTC-USD-221007-17000-C,BTC-USD-221007-17000-P,BTC-USD-221007-18000-C,BTC-USD-221007-18000-P,BTC-USD-221007-18500-C,BTC-USD-221007-18500-P,BTC-USD-221007-19000-C,BTC-USD-221007-19000-P,BTC-USD-221007-20000-C,BTC-USD-221007-20000-P,BTC-USD-221007-21000-C,BTC-USD-221007-21000-P,BTC-USD-221007-22000-C,BTC-USD-221007-22000-P,BTC-USD-221007-23000-C,BTC-USD-221007-23000-P,BTC-USD-221007-24000-C,BTC-USD-221007-24000-P,BTC-USD-221007-26000-C,BTC-USD-221007-26000-P,BTC-USD-221007-28000-C,BTC-USD-221007-28000-P,BTC-USD-221007-30000-C,BTC-USD-221007-30000-P,BTC-USD-221014-14000-C,BTC-USD-221014-14000-P,BTC-USD-221014-15000-C,BTC-USD-221014-15000-P,BTC-USD-221014-16000-C,BTC-USD-221014-16000-P,BTC-USD-221014-17000-C,BTC-USD-221014-17000-P,BTC-USD-221014-18000-C,BTC-USD-221014-18000-P,BTC-USD-221014-19000-C,BTC-USD-221014-19000-P,BTC-USD-221014-20000-C,BTC-USD-221014-20000-P,BTC-USD-221014-21000-C,BTC-USD-221014-21000-P,BTC-USD-221014-22000-C,BTC-USD-221014-22000-P,BTC-USD-221014-23000-C,BTC-USD-221014-23000-P,BTC-USD-221014-24000-C,BTC-USD-221014-24000-P,BTC-USD-221014-26000-C,BTC-USD-221014-26000-P,BTC-USD-221014-28000-C,BTC-USD-221014-28000-P,BTC-USD-221014-30000-C,BTC-USD-221014-30000-P,BTC-USD-221028-10000-C,BTC-USD-221028-10000-P,BTC-USD-221028-12000-C,BTC-USD-221028-12000-P,BTC-USD-221028-14000-C,BTC-USD-221028-14000-P,BTC-USD-221028-16000-C,BTC-USD-221028-16000-P,BTC-USD-221028-17000-C,BTC-USD-221028-17000-P,BTC-USD-221028-18000-C,BTC-USD-221028-18000-P,BTC-USD-221028-19000-C,BTC-USD-221028-19000-P,BTC-USD-221028-20000-C,BTC-USD-221028-20000-P,BTC-USD-221028-21000-C,BTC-USD-221028-21000-P,BTC-USD-221028-22000-C,BTC-USD-221028-22000-P,BTC-USD-221028-24000-C,BTC-USD-221028-24000-P,BTC-USD-221028-26000-C,BTC-USD-221028-26000-P,BTC-USD-221028-28000-C,BTC-USD-221028-28000-P,BTC-USD-221028-30000-C,BTC-USD-221028-30000-P,BTC-USD-221028-32000-C,BTC-USD-221028-32000-P,BTC-USD-221028-35000-C,BTC-USD-220927-17000-C,BTC-USD-220927-17000-P,BTC-USD-220927-17500-C,BTC-USD-220927-17500-P,BTC-USD-220927-18000-C,BTC-USD-220927-18000-P,BTC-USD-220927-18200-C,BTC-USD-220927-18200-P,BTC-USD-220927-18400-C,BTC-USD-220927-18400-P,BTC-USD-220927-18600-C,BTC-USD-220927-18600-P,BTC-USD-220927-18800-C,BTC-USD-220927-18800-P,BTC-USD-220927-19000-C,BTC-USD-220927-19000-P,BTC-USD-220927-19200-C,BTC-USD-220927-19200-P,BTC-USD-220927-19400-C,BTC-USD-220927-19400-P,BTC-USD-220927-19600-C,BTC-USD-220927-19600-P,BTC-USD-220927-19800-C,BTC-USD-220927-19800-P,BTC-USD-220927-20000-C,BTC-USD-220927-20000-P,BTC-USD-220927-20500-C,BTC-USD-220927-20500-P,BTC-USD-220927-21000-C,BTC-USD-220927-21000-P,BTC-USD-220927-21500-C,BTC-USD-220927-21500-P,BTC-USD-220928-16500-C,BTC-USD-220928-16500-P,BTC-USD-220928-17000-C,BTC-USD-220928-17000-P,BTC-USD-220928-17500-C,BTC-USD-220928-17500-P,BTC-USD-220928-18000-C,BTC-USD-220928-18000-P,BTC-USD-220928-18200-C,BTC-USD-220928-18200-P,BTC-USD-220928-18400-C,BTC-USD-220928-18400-P,BTC-USD-220928-18600-C,BTC-USD-220928-18600-P,BTC-USD-220928-18800-C,BTC-USD-220928-18800-P,BTC-USD-220928-19000-C,BTC-USD-220928-19000-P,BTC-USD-220928-19200-C,BTC-USD-220928-19200-P,BTC-USD-220928-19400-C,BTC-USD-220928-19400-P,BTC-USD-220928-19600-C,BTC-USD-220928-19600-P,BTC-USD-220928-20000-C,BTC-USD-220928-20000-P,BTC-USD-220928-20500-C,BTC-USD-220928-20500-P,BTC-USD-220928-21000-C,BTC-USD-220928-21000-P,BTC-USD-220928-21500-C,BTC-USD-220928-21500-P,BTC-USD-220930-5000-C,BTC-USD-220930-5000-P,BTC-USD-220930-10000-C,BTC-USD-220930-10000-P,BTC-USD-220930-12000-C,BTC-USD-220930-12000-P,BTC-USD-220930-15000-C,BTC-USD-220930-15000-P,BTC-USD-220930-16000-C,BTC-USD-220930-16000-P,BTC-USD-220930-17000-C,BTC-USD-220930-17000-P,BTC-USD-220930-17500-C,BTC-USD-220930-17500-P,BTC-USD-220930-18000-C,BTC-USD-220930-18000-P,BTC-USD-220930-18500-C,BTC-USD-220930-18500-P,BTC-USD-220930-19000-C,BTC-USD-220930-19000-P,BTC-USD-220930-19500-C,BTC-USD-220930-19500-P,BTC-USD-220930-20000-C,BTC-USD-220930-20000-P,BTC-USD-220930-20500-C,BTC-USD-220930-20500-P,BTC-USD-220930-21000-C,BTC-USD-220930-21000-P,BTC-USD-220930-22000-C,BTC-USD-220930-22000-P,BTC-USD-220930-23000-C,BTC-USD-220930-23000-P,BTC-USD-220930-24000-C,BTC-USD-220930-24000-P,BTC-USD-220930-25000-C,BTC-USD-220930-25000-P,BTC-USD-220930-26000-C,BTC-USD-220930-26000-P,BTC-USD-220930-28000-C,BTC-USD-220930-28000-P,BTC-USD-220930-30000-C,BTC-USD-220930-30000-P,BTC-USD-220930-32000-C,BTC-USD-220930-32000-P,BTC-USD-220930-34000-C,BTC-USD-220930-34000-P,BTC-USD-220930-35000-C,BTC-USD-220930-35000-P,BTC-USD-220930-36000-C,BTC-USD-220930-36000-P,BTC-USD-220930-40000-C,BTC-USD-220930-40000-P,BTC-USD-220930-45000-C,BTC-USD-220930-45000-P,BTC-USD-220930-50000-C,BTC-USD-220930-50000-P,BTC-USD-220930-55000-C,BTC-USD-220930-55000-P,BTC-USD-220930-60000-C,BTC-USD-220930-60000-P,BTC-USD-220930-65000-C,BTC-USD-220930-65000-P,BTC-USD-220930-70000-C,BTC-USD-220930-70000-P,BTC-USD-220930-80000-C,BTC-USD-220930-80000-P,BTC-USD-220930-90000-C,BTC-USD-220930-90000-P,BTC-USD-220930-100000-C,BTC-USD-220930-100000-P,BTC-USD-220930-120000-C,BTC-USD-220930-120000-P,BTC-USD-221007-13000-C,BTC-USD-221007-13000-P,BTC-USD-221007-14000-C,BTC-USD-221007-14000-P,BTC-USD-221007-15000-C,BTC-USD-221007-15000-P,BTC-USD-221007-16000-C,BTC-USD-221007-16000-P,BTC-USD-221007-17000-C,BTC-USD-221007-17000-P,BTC-USD-221007-18000-C,BTC-USD-221007-18000-P,BTC-USD-221007-18500-C,BTC-USD-221007-18500-P,BTC-USD-221007-19000-C,BTC-USD-221007-19000-P,BTC-USD-221007-20000-C,BTC-USD-221007-20000-P,BTC-USD-221007-21000-C,BTC-USD-221007-21000-P,BTC-USD-221007-22000-C,BTC-USD-221007-22000-P,BTC-USD-221007-23000-C,BTC-USD-221007-23000-P,BTC-USD-221007-24000-C,BTC-USD-221007-24000-P,BTC-USD-221007-26000-C,BTC-USD-221007-26000-P,BTC-USD-221007-28000-C,BTC-USD-221007-28000-P,BTC-USD-221007-30000-C,BTC-USD-221007-30000-P,BTC-USD-221014-14000-C,BTC-USD-221014-14000-P,BTC-USD-221014-15000-C,BTC-USD-221014-15000-P,BTC-USD-221014-16000-C,BTC-USD-221014-16000-P,BTC-USD-221014-17000-C,BTC-USD-221014-17000-P,BTC-USD-221014-18000-C,BTC-USD-221014-18000-P,BTC-USD-221014-19000-C,BTC-USD-221014-19000-P,BTC-USD-221014-20000-C,BTC-USD-221014-20000-P,BTC-USD-221014-21000-C,BTC-USD-221014-21000-P,BTC-USD-221014-22000-C,BTC-USD-221014-22000-P,BTC-USD-221014-23000-C,BTC-USD-221014-23000-P,BTC-USD-221014-24000-C,BTC-USD-221014-24000-P,BTC-USD-221014-26000-C,BTC-USD-221014-26000-P,BTC-USD-221014-28000-C,BTC-USD-221014-28000-P,BTC-USD-221014-30000-C,BTC-USD-221014-30000-P,BTC-USD-221028-10000-C,BTC-USD-221028-10000-P,BTC-USD-221028-12000-C,BTC-USD-221028-12000-P,BTC-USD-221028-14000-C,BTC-USD-221028-14000-P,BTC-USD-221028-16000-C,BTC-USD-221028-16000-P,BTC-USD-221028-17000-C,BTC-USD-221028-17000-P,BTC-USD-221028-18000-C,BTC-USD-221028-18000-P,BTC-USD-221028-19000-C,BTC-USD-221028-19000-P,BTC-USD-221028-20000-C,BTC-USD-221028-20000-P,BTC-USD-221028-21000-C,BTC-USD-221028-21000-P,BTC-USD-221028-22000-C,BTC-USD-221028-22000-P,BTC-USD-221028-24000-C,BTC-USD-221028-24000-P,BTC-USD-221028-26000-C,BTC-USD-221028-26000-P,BTC-USD-221028-28000-C,BTC-USD-221028-28000-P,BTC-USD-221028-30000-C,BTC-USD-221028-30000-P,BTC-USD-221028-32000-C,BTC-USD-221028-32000-P,BTC-USD-221028-35000-C,BTC-USD-221028-35000-P,BTC-USD-221028-40000-C,BTC-USD-221028-40000-P,BTC-USD-221028-50000-C,BTC-USD-221028-50000-P,BTC-USD-221028-60000-C,BTC-USD-221028-60000-P,BTC-USD-221028-70000-C,BTC-USD-221028-70000-P,BTC-USD-221125-5000-C,BTC-USD-221125-5000-P,BTC-USD-221125-10000-C,BTC-USD-221125-10000-P,BTC-USD-221125-12000-C,BTC-USD-221125-12000-P,BTC-USD-221125-15000-C,BTC-USD-221125-15000-P,BTC-USD-221125-16000-C,BTC-USD-221125-16000-P,BTC-USD-221125-17000-C,BTC-USD-221125-17000-P,BTC-USD-221125-18000-C,BTC-USD-221125-18000-P,BTC-USD-221125-20000-C,BTC-USD-221125-20000-P,BTC-USD-221125-22000-C,BTC-USD-221125-22000-P,BTC-USD-221125-24000-C,BTC-USD-221125-24000-P,BTC-USD-221125-26000-C,BTC-USD-221125-26000-P,BTC-USD-221125-28000-C,BTC-USD-221125-28000-P,BTC-USD-221125-30000-C,BTC-USD-221125-30000-P,BTC-USD-221125-32000-C,BTC-USD-221125-32000-P,BTC-USD-221125-35000-C,BTC-USD-221125-35000-P,BTC-USD-221125-40000-C,BTC-USD-221125-40000-P,BTC-USD-221125-50000-C,BTC-USD-221125-50000-P,BTC-USD-221125-60000-C,BTC-USD-221125-60000-P,BTC-USD-221125-70000-C,BTC-USD-221125-70000-P,BTC-USD-221230-5000-C,BTC-USD-221230-5000-P,BTC-USD-221230-10000-C,BTC-USD-221230-10000-P,BTC-USD-221230-12000-C,BTC-USD-221230-12000-P,BTC-USD-221230-13000-C,BTC-USD-221230-13000-P,BTC-USD-221230-15000-C,BTC-USD-221230-15000-P,BTC-USD-221230-16000-C,BTC-USD-221230-16000-P,BTC-USD-221230-17000-C,BTC-USD-221230-17000-P,BTC-USD-221230-18000-C,BTC-USD-221230-18000-P,BTC-USD-221230-19000-C,BTC-USD-221230-19000-P,BTC-USD-221230-20000-C,BTC-USD-221230-20000-P,BTC-USD-221230-21000-C,BTC-USD-221230-21000-P,BTC-USD-221230-22000-C,BTC-USD-221230-22000-P,BTC-USD-221230-23000-C,BTC-USD-221230-23000-P,BTC-USD-221230-24000-C,BTC-USD-221230-24000-P,BTC-USD-221230-25000-C,BTC-USD-221230-25000-P,BTC-USD-221230-26000-C,BTC-USD-221230-26000-P,BTC-USD-221230-28000-C,BTC-USD-221230-28000-P,BTC-USD-221230-30000-C,BTC-USD-221230-30000-P,BTC-USD-221230-32000-C,BTC-USD-221230-32000-P,BTC-USD-221230-35000-C,BTC-USD-221230-35000-P" - }, - "perpetualswap": { - "assetEnabled": true, - "enabled": "BTC-USD-SWAP,ETH-USD-SWAP", - "available": "LTC-USD-SWAP,DOT-USD-SWAP,DOGE-USD-SWAP,MASK-USDT-SWAP,MATIC-USDT-SWAP,MINA-USDT-SWAP,MKR-USDT-SWAP,NEAR-USDT-SWAP,NEO-USDT-SWAP,NFT-USDT-SWAP,NYM-USDT-SWAP,OMG-USDT-SWAP,ONT-USDT-SWAP,OP-USDT-SWAP,PEOPLE-USDT-SWAP,BTC-USD-SWAP,ETH-USD-SWAP,LTC-USD-SWAP,DOT-USD-SWAP,DOGE-USD-SWAP,FIL-USD-SWAP,XRP-USD-SWAP,1INCH-USD-SWAP,ADA-USD-SWAP,ALGO-USD-SWAP,ATOM-USD-SWAP,AVAX-USD-SWAP,BCH-USD-SWAP,BSV-USD-SWAP,CRV-USD-SWAP,DASH-USD-SWAP,EOS-USD-SWAP,ETC-USD-SWAP,GRT-USD-SWAP,IOST-USD-SWAP,IOTA-USD-SWAP,KNC-USD-SWAP,KSM-USD-SWAP,LINK-USD-SWAP,MANA-USD-SWAP,NEO-USD-SWAP,ONT-USD-SWAP,QTUM-USD-SWAP,SAND-USD-SWAP,SOL-USD-SWAP,SUSHI-USD-SWAP,THETA-USD-SWAP,TRX-USD-SWAP,UNI-USD-SWAP,XLM-USD-SWAP,XMR-USD-SWAP,XTZ-USD-SWAP,YFI-USD-SWAP,YFII-USD-SWAP,ZEC-USD-SWAP,BTC-USDT-SWAP,ETH-USDT-SWAP,LTC-USDT-SWAP,DOT-USDT-SWAP,DOGE-USDT-SWAP,LUNC-USDT-SWAP,ETHW-USDT-SWAP,LUNA-USDT-SWAP,FIL-USDT-SWAP,XRP-USDT-SWAP,1INCH-USDT-SWAP,AAVE-USDT-SWAP,ADA-USDT-SWAP,AGLD-USDT-SWAP,ALGO-USDT-SWAP,ALPHA-USDT-SWAP,ANT-USDT-SWAP,APE-USDT-SWAP,API3-USDT-SWAP,ASTR-USDT-SWAP,ATOM-USDT-SWAP,AVAX-USDT-SWAP,AXS-USDT-SWAP,BABYDOGE-USDT-SWAP,BADGER-USDT-SWAP,BAL-USDT-SWAP,BAND-USDT-SWAP,BAT-USDT-SWAP,BCH-USDT-SWAP,BICO-USDT-SWAP,BNT-USDT-SWAP,BSV-USDT-SWAP,BTT-USDT-SWAP,CELO-USDT-SWAP,CEL-USDT-SWAP,CFX-USDT-SWAP,CHZ-USDT-SWAP,COMP-USDT-SWAP,CRO-USDT-SWAP,CRV-USDT-SWAP,CSPR-USDT-SWAP,CVC-USDT-SWAP,DASH-USDT-SWAP,DOME-USDT-SWAP,DORA-USDT-SWAP,DYDX-USDT-SWAP,EGLD-USDT-SWAP,ELON-USDT-SWAP,ENJ-USDT-SWAP,ENS-USDT-SWAP,EOS-USDT-SWAP,ETC-USDT-SWAP,FITFI-USDT-SWAP,FLM-USDT-SWAP,FTM-USDT-SWAP,GALA-USDT-SWAP,GMT-USDT-SWAP,GODS-USDT-SWAP,GRT-USDT-SWAP,ICP-USDT-SWAP,IMX-USDT-SWAP,IOST-USDT-SWAP,IOTA-USDT-SWAP,JST-USDT-SWAP,KISHU-USDT-SWAP,KNC-USDT-SWAP,KSM-USDT-SWAP,LINK-USDT-SWAP,LOOKS-USDT-SWAP,LPT-USDT-SWAP,LRC-USDT-SWAP,MANA-USDT-SWAP,MASK-USDT-SWAP,MATIC-USDT-SWAP,MINA-USDT-SWAP,MKR-USDT-SWAP,NEAR-USDT-SWAP,NEO-USDT-SWAP,NFT-USDT-SWAP,NYM-USDT-SWAP,OMG-USDT-SWAP,ONT-USDT-SWAP,OP-USDT-SWAP,PEOPLE-USDT-SWAP,PERP-USDT-SWAP,QTUM-USDT-SWAP,REN-USDT-SWAP,RSR-USDT-SWAP,RVN-USDT-SWAP,SAND-USDT-SWAP,SC-USDT-SWAP,SHIB-USDT-SWAP,SLP-USDT-SWAP,SNX-USDT-SWAP,SOL-USDT-SWAP,SOS-USDT-SWAP,SRM-USDT-SWAP,STARL-USDT-SWAP,STORJ-USDT-SWAP,SUSHI-USDT-SWAP,SWEAT-USDT-SWAP,THETA-USDT-SWAP,TRB-USDT-SWAP,TRX-USDT-SWAP,UMA-USDT-SWAP,UMEE-USDT-SWAP,UNI-USDT-SWAP,WAVES-USDT-SWAP,WNXM-USDT-SWAP,XCH-USDT-SWAP,XEM-USDT-SWAP,XLM-USDT-SWAP,XMR-USDT-SWAP,XTZ-USDT-SWAP,YFI-USDT-SWAP,YFII-USDT-SWAP,YGG-USDT-SWAP,ZEC-USDT-SWAP,ZEN-USDT-SWAP,ZIL-USDT-SWAP,ZRX-USDT-SWAP" - }, - "spot": { - "assetEnabled": true, - "enabled": "BTC-USDT,ETH-USDT,OKB-USDT", - "available": "OKT-USDT,LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,CITY-USDT,MENGO-USDT,ARG-USDT,POR-USDT,1INCH-USDT,AAVE-USDT,ABT-USDT,ACA-USDT,ADA-USDT,AERGO-USDT,AGLD-USDT,XMR-ETH,YFI-ETH,ZEC-ETH,LTC-OKB,XRP-OKB,ETC-OKB,BTC-USDT,ETH-USDT,OKB-USDT,OKT-USDT,LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,CITY-USDT,MENGO-USDT,ARG-USDT,POR-USDT,1INCH-USDT,AAVE-USDT,ABT-USDT,ACA-USDT,ADA-USDT,AERGO-USDT,AGLD-USDT,AKITA-USDT,ALCX-USDT,ALGO-USDT,ALPHA-USDT,ANC-USDT,ANT-USDT,ANW-USDT,APE-USDT,APIX-USDT,API3-USDT,APM-USDT,AR-USDT,ARK-USDT,AST-USDT,ASTR-USDT,ATOM-USDT,AUCTION-USDT,AVAX-USDT,AXS-USDT,AZY-USDT,BABYDOGE-USDT,BADGER-USDT,BAL-USDT,BAND-USDT,BAT-USDT,BCD-USDT,BCH-USDT,BETH-USDT,BHP-USDT,BICO-USDT,BLOK-USDT,BNT-USDT,BORING-USDT,BORA-USDT,BRWL-USDT,BSV-USDT,BTG-USDT,BTM-USDT,BTT-USDT,BZZ-USDT,CELO-USDT,CEL-USDT,CELR-USDT,CELT-USDT,CFG-USDT,CFX-USDT,CGS-USDT,CHAT-USDT,CHE-USDT,CHZ-USDT,CLV-USDT,CMT-USDT,CNTM-USDT,COMP-USDT,CONV-USDT,COVER-USDT,CQT-USDT,CRO-USDT,CRV-USDT,CSPR-USDT,CTC-USDT,CTXC-USDT,CVC-USDT,CVP-USDT,CVT-USDT,CVX-USDT,DAI-USDT,DAO-USDT,DASH-USDT,DCR-USDT,DEP-USDT,DEVT-USDT,DGB-USDT,DHT-USDT,DIA-USDT,DMD-USDT,DNA-USDT,DOME-USDT,DORA-USDT,DOSE-USDT,DYDX-USDT,EC-USDT,EDEN-USDT,EFI-USDT,EGLD-USDT,EGT-USDT,ELF-USDT,ELON-USDT,ELT-USDT,EM-USDT,ENJ-USDT,ENS-USDT,EOS-USDT,ERN-USDT,ETC-USDT,EURT-USDT,FAIR-USDT,FAME-USDT,FITFI-USDT,FLM-USDT,FLOW-USDT,FODL-USDT,FORTH-USDT,FRONT-USDT,FSN-USDT,FTM-USDT,GALA-USDT,GALFT-USDT,GARI-USDT,GAS-USDT,GF-USDT,GHST-USDT,GLM-USDT,GLMR-USDT,GM-USDT,GMT-USDT,GODS-USDT,GOG-USDT,GRT-USDT,GTO-USDT,GUSD-USDT,HBAR-USDT,HC-USDT,HDAO-USDT,HEGIC-USDT,HYC-USDT,ICP-USDT,ICX-USDT,ILV-USDT,IMX-USDT,INT-USDT,INX-USDT,IOST-USDT,IOTA-USDT,IQ-USDT,JFI-USDT,JOE-USDT,JST-USDT,KAN-USDT,KAR-USDT,KCASH-USDT,KDA-USDT,KINE-USDT,KISHU-USDT,KLAY-USDT,KNC-USDT,KOL-USDT,KONO-USDT,KP3R-USDT,KSM-USDT,LAMB-USDT,LAT-USDT,LBA-USDT,LDN-USDT,LDO-USDT,LEASH-USDT,LEO-USDT,LET-USDT,LINK-USDT,LING-USDT,LITH-USDT,LON-USDT,LOON-USDT,LOOKS-USDT,LPT-USDT,LRC-USDT,LSK-USDT,MAGIC-USDT,MANA-USDT,MASK-USDT,MATIC-USDT,MCO-USDT,MDA-USDT,MDT-USDT,MEME-USDT,METIS-USDT,MILO-USDT,MINA-USDT,MIR-USDT,MITH-USDT,MKR-USDT,MLN-USDT,MOF-USDT,MON-USDT,MOVR-USDT,MOVEZ-USDT,MXC-USDT,MXT-USDT,NAS-USDT,NEAR-USDT,NEO-USDT,NFT-USDT,NMR-USDT,NULS-USDT,NYM-USDT,OM-USDT,OMG-USDT,OMI-USDT,ONE-USDT,ONT-USDT,OP-USDT,ORBS-USDT,ORB-USDT,ORS-USDT,OXT-USDT,PAY-USDT,PCI-USDT,PEOPLE-USDT,PERP-USDT,PHA-USDT,PICKLE-USDT,PIT-USDT,PLG-USDT,PNK-USDT,POLS-USDT,POLYDOGE-USDT,PPT-USDT,PRQ-USDT,PST-USDT,PSTAKE-USDT,QOM-USDT,QTUM-USDT,RACA-USDT,RAY-USDT,REN-USDT,REP-USDT,REVV-USDT,RFUEL-USDT,RIO-USDT,RNT-USDT,ROAD-USDT,RON-USDT,RSR-USDT,RSS3-USDT,RVN-USDT,SAITAMA-USDT,SAMO-USDT,SAND-USDT,SC-USDT,SD-USDT,SFG-USDT,SHIB-USDT,SIS-USDT,SKEB-USDT,SKL-USDT,SLP-USDT,SNT-USDT,SNX-USDT,SOC-USDT,SOL-USDT,SOS-USDT,SPELL-USDT,SRM-USDT,STARL-USDT,STC-USDT,STORJ-USDT,STRK-USDT,STX-USDT,SUN-USDT,SUSHI-USDT,SWEAT-USDT,SWFTC-USDT,SWRV-USDT,T-USDT,TAI-USDT,TAKI-USDT,TCT-USDT,THETA-USDT,THG-USDT,TON-USDT,TOPC-USDT,TORN-USDT,TOWN-USDT,TRADE-USDT,TRA-USDT,TRB-USDT,TRUE-USDT,TRX-USDT,TUP-USDT,TUSD-USDT,UMA-USDT,UMEE-USDT,UNI-USDT,USDC-USDT,USDP-USDT,USTC-USDT,UTK-USDT,VALUE-USDT,VELO-USDT,VRA-USDT,VSYS-USDT,WAVES-USDT,WAXP-USDT,WBTC-USDT,WEMIX-USDT,WGRT-USDT,WING-USDT,WIN-USDT,WNCG-USDT,WNXM-USDT,WOO-USDT,WSB-USDT,WXT-USDT,XAUT-USDT,XCH-USDT,XEC-USDT,XEM-USDT,XETA-USDT,XLM-USDT,XMR-USDT,XNO-USDT,XPR-USDT,XTZ-USDT,YEE-USDT,YFI-USDT,YFII-USDT,YGG-USDT,YOU-USDT,YOYO-USDT,ZBC-USDT,ZEC-USDT,ZEN-USDT,ZIL-USDT,ZKS-USDT,ZRX-USDT,ZYRO-USDT,BTC-USDC,ETH-USDC,ETH-BTC,OKB-USDC,OKT-USDC,LTC-USDC,DOT-USDC,DOGE-USDC,LUNC-USDC,ETHW-USDC,LUNA-USDC,FIL-USDC,XRP-USDC,1INCH-USDC,AAVE-USDC,ADA-USDC,AGLD-USDC,ALGO-USDC,ANC-USDC,ANT-USDC,APE-USDC,API3-USDC,AR-USDC,ASTR-USDC,ATOM-USDC,AVAX-USDC,AXS-USDC,AZY-USDC,BABYDOGE-USDC,BAT-USDC,BCH-USDC,BICO-USDC,BSV-USDC,CEL-USDC,CELO-USDC,CELT-USDC,CHZ-USDC,COMP-USDC,CRO-USDC,CRV-USDC,CSPR-USDC,DASH-USDC,DEP-USDC,DOME-USDC,DYDX-USDC,EGLD-USDC,ELT-USDC,ENS-USDC,EOS-USDC,ETC-USDC,FITFI-USDC,FLM-USDC,FLOW-USDC,FTM-USDC,GALA-USDC,GALFT-USDC,GARI-USDC,GLMR-USDC,GMT-USDC,GODS-USDC,GRT-USDC,HBAR-USDC,ICP-USDC,IMX-USDC,IOST-USDC,JST-USDC,KISHU-USDC,KLAY-USDC,KNC-USDC,KSM-USDC,LINK-USDC,LOOKS-USDC,LRC-USDC,MANA-USDC,MASK-USDC,MATIC-USDC,MINA-USDC,MKR-USDC,MOF-USDC,MOVEZ-USDC,MXC-USDC,NEAR-USDC,NFT-USDC,NMR-USDC,NYM-USDC,OMG-USDC,OP-USDC,PEOPLE-USDC,PERP-USDC,RACA-USDC,RSR-USDC,SAITAMA-USDC,SAND-USDC,SHIB-USDC,SLP-USDC,SNX-USDC,SOC-USDC,SOL-USDC,SOS-USDC,SRM-USDC,STARL-USDC,STC-USDC,STORJ-USDC,STX-USDC,SUSHI-USDC,SWFTC-USDC,THETA-USDC,TON-USDC,TORN-USDC,TRB-USDC,TRX-USDC,UNI-USDC,USDP-USDC,USTC-USDC,VRA-USDC,WAVES-USDC,XCH-USDC,XEM-USDC,XLM-USDC,XMR-USDC,XNO-USDC,XTZ-USDC,YFI-USDC,YFII-USDC,YGG-USDC,ZEC-USDC,ZIL-USDC,BTC-DAI,ETH-DAI,BTC-USDK,ETH-USDK,USDT-USDK,OKB-BTC,OKT-BTC,LTC-BTC,DOT-BTC,DOGE-BTC,FIL-BTC,XRP-BTC,AAVE-BTC,ADA-BTC,ALGO-BTC,ALPHA-BTC,ANT-BTC,ATOM-BTC,AVAX-BTC,BADGER-BTC,BAT-BTC,BCD-BTC,BCH-BTC,BNT-BTC,BSV-BTC,BTG-BTC,BTM-BTC,CELO-BTC,CELT-BTC,CHZ-BTC,COMP-BTC,CQT-BTC,CRO-BTC,CRV-BTC,CTC-BTC,CVC-BTC,DASH-BTC,DCR-BTC,DGB-BTC,EGLD-BTC,ELF-BTC,ENJ-BTC,EOS-BTC,ETC-BTC,FLOW-BTC,GAS-BTC,GRT-BTC,GTO-BTC,HBAR-BTC,HC-BTC,ICP-BTC,ICX-BTC,INT-BTC,IOST-BTC,IOTA-BTC,KLAY-BTC,KNC-BTC,KSM-BTC,LINK-BTC,LRC-BTC,LSK-BTC,MANA-BTC,MITH-BTC,MKR-BTC,NAS-BTC,NEAR-BTC,NEO-BTC,NULS-BTC,OMG-BTC,ONT-BTC,PST-BTC,QTUM-BTC,REN-BTC,RSR-BTC,RVN-BTC,SC-BTC,SNT-BTC,SOL-BTC,SRM-BTC,STX-BTC,SWFTC-BTC,THETA-BTC,TRUE-BTC,TRX-BTC,UNI-BTC,VSYS-BTC,WAVES-BTC,WBTC-BTC,WXT-BTC,XCH-BTC,XEM-BTC,XLM-BTC,XMR-BTC,XTZ-BTC,YFI-BTC,YOU-BTC,ZEC-BTC,ZEN-BTC,ZIL-BTC,ZRX-BTC,OKB-ETH,OKT-ETH,LTC-ETH,DOT-ETH,DOGE-ETH,FIL-ETH,XRP-ETH,AAVE-ETH,ADA-ETH,API3-ETH,ATOM-ETH,AVAX-ETH,BETH-ETH,CRV-ETH,DASH-ETH,EOS-ETH,ETC-ETH,FLOW-ETH,GAS-ETH,GHST-ETH,HEGIC-ETH,INT-ETH,IOST-ETH,KSM-ETH,LINK-ETH,MANA-ETH,MKR-ETH,NEAR-ETH,NEO-ETH,NULS-ETH,OM-ETH,QTUM-ETH,SNX-ETH,SOL-ETH,SUSHI-ETH,SWFTC-ETH,TRX-ETH,UNI-ETH,WBTC-ETH,XLM-ETH,XMR-ETH,YFI-ETH,ZEC-ETH,LTC-OKB,XRP-OKB,ETC-OKB,OKDOT1-DOT,OKDOT2-DOT,BTC-EURT,ETH-EURT" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "credentials": { - "key": "", - "secret": "", - "clientID": "" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true, - "requiresClientID": true - }, - "urlEndpoints": { - "RestSpotURL": "https://www.okx.com/api/v5/", - "WebsocketSpotURL": "wss://ws.okx.com:8443/ws/v5/public" - } + "restPollingDelay": 600, + "apiKey": "Key", + "apiKeyLvl": -1, + "primaryProvider": true + } + ], + "cryptocurrencyProvider": { + "name": "CoinMarketCap", + "enabled": false, + "verbose": false, + "apiKey": "Key", + "accountPlan": "accountPlan" + }, + "cryptocurrencies": "BTC,LTC,ETH,DOGE,DASH,XRP,XMR", + "currencyPairFormat": { + "uppercase": true, + "delimiter": "-" + }, + "fiatDisplayCurrency": "USD", + "currencyFileUpdateDuration": 0, + "foreignExchangeUpdateDuration": 0 + }, + "communications": { + "slack": { + "name": "Slack", + "enabled": false, + "verbose": false, + "targetChannel": "general", + "verificationToken": "testtest" + }, + "smsGlobal": { + "name": "SMSGlobal", + "from": "Skynet", + "enabled": true, + "verbose": false, + "username": "1234", + "password": "12334", + "contacts": [ + { + "name": "StyleGherkin", + "number": "1231424", + "enabled": true + } + ] + }, + "smtp": { + "name": "SMTP", + "enabled": false, + "verbose": false, + "host": "smtp.google.com", + "port": "537", + "accountName": "some", + "accountPassword": "password", + "from": "", + "recipientList": "lol123@gmail.com" + }, + "telegram": { + "name": "Telegram", + "enabled": false, + "verbose": false, + "verificationToken": "testest", + "authorisedClients": { + "user_example": 0 + } + } + }, + "remoteControl": { + "username": "admin", + "password": "Password", + "gRPC": { + "enabled": true, + "listenAddress": "localhost:9052", + "grpcProxyEnabled": true, + "grpcProxyListenAddress": "localhost:9053" + }, + "deprecatedRPC": { + "enabled": true, + "listenAddress": "localhost:9050" + }, + "websocketRPC": { + "enabled": true, + "listenAddress": "localhost:9051", + "connectionLimit": 1, + "maxAuthFailures": 3, + "allowInsecureOrigin": true + } + }, + "portfolioAddresses": { + "addresses": [ + { + "Address": "1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy", + "CoinType": "BTC", + "Balance": 53000.01741264, + "Description": "", + "WhiteListed": false, + "ColdStorage": false, + "SupportedExchanges": "" + }, + { + "Address": "3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v", + "CoinType": "BTC", + "Balance": 107848.28963408, + "Description": "", + "WhiteListed": false, + "ColdStorage": false, + "SupportedExchanges": "" + }, + { + "Address": "LgY8ahfHRhvjVQC1zJnBhFMG5pCTMuKRqh", + "CoinType": "LTC", + "Balance": 0.03665026, + "Description": "", + "WhiteListed": false, + "ColdStorage": false, + "SupportedExchanges": "" + }, + { + "Address": "0xb794f5ea0ba39494ce839613fffba74279579268", + "CoinType": "ETH", + "Balance": 0.25555604051326, + "Description": "", + "WhiteListed": false, + "ColdStorage": false, + "SupportedExchanges": "" + } + ] + }, + "exchanges": [ + { + "name": "Okx", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "baseCurrencies": "USD", + "currencyPairs": { + "bypassConfigFormatUpgrades": false, + "requestFormat": { + "uppercase": true, + "delimiter": "-" }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} - }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": true, - "saveTradeData": false, - "tradeFeed": false, - "fillsFeed": false - } + "configFormat": { + "uppercase": true, + "delimiter": "-" }, - "bankAccounts": [ - { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } + "useGlobalFormat": true, + "assetTypes": [ + "futures", + "margin", + "option", + "perpetualswap", + "spot" ], - "orderbook": { - "verificationBypass": false, - "websocketBufferLimit": 5, - "websocketBufferEnabled": false, - "publishPeriod": 10000000000 + "pairs": { + "futures": { + "assetEnabled": true, + "enabled": "BTC-USD-221007,BTC-USD-221014", + "available": "BTC-USD-221230,BTC-USD-230331,ETH-USD-221007,ETH-USD-221014,ETH-USD-221230,ETH-USD-230331,LTC-USD-221007,LTC-USD-221014,LTC-USD-221230,LTC-USD-230331,DOT-USD-221007,DOT-USD-221014,DOT-USD-221230,DOT-USD-230331,FIL-USD-221007,FIL-USD-221014,FIL-USD-221230,FIL-USD-230331,ADA-USD-221007,ADA-USD-221014,ADA-USD-221230,ADA-USD-230331,AVAX-USD-221007,AVAX-USD-221014,AVAX-USD-221230,BTC-USD-221007,BTC-USD-221014,BTC-USD-221230,BTC-USD-230331,ETH-USD-221007,ETH-USD-221014,ETH-USD-221230,ETH-USD-230331,LTC-USD-221007,LTC-USD-221014,LTC-USD-221230,LTC-USD-230331,DOT-USD-221007,DOT-USD-221014,DOT-USD-221230,DOT-USD-230331,FIL-USD-221007,FIL-USD-221014,FIL-USD-221230,FIL-USD-230331,ADA-USD-221007,ADA-USD-221014,ADA-USD-221230,ADA-USD-230331,AVAX-USD-221007,AVAX-USD-221014,AVAX-USD-221230,AVAX-USD-230331,BCH-USD-221007,BCH-USD-221014,BCH-USD-221230,BCH-USD-230331,EOS-USD-221007,EOS-USD-221014,EOS-USD-221230,EOS-USD-230331,ETC-USD-221007,ETC-USD-221014,ETC-USD-221230,ETC-USD-230331,LINK-USD-221007,LINK-USD-221014,LINK-USD-221230,LINK-USD-230331,SOL-USD-221007,SOL-USD-221014,SOL-USD-221230,SOL-USD-230331,TRX-USD-221007,TRX-USD-221014,TRX-USD-221230,TRX-USD-230331,XRP-USD-221007,XRP-USD-221014,XRP-USD-221230,XRP-USD-230331,BTC-USDT-221007,BTC-USDT-221014,BTC-USDT-221230,BTC-USDT-230331,ETH-USDT-221007,ETH-USDT-221014,ETH-USDT-221230,ETH-USDT-230331,LTC-USDT-221007,LTC-USDT-221014,LTC-USDT-221230,LTC-USDT-230331,DOT-USDT-221007,DOT-USDT-221014,DOT-USDT-221230,DOT-USDT-230331,FIL-USDT-221007,FIL-USDT-221014,FIL-USDT-221230,FIL-USDT-230331,ADA-USDT-221007,ADA-USDT-221014,ADA-USDT-221230,ADA-USDT-230331,BCH-USDT-221007,BCH-USDT-221014,BCH-USDT-221230,BCH-USDT-230331,EOS-USDT-221007,EOS-USDT-221014,EOS-USDT-221230,EOS-USDT-230331,ETC-USDT-221007,ETC-USDT-221014,ETC-USDT-221230,ETC-USDT-230331,LINK-USDT-221007,LINK-USDT-221014,LINK-USDT-221230,LINK-USDT-230331,TRX-USDT-221007,TRX-USDT-221014,TRX-USDT-221230,TRX-USDT-230331,XRP-USDT-221007,XRP-USDT-221014,XRP-USDT-221230,XRP-USDT-230331" + }, + "margin": { + "assetEnabled": true, + "enabled": "BTC-USDT,ETH-USDT,OKB-USDT", + "available": "LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,1INCH-USDT,AAVE-USDT,ADA-USDT,AGLD-USDT,AKITA-USDT,ALGO-USDT,ALPHA-USDT,BTC-USDT,ETH-USDT,OKB-USDT,LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,1INCH-USDT,AAVE-USDT,ADA-USDT,AGLD-USDT,AKITA-USDT,ALGO-USDT,ALPHA-USDT,ANT-USDT,APE-USDT,API3-USDT,ASTR-USDT,ATOM-USDT,AVAX-USDT,AXS-USDT,BABYDOGE-USDT,BADGER-USDT,BAL-USDT,BAND-USDT,BAT-USDT,BCH-USDT,BICO-USDT,BNT-USDT,BSV-USDT,BTM-USDT,BTT-USDT,BZZ-USDT,CELO-USDT,CEL-USDT,CELR-USDT,CFX-USDT,CHZ-USDT,CLV-USDT,COMP-USDT,CONV-USDT,CQT-USDT,CRO-USDT,CRV-USDT,CSPR-USDT,CVC-USDT,DASH-USDT,DOME-USDT,DORA-USDT,DYDX-USDT,EFI-USDT,EGLD-USDT,ELF-USDT,ENJ-USDT,ENS-USDT,EOS-USDT,ETC-USDT,FITFI-USDT,FLM-USDT,FLOW-USDT,FTM-USDT,GALA-USDT,GLMR-USDT,GMT-USDT,GODS-USDT,GRT-USDT,HBAR-USDT,HC-USDT,ICP-USDT,IMX-USDT,IOST-USDT,IOTA-USDT,JST-USDT,KAR-USDT,KISHU-USDT,KNC-USDT,KSM-USDT,LAMB-USDT,LAT-USDT,LINK-USDT,LON-USDT,LOOKS-USDT,LPT-USDT,LRC-USDT,MANA-USDT,MASK-USDT,MATIC-USDT,MINA-USDT,MKR-USDT,NAS-USDT,NEAR-USDT,NEO-USDT,NFT-USDT,NYM-USDT,OMG-USDT,ONT-USDT,OP-USDT,PEOPLE-USDT,PERP-USDT,QTUM-USDT,REN-USDT,RSR-USDT,RSS3-USDT,RVN-USDT,SAND-USDT,SC-USDT,SHIB-USDT,SKL-USDT,SLP-USDT,SNT-USDT,SNX-USDT,SOL-USDT,SOS-USDT,SRM-USDT,STARL-USDT,STORJ-USDT,SUSHI-USDT,SWEAT-USDT,SWRV-USDT,THETA-USDT,TORN-USDT,TRB-USDT,TRX-USDT,UMA-USDT,UMEE-USDT,UNI-USDT,USDC-USDT,VSYS-USDT,WAVES-USDT,WNCG-USDT,WNXM-USDT,XCH-USDT,XEM-USDT,XLM-USDT,XMR-USDT,XTZ-USDT,YFI-USDT,YFII-USDT,YGG-USDT,ZEC-USDT,ZEN-USDT,ZIL-USDT,ZRX-USDT,BTC-USDC,ETH-BTC,LTC-USDC,DOT-USDC,DOGE-USDC,LUNC-USDC,LUNA-USDC,XRP-USDC,ADA-USDC,ATOM-USDC,AVAX-USDC,NEAR-USDC,OP-USDC,SOL-USDC,OKB-BTC,LTC-BTC,DOT-BTC,DOGE-BTC,FIL-BTC,XRP-BTC,AAVE-BTC,ADA-BTC,ALGO-BTC,ANT-BTC,ATOM-BTC,AVAX-BTC,BADGER-BTC,BAT-BTC,BCH-BTC,BNT-BTC,BSV-BTC,BTM-BTC,CHZ-BTC,COMP-BTC,CRO-BTC,CRV-BTC,CVC-BTC,DASH-BTC,EGLD-BTC,ELF-BTC,ENJ-BTC,EOS-BTC,ETC-BTC,GRT-BTC,HBAR-BTC,HC-BTC,ICP-BTC,IOST-BTC,IOTA-BTC,KNC-BTC,KSM-BTC,LINK-BTC,LRC-BTC,MANA-BTC,MKR-BTC,NAS-BTC,NEAR-BTC,NEO-BTC,OMG-BTC,ONT-BTC,QTUM-BTC,REN-BTC,RSR-BTC,RVN-BTC,SNT-BTC,SOL-BTC,SRM-BTC,THETA-BTC,TRX-BTC,UNI-BTC,VSYS-BTC,WAVES-BTC,XCH-BTC,XEM-BTC,XLM-BTC,XMR-BTC,XTZ-BTC,ZEC-BTC,ZIL-BTC,ZRX-BTC" + }, + "option": { + "assetEnabled": true, + "enabled": "BTC-USD-220930-28000-P,BTC-USD-220930-30000-C", + "available": "BTC-USD-220930-30000-P,BTC-USD-220930-32000-C,BTC-USD-220930-32000-P,BTC-USD-220930-34000-C,BTC-USD-220930-34000-P,BTC-USD-220930-35000-C,BTC-USD-220930-35000-P,BTC-USD-220930-36000-C,BTC-USD-220930-36000-P,BTC-USD-220930-40000-C,BTC-USD-220930-40000-P,BTC-USD-220930-45000-C,BTC-USD-220930-45000-P,BTC-USD-220930-50000-C,BTC-USD-220930-50000-P,BTC-USD-220930-55000-C,BTC-USD-220930-55000-P,BTC-USD-220930-60000-C,BTC-USD-220930-60000-P,BTC-USD-220930-65000-C,BTC-USD-220930-65000-P,BTC-USD-220930-70000-C,BTC-USD-220930-70000-P,BTC-USD-220930-80000-C,BTC-USD-220930-80000-P,BTC-USD-220930-90000-C,BTC-USD-220930-90000-P,BTC-USD-220930-100000-C,BTC-USD-220930-100000-P,BTC-USD-220930-120000-C,BTC-USD-220930-120000-P,BTC-USD-221007-13000-C,BTC-USD-221007-13000-P,BTC-USD-221007-14000-C,BTC-USD-221007-14000-P,BTC-USD-221007-15000-C,BTC-USD-221007-15000-P,BTC-USD-221007-16000-C,BTC-USD-221007-16000-P,BTC-USD-221007-17000-C,BTC-USD-221007-17000-P,BTC-USD-221007-18000-C,BTC-USD-221007-18000-P,BTC-USD-221007-18500-C,BTC-USD-221007-18500-P,BTC-USD-221007-19000-C,BTC-USD-221007-19000-P,BTC-USD-221007-20000-C,BTC-USD-221007-20000-P,BTC-USD-221007-21000-C,BTC-USD-221007-21000-P,BTC-USD-221007-22000-C,BTC-USD-221007-22000-P,BTC-USD-221007-23000-C,BTC-USD-221007-23000-P,BTC-USD-221007-24000-C,BTC-USD-221007-24000-P,BTC-USD-221007-26000-C,BTC-USD-221007-26000-P,BTC-USD-221007-28000-C,BTC-USD-221007-28000-P,BTC-USD-221007-30000-C,BTC-USD-221007-30000-P,BTC-USD-221014-14000-C,BTC-USD-221014-14000-P,BTC-USD-221014-15000-C,BTC-USD-221014-15000-P,BTC-USD-221014-16000-C,BTC-USD-221014-16000-P,BTC-USD-221014-17000-C,BTC-USD-221014-17000-P,BTC-USD-221014-18000-C,BTC-USD-221014-18000-P,BTC-USD-221014-19000-C,BTC-USD-221014-19000-P,BTC-USD-221014-20000-C,BTC-USD-221014-20000-P,BTC-USD-221014-21000-C,BTC-USD-221014-21000-P,BTC-USD-221014-22000-C,BTC-USD-221014-22000-P,BTC-USD-221014-23000-C,BTC-USD-221014-23000-P,BTC-USD-221014-24000-C,BTC-USD-221014-24000-P,BTC-USD-221014-26000-C,BTC-USD-221014-26000-P,BTC-USD-221014-28000-C,BTC-USD-221014-28000-P,BTC-USD-221014-30000-C,BTC-USD-221014-30000-P,BTC-USD-221028-10000-C,BTC-USD-221028-10000-P,BTC-USD-221028-12000-C,BTC-USD-221028-12000-P,BTC-USD-221028-14000-C,BTC-USD-221028-14000-P,BTC-USD-221028-16000-C,BTC-USD-221028-16000-P,BTC-USD-221028-17000-C,BTC-USD-221028-17000-P,BTC-USD-221028-18000-C,BTC-USD-221028-18000-P,BTC-USD-221028-19000-C,BTC-USD-221028-19000-P,BTC-USD-221028-20000-C,BTC-USD-221028-20000-P,BTC-USD-221028-21000-C,BTC-USD-221028-21000-P,BTC-USD-221028-22000-C,BTC-USD-221028-22000-P,BTC-USD-221028-24000-C,BTC-USD-221028-24000-P,BTC-USD-221028-26000-C,BTC-USD-221028-26000-P,BTC-USD-221028-28000-C,BTC-USD-221028-28000-P,BTC-USD-221028-30000-C,BTC-USD-221028-30000-P,BTC-USD-221028-32000-C,BTC-USD-221028-32000-P,BTC-USD-221028-35000-C,BTC-USD-220927-17000-C,BTC-USD-220927-17000-P,BTC-USD-220927-17500-C,BTC-USD-220927-17500-P,BTC-USD-220927-18000-C,BTC-USD-220927-18000-P,BTC-USD-220927-18200-C,BTC-USD-220927-18200-P,BTC-USD-220927-18400-C,BTC-USD-220927-18400-P,BTC-USD-220927-18600-C,BTC-USD-220927-18600-P,BTC-USD-220927-18800-C,BTC-USD-220927-18800-P,BTC-USD-220927-19000-C,BTC-USD-220927-19000-P,BTC-USD-220927-19200-C,BTC-USD-220927-19200-P,BTC-USD-220927-19400-C,BTC-USD-220927-19400-P,BTC-USD-220927-19600-C,BTC-USD-220927-19600-P,BTC-USD-220927-19800-C,BTC-USD-220927-19800-P,BTC-USD-220927-20000-C,BTC-USD-220927-20000-P,BTC-USD-220927-20500-C,BTC-USD-220927-20500-P,BTC-USD-220927-21000-C,BTC-USD-220927-21000-P,BTC-USD-220927-21500-C,BTC-USD-220927-21500-P,BTC-USD-220928-16500-C,BTC-USD-220928-16500-P,BTC-USD-220928-17000-C,BTC-USD-220928-17000-P,BTC-USD-220928-17500-C,BTC-USD-220928-17500-P,BTC-USD-220928-18000-C,BTC-USD-220928-18000-P,BTC-USD-220928-18200-C,BTC-USD-220928-18200-P,BTC-USD-220928-18400-C,BTC-USD-220928-18400-P,BTC-USD-220928-18600-C,BTC-USD-220928-18600-P,BTC-USD-220928-18800-C,BTC-USD-220928-18800-P,BTC-USD-220928-19000-C,BTC-USD-220928-19000-P,BTC-USD-220928-19200-C,BTC-USD-220928-19200-P,BTC-USD-220928-19400-C,BTC-USD-220928-19400-P,BTC-USD-220928-19600-C,BTC-USD-220928-19600-P,BTC-USD-220928-20000-C,BTC-USD-220928-20000-P,BTC-USD-220928-20500-C,BTC-USD-220928-20500-P,BTC-USD-220928-21000-C,BTC-USD-220928-21000-P,BTC-USD-220928-21500-C,BTC-USD-220928-21500-P,BTC-USD-220930-5000-C,BTC-USD-220930-5000-P,BTC-USD-220930-10000-C,BTC-USD-220930-10000-P,BTC-USD-220930-12000-C,BTC-USD-220930-12000-P,BTC-USD-220930-15000-C,BTC-USD-220930-15000-P,BTC-USD-220930-16000-C,BTC-USD-220930-16000-P,BTC-USD-220930-17000-C,BTC-USD-220930-17000-P,BTC-USD-220930-17500-C,BTC-USD-220930-17500-P,BTC-USD-220930-18000-C,BTC-USD-220930-18000-P,BTC-USD-220930-18500-C,BTC-USD-220930-18500-P,BTC-USD-220930-19000-C,BTC-USD-220930-19000-P,BTC-USD-220930-19500-C,BTC-USD-220930-19500-P,BTC-USD-220930-20000-C,BTC-USD-220930-20000-P,BTC-USD-220930-20500-C,BTC-USD-220930-20500-P,BTC-USD-220930-21000-C,BTC-USD-220930-21000-P,BTC-USD-220930-22000-C,BTC-USD-220930-22000-P,BTC-USD-220930-23000-C,BTC-USD-220930-23000-P,BTC-USD-220930-24000-C,BTC-USD-220930-24000-P,BTC-USD-220930-25000-C,BTC-USD-220930-25000-P,BTC-USD-220930-26000-C,BTC-USD-220930-26000-P,BTC-USD-220930-28000-C,BTC-USD-220930-28000-P,BTC-USD-220930-30000-C,BTC-USD-220930-30000-P,BTC-USD-220930-32000-C,BTC-USD-220930-32000-P,BTC-USD-220930-34000-C,BTC-USD-220930-34000-P,BTC-USD-220930-35000-C,BTC-USD-220930-35000-P,BTC-USD-220930-36000-C,BTC-USD-220930-36000-P,BTC-USD-220930-40000-C,BTC-USD-220930-40000-P,BTC-USD-220930-45000-C,BTC-USD-220930-45000-P,BTC-USD-220930-50000-C,BTC-USD-220930-50000-P,BTC-USD-220930-55000-C,BTC-USD-220930-55000-P,BTC-USD-220930-60000-C,BTC-USD-220930-60000-P,BTC-USD-220930-65000-C,BTC-USD-220930-65000-P,BTC-USD-220930-70000-C,BTC-USD-220930-70000-P,BTC-USD-220930-80000-C,BTC-USD-220930-80000-P,BTC-USD-220930-90000-C,BTC-USD-220930-90000-P,BTC-USD-220930-100000-C,BTC-USD-220930-100000-P,BTC-USD-220930-120000-C,BTC-USD-220930-120000-P,BTC-USD-221007-13000-C,BTC-USD-221007-13000-P,BTC-USD-221007-14000-C,BTC-USD-221007-14000-P,BTC-USD-221007-15000-C,BTC-USD-221007-15000-P,BTC-USD-221007-16000-C,BTC-USD-221007-16000-P,BTC-USD-221007-17000-C,BTC-USD-221007-17000-P,BTC-USD-221007-18000-C,BTC-USD-221007-18000-P,BTC-USD-221007-18500-C,BTC-USD-221007-18500-P,BTC-USD-221007-19000-C,BTC-USD-221007-19000-P,BTC-USD-221007-20000-C,BTC-USD-221007-20000-P,BTC-USD-221007-21000-C,BTC-USD-221007-21000-P,BTC-USD-221007-22000-C,BTC-USD-221007-22000-P,BTC-USD-221007-23000-C,BTC-USD-221007-23000-P,BTC-USD-221007-24000-C,BTC-USD-221007-24000-P,BTC-USD-221007-26000-C,BTC-USD-221007-26000-P,BTC-USD-221007-28000-C,BTC-USD-221007-28000-P,BTC-USD-221007-30000-C,BTC-USD-221007-30000-P,BTC-USD-221014-14000-C,BTC-USD-221014-14000-P,BTC-USD-221014-15000-C,BTC-USD-221014-15000-P,BTC-USD-221014-16000-C,BTC-USD-221014-16000-P,BTC-USD-221014-17000-C,BTC-USD-221014-17000-P,BTC-USD-221014-18000-C,BTC-USD-221014-18000-P,BTC-USD-221014-19000-C,BTC-USD-221014-19000-P,BTC-USD-221014-20000-C,BTC-USD-221014-20000-P,BTC-USD-221014-21000-C,BTC-USD-221014-21000-P,BTC-USD-221014-22000-C,BTC-USD-221014-22000-P,BTC-USD-221014-23000-C,BTC-USD-221014-23000-P,BTC-USD-221014-24000-C,BTC-USD-221014-24000-P,BTC-USD-221014-26000-C,BTC-USD-221014-26000-P,BTC-USD-221014-28000-C,BTC-USD-221014-28000-P,BTC-USD-221014-30000-C,BTC-USD-221014-30000-P,BTC-USD-221028-10000-C,BTC-USD-221028-10000-P,BTC-USD-221028-12000-C,BTC-USD-221028-12000-P,BTC-USD-221028-14000-C,BTC-USD-221028-14000-P,BTC-USD-221028-16000-C,BTC-USD-221028-16000-P,BTC-USD-221028-17000-C,BTC-USD-221028-17000-P,BTC-USD-221028-18000-C,BTC-USD-221028-18000-P,BTC-USD-221028-19000-C,BTC-USD-221028-19000-P,BTC-USD-221028-20000-C,BTC-USD-221028-20000-P,BTC-USD-221028-21000-C,BTC-USD-221028-21000-P,BTC-USD-221028-22000-C,BTC-USD-221028-22000-P,BTC-USD-221028-24000-C,BTC-USD-221028-24000-P,BTC-USD-221028-26000-C,BTC-USD-221028-26000-P,BTC-USD-221028-28000-C,BTC-USD-221028-28000-P,BTC-USD-221028-30000-C,BTC-USD-221028-30000-P,BTC-USD-221028-32000-C,BTC-USD-221028-32000-P,BTC-USD-221028-35000-C,BTC-USD-221028-35000-P,BTC-USD-221028-40000-C,BTC-USD-221028-40000-P,BTC-USD-221028-50000-C,BTC-USD-221028-50000-P,BTC-USD-221028-60000-C,BTC-USD-221028-60000-P,BTC-USD-221028-70000-C,BTC-USD-221028-70000-P,BTC-USD-221125-5000-C,BTC-USD-221125-5000-P,BTC-USD-221125-10000-C,BTC-USD-221125-10000-P,BTC-USD-221125-12000-C,BTC-USD-221125-12000-P,BTC-USD-221125-15000-C,BTC-USD-221125-15000-P,BTC-USD-221125-16000-C,BTC-USD-221125-16000-P,BTC-USD-221125-17000-C,BTC-USD-221125-17000-P,BTC-USD-221125-18000-C,BTC-USD-221125-18000-P,BTC-USD-221125-20000-C,BTC-USD-221125-20000-P,BTC-USD-221125-22000-C,BTC-USD-221125-22000-P,BTC-USD-221125-24000-C,BTC-USD-221125-24000-P,BTC-USD-221125-26000-C,BTC-USD-221125-26000-P,BTC-USD-221125-28000-C,BTC-USD-221125-28000-P,BTC-USD-221125-30000-C,BTC-USD-221125-30000-P,BTC-USD-221125-32000-C,BTC-USD-221125-32000-P,BTC-USD-221125-35000-C,BTC-USD-221125-35000-P,BTC-USD-221125-40000-C,BTC-USD-221125-40000-P,BTC-USD-221125-50000-C,BTC-USD-221125-50000-P,BTC-USD-221125-60000-C,BTC-USD-221125-60000-P,BTC-USD-221125-70000-C,BTC-USD-221125-70000-P,BTC-USD-221230-5000-C,BTC-USD-221230-5000-P,BTC-USD-221230-10000-C,BTC-USD-221230-10000-P,BTC-USD-221230-12000-C,BTC-USD-221230-12000-P,BTC-USD-221230-13000-C,BTC-USD-221230-13000-P,BTC-USD-221230-15000-C,BTC-USD-221230-15000-P,BTC-USD-221230-16000-C,BTC-USD-221230-16000-P,BTC-USD-221230-17000-C,BTC-USD-221230-17000-P,BTC-USD-221230-18000-C,BTC-USD-221230-18000-P,BTC-USD-221230-19000-C,BTC-USD-221230-19000-P,BTC-USD-221230-20000-C,BTC-USD-221230-20000-P,BTC-USD-221230-21000-C,BTC-USD-221230-21000-P,BTC-USD-221230-22000-C,BTC-USD-221230-22000-P,BTC-USD-221230-23000-C,BTC-USD-221230-23000-P,BTC-USD-221230-24000-C,BTC-USD-221230-24000-P,BTC-USD-221230-25000-C,BTC-USD-221230-25000-P,BTC-USD-221230-26000-C,BTC-USD-221230-26000-P,BTC-USD-221230-28000-C,BTC-USD-221230-28000-P,BTC-USD-221230-30000-C,BTC-USD-221230-30000-P,BTC-USD-221230-32000-C,BTC-USD-221230-32000-P,BTC-USD-221230-35000-C,BTC-USD-221230-35000-P" + }, + "perpetualswap": { + "assetEnabled": true, + "enabled": "BTC-USD-SWAP,ETH-USD-SWAP", + "available": "LTC-USD-SWAP,DOT-USD-SWAP,DOGE-USD-SWAP,MASK-USDT-SWAP,MATIC-USDT-SWAP,MINA-USDT-SWAP,MKR-USDT-SWAP,NEAR-USDT-SWAP,NEO-USDT-SWAP,NFT-USDT-SWAP,NYM-USDT-SWAP,OMG-USDT-SWAP,ONT-USDT-SWAP,OP-USDT-SWAP,PEOPLE-USDT-SWAP,BTC-USD-SWAP,ETH-USD-SWAP,LTC-USD-SWAP,DOT-USD-SWAP,DOGE-USD-SWAP,FIL-USD-SWAP,XRP-USD-SWAP,1INCH-USD-SWAP,ADA-USD-SWAP,ALGO-USD-SWAP,ATOM-USD-SWAP,AVAX-USD-SWAP,BCH-USD-SWAP,BSV-USD-SWAP,CRV-USD-SWAP,DASH-USD-SWAP,EOS-USD-SWAP,ETC-USD-SWAP,GRT-USD-SWAP,IOST-USD-SWAP,IOTA-USD-SWAP,KNC-USD-SWAP,KSM-USD-SWAP,LINK-USD-SWAP,MANA-USD-SWAP,NEO-USD-SWAP,ONT-USD-SWAP,QTUM-USD-SWAP,SAND-USD-SWAP,SOL-USD-SWAP,SUSHI-USD-SWAP,THETA-USD-SWAP,TRX-USD-SWAP,UNI-USD-SWAP,XLM-USD-SWAP,XMR-USD-SWAP,XTZ-USD-SWAP,YFI-USD-SWAP,YFII-USD-SWAP,ZEC-USD-SWAP,BTC-USDT-SWAP,ETH-USDT-SWAP,LTC-USDT-SWAP,DOT-USDT-SWAP,DOGE-USDT-SWAP,LUNC-USDT-SWAP,ETHW-USDT-SWAP,LUNA-USDT-SWAP,FIL-USDT-SWAP,XRP-USDT-SWAP,1INCH-USDT-SWAP,AAVE-USDT-SWAP,ADA-USDT-SWAP,AGLD-USDT-SWAP,ALGO-USDT-SWAP,ALPHA-USDT-SWAP,ANT-USDT-SWAP,APE-USDT-SWAP,API3-USDT-SWAP,ASTR-USDT-SWAP,ATOM-USDT-SWAP,AVAX-USDT-SWAP,AXS-USDT-SWAP,BABYDOGE-USDT-SWAP,BADGER-USDT-SWAP,BAL-USDT-SWAP,BAND-USDT-SWAP,BAT-USDT-SWAP,BCH-USDT-SWAP,BICO-USDT-SWAP,BNT-USDT-SWAP,BSV-USDT-SWAP,BTT-USDT-SWAP,CELO-USDT-SWAP,CEL-USDT-SWAP,CFX-USDT-SWAP,CHZ-USDT-SWAP,COMP-USDT-SWAP,CRO-USDT-SWAP,CRV-USDT-SWAP,CSPR-USDT-SWAP,CVC-USDT-SWAP,DASH-USDT-SWAP,DOME-USDT-SWAP,DORA-USDT-SWAP,DYDX-USDT-SWAP,EGLD-USDT-SWAP,ELON-USDT-SWAP,ENJ-USDT-SWAP,ENS-USDT-SWAP,EOS-USDT-SWAP,ETC-USDT-SWAP,FITFI-USDT-SWAP,FLM-USDT-SWAP,FTM-USDT-SWAP,GALA-USDT-SWAP,GMT-USDT-SWAP,GODS-USDT-SWAP,GRT-USDT-SWAP,ICP-USDT-SWAP,IMX-USDT-SWAP,IOST-USDT-SWAP,IOTA-USDT-SWAP,JST-USDT-SWAP,KISHU-USDT-SWAP,KNC-USDT-SWAP,KSM-USDT-SWAP,LINK-USDT-SWAP,LOOKS-USDT-SWAP,LPT-USDT-SWAP,LRC-USDT-SWAP,MANA-USDT-SWAP,MASK-USDT-SWAP,MATIC-USDT-SWAP,MINA-USDT-SWAP,MKR-USDT-SWAP,NEAR-USDT-SWAP,NEO-USDT-SWAP,NFT-USDT-SWAP,NYM-USDT-SWAP,OMG-USDT-SWAP,ONT-USDT-SWAP,OP-USDT-SWAP,PEOPLE-USDT-SWAP,PERP-USDT-SWAP,QTUM-USDT-SWAP,REN-USDT-SWAP,RSR-USDT-SWAP,RVN-USDT-SWAP,SAND-USDT-SWAP,SC-USDT-SWAP,SHIB-USDT-SWAP,SLP-USDT-SWAP,SNX-USDT-SWAP,SOL-USDT-SWAP,SOS-USDT-SWAP,SRM-USDT-SWAP,STARL-USDT-SWAP,STORJ-USDT-SWAP,SUSHI-USDT-SWAP,SWEAT-USDT-SWAP,THETA-USDT-SWAP,TRB-USDT-SWAP,TRX-USDT-SWAP,UMA-USDT-SWAP,UMEE-USDT-SWAP,UNI-USDT-SWAP,WAVES-USDT-SWAP,WNXM-USDT-SWAP,XCH-USDT-SWAP,XEM-USDT-SWAP,XLM-USDT-SWAP,XMR-USDT-SWAP,XTZ-USDT-SWAP,YFI-USDT-SWAP,YFII-USDT-SWAP,YGG-USDT-SWAP,ZEC-USDT-SWAP,ZEN-USDT-SWAP,ZIL-USDT-SWAP,ZRX-USDT-SWAP" + }, + "spot": { + "assetEnabled": true, + "enabled": "BTC-USDT,ETH-USDT,OKB-USDT", + "available": "OKT-USDT,LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,CITY-USDT,MENGO-USDT,ARG-USDT,POR-USDT,1INCH-USDT,AAVE-USDT,ABT-USDT,ACA-USDT,ADA-USDT,AERGO-USDT,AGLD-USDT,XMR-ETH,YFI-ETH,ZEC-ETH,LTC-OKB,XRP-OKB,ETC-OKB,BTC-USDT,ETH-USDT,OKB-USDT,OKT-USDT,LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,CITY-USDT,MENGO-USDT,ARG-USDT,POR-USDT,1INCH-USDT,AAVE-USDT,ABT-USDT,ACA-USDT,ADA-USDT,AERGO-USDT,AGLD-USDT,AKITA-USDT,ALCX-USDT,ALGO-USDT,ALPHA-USDT,ANC-USDT,ANT-USDT,ANW-USDT,APE-USDT,APIX-USDT,API3-USDT,APM-USDT,AR-USDT,ARK-USDT,AST-USDT,ASTR-USDT,ATOM-USDT,AUCTION-USDT,AVAX-USDT,AXS-USDT,AZY-USDT,BABYDOGE-USDT,BADGER-USDT,BAL-USDT,BAND-USDT,BAT-USDT,BCD-USDT,BCH-USDT,BETH-USDT,BHP-USDT,BICO-USDT,BLOK-USDT,BNT-USDT,BORING-USDT,BORA-USDT,BRWL-USDT,BSV-USDT,BTG-USDT,BTM-USDT,BTT-USDT,BZZ-USDT,CELO-USDT,CEL-USDT,CELR-USDT,CELT-USDT,CFG-USDT,CFX-USDT,CGS-USDT,CHAT-USDT,CHE-USDT,CHZ-USDT,CLV-USDT,CMT-USDT,CNTM-USDT,COMP-USDT,CONV-USDT,COVER-USDT,CQT-USDT,CRO-USDT,CRV-USDT,CSPR-USDT,CTC-USDT,CTXC-USDT,CVC-USDT,CVP-USDT,CVT-USDT,CVX-USDT,DAI-USDT,DAO-USDT,DASH-USDT,DCR-USDT,DEP-USDT,DEVT-USDT,DGB-USDT,DHT-USDT,DIA-USDT,DMD-USDT,DNA-USDT,DOME-USDT,DORA-USDT,DOSE-USDT,DYDX-USDT,EC-USDT,EDEN-USDT,EFI-USDT,EGLD-USDT,EGT-USDT,ELF-USDT,ELON-USDT,ELT-USDT,EM-USDT,ENJ-USDT,ENS-USDT,EOS-USDT,ERN-USDT,ETC-USDT,EURT-USDT,FAIR-USDT,FAME-USDT,FITFI-USDT,FLM-USDT,FLOW-USDT,FODL-USDT,FORTH-USDT,FRONT-USDT,FSN-USDT,FTM-USDT,GALA-USDT,GALFT-USDT,GARI-USDT,GAS-USDT,GF-USDT,GHST-USDT,GLM-USDT,GLMR-USDT,GM-USDT,GMT-USDT,GODS-USDT,GOG-USDT,GRT-USDT,GTO-USDT,GUSD-USDT,HBAR-USDT,HC-USDT,HDAO-USDT,HEGIC-USDT,HYC-USDT,ICP-USDT,ICX-USDT,ILV-USDT,IMX-USDT,INT-USDT,INX-USDT,IOST-USDT,IOTA-USDT,IQ-USDT,JFI-USDT,JOE-USDT,JST-USDT,KAN-USDT,KAR-USDT,KCASH-USDT,KDA-USDT,KINE-USDT,KISHU-USDT,KLAY-USDT,KNC-USDT,KOL-USDT,KONO-USDT,KP3R-USDT,KSM-USDT,LAMB-USDT,LAT-USDT,LBA-USDT,LDN-USDT,LDO-USDT,LEASH-USDT,LEO-USDT,LET-USDT,LINK-USDT,LING-USDT,LITH-USDT,LON-USDT,LOON-USDT,LOOKS-USDT,LPT-USDT,LRC-USDT,LSK-USDT,MAGIC-USDT,MANA-USDT,MASK-USDT,MATIC-USDT,MCO-USDT,MDA-USDT,MDT-USDT,MEME-USDT,METIS-USDT,MILO-USDT,MINA-USDT,MIR-USDT,MITH-USDT,MKR-USDT,MLN-USDT,MOF-USDT,MON-USDT,MOVR-USDT,MOVEZ-USDT,MXC-USDT,MXT-USDT,NAS-USDT,NEAR-USDT,NEO-USDT,NFT-USDT,NMR-USDT,NULS-USDT,NYM-USDT,OM-USDT,OMG-USDT,OMI-USDT,ONE-USDT,ONT-USDT,OP-USDT,ORBS-USDT,ORB-USDT,ORS-USDT,OXT-USDT,PAY-USDT,PCI-USDT,PEOPLE-USDT,PERP-USDT,PHA-USDT,PICKLE-USDT,PIT-USDT,PLG-USDT,PNK-USDT,POLS-USDT,POLYDOGE-USDT,PPT-USDT,PRQ-USDT,PST-USDT,PSTAKE-USDT,QOM-USDT,QTUM-USDT,RACA-USDT,RAY-USDT,REN-USDT,REP-USDT,REVV-USDT,RFUEL-USDT,RIO-USDT,RNT-USDT,ROAD-USDT,RON-USDT,RSR-USDT,RSS3-USDT,RVN-USDT,SAITAMA-USDT,SAMO-USDT,SAND-USDT,SC-USDT,SD-USDT,SFG-USDT,SHIB-USDT,SIS-USDT,SKEB-USDT,SKL-USDT,SLP-USDT,SNT-USDT,SNX-USDT,SOC-USDT,SOL-USDT,SOS-USDT,SPELL-USDT,SRM-USDT,STARL-USDT,STC-USDT,STORJ-USDT,STRK-USDT,STX-USDT,SUN-USDT,SUSHI-USDT,SWEAT-USDT,SWFTC-USDT,SWRV-USDT,T-USDT,TAI-USDT,TAKI-USDT,TCT-USDT,THETA-USDT,THG-USDT,TON-USDT,TOPC-USDT,TORN-USDT,TOWN-USDT,TRADE-USDT,TRA-USDT,TRB-USDT,TRUE-USDT,TRX-USDT,TUP-USDT,TUSD-USDT,UMA-USDT,UMEE-USDT,UNI-USDT,USDC-USDT,USDP-USDT,USTC-USDT,UTK-USDT,VALUE-USDT,VELO-USDT,VRA-USDT,VSYS-USDT,WAVES-USDT,WAXP-USDT,WBTC-USDT,WEMIX-USDT,WGRT-USDT,WING-USDT,WIN-USDT,WNCG-USDT,WNXM-USDT,WOO-USDT,WSB-USDT,WXT-USDT,XAUT-USDT,XCH-USDT,XEC-USDT,XEM-USDT,XETA-USDT,XLM-USDT,XMR-USDT,XNO-USDT,XPR-USDT,XTZ-USDT,YEE-USDT,YFI-USDT,YFII-USDT,YGG-USDT,YOU-USDT,YOYO-USDT,ZBC-USDT,ZEC-USDT,ZEN-USDT,ZIL-USDT,ZKS-USDT,ZRX-USDT,ZYRO-USDT,BTC-USDC,ETH-USDC,ETH-BTC,OKB-USDC,OKT-USDC,LTC-USDC,DOT-USDC,DOGE-USDC,LUNC-USDC,ETHW-USDC,LUNA-USDC,FIL-USDC,XRP-USDC,1INCH-USDC,AAVE-USDC,ADA-USDC,AGLD-USDC,ALGO-USDC,ANC-USDC,ANT-USDC,APE-USDC,API3-USDC,AR-USDC,ASTR-USDC,ATOM-USDC,AVAX-USDC,AXS-USDC,AZY-USDC,BABYDOGE-USDC,BAT-USDC,BCH-USDC,BICO-USDC,BSV-USDC,CEL-USDC,CELO-USDC,CELT-USDC,CHZ-USDC,COMP-USDC,CRO-USDC,CRV-USDC,CSPR-USDC,DASH-USDC,DEP-USDC,DOME-USDC,DYDX-USDC,EGLD-USDC,ELT-USDC,ENS-USDC,EOS-USDC,ETC-USDC,FITFI-USDC,FLM-USDC,FLOW-USDC,FTM-USDC,GALA-USDC,GALFT-USDC,GARI-USDC,GLMR-USDC,GMT-USDC,GODS-USDC,GRT-USDC,HBAR-USDC,ICP-USDC,IMX-USDC,IOST-USDC,JST-USDC,KISHU-USDC,KLAY-USDC,KNC-USDC,KSM-USDC,LINK-USDC,LOOKS-USDC,LRC-USDC,MANA-USDC,MASK-USDC,MATIC-USDC,MINA-USDC,MKR-USDC,MOF-USDC,MOVEZ-USDC,MXC-USDC,NEAR-USDC,NFT-USDC,NMR-USDC,NYM-USDC,OMG-USDC,OP-USDC,PEOPLE-USDC,PERP-USDC,RACA-USDC,RSR-USDC,SAITAMA-USDC,SAND-USDC,SHIB-USDC,SLP-USDC,SNX-USDC,SOC-USDC,SOL-USDC,SOS-USDC,SRM-USDC,STARL-USDC,STC-USDC,STORJ-USDC,STX-USDC,SUSHI-USDC,SWFTC-USDC,THETA-USDC,TON-USDC,TORN-USDC,TRB-USDC,TRX-USDC,UNI-USDC,USDP-USDC,USTC-USDC,VRA-USDC,WAVES-USDC,XCH-USDC,XEM-USDC,XLM-USDC,XMR-USDC,XNO-USDC,XTZ-USDC,YFI-USDC,YFII-USDC,YGG-USDC,ZEC-USDC,ZIL-USDC,BTC-DAI,ETH-DAI,BTC-USDK,ETH-USDK,USDT-USDK,OKB-BTC,OKT-BTC,LTC-BTC,DOT-BTC,DOGE-BTC,FIL-BTC,XRP-BTC,AAVE-BTC,ADA-BTC,ALGO-BTC,ALPHA-BTC,ANT-BTC,ATOM-BTC,AVAX-BTC,BADGER-BTC,BAT-BTC,BCD-BTC,BCH-BTC,BNT-BTC,BSV-BTC,BTG-BTC,BTM-BTC,CELO-BTC,CELT-BTC,CHZ-BTC,COMP-BTC,CQT-BTC,CRO-BTC,CRV-BTC,CTC-BTC,CVC-BTC,DASH-BTC,DCR-BTC,DGB-BTC,EGLD-BTC,ELF-BTC,ENJ-BTC,EOS-BTC,ETC-BTC,FLOW-BTC,GAS-BTC,GRT-BTC,GTO-BTC,HBAR-BTC,HC-BTC,ICP-BTC,ICX-BTC,INT-BTC,IOST-BTC,IOTA-BTC,KLAY-BTC,KNC-BTC,KSM-BTC,LINK-BTC,LRC-BTC,LSK-BTC,MANA-BTC,MITH-BTC,MKR-BTC,NAS-BTC,NEAR-BTC,NEO-BTC,NULS-BTC,OMG-BTC,ONT-BTC,PST-BTC,QTUM-BTC,REN-BTC,RSR-BTC,RVN-BTC,SC-BTC,SNT-BTC,SOL-BTC,SRM-BTC,STX-BTC,SWFTC-BTC,THETA-BTC,TRUE-BTC,TRX-BTC,UNI-BTC,VSYS-BTC,WAVES-BTC,WBTC-BTC,WXT-BTC,XCH-BTC,XEM-BTC,XLM-BTC,XMR-BTC,XTZ-BTC,YFI-BTC,YOU-BTC,ZEC-BTC,ZEN-BTC,ZIL-BTC,ZRX-BTC,OKB-ETH,OKT-ETH,LTC-ETH,DOT-ETH,DOGE-ETH,FIL-ETH,XRP-ETH,AAVE-ETH,ADA-ETH,API3-ETH,ATOM-ETH,AVAX-ETH,BETH-ETH,CRV-ETH,DASH-ETH,EOS-ETH,ETC-ETH,FLOW-ETH,GAS-ETH,GHST-ETH,HEGIC-ETH,INT-ETH,IOST-ETH,KSM-ETH,LINK-ETH,MANA-ETH,MKR-ETH,NEAR-ETH,NEO-ETH,NULS-ETH,OM-ETH,QTUM-ETH,SNX-ETH,SOL-ETH,SUSHI-ETH,SWFTC-ETH,TRX-ETH,UNI-ETH,WBTC-ETH,XLM-ETH,XMR-ETH,YFI-ETH,ZEC-ETH,LTC-OKB,XRP-OKB,ETC-OKB,OKDOT1-DOT,OKDOT2-DOT,BTC-EURT,ETH-EURT" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "credentials": { + "key": "", + "secret": "", + "clientID": "" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true + }, + "urlEndpoints": { + "RestSpotURL": "https://www.okx.com/api/v5/", + "WebsocketSpotURL": "wss://ws.okx.com:8443/ws/v5/public" + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "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": "Binanceus", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "baseCurrencies": "USD", - "currencyPairs": { - "bypassConfigFormatUpgrades": false, - "pairs": { + "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": "-" - } + "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": { + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "credentials": { "key": "", "secret": "" - }, - "credentialsValidator": { + }, + "credentialsValidator": { "requiresKey": true, "requiresSecret": true - }, - "urlEndpoints": { + }, + "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": { + } + }, + "features": { + "supports": { "restAPI": true, "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true + "tickerBatching": true, + "autoPairUpdates": true }, "websocketAPI": true, "websocketCapabilities": {} - }, - "enabled": { + }, + "enabled": { "autoPairUpdates": true, "websocketAPI": true, "saveTradeData": false, "tradeFeed": false, "fillsFeed": false - } - }, - "bankAccounts": [ - { + } + }, + "bankAccounts": [ + { "enabled": false, "bankName": "", "bankAddress": "", @@ -428,2030 +428,2030 @@ "swiftCode": "", "iban": "", "supportedCurrencies": "" - } - ], - "orderbook": { - "verificationBypass": false, - "websocketBufferLimit": 5, - "websocketBufferEnabled": false, - "publishPeriod": 10000000000 } + ], + "orderbook": { + "verificationBypass": false, + "websocketBufferLimit": 5, + "websocketBufferEnabled": false, + "publishPeriod": 10000000000 + } }, - { - "name": "Binance", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": true - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC-USDT,DOGE-USDT", - "available": "ETH-BTC,LTC-BTC,BNB-BTC,NEO-BTC,QTUM-ETH,EOS-ETH,SNT-ETH,BNT-ETH,GAS-BTC,BNB-ETH,BTC-USDT,ETH-USDT,OAX-ETH,DNT-ETH,MCO-ETH,MCO-BTC,WTC-BTC,WTC-ETH,LRC-BTC,LRC-ETH,QTUM-BTC,YOYO-BTC,OMG-BTC,OMG-ETH,ZRX-BTC,ZRX-ETH,STRAT-BTC,STRAT-ETH,SNGLS-BTC,BQX-BTC,BQX-ETH,KNC-BTC,KNC-ETH,FUN-BTC,FUN-ETH,SNM-BTC,SNM-ETH,NEO-ETH,IOTA-BTC,IOTA-ETH,LINK-BTC,LINK-ETH,XVG-BTC,XVG-ETH,MDA-BTC,MDA-ETH,MTL-BTC,MTL-ETH,EOS-BTC,SNT-BTC,ETC-ETH,ETC-BTC,MTH-BTC,MTH-ETH,ENG-BTC,ENG-ETH,DNT-BTC,ZEC-BTC,ZEC-ETH,BNT-BTC,AST-BTC,AST-ETH,DASH-BTC,DASH-ETH,OAX-BTC,BTG-BTC,BTG-ETH,EVX-BTC,EVX-ETH,REQ-BTC,REQ-ETH,VIB-BTC,VIB-ETH,TRX-BTC,TRX-ETH,POWR-BTC,POWR-ETH,ARK-BTC,ARK-ETH,YOYO-ETH,XRP-BTC,XRP-ETH,ENJ-BTC,ENJ-ETH,STORJ-BTC,STORJ-ETH,BNB-USDT,YOYO-BNB,POWR-BNB,KMD-BTC,KMD-ETH,NULS-BNB,RCN-BTC,RCN-ETH,RCN-BNB,NULS-BTC,NULS-ETH,RDN-BTC,RDN-ETH,RDN-BNB,XMR-BTC,XMR-ETH,DLT-BNB,WTC-BNB,DLT-BTC,DLT-ETH,AMB-BTC,AMB-ETH,AMB-BNB,BAT-BTC,BAT-ETH,BAT-BNB,BCPT-BTC,BCPT-ETH,BCPT-BNB,ARN-BTC,ARN-ETH,GVT-BTC,GVT-ETH,CDT-BTC,CDT-ETH,GXS-BTC,GXS-ETH,NEO-USDT,NEO-BNB,POE-BTC,POE-ETH,QSP-BTC,QSP-ETH,QSP-BNB,BTS-BTC,BTS-ETH,XZC-BTC,XZC-ETH,XZC-BNB,LSK-BTC,LSK-ETH,LSK-BNB,TNT-BTC,TNT-ETH,FUEL-BTC,MANA-BTC,MANA-ETH,BCD-BTC,BCD-ETH,DGD-BTC,DGD-ETH,IOTA-BNB,ADX-BTC,ADX-ETH,ADA-BTC,ADA-ETH,PPT-BTC,PPT-ETH,CMT-BTC,CMT-ETH,CMT-BNB,XLM-BTC,XLM-ETH,XLM-BNB,CND-BTC,CND-ETH,CND-BNB,LEND-BTC,LEND-ETH,WABI-BTC,WABI-ETH,WABI-BNB,LTC-ETH,LTC-USDT,LTC-BNB,TNB-BTC,TNB-ETH,WAVES-BTC,WAVES-ETH,WAVES-BNB,GTO-BTC,GTO-ETH,GTO-BNB,ICX-BTC,ICX-ETH,ICX-BNB,OST-BTC,OST-ETH,OST-BNB,ELF-BTC,ELF-ETH,AION-BTC,AION-ETH,AION-BNB,NEBL-BTC,NEBL-ETH,NEBL-BNB,BRD-BTC,BRD-ETH,BRD-BNB,MCO-BNB,EDO-BTC,EDO-ETH,NAV-BTC,LUN-BTC,APPC-BTC,APPC-ETH,APPC-BNB,VIBE-BTC,VIBE-ETH,RLC-BTC,RLC-ETH,RLC-BNB,INS-BTC,INS-ETH,PIVX-BTC,PIVX-ETH,PIVX-BNB,IOST-BTC,IOST-ETH,STEEM-BTC,STEEM-ETH,STEEM-BNB,NANO-BTC,NANO-ETH,NANO-BNB,VIA-BTC,VIA-ETH,VIA-BNB,BLZ-BTC,BLZ-ETH,BLZ-BNB,AE-BTC,AE-ETH,AE-BNB,NCASH-BTC,NCASH-ETH,POA-BTC,POA-ETH,ZIL-BTC,ZIL-ETH,ZIL-BNB,ONT-BTC,ONT-ETH,ONT-BNB,STORM-BTC,STORM-ETH,STORM-BNB,QTUM-BNB,QTUM-USDT,XEM-BTC,XEM-ETH,XEM-BNB,WAN-BTC,WAN-ETH,WAN-BNB,WPR-BTC,WPR-ETH,QLC-BTC,QLC-ETH,SYS-BTC,SYS-ETH,SYS-BNB,QLC-BNB,GRS-BTC,GRS-ETH,ADA-USDT,ADA-BNB,GNT-BTC,GNT-ETH,LOOM-BTC,LOOM-ETH,LOOM-BNB,XRP-USDT,REP-BTC,REP-ETH,BTC-TUSD,ETH-TUSD,ZEN-BTC,ZEN-ETH,ZEN-BNB,SKY-BTC,SKY-ETH,SKY-BNB,EOS-USDT,EOS-BNB,CVC-BTC,CVC-ETH,THETA-BTC,THETA-ETH,THETA-BNB,XRP-BNB,TUSD-USDT,IOTA-USDT,XLM-USDT,IOTX-BTC,IOTX-ETH,QKC-BTC,QKC-ETH,AGI-BTC,AGI-ETH,AGI-BNB,NXS-BTC,NXS-ETH,NXS-BNB,ENJ-BNB,DATA-BTC,DATA-ETH,ONT-USDT,TRX-BNB,TRX-USDT,ETC-USDT,ETC-BNB,ICX-USDT,SC-BTC,SC-ETH,SC-BNB,NPXS-ETH,KEY-BTC,KEY-ETH,NAS-BTC,NAS-ETH,NAS-BNB,MFT-BTC,MFT-ETH,MFT-BNB,DENT-ETH,ARDR-BTC,ARDR-ETH,NULS-USDT,HOT-BTC,HOT-ETH,VET-BTC,VET-ETH,VET-USDT,VET-BNB,DOCK-BTC,DOCK-ETH,POLY-BTC,POLY-BNB,HC-BTC,HC-ETH,GO-BTC,GO-BNB,PAX-USDT,RVN-BTC,RVN-BNB,DCR-BTC,DCR-BNB,MITH-BTC,MITH-BNB,BNB-PAX,BTC-PAX,ETH-PAX,XRP-PAX,EOS-PAX,XLM-PAX,REN-BTC,REN-BNB,BNB-TUSD,XRP-TUSD,EOS-TUSD,XLM-TUSD,BNB-USDC,BTC-USDC,ETH-USDC,XRP-USDC,EOS-USDC,XLM-USDC,USDC-USDT,ADA-TUSD,TRX-TUSD,NEO-TUSD,TRX-XRP,XZC-XRP,PAX-TUSD,USDC-TUSD,USDC-PAX,LINK-USDT,LINK-TUSD,LINK-PAX,LINK-USDC,WAVES-USDT,WAVES-TUSD,WAVES-USDC,LTC-TUSD,LTC-PAX,LTC-USDC,TRX-PAX,TRX-USDC,BTT-BNB,BTT-USDT,BNB-USDS,BTC-USDS,USDS-USDT,USDS-PAX,USDS-TUSD,USDS-USDC,BTT-PAX,BTT-TUSD,BTT-USDC,ONG-BNB,ONG-BTC,ONG-USDT,HOT-BNB,HOT-USDT,ZIL-USDT,ZRX-BNB,ZRX-USDT,FET-BNB,FET-BTC,FET-USDT,BAT-USDT,XMR-BNB,XMR-USDT,ZEC-BNB,ZEC-USDT,ZEC-PAX,ZEC-TUSD,ZEC-USDC,IOST-BNB,IOST-USDT,CELR-BNB,CELR-BTC,CELR-USDT,ADA-PAX,ADA-USDC,NEO-PAX,NEO-USDC,DASH-BNB,DASH-USDT,NANO-USDT,OMG-BNB,OMG-USDT,THETA-USDT,ENJ-USDT,MITH-USDT,MATIC-BNB,MATIC-BTC,MATIC-USDT,ATOM-BNB,ATOM-BTC,ATOM-USDT,ATOM-USDC,ATOM-TUSD,ETC-TUSD,BAT-USDC,BAT-PAX,BAT-TUSD,PHB-BNB,PHB-BTC,PHB-TUSD,TFUEL-BNB,TFUEL-BTC,TFUEL-USDT,ONE-BNB,ONE-BTC,ONE-USDT,ONE-USDC,FTM-BNB,FTM-BTC,FTM-USDT,FTM-USDC,ALGO-BNB,ALGO-BTC,ALGO-USDT,ALGO-TUSD,ALGO-PAX,ALGO-USDC,GTO-USDT,ERD-BNB,ERD-BTC,ERD-USDT,DOGE-BNB,DOGE-BTC,DOGE-USDT,DUSK-BNB,DUSK-BTC,DUSK-USDT,DUSK-USDC,DUSK-PAX,BGBP-USDC,ANKR-BNB,ANKR-BTC,ANKR-USDT,ONT-PAX,ONT-USDC,WIN-BNB,WIN-USDT,WIN-USDC,COS-BNB,COS-BTC,COS-USDT,NPXS-USDT,COCOS-BNB,COCOS-BTC,COCOS-USDT,MTL-USDT,TOMO-BNB,TOMO-BTC,TOMO-USDT,TOMO-USDC,PERL-BNB,PERL-BTC,PERL-USDT,DENT-USDT,MFT-USDT,KEY-USDT,STORM-USDT,DOCK-USDT,WAN-USDT,FUN-USDT,CVC-USDT,BTT-TRX,WIN-TRX,CHZ-BNB,CHZ-BTC,CHZ-USDT,BAND-BNB,BAND-BTC,BAND-USDT,BNB-BUSD,BTC-BUSD,BUSD-USDT,BEAM-BNB,BEAM-BTC,BEAM-USDT,XTZ-BNB,XTZ-BTC,XTZ-USDT,REN-USDT,RVN-USDT,HC-USDT,HBAR-BNB,HBAR-BTC,HBAR-USDT,NKN-BNB,NKN-BTC,NKN-USDT,XRP-BUSD,ETH-BUSD,LTC-BUSD,LINK-BUSD,ETC-BUSD,STX-BNB,STX-BTC,STX-USDT,KAVA-BNB,KAVA-BTC,KAVA-USDT,BUSD-NGN,BNB-NGN,BTC-NGN,ARPA-BNB,ARPA-BTC,ARPA-USDT,TRX-BUSD,EOS-BUSD,IOTX-USDT,RLC-USDT,MCO-USDT,XLM-BUSD,ADA-BUSD,CTXC-BNB,CTXC-BTC,CTXC-USDT,BCH-BNB,BCH-BTC,BCH-USDT,BCH-USDC,BCH-TUSD,BCH-PAX,BCH-BUSD,BTC-RUB,ETH-RUB,XRP-RUB,BNB-RUB,TROY-BNB,TROY-BTC,TROY-USDT,BUSD-RUB,QTUM-BUSD,VET-BUSD" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} - }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "Bitfinex", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": true - }, - "configFormat": { - "uppercase": true - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC", - "available": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC,BTCEUR,BTCJPY,XRPUSD,XRPBTC,IOTUSD,IOTBTC,IOTETH,EOSUSD,EOSBTC,EOSETH,SANUSD,SANBTC,SANETH,OMGUSD,OMGBTC,OMGETH,NEOUSD,NEOBTC,NEOETH,ETPUSD,ETPBTC,ETPETH,QTMUSD,QTMBTC,QTMETH,AVTUSD,AVTBTC,AVTETH,EDOUSD,EDOBTC,EDOETH,BTGUSD,BTGBTC,DATUSD,DATBTC,DATETH,QSHUSD,QSHBTC,QSHETH,YYWUSD,YYWBTC,YYWETH,GNTUSD,GNTBTC,GNTETH,SNTUSD,SNTBTC,SNTETH,IOTEUR,BATUSD,BATBTC,BATETH,MNAUSD,MNABTC,MNAETH,FUNUSD,FUNBTC,FUNETH,ZRXUSD,ZRXBTC,ZRXETH,TNBUSD,TNBBTC,TNBETH,SPKUSD,SPKBTC,SPKETH,TRXUSD,TRXBTC,TRXETH,RCNUSD,RCNBTC,RCNETH,RLCUSD,RLCBTC,RLCETH,AIDUSD,AIDBTC,AIDETH,SNGUSD,SNGBTC,SNGETH,REPUSD,REPBTC,REPETH,ELFUSD,ELFBTC,ELFETH,NECUSD,NECBTC,NECETH,BTCGBP,ETHEUR,ETHJPY,ETHGBP,NEOEUR,NEOJPY,NEOGBP,EOSEUR,EOSJPY,EOSGBP,IOTJPY,IOTGBP,IOSUSD,IOSBTC,IOSETH,AIOUSD,AIOBTC,AIOETH,REQUSD,REQBTC,REQETH,RDNUSD,RDNBTC,RDNETH,LRCUSD,LRCBTC,LRCETH,WAXUSD,WAXBTC,WAXETH,DAIUSD,DAIBTC,DAIETH,AGIUSD,AGIBTC,AGIETH,BFTUSD,BFTBTC,BFTETH,MTNUSD,MTNBTC,MTNETH,ODEUSD,ODEBTC,ODEETH,ANTUSD,ANTBTC,ANTETH,DTHUSD,DTHBTC,DTHETH,MITUSD,MITBTC,MITETH,STJUSD,STJBTC,STJETH,XLMUSD,XLMEUR,XLMJPY,XLMGBP,XLMBTC,XLMETH,XVGUSD,XVGEUR,XVGJPY,XVGGBP,XVGBTC,XVGETH,BCIUSD,BCIBTC,MKRUSD,MKRBTC,MKRETH,KNCUSD,KNCBTC,KNCETH,POAUSD,POABTC,POAETH,EVTUSD,LYMUSD,LYMBTC,LYMETH,UTKUSD,UTKBTC,UTKETH,VEEUSD,VEEBTC,VEEETH,DADUSD,DADBTC,DADETH,ORSUSD,ORSBTC,ORSETH,AUCUSD,AUCBTC,AUCETH,POYUSD,POYBTC,POYETH,FSNUSD,FSNBTC,FSNETH,CBTUSD,CBTBTC,CBTETH,ZCNUSD,ZCNBTC,ZCNETH,SENUSD,SENBTC,SENETH,NCAUSD,NCABTC,NCAETH,CNDUSD,CNDBTC,CNDETH,CTXUSD,CTXBTC,CTXETH,PAIUSD,PAIBTC,SEEUSD,SEEBTC,SEEETH,ESSUSD,ESSBTC,ESSETH,ATMUSD,ATMBTC,ATMETH,HOTUSD,HOTBTC,HOTETH,DTAUSD,DTABTC,DTAETH,IQXUSD,IQXBTC,IQXEOS,WPRUSD,WPRBTC,WPRETH,ZILUSD,ZILBTC,ZILETH,BNTUSD,BNTBTC,BNTETH,ABSUSD,ABSETH,XRAUSD,XRAETH,MANUSD,MANETH,BBNUSD,BBNETH,NIOUSD,NIOETH,DGXUSD,DGXETH,VETUSD,VETBTC,VETETH,UTNUSD,UTNETH,TKNUSD,TKNETH,GOTUSD,GOTEUR,GOTETH,XTZUSD,XTZBTC,CNNUSD,CNNETH,BOXUSD,BOXETH,TRXEUR,TRXGBP,TRXJPY,MGOUSD,MGOETH,RTEUSD,RTEETH,YGGUSD,YGGETH,MLNUSD,MLNETH,WTCUSD,WTCETH,CSXUSD,CSXETH,OMNUSD,OMNBTC,INTUSD,INTETH,DRNUSD,DRNETH,PNKUSD,PNKETH,DGBUSD,DGBBTC,BSVUSD,BSVBTC,BABUSD,BABBTC,WLOUSD,WLOXLM,VLDUSD,VLDETH,ENJUSD,ENJETH,ONLUSD,ONLETH,RBTUSD,RBTBTC,USTUSD,EUTEUR,EUTUSD,GSDUSD,UDCUSD,TSDUSD,PAXUSD,RIFUSD,RIFBTC,PASUSD,PASETH,VSYUSD,VSYBTC,ZRXDAI,MKRDAI,OMGDAI,BTTUSD,BTTBTC,BTCUST,ETHUST,CLOUSD,CLOBTC,IMPUSD,IMPETH,LTCUST,EOSUST,BABUST,SCRUSD,SCRETH,GNOUSD,GNOETH,GENUSD,GENETH,ATOUSD,ATOBTC,ATOETH,WBTUSD,XCHUSD,EUSUSD,WBTETH,XCHETH,EUSETH,LEOUSD,LEOBTC,LEOUST,LEOEOS,LEOETH,ASTUSD,ASTETH,FOAUSD,FOAETH,UFRUSD,UFRETH,ZBTUSD,ZBTUST,OKBUSD,USKUSD,GTXUSD,KANUSD,OKBUST,OKBETH,OKBBTC,USKUST,USKETH,USKBTC,USKEOS,GTXUST,KANUST,AMPUSD,ALGUSD,ALGBTC,ALGUST,BTCXCH,SWMUSD,SWMETH,TRIUSD,TRIETH,LOOUSD,LOOETH,AMPUST,DUSK:USD,DUSK:BTC,UOSUSD,UOSBTC,RRBUSD,RRBUST,DTXUSD,DTXUST,AMPBTC,FTTUSD,FTTUST,PAXUST,UDCUST,TSDUST,BTC:CNHT,UST:CNHT,CNH:CNHT,CHZUSD,CHZUST,BTCF0:USTF0,ETHF0:USTF0" - } - } - }, - "api": { - "authenticatedSupport": true, - "authenticatedWebsocketApiSupport": true, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + "name": "Binance", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USDT,DOGE-USDT", + "available": "ETH-BTC,LTC-BTC,BNB-BTC,NEO-BTC,QTUM-ETH,EOS-ETH,SNT-ETH,BNT-ETH,GAS-BTC,BNB-ETH,BTC-USDT,ETH-USDT,OAX-ETH,DNT-ETH,MCO-ETH,MCO-BTC,WTC-BTC,WTC-ETH,LRC-BTC,LRC-ETH,QTUM-BTC,YOYO-BTC,OMG-BTC,OMG-ETH,ZRX-BTC,ZRX-ETH,STRAT-BTC,STRAT-ETH,SNGLS-BTC,BQX-BTC,BQX-ETH,KNC-BTC,KNC-ETH,FUN-BTC,FUN-ETH,SNM-BTC,SNM-ETH,NEO-ETH,IOTA-BTC,IOTA-ETH,LINK-BTC,LINK-ETH,XVG-BTC,XVG-ETH,MDA-BTC,MDA-ETH,MTL-BTC,MTL-ETH,EOS-BTC,SNT-BTC,ETC-ETH,ETC-BTC,MTH-BTC,MTH-ETH,ENG-BTC,ENG-ETH,DNT-BTC,ZEC-BTC,ZEC-ETH,BNT-BTC,AST-BTC,AST-ETH,DASH-BTC,DASH-ETH,OAX-BTC,BTG-BTC,BTG-ETH,EVX-BTC,EVX-ETH,REQ-BTC,REQ-ETH,VIB-BTC,VIB-ETH,TRX-BTC,TRX-ETH,POWR-BTC,POWR-ETH,ARK-BTC,ARK-ETH,YOYO-ETH,XRP-BTC,XRP-ETH,ENJ-BTC,ENJ-ETH,STORJ-BTC,STORJ-ETH,BNB-USDT,YOYO-BNB,POWR-BNB,KMD-BTC,KMD-ETH,NULS-BNB,RCN-BTC,RCN-ETH,RCN-BNB,NULS-BTC,NULS-ETH,RDN-BTC,RDN-ETH,RDN-BNB,XMR-BTC,XMR-ETH,DLT-BNB,WTC-BNB,DLT-BTC,DLT-ETH,AMB-BTC,AMB-ETH,AMB-BNB,BAT-BTC,BAT-ETH,BAT-BNB,BCPT-BTC,BCPT-ETH,BCPT-BNB,ARN-BTC,ARN-ETH,GVT-BTC,GVT-ETH,CDT-BTC,CDT-ETH,GXS-BTC,GXS-ETH,NEO-USDT,NEO-BNB,POE-BTC,POE-ETH,QSP-BTC,QSP-ETH,QSP-BNB,BTS-BTC,BTS-ETH,XZC-BTC,XZC-ETH,XZC-BNB,LSK-BTC,LSK-ETH,LSK-BNB,TNT-BTC,TNT-ETH,FUEL-BTC,MANA-BTC,MANA-ETH,BCD-BTC,BCD-ETH,DGD-BTC,DGD-ETH,IOTA-BNB,ADX-BTC,ADX-ETH,ADA-BTC,ADA-ETH,PPT-BTC,PPT-ETH,CMT-BTC,CMT-ETH,CMT-BNB,XLM-BTC,XLM-ETH,XLM-BNB,CND-BTC,CND-ETH,CND-BNB,LEND-BTC,LEND-ETH,WABI-BTC,WABI-ETH,WABI-BNB,LTC-ETH,LTC-USDT,LTC-BNB,TNB-BTC,TNB-ETH,WAVES-BTC,WAVES-ETH,WAVES-BNB,GTO-BTC,GTO-ETH,GTO-BNB,ICX-BTC,ICX-ETH,ICX-BNB,OST-BTC,OST-ETH,OST-BNB,ELF-BTC,ELF-ETH,AION-BTC,AION-ETH,AION-BNB,NEBL-BTC,NEBL-ETH,NEBL-BNB,BRD-BTC,BRD-ETH,BRD-BNB,MCO-BNB,EDO-BTC,EDO-ETH,NAV-BTC,LUN-BTC,APPC-BTC,APPC-ETH,APPC-BNB,VIBE-BTC,VIBE-ETH,RLC-BTC,RLC-ETH,RLC-BNB,INS-BTC,INS-ETH,PIVX-BTC,PIVX-ETH,PIVX-BNB,IOST-BTC,IOST-ETH,STEEM-BTC,STEEM-ETH,STEEM-BNB,NANO-BTC,NANO-ETH,NANO-BNB,VIA-BTC,VIA-ETH,VIA-BNB,BLZ-BTC,BLZ-ETH,BLZ-BNB,AE-BTC,AE-ETH,AE-BNB,NCASH-BTC,NCASH-ETH,POA-BTC,POA-ETH,ZIL-BTC,ZIL-ETH,ZIL-BNB,ONT-BTC,ONT-ETH,ONT-BNB,STORM-BTC,STORM-ETH,STORM-BNB,QTUM-BNB,QTUM-USDT,XEM-BTC,XEM-ETH,XEM-BNB,WAN-BTC,WAN-ETH,WAN-BNB,WPR-BTC,WPR-ETH,QLC-BTC,QLC-ETH,SYS-BTC,SYS-ETH,SYS-BNB,QLC-BNB,GRS-BTC,GRS-ETH,ADA-USDT,ADA-BNB,GNT-BTC,GNT-ETH,LOOM-BTC,LOOM-ETH,LOOM-BNB,XRP-USDT,REP-BTC,REP-ETH,BTC-TUSD,ETH-TUSD,ZEN-BTC,ZEN-ETH,ZEN-BNB,SKY-BTC,SKY-ETH,SKY-BNB,EOS-USDT,EOS-BNB,CVC-BTC,CVC-ETH,THETA-BTC,THETA-ETH,THETA-BNB,XRP-BNB,TUSD-USDT,IOTA-USDT,XLM-USDT,IOTX-BTC,IOTX-ETH,QKC-BTC,QKC-ETH,AGI-BTC,AGI-ETH,AGI-BNB,NXS-BTC,NXS-ETH,NXS-BNB,ENJ-BNB,DATA-BTC,DATA-ETH,ONT-USDT,TRX-BNB,TRX-USDT,ETC-USDT,ETC-BNB,ICX-USDT,SC-BTC,SC-ETH,SC-BNB,NPXS-ETH,KEY-BTC,KEY-ETH,NAS-BTC,NAS-ETH,NAS-BNB,MFT-BTC,MFT-ETH,MFT-BNB,DENT-ETH,ARDR-BTC,ARDR-ETH,NULS-USDT,HOT-BTC,HOT-ETH,VET-BTC,VET-ETH,VET-USDT,VET-BNB,DOCK-BTC,DOCK-ETH,POLY-BTC,POLY-BNB,HC-BTC,HC-ETH,GO-BTC,GO-BNB,PAX-USDT,RVN-BTC,RVN-BNB,DCR-BTC,DCR-BNB,MITH-BTC,MITH-BNB,BNB-PAX,BTC-PAX,ETH-PAX,XRP-PAX,EOS-PAX,XLM-PAX,REN-BTC,REN-BNB,BNB-TUSD,XRP-TUSD,EOS-TUSD,XLM-TUSD,BNB-USDC,BTC-USDC,ETH-USDC,XRP-USDC,EOS-USDC,XLM-USDC,USDC-USDT,ADA-TUSD,TRX-TUSD,NEO-TUSD,TRX-XRP,XZC-XRP,PAX-TUSD,USDC-TUSD,USDC-PAX,LINK-USDT,LINK-TUSD,LINK-PAX,LINK-USDC,WAVES-USDT,WAVES-TUSD,WAVES-USDC,LTC-TUSD,LTC-PAX,LTC-USDC,TRX-PAX,TRX-USDC,BTT-BNB,BTT-USDT,BNB-USDS,BTC-USDS,USDS-USDT,USDS-PAX,USDS-TUSD,USDS-USDC,BTT-PAX,BTT-TUSD,BTT-USDC,ONG-BNB,ONG-BTC,ONG-USDT,HOT-BNB,HOT-USDT,ZIL-USDT,ZRX-BNB,ZRX-USDT,FET-BNB,FET-BTC,FET-USDT,BAT-USDT,XMR-BNB,XMR-USDT,ZEC-BNB,ZEC-USDT,ZEC-PAX,ZEC-TUSD,ZEC-USDC,IOST-BNB,IOST-USDT,CELR-BNB,CELR-BTC,CELR-USDT,ADA-PAX,ADA-USDC,NEO-PAX,NEO-USDC,DASH-BNB,DASH-USDT,NANO-USDT,OMG-BNB,OMG-USDT,THETA-USDT,ENJ-USDT,MITH-USDT,MATIC-BNB,MATIC-BTC,MATIC-USDT,ATOM-BNB,ATOM-BTC,ATOM-USDT,ATOM-USDC,ATOM-TUSD,ETC-TUSD,BAT-USDC,BAT-PAX,BAT-TUSD,PHB-BNB,PHB-BTC,PHB-TUSD,TFUEL-BNB,TFUEL-BTC,TFUEL-USDT,ONE-BNB,ONE-BTC,ONE-USDT,ONE-USDC,FTM-BNB,FTM-BTC,FTM-USDT,FTM-USDC,ALGO-BNB,ALGO-BTC,ALGO-USDT,ALGO-TUSD,ALGO-PAX,ALGO-USDC,GTO-USDT,ERD-BNB,ERD-BTC,ERD-USDT,DOGE-BNB,DOGE-BTC,DOGE-USDT,DUSK-BNB,DUSK-BTC,DUSK-USDT,DUSK-USDC,DUSK-PAX,BGBP-USDC,ANKR-BNB,ANKR-BTC,ANKR-USDT,ONT-PAX,ONT-USDC,WIN-BNB,WIN-USDT,WIN-USDC,COS-BNB,COS-BTC,COS-USDT,NPXS-USDT,COCOS-BNB,COCOS-BTC,COCOS-USDT,MTL-USDT,TOMO-BNB,TOMO-BTC,TOMO-USDT,TOMO-USDC,PERL-BNB,PERL-BTC,PERL-USDT,DENT-USDT,MFT-USDT,KEY-USDT,STORM-USDT,DOCK-USDT,WAN-USDT,FUN-USDT,CVC-USDT,BTT-TRX,WIN-TRX,CHZ-BNB,CHZ-BTC,CHZ-USDT,BAND-BNB,BAND-BTC,BAND-USDT,BNB-BUSD,BTC-BUSD,BUSD-USDT,BEAM-BNB,BEAM-BTC,BEAM-USDT,XTZ-BNB,XTZ-BTC,XTZ-USDT,REN-USDT,RVN-USDT,HC-USDT,HBAR-BNB,HBAR-BTC,HBAR-USDT,NKN-BNB,NKN-BTC,NKN-USDT,XRP-BUSD,ETH-BUSD,LTC-BUSD,LINK-BUSD,ETC-BUSD,STX-BNB,STX-BTC,STX-USDT,KAVA-BNB,KAVA-BTC,KAVA-USDT,BUSD-NGN,BNB-NGN,BTC-NGN,ARPA-BNB,ARPA-BTC,ARPA-USDT,TRX-BUSD,EOS-BUSD,IOTX-USDT,RLC-USDT,MCO-USDT,XLM-BUSD,ADA-BUSD,CTXC-BNB,CTXC-BTC,CTXC-USDT,BCH-BNB,BCH-BTC,BCH-USDT,BCH-USDC,BCH-TUSD,BCH-PAX,BCH-BUSD,BTC-RUB,ETH-RUB,XRP-RUB,BNB-RUB,TROY-BNB,TROY-BTC,TROY-USDT,BUSD-RUB,QTUM-BUSD,VET-BUSD" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": true - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "Deutsche Bank Privat Und Geschaeftskunden AG", - "bankAddress": "Karlsruhe, 76125, GERMANY", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "GLOBAL TRADE SOLUTIONS GmbH", - "accountNumber": "DE51660700240057016802", - "swiftCode": "DEUTDEDB660", - "iban": "DE51660700240057016802", - "supportedCurrencies": "EUR,USD" + "name": "Bitfinex", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC", + "available": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC,BTCEUR,BTCJPY,XRPUSD,XRPBTC,IOTUSD,IOTBTC,IOTETH,EOSUSD,EOSBTC,EOSETH,SANUSD,SANBTC,SANETH,OMGUSD,OMGBTC,OMGETH,NEOUSD,NEOBTC,NEOETH,ETPUSD,ETPBTC,ETPETH,QTMUSD,QTMBTC,QTMETH,AVTUSD,AVTBTC,AVTETH,EDOUSD,EDOBTC,EDOETH,BTGUSD,BTGBTC,DATUSD,DATBTC,DATETH,QSHUSD,QSHBTC,QSHETH,YYWUSD,YYWBTC,YYWETH,GNTUSD,GNTBTC,GNTETH,SNTUSD,SNTBTC,SNTETH,IOTEUR,BATUSD,BATBTC,BATETH,MNAUSD,MNABTC,MNAETH,FUNUSD,FUNBTC,FUNETH,ZRXUSD,ZRXBTC,ZRXETH,TNBUSD,TNBBTC,TNBETH,SPKUSD,SPKBTC,SPKETH,TRXUSD,TRXBTC,TRXETH,RCNUSD,RCNBTC,RCNETH,RLCUSD,RLCBTC,RLCETH,AIDUSD,AIDBTC,AIDETH,SNGUSD,SNGBTC,SNGETH,REPUSD,REPBTC,REPETH,ELFUSD,ELFBTC,ELFETH,NECUSD,NECBTC,NECETH,BTCGBP,ETHEUR,ETHJPY,ETHGBP,NEOEUR,NEOJPY,NEOGBP,EOSEUR,EOSJPY,EOSGBP,IOTJPY,IOTGBP,IOSUSD,IOSBTC,IOSETH,AIOUSD,AIOBTC,AIOETH,REQUSD,REQBTC,REQETH,RDNUSD,RDNBTC,RDNETH,LRCUSD,LRCBTC,LRCETH,WAXUSD,WAXBTC,WAXETH,DAIUSD,DAIBTC,DAIETH,AGIUSD,AGIBTC,AGIETH,BFTUSD,BFTBTC,BFTETH,MTNUSD,MTNBTC,MTNETH,ODEUSD,ODEBTC,ODEETH,ANTUSD,ANTBTC,ANTETH,DTHUSD,DTHBTC,DTHETH,MITUSD,MITBTC,MITETH,STJUSD,STJBTC,STJETH,XLMUSD,XLMEUR,XLMJPY,XLMGBP,XLMBTC,XLMETH,XVGUSD,XVGEUR,XVGJPY,XVGGBP,XVGBTC,XVGETH,BCIUSD,BCIBTC,MKRUSD,MKRBTC,MKRETH,KNCUSD,KNCBTC,KNCETH,POAUSD,POABTC,POAETH,EVTUSD,LYMUSD,LYMBTC,LYMETH,UTKUSD,UTKBTC,UTKETH,VEEUSD,VEEBTC,VEEETH,DADUSD,DADBTC,DADETH,ORSUSD,ORSBTC,ORSETH,AUCUSD,AUCBTC,AUCETH,POYUSD,POYBTC,POYETH,FSNUSD,FSNBTC,FSNETH,CBTUSD,CBTBTC,CBTETH,ZCNUSD,ZCNBTC,ZCNETH,SENUSD,SENBTC,SENETH,NCAUSD,NCABTC,NCAETH,CNDUSD,CNDBTC,CNDETH,CTXUSD,CTXBTC,CTXETH,PAIUSD,PAIBTC,SEEUSD,SEEBTC,SEEETH,ESSUSD,ESSBTC,ESSETH,ATMUSD,ATMBTC,ATMETH,HOTUSD,HOTBTC,HOTETH,DTAUSD,DTABTC,DTAETH,IQXUSD,IQXBTC,IQXEOS,WPRUSD,WPRBTC,WPRETH,ZILUSD,ZILBTC,ZILETH,BNTUSD,BNTBTC,BNTETH,ABSUSD,ABSETH,XRAUSD,XRAETH,MANUSD,MANETH,BBNUSD,BBNETH,NIOUSD,NIOETH,DGXUSD,DGXETH,VETUSD,VETBTC,VETETH,UTNUSD,UTNETH,TKNUSD,TKNETH,GOTUSD,GOTEUR,GOTETH,XTZUSD,XTZBTC,CNNUSD,CNNETH,BOXUSD,BOXETH,TRXEUR,TRXGBP,TRXJPY,MGOUSD,MGOETH,RTEUSD,RTEETH,YGGUSD,YGGETH,MLNUSD,MLNETH,WTCUSD,WTCETH,CSXUSD,CSXETH,OMNUSD,OMNBTC,INTUSD,INTETH,DRNUSD,DRNETH,PNKUSD,PNKETH,DGBUSD,DGBBTC,BSVUSD,BSVBTC,BABUSD,BABBTC,WLOUSD,WLOXLM,VLDUSD,VLDETH,ENJUSD,ENJETH,ONLUSD,ONLETH,RBTUSD,RBTBTC,USTUSD,EUTEUR,EUTUSD,GSDUSD,UDCUSD,TSDUSD,PAXUSD,RIFUSD,RIFBTC,PASUSD,PASETH,VSYUSD,VSYBTC,ZRXDAI,MKRDAI,OMGDAI,BTTUSD,BTTBTC,BTCUST,ETHUST,CLOUSD,CLOBTC,IMPUSD,IMPETH,LTCUST,EOSUST,BABUST,SCRUSD,SCRETH,GNOUSD,GNOETH,GENUSD,GENETH,ATOUSD,ATOBTC,ATOETH,WBTUSD,XCHUSD,EUSUSD,WBTETH,XCHETH,EUSETH,LEOUSD,LEOBTC,LEOUST,LEOEOS,LEOETH,ASTUSD,ASTETH,FOAUSD,FOAETH,UFRUSD,UFRETH,ZBTUSD,ZBTUST,OKBUSD,USKUSD,GTXUSD,KANUSD,OKBUST,OKBETH,OKBBTC,USKUST,USKETH,USKBTC,USKEOS,GTXUST,KANUST,AMPUSD,ALGUSD,ALGBTC,ALGUST,BTCXCH,SWMUSD,SWMETH,TRIUSD,TRIETH,LOOUSD,LOOETH,AMPUST,DUSK:USD,DUSK:BTC,UOSUSD,UOSBTC,RRBUSD,RRBUST,DTXUSD,DTXUST,AMPBTC,FTTUSD,FTTUST,PAXUST,UDCUST,TSDUST,BTC:CNHT,UST:CNHT,CNH:CNHT,CHZUSD,CHZUST,BTCF0:USTF0,ETHF0:USTF0" + } + } + }, + "api": { + "authenticatedSupport": true, + "authenticatedWebsocketApiSupport": true, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "Deutsche Bank Privat Und Geschaeftskunden AG", + "bankAddress": "Karlsruhe, 76125, GERMANY", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "GLOBAL TRADE SOLUTIONS GmbH", + "accountNumber": "DE51660700240057016802", + "swiftCode": "DEUTDEDB660", + "iban": "DE51660700240057016802", + "supportedCurrencies": "EUR,USD" + }, + { + "enabled": false, + "bankName": "Deutsche Bank Privat Und Geschaeftskunden AG", + "bankAddress": "Karlsruhe, 76125, GERMANY", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "GLOBAL TRADE SOLUTIONS GmbH", + "accountNumber": "DE78660700240057016801", + "swiftCode": "DEUTDEDB660", + "iban": "DE78660700240057016801", + "supportedCurrencies": "JPY,GBP" + } + ] }, { - "enabled": false, - "bankName": "Deutsche Bank Privat Und Geschaeftskunden AG", - "bankAddress": "Karlsruhe, 76125, GERMANY", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "GLOBAL TRADE SOLUTIONS GmbH", - "accountNumber": "DE78660700240057016801", - "swiftCode": "DEUTDEDB660", - "iban": "DE78660700240057016801", - "supportedCurrencies": "JPY,GBP" - } - ] - }, - { - "name": "Bitflyer", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "JPY", - "currencyPairs": { - "requestFormat": { - "uppercase": true, - "delimiter": "_" - }, - "configFormat": { - "uppercase": true, - "delimiter": "_" + "name": "Bitflyer", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "JPY", + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "lastUpdated": 1566798411, + "assetTypes": [ + "spot", + "futures" + ], + "pairs": { + "spot": { + "enabled": "BTC_JPY,ETH_BTC,BCH_BTC", + "available": "BTC_JPY,FXBTC_JPY,ETH_BTC,BCH_BTC" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": false, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "useGlobalFormat": true, - "lastUpdated": 1566798411, - "assetTypes": [ - "spot", - "futures" - ], - "pairs": { - "spot": { - "enabled": "BTC_JPY,ETH_BTC,BCH_BTC", - "available": "BTC_JPY,FXBTC_JPY,ETH_BTC,BCH_BTC" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "autoPairUpdates": true - }, - "websocketAPI": false, - "websocketCapabilities": {} - }, - "enabled": { - "autoPairUpdates": false, - "websocketAPI": false - } - }, - "bankAccounts": [ - { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "Bithumb", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "KRW", - "currencyPairs": { - "requestFormat": { - "uppercase": true - }, - "configFormat": { - "uppercase": true, - "index": "KRW" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTCKRW,ETHKRW,ETCKRW,XRPKRW,BCHKRW,QTUMKRW,BTGKRW,EOSKRW", - "available": "BHPKRW,STEEMKRW,GTOKRW,ETCKRW,STRATKRW,FXKRW,MIXKRW,THETAKRW,QTUMKRW,ADAKRW,MCOKRW,INSKRW,RDNKRW,CONKRW,FABKRW,ETHKRW,HDACKRW,BTCKRW,POWRKRW,CMTKRW,LBAKRW,ETHOSKRW,HCKRW,ETZKRW,PPTKRW,XVGKRW,WTCKRW,TMTGKRW,LOOMKRW,WETKRW,ABTKRW,ITCKRW,GXCKRW,ORBSKRW,ICXKRW,BSVKRW,MXCKRW,MITHKRW,AEKRW,SALTKRW,ARNKRW,TRUEKRW,ENJKRW,GNTKRW,PLYKRW,REPKRW,ZRXKRW,BTGKRW,APISKRW,QKCKRW,LRCKRW,DVPKRW,DADKRW,CHRKRW,BCHKRW,NPXSKRW,PIVXKRW,AMOKRW,RNTKRW,XEMKRW,FCTKRW,WOMKRW,WAXPKRW,DACKRW,OMGKRW,PCMKRW,CROKRW,FNBKRW,ANKRKRW,EOSKRW,KNCKRW,OCNKRW,MTLKRW,XSRKRW,VALORKRW,TRVKRW,AUTOKRW,HYCKRW,AOAKRW,BTTKRW,MBLKRW,VETKRW,XRPKRW,ZILKRW,ELFKRW,LAMBKRW,POLYKRW,IOSTKRW,BZNTKRW,CTXCKRW,BATKRW,FZZKRW,PAYKRW,BCDKRW,SNTKRW,WAVESKRW,XLMKRW,LINKKRW,OGOKRW,WICCKRW,TRXKRW" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret", - "clientID": "ClientID" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": false, - "websocketCapabilities": {} - }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ - { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "Bitstamp", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD,EUR", - "currencyPairs": { - "requestFormat": { - "uppercase": true - }, - "configFormat": { - "uppercase": true - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", - "available": "LTCUSD,ETHUSD,XRPEUR,BCHUSD,BCHEUR,BTCEUR,XRPBTC,EURUSD,BCHBTC,LTCEUR,BTCUSD,LTCBTC,XRPUSD,ETHBTC,ETHEUR" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret", - "clientID": "ClientID" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true, - "requiresClientID": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} - }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "Bybit", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "baseCurrencies": "", - "currencyPairs": { - "bypassConfigFormatUpgrades": false, - "requestFormat": { - "uppercase": true - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "pairs": { - "coinmarginedfutures": { - "assetEnabled": true, - "enabled": "BTC-USD", - "available": "BTC-USD,XRP-USD,SOL-USD,ADA-USD,MANA-USD,LTC-USD,ETH-USD,EOS-USD,DOT-USD,BIT-USD" - }, - "futures": { - "assetEnabled": true, - "enabled": "BTCUSD-Z22", - "available": "BTCUSD-U22,BTCUSD-M22,BTCUSD-Z22,ETHUSD-U22,ETHUSD-M22" - }, - "spot": { - "assetEnabled": true, - "enabled": "BTC-USDT,ETH-USDT", - "available": "BTC-USDT,ETH-USDT,XRP-USDT,EOS-USDT,ETH-BTC,XRP-BTC,DOT-USDT,XLM-USDT,LTC-USDT,DOGE-USDT,BIT-USDT,CHZ-USDT,AXS-USDT,MANA-USDT,DYDX-USDT,MKR-USDT,COMP-USDT,AAVE-USDT,YFI-USDT,LINK-USDT,SUSHI-USDT,UNI-USDT,KSM-USDT,ICP-USDT,ADA-USDT,KLAY-USDT,XTZ-USDT,BCH-USDT,SRM-USDT,QNT-USDT,USDC-USDT,GRT-USDT,SOL-USDT,FIL-USDT,OMG-USDT,TRIBE-USDT,BAT-USDT,ZRX-USDT,CRV-USDT,AGLD-USDT,ANKR-USDT,PERP-USDT,MATIC-USDT,WAVES-USDT,LUNC-USDT,SPELL-USDT,SHIB-USDT,FTM-USDT,ATOM-USDT,ALGO-USDT,ENJ-USDT,CBX-USDT,SAND-USDT,AVAX-USDT,WOO-USDT,FTT-USDT,GODS-USDT,IMX-USDT,ENS-USDT,GM-USDT,CWAR-USDT,CAKE-USDT,STETH-USDT,GALFT-USDT,LFW-USDT,SLP-USDT,C98-USDT,PSP-USDT,GENE-USDT,AVA-USDT,ONE-USDT,PTU-USDT,SHILL-USDT,XYM-USDT,BOBA-USDT,INSUR-USDT,JASMY-USDT,GALA-USDT,RNDR-USDT,TRVL-USDT,WEMIX-USDT,XEM-USDT,KMA-USDT,BICO-USDT,CEL-USDT,UMA-USDT,HOT-USDT,NEXO-USDT,AMP-USDT,BNT-USDT,SNX-USDT,REN-USDT,1INCH-USDT,TEL-USDT,SIS-USDT,LRC-USDT,LDO-USDT,REAL-USDT,KRL-USDT,DEVT-USDT,CRAFT-USDT,BIT-BTC,BIT-USDC,ETH-USDC,BTC-USDC,1SOL-USDT,PLT-USDT,IZI-USDT,QTUM-USDT,DCR-USDT,ZEN-USDT,THETA-USDT,MX-USDT,DGB-USDT,RVN-USDT,EGLD-USDT,RUNE-USDT,XLM-BTC,XLM-USDC,SOL-USDC,XRP-USDC,ALGO-BTC,SOL-BTC,DFL-USDT,RAIN-USDT,RUN-USDT,XEC-USDT,ICX-USDT,XDC-USDT,HNT-USDT,BTG-USDT,ZIL-USDT,HBAR-USDT,FLOW-USDT,SOS-USDT,KASTA-USDT,GAS-USDT,STX-USDT,SIDUS-USDT,VPAD-USDT,GGM-USDT,LOOKS-USDT,MBS-USDT,DAI-USDT,BUSD-USDT,ACA-USDT,MV-USDT,MIX-USDT,LTC-USDC,MANA-BTC,MATIC-BTC,LTC-BTC,DOT-BTC,SAND-BTC,MANA-USDC,MATIC-USDC,SAND-USDC,DOT-USDC,LUNC-USDC,RSS3-USDT,SYNR-USDT,TAP-USDT,ERTHA-USDT,GMX-USDT,POSI-USDT,T-USDT,ACH-USDT,JST-USDT,SUN-USDT,BTT-USDT,TRX-USDT,NFT-USDT,POKT-USDT,SCRT-USDT,PSTAKE-USDT,SON-USDT,HERO-USDT,DOME-USDT,ZBC-USDT,USTC-USDT,BNB-USDT,NEAR-USDT,PAXG-USDT,SD-USDT,APE-USDT,BTC3S-USDT,BTC3L-USDT,FIDA-USDT,MINA-USDT,SC-USDT,RACA-USDT,IME-USDT,CAPS-USDT,STG-USDT,LMR-USDT,GLMR-USDT,MOVR-USDT,ZAM-USDT,ETH-DAI,BTC-DAI,WBTC-USDT,XAVA-USDT,MELOS-USDT,GMT-USDT,GST-USDT,CELO-USDT,SFUND-USDT,ELT-USDT,LGX-USDT,BIT-DAI,APEX-USDT,CTC-USDT,COT-USDT,KMON-USDT,PLY-USDT,XWG-USDT,FITFI-USDT,STRM-USDT,GAL-USDT,FCD-USDT,ETH3S-USDT,ETH3L-USDT,KOK-USDT,FAME-USDT,XRP3S-USDT,XRP3L-USDT,USDD-USDT,LUNA-USDT" - }, - "usdcmarginedfutures": { - "assetEnabled": true, - "enabled": "BTC-PERP", - "available": "BTC-PERP" - }, - "usdtmarginedfutures": { - "assetEnabled": true, - "enabled": "BTC-USDT", - "available": "BTC-USDT,AGLD-USDT,RAY-USDT,ASTR-USDT,XTZ-USDT,SFP-USDT,REEF-USDT,XEM-USDT,IOST-USDT,SLP-USDT,JASMY-USDT,ZEC-USDT,SC-USDT,MINA-USDT,HNT-USDT,XRP-USDT,MATIC-USDT,BNB-USDT,CHZ-USDT,OMG-USDT,ZEN-USDT,PEOPLE-USDT,DAR-USDT,SUN-USDT,DODO-USDT,AKRO-USDT,RUNE-USDT,YGG-USDT,XCN-USDT,FITFI-USDT,BIT-USDT,PAXG-USDT,KDA-USDT,AVAX-USDT,GALA-USDT,RVN-USDT,MTL-USDT,GAL-USDT,SXP-USDT,KAVA-USDT,CREAM-USDT,CKB-USDT,BSW-USDT,EOS-USDT,UNI-USDT,SUSHI-USDT,ATOM-USDT,LRC-USDT,SAND-USDT,GRT-USDT,CVC-USDT,1000BTT-USDT,FLM-USDT,VET-USDT,CELO-USDT,OGN-USDT,BCH-USDT,DOGE-USDT,CRV-USDT,LPT-USDT,DGB-USDT,TOMO-USDT,ETC-USDT,FLOW-USDT,STORJ-USDT,KLAY-USDT,SPELL-USDT,10000NFT-USDT,STMX-USDT,ALGO-USDT,DASH-USDT,API3-USDT,GLMR-USDT,ZRX-USDT,IOTX-USDT,QTUM-USDT,SKL-USDT,IOTA-USDT,KNC-USDT,ROSE-USDT,ARPA-USDT,SOL-USDT,BAT-USDT,ICX-USDT,ACH-USDT,XLM-USDT,SHIB1000-USDT,YFI-USDT,EGLD-USDT,REQ-USDT,APE-USDT,BOBA-USDT,TRX-USDT,COMP-USDT,CRO-USDT,ALPHA-USDT,LTC-USDT,AAVE-USDT,NEAR-USDT,ENS-USDT,RSR-USDT,STX-USDT,XMR-USDT,BNX-USDT,CVX-USDT,GST-USDT,ADA-USDT,SRM-USDT,CELR-USDT,C98-USDT,GTC-USDT,AR-USDT,SCRT-USDT,LINK-USDT,DOT-USDT,DYDX-USDT,ANKR-USDT,AUDIO-USDT,KSM-USDT,REN-USDT,IMX-USDT,MASK-USDT,WAVES-USDT,CTSI-USDT,LINA-USDT,MKR-USDT,ANT-USDT,DUSK-USDT,HOT-USDT,BSV-USDT,ALICE-USDT,GMT-USDT,BAKE-USDT,SNX-USDT,BICO-USDT,ZIL-USDT,FIL-USDT,ENJ-USDT,ICP-USDT,FTT-USDT,ILV-USDT,CTK-USDT,LOOKS-USDT,THETA-USDT,ONE-USDT,TLM-USDT,RNDR-USDT,DENT-USDT,NEO-USDT,OCEAN-USDT,ETH-USDT,AXS-USDT,COTI-USDT,JST-USDT,FXS-USDT,HBAR-USDT,CHR-USDT,1INCH-USDT,RSS3-USDT,1000XEC-USDT,BAL-USDT,CTC-USDT,MANA-USDT,FTM-USDT,WOO-USDT,LIT-USDT,BAND-USDT" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - }, - "urlEndpoints": { - "RestCoinMarginedFuturesURL": "https://api.bybit.com", - "RestFuturesURL": "https://api.bybit.com", - "RestSpotURL": "https://api.bybit.com", - "RestUSDTMarginedFuturesURL": "https://api.bybit.com", - "WebsocketSpotURL": "wss://stream.bybit.com/spot/quote/ws/v2", - "RestUSDCMarginedFuturesURL": "https://api.bybit.com" - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + "name": "Bithumb", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "KRW", + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "index": "KRW" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCKRW,ETHKRW,ETCKRW,XRPKRW,BCHKRW,QTUMKRW,BTGKRW,EOSKRW", + "available": "BHPKRW,STEEMKRW,GTOKRW,ETCKRW,STRATKRW,FXKRW,MIXKRW,THETAKRW,QTUMKRW,ADAKRW,MCOKRW,INSKRW,RDNKRW,CONKRW,FABKRW,ETHKRW,HDACKRW,BTCKRW,POWRKRW,CMTKRW,LBAKRW,ETHOSKRW,HCKRW,ETZKRW,PPTKRW,XVGKRW,WTCKRW,TMTGKRW,LOOMKRW,WETKRW,ABTKRW,ITCKRW,GXCKRW,ORBSKRW,ICXKRW,BSVKRW,MXCKRW,MITHKRW,AEKRW,SALTKRW,ARNKRW,TRUEKRW,ENJKRW,GNTKRW,PLYKRW,REPKRW,ZRXKRW,BTGKRW,APISKRW,QKCKRW,LRCKRW,DVPKRW,DADKRW,CHRKRW,BCHKRW,NPXSKRW,PIVXKRW,AMOKRW,RNTKRW,XEMKRW,FCTKRW,WOMKRW,WAXPKRW,DACKRW,OMGKRW,PCMKRW,CROKRW,FNBKRW,ANKRKRW,EOSKRW,KNCKRW,OCNKRW,MTLKRW,XSRKRW,VALORKRW,TRVKRW,AUTOKRW,HYCKRW,AOAKRW,BTTKRW,MBLKRW,VETKRW,XRPKRW,ZILKRW,ELFKRW,LAMBKRW,POLYKRW,IOSTKRW,BZNTKRW,CTXCKRW,BATKRW,FZZKRW,PAYKRW,BCDKRW,SNTKRW,WAVESKRW,XLMKRW,LINKKRW,OGOKRW,WICCKRW,TRXKRW" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "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": "LBank", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": false, - "delimiter": "_" - }, - "configFormat": { - "uppercase": false, - "delimiter": "_" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "eth_btc", - "available": "FBC_USDT,GALT_USDT,IOEX_USDT,OATH_USDT,BLOC_USDT,BTC_USDT,ETH_USDT,ETH_BTC,ABBC_BTC,KISC_ETH,BXA_USDT,ATP_USDT,MAT_USDT,SKY_BTC,RNT_USDT,VENA_USDT,GRIN_USDT,IDA_USDT,PNT_USDT,OPX_USDT,VTHO_BTC,AMO_ETH,UBEX_BTC,EOS_BTC,UBEX_USDT,TNS_BTC,SAIT_ETH,DAX_BTC,DAX_ETH,DALI_USDT,VET_USDT,BCH_BTC,BCH_USDT,NEO_USDT,QTUM_USDT,ZEC_USDT,VET_BTC,PAI_BTC,PNT_BTC,NEO_BTC,DASH_BTC,LTC_BTC,ETC_BTC,QTUM_BTC,ZEC_BTC,SC_BTC,BTS_BTC,CPX_BTC,XWC_BTC,FIL6_BTC,FIL12_BTC,FIL36_BTC,EOS_USDT,UT_ETH,ELA_ETH,VET_ETH,VTHO_ETH,PAI_ETH,HER_ETH,PTT_ETH,TAC_ETH,IDHUB_ETH,SSC_ETH,SKM_ETH,PLY_ETH,EXT_ETH,EOS_ETH,YOYOW_ETH,TRX_ETH,QTUM_ETH,ZEC_ETH,BTS_ETH,BTM_ETH,MITH_ETH,NAS_ETH,MAN_ETH,DBC_ETH,BTO_ETH,DDD_ETH,CPX_ETH,CS_ETH,IHT_ETH,OCN_ETH,EKO_ETH,XWC_ETH,PUT_ETH,PNT_ETH,AAC_ETH,FIL6_ETH,FIL12_ETH,FIL36_ETH,SEER_ETH,BSB_ETH,CDC_ETH,GRAMS_ETH,DDMX_ETH,EAI_ETH,BNB_USDT,HT_USDT,KBC_BTC,KBC_USDT,MAI_USDT,PHV_USDT,GT_USDT,VOKEN_USDT,CYE_USDT,BRC_USDT,BTC_AUSD,DDMX_USDT,SEAL_USDT,SEOS_BTC,BTY_USDT,FO_USDT,DLX_USDT,BFC_USDT,LBK_USDT,SERO_USDT,MTV_USDT,CKB_USDT,ARPA_USDT,ZIP_USDT,AT_USDT,DOT_USDT,DILI_USDT,DUO_USDT,TEP_USDT,BIKI_USDT,MX_USDT,DNS_USDT,OKB_USDT,FLDT_USDT,CCTC_USDT,WIN_USDT,BTT_USDT,TRX_USDT,GRS_BTC,GST_USDT,GST_ETH,ABBC_USDT,UTK_USDT,GKI_USDT,BPX_USDT,SUTER_USDT,LT_USDT,LM_USDT,HTDF_USDT" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": false, - "websocketCapabilities": {} + "name": "Bitstamp", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD,EUR", + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", + "available": "LTCUSD,ETHUSD,XRPEUR,BCHUSD,BCHEUR,BTCEUR,XRPBTC,EURUSD,BCHBTC,LTCEUR,BTCUSD,LTCBTC,XRPUSD,ETHBTC,ETHEUR" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "BTSE", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": true, - "delimiter": "-" - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC-USD", - "available": "BTC-CNY,BTC-EUR,BTC-GBP,BTC-HKD,BTC-JPY,BTC-SGD,BTC-USD,ETH-CNY,ETH-EUR,ETH-GBP,ETH-HKD,ETH-JPY,ETH-SGD,ETH-USD,LTC-CNY,LTC-EUR,LTC-GBP,LTC-HKD,LTC-JPY,LTC-SGD,LTC-USD,USDT-CNY,USDT-EUR,USDT-GBP,USDT-HKD,USDT-JPY,USDT-SGD,USDT-USD,XMR-CNY,XMR-EUR,XMR-GBP,XMR-HKD,XMR-JPY,XMR-SGD,XMR-USD" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + "name": "Bybit", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "baseCurrencies": "", + "currencyPairs": { + "bypassConfigFormatUpgrades": false, + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "pairs": { + "coinmarginedfutures": { + "assetEnabled": true, + "enabled": "BTC-USD", + "available": "BTC-USD,XRP-USD,SOL-USD,ADA-USD,MANA-USD,LTC-USD,ETH-USD,EOS-USD,DOT-USD,BIT-USD" + }, + "futures": { + "assetEnabled": true, + "enabled": "BTCUSD-Z22", + "available": "BTCUSD-U22,BTCUSD-M22,BTCUSD-Z22,ETHUSD-U22,ETHUSD-M22" + }, + "spot": { + "assetEnabled": true, + "enabled": "BTC-USDT,ETH-USDT", + "available": "BTC-USDT,ETH-USDT,XRP-USDT,EOS-USDT,ETH-BTC,XRP-BTC,DOT-USDT,XLM-USDT,LTC-USDT,DOGE-USDT,BIT-USDT,CHZ-USDT,AXS-USDT,MANA-USDT,DYDX-USDT,MKR-USDT,COMP-USDT,AAVE-USDT,YFI-USDT,LINK-USDT,SUSHI-USDT,UNI-USDT,KSM-USDT,ICP-USDT,ADA-USDT,KLAY-USDT,XTZ-USDT,BCH-USDT,SRM-USDT,QNT-USDT,USDC-USDT,GRT-USDT,SOL-USDT,FIL-USDT,OMG-USDT,TRIBE-USDT,BAT-USDT,ZRX-USDT,CRV-USDT,AGLD-USDT,ANKR-USDT,PERP-USDT,MATIC-USDT,WAVES-USDT,LUNC-USDT,SPELL-USDT,SHIB-USDT,FTM-USDT,ATOM-USDT,ALGO-USDT,ENJ-USDT,CBX-USDT,SAND-USDT,AVAX-USDT,WOO-USDT,FTT-USDT,GODS-USDT,IMX-USDT,ENS-USDT,GM-USDT,CWAR-USDT,CAKE-USDT,STETH-USDT,GALFT-USDT,LFW-USDT,SLP-USDT,C98-USDT,PSP-USDT,GENE-USDT,AVA-USDT,ONE-USDT,PTU-USDT,SHILL-USDT,XYM-USDT,BOBA-USDT,INSUR-USDT,JASMY-USDT,GALA-USDT,RNDR-USDT,TRVL-USDT,WEMIX-USDT,XEM-USDT,KMA-USDT,BICO-USDT,CEL-USDT,UMA-USDT,HOT-USDT,NEXO-USDT,AMP-USDT,BNT-USDT,SNX-USDT,REN-USDT,1INCH-USDT,TEL-USDT,SIS-USDT,LRC-USDT,LDO-USDT,REAL-USDT,KRL-USDT,DEVT-USDT,CRAFT-USDT,BIT-BTC,BIT-USDC,ETH-USDC,BTC-USDC,1SOL-USDT,PLT-USDT,IZI-USDT,QTUM-USDT,DCR-USDT,ZEN-USDT,THETA-USDT,MX-USDT,DGB-USDT,RVN-USDT,EGLD-USDT,RUNE-USDT,XLM-BTC,XLM-USDC,SOL-USDC,XRP-USDC,ALGO-BTC,SOL-BTC,DFL-USDT,RAIN-USDT,RUN-USDT,XEC-USDT,ICX-USDT,XDC-USDT,HNT-USDT,BTG-USDT,ZIL-USDT,HBAR-USDT,FLOW-USDT,SOS-USDT,KASTA-USDT,GAS-USDT,STX-USDT,SIDUS-USDT,VPAD-USDT,GGM-USDT,LOOKS-USDT,MBS-USDT,DAI-USDT,BUSD-USDT,ACA-USDT,MV-USDT,MIX-USDT,LTC-USDC,MANA-BTC,MATIC-BTC,LTC-BTC,DOT-BTC,SAND-BTC,MANA-USDC,MATIC-USDC,SAND-USDC,DOT-USDC,LUNC-USDC,RSS3-USDT,SYNR-USDT,TAP-USDT,ERTHA-USDT,GMX-USDT,POSI-USDT,T-USDT,ACH-USDT,JST-USDT,SUN-USDT,BTT-USDT,TRX-USDT,NFT-USDT,POKT-USDT,SCRT-USDT,PSTAKE-USDT,SON-USDT,HERO-USDT,DOME-USDT,ZBC-USDT,USTC-USDT,BNB-USDT,NEAR-USDT,PAXG-USDT,SD-USDT,APE-USDT,BTC3S-USDT,BTC3L-USDT,FIDA-USDT,MINA-USDT,SC-USDT,RACA-USDT,IME-USDT,CAPS-USDT,STG-USDT,LMR-USDT,GLMR-USDT,MOVR-USDT,ZAM-USDT,ETH-DAI,BTC-DAI,WBTC-USDT,XAVA-USDT,MELOS-USDT,GMT-USDT,GST-USDT,CELO-USDT,SFUND-USDT,ELT-USDT,LGX-USDT,BIT-DAI,APEX-USDT,CTC-USDT,COT-USDT,KMON-USDT,PLY-USDT,XWG-USDT,FITFI-USDT,STRM-USDT,GAL-USDT,FCD-USDT,ETH3S-USDT,ETH3L-USDT,KOK-USDT,FAME-USDT,XRP3S-USDT,XRP3L-USDT,USDD-USDT,LUNA-USDT" + }, + "usdcmarginedfutures": { + "assetEnabled": true, + "enabled": "BTC-PERP", + "available": "BTC-PERP" + }, + "usdtmarginedfutures": { + "assetEnabled": true, + "enabled": "BTC-USDT", + "available": "BTC-USDT,AGLD-USDT,RAY-USDT,ASTR-USDT,XTZ-USDT,SFP-USDT,REEF-USDT,XEM-USDT,IOST-USDT,SLP-USDT,JASMY-USDT,ZEC-USDT,SC-USDT,MINA-USDT,HNT-USDT,XRP-USDT,MATIC-USDT,BNB-USDT,CHZ-USDT,OMG-USDT,ZEN-USDT,PEOPLE-USDT,DAR-USDT,SUN-USDT,DODO-USDT,AKRO-USDT,RUNE-USDT,YGG-USDT,XCN-USDT,FITFI-USDT,BIT-USDT,PAXG-USDT,KDA-USDT,AVAX-USDT,GALA-USDT,RVN-USDT,MTL-USDT,GAL-USDT,SXP-USDT,KAVA-USDT,CREAM-USDT,CKB-USDT,BSW-USDT,EOS-USDT,UNI-USDT,SUSHI-USDT,ATOM-USDT,LRC-USDT,SAND-USDT,GRT-USDT,CVC-USDT,1000BTT-USDT,FLM-USDT,VET-USDT,CELO-USDT,OGN-USDT,BCH-USDT,DOGE-USDT,CRV-USDT,LPT-USDT,DGB-USDT,TOMO-USDT,ETC-USDT,FLOW-USDT,STORJ-USDT,KLAY-USDT,SPELL-USDT,10000NFT-USDT,STMX-USDT,ALGO-USDT,DASH-USDT,API3-USDT,GLMR-USDT,ZRX-USDT,IOTX-USDT,QTUM-USDT,SKL-USDT,IOTA-USDT,KNC-USDT,ROSE-USDT,ARPA-USDT,SOL-USDT,BAT-USDT,ICX-USDT,ACH-USDT,XLM-USDT,SHIB1000-USDT,YFI-USDT,EGLD-USDT,REQ-USDT,APE-USDT,BOBA-USDT,TRX-USDT,COMP-USDT,CRO-USDT,ALPHA-USDT,LTC-USDT,AAVE-USDT,NEAR-USDT,ENS-USDT,RSR-USDT,STX-USDT,XMR-USDT,BNX-USDT,CVX-USDT,GST-USDT,ADA-USDT,SRM-USDT,CELR-USDT,C98-USDT,GTC-USDT,AR-USDT,SCRT-USDT,LINK-USDT,DOT-USDT,DYDX-USDT,ANKR-USDT,AUDIO-USDT,KSM-USDT,REN-USDT,IMX-USDT,MASK-USDT,WAVES-USDT,CTSI-USDT,LINA-USDT,MKR-USDT,ANT-USDT,DUSK-USDT,HOT-USDT,BSV-USDT,ALICE-USDT,GMT-USDT,BAKE-USDT,SNX-USDT,BICO-USDT,ZIL-USDT,FIL-USDT,ENJ-USDT,ICP-USDT,FTT-USDT,ILV-USDT,CTK-USDT,LOOKS-USDT,THETA-USDT,ONE-USDT,TLM-USDT,RNDR-USDT,DENT-USDT,NEO-USDT,OCEAN-USDT,ETH-USDT,AXS-USDT,COTI-USDT,JST-USDT,FXS-USDT,HBAR-USDT,CHR-USDT,1INCH-USDT,RSS3-USDT,1000XEC-USDT,BAL-USDT,CTC-USDT,MANA-USDT,FTM-USDT,WOO-USDT,LIT-USDT,BAND-USDT" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + }, + "urlEndpoints": { + "RestCoinMarginedFuturesURL": "https://api.bybit.com", + "RestFuturesURL": "https://api.bybit.com", + "RestSpotURL": "https://api.bybit.com", + "RestUSDTMarginedFuturesURL": "https://api.bybit.com", + "WebsocketSpotURL": "wss://stream.bybit.com/spot/quote/ws/v2", + "RestUSDCMarginedFuturesURL": "https://api.bybit.com" + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "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 + } }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": true - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "BTC Markets", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "AUD", - "currencyPairs": { - "requestFormat": { - "uppercase": true, - "delimiter": "-" - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC-AUD", - "available": "BTC-AUD,LTC-AUD,LTC-BTC,ETH-BTC,ETH-AUD,ETC-AUD,ETC-BTC,XRP-AUD,XRP-BTC,POWR-AUD,POWR-BTC,OMG-AUD,OMG-BTC,BCHABC-AUD,BCHABC-BTC,BCHSV-AUD,BCHSV-BTC,GNT-AUD,GNT-BTC,BAT-AUD,BAT-BTC,XLM-AUD,XLM-BTC" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true, - "requiresBase64DecodeSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + "name": "LBank", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_" + }, + "configFormat": { + "uppercase": false, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "eth_btc", + "available": "FBC_USDT,GALT_USDT,IOEX_USDT,OATH_USDT,BLOC_USDT,BTC_USDT,ETH_USDT,ETH_BTC,ABBC_BTC,KISC_ETH,BXA_USDT,ATP_USDT,MAT_USDT,SKY_BTC,RNT_USDT,VENA_USDT,GRIN_USDT,IDA_USDT,PNT_USDT,OPX_USDT,VTHO_BTC,AMO_ETH,UBEX_BTC,EOS_BTC,UBEX_USDT,TNS_BTC,SAIT_ETH,DAX_BTC,DAX_ETH,DALI_USDT,VET_USDT,BCH_BTC,BCH_USDT,NEO_USDT,QTUM_USDT,ZEC_USDT,VET_BTC,PAI_BTC,PNT_BTC,NEO_BTC,DASH_BTC,LTC_BTC,ETC_BTC,QTUM_BTC,ZEC_BTC,SC_BTC,BTS_BTC,CPX_BTC,XWC_BTC,FIL6_BTC,FIL12_BTC,FIL36_BTC,EOS_USDT,UT_ETH,ELA_ETH,VET_ETH,VTHO_ETH,PAI_ETH,HER_ETH,PTT_ETH,TAC_ETH,IDHUB_ETH,SSC_ETH,SKM_ETH,PLY_ETH,EXT_ETH,EOS_ETH,YOYOW_ETH,TRX_ETH,QTUM_ETH,ZEC_ETH,BTS_ETH,BTM_ETH,MITH_ETH,NAS_ETH,MAN_ETH,DBC_ETH,BTO_ETH,DDD_ETH,CPX_ETH,CS_ETH,IHT_ETH,OCN_ETH,EKO_ETH,XWC_ETH,PUT_ETH,PNT_ETH,AAC_ETH,FIL6_ETH,FIL12_ETH,FIL36_ETH,SEER_ETH,BSB_ETH,CDC_ETH,GRAMS_ETH,DDMX_ETH,EAI_ETH,BNB_USDT,HT_USDT,KBC_BTC,KBC_USDT,MAI_USDT,PHV_USDT,GT_USDT,VOKEN_USDT,CYE_USDT,BRC_USDT,BTC_AUSD,DDMX_USDT,SEAL_USDT,SEOS_BTC,BTY_USDT,FO_USDT,DLX_USDT,BFC_USDT,LBK_USDT,SERO_USDT,MTV_USDT,CKB_USDT,ARPA_USDT,ZIP_USDT,AT_USDT,DOT_USDT,DILI_USDT,DUO_USDT,TEP_USDT,BIKI_USDT,MX_USDT,DNS_USDT,OKB_USDT,FLDT_USDT,CCTC_USDT,WIN_USDT,BTT_USDT,TRX_USDT,GRS_BTC,GST_USDT,GST_ETH,ABBC_USDT,UTK_USDT,GKI_USDT,BPX_USDT,SUTER_USDT,LT_USDT,LM_USDT,HTDF_USDT" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "COINUT", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": true - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "LTC-USDT", - "available": "LTC-CAD,LTC-SGD,USDT-USD,ETC-LTC,LTC-BTC,USDT-SGD,XMR-USDT,ZEC-SGD,ETH-USD,BTC-USDT,ETC-BTC,ETH-LTC,LTC-USD,BTC-USD,ETH-USDT,XMR-LTC,ZEC-USD,ETC-SGD,DAI-SGD,ZEC-CAD,BTC-SGD,ETH-BTC,ETH-SGD,LTC-USDT,ZEC-BTC,ZEC-USDT,BTC-CAD,XMR-BTC,ZEC-LTC,ETC-USDT,ETH-CAD" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret", - "clientID": "ClientID" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresClientID": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + "name": "BTSE", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USD", + "available": "BTC-CNY,BTC-EUR,BTC-GBP,BTC-HKD,BTC-JPY,BTC-SGD,BTC-USD,ETH-CNY,ETH-EUR,ETH-GBP,ETH-HKD,ETH-JPY,ETH-SGD,ETH-USD,LTC-CNY,LTC-EUR,LTC-GBP,LTC-HKD,LTC-JPY,LTC-SGD,LTC-USD,USDT-CNY,USDT-EUR,USDT-GBP,USDT-HKD,USDT-JPY,USDT-SGD,USDT-USD,XMR-CNY,XMR-EUR,XMR-GBP,XMR-HKD,XMR-JPY,XMR-SGD,XMR-USD" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "EXMO", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD,EUR,RUB,PLN,UAH", - "currencyPairs": { - "requestFormat": { - "uppercase": true, - "delimiter": "_", - "separator": "," - }, - "configFormat": { - "uppercase": true, - "delimiter": "_" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC_USD,LTC_USD", - "available": "BCH_RUB,DASH_RUB,EOS_USD,ETH_TRY,GNT_ETH,LTC_USD,PTI_USDT,XRP_BTC,EXM_BTC,BTG_BTC,ETC_RUB,BTG_USD,NEO_RUB,XMR_BTC,ZRX_ETH,MNX_BTC,USDC_BTC,XRP_EUR,SMART_USD,EOS_BTC,MNX_ETH,ZEC_BTC,BCH_USD,WAVES_USD,TRX_BTC,XRP_TRY,DASH_USD,DOGE_USD,ETZ_USDT,GUSD_USD,MNC_BTC,ZEC_USD,DCR_BTC,DXT_USD,PTI_RUB,XMR_ETH,ZRX_USD,DAI_RUB,MNC_USD,XLM_TRY,DAI_BTC,BTC_EUR,LTC_EUR,OMG_BTC,PTI_EOS,SMART_RUB,XTZ_USD,HP_EXM,ADA_USD,OMG_ETH,QTUM_USD,TRX_RUB,USDC_ETH,USDC_USDT,USD_RUB,BTC_UAH,BCH_USDT,ETH_PLN,KICK_RUB,LSK_RUB,SMART_BTC,XMR_UAH,XRP_USD,GUSD_BTC,QTUM_ETH,USDT_EUR,BTC_RUB,DCR_UAH,ETH_RUB,DOGE_BTC,ETZ_BTC,INK_USD,LTC_UAH,BTT_UAH,BTC_USDT,MNC_ETH,XTZ_ETH,BTC_TRY,DXT_BTC,KICK_USDT,OMG_USD,WAVES_BTC,XLM_BTC,BTCZ_BTC,GNT_BTC,LSK_BTC,LTC_RUB,NEO_BTC,XEM_UAH,XMR_USD,ZAG_BTC,GAS_USD,LTC_BTC,TRX_UAH,XEM_EUR,XMR_RUB,XTZ_RUB,ETZ_ETH,ETC_BTC,GUSD_RUB,INK_BTC,LSK_USD,MNX_USD,SMART_EUR,VLX_BTC,BCH_ETH,XMR_EUR,ADA_ETH,QTUM_BTC,XEM_USD,ATMCASH_BTC,ADA_BTC,ETH_EUR,TRX_USD,USDC_USD,BCH_BTC,ETH_UAH,KICK_BTC,WAVES_RUB,XEM_BTC,ETH_BTC,BCH_EUR,BTT_BTC,ROOBEE_BTC,XLM_USD,XRP_ETH,ETH_USD,MKR_DAI,XTZ_BTC,DAI_USD,BCH_UAH,INK_ETH,KICK_ETH,MKR_BTC,NEO_USD,XRP_USDT,ZEC_EUR,BTC_USD,XRP_RUB,EOS_EUR,ETH_USDT,USDT_UAH,XRP_UAH,ZEC_RUB,HP_BTC,BTT_RUB,DAI_ETH,DASH_UAH,DASH_USDT,ETH_LTC,GAS_BTC,USDT_USD,BTG_ETH,XLM_RUB,WAVES_ETH,USDT_RUB,ZRX_BTC,DASH_BTC,DCR_RUB,ETC_USD,HB_BTC,PTI_BTC,BTC_PLN" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": false, - "websocketCapabilities": {} + "name": "BTC Markets", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "AUD", + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-AUD", + "available": "BTC-AUD,LTC-AUD,LTC-BTC,ETH-BTC,ETH-AUD,ETC-AUD,ETC-BTC,XRP-AUD,XRP-BTC,POWR-AUD,POWR-BTC,OMG-AUD,OMG-BTC,BCHABC-AUD,BCHABC-BTC,BCHSV-AUD,BCHSV-BTC,GNT-AUD,GNT-BTC,BAT-AUD,BAT-BTC,XLM-AUD,XLM-BTC" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresBase64DecodeSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "CoinbasePro", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD,GBP,EUR", - "currencyPairs": { - "requestFormat": { - "uppercase": true, - "delimiter": "-" - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC-USD", - "available": "LTC-GBP,XLM-BTC,DASH-BTC,DAI-USDC,ZEC-USDC,XLM-EUR,ZRX-BTC,LTC-BTC,ETC-BTC,ETH-USD,XRP-EUR,BTC-USDC,REP-USD,EOS-BTC,ZEC-BTC,ETC-GBP,LINK-ETH,XRP-BTC,ZRX-USD,ETH-USDC,MANA-USDC,BTC-EUR,BCH-GBP,DNT-USDC,EOS-EUR,BCH-EUR,LTC-EUR,CVC-USDC,ETH-GBP,DASH-USD,ETH-EUR,XTZ-BTC,ZRX-EUR,BAT-ETH,BTC-GBP,ETC-USD,BAT-USDC,BCH-USD,GNT-USDC,ALGO-USD,LINK-USD,XLM-USD,ETH-BTC,EOS-USD,REP-BTC,ETH-DAI,XRP-USD,LTC-USD,ETC-EUR,BTC-USD,XTZ-USD,BCH-BTC,LOOM-USDC" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret", - "clientID": "ClientID" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true, - "requiresClientID": true, - "requiresBase64DecodeSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + "name": "COINUT", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "LTC-USDT", + "available": "LTC-CAD,LTC-SGD,USDT-USD,ETC-LTC,LTC-BTC,USDT-SGD,XMR-USDT,ZEC-SGD,ETH-USD,BTC-USDT,ETC-BTC,ETH-LTC,LTC-USD,BTC-USD,ETH-USDT,XMR-LTC,ZEC-USD,ETC-SGD,DAI-SGD,ZEC-CAD,BTC-SGD,ETH-BTC,ETH-SGD,LTC-USDT,ZEC-BTC,ZEC-USDT,BTC-CAD,XMR-BTC,ZEC-LTC,ETC-USDT,ETH-CAD" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "GateIO", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": true, - "delimiter": "_" - }, - "configFormat": { - "uppercase": true, - "delimiter": "_" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot", - "option", - "futures", - "cross_margin", - "margin", - "delivery" - ], - "pairs": { - "spot": { - "enabled": "BTC_USDT,IHT_ETH,AME_ETH,CEUR_ETH,ALEPH_USDT,OMG_TRY,BTC_TRY,OGN_USDT,ALA_USDT", - "available": "IHT_ETH,AME_ETH,CEUR_ETH,ALEPH_USDT,OMG_TRY,BTC_TRY,OGN_USDT,ALA_USDT,HC_USDT,BTC_USDT,QNT_USDT,QTUM_ETH,MAHA_ETH,XCN_ETH,POOL_USDT,KGC_USDT,MCO2_USDT,HARD_USDT,GHNY_USDT,FTT_ETH,K21_ETH,FINE_USDT,REP_USDT,SBR_USDT,SKM_ETH,QLC_ETH,GAS_BTC,ALICE3L_USDT,BAO_USDT,FALCONS_USDT,ANT_USDT,VIDYX_USDT,DXCT_ETH,SMTY_ETH,HERO_USDT,SHARE_USDT,FIN_USDT,MTV_USDT,MOO_USDT,SMTY_USDT,ORAO_USDT,AE_ETH,SUSD_USDT,MAN_USDT,UNDEAD_USDT,MC_USDT,VET_USDT,WAXP_ETH,MDA_ETH,LYXE_USDT,SPS_USDT,STX_ETH,WSIENNA_USDT,NAOS_BTC,NFTX_USDT,OPUL_USDT,ICP3L_USDT,SFI_ETH,CTT_USDT,BSV3L_USDT,DFI_USDT,DIS_ETH,FET_USDT,ARG_USDT,VELO_USDT,NSBT_BTC,GSE_ETH,HNS_BTC,DOGEDASH_ETH,BACON_USDT,DUSK_USDT,MAPE_USDT,EGLD_ETH,TDROP_USDT,C983L_USDT,FAN_ETH,CZZ_USDT,FIU_USDT,SWRV_USDT,ONT_ETH,KINE_ETH,IMX_ETH,SPAY_ETH,CFG_BTC,RACA3S_USDT,UNO_ETH,DMLG_USDT,SAKE_ETH,ASM_USDT,CUSD_ETH,SUSD_ETH,ONC_USDT,DAI_USDT,VEGA_ETH,PYM_USDT,LTC_TRY,LOKA_USDT,NIF_USDT,BNC_USDT,PERL_ETH,MATIC3S_USDT,STMX_USDT,SKL_USDT,WLKN_USDT,XYO_ETH,AMPL3S_USDT,WEX_USDT,ULU_ETH,LIKE_ETH,INSUR_ETH,CAKE_ETH,SXP_ETH,COTI_USDT,ORT_USDT,RACA3L_USDT,GASDAO_USDT,AVA_USDT,OPA_USDT,ATS_USDT,VEGA_USDT,KILT_USDT,HIT_ETH,BRISE_USDT,SAUBER_USDT,SPS_ETH,FSN_USDT,EOS_ETH,KYL_USDT,REVV_ETH,SVT_ETH,XRP_USDT,DYDX3S_USDT,MANA3S_USDT,ICP_ETH,ALICE3S_USDT,PCX_USDT,LEMO_ETH,MKR_ETH,WOO3S_USDT,CART_ETH,MATIC_USDT,UNI_USD,MOBI_BTC,ICP3S_USDT,BEAM_BTC,CRO3S_USDT,FTT_USDT,IQ_ETH,TAP_USDT,MLT_USDT,RBN_USDT,AMPL3L_USDT,KINT_ETH,HECH_USDT,GAFI_ETH,WOO3L_USDT,TAI_USDT,HERA_USDT,AST_USDT,DHV_ETH,XAVA_USDT,LSS_USDT,SNX3S_USDT,PBR_USDT,XEND_ETH,SHR_ETH,PRQ_USDT,MATIC3L_USDT,WIT_ETH,LPOOL_USDT,PSP_USDT,BXC_USDT,CBK_USDT,REVO_BTC,MANA3L_USDT,ALPINE_USDT,DEGO_USDT,SIN_USDT,OCT_USDT,KZEN_USDT,L3P_USDT,FX_ETH,ONC_ETH,AXS_USD,BORA_USDT,XTZ_ETH,NEO3L_USDT,FROG_USDT,CHAMP_USDT,XNFT_USDT,BCH3S_USDT,FORT_USDT,XLM_TRY,TRX_TRY,CRPT_USDT,ROUTE_USDT,GLM_USDT,SLRS_ETH,TIMECHRONO_USDT,VRA_USDT,ONS_USDT,ZEC3L_USDT,KFT_ETH,TFD_ETH,FRA_USDT,RDN_ETH,BLANK_USDT,IOST3L_USDT,DDD_USDT,DOGE_USD,UNQ_USDT,API33S_USDT,AKRO_ETH,GITCOIN_USDT,THG_USDT,BDX_USDT,LTO_ETH,FLY_USDT,CREDIT_USDT,RENA_USDT,ZRX_ETH,CRP_ETH,NBOT_USDT,HT3L_USDT,DORA_ETH,LLT_SNET,ASD_USDT,XMR_USDT,SSV_BTC,FTM_USDT,XELS_USDT,MTL_ETH,ADX_ETH,API33L_USDT,PIG_USDT,RUNE_ETH,QRDO_BTC,THN_USDT,BCUG_USDT,EGG_ETH,GGM_USDT,HOTCROSS_USDT,SKYRIM_USDT,BTG_USDT,POT_USDT,CS_USDT,XVS_USDT,A5T_USDT,GOD_BTC,WAVES_USDT,LSK_BTC,BTT_TRY,YIN_USDT,PEOPLE_USDT,SPELL_ETH,POLC_USDT,BZZ3L_USDT,UNO_USDT,HDV_USDT,CELL_USDT,DAR_ETH,MIR_ETH,FODL_USDT,SRM_ETH,PROS_USDT,ORN_ETH,WAG_USDT,RBC_ETH,VENT_USDT,WND_USDT,AAA_ETH,BSCS_ETH,ZEC3S_USDT,DOS_USDT,HT3S_USDT,LAND_USDT,BCD_BTC,RING_USDT,FIRO_USDT,AUDIO_USDT,KUMA_USDT,SOLO_BTC,CRBN_USDT,MM_ETH,SAKE_USDT,XMARK_USDT,SLP_USDT,F2C_USDT,LUNA_USDT,ONIT_USDT,FTM3L_USDT,POPK_USDT,RFUEL_USDT,NEO3S_USDT,MIR_USDT,ETC_BTC,STETH_ETH,MANA_TRY,ALPACA_ETH,WAXL_USDT,EGS_USDT,DAR_USDT,KSM_USDT,XMARK_ETH,QTUM_USDT,C983S_USDT,INDI_ETH,DOGE3S_USDT,RVN_USDT,NOS_USDT,ALU_ETH,ALD_ETH,LUNC_USDT,ARES_ETH,BZZ3S_USDT,TNC_ETH,ONE_USDT,SENC_ETH,FTM3S_USDT,FLUX_USDT,STORJ_ETH,MTN_ETH,MNW_USDT,BLES_ETH,STG_ETH,LIME_ETH,WAGYU_USDT,XRP_TRY,XOR_ETH,ANGLE_USDT,DOGA_USDT,JFI_USDT,USDG_USDT,GRND_USDT,BOND_ETH,DMTR_USDT,YIN_ETH,ENJ_USDT,GOLDMINER_USDT,WIT_USDT,DOGE3L_USDT,FORM_USDT,LYXE_ETH,MLK_USDT,VR_USDT,DMS_USDT,LRC_TRY,ONX_USDT,ASK_USDT,ISP_ETH,TXT_USDT,IOEN_ETH,NIIFI_USDT,VRX_USDT,DOME_USDT,CTSI_USDT,ORBS_USDT,ZLW_ETH,FIL_USDT,FTI_ETH,CTK_USDT,ASR_USDT,GBPT_BTC,CBK_BTC,MBOX_ETH,RAM_USDT,IRIS_USDT,AME_USDT,KUB_USDT,ENV_USDT,RING_ETH,COTI3S_USDT,JULD_ETH,POLK_ETH,ACH3S_USDT,HYVE_ETH,MIX_ETH,RFT_USDT,ORAO_ETH,IHT_USDT,POLYPAD_USDT,CTRC_USDT,SFUND_USDT,MXC_BTC,DDD_BTC,CHESS_ETH,SHIB_USDT,SN_USDT,NFT_USDT,ASTRO_ETH,SOLO_USDT,TSHP_USDT,AMP_USDT,BTCST_ETH,VLXPAD_USDT,GAN_USDT,O3_USDT,WBTC_TRY,TULIP_USDT,GS_ETH,DX_ETH,NYZO_ETH,TT_USDT,SHILL_USDT,RATING_ETH,DUST_USDT,PSB_USDT,BFT1_USDT,GALA_ETH,XDC_USDT,LON3L_USDT,HE_USDT,ICE_ETH,LINK_ETH,SKU_USDT,QLC_USDT,DOMI_USDT,IDEA_USDT,METO_USDT,LIFE_ETH,ACH3L_USDT,POWR_ETH,VET_ETH,ALGO_USDT,BLIN_USDT,BAO_ETH,RBLS_USDT,TORN_ETH,VRT_USDT,BLANKV2_ETH,AUCTION_ETH,OLE_USDT,NWC_BTC,DOT5S_USDT,M RCH_ETH,SUNNY_ETH,GST_USDT,ENJ_TRY,KIBA_USDT,KLAP_USDT,SNTR_ETH,CELR_ETH,CHESS_USDT,XLM3L_USDT,LIQ_USDT,TRU_ETH,CHZ_USD,EPK_USDT,MED_ETH,BSCPAD_ETH,ZCN_USDT,AIOZ_ETH,FOR_ETH,CVC3L_USDT,MNY_USDT,SALT_USDT,CSTR_USDT,MPL_USDT,PLY_ETH,FIS_USDT,CHO_USDT,BICO_ETH,STOX_ETH,HIGH_USDT,SDAO_BTC,STEP_USD,CRV_BTC,SCRT_ETH,ROSE_USDT,SKILL_ETH,FRAX_USDT,BAGS_USDT,WIKEN_BTC,WOO_USDT,BBANK_ETH,SNX3L_USDT,XRD_ETH,VTHO_USDT,OKB3L_USDT,SAFEMOON_USDT,RAD_ETH,IOI_USDT,LAMB_USDT,CHZ_USDT,FAR_ETH,OKB3S_USDT,ELU_USDT,JGN_ETH,EOS3S_USDT,DBC_USDT,ATOM_USDT,ACH_ETH,LBLOCK_USDT,WZRD_USDT,OST_ETH,MEAN_USDT,IDEX_USDT,HOT_TRY,EWT_ETH,EMON_USDT,FXS_USDT,PSY_ETH,SIDUS_USDT,ATA_USDT,CVC3S_USDT,LOOKS_ETH,ALPA_ETH,CGG_ETH,CIR_ETH,PRT_ETH,LON3S_USDT,INJ_USDT,FIRE_ETH,MAHA_USDT,IOST3S_USDT,NU_ETH,LEO_BTC,VOXEL_USDT,CRV_USDT,EQX_USDT,WHALE_USDT,INJ_ETH,GRAP_USDT,AVAX3S_USDT,TIFI_USDT,C98_USDT,ERN_ETH,SUSHI_ETH,VET3S_USDT,KPAD_USDT,CRPT_ETH,CRO_USDT,AZY_USDT,LEMD_USDT,ETH2_ETH,BASE_ETH,TT_ETH,PERL_USDT,BANK_ETH,LST_ETH,PYR_ETH,RATIO_USDT,UMB_USDT,M ETALDR_USDT,SWINGBY_ETH,WICC_ETH,NUM_USDT,SHOE_USDT,BORING_ETH,SDN_USDT,GXS_BTC,ALICE_ETH,BRKL_USDT,GF_ETH,ELEC_USDT,SFG_USDT,COFIX_USDT,TIPS_ETH,FIL_BTC,CWAR_USDT,WILD_USDT,RENBTC_USDT,BNX_USDT,TRU_USDT,SWEAT_USDT,IOST_BTC,NVIR_USDT,1EARTH_USDT,ADAPAD_USDT,PPS_USDT,CUBE_USDT,DLC_USDT,DAFI_ETH,UNISTAKE_ETH,NFTL_USDT,ATOM_TRY,SHIB3S_USDT,BNB_USD,CNAME_USDT,GTH_ETH,ZCX_USDT,DYDX3L_USDT,ASTRO_USDT,GLQ_USDT,PROPS_USDT,AART_USDT,BTRST_ETH,KFT_USDT,AERGO_USDT,RUFF_ETH,EOS3L_USDT,API3_USDT,MINA_BTC,ETHA_ETH,AXIS_ETH,LOON_USDT,AVAX3L_USDT,VET3L_USDT,AE_USDT,SHX_USDT,LYM_USDT,DCR_BTC,LBK_USDT,QTC_USDT,LAVA_USDT,XCN_USDT,BRT_USDT,RSV_USDT,KIF_USDT,PSL_USDT,AZERO_USDT,LUNA_ETH,MILO_USDT,OGN_ETH,TOTM_USDT,BYN_ETH,MINA_USDT,PUNDIX_ETH,SRT_USDT,DG_ETH,IHC_USDT,SYS_ETH,TITA_USDT,COTI3L_USDT,DAG_USDT,DOT5L_USDT,TRADE_USDT,SHPING_USDT,NU_USDT,BLANK_ETH,PCNT_ETH,SCCP_USDT,POLS_USDT,NPT_USDT,MTA_USDT,YIELD_USDT,ZCN_ETH,DVP_ETH,KART_USDT,SYLO_USDT,MCRT_USDT,SPFC_USDT,BASE_USDT,ICX_USDT,PET_USDT,GZONE_USDT,RED_ETH,SBTC_USDT,BATH_ ETH,SOL_USD,NAFT_USDT,GMX_USDT,VADER_USDT,GTC_USDT,CVP_ETH,XRPBEAR_USDT,TIME_USDT,SXP_USDT,CITY_USDT,QASH_USDT,FAST_USDT,BCD_USDT,KNIGHT_USDT,BOO_ETH,ZODI_USDT,REI_USDT,PBX_ETH,SRM_USDT,LDO_ETH,ZEC_USDT,UFT_USDT,DAG_BTC,RIDE_USDT,ERN_USDT,T_USDT,CEEK_USDT,STI_USDT,IMX3S_USDT,ELA_USDT,MNGO_ETH,EHASH_ETH,BADGER_ETH,SUPE_USDT,AR3L_USDT,AUDIO_ETH,DOCK_ETH,QSP_USDT,FLM_USDT,AAVE3S_USDT,BOND_USDT,HT_USD,TARA_USDT,TRX_USDT,SPO_USDT,DSLA_USDT,LTC_BTC,DOGE_USDT,SLIM_ETH,ALN_ETH,CFX3S_USDT,FXS_ETH,RARE_ETH,VLXPAD_ETH,ETH_USD,SDN_BTC,QUICK_USDT,UTK_USDT,XPNET_USDT,TRB_USDT,LAZIO_USDT,FTM_TRY,ALPHA_ETH,CVC_ETH,WSG_USDT,UNI_ETH,DASH3L_USDT,BTL_USDT,CPOOL_USDT,MCG_USDT,SFP_ETH,REALM_USDT,RUFF_BTC,MOB_ETH,IBFK_USDT,ALPHA3S_USDT,BLOK_USDT,WIKEN_USDT,OMG3S_USDT,UTK_ETH,BCH5S_USDT,MED_USDT,REN_USD,MAN_ETH,SLND_ETH,CGG_USDT,CRE_USDT,SOURCE_USDT,ABT_USDT,DPET_USDT,WOM_USDT,FOREX_ETH,SNFT1_USDT,RIF_USDT,BENQI_USDT,XCV_ETH,GTC_BTC,ADA_TRY,LAT_USDT,ITGR_USDT,DLTA_USDT,SMT_USDT,APYS_USDT,MFT_ETH,ABT_ETH,STOX_USDT,ZRX_BTC,GMAT_USDT,R OOM_ETH,STORJ_BTC,RAZOR_USDT,RAGE_USDT,DOCK_USDT,RDN_USDT,MTR_USDT,NKN_USDT,SWASH_USDT,FX_USDT,POR_USDT,DENT_ETH,DERI_USDT,DFND_USDT,BLES_USDT,SLND_USDT,WNXM_ETH,CRTS_USDT,BTC3S_USDT,BKC_USDT,STEPG_ETH,THETA3L_USDT,NBS_BTC,AVAX_ETH,NANO_BTC,DEFILAND_ETH,LOOKS_USDT,BCX_BTC,BCH_USD,ETH3L_USDT,QLC_BTC,BCUG_ETH,RDF_USDT,DOGEDASH_USDT,ARSW_USDT,NEAR_ETH,QTCON_USDT,BABI_USDT,MBX_USDT,PNL_USDT,ODDZ_ETH,ATOM_BTC,XRP_BTC,BTCBULL_USDT,HMT_USDT,PORTO_USDT,STND_USDT,ETHW_ETH,LPT_USDT,LTC3L_USDT,TOKAU_USDT,QI_ETH,TVK_USDT,CWS_USDT,SWOP_USDT,WBTC_USDT,INSTAR_ETH,ICX_ETH,GALA5L_USDT,XTZ_BTC,AGS_USDT,TARA_BTC,DYDX_ETH,CATGIRL_USDT,SASHIMI_ETH,EPX_ETH,GCOIN_USDT,GDAO_USDT,MARS_ETH,OMG_USD,PMON_USDT,MNGO_USDT,TVK_ETH,SLG_USDT,MSOL_USDT,POWR_USDT,UOS_USDT,USDD_USDT,SLICE_USDT,ARRR_ETH,NSBT_USDT,STR_ETH,BEAM3L_USDT,BEL_USDT,MM_USDT,AXS_ETH,WEST_ETH,FTT3L_USDT,OMI_USDT,TIPS_USDT,SLC_ETH,SQUID_USDT,FEI_USDT,GEM_USDT,UMEE_USDT,DOGE_TRY,FCD_USDT,PVU_USDT,XED_ETH,LRN_ETH,NRFB_USDT,LION_USDT,BLACK_USDT,DOGE5S_USDT,CUDOS_USDT,PCNT_USDT ,OVR_USDT,ETC3S_USDT,CHR_ETH,MER_USDT,BOBA_USDT,FUEL_USDT,BAC_USDT,ONE3S_USDT,CONV_ETH,CDT_BTC,CELL_ETH,ASM_ETH,OPIUM_USDT,JST3L_USDT,BONDLY_USDT,RAZE_USDT,LIME_BTC,NFTX_ETH,PNK_ETH,LDO_USDT,DKS_USDT,ORO_USDT,LITH_USDT,ALPHR_ETH,INK_BTC,RLY_USDT,NEAR3S_USDT,XLM3S_USDT,AR_USDT,AKT_USDT,HCT_USDT,REEF_ETH,BZZ_USDT,SRM3L_USDT,AQDC_USDT,OPIUM_ETH,BAT_TRY,EWT_USDT,ALCX_ETH,CORN_USDT,HYDRA_USDT,RUNE_USD,STEP_USDT,CKB_BTC,MATTER_USDT,STSOL_ETH,CEEK_ETH,FXF_ETH,LIKE_USDT,HIT_USDT,LEO_USDT,COMP_USDT,BAL_USDT,LMR_USDT,AQT_USDT,BUY_ETH,LINK3S_USDT,ROOK_ETH,IMX_USDT,EFI_USDT,TAUR_USDT,OKT_ETH,GALO_USDT,MOOV_USDT,RUNE_USDT,TCP_USDT,ITEM_USDT,SCLP_USDT,RBC_USDT,SPI_USDT,ETC_USDT,RENBTC_BTC,CHICKS_USDT,KNOT_USDT,XEC3L_USDT,XCV_USDT,ETC_ETH,AAVE_TRY,APT_USDT,GNX_ETH,KISHU_USDT,AE_BTC,LIEN_USDT,CREAM_USDT,ATOM3S_USDT,OP_ETH,FORTH_ETH,PYR_USDT,KTN_ETH,TKO_ETH,METAG_USDT,ACE_USDT,CIR_USDT,BEAM_ETH,TCP_ETH,SRM_USD,CEL_USD,TRIBE3S_USDT,MESA_ETH,EVA_USDT,BBANK_USDT,BLANKV2_USDT,FORM_ETH,BAL3S_USDT,VISR_ETH,REVO_ETH,ALTB_USDT,KNC_US DT,GAS_USDT,SAFEMARS_USDT,TIP_USDT,VADER_ETH,NWC_USDT,VALUE_USDT,ATA_ETH,SSX_USDT,JOE_USDT,BAS_ETH,FITFI3S_USDT,BIT_USDT,QNT_ETH,RFOX_ETH,MSU_USDT,MSOL_ETH,CRV3L_USDT,OXT_USDT,SHFT_USDT,VERA_ETH,LYM_ETH,BP_USDT,KBOX_USDT,DOGNFT_ETH,PERP_USDT,VELO_ETH,SAO_USDT,DUCK2_USDT,DEFILAND_USDT,DUCK2_ETH,GLMR3L_USDT,SERO_ETH,MTS_USDT,STX_USDT,KEX_ETH,ZIG_USDT,CARDS_USDT,ANML_USDT,GALA_USDT,RAY3S_USDT,KAVA3L_USDT,GARD_USDT,GRT3L_USDT,BFC_USDT,NIFT_USDT,ORION_USDT,CTX_USDT,ASW_USDT,CERE_USDT,COMBO_ETH,MKR_USDT,MASK_USDT,MGA_USDT,AVAX_USDT,SKL3L_USDT,FRR_USDT,MV_USDT,BMI_ETH,SFIL_USDT,TEER_USDT,KLV_USDT,DMS_ETH,LBL_USDT,MKR3L_USDT,LEDU_BTC,XLM_BTC,MIST_ETH,OIN_USDT,CAKE_USDT,RNDR_USDT,STEPG_USDT,YCT_USDT,OPS_ETH,SHR_USDT,OXY_ETH" - }, - "option": { - "enabled": "BTC_USDT-20230217-28000-P,BTC_USDT-20221028-34000-P,BTC_USDT-20221028-40000-C", - "available": "BTC_USDT-20221028-26000-C,BTC_USDT-20221028-34000-P,BTC_USDT-20221028-40000-C,BTC_USDT-20221028-28000-P,BTC_USDT-20221028-34000-C,BTC_USDT-20221028-28000-C,BTC_USDT-20221028-36000-P,BTC_USDT-20221028-50000-P,BTC_USDT-20221028-36000-C,BTC_USDT-20221028-50000-C,BTC_USDT-20221028-21000-P,BTC_USDT-20221028-38000-P,BTC_USDT-20221028-21000-C,BTC_USDT-20221028-38000-C,BTC_USDT-20221028-23000-P,BTC_USDT-20221028-17000-P,BTC_USDT-20221028-23000-C,BTC_USDT-20221028-17000-C,BTC_USDT-20221028-25000-P,BTC_USDT-20221028-19000-P,BTC_USDT-20221028-25000-C,BTC_USDT-20221028-10000-P,BTC_USDT-20221028-19000-C,BTC_USDT-20221028-27000-P,BTC_USDT-20221028-10000-C,BTC_USDT-20221028-27000-C,BTC_USDT-20221028-12000-P,BTC_USDT-20221028-12000-C,BTC_USDT-20221028-20000-P,BTC_USDT-20221028-5000-P,BTC_USDT-20221028-14000-P,BTC_USDT-20221028-20000-C,BTC_USDT-20221028-45000-P,BTC_USDT-20221028-5000-C,BTC_USDT-20221028-14000-C,BTC_USDT-20221028-22000-P,BTC_USDT-20221028-45000-C,BTC_USDT-20221028-16000-P,BTC_USDT-20221028-22000-C,BTC_USDT-20221028-30000-P,BTC_USDT-20221028-16000-C,BTC_USDT-20221028-24000-P,BTC_USDT-20221028-30000-C,BTC_USDT-20221028-18000-P,BTC_USDT-20221028-24000-C,BTC_USDT-20221028-32000-P,BTC_USDT-20221028-18000-C,BTC_USDT-20221028-26000-P,BTC_USDT-20221028-32000-C,BTC_USDT-20221028-40000-P" - }, - "futures": { - "enabled": "ETH_USD,BTC_USD,KNC_USDT,OOKI_USDT,BIT_USDT,ZEC_USDT,SC_USDT,RVN_USDT,ICX_USDT", - "available": "ETH_USD,BTC_USD,KNC_USDT,OOKI_USDT,BIT_USDT,ZEC_USDT,SC_USDT,RVN_USDT,ICX_USDT,DUSK_USDT,BEL_USDT,REEF_USDT,ALCX_USDT,ASTR_USDT,INJ_USDT,CAKE_USDT,LAZIO_USDT,ONE_USDT,CEL_USDT,ETH_USDT,KLAY_USDT,COTI_USDT,MKISHU_USDT,MANA_USDT,MOVR_USDT,OMG_USDT,UNI_USDT,LTC_USDT,AAVE_USDT,DENT_USDT,QRDO_USDT,BNB_USDT,ALPHA_USDT,RAY_USDT,APE_USDT,CERE_USDT,STMX_USDT,XCN_USDT,OGN_USDT,OKB_USDT,DOT_USDT,TLM_USDT,BTM_USDT,ADA_USDT,ANKR_USDT,ANT_USDT,TRX_USDT,MTL_USDT,YFII_USDT,SUN_USDT,SAND_USDT,MBABYDOGE_USDT,WIN_USDT,LUNC_USDT,SRM_USDT,STG_USDT,BAT_USDT,AXS_USDT,SOL_USDT,MAKITA_USDT,BNT_USDT,BLZ_USDT,PSG_USDT,IOTA_USDT,BONK_USDT,RSR_USDT,PYR_USDT,FITFI_USDT,MKR_USDT,PERP_USDT,COMP_USDT,LINK_USDT,CHR_USDT,CFX_USDT,GARI_USDT,DGB_USDT,MBOX_USDT,WEMIX_USDT,DYDX_USDT,LUNA_USDT,HT_USDT,TRB_USDT,CTK_USDT,ACA_USDT,TFUEL_USDT,OCEAN_USDT,XLM_USDT,HOT_USDT,FTM_USDT,LPT_USDT,SOS_USDT,ALGO_USDT,SHIB_USDT,BSV_USDT,PORTO_USDT,SFP_USDT,SANTOS_USDT,BADGER_USDT,DAR_USDT,DEFI_USDT,XEM_USDT,ALICE_USDT,ICP_USDT,RARE_USDT,LRC_USDT,BAKE_USDT,FLUX_USDT,CRO_USDT,CVC_USDT,MINA_USDT,LIT_USDT,AUDIO_USDT,ZIL_USDT,XMR_USDT,FRONT_USDT,CTSI_USDT,AGLD_USDT,YGG_USDT,OP_USDT,ZRX_USDT,GT_USDT,XCH_USDT,VET_USDT,MOB_USDT,BICO_USDT,SLP_USDT,ACH_USDT,AR_USDT,CLV_USDT,IMX_USDT,SPELL_USDT,UNFI_USDT,SUSHI_USDT,FTT_USDT,HIGH_USDT,HNT_USDT,ALT_USDT,YFI_USDT,NEAR_USDT,NKN_USDT,XVS_USDT,BAND_USDT,LOKA_USDT,BCH_USDT,TOMO_USDT,WAVES_USDT,FIDA_USDT,DIA_USDT,ANC_USDT,CELO_USDT,CRV_USDT,FLM_USDT,GLMR_USDT,FIL_USDT,PEOPLE_USDT,WAXP_USDT,IOTX_USDT,ATOM_USDT,RLC_USDT,HBAR_USDT,REN_USDT,GMT_USDT,KAVA_USDT,KDA_USDT,GALA_USDT,STORJ_USDT,PUNDIX_USDT,BAL_USDT,XAUG_USDT,GRIN_USDT,SXP_USDT,AKRO_USDT,NEXO_USDT,CKB_USDT,API3_USDT,NEST_USDT,ETHW_USDT,TONCOIN_USDT,THETA_USDT,CREAM_USDT,BTC_USDT,GST_USDT,BEAM_USDT,HFT_USDT,KSM_USDT,RAD_USDT,QTUM_USDT,WOO_USDT,ATA_USDT,AVAX_USDT,EOS_USDT,SNX_USDT,AUCTION_USDT,XRP_USDT,GITCOIN_USDT,MATIC_USDT,ONT_USDT,LINA_USDT,DASH_USDT,MASK_USDT,ETC_USDT,JST_USDT,BSW_USDT,CONV_USDT,SKL_USDT,GAL_USDT,DODO_USDT,GRT_USDT,TRU_USDT,STX_USDT,CVX_USDT,JASMY_USDT,HIVE_USDT,EXCH_USDT,ROSE_USDT,SUPER_USDT,SCRT_USDT,USTC_USDT,ENJ_USDT,BTS_USDT,LOOKS_USDT,QNT_USDT,HOOK_USDT,FLOW_USDT,RUNE_USDT,APT_USDT,CHZ_USDT,DOGE_USDT,1INCH_USDT,PRIV_USDT,CSPR_USDT,C98_USDT,RACA_USDT,CELR_USDT,XEC_USDT,ENS_USDT,POND_USDT,NYM_USDT,PROM_USDT,IOST_USDT,ZEN_USDT,LDO_USDT,RNDR_USDT,REQ_USDT,DEGO_USDT,VRA_USDT,QUICK_USDT,VGX_USDT,XTZ_USDT,EGLD_USDT,POLS_USDT,ARPA_USDT,NFT_USDT" - }, - "cross_margin": { - "enabled": "BTC_USDT,ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT", - "available": "ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT,TARA_USDT,TRX_USDT,OXY_USDT,LON_USDT,DOGE_USDT,ISP_USDT,TWT_USDT,BAO_USDT,QUACK_USDT,ANT_USDT,VGX_USDT,ARPA_USDT,QUICK_USDT,UTK_USDT,HERO_USDT,WSG_USDT,BICO_USDT,MTV_USDT,VET_USDT,GARI_USDT,BCH_USDT,KLAY_USDT,WING_USDT,BLOK_USDT,SPS_USDT,WIKEN_USDT,WSIENNA_USDT,PUNDIX_USDT,FIC_USDT,ASTR_USDT,FET_USDT,VELO_USDT,BENQI_USDT,CWEB_USDT,RIF_USDT,UNI_USDT,ONG_USDT,ERG_USDT,ALPHA_USDT,CELO_USDT,XVG_USDT,GMAT_USDT,BTS_USDT,DOCK_USDT,GMT_USDT,DIA_USDT,CSPR_USDT,NKN_USDT,STAKE_USDT,SWASH_USDT,XEC_USDT,SWRV_USDT,QRDO_USDT,BLES_USDT,EOS_USDT,GRT_USDT,ASM_USDT,FIL6_USDT,GNO_USDT,EGLD_USDT,XYM_USDT,LOOKS_USDT,LOKA_USDT,BNC_USDT,BAS_USDT,SKL_USDT,STMX_USDT,CVC_USDT,DDOS_USDT,COTI_USDT,AVA_USDT,HMT_USDT,DF_USDT,LPT_USDT,XRP_USDT,TVK_USDT,FEVR_USDT,MBL_USDT,KIN_USDT,SPELL_USDT,MATIC_USDT,FTT_USDT,NMR_USDT,PMON_USDT,BNB_USDT,USDD_USDT,LSS_USDT,MDX_USDT,PRQ_USDT,ALPINE_USDT,DEGO_USDT,OMI_USDT,TIPS_USDT,OCT_USDT,FEI_USDT,UMEE_USDT,CRP_USDT,LION_USDT,YFI_USDT,DASH_USDT,REQ_USDT,SDAO_USDT,PNT_USDT,INSUR_USDT,OOKI_USDT,SUN_USDT,CRPT_USDT,BAC_USDT,DATA_USDT,LRN_USDT,JGN_USDT,KIMCHI_USDT,SUKU_USDT,VRA_USDT,AAVE_USDT,FTI_USDT,LDO_USDT,FRA_USDT,BLANK_USDT,NEAR_USDT,ZKS_USDT,MTRG_USDT,RLY_USDT,TCT_USDT,FLY_USDT,JST_USDT,YFII_USDT,AR_USDT,POLY_USDT,JULD_USDT,SOL_USDT,BZZ_USDT,AXS_USDT,ASD_USDT,XMR_USDT,FTM_USDT,HIT_USDT,LEO_USDT,LIT_USDT,PIG_USDT,COMP_USDT,ELON_USDT,IMX_USDT,EFI_USDT,XVS_USDT,WAVES_USDT,PEOPLE_USDT,SOS_USDT,RUNE_USDT,POLC_USDT,SCLP_USDT,BABYDOGE_USDT,KONO_USDT,SPI_USDT,ETC_USDT,MDA_USDT,MTL_USDT,BCHA_USDT,KISHU_USDT,SUNNY_USDT,PYR_USDT,XTZ_USDT,TRIBE_USDT,AUDIO_USDT,FIRO_USDT,MANA_USDT,OKB_USDT,DOG_USDT,SLP_USDT,KNC_USDT,GAS_USDT,LUNA_USDT,SAFEMARS_USDT,MIR_USDT,DAR_USDT,EGS_USDT,KSM_USDT,ATP_USDT,BIT_USDT,STORJ_USDT,XEM_USDT,QTUM_USDT,AGLD_USDT,RVN_USDT,OXT_USDT,SHFT_USDT,IOTX_USDT,LUNC_USDT,NEXO_USDT,AKITA_USDT,PERP_USDT,ONE_USDT,ETH_USDT,FLUX_USDT,FLOKI_USDT,STX_USDT,ANML_USDT,XPRT_USDT,GALA_USDT,GXS_USDT,TORN_USDT,KAI_USDT,1INCH_USDT,CHR_USDT,GAL_USDT,GLMR_USDT,CTX_USDT,CERE_USDT,CART_USDT,STRAX_USDT,MASK_USDT,MKR_USDT,AVAX_USDT,ENJ_USDT,YAM_USDT,ALPACA_USDT,DODO_USDT,MFT_USDT,CAKE_USDT,RNDR_USDT,CTSI_USDT,GRIN_USDT,MXC_USDT,ONT_USDT,ANKR_USDT,SLIM_USDT,FIL_USDT,CTK_USDT,ASR_USDT,FEG_USDT,SERO_USDT,RSS3_USDT,IRIS_USDT,XCH_USDT,ZRX_USDT,BAND_USDT,BADGER_USDT,DAO_USDT,EPS_USDT,THETA_USDT,BAKE_USDT,SHIB_USDT,MBOX_USDT,NBS_USDT,SNT_USDT,DREP_USDT,NFT_USDT,AUCTION_USDT,BOSON_USDT,O3_USDT,NULS_USDT,OMG_USDT,PEARL_USDT,HAPI_USDT,STG_USDT,IDV_USDT,HORD_USDT,ZIL_USDT,SUPER_USDT,DENT_USDT,REN_USDT,RAI_USDT,ZEN_USDT,ALGO_USDT,BLZ_USDT,BOR_USDT,SC_USDT,HEGIC_USDT,MOB_USDT,DORA_USDT,FOR_USDT,FLOW_USDT,RARI_USDT,DYDX_USDT,ATLAS_USDT,GST_USDT,REEF_USDT,HT_USDT,XYO_USDT,CHESS_USDT,BAT_USDT,NYM_USDT,RAMP_USDT,USDC_USDT,ICP_USDT,EPK_USDT,EXRD_USDT,DOT_USDT,COOK_USDT,CKB_USDT,YGG_USDT,CRU_USDT,ANC_USDT,FIS_USDT,ALCX_USDT,HIGH_USDT,BEAM_USDT,BSW_USDT,STAR_USDT,ROSE_USDT,CNNS_USDT,BZRX_USDT,WOO_USDT,SAFEMOON_USDT,VTHO_USDT,OM_USDT,LAMB_USDT,CHZ_USDT,AIOZ_USDT,EDEN_USDT,POND_USDT,ATOM_USDT,UNFI_USDT,FORTH_USDT,MLN_USDT,NEO_USDT,MOVR_USDT,RLC_USDT,FXS_USDT,ENS_USDT,ATA_USDT,XPR_USDT,NEST_USDT,XLM_USDT,AUTO_USDT,SNX_USDT,OCN_USDT,RSR_USDT,MITH_USDT,KAR_USDT,INJ_USDT,PLA_USDT,CYS_USDT,WAXP_USDT,VOXEL_USDT,CRV_USDT,FITFI_USDT,WHALE_USDT,WRX_USDT,TIDAL_USDT,C98_USDT,HNT_USDT,TONCOIN_USDT,DOGGY_USDT,SYS_USDT,NPXS_USDT,CRO_USDT,LEMD_USDT,RAY_USDT,PERL_USDT,CQT_USDT,CFX_USDT,TOMO_USDT,ACA_USDT,SDN_USDT,OKT_USDT,WILD_USDT,BNX_USDT,TRU_USDT,RACA_USDT,SWEAT_USDT,ACH_USDT,AKRO_USDT,BTM_USDT,TKO_USDT,GT_USDT,OCEAN_USDT,WNCG_USDT,BSV_USDT,GHST_USDT,CELR_USDT,LINA_USDT,SAND_USDT,APE_USDT,WICC_USDT,FIDA_USDT,ADA_USDT,PROPS_USDT,METIS_USDT,KAVA_USDT,AERGO_USDT,CONV_USDT,TFUEL_USDT,FRONT_USDT,API3_USDT,FARM_USDT,AE_USDT,LRC_USDT,IOTA_USDT,RFOX_USDT,PHA_USDT,XCN_USDT,NAS_USDT,KEEP_USDT,VIDY_USDT,HOT_USDT,MINA_USDT,ETHW_USDT,ALICE_USDT,HAI_USDT,BTC_USDT,LTC_USDT,LTO_USDT,DC_USDT,NU_USDT,IOST_USDT,RAD_USDT,POLS_USDT,OP_USDT,WXT_USDT,STR_USDT,YIELD_USDT,GM_USDT,SPA_USDT,BTCST_USDT,WEMIX_USDT,CLV_USDT,ICX_USDT,PET_USDT,STARL_USDT,HBAR_USDT,REDTOKEN_USDT,BTT_USDT,LINK_USDT,TLM_USDT,ARES_USDT,GTC_USDT,SUSHI_USDT,KEY_USDT,ALN_USDT,KDA_USDT,DVI_USDT,SXP_USDT,MAPS_USDT,BCD_USDT,SRM_USDT,WIN_USDT,ZEC_USDT,JASMY_USDT" - }, - "margin": { - "enabled": "BTC_USDT,ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT", - "available": "BTC_USDT,ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT,TARA_USDT,TRX_USDT,OXY_USDT,LON_USDT,DOGE_USDT,ISP_USDT,TWT_USDT,BAO_USDT,QUACK_USDT,ANT_USDT,VGX_USDT,ARPA_USDT,QUICK_USDT,UTK_USDT,HERO_USDT,WSG_USDT,BICO_USDT,MTV_USDT,VET_USDT,GARI_USDT,BCH_USDT,KLAY_USDT,WING_USDT,BLOK_USDT,SPS_USDT,WIKEN_USDT,WSIENNA_USDT,PUNDIX_USDT,FIC_USDT,ASTR_USDT,FET_USDT,VELO_USDT,BENQI_USDT,CWEB_USDT,RIF_USDT,UNI_USDT,ONG_USDT,ERG_USDT,ALPHA_USDT,CELO_USDT,XVG_USDT,GMAT_USDT,BTS_USDT,DOCK_USDT,GMT_USDT,DIA_USDT,CSPR_USDT,NKN_USDT,STAKE_USDT,SWASH_USDT,XEC_USDT,SWRV_USDT,QRDO_USDT,BLES_USDT,EOS_USDT,GRT_USDT,ASM_USDT,FIL6_USDT,GNO_USDT,EGLD_USDT,XYM_USDT,LOOKS_USDT,LOKA_USDT,BNC_USDT,BAS_USDT,SKL_USDT,STMX_USDT,CVC_USDT,DDOS_USDT,COTI_USDT,AVA_USDT,HMT_USDT,DF_USDT,LPT_USDT,XRP_USDT,TVK_USDT,FEVR_USDT,MBL_USDT,KIN_USDT,SPELL_USDT,MATIC_USDT,FTT_USDT,NMR_USDT,PMON_USDT,BNB_USDT,USDD_USDT,LSS_USDT,MDX_USDT,PRQ_USDT,ALPINE_USDT,DEGO_USDT,OMI_USDT,TIPS_USDT,OCT_USDT,FEI_USDT,UMEE_USDT,CRP_USDT,LION_USDT,YFI_USDT,DASH_USDT,REQ_USDT,SDAO_USDT,PNT_USDT,INSUR_USDT,OOKI_USDT,SUN_USDT,CRPT_USDT,BAC_USDT,DATA_USDT,LRN_USDT,JGN_USDT,KIMCHI_USDT,SUKU_USDT,VRA_USDT,AAVE_USDT,FTI_USDT,LDO_USDT,FRA_USDT,BLANK_USDT,NEAR_USDT,ZKS_USDT,MTRG_USDT,RLY_USDT,TCT_USDT,FLY_USDT,JST_USDT,YFII_USDT,AR_USDT,POLY_USDT,JULD_USDT,SOL_USDT,BZZ_USDT,AXS_USDT,ASD_USDT,XMR_USDT,FTM_USDT,HIT_USDT,LEO_USDT,LIT_USDT,PIG_USDT,COMP_USDT,ELON_USDT,IMX_USDT,EFI_USDT,XVS_USDT,WAVES_USDT,PEOPLE_USDT,SOS_USDT,RUNE_USDT,POLC_USDT,SCLP_USDT,BABYDOGE_USDT,KONO_USDT,SPI_USDT,ETC_USDT,MDA_USDT,MTL_USDT,BCHA_USDT,KISHU_USDT,SUNNY_USDT,PYR_USDT,XTZ_USDT,TRIBE_USDT,AUDIO_USDT,FIRO_USDT,MANA_USDT,OKB_USDT,DOG_USDT,SLP_USDT,KNC_USDT,GAS_USDT,LUNA_USDT,SAFEMARS_USDT,MIR_USDT,DAR_USDT,EGS_USDT,KSM_USDT,ATP_USDT,BIT_USDT,STORJ_USDT,XEM_USDT,QTUM_USDT,AGLD_USDT,RVN_USDT,OXT_USDT,SHFT_USDT,IOTX_USDT,LUNC_USDT,NEXO_USDT,AKITA_USDT,PERP_USDT,ONE_USDT,ETH_USDT,FLUX_USDT,FLOKI_USDT,STX_USDT,ANML_USDT,XPRT_USDT,GALA_USDT,GXS_USDT,TORN_USDT,KAI_USDT,1INCH_USDT,CHR_USDT,GAL_USDT,GLMR_USDT,CTX_USDT,CERE_USDT,CART_USDT,STRAX_USDT,MASK_USDT,MKR_USDT,AVAX_USDT,ENJ_USDT,YAM_USDT,ALPACA_USDT,DODO_USDT,MFT_USDT,CAKE_USDT,RNDR_USDT,CTSI_USDT,GRIN_USDT,MXC_USDT,ONT_USDT,ANKR_USDT,SLIM_USDT,FIL_USDT,CTK_USDT,ASR_USDT,FEG_USDT,SERO_USDT,RSS3_USDT,IRIS_USDT,XCH_USDT,ZRX_USDT,BAND_USDT,BADGER_USDT,DAO_USDT,EPS_USDT,THETA_USDT,BAKE_USDT,SHIB_USDT,MBOX_USDT,NBS_USDT,SNT_USDT,DREP_USDT,NFT_USDT,AUCTION_USDT,BOSON_USDT,O3_USDT,NULS_USDT,OMG_USDT,PEARL_USDT,HAPI_USDT,STG_USDT,IDV_USDT,HORD_USDT,ZIL_USDT,SUPER_USDT,DENT_USDT,REN_USDT,RAI_USDT,ZEN_USDT,ALGO_USDT,BLZ_USDT,BOR_USDT,SC_USDT,HEGIC_USDT,MOB_USDT,DORA_USDT,FOR_USDT,FLOW_USDT,RARI_USDT,DYDX_USDT,ATLAS_USDT,GST_USDT,REEF_USDT,HT_USDT,XYO_USDT,CHESS_USDT,BAT_USDT,NYM_USDT,RAMP_USDT,USDC_USDT,ICP_USDT,EPK_USDT,EXRD_USDT,DOT_USDT,COOK_USDT,CKB_USDT,YGG_USDT,CRU_USDT,ANC_USDT,FIS_USDT,ALCX_USDT,HIGH_USDT,BEAM_USDT,BSW_USDT,STAR_USDT,ROSE_USDT,CNNS_USDT,BZRX_USDT,WOO_USDT,SAFEMOON_USDT,VTHO_USDT,OM_USDT,LAMB_USDT,CHZ_USDT,AIOZ_USDT,EDEN_USDT,POND_USDT,ATOM_USDT,UNFI_USDT,FORTH_USDT,MLN_USDT,NEO_USDT,MOVR_USDT,RLC_USDT,FXS_USDT,ENS_USDT,ATA_USDT,XPR_USDT,NEST_USDT,XLM_USDT,AUTO_USDT,SNX_USDT,OCN_USDT,RSR_USDT,MITH_USDT,KAR_USDT,INJ_USDT,PLA_USDT,CYS_USDT,WAXP_USDT,VOXEL_USDT,CRV_USDT,FITFI_USDT,WHALE_USDT,WRX_USDT,TIDAL_USDT,C98_USDT,HNT_USDT,TONCOIN_USDT,DOGGY_USDT,SYS_USDT,NPXS_USDT,CRO_USDT,LEMD_USDT,RAY_USDT,PERL_USDT,CQT_USDT,CFX_USDT,TOMO_USDT,ACA_USDT,SDN_USDT,OKT_USDT,WILD_USDT,BNX_USDT,TRU_USDT,RACA_USDT,SWEAT_USDT,ACH_USDT,AKRO_USDT,BTM_USDT,TKO_USDT,GT_USDT,OCEAN_USDT,WNCG_USDT,BSV_USDT,GHST_USDT,CELR_USDT,LINA_USDT,SAND_USDT,APE_USDT,WICC_USDT,FIDA_USDT,ADA_USDT,PROPS_USDT,METIS_USDT,KAVA_USDT,AERGO_USDT,CONV_USDT,TFUEL_USDT,FRONT_USDT,API3_USDT,FARM_USDT,AE_USDT,LRC_USDT,IOTA_USDT,RFOX_USDT,PHA_USDT,XCN_USDT,NAS_USDT,KEEP_USDT,VIDY_USDT,HOT_USDT,MINA_USDT,ETHW_USDT,ALICE_USDT,HAI_USDT,LTC_USDT,LTO_USDT,DC_USDT,NU_USDT,IOST_USDT,RAD_USDT,POLS_USDT,OP_USDT,WXT_USDT,STR_USDT,YIELD_USDT,GM_USDT,SPA_USDT,BTCST_USDT,WEMIX_USDT,CLV_USDT,ICX_USDT,PET_USDT,STARL_USDT,HBAR_USDT,REDTOKEN_USDT,BTT_USDT,LINK_USDT,TLM_USDT,ARES_USDT,GTC_USDT,SUSHI_USDT,KEY_USDT,ALN_USDT,KDA_USDT,DVI_USDT,SXP_USDT,MAPS_USDT,BCD_USDT,SRM_USDT,WIN_USDT,ZEC_USDT,JASMY_USDT" - }, - "delivery": { - "enabled": "BTC_USD_20230331,BTC_USD_20221230,BTC_USDT_20221021,BTC_USDT_20221014", - "available": "BTC_USD_20221021,BTC_USD_20221014,BTC_USD_20230331,BTC_USD_20221230,BTC_USDT_20221021,BTC_USDT_20221014,BTC_USDT_20230331,BTC_USDT_20221230" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} - }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": true - } - }, - "bankAccounts": [ - { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "Gemini", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": true - }, - "configFormat": { - "uppercase": true - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTCUSD", - "available": "BTCUSD,ETHBTC,ETHUSD,BCHUSD,BCHBTC,BCHETH,LTCUSD,LTCBTC,LTCETH,LTCBCH,ZECUSD,ZECBTC,ZECETH,ZECBCH,ZECLTC" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + "name": "EXMO", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD,EUR,RUB,PLN,UAH", + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "_", + "separator": "," + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_USD,LTC_USD", + "available": "BCH_RUB,DASH_RUB,EOS_USD,ETH_TRY,GNT_ETH,LTC_USD,PTI_USDT,XRP_BTC,EXM_BTC,BTG_BTC,ETC_RUB,BTG_USD,NEO_RUB,XMR_BTC,ZRX_ETH,MNX_BTC,USDC_BTC,XRP_EUR,SMART_USD,EOS_BTC,MNX_ETH,ZEC_BTC,BCH_USD,WAVES_USD,TRX_BTC,XRP_TRY,DASH_USD,DOGE_USD,ETZ_USDT,GUSD_USD,MNC_BTC,ZEC_USD,DCR_BTC,DXT_USD,PTI_RUB,XMR_ETH,ZRX_USD,DAI_RUB,MNC_USD,XLM_TRY,DAI_BTC,BTC_EUR,LTC_EUR,OMG_BTC,PTI_EOS,SMART_RUB,XTZ_USD,HP_EXM,ADA_USD,OMG_ETH,QTUM_USD,TRX_RUB,USDC_ETH,USDC_USDT,USD_RUB,BTC_UAH,BCH_USDT,ETH_PLN,KICK_RUB,LSK_RUB,SMART_BTC,XMR_UAH,XRP_USD,GUSD_BTC,QTUM_ETH,USDT_EUR,BTC_RUB,DCR_UAH,ETH_RUB,DOGE_BTC,ETZ_BTC,INK_USD,LTC_UAH,BTT_UAH,BTC_USDT,MNC_ETH,XTZ_ETH,BTC_TRY,DXT_BTC,KICK_USDT,OMG_USD,WAVES_BTC,XLM_BTC,BTCZ_BTC,GNT_BTC,LSK_BTC,LTC_RUB,NEO_BTC,XEM_UAH,XMR_USD,ZAG_BTC,GAS_USD,LTC_BTC,TRX_UAH,XEM_EUR,XMR_RUB,XTZ_RUB,ETZ_ETH,ETC_BTC,GUSD_RUB,INK_BTC,LSK_USD,MNX_USD,SMART_EUR,VLX_BTC,BCH_ETH,XMR_EUR,ADA_ETH,QTUM_BTC,XEM_USD,ATMCASH_BTC,ADA_BTC,ETH_EUR,TRX_USD,USDC_USD,BCH_BTC,ETH_UAH,KICK_BTC,WAVES_RUB,XEM_BTC,ETH_BTC,BCH_EUR,BTT_BTC,ROOBEE_BTC,XLM_USD,XRP_ETH,ETH_USD,MKR_DAI,XTZ_BTC,DAI_USD,BCH_UAH,INK_ETH,KICK_ETH,MKR_BTC,NEO_USD,XRP_USDT,ZEC_EUR,BTC_USD,XRP_RUB,EOS_EUR,ETH_USDT,USDT_UAH,XRP_UAH,ZEC_RUB,HP_BTC,BTT_RUB,DAI_ETH,DASH_UAH,DASH_USDT,ETH_LTC,GAS_BTC,USDT_USD,BTG_ETH,XLM_RUB,WAVES_ETH,USDT_RUB,ZRX_BTC,DASH_BTC,DCR_RUB,ETC_USD,HB_BTC,PTI_BTC,BTC_PLN" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "HitBTC", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": true - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC-USD", - "available": "BCN-BTC,BTC-USD,DASH-BTC,DOGE-BTC,DOGE-USD,EMC-BTC,ETH-BTC,LSK-BTC,LTC-BTC,LTC-USD,NXT-BTC,SBD-BTC,SC-BTC,STEEM-BTC,XDN-BTC,XEM-BTC,XMR-BTC,ARDR-BTC,ZEC-BTC,WAVES-BTC,MAID-BTC,DGD-BTC,SNGLS-BTC,1ST-BTC,TRST-BTC,TIME-BTC,GNO-BTC,REP-BTC,XMR-USD,DASH-USD,ETH-USD,NXT-USD,ZRC-BTC,BOS-BTC,DCT-BTC,ANT-BTC,AEON-BTC,GUP-BTC,PLU-BTC,LUN-BTC,EDG-BTC,RLC-BTC,SWT-BTC,TKN-BTC,WINGS-BTC,XAUR-BTC,AE-BTC,PTOY-BTC,ZEC-USD,XEM-USD,BCN-USD,XDN-USD,MAID-USD,ETC-BTC,ETC-USD,PLBT-BTC,BNT-BTC,SNT-ETH,CVC-USD,PAY-ETH,OAX-ETH,OMG-ETH,BQX-ETH,XTZ-BTC,DICE-BTC,PTOY-ETH,1ST-ETH,XAUR-ETH,TIME-ETH,DICE-ETH,SWT-ETH,XMR-ETH,ETC-ETH,DASH-ETH,ZEC-ETH,PLU-ETH,GNO-ETH,XRP-BTC,STRAT-USD,STRAT-BTC,SNC-ETH,ADX-ETH,BET-ETH,EOS-ETH,DENT-ETH,SAN-ETH,EOS-BTC,EOS-USD,XTZ-ETH,XTZ-USD,MYB-ETH,SUR-ETH,IXT-ETH,PLR-ETH,TIX-ETH,PRO-ETH,AVT-ETH,EVX-USD,DLT-BTC,BNT-ETH,BNT-USD,MANA-USD,DNT-BTC,FYP-BTC,OPT-BTC,TNT-ETH,STX-BTC,STX-ETH,STX-USD,TNT-USD,TNT-BTC,ENG-ETH,XUC-USD,SNC-BTC,SNC-USD,OAX-USD,OAX-BTC,ZRX-BTC,ZRX-ETH,ZRX-USD,RVT-BTC,PPC-BTC,PPC-USD,QTUM-ETH,IGNIS-ETH,BMC-BTC,BMC-ETH,BMC-USD,CND-BTC,CND-ETH,CND-USD,CDT-ETH,CDT-USD,FUN-BTC,FUN-ETH,FUN-USD,HVN-BTC,HVN-ETH,POE-BTC,POE-ETH,AMB-USD,AMB-ETH,AMB-BTC,HPC-BTC,PPT-ETH,MTH-BTC,MTH-ETH,LRC-BTC,LRC-ETH,ICX-BTC,ICX-ETH,NEO-BTC,NEO-ETH,NEO-USD,CSNO-BTC,ICX-USD,IND-ETH,KICK-BTC,YOYOW-BTC,CDT-BTC,XVG-BTC,XVG-ETH,XVG-USD,DGB-BTC,DGB-ETH,DGB-USD,DCN-ETH,DCN-USD,VIBE-BTC,ENJ-BTC,ENJ-ETH,ENJ-USD,ZSC-BTC,ZSC-ETH,ZSC-USD,TRX-BTC,TRX-ETH,TRX-USD,ART-BTC,EVX-BTC,EVX-ETH,SUB-BTC,SUB-ETH,SUB-USD,WTC-BTC,BTM-BTC,BTM-ETH,BTM-USD,LIFE-BTC,VIB-BTC,VIB-ETH,VIB-USD,DRT-ETH,STU-USD,OMG-BTC,PAY-BTC,PPT-BTC,SNT-BTC,BTG-BTC,BTG-ETH,BTG-USD,SMART-BTC,SMART-ETH,SMART-USD,XUC-ETH,XUC-BTC,LA-ETH,EDO-BTC,EDO-ETH,EDO-USD,HGT-ETH,IXT-BTC,SCL-BTC,ETP-BTC,ETP-ETH,ETP-USD,NEBL-BTC,NEBL-ETH,ARN-BTC,ARN-ETH,STU-BTC,STU-ETH,GVT-ETH,BTX-BTC,LTC-ETH,BCN-ETH,MAID-ETH,NXT-ETH,STRAT-ETH,XDN-ETH,XEM-ETH,PLR-BTC,SUR-BTC,BQX-BTC,DOGE-ETH,AMM-BTC,AMM-ETH,AMM-USD,DBIX-BTC,PRE-BTC,ZAP-BTC,DOV-BTC,DOV-ETH,XRP-ETH,XRP-USD,HSR-BTC,LEND-BTC,LEND-ETH,SPF-ETH,SBTC-BTC,SBTC-ETH,LOC-BTC,LOC-ETH,LOC-USD,SWFTC-BTC,SWFTC-ETH,SWFTC-USD,STAR-ETH,SBTC-USD,STORM-BTC,DIM-ETH,DIM-USD,DIM-BTC,NGC-BTC,NGC-ETH,NGC-USD,EMC-ETH,EMC-USD,MCO-BTC,MCO-ETH,MCO-USD,MANA-ETH,MANA-BTC,CPAY-ETH,DATA-BTC,DATA-ETH,DATA-USD,UTT-BTC,UTT-ETH,UTT-USD,KMD-BTC,KMD-ETH,KMD-USD,QTUM-USD,QTUM-BTC,SNT-USD,OMG-USD,EKO-BTC,EKO-ETH,ADX-BTC,ADX-USD,LSK-ETH,LSK-USD,PLR-USD,SUR-USD,BQX-USD,DRT-USD,REP-ETH,REP-USD,WAXP-BTC,WAXP-ETH,WAXP-USD,C20-BTC,C20-ETH,IDH-BTC,IDH-ETH,IPL-BTC,COV-BTC,COV-ETH,SENT-BTC,SENT-ETH,SENT-USD,SMT-BTC,SMT-ETH,SMT-USD,CHAT-BTC,CHAT-ETH,CHAT-USD,TRAC-ETH,JNT-ETH,UTK-BTC,UTK-ETH,UTK-USD,GNX-ETH,CHSB-BTC,CHSB-ETH,DAY-BTC,DAY-ETH,DAY-USD,NEU-BTC,NEU-ETH,NEU-USD,TAU-BTC,FLP-BTC,FLP-ETH,FLP-USD,R-BTC,R-ETH,EKO-USD,BCPT-ETH,BCPT-USD,PKT-BTC,PKT-ETH,BETR-BTC,BETR-ETH,HAND-ETH,HAND-USD,CHP-ETH,BCPT-BTC,ACT-BTC,ACT-ETH,ACT-USD,ADA-BTC,ADA-ETH,ADA-USD,SIG-BTC,MTX-BTC,MTX-ETH,MTX-USD,WIZ-BTC,WIZ-ETH,WIZ-USD,DADI-BTC,DADI-ETH,BDG-ETH,DATX-BTC,DATX-ETH,TRUE-BTC,DRG-BTC,DRG-ETH,BANCA-BTC,BANCA-ETH,ZAP-ETH,ZAP-USD,AUTO-BTC,SOC-BTC,OCN-BTC,OCN-ETH,STQ-BTC,STQ-ETH,XLM-BTC,XLM-ETH,XLM-USD,IOTA-BTC,IOTA-ETH,IOTA-USD,DRT-BTC,BETR-USD,ERT-BTC,CRPT-BTC,CRPT-USD,MESH-BTC,MESH-ETH,MESH-USD,IHT-BTC,IHT-ETH,IHT-USD,SCC-BTC,YCC-BTC,DAN-BTC,TEL-BTC,TEL-ETH,NCT-BTC,NCT-ETH,NCT-USD,BMH-BTC,BANCA-USD,BERRY-BTC,BERRY-ETH,BERRY-USD,GBX-BTC,GBX-ETH,GBX-USD,SHIP-BTC,SHIP-ETH,NANO-BTC,NANO-ETH,NANO-USD,LNC-BTC,KIN-ETH,ARDR-USD,FOTA-ETH,FOTA-BTC,CVT-BTC,CVT-ETH,CVT-USD,STQ-USD,GNT-BTC,GNT-ETH,GNT-USD,GET-BTC,MITH-BTC,MITH-ETH,MITH-USD,DADI-USD,TKY-BTC,ACAT-BTC,ACAT-ETH,ACAT-USD,BTX-USD,WIKI-BTC,WIKI-ETH,WIKI-USD,ONT-BTC,ONT-ETH,ONT-USD,FTX-BTC,FTX-ETH,NAVI-BTC,VME-ETH,NAVI-ETH,LND-ETH,CSM-BTC,NANJ-BTC,NTK-BTC,NTK-ETH,NTK-USD,AUC-BTC,AUC-ETH,CMCT-BTC,CMCT-ETH,CMCT-USD,MAN-BTC,MAN-ETH,MAN-USD,PNT-BTC,PNT-ETH,FXT-BTC,NEXO-BTC,PAT-BTC,PAT-ETH,XMC-BTC,FXT-ETH,XMC-ETH,XMC-USD,FDZ-BTC,FDZ-ETH,FDZ-USD,SPD-BTC,SPD-ETH,MITX-BTC,TIV-BTC,B2G-BTC,B2G-USD,HBZ-BTC,FACE-BTC,FACE-ETH,HBZ-ETH,HBZ-USD,CPT-BTC,PAT-USD,HTML-BTC,HTML-ETH,MITX-ETH,BTS-BTC,BNK-BTC,BNK-ETH,BNK-USD,TIV-ETH,TIV-USD,CSM-ETH,CSM-USD,INK-BTC,IOST-BTC,INK-ETH,INK-USD,CBC-BTC,IOST-USD,ZIL-BTC,ABYSS-BTC,ABYSS-ETH,ZIL-USD,BCI-BTC,CBC-ETH,CBC-USD,PITCH-BTC,PITCH-ETH,HTML-USD,TDS-BTC,TDS-ETH,TDS-USD,SBD-ETH,SBD-USD,DPN-BTC,UUU-BTC,UUU-ETH,XBP-BTC,ELEC-BTC,ELEC-ETH,ELEC-USD,QNTU-BTC,QNTU-ETH,QNTU-USD,IPL-ETH,IPL-USD,CENNZ-BTC,CENNZ-ETH,SWM-BTC,SPF-USD,SPF-BTC,LCC-BTC,HGT-BTC,ETH-TUSD,BTC-TUSD,LTC-TUSD,XMR-TUSD,ZRX-TUSD,NEO-TUSD,USD-TUSD,BTC-DAI,ETH-DAI,MKR-DAI,EOS-DAI,USD-DAI,MKR-BTC,MKR-ETH,MKR-USD,TUSD-DAI,NEO-DAI,LTC-DAI,XMR-DAI,XRP-DAI,NEXO-ETH,NEXO-USD,DWS-BTC,DWS-ETH,DWS-USD,APPC-BTC,APPC-ETH,APPC-USD,BIT-ETH,SPC-BTC,SPC-ETH,SPC-USD,REX-BTC,REX-ETH,REX-USD,ELF-BTC,ELF-USD,BCD-BTC,BCD-USD,CVCOIN-BTC,CVCOIN-ETH,CVCOIN-USD,EDG-ETH,EDG-USD,NLC2-BTC,DASH-EURS,ZEC-EURS,BTC-EURS,EOS-EURS,ETH-EURS,LTC-EURS,NEO-EURS,XMR-EURS,XRP-EURS,EURS-USD,EURS-TUSD,EURS-DAI,MNX-USD,ROX-ETH,ZPR-ETH,MNX-BTC,MNX-ETH,KIND-BTC,KIND-ETH,ENGT-BTC,ENGT-ETH,PMA-BTC,PMA-ETH,TV-BTC,TV-ETH,TV-USD,BAT-BTC,BAT-ETH,BAT-USD,SRN-BTC,SRN-ETH,SRN-USD,SVD-BTC,SVD-ETH,SVD-USD,GST-BTC,GST-ETH,GST-USD,BNB-BTC,BNB-ETH,BNB-USD,DIT-BTC,DIT-ETH,POA20-BTC,PROC-BTC,POA20-ETH,POA20-USD,POA20-DAI,NIM-BTC,USE-BTC,USE-ETH,DAV-BTC,DAV-ETH,ABTC-BTC,NIM-ETH,ABA-BTC,ABA-ETH,ABA-USD,BCN-EOS,LTC-EOS,XMR-EOS,DASH-EOS,TRX-EOS,NEO-EOS,ZEC-EOS,LSK-EOS,XEM-EOS,XRP-EOS,RCN-BTC,RCN-ETH,RCN-USD,HMQ-BTC,HMQ-ETH,MYST-BTC,MYST-ETH,USD-GUSD,BTC-GUSD,ETH-GUSD,EOS-GUSD,AXPR-BTC,AXPR-ETH,DAG-BTC,DAG-ETH,BITS-BTC,BITS-ETH,BITS-USD,CDCC-BTC,CDCC-ETH,CDCC-USD,VET-BTC,VET-ETH,VET-USD,SILK-ETH,BOX-BTC,BOX-ETH,BOX-EURS,BOX-EOS,VOCO-BTC,VOCO-ETH,VOCO-USD,PASS-BTC,PASS-ETH,SLX-BTC,SLX-USD,PBTT-BTC,PMA-USD,TRAD-BTC,DGTX-BTC,DGTX-ETH,DGTX-USD,MRK-BTC,MRK-ETH,DGB-TUSD,SNBL-BTC,BCH-BTC,BCH-USD,BSV-BTC,BSV-USD,BKX-BTC,NPLC-BTC,NPLC-ETH,ETN-BTC,ETN-ETH,ETN-USD,DTR-BTC,DTR-ETH,TDP-BTC,HBT-ETH,PXG-BTC,PXG-USD,BTC-PAX,ETH-PAX,USD-PAX,BTC-USDC,ETH-USDC,USD-USDC,TUSD-USDC,DAI-USDC,EOS-PAX,CLO-BTC,CLO-ETH,CLO-USD,PETH-BTC,PETH-ETH,PETH-USD,BRD-BTC,BRD-ETH,NMR-BTC,SALT-BTC,SALT-ETH,POLY-BTC,POLY-ETH,POWR-BTC,POWR-ETH,STORJ-BTC,STORJ-ETH,STORJ-USD,MLN-BTC,MLN-ETH,BDG-BTC,POA-ETH,POA-BTC,POA-USD,POA-DAI,KIN-BTC,VEO-BTC,PLA-BTC,PLA-ETH,PLA-USD,BTT-BTC,BTT-USD,BTT-ETH,ZEN-BTC,ZEN-ETH,ZEN-USD,GRIN-BTC,GRIN-ETH,GRIN-USD,FET-BTC,HT-BTC,HT-USD,XZC-BTC,XZC-ETH,XZC-USD,VRA-BTC,VRA-ETH,BTC-KRWB,USD-KRWB,WBTC-ETH,CRO-BTC,CRO-ETH,CRO-USD,GAS-BTC,GAS-ETH,GAS-USD,ORMEUS-BTC,ORMEUS-ETH,SWM-ETH,SWM-USD,PRE-ETH,PHX-BTC,PHX-ETH,PHX-USD,BET-BTC,USD-EOSDT,BTC-EOSDT,ETH-EOSDT,EOS-EOSDT,DAI-EOSDT,NUT-BTC,NUT-EOS,NUT-USD,CUTE-BTC,CUTE-ETH,CUTE-USD,CUTE-EOS,XCON-BTC,DCR-BTC,DCR-ETH,DCR-USD,MG-BTC,MG-ETH,MG-EOS,MG-USD,GNX-BTC,PRO-BTC,EURS-EOSDT,TUSD-EOSDT,ECOIN-BTC,ECOIN-ETH,ECOIN-USD,AGI-BTC,LOOM-BTC,LOOM-ETH,BLZ-BTC,QKC-BTC,QKC-ETH,KNC-BTC,KNC-ETH,KNC-USD,KEY-BTC,KEY-ETH,ATOM-BTC,ATOM-USD,ATOM-ETH,BRDG-BTC,BRDG-ETH,BRDG-USD,MTL-BTC,MTL-ETH,EXP-BTC,BTCB-BTC,PBT-BTC,PBT-ETH,LINK-BTC,LINK-ETH,LINK-USD,USD-USDT20,PHB-BTC,BCH-ETH,BCH-DAI,BCH-TUSD,BCH-EURS,DAPP-BTC,DAPP-EOS,BTC-USDT20,DENT-BTC,DENT-USD,NJBC-BTC,NJBC-ETH,XRC-BTC,EOS-BCH,LTC-BCH,XRP-BCH,TRX-BCH,XLM-BCH,ETC-BCH,DASH-BCH,ZEC-BCH,BKX-USD,LAMB-BTC,NPXS-BTC,HBAR-BTC,HBAR-USD,ONE-BTC,RFR-BTC,RFR-USD,BUSD-USD,PAXG-BTC,PAXG-USD,REN-BTC,IGNIS-BTC,CEL-BTC,CEL-ETH,WIN-USD,ADK-BTC,PART-BTC,SOZ-BTC,SOZ-ETH,SOZ-USD,WAVES-USD,ADA-BCH,ONT-BCH,XMR-BCH,ATOM-BCH,LINK-BCH,OMG-BCH,WAVES-BCH,IOTX-BTC,HOT-BTC,SLV-BTC,HEDG-BTC,CHZ-BTC,CHZ-USD,COCOS-BTC,COCOS-USD,SEELE-BTC,SEELE-USD,MDA-BTC,LEO-USD,REM-BTC,REM-ETH,REM-USD,SCD-DAI,BTC-BUSD,RVN-BTC,BST-BTC,ERD-BTC,KRL-BTC,FTT-BTC,FTT-USD,RAISE-BTC,RAISE-ETH" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + "name": "CoinbasePro", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD,GBP,EUR", + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USD", + "available": "LTC-GBP,XLM-BTC,DASH-BTC,DAI-USDC,ZEC-USDC,XLM-EUR,ZRX-BTC,LTC-BTC,ETC-BTC,ETH-USD,XRP-EUR,BTC-USDC,REP-USD,EOS-BTC,ZEC-BTC,ETC-GBP,LINK-ETH,XRP-BTC,ZRX-USD,ETH-USDC,MANA-USDC,BTC-EUR,BCH-GBP,DNT-USDC,EOS-EUR,BCH-EUR,LTC-EUR,CVC-USDC,ETH-GBP,DASH-USD,ETH-EUR,XTZ-BTC,ZRX-EUR,BAT-ETH,BTC-GBP,ETC-USD,BAT-USDC,BCH-USD,GNT-USDC,ALGO-USD,LINK-USD,XLM-USD,ETH-BTC,EOS-USD,REP-BTC,ETH-DAI,XRP-USD,LTC-USD,ETC-EUR,BTC-USD,XTZ-USD,BCH-BTC,LOOM-USDC" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true, + "requiresBase64DecodeSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "Huobi", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": false - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC-USDT", - "available": "PROPY-ETH,IOTA-BTC,UGAS-ETH,PAI-USDT,BSV-HUSD,MTX-ETH,BCH-BTC,LTC-HT,SOC-USDT,WXT-BTC,SALT-BTC,RCN-ETH,PNT-ETH,TT-USDT,AIDOC-ETH,BIX-BTC,OCN-USDT,QTUM-ETH,KCASH-ETH,SNT-USDT,LUN-BTC,QASH-BTC,ITC-BTC,NAS-BTC,XMR-BTC,TNT-ETH,UC-ETH,FAIR-BTC,PC-ETH,YEE-BTC,PAY-ETH,XMX-BTC,CRE-USDT,BAT-ETH,BHT-USDT,CKB-HT,LAMB-HT,AE-USDT,QUN-ETH,LYM-BTC,BCH-HT,BHT-BTC,RUFF-ETH,CNN-BTC,FOR-USDT,GTC-ETH,TRX-ETH,ELA-USDT,ACT-ETH,SMT-ETH,BUT-ETH,BCH-USDT,ICX-BTC,MEET-BTC,NCC-BTC,APPC-BTC,GVE-ETH,TNB-BTC,STEEM-ETH,18C-ETH,LBA-BTC,EKO-BTC,REQ-BTC,SOC-BTC,BOX-ETH,ELF-BTC,ZRX-ETH,LET-USDT,HT-BTC,TUSD-HUSD,EGCC-BTC,WTC-BTC,ATP-USDT,DOCK-USDT,PAI-BTC,ONT-ETH,IRIS-BTC,BTT-ETH,SC-BTC,XZC-BTC,LBA-USDT,HT-USDT,VET-ETH,KMD-ETH,SHE-ETH,PORTAL-BTC,ONE-BTC,BIX-USDT,RCCC-BTC,SKM-USDT,XTZ-ETH,SWFTC-BTC,RSR-BTC,LINK-ETH,DATX-BTC,HPT-HT,GET-ETH,BLZ-ETH,CTXC-USDT,CNNS-USDT,PVT-HT,ITC-USDT,LTC-BTC,NCASH-BTC,HOT-ETH,ADA-USDT,ADX-BTC,NODE-USDT,TRIO-BTC,GXC-ETH,SNT-BTC,FOR-BTC,DBC-BTC,UUU-USDT,CVCOIN-ETH,RSR-USDT,CRO-USDT,OCN-BTC,NEW-USDT,EGT-USDT,MANA-BTC,CMT-USDT,WXT-HT,XRP-BTC,MT-ETH,PAX-HUSD,LSK-ETH,IOTA-USDT,SRN-ETH,ZIL-ETH,ELF-USDT,LXT-ETH,LAMB-BTC,CRE-HT,CKB-BTC,XVG-BTC,BSV-BTC,BFT-BTC,WPR-ETH,HT-HUSD,POWR-BTC,MANA-ETH,ENG-ETH,ZJLT-ETH,SNC-ETH,ATOM-ETH,WICC-USDT,KAN-ETH,DGD-BTC,VSYS-HT,BCD-BTC,BTM-ETH,DOGE-USDT,MEX-BTC,BTG-BTC,DAC-ETH,DAT-BTC,GRS-ETH,ADX-ETH,EM-HT,GXC-USDT,CVC-BTC,OMG-ETH,SSP-ETH,OGO-HT,CMT-ETH,POLY-ETH,XZC-USDT,THETA-USDT,XEM-USDT,LOL-USDT,BCH-HUSD,GSC-BTC,DOGE-ETH,MDS-BTC,BTS-ETH,CTXC-BTC,MCO-BTC,BCX-BTC,ZLA-ETH,EKT-USDT,MAN-BTC,BLZ-BTC,ATOM-USDT,LOL-BTC,HPT-USDT,EM-BTC,EOS-USDT,WAN-BTC,GNT-BTC,CRO-BTC,MANA-USDT,SEELE-USDT,FSN-BTC,VIDY-HT,USDC-HUSD,LTC-HUSD,XRP-USDT,VSYS-BTC,STORJ-BTC,LOOM-ETH,SKM-BTC,LINK-USDT,TT-HT,QSP-ETH,ETN-BTC,FSN-HT,NODE-BTC,HC-USDT,PHX-BTC,XLM-BTC,RCCC-ETH,LTC-USDT,UUU-BTC,SEELE-ETH,PVT-BTC,HC-ETH,REN-ETH,KAN-USDT,EOS-ETH,BSV-USDT,BTS-USDT,KMD-BTC,OGO-USDT,THETA-ETH,MUSK-BTC,CNNS-HT,ETC-BTC,COVA-BTC,BTT-TRX,XMR-USDT,MTN-ETH,QUN-BTC,NAS-USDT,ELA-ETH,HIT-ETH,BTT-USDT,EKT-ETH,TOS-BTC,GAS-ETH,DCR-USDT,ONT-BTC,NEW-HT,NEXO-BTC,ETH-USDT,WXT-USDT,FOR-HT,ADA-BTC,EVX-ETH,VET-BTC,ZEC-USDT,NANO-ETH,IOST-HT,BCV-ETH,REN-USDT,NULS-ETH,ACT-USDT,LET-ETH,BTM-USDT,MEET-ETH,AKRO-HT,ARDR-BTC,DCR-ETH,NANO-USDT,BTC-HUSD,ALGO-BTC,IIC-ETH,BHD-BTC,KNC-ETH,ATP-BTC,ZRX-BTC,ABT-BTC,18C-BTC,XMR-ETH,WAXP-BTC,CVNT-BTC,MX-USDT,OST-ETH,NKN-BTC,TOPC-BTC,GNX-BTC,FTT-USDT,ONE-HT,DGB-ETH,NULS-USDT,DASH-BTC,UIP-BTC,KCASH-HT,WICC-ETH,EKO-ETH,EGT-HT,IRIS-USDT,STK-ETH,MXC-BTC,NAS-ETH,OMG-USDT,SMT-BTC,BUT-BTC,HIT-USDT,BAT-BTC,IRIS-ETH,NKN-HT,PC-BTC,TOP-USDT,GTC-BTC,LSK-BTC,ITC-ETH,DTA-BTC,HOT-BTC,BTT-BTC,FAIR-ETH,DOCK-ETH,QTUM-BTC,ZEN-BTC,ZIL-BTC,RCN-BTC,FTI-BTC,BHD-USDT,VIDY-USDT,LUN-ETH,DBC-ETH,TOPC-ETH,IIC-BTC,STEEM-USDT,IOTA-ETH,KCASH-BTC,RUFF-BTC,APPC-ETH,MT-BTC,SOC-ETH,GT-HT,PROPY-BTC,AIDOC-BTC,ACT-BTC,LYM-ETH,CHAT-BTC,SWFTC-ETH,ETH-BTC,UIP-USDT,UGAS-BTC,XRP-HUSD,ALGO-USDT,TNT-BTC,ONT-USDT,YEE-ETH,AKRO-BTC,TRX-USDT,OCN-ETH,SRN-BTC,DASH-USDT,XMX-ETH,NANO-BTC,QASH-ETH,EOS-HT,GT-BTC,XTZ-USDT,ARPA-USDT,SALT-ETH,BKBT-ETH,MTX-BTC,SMT-USDT,GXC-BTC,VIDY-BTC,FTT-HT,LAMB-ETH,TRX-BTC,TRIO-ETH,BFT-ETH,LINK-BTC,AE-ETH,NULS-BTC,BHD-HT,AST-ETH,NEO-USDT,EDU-BTC,CVCOIN-BTC,GVE-BTC,GET-BTC,ZRX-USDT,ELF-ETH,DATX-ETH,ADA-ETH,TOP-HT,NCASH-ETH,QTUM-USDT,ETC-HT,ZIL-USDT,TNB-ETH,BIX-ETH,SHE-BTC,PNT-BTC,BTC-USDT,PORTAL-ETH,WAVES-USDT,XZC-ETH,HT-ETH,POLY-BTC,MCO-ETH,MUSK-ETH,PAI-ETH,LXT-USDT,UTK-BTC,RTE-BTC,NCC-ETH,HB10-USDT,BOX-BTC,RDN-ETH,ARPA-BTC,LBA-ETH,CNN-ETH,AAC-ETH,XTZ-BTC,IDT-BTC,AKRO-USDT,IOST-BTC,GT-USDT,WAN-ETH,ETN-ETH,PVT-USDT,NEO-BTC,WAVES-ETH,ONE-USDT,ZEC-BTC,SKM-HT,IOST-ETH,NPXS-ETH,CVC-ETH,CMT-BTC,COVA-ETH,ARDR-ETH,RDN-BTC,DCR-BTC,REN-BTC,YCC-ETH,MX-HT,NEXO-ETH,XLM-ETH,YCC-BTC,ENG-BTC,CNNS-BTC,ZLA-BTC,QSP-BTC,MAN-ETH,UUU-ETH,ETH-HUSD,RTE-ETH,ATP-HT,BTM-BTC,DAC-BTC,TOS-ETH,LAMB-USDT,DASH-HT,NPXS-BTC,NEW-BTC,FTT-BTC,EOS-HUSD,GRS-BTC,POWR-ETH,VET-USDT,AAC-BTC,MX-BTC,MTN-BTC,XVG-ETH,GNX-ETH,SSP-BTC,WAVES-BTC,EGT-BTC,CTXC-ETH,IDT-ETH,STK-BTC,WICC-BTC,UTK-ETH,CRO-HT,LXT-BTC,GSC-ETH,OMG-BTC,XRP-HT,DGB-BTC,IOST-USDT,CVNT-ETH,GAS-BTC,HIT-BTC,CKB-USDT,ARPA-HT,RUFF-USDT,HC-BTC,WTC-ETH,MDS-USDT,ABT-ETH,ALGO-ETH,BIFI-BTC,KNC-BTC,TT-BTC,LET-BTC,NKN-USDT,PAY-BTC,DTA-USDT,AE-BTC,UC-BTC,VSYS-USDT,USDT-HUSD,EOS-BTC,STEEM-BTC,DOGE-BTC,NODE-HT,MDS-ETH,CRE-BTC,GNT-USDT,UIP-ETH,AST-BTC,XEM-BTC,ZEN-ETH,EDU-ETH,MEX-ETH,EKT-BTC,CVC-USDT,WAXP-ETH,REQ-ETH,OST-BTC,STORJ-USDT,SBTC-BTC,DGD-ETH,SC-ETH,WTC-USDT,THETA-BTC,DTA-ETH,BCV-BTC,SNC-BTC,RSR-HT,KAN-BTC,ELA-BTC,ATOM-BTC,BKBT-BTC,FSN-USDT,EM-USDT,WPR-BTC,TOP-BTC,BTS-BTC,EGCC-ETH,MTL-BTC,GNT-ETH,SEELE-BTC,EVX-BTC,FTI-ETH,BAT-USDT,MT-HT,LOL-HT,ICX-ETH,LOOM-BTC,ZJLT-BTC,XLM-USDT,OGO-BTC,DOCK-BTC,CHAT-ETH,DAT-ETH,ETC-USDT,HPT-BTC,BHT-HT" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + "name": "GateIO", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot", + "option", + "futures", + "cross_margin", + "margin", + "delivery" + ], + "pairs": { + "spot": { + "enabled": "BTC_USDT,IHT_ETH,AME_ETH,CEUR_ETH,ALEPH_USDT,OMG_TRY,BTC_TRY,OGN_USDT,ALA_USDT", + "available": "IHT_ETH,AME_ETH,CEUR_ETH,ALEPH_USDT,OMG_TRY,BTC_TRY,OGN_USDT,ALA_USDT,HC_USDT,BTC_USDT,QNT_USDT,QTUM_ETH,MAHA_ETH,XCN_ETH,POOL_USDT,KGC_USDT,MCO2_USDT,HARD_USDT,GHNY_USDT,FTT_ETH,K21_ETH,FINE_USDT,REP_USDT,SBR_USDT,SKM_ETH,QLC_ETH,GAS_BTC,ALICE3L_USDT,BAO_USDT,FALCONS_USDT,ANT_USDT,VIDYX_USDT,DXCT_ETH,SMTY_ETH,HERO_USDT,SHARE_USDT,FIN_USDT,MTV_USDT,MOO_USDT,SMTY_USDT,ORAO_USDT,AE_ETH,SUSD_USDT,MAN_USDT,UNDEAD_USDT,MC_USDT,VET_USDT,WAXP_ETH,MDA_ETH,LYXE_USDT,SPS_USDT,STX_ETH,WSIENNA_USDT,NAOS_BTC,NFTX_USDT,OPUL_USDT,ICP3L_USDT,SFI_ETH,CTT_USDT,BSV3L_USDT,DFI_USDT,DIS_ETH,FET_USDT,ARG_USDT,VELO_USDT,NSBT_BTC,GSE_ETH,HNS_BTC,DOGEDASH_ETH,BACON_USDT,DUSK_USDT,MAPE_USDT,EGLD_ETH,TDROP_USDT,C983L_USDT,FAN_ETH,CZZ_USDT,FIU_USDT,SWRV_USDT,ONT_ETH,KINE_ETH,IMX_ETH,SPAY_ETH,CFG_BTC,RACA3S_USDT,UNO_ETH,DMLG_USDT,SAKE_ETH,ASM_USDT,CUSD_ETH,SUSD_ETH,ONC_USDT,DAI_USDT,VEGA_ETH,PYM_USDT,LTC_TRY,LOKA_USDT,NIF_USDT,BNC_USDT,PERL_ETH,MATIC3S_USDT,STMX_USDT,SKL_USDT,WLKN_USDT,XYO_ETH,AMPL3S_USDT,WEX_USDT,ULU_ETH,LIKE_ETH,INSUR_ETH,CAKE_ETH,SXP_ETH,COTI_USDT,ORT_USDT,RACA3L_USDT,GASDAO_USDT,AVA_USDT,OPA_USDT,ATS_USDT,VEGA_USDT,KILT_USDT,HIT_ETH,BRISE_USDT,SAUBER_USDT,SPS_ETH,FSN_USDT,EOS_ETH,KYL_USDT,REVV_ETH,SVT_ETH,XRP_USDT,DYDX3S_USDT,MANA3S_USDT,ICP_ETH,ALICE3S_USDT,PCX_USDT,LEMO_ETH,MKR_ETH,WOO3S_USDT,CART_ETH,MATIC_USDT,UNI_USD,MOBI_BTC,ICP3S_USDT,BEAM_BTC,CRO3S_USDT,FTT_USDT,IQ_ETH,TAP_USDT,MLT_USDT,RBN_USDT,AMPL3L_USDT,KINT_ETH,HECH_USDT,GAFI_ETH,WOO3L_USDT,TAI_USDT,HERA_USDT,AST_USDT,DHV_ETH,XAVA_USDT,LSS_USDT,SNX3S_USDT,PBR_USDT,XEND_ETH,SHR_ETH,PRQ_USDT,MATIC3L_USDT,WIT_ETH,LPOOL_USDT,PSP_USDT,BXC_USDT,CBK_USDT,REVO_BTC,MANA3L_USDT,ALPINE_USDT,DEGO_USDT,SIN_USDT,OCT_USDT,KZEN_USDT,L3P_USDT,FX_ETH,ONC_ETH,AXS_USD,BORA_USDT,XTZ_ETH,NEO3L_USDT,FROG_USDT,CHAMP_USDT,XNFT_USDT,BCH3S_USDT,FORT_USDT,XLM_TRY,TRX_TRY,CRPT_USDT,ROUTE_USDT,GLM_USDT,SLRS_ETH,TIMECHRONO_USDT,VRA_USDT,ONS_USDT,ZEC3L_USDT,KFT_ETH,TFD_ETH,FRA_USDT,RDN_ETH,BLANK_USDT,IOST3L_USDT,DDD_USDT,DOGE_USD,UNQ_USDT,API33S_USDT,AKRO_ETH,GITCOIN_USDT,THG_USDT,BDX_USDT,LTO_ETH,FLY_USDT,CREDIT_USDT,RENA_USDT,ZRX_ETH,CRP_ETH,NBOT_USDT,HT3L_USDT,DORA_ETH,LLT_SNET,ASD_USDT,XMR_USDT,SSV_BTC,FTM_USDT,XELS_USDT,MTL_ETH,ADX_ETH,API33L_USDT,PIG_USDT,RUNE_ETH,QRDO_BTC,THN_USDT,BCUG_USDT,EGG_ETH,GGM_USDT,HOTCROSS_USDT,SKYRIM_USDT,BTG_USDT,POT_USDT,CS_USDT,XVS_USDT,A5T_USDT,GOD_BTC,WAVES_USDT,LSK_BTC,BTT_TRY,YIN_USDT,PEOPLE_USDT,SPELL_ETH,POLC_USDT,BZZ3L_USDT,UNO_USDT,HDV_USDT,CELL_USDT,DAR_ETH,MIR_ETH,FODL_USDT,SRM_ETH,PROS_USDT,ORN_ETH,WAG_USDT,RBC_ETH,VENT_USDT,WND_USDT,AAA_ETH,BSCS_ETH,ZEC3S_USDT,DOS_USDT,HT3S_USDT,LAND_USDT,BCD_BTC,RING_USDT,FIRO_USDT,AUDIO_USDT,KUMA_USDT,SOLO_BTC,CRBN_USDT,MM_ETH,SAKE_USDT,XMARK_USDT,SLP_USDT,F2C_USDT,LUNA_USDT,ONIT_USDT,FTM3L_USDT,POPK_USDT,RFUEL_USDT,NEO3S_USDT,MIR_USDT,ETC_BTC,STETH_ETH,MANA_TRY,ALPACA_ETH,WAXL_USDT,EGS_USDT,DAR_USDT,KSM_USDT,XMARK_ETH,QTUM_USDT,C983S_USDT,INDI_ETH,DOGE3S_USDT,RVN_USDT,NOS_USDT,ALU_ETH,ALD_ETH,LUNC_USDT,ARES_ETH,BZZ3S_USDT,TNC_ETH,ONE_USDT,SENC_ETH,FTM3S_USDT,FLUX_USDT,STORJ_ETH,MTN_ETH,MNW_USDT,BLES_ETH,STG_ETH,LIME_ETH,WAGYU_USDT,XRP_TRY,XOR_ETH,ANGLE_USDT,DOGA_USDT,JFI_USDT,USDG_USDT,GRND_USDT,BOND_ETH,DMTR_USDT,YIN_ETH,ENJ_USDT,GOLDMINER_USDT,WIT_USDT,DOGE3L_USDT,FORM_USDT,LYXE_ETH,MLK_USDT,VR_USDT,DMS_USDT,LRC_TRY,ONX_USDT,ASK_USDT,ISP_ETH,TXT_USDT,IOEN_ETH,NIIFI_USDT,VRX_USDT,DOME_USDT,CTSI_USDT,ORBS_USDT,ZLW_ETH,FIL_USDT,FTI_ETH,CTK_USDT,ASR_USDT,GBPT_BTC,CBK_BTC,MBOX_ETH,RAM_USDT,IRIS_USDT,AME_USDT,KUB_USDT,ENV_USDT,RING_ETH,COTI3S_USDT,JULD_ETH,POLK_ETH,ACH3S_USDT,HYVE_ETH,MIX_ETH,RFT_USDT,ORAO_ETH,IHT_USDT,POLYPAD_USDT,CTRC_USDT,SFUND_USDT,MXC_BTC,DDD_BTC,CHESS_ETH,SHIB_USDT,SN_USDT,NFT_USDT,ASTRO_ETH,SOLO_USDT,TSHP_USDT,AMP_USDT,BTCST_ETH,VLXPAD_USDT,GAN_USDT,O3_USDT,WBTC_TRY,TULIP_USDT,GS_ETH,DX_ETH,NYZO_ETH,TT_USDT,SHILL_USDT,RATING_ETH,DUST_USDT,PSB_USDT,BFT1_USDT,GALA_ETH,XDC_USDT,LON3L_USDT,HE_USDT,ICE_ETH,LINK_ETH,SKU_USDT,QLC_USDT,DOMI_USDT,IDEA_USDT,METO_USDT,LIFE_ETH,ACH3L_USDT,POWR_ETH,VET_ETH,ALGO_USDT,BLIN_USDT,BAO_ETH,RBLS_USDT,TORN_ETH,VRT_USDT,BLANKV2_ETH,AUCTION_ETH,OLE_USDT,NWC_BTC,DOT5S_USDT,M RCH_ETH,SUNNY_ETH,GST_USDT,ENJ_TRY,KIBA_USDT,KLAP_USDT,SNTR_ETH,CELR_ETH,CHESS_USDT,XLM3L_USDT,LIQ_USDT,TRU_ETH,CHZ_USD,EPK_USDT,MED_ETH,BSCPAD_ETH,ZCN_USDT,AIOZ_ETH,FOR_ETH,CVC3L_USDT,MNY_USDT,SALT_USDT,CSTR_USDT,MPL_USDT,PLY_ETH,FIS_USDT,CHO_USDT,BICO_ETH,STOX_ETH,HIGH_USDT,SDAO_BTC,STEP_USD,CRV_BTC,SCRT_ETH,ROSE_USDT,SKILL_ETH,FRAX_USDT,BAGS_USDT,WIKEN_BTC,WOO_USDT,BBANK_ETH,SNX3L_USDT,XRD_ETH,VTHO_USDT,OKB3L_USDT,SAFEMOON_USDT,RAD_ETH,IOI_USDT,LAMB_USDT,CHZ_USDT,FAR_ETH,OKB3S_USDT,ELU_USDT,JGN_ETH,EOS3S_USDT,DBC_USDT,ATOM_USDT,ACH_ETH,LBLOCK_USDT,WZRD_USDT,OST_ETH,MEAN_USDT,IDEX_USDT,HOT_TRY,EWT_ETH,EMON_USDT,FXS_USDT,PSY_ETH,SIDUS_USDT,ATA_USDT,CVC3S_USDT,LOOKS_ETH,ALPA_ETH,CGG_ETH,CIR_ETH,PRT_ETH,LON3S_USDT,INJ_USDT,FIRE_ETH,MAHA_USDT,IOST3S_USDT,NU_ETH,LEO_BTC,VOXEL_USDT,CRV_USDT,EQX_USDT,WHALE_USDT,INJ_ETH,GRAP_USDT,AVAX3S_USDT,TIFI_USDT,C98_USDT,ERN_ETH,SUSHI_ETH,VET3S_USDT,KPAD_USDT,CRPT_ETH,CRO_USDT,AZY_USDT,LEMD_USDT,ETH2_ETH,BASE_ETH,TT_ETH,PERL_USDT,BANK_ETH,LST_ETH,PYR_ETH,RATIO_USDT,UMB_USDT,M ETALDR_USDT,SWINGBY_ETH,WICC_ETH,NUM_USDT,SHOE_USDT,BORING_ETH,SDN_USDT,GXS_BTC,ALICE_ETH,BRKL_USDT,GF_ETH,ELEC_USDT,SFG_USDT,COFIX_USDT,TIPS_ETH,FIL_BTC,CWAR_USDT,WILD_USDT,RENBTC_USDT,BNX_USDT,TRU_USDT,SWEAT_USDT,IOST_BTC,NVIR_USDT,1EARTH_USDT,ADAPAD_USDT,PPS_USDT,CUBE_USDT,DLC_USDT,DAFI_ETH,UNISTAKE_ETH,NFTL_USDT,ATOM_TRY,SHIB3S_USDT,BNB_USD,CNAME_USDT,GTH_ETH,ZCX_USDT,DYDX3L_USDT,ASTRO_USDT,GLQ_USDT,PROPS_USDT,AART_USDT,BTRST_ETH,KFT_USDT,AERGO_USDT,RUFF_ETH,EOS3L_USDT,API3_USDT,MINA_BTC,ETHA_ETH,AXIS_ETH,LOON_USDT,AVAX3L_USDT,VET3L_USDT,AE_USDT,SHX_USDT,LYM_USDT,DCR_BTC,LBK_USDT,QTC_USDT,LAVA_USDT,XCN_USDT,BRT_USDT,RSV_USDT,KIF_USDT,PSL_USDT,AZERO_USDT,LUNA_ETH,MILO_USDT,OGN_ETH,TOTM_USDT,BYN_ETH,MINA_USDT,PUNDIX_ETH,SRT_USDT,DG_ETH,IHC_USDT,SYS_ETH,TITA_USDT,COTI3L_USDT,DAG_USDT,DOT5L_USDT,TRADE_USDT,SHPING_USDT,NU_USDT,BLANK_ETH,PCNT_ETH,SCCP_USDT,POLS_USDT,NPT_USDT,MTA_USDT,YIELD_USDT,ZCN_ETH,DVP_ETH,KART_USDT,SYLO_USDT,MCRT_USDT,SPFC_USDT,BASE_USDT,ICX_USDT,PET_USDT,GZONE_USDT,RED_ETH,SBTC_USDT,BATH_ ETH,SOL_USD,NAFT_USDT,GMX_USDT,VADER_USDT,GTC_USDT,CVP_ETH,XRPBEAR_USDT,TIME_USDT,SXP_USDT,CITY_USDT,QASH_USDT,FAST_USDT,BCD_USDT,KNIGHT_USDT,BOO_ETH,ZODI_USDT,REI_USDT,PBX_ETH,SRM_USDT,LDO_ETH,ZEC_USDT,UFT_USDT,DAG_BTC,RIDE_USDT,ERN_USDT,T_USDT,CEEK_USDT,STI_USDT,IMX3S_USDT,ELA_USDT,MNGO_ETH,EHASH_ETH,BADGER_ETH,SUPE_USDT,AR3L_USDT,AUDIO_ETH,DOCK_ETH,QSP_USDT,FLM_USDT,AAVE3S_USDT,BOND_USDT,HT_USD,TARA_USDT,TRX_USDT,SPO_USDT,DSLA_USDT,LTC_BTC,DOGE_USDT,SLIM_ETH,ALN_ETH,CFX3S_USDT,FXS_ETH,RARE_ETH,VLXPAD_ETH,ETH_USD,SDN_BTC,QUICK_USDT,UTK_USDT,XPNET_USDT,TRB_USDT,LAZIO_USDT,FTM_TRY,ALPHA_ETH,CVC_ETH,WSG_USDT,UNI_ETH,DASH3L_USDT,BTL_USDT,CPOOL_USDT,MCG_USDT,SFP_ETH,REALM_USDT,RUFF_BTC,MOB_ETH,IBFK_USDT,ALPHA3S_USDT,BLOK_USDT,WIKEN_USDT,OMG3S_USDT,UTK_ETH,BCH5S_USDT,MED_USDT,REN_USD,MAN_ETH,SLND_ETH,CGG_USDT,CRE_USDT,SOURCE_USDT,ABT_USDT,DPET_USDT,WOM_USDT,FOREX_ETH,SNFT1_USDT,RIF_USDT,BENQI_USDT,XCV_ETH,GTC_BTC,ADA_TRY,LAT_USDT,ITGR_USDT,DLTA_USDT,SMT_USDT,APYS_USDT,MFT_ETH,ABT_ETH,STOX_USDT,ZRX_BTC,GMAT_USDT,R OOM_ETH,STORJ_BTC,RAZOR_USDT,RAGE_USDT,DOCK_USDT,RDN_USDT,MTR_USDT,NKN_USDT,SWASH_USDT,FX_USDT,POR_USDT,DENT_ETH,DERI_USDT,DFND_USDT,BLES_USDT,SLND_USDT,WNXM_ETH,CRTS_USDT,BTC3S_USDT,BKC_USDT,STEPG_ETH,THETA3L_USDT,NBS_BTC,AVAX_ETH,NANO_BTC,DEFILAND_ETH,LOOKS_USDT,BCX_BTC,BCH_USD,ETH3L_USDT,QLC_BTC,BCUG_ETH,RDF_USDT,DOGEDASH_USDT,ARSW_USDT,NEAR_ETH,QTCON_USDT,BABI_USDT,MBX_USDT,PNL_USDT,ODDZ_ETH,ATOM_BTC,XRP_BTC,BTCBULL_USDT,HMT_USDT,PORTO_USDT,STND_USDT,ETHW_ETH,LPT_USDT,LTC3L_USDT,TOKAU_USDT,QI_ETH,TVK_USDT,CWS_USDT,SWOP_USDT,WBTC_USDT,INSTAR_ETH,ICX_ETH,GALA5L_USDT,XTZ_BTC,AGS_USDT,TARA_BTC,DYDX_ETH,CATGIRL_USDT,SASHIMI_ETH,EPX_ETH,GCOIN_USDT,GDAO_USDT,MARS_ETH,OMG_USD,PMON_USDT,MNGO_USDT,TVK_ETH,SLG_USDT,MSOL_USDT,POWR_USDT,UOS_USDT,USDD_USDT,SLICE_USDT,ARRR_ETH,NSBT_USDT,STR_ETH,BEAM3L_USDT,BEL_USDT,MM_USDT,AXS_ETH,WEST_ETH,FTT3L_USDT,OMI_USDT,TIPS_USDT,SLC_ETH,SQUID_USDT,FEI_USDT,GEM_USDT,UMEE_USDT,DOGE_TRY,FCD_USDT,PVU_USDT,XED_ETH,LRN_ETH,NRFB_USDT,LION_USDT,BLACK_USDT,DOGE5S_USDT,CUDOS_USDT,PCNT_USDT ,OVR_USDT,ETC3S_USDT,CHR_ETH,MER_USDT,BOBA_USDT,FUEL_USDT,BAC_USDT,ONE3S_USDT,CONV_ETH,CDT_BTC,CELL_ETH,ASM_ETH,OPIUM_USDT,JST3L_USDT,BONDLY_USDT,RAZE_USDT,LIME_BTC,NFTX_ETH,PNK_ETH,LDO_USDT,DKS_USDT,ORO_USDT,LITH_USDT,ALPHR_ETH,INK_BTC,RLY_USDT,NEAR3S_USDT,XLM3S_USDT,AR_USDT,AKT_USDT,HCT_USDT,REEF_ETH,BZZ_USDT,SRM3L_USDT,AQDC_USDT,OPIUM_ETH,BAT_TRY,EWT_USDT,ALCX_ETH,CORN_USDT,HYDRA_USDT,RUNE_USD,STEP_USDT,CKB_BTC,MATTER_USDT,STSOL_ETH,CEEK_ETH,FXF_ETH,LIKE_USDT,HIT_USDT,LEO_USDT,COMP_USDT,BAL_USDT,LMR_USDT,AQT_USDT,BUY_ETH,LINK3S_USDT,ROOK_ETH,IMX_USDT,EFI_USDT,TAUR_USDT,OKT_ETH,GALO_USDT,MOOV_USDT,RUNE_USDT,TCP_USDT,ITEM_USDT,SCLP_USDT,RBC_USDT,SPI_USDT,ETC_USDT,RENBTC_BTC,CHICKS_USDT,KNOT_USDT,XEC3L_USDT,XCV_USDT,ETC_ETH,AAVE_TRY,APT_USDT,GNX_ETH,KISHU_USDT,AE_BTC,LIEN_USDT,CREAM_USDT,ATOM3S_USDT,OP_ETH,FORTH_ETH,PYR_USDT,KTN_ETH,TKO_ETH,METAG_USDT,ACE_USDT,CIR_USDT,BEAM_ETH,TCP_ETH,SRM_USD,CEL_USD,TRIBE3S_USDT,MESA_ETH,EVA_USDT,BBANK_USDT,BLANKV2_USDT,FORM_ETH,BAL3S_USDT,VISR_ETH,REVO_ETH,ALTB_USDT,KNC_US DT,GAS_USDT,SAFEMARS_USDT,TIP_USDT,VADER_ETH,NWC_USDT,VALUE_USDT,ATA_ETH,SSX_USDT,JOE_USDT,BAS_ETH,FITFI3S_USDT,BIT_USDT,QNT_ETH,RFOX_ETH,MSU_USDT,MSOL_ETH,CRV3L_USDT,OXT_USDT,SHFT_USDT,VERA_ETH,LYM_ETH,BP_USDT,KBOX_USDT,DOGNFT_ETH,PERP_USDT,VELO_ETH,SAO_USDT,DUCK2_USDT,DEFILAND_USDT,DUCK2_ETH,GLMR3L_USDT,SERO_ETH,MTS_USDT,STX_USDT,KEX_ETH,ZIG_USDT,CARDS_USDT,ANML_USDT,GALA_USDT,RAY3S_USDT,KAVA3L_USDT,GARD_USDT,GRT3L_USDT,BFC_USDT,NIFT_USDT,ORION_USDT,CTX_USDT,ASW_USDT,CERE_USDT,COMBO_ETH,MKR_USDT,MASK_USDT,MGA_USDT,AVAX_USDT,SKL3L_USDT,FRR_USDT,MV_USDT,BMI_ETH,SFIL_USDT,TEER_USDT,KLV_USDT,DMS_ETH,LBL_USDT,MKR3L_USDT,LEDU_BTC,XLM_BTC,MIST_ETH,OIN_USDT,CAKE_USDT,RNDR_USDT,STEPG_USDT,YCT_USDT,OPS_ETH,SHR_USDT,OXY_ETH" + }, + "option": { + "enabled": "BTC_USDT-20230217-28000-P,BTC_USDT-20221028-34000-P,BTC_USDT-20221028-40000-C", + "available": "BTC_USDT-20221028-26000-C,BTC_USDT-20221028-34000-P,BTC_USDT-20221028-40000-C,BTC_USDT-20221028-28000-P,BTC_USDT-20221028-34000-C,BTC_USDT-20221028-28000-C,BTC_USDT-20221028-36000-P,BTC_USDT-20221028-50000-P,BTC_USDT-20221028-36000-C,BTC_USDT-20221028-50000-C,BTC_USDT-20221028-21000-P,BTC_USDT-20221028-38000-P,BTC_USDT-20221028-21000-C,BTC_USDT-20221028-38000-C,BTC_USDT-20221028-23000-P,BTC_USDT-20221028-17000-P,BTC_USDT-20221028-23000-C,BTC_USDT-20221028-17000-C,BTC_USDT-20221028-25000-P,BTC_USDT-20221028-19000-P,BTC_USDT-20221028-25000-C,BTC_USDT-20221028-10000-P,BTC_USDT-20221028-19000-C,BTC_USDT-20221028-27000-P,BTC_USDT-20221028-10000-C,BTC_USDT-20221028-27000-C,BTC_USDT-20221028-12000-P,BTC_USDT-20221028-12000-C,BTC_USDT-20221028-20000-P,BTC_USDT-20221028-5000-P,BTC_USDT-20221028-14000-P,BTC_USDT-20221028-20000-C,BTC_USDT-20221028-45000-P,BTC_USDT-20221028-5000-C,BTC_USDT-20221028-14000-C,BTC_USDT-20221028-22000-P,BTC_USDT-20221028-45000-C,BTC_USDT-20221028-16000-P,BTC_USDT-20221028-22000-C,BTC_USDT-20221028-30000-P,BTC_USDT-20221028-16000-C,BTC_USDT-20221028-24000-P,BTC_USDT-20221028-30000-C,BTC_USDT-20221028-18000-P,BTC_USDT-20221028-24000-C,BTC_USDT-20221028-32000-P,BTC_USDT-20221028-18000-C,BTC_USDT-20221028-26000-P,BTC_USDT-20221028-32000-C,BTC_USDT-20221028-40000-P" + }, + "futures": { + "enabled": "ETH_USD,BTC_USD,KNC_USDT,OOKI_USDT,BIT_USDT,ZEC_USDT,SC_USDT,RVN_USDT,ICX_USDT", + "available": "ETH_USD,BTC_USD,KNC_USDT,OOKI_USDT,BIT_USDT,ZEC_USDT,SC_USDT,RVN_USDT,ICX_USDT,DUSK_USDT,BEL_USDT,REEF_USDT,ALCX_USDT,ASTR_USDT,INJ_USDT,CAKE_USDT,LAZIO_USDT,ONE_USDT,CEL_USDT,ETH_USDT,KLAY_USDT,COTI_USDT,MKISHU_USDT,MANA_USDT,MOVR_USDT,OMG_USDT,UNI_USDT,LTC_USDT,AAVE_USDT,DENT_USDT,QRDO_USDT,BNB_USDT,ALPHA_USDT,RAY_USDT,APE_USDT,CERE_USDT,STMX_USDT,XCN_USDT,OGN_USDT,OKB_USDT,DOT_USDT,TLM_USDT,BTM_USDT,ADA_USDT,ANKR_USDT,ANT_USDT,TRX_USDT,MTL_USDT,YFII_USDT,SUN_USDT,SAND_USDT,MBABYDOGE_USDT,WIN_USDT,LUNC_USDT,SRM_USDT,STG_USDT,BAT_USDT,AXS_USDT,SOL_USDT,MAKITA_USDT,BNT_USDT,BLZ_USDT,PSG_USDT,IOTA_USDT,BONK_USDT,RSR_USDT,PYR_USDT,FITFI_USDT,MKR_USDT,PERP_USDT,COMP_USDT,LINK_USDT,CHR_USDT,CFX_USDT,GARI_USDT,DGB_USDT,MBOX_USDT,WEMIX_USDT,DYDX_USDT,LUNA_USDT,HT_USDT,TRB_USDT,CTK_USDT,ACA_USDT,TFUEL_USDT,OCEAN_USDT,XLM_USDT,HOT_USDT,FTM_USDT,LPT_USDT,SOS_USDT,ALGO_USDT,SHIB_USDT,BSV_USDT,PORTO_USDT,SFP_USDT,SANTOS_USDT,BADGER_USDT,DAR_USDT,DEFI_USDT,XEM_USDT,ALICE_USDT,ICP_USDT,RARE_USDT,LRC_USDT,BAKE_USDT,FLUX_USDT,CRO_USDT,CVC_USDT,MINA_USDT,LIT_USDT,AUDIO_USDT,ZIL_USDT,XMR_USDT,FRONT_USDT,CTSI_USDT,AGLD_USDT,YGG_USDT,OP_USDT,ZRX_USDT,GT_USDT,XCH_USDT,VET_USDT,MOB_USDT,BICO_USDT,SLP_USDT,ACH_USDT,AR_USDT,CLV_USDT,IMX_USDT,SPELL_USDT,UNFI_USDT,SUSHI_USDT,FTT_USDT,HIGH_USDT,HNT_USDT,ALT_USDT,YFI_USDT,NEAR_USDT,NKN_USDT,XVS_USDT,BAND_USDT,LOKA_USDT,BCH_USDT,TOMO_USDT,WAVES_USDT,FIDA_USDT,DIA_USDT,ANC_USDT,CELO_USDT,CRV_USDT,FLM_USDT,GLMR_USDT,FIL_USDT,PEOPLE_USDT,WAXP_USDT,IOTX_USDT,ATOM_USDT,RLC_USDT,HBAR_USDT,REN_USDT,GMT_USDT,KAVA_USDT,KDA_USDT,GALA_USDT,STORJ_USDT,PUNDIX_USDT,BAL_USDT,XAUG_USDT,GRIN_USDT,SXP_USDT,AKRO_USDT,NEXO_USDT,CKB_USDT,API3_USDT,NEST_USDT,ETHW_USDT,TONCOIN_USDT,THETA_USDT,CREAM_USDT,BTC_USDT,GST_USDT,BEAM_USDT,HFT_USDT,KSM_USDT,RAD_USDT,QTUM_USDT,WOO_USDT,ATA_USDT,AVAX_USDT,EOS_USDT,SNX_USDT,AUCTION_USDT,XRP_USDT,GITCOIN_USDT,MATIC_USDT,ONT_USDT,LINA_USDT,DASH_USDT,MASK_USDT,ETC_USDT,JST_USDT,BSW_USDT,CONV_USDT,SKL_USDT,GAL_USDT,DODO_USDT,GRT_USDT,TRU_USDT,STX_USDT,CVX_USDT,JASMY_USDT,HIVE_USDT,EXCH_USDT,ROSE_USDT,SUPER_USDT,SCRT_USDT,USTC_USDT,ENJ_USDT,BTS_USDT,LOOKS_USDT,QNT_USDT,HOOK_USDT,FLOW_USDT,RUNE_USDT,APT_USDT,CHZ_USDT,DOGE_USDT,1INCH_USDT,PRIV_USDT,CSPR_USDT,C98_USDT,RACA_USDT,CELR_USDT,XEC_USDT,ENS_USDT,POND_USDT,NYM_USDT,PROM_USDT,IOST_USDT,ZEN_USDT,LDO_USDT,RNDR_USDT,REQ_USDT,DEGO_USDT,VRA_USDT,QUICK_USDT,VGX_USDT,XTZ_USDT,EGLD_USDT,POLS_USDT,ARPA_USDT,NFT_USDT" + }, + "cross_margin": { + "enabled": "BTC_USDT,ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT", + "available": "ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT,TARA_USDT,TRX_USDT,OXY_USDT,LON_USDT,DOGE_USDT,ISP_USDT,TWT_USDT,BAO_USDT,QUACK_USDT,ANT_USDT,VGX_USDT,ARPA_USDT,QUICK_USDT,UTK_USDT,HERO_USDT,WSG_USDT,BICO_USDT,MTV_USDT,VET_USDT,GARI_USDT,BCH_USDT,KLAY_USDT,WING_USDT,BLOK_USDT,SPS_USDT,WIKEN_USDT,WSIENNA_USDT,PUNDIX_USDT,FIC_USDT,ASTR_USDT,FET_USDT,VELO_USDT,BENQI_USDT,CWEB_USDT,RIF_USDT,UNI_USDT,ONG_USDT,ERG_USDT,ALPHA_USDT,CELO_USDT,XVG_USDT,GMAT_USDT,BTS_USDT,DOCK_USDT,GMT_USDT,DIA_USDT,CSPR_USDT,NKN_USDT,STAKE_USDT,SWASH_USDT,XEC_USDT,SWRV_USDT,QRDO_USDT,BLES_USDT,EOS_USDT,GRT_USDT,ASM_USDT,FIL6_USDT,GNO_USDT,EGLD_USDT,XYM_USDT,LOOKS_USDT,LOKA_USDT,BNC_USDT,BAS_USDT,SKL_USDT,STMX_USDT,CVC_USDT,DDOS_USDT,COTI_USDT,AVA_USDT,HMT_USDT,DF_USDT,LPT_USDT,XRP_USDT,TVK_USDT,FEVR_USDT,MBL_USDT,KIN_USDT,SPELL_USDT,MATIC_USDT,FTT_USDT,NMR_USDT,PMON_USDT,BNB_USDT,USDD_USDT,LSS_USDT,MDX_USDT,PRQ_USDT,ALPINE_USDT,DEGO_USDT,OMI_USDT,TIPS_USDT,OCT_USDT,FEI_USDT,UMEE_USDT,CRP_USDT,LION_USDT,YFI_USDT,DASH_USDT,REQ_USDT,SDAO_USDT,PNT_USDT,INSUR_USDT,OOKI_USDT,SUN_USDT,CRPT_USDT,BAC_USDT,DATA_USDT,LRN_USDT,JGN_USDT,KIMCHI_USDT,SUKU_USDT,VRA_USDT,AAVE_USDT,FTI_USDT,LDO_USDT,FRA_USDT,BLANK_USDT,NEAR_USDT,ZKS_USDT,MTRG_USDT,RLY_USDT,TCT_USDT,FLY_USDT,JST_USDT,YFII_USDT,AR_USDT,POLY_USDT,JULD_USDT,SOL_USDT,BZZ_USDT,AXS_USDT,ASD_USDT,XMR_USDT,FTM_USDT,HIT_USDT,LEO_USDT,LIT_USDT,PIG_USDT,COMP_USDT,ELON_USDT,IMX_USDT,EFI_USDT,XVS_USDT,WAVES_USDT,PEOPLE_USDT,SOS_USDT,RUNE_USDT,POLC_USDT,SCLP_USDT,BABYDOGE_USDT,KONO_USDT,SPI_USDT,ETC_USDT,MDA_USDT,MTL_USDT,BCHA_USDT,KISHU_USDT,SUNNY_USDT,PYR_USDT,XTZ_USDT,TRIBE_USDT,AUDIO_USDT,FIRO_USDT,MANA_USDT,OKB_USDT,DOG_USDT,SLP_USDT,KNC_USDT,GAS_USDT,LUNA_USDT,SAFEMARS_USDT,MIR_USDT,DAR_USDT,EGS_USDT,KSM_USDT,ATP_USDT,BIT_USDT,STORJ_USDT,XEM_USDT,QTUM_USDT,AGLD_USDT,RVN_USDT,OXT_USDT,SHFT_USDT,IOTX_USDT,LUNC_USDT,NEXO_USDT,AKITA_USDT,PERP_USDT,ONE_USDT,ETH_USDT,FLUX_USDT,FLOKI_USDT,STX_USDT,ANML_USDT,XPRT_USDT,GALA_USDT,GXS_USDT,TORN_USDT,KAI_USDT,1INCH_USDT,CHR_USDT,GAL_USDT,GLMR_USDT,CTX_USDT,CERE_USDT,CART_USDT,STRAX_USDT,MASK_USDT,MKR_USDT,AVAX_USDT,ENJ_USDT,YAM_USDT,ALPACA_USDT,DODO_USDT,MFT_USDT,CAKE_USDT,RNDR_USDT,CTSI_USDT,GRIN_USDT,MXC_USDT,ONT_USDT,ANKR_USDT,SLIM_USDT,FIL_USDT,CTK_USDT,ASR_USDT,FEG_USDT,SERO_USDT,RSS3_USDT,IRIS_USDT,XCH_USDT,ZRX_USDT,BAND_USDT,BADGER_USDT,DAO_USDT,EPS_USDT,THETA_USDT,BAKE_USDT,SHIB_USDT,MBOX_USDT,NBS_USDT,SNT_USDT,DREP_USDT,NFT_USDT,AUCTION_USDT,BOSON_USDT,O3_USDT,NULS_USDT,OMG_USDT,PEARL_USDT,HAPI_USDT,STG_USDT,IDV_USDT,HORD_USDT,ZIL_USDT,SUPER_USDT,DENT_USDT,REN_USDT,RAI_USDT,ZEN_USDT,ALGO_USDT,BLZ_USDT,BOR_USDT,SC_USDT,HEGIC_USDT,MOB_USDT,DORA_USDT,FOR_USDT,FLOW_USDT,RARI_USDT,DYDX_USDT,ATLAS_USDT,GST_USDT,REEF_USDT,HT_USDT,XYO_USDT,CHESS_USDT,BAT_USDT,NYM_USDT,RAMP_USDT,USDC_USDT,ICP_USDT,EPK_USDT,EXRD_USDT,DOT_USDT,COOK_USDT,CKB_USDT,YGG_USDT,CRU_USDT,ANC_USDT,FIS_USDT,ALCX_USDT,HIGH_USDT,BEAM_USDT,BSW_USDT,STAR_USDT,ROSE_USDT,CNNS_USDT,BZRX_USDT,WOO_USDT,SAFEMOON_USDT,VTHO_USDT,OM_USDT,LAMB_USDT,CHZ_USDT,AIOZ_USDT,EDEN_USDT,POND_USDT,ATOM_USDT,UNFI_USDT,FORTH_USDT,MLN_USDT,NEO_USDT,MOVR_USDT,RLC_USDT,FXS_USDT,ENS_USDT,ATA_USDT,XPR_USDT,NEST_USDT,XLM_USDT,AUTO_USDT,SNX_USDT,OCN_USDT,RSR_USDT,MITH_USDT,KAR_USDT,INJ_USDT,PLA_USDT,CYS_USDT,WAXP_USDT,VOXEL_USDT,CRV_USDT,FITFI_USDT,WHALE_USDT,WRX_USDT,TIDAL_USDT,C98_USDT,HNT_USDT,TONCOIN_USDT,DOGGY_USDT,SYS_USDT,NPXS_USDT,CRO_USDT,LEMD_USDT,RAY_USDT,PERL_USDT,CQT_USDT,CFX_USDT,TOMO_USDT,ACA_USDT,SDN_USDT,OKT_USDT,WILD_USDT,BNX_USDT,TRU_USDT,RACA_USDT,SWEAT_USDT,ACH_USDT,AKRO_USDT,BTM_USDT,TKO_USDT,GT_USDT,OCEAN_USDT,WNCG_USDT,BSV_USDT,GHST_USDT,CELR_USDT,LINA_USDT,SAND_USDT,APE_USDT,WICC_USDT,FIDA_USDT,ADA_USDT,PROPS_USDT,METIS_USDT,KAVA_USDT,AERGO_USDT,CONV_USDT,TFUEL_USDT,FRONT_USDT,API3_USDT,FARM_USDT,AE_USDT,LRC_USDT,IOTA_USDT,RFOX_USDT,PHA_USDT,XCN_USDT,NAS_USDT,KEEP_USDT,VIDY_USDT,HOT_USDT,MINA_USDT,ETHW_USDT,ALICE_USDT,HAI_USDT,BTC_USDT,LTC_USDT,LTO_USDT,DC_USDT,NU_USDT,IOST_USDT,RAD_USDT,POLS_USDT,OP_USDT,WXT_USDT,STR_USDT,YIELD_USDT,GM_USDT,SPA_USDT,BTCST_USDT,WEMIX_USDT,CLV_USDT,ICX_USDT,PET_USDT,STARL_USDT,HBAR_USDT,REDTOKEN_USDT,BTT_USDT,LINK_USDT,TLM_USDT,ARES_USDT,GTC_USDT,SUSHI_USDT,KEY_USDT,ALN_USDT,KDA_USDT,DVI_USDT,SXP_USDT,MAPS_USDT,BCD_USDT,SRM_USDT,WIN_USDT,ZEC_USDT,JASMY_USDT" + }, + "margin": { + "enabled": "BTC_USDT,ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT", + "available": "BTC_USDT,ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT,TARA_USDT,TRX_USDT,OXY_USDT,LON_USDT,DOGE_USDT,ISP_USDT,TWT_USDT,BAO_USDT,QUACK_USDT,ANT_USDT,VGX_USDT,ARPA_USDT,QUICK_USDT,UTK_USDT,HERO_USDT,WSG_USDT,BICO_USDT,MTV_USDT,VET_USDT,GARI_USDT,BCH_USDT,KLAY_USDT,WING_USDT,BLOK_USDT,SPS_USDT,WIKEN_USDT,WSIENNA_USDT,PUNDIX_USDT,FIC_USDT,ASTR_USDT,FET_USDT,VELO_USDT,BENQI_USDT,CWEB_USDT,RIF_USDT,UNI_USDT,ONG_USDT,ERG_USDT,ALPHA_USDT,CELO_USDT,XVG_USDT,GMAT_USDT,BTS_USDT,DOCK_USDT,GMT_USDT,DIA_USDT,CSPR_USDT,NKN_USDT,STAKE_USDT,SWASH_USDT,XEC_USDT,SWRV_USDT,QRDO_USDT,BLES_USDT,EOS_USDT,GRT_USDT,ASM_USDT,FIL6_USDT,GNO_USDT,EGLD_USDT,XYM_USDT,LOOKS_USDT,LOKA_USDT,BNC_USDT,BAS_USDT,SKL_USDT,STMX_USDT,CVC_USDT,DDOS_USDT,COTI_USDT,AVA_USDT,HMT_USDT,DF_USDT,LPT_USDT,XRP_USDT,TVK_USDT,FEVR_USDT,MBL_USDT,KIN_USDT,SPELL_USDT,MATIC_USDT,FTT_USDT,NMR_USDT,PMON_USDT,BNB_USDT,USDD_USDT,LSS_USDT,MDX_USDT,PRQ_USDT,ALPINE_USDT,DEGO_USDT,OMI_USDT,TIPS_USDT,OCT_USDT,FEI_USDT,UMEE_USDT,CRP_USDT,LION_USDT,YFI_USDT,DASH_USDT,REQ_USDT,SDAO_USDT,PNT_USDT,INSUR_USDT,OOKI_USDT,SUN_USDT,CRPT_USDT,BAC_USDT,DATA_USDT,LRN_USDT,JGN_USDT,KIMCHI_USDT,SUKU_USDT,VRA_USDT,AAVE_USDT,FTI_USDT,LDO_USDT,FRA_USDT,BLANK_USDT,NEAR_USDT,ZKS_USDT,MTRG_USDT,RLY_USDT,TCT_USDT,FLY_USDT,JST_USDT,YFII_USDT,AR_USDT,POLY_USDT,JULD_USDT,SOL_USDT,BZZ_USDT,AXS_USDT,ASD_USDT,XMR_USDT,FTM_USDT,HIT_USDT,LEO_USDT,LIT_USDT,PIG_USDT,COMP_USDT,ELON_USDT,IMX_USDT,EFI_USDT,XVS_USDT,WAVES_USDT,PEOPLE_USDT,SOS_USDT,RUNE_USDT,POLC_USDT,SCLP_USDT,BABYDOGE_USDT,KONO_USDT,SPI_USDT,ETC_USDT,MDA_USDT,MTL_USDT,BCHA_USDT,KISHU_USDT,SUNNY_USDT,PYR_USDT,XTZ_USDT,TRIBE_USDT,AUDIO_USDT,FIRO_USDT,MANA_USDT,OKB_USDT,DOG_USDT,SLP_USDT,KNC_USDT,GAS_USDT,LUNA_USDT,SAFEMARS_USDT,MIR_USDT,DAR_USDT,EGS_USDT,KSM_USDT,ATP_USDT,BIT_USDT,STORJ_USDT,XEM_USDT,QTUM_USDT,AGLD_USDT,RVN_USDT,OXT_USDT,SHFT_USDT,IOTX_USDT,LUNC_USDT,NEXO_USDT,AKITA_USDT,PERP_USDT,ONE_USDT,ETH_USDT,FLUX_USDT,FLOKI_USDT,STX_USDT,ANML_USDT,XPRT_USDT,GALA_USDT,GXS_USDT,TORN_USDT,KAI_USDT,1INCH_USDT,CHR_USDT,GAL_USDT,GLMR_USDT,CTX_USDT,CERE_USDT,CART_USDT,STRAX_USDT,MASK_USDT,MKR_USDT,AVAX_USDT,ENJ_USDT,YAM_USDT,ALPACA_USDT,DODO_USDT,MFT_USDT,CAKE_USDT,RNDR_USDT,CTSI_USDT,GRIN_USDT,MXC_USDT,ONT_USDT,ANKR_USDT,SLIM_USDT,FIL_USDT,CTK_USDT,ASR_USDT,FEG_USDT,SERO_USDT,RSS3_USDT,IRIS_USDT,XCH_USDT,ZRX_USDT,BAND_USDT,BADGER_USDT,DAO_USDT,EPS_USDT,THETA_USDT,BAKE_USDT,SHIB_USDT,MBOX_USDT,NBS_USDT,SNT_USDT,DREP_USDT,NFT_USDT,AUCTION_USDT,BOSON_USDT,O3_USDT,NULS_USDT,OMG_USDT,PEARL_USDT,HAPI_USDT,STG_USDT,IDV_USDT,HORD_USDT,ZIL_USDT,SUPER_USDT,DENT_USDT,REN_USDT,RAI_USDT,ZEN_USDT,ALGO_USDT,BLZ_USDT,BOR_USDT,SC_USDT,HEGIC_USDT,MOB_USDT,DORA_USDT,FOR_USDT,FLOW_USDT,RARI_USDT,DYDX_USDT,ATLAS_USDT,GST_USDT,REEF_USDT,HT_USDT,XYO_USDT,CHESS_USDT,BAT_USDT,NYM_USDT,RAMP_USDT,USDC_USDT,ICP_USDT,EPK_USDT,EXRD_USDT,DOT_USDT,COOK_USDT,CKB_USDT,YGG_USDT,CRU_USDT,ANC_USDT,FIS_USDT,ALCX_USDT,HIGH_USDT,BEAM_USDT,BSW_USDT,STAR_USDT,ROSE_USDT,CNNS_USDT,BZRX_USDT,WOO_USDT,SAFEMOON_USDT,VTHO_USDT,OM_USDT,LAMB_USDT,CHZ_USDT,AIOZ_USDT,EDEN_USDT,POND_USDT,ATOM_USDT,UNFI_USDT,FORTH_USDT,MLN_USDT,NEO_USDT,MOVR_USDT,RLC_USDT,FXS_USDT,ENS_USDT,ATA_USDT,XPR_USDT,NEST_USDT,XLM_USDT,AUTO_USDT,SNX_USDT,OCN_USDT,RSR_USDT,MITH_USDT,KAR_USDT,INJ_USDT,PLA_USDT,CYS_USDT,WAXP_USDT,VOXEL_USDT,CRV_USDT,FITFI_USDT,WHALE_USDT,WRX_USDT,TIDAL_USDT,C98_USDT,HNT_USDT,TONCOIN_USDT,DOGGY_USDT,SYS_USDT,NPXS_USDT,CRO_USDT,LEMD_USDT,RAY_USDT,PERL_USDT,CQT_USDT,CFX_USDT,TOMO_USDT,ACA_USDT,SDN_USDT,OKT_USDT,WILD_USDT,BNX_USDT,TRU_USDT,RACA_USDT,SWEAT_USDT,ACH_USDT,AKRO_USDT,BTM_USDT,TKO_USDT,GT_USDT,OCEAN_USDT,WNCG_USDT,BSV_USDT,GHST_USDT,CELR_USDT,LINA_USDT,SAND_USDT,APE_USDT,WICC_USDT,FIDA_USDT,ADA_USDT,PROPS_USDT,METIS_USDT,KAVA_USDT,AERGO_USDT,CONV_USDT,TFUEL_USDT,FRONT_USDT,API3_USDT,FARM_USDT,AE_USDT,LRC_USDT,IOTA_USDT,RFOX_USDT,PHA_USDT,XCN_USDT,NAS_USDT,KEEP_USDT,VIDY_USDT,HOT_USDT,MINA_USDT,ETHW_USDT,ALICE_USDT,HAI_USDT,LTC_USDT,LTO_USDT,DC_USDT,NU_USDT,IOST_USDT,RAD_USDT,POLS_USDT,OP_USDT,WXT_USDT,STR_USDT,YIELD_USDT,GM_USDT,SPA_USDT,BTCST_USDT,WEMIX_USDT,CLV_USDT,ICX_USDT,PET_USDT,STARL_USDT,HBAR_USDT,REDTOKEN_USDT,BTT_USDT,LINK_USDT,TLM_USDT,ARES_USDT,GTC_USDT,SUSHI_USDT,KEY_USDT,ALN_USDT,KDA_USDT,DVI_USDT,SXP_USDT,MAPS_USDT,BCD_USDT,SRM_USDT,WIN_USDT,ZEC_USDT,JASMY_USDT" + }, + "delivery": { + "enabled": "BTC_USD_20230331,BTC_USD_20221230,BTC_USDT_20221021,BTC_USDT_20221014", + "available": "BTC_USD_20221021,BTC_USD_20221014,BTC_USD_20230331,BTC_USD_20221230,BTC_USDT_20221021,BTC_USDT_20221014,BTC_USDT_20230331,BTC_USDT_20221230" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "ITBIT", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD,SGD", - "currencyPairs": { - "requestFormat": { - "uppercase": true - }, - "configFormat": { - "uppercase": true - }, - "useGlobalFormat": true, - "lastUpdated": 1591062026, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "XBTUSD,XBTSGD", - "available": "XBTUSD,XBTSGD" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret", - "clientID": "ClientID" - }, - "credentialsValidator": { - "requiresSecret": true, - "requiresClientID": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": {}, - "websocketAPI": false, - "websocketCapabilities": {} + "name": "Gemini", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTCUSD", + "available": "BTCUSD,ETHBTC,ETHUSD,BCHUSD,BCHBTC,BCHETH,LTCUSD,LTCBTC,LTCETH,LTCBCH,ZECUSD,ZECBTC,ZECETH,ZECBCH,ZECLTC" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": false, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "Kraken", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "EUR,USD,CAD,GBP,JPY", - "currencyPairs": { - "requestFormat": { - "uppercase": true, - "separator": "," + "name": "HitBTC", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USD", + "available": "BCN-BTC,BTC-USD,DASH-BTC,DOGE-BTC,DOGE-USD,EMC-BTC,ETH-BTC,LSK-BTC,LTC-BTC,LTC-USD,NXT-BTC,SBD-BTC,SC-BTC,STEEM-BTC,XDN-BTC,XEM-BTC,XMR-BTC,ARDR-BTC,ZEC-BTC,WAVES-BTC,MAID-BTC,DGD-BTC,SNGLS-BTC,1ST-BTC,TRST-BTC,TIME-BTC,GNO-BTC,REP-BTC,XMR-USD,DASH-USD,ETH-USD,NXT-USD,ZRC-BTC,BOS-BTC,DCT-BTC,ANT-BTC,AEON-BTC,GUP-BTC,PLU-BTC,LUN-BTC,EDG-BTC,RLC-BTC,SWT-BTC,TKN-BTC,WINGS-BTC,XAUR-BTC,AE-BTC,PTOY-BTC,ZEC-USD,XEM-USD,BCN-USD,XDN-USD,MAID-USD,ETC-BTC,ETC-USD,PLBT-BTC,BNT-BTC,SNT-ETH,CVC-USD,PAY-ETH,OAX-ETH,OMG-ETH,BQX-ETH,XTZ-BTC,DICE-BTC,PTOY-ETH,1ST-ETH,XAUR-ETH,TIME-ETH,DICE-ETH,SWT-ETH,XMR-ETH,ETC-ETH,DASH-ETH,ZEC-ETH,PLU-ETH,GNO-ETH,XRP-BTC,STRAT-USD,STRAT-BTC,SNC-ETH,ADX-ETH,BET-ETH,EOS-ETH,DENT-ETH,SAN-ETH,EOS-BTC,EOS-USD,XTZ-ETH,XTZ-USD,MYB-ETH,SUR-ETH,IXT-ETH,PLR-ETH,TIX-ETH,PRO-ETH,AVT-ETH,EVX-USD,DLT-BTC,BNT-ETH,BNT-USD,MANA-USD,DNT-BTC,FYP-BTC,OPT-BTC,TNT-ETH,STX-BTC,STX-ETH,STX-USD,TNT-USD,TNT-BTC,ENG-ETH,XUC-USD,SNC-BTC,SNC-USD,OAX-USD,OAX-BTC,ZRX-BTC,ZRX-ETH,ZRX-USD,RVT-BTC,PPC-BTC,PPC-USD,QTUM-ETH,IGNIS-ETH,BMC-BTC,BMC-ETH,BMC-USD,CND-BTC,CND-ETH,CND-USD,CDT-ETH,CDT-USD,FUN-BTC,FUN-ETH,FUN-USD,HVN-BTC,HVN-ETH,POE-BTC,POE-ETH,AMB-USD,AMB-ETH,AMB-BTC,HPC-BTC,PPT-ETH,MTH-BTC,MTH-ETH,LRC-BTC,LRC-ETH,ICX-BTC,ICX-ETH,NEO-BTC,NEO-ETH,NEO-USD,CSNO-BTC,ICX-USD,IND-ETH,KICK-BTC,YOYOW-BTC,CDT-BTC,XVG-BTC,XVG-ETH,XVG-USD,DGB-BTC,DGB-ETH,DGB-USD,DCN-ETH,DCN-USD,VIBE-BTC,ENJ-BTC,ENJ-ETH,ENJ-USD,ZSC-BTC,ZSC-ETH,ZSC-USD,TRX-BTC,TRX-ETH,TRX-USD,ART-BTC,EVX-BTC,EVX-ETH,SUB-BTC,SUB-ETH,SUB-USD,WTC-BTC,BTM-BTC,BTM-ETH,BTM-USD,LIFE-BTC,VIB-BTC,VIB-ETH,VIB-USD,DRT-ETH,STU-USD,OMG-BTC,PAY-BTC,PPT-BTC,SNT-BTC,BTG-BTC,BTG-ETH,BTG-USD,SMART-BTC,SMART-ETH,SMART-USD,XUC-ETH,XUC-BTC,LA-ETH,EDO-BTC,EDO-ETH,EDO-USD,HGT-ETH,IXT-BTC,SCL-BTC,ETP-BTC,ETP-ETH,ETP-USD,NEBL-BTC,NEBL-ETH,ARN-BTC,ARN-ETH,STU-BTC,STU-ETH,GVT-ETH,BTX-BTC,LTC-ETH,BCN-ETH,MAID-ETH,NXT-ETH,STRAT-ETH,XDN-ETH,XEM-ETH,PLR-BTC,SUR-BTC,BQX-BTC,DOGE-ETH,AMM-BTC,AMM-ETH,AMM-USD,DBIX-BTC,PRE-BTC,ZAP-BTC,DOV-BTC,DOV-ETH,XRP-ETH,XRP-USD,HSR-BTC,LEND-BTC,LEND-ETH,SPF-ETH,SBTC-BTC,SBTC-ETH,LOC-BTC,LOC-ETH,LOC-USD,SWFTC-BTC,SWFTC-ETH,SWFTC-USD,STAR-ETH,SBTC-USD,STORM-BTC,DIM-ETH,DIM-USD,DIM-BTC,NGC-BTC,NGC-ETH,NGC-USD,EMC-ETH,EMC-USD,MCO-BTC,MCO-ETH,MCO-USD,MANA-ETH,MANA-BTC,CPAY-ETH,DATA-BTC,DATA-ETH,DATA-USD,UTT-BTC,UTT-ETH,UTT-USD,KMD-BTC,KMD-ETH,KMD-USD,QTUM-USD,QTUM-BTC,SNT-USD,OMG-USD,EKO-BTC,EKO-ETH,ADX-BTC,ADX-USD,LSK-ETH,LSK-USD,PLR-USD,SUR-USD,BQX-USD,DRT-USD,REP-ETH,REP-USD,WAXP-BTC,WAXP-ETH,WAXP-USD,C20-BTC,C20-ETH,IDH-BTC,IDH-ETH,IPL-BTC,COV-BTC,COV-ETH,SENT-BTC,SENT-ETH,SENT-USD,SMT-BTC,SMT-ETH,SMT-USD,CHAT-BTC,CHAT-ETH,CHAT-USD,TRAC-ETH,JNT-ETH,UTK-BTC,UTK-ETH,UTK-USD,GNX-ETH,CHSB-BTC,CHSB-ETH,DAY-BTC,DAY-ETH,DAY-USD,NEU-BTC,NEU-ETH,NEU-USD,TAU-BTC,FLP-BTC,FLP-ETH,FLP-USD,R-BTC,R-ETH,EKO-USD,BCPT-ETH,BCPT-USD,PKT-BTC,PKT-ETH,BETR-BTC,BETR-ETH,HAND-ETH,HAND-USD,CHP-ETH,BCPT-BTC,ACT-BTC,ACT-ETH,ACT-USD,ADA-BTC,ADA-ETH,ADA-USD,SIG-BTC,MTX-BTC,MTX-ETH,MTX-USD,WIZ-BTC,WIZ-ETH,WIZ-USD,DADI-BTC,DADI-ETH,BDG-ETH,DATX-BTC,DATX-ETH,TRUE-BTC,DRG-BTC,DRG-ETH,BANCA-BTC,BANCA-ETH,ZAP-ETH,ZAP-USD,AUTO-BTC,SOC-BTC,OCN-BTC,OCN-ETH,STQ-BTC,STQ-ETH,XLM-BTC,XLM-ETH,XLM-USD,IOTA-BTC,IOTA-ETH,IOTA-USD,DRT-BTC,BETR-USD,ERT-BTC,CRPT-BTC,CRPT-USD,MESH-BTC,MESH-ETH,MESH-USD,IHT-BTC,IHT-ETH,IHT-USD,SCC-BTC,YCC-BTC,DAN-BTC,TEL-BTC,TEL-ETH,NCT-BTC,NCT-ETH,NCT-USD,BMH-BTC,BANCA-USD,BERRY-BTC,BERRY-ETH,BERRY-USD,GBX-BTC,GBX-ETH,GBX-USD,SHIP-BTC,SHIP-ETH,NANO-BTC,NANO-ETH,NANO-USD,LNC-BTC,KIN-ETH,ARDR-USD,FOTA-ETH,FOTA-BTC,CVT-BTC,CVT-ETH,CVT-USD,STQ-USD,GNT-BTC,GNT-ETH,GNT-USD,GET-BTC,MITH-BTC,MITH-ETH,MITH-USD,DADI-USD,TKY-BTC,ACAT-BTC,ACAT-ETH,ACAT-USD,BTX-USD,WIKI-BTC,WIKI-ETH,WIKI-USD,ONT-BTC,ONT-ETH,ONT-USD,FTX-BTC,FTX-ETH,NAVI-BTC,VME-ETH,NAVI-ETH,LND-ETH,CSM-BTC,NANJ-BTC,NTK-BTC,NTK-ETH,NTK-USD,AUC-BTC,AUC-ETH,CMCT-BTC,CMCT-ETH,CMCT-USD,MAN-BTC,MAN-ETH,MAN-USD,PNT-BTC,PNT-ETH,FXT-BTC,NEXO-BTC,PAT-BTC,PAT-ETH,XMC-BTC,FXT-ETH,XMC-ETH,XMC-USD,FDZ-BTC,FDZ-ETH,FDZ-USD,SPD-BTC,SPD-ETH,MITX-BTC,TIV-BTC,B2G-BTC,B2G-USD,HBZ-BTC,FACE-BTC,FACE-ETH,HBZ-ETH,HBZ-USD,CPT-BTC,PAT-USD,HTML-BTC,HTML-ETH,MITX-ETH,BTS-BTC,BNK-BTC,BNK-ETH,BNK-USD,TIV-ETH,TIV-USD,CSM-ETH,CSM-USD,INK-BTC,IOST-BTC,INK-ETH,INK-USD,CBC-BTC,IOST-USD,ZIL-BTC,ABYSS-BTC,ABYSS-ETH,ZIL-USD,BCI-BTC,CBC-ETH,CBC-USD,PITCH-BTC,PITCH-ETH,HTML-USD,TDS-BTC,TDS-ETH,TDS-USD,SBD-ETH,SBD-USD,DPN-BTC,UUU-BTC,UUU-ETH,XBP-BTC,ELEC-BTC,ELEC-ETH,ELEC-USD,QNTU-BTC,QNTU-ETH,QNTU-USD,IPL-ETH,IPL-USD,CENNZ-BTC,CENNZ-ETH,SWM-BTC,SPF-USD,SPF-BTC,LCC-BTC,HGT-BTC,ETH-TUSD,BTC-TUSD,LTC-TUSD,XMR-TUSD,ZRX-TUSD,NEO-TUSD,USD-TUSD,BTC-DAI,ETH-DAI,MKR-DAI,EOS-DAI,USD-DAI,MKR-BTC,MKR-ETH,MKR-USD,TUSD-DAI,NEO-DAI,LTC-DAI,XMR-DAI,XRP-DAI,NEXO-ETH,NEXO-USD,DWS-BTC,DWS-ETH,DWS-USD,APPC-BTC,APPC-ETH,APPC-USD,BIT-ETH,SPC-BTC,SPC-ETH,SPC-USD,REX-BTC,REX-ETH,REX-USD,ELF-BTC,ELF-USD,BCD-BTC,BCD-USD,CVCOIN-BTC,CVCOIN-ETH,CVCOIN-USD,EDG-ETH,EDG-USD,NLC2-BTC,DASH-EURS,ZEC-EURS,BTC-EURS,EOS-EURS,ETH-EURS,LTC-EURS,NEO-EURS,XMR-EURS,XRP-EURS,EURS-USD,EURS-TUSD,EURS-DAI,MNX-USD,ROX-ETH,ZPR-ETH,MNX-BTC,MNX-ETH,KIND-BTC,KIND-ETH,ENGT-BTC,ENGT-ETH,PMA-BTC,PMA-ETH,TV-BTC,TV-ETH,TV-USD,BAT-BTC,BAT-ETH,BAT-USD,SRN-BTC,SRN-ETH,SRN-USD,SVD-BTC,SVD-ETH,SVD-USD,GST-BTC,GST-ETH,GST-USD,BNB-BTC,BNB-ETH,BNB-USD,DIT-BTC,DIT-ETH,POA20-BTC,PROC-BTC,POA20-ETH,POA20-USD,POA20-DAI,NIM-BTC,USE-BTC,USE-ETH,DAV-BTC,DAV-ETH,ABTC-BTC,NIM-ETH,ABA-BTC,ABA-ETH,ABA-USD,BCN-EOS,LTC-EOS,XMR-EOS,DASH-EOS,TRX-EOS,NEO-EOS,ZEC-EOS,LSK-EOS,XEM-EOS,XRP-EOS,RCN-BTC,RCN-ETH,RCN-USD,HMQ-BTC,HMQ-ETH,MYST-BTC,MYST-ETH,USD-GUSD,BTC-GUSD,ETH-GUSD,EOS-GUSD,AXPR-BTC,AXPR-ETH,DAG-BTC,DAG-ETH,BITS-BTC,BITS-ETH,BITS-USD,CDCC-BTC,CDCC-ETH,CDCC-USD,VET-BTC,VET-ETH,VET-USD,SILK-ETH,BOX-BTC,BOX-ETH,BOX-EURS,BOX-EOS,VOCO-BTC,VOCO-ETH,VOCO-USD,PASS-BTC,PASS-ETH,SLX-BTC,SLX-USD,PBTT-BTC,PMA-USD,TRAD-BTC,DGTX-BTC,DGTX-ETH,DGTX-USD,MRK-BTC,MRK-ETH,DGB-TUSD,SNBL-BTC,BCH-BTC,BCH-USD,BSV-BTC,BSV-USD,BKX-BTC,NPLC-BTC,NPLC-ETH,ETN-BTC,ETN-ETH,ETN-USD,DTR-BTC,DTR-ETH,TDP-BTC,HBT-ETH,PXG-BTC,PXG-USD,BTC-PAX,ETH-PAX,USD-PAX,BTC-USDC,ETH-USDC,USD-USDC,TUSD-USDC,DAI-USDC,EOS-PAX,CLO-BTC,CLO-ETH,CLO-USD,PETH-BTC,PETH-ETH,PETH-USD,BRD-BTC,BRD-ETH,NMR-BTC,SALT-BTC,SALT-ETH,POLY-BTC,POLY-ETH,POWR-BTC,POWR-ETH,STORJ-BTC,STORJ-ETH,STORJ-USD,MLN-BTC,MLN-ETH,BDG-BTC,POA-ETH,POA-BTC,POA-USD,POA-DAI,KIN-BTC,VEO-BTC,PLA-BTC,PLA-ETH,PLA-USD,BTT-BTC,BTT-USD,BTT-ETH,ZEN-BTC,ZEN-ETH,ZEN-USD,GRIN-BTC,GRIN-ETH,GRIN-USD,FET-BTC,HT-BTC,HT-USD,XZC-BTC,XZC-ETH,XZC-USD,VRA-BTC,VRA-ETH,BTC-KRWB,USD-KRWB,WBTC-ETH,CRO-BTC,CRO-ETH,CRO-USD,GAS-BTC,GAS-ETH,GAS-USD,ORMEUS-BTC,ORMEUS-ETH,SWM-ETH,SWM-USD,PRE-ETH,PHX-BTC,PHX-ETH,PHX-USD,BET-BTC,USD-EOSDT,BTC-EOSDT,ETH-EOSDT,EOS-EOSDT,DAI-EOSDT,NUT-BTC,NUT-EOS,NUT-USD,CUTE-BTC,CUTE-ETH,CUTE-USD,CUTE-EOS,XCON-BTC,DCR-BTC,DCR-ETH,DCR-USD,MG-BTC,MG-ETH,MG-EOS,MG-USD,GNX-BTC,PRO-BTC,EURS-EOSDT,TUSD-EOSDT,ECOIN-BTC,ECOIN-ETH,ECOIN-USD,AGI-BTC,LOOM-BTC,LOOM-ETH,BLZ-BTC,QKC-BTC,QKC-ETH,KNC-BTC,KNC-ETH,KNC-USD,KEY-BTC,KEY-ETH,ATOM-BTC,ATOM-USD,ATOM-ETH,BRDG-BTC,BRDG-ETH,BRDG-USD,MTL-BTC,MTL-ETH,EXP-BTC,BTCB-BTC,PBT-BTC,PBT-ETH,LINK-BTC,LINK-ETH,LINK-USD,USD-USDT20,PHB-BTC,BCH-ETH,BCH-DAI,BCH-TUSD,BCH-EURS,DAPP-BTC,DAPP-EOS,BTC-USDT20,DENT-BTC,DENT-USD,NJBC-BTC,NJBC-ETH,XRC-BTC,EOS-BCH,LTC-BCH,XRP-BCH,TRX-BCH,XLM-BCH,ETC-BCH,DASH-BCH,ZEC-BCH,BKX-USD,LAMB-BTC,NPXS-BTC,HBAR-BTC,HBAR-USD,ONE-BTC,RFR-BTC,RFR-USD,BUSD-USD,PAXG-BTC,PAXG-USD,REN-BTC,IGNIS-BTC,CEL-BTC,CEL-ETH,WIN-USD,ADK-BTC,PART-BTC,SOZ-BTC,SOZ-ETH,SOZ-USD,WAVES-USD,ADA-BCH,ONT-BCH,XMR-BCH,ATOM-BCH,LINK-BCH,OMG-BCH,WAVES-BCH,IOTX-BTC,HOT-BTC,SLV-BTC,HEDG-BTC,CHZ-BTC,CHZ-USD,COCOS-BTC,COCOS-USD,SEELE-BTC,SEELE-USD,MDA-BTC,LEO-USD,REM-BTC,REM-ETH,REM-USD,SCD-DAI,BTC-BUSD,RVN-BTC,BST-BTC,ERD-BTC,KRL-BTC,FTT-BTC,FTT-USD,RAISE-BTC,RAISE-ETH" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "configFormat": { - "uppercase": true, - "delimiter": "-", - "separator": "," + { + "name": "Huobi", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": false + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC-USDT", + "available": "PROPY-ETH,IOTA-BTC,UGAS-ETH,PAI-USDT,BSV-HUSD,MTX-ETH,BCH-BTC,LTC-HT,SOC-USDT,WXT-BTC,SALT-BTC,RCN-ETH,PNT-ETH,TT-USDT,AIDOC-ETH,BIX-BTC,OCN-USDT,QTUM-ETH,KCASH-ETH,SNT-USDT,LUN-BTC,QASH-BTC,ITC-BTC,NAS-BTC,XMR-BTC,TNT-ETH,UC-ETH,FAIR-BTC,PC-ETH,YEE-BTC,PAY-ETH,XMX-BTC,CRE-USDT,BAT-ETH,BHT-USDT,CKB-HT,LAMB-HT,AE-USDT,QUN-ETH,LYM-BTC,BCH-HT,BHT-BTC,RUFF-ETH,CNN-BTC,FOR-USDT,GTC-ETH,TRX-ETH,ELA-USDT,ACT-ETH,SMT-ETH,BUT-ETH,BCH-USDT,ICX-BTC,MEET-BTC,NCC-BTC,APPC-BTC,GVE-ETH,TNB-BTC,STEEM-ETH,18C-ETH,LBA-BTC,EKO-BTC,REQ-BTC,SOC-BTC,BOX-ETH,ELF-BTC,ZRX-ETH,LET-USDT,HT-BTC,TUSD-HUSD,EGCC-BTC,WTC-BTC,ATP-USDT,DOCK-USDT,PAI-BTC,ONT-ETH,IRIS-BTC,BTT-ETH,SC-BTC,XZC-BTC,LBA-USDT,HT-USDT,VET-ETH,KMD-ETH,SHE-ETH,PORTAL-BTC,ONE-BTC,BIX-USDT,RCCC-BTC,SKM-USDT,XTZ-ETH,SWFTC-BTC,RSR-BTC,LINK-ETH,DATX-BTC,HPT-HT,GET-ETH,BLZ-ETH,CTXC-USDT,CNNS-USDT,PVT-HT,ITC-USDT,LTC-BTC,NCASH-BTC,HOT-ETH,ADA-USDT,ADX-BTC,NODE-USDT,TRIO-BTC,GXC-ETH,SNT-BTC,FOR-BTC,DBC-BTC,UUU-USDT,CVCOIN-ETH,RSR-USDT,CRO-USDT,OCN-BTC,NEW-USDT,EGT-USDT,MANA-BTC,CMT-USDT,WXT-HT,XRP-BTC,MT-ETH,PAX-HUSD,LSK-ETH,IOTA-USDT,SRN-ETH,ZIL-ETH,ELF-USDT,LXT-ETH,LAMB-BTC,CRE-HT,CKB-BTC,XVG-BTC,BSV-BTC,BFT-BTC,WPR-ETH,HT-HUSD,POWR-BTC,MANA-ETH,ENG-ETH,ZJLT-ETH,SNC-ETH,ATOM-ETH,WICC-USDT,KAN-ETH,DGD-BTC,VSYS-HT,BCD-BTC,BTM-ETH,DOGE-USDT,MEX-BTC,BTG-BTC,DAC-ETH,DAT-BTC,GRS-ETH,ADX-ETH,EM-HT,GXC-USDT,CVC-BTC,OMG-ETH,SSP-ETH,OGO-HT,CMT-ETH,POLY-ETH,XZC-USDT,THETA-USDT,XEM-USDT,LOL-USDT,BCH-HUSD,GSC-BTC,DOGE-ETH,MDS-BTC,BTS-ETH,CTXC-BTC,MCO-BTC,BCX-BTC,ZLA-ETH,EKT-USDT,MAN-BTC,BLZ-BTC,ATOM-USDT,LOL-BTC,HPT-USDT,EM-BTC,EOS-USDT,WAN-BTC,GNT-BTC,CRO-BTC,MANA-USDT,SEELE-USDT,FSN-BTC,VIDY-HT,USDC-HUSD,LTC-HUSD,XRP-USDT,VSYS-BTC,STORJ-BTC,LOOM-ETH,SKM-BTC,LINK-USDT,TT-HT,QSP-ETH,ETN-BTC,FSN-HT,NODE-BTC,HC-USDT,PHX-BTC,XLM-BTC,RCCC-ETH,LTC-USDT,UUU-BTC,SEELE-ETH,PVT-BTC,HC-ETH,REN-ETH,KAN-USDT,EOS-ETH,BSV-USDT,BTS-USDT,KMD-BTC,OGO-USDT,THETA-ETH,MUSK-BTC,CNNS-HT,ETC-BTC,COVA-BTC,BTT-TRX,XMR-USDT,MTN-ETH,QUN-BTC,NAS-USDT,ELA-ETH,HIT-ETH,BTT-USDT,EKT-ETH,TOS-BTC,GAS-ETH,DCR-USDT,ONT-BTC,NEW-HT,NEXO-BTC,ETH-USDT,WXT-USDT,FOR-HT,ADA-BTC,EVX-ETH,VET-BTC,ZEC-USDT,NANO-ETH,IOST-HT,BCV-ETH,REN-USDT,NULS-ETH,ACT-USDT,LET-ETH,BTM-USDT,MEET-ETH,AKRO-HT,ARDR-BTC,DCR-ETH,NANO-USDT,BTC-HUSD,ALGO-BTC,IIC-ETH,BHD-BTC,KNC-ETH,ATP-BTC,ZRX-BTC,ABT-BTC,18C-BTC,XMR-ETH,WAXP-BTC,CVNT-BTC,MX-USDT,OST-ETH,NKN-BTC,TOPC-BTC,GNX-BTC,FTT-USDT,ONE-HT,DGB-ETH,NULS-USDT,DASH-BTC,UIP-BTC,KCASH-HT,WICC-ETH,EKO-ETH,EGT-HT,IRIS-USDT,STK-ETH,MXC-BTC,NAS-ETH,OMG-USDT,SMT-BTC,BUT-BTC,HIT-USDT,BAT-BTC,IRIS-ETH,NKN-HT,PC-BTC,TOP-USDT,GTC-BTC,LSK-BTC,ITC-ETH,DTA-BTC,HOT-BTC,BTT-BTC,FAIR-ETH,DOCK-ETH,QTUM-BTC,ZEN-BTC,ZIL-BTC,RCN-BTC,FTI-BTC,BHD-USDT,VIDY-USDT,LUN-ETH,DBC-ETH,TOPC-ETH,IIC-BTC,STEEM-USDT,IOTA-ETH,KCASH-BTC,RUFF-BTC,APPC-ETH,MT-BTC,SOC-ETH,GT-HT,PROPY-BTC,AIDOC-BTC,ACT-BTC,LYM-ETH,CHAT-BTC,SWFTC-ETH,ETH-BTC,UIP-USDT,UGAS-BTC,XRP-HUSD,ALGO-USDT,TNT-BTC,ONT-USDT,YEE-ETH,AKRO-BTC,TRX-USDT,OCN-ETH,SRN-BTC,DASH-USDT,XMX-ETH,NANO-BTC,QASH-ETH,EOS-HT,GT-BTC,XTZ-USDT,ARPA-USDT,SALT-ETH,BKBT-ETH,MTX-BTC,SMT-USDT,GXC-BTC,VIDY-BTC,FTT-HT,LAMB-ETH,TRX-BTC,TRIO-ETH,BFT-ETH,LINK-BTC,AE-ETH,NULS-BTC,BHD-HT,AST-ETH,NEO-USDT,EDU-BTC,CVCOIN-BTC,GVE-BTC,GET-BTC,ZRX-USDT,ELF-ETH,DATX-ETH,ADA-ETH,TOP-HT,NCASH-ETH,QTUM-USDT,ETC-HT,ZIL-USDT,TNB-ETH,BIX-ETH,SHE-BTC,PNT-BTC,BTC-USDT,PORTAL-ETH,WAVES-USDT,XZC-ETH,HT-ETH,POLY-BTC,MCO-ETH,MUSK-ETH,PAI-ETH,LXT-USDT,UTK-BTC,RTE-BTC,NCC-ETH,HB10-USDT,BOX-BTC,RDN-ETH,ARPA-BTC,LBA-ETH,CNN-ETH,AAC-ETH,XTZ-BTC,IDT-BTC,AKRO-USDT,IOST-BTC,GT-USDT,WAN-ETH,ETN-ETH,PVT-USDT,NEO-BTC,WAVES-ETH,ONE-USDT,ZEC-BTC,SKM-HT,IOST-ETH,NPXS-ETH,CVC-ETH,CMT-BTC,COVA-ETH,ARDR-ETH,RDN-BTC,DCR-BTC,REN-BTC,YCC-ETH,MX-HT,NEXO-ETH,XLM-ETH,YCC-BTC,ENG-BTC,CNNS-BTC,ZLA-BTC,QSP-BTC,MAN-ETH,UUU-ETH,ETH-HUSD,RTE-ETH,ATP-HT,BTM-BTC,DAC-BTC,TOS-ETH,LAMB-USDT,DASH-HT,NPXS-BTC,NEW-BTC,FTT-BTC,EOS-HUSD,GRS-BTC,POWR-ETH,VET-USDT,AAC-BTC,MX-BTC,MTN-BTC,XVG-ETH,GNX-ETH,SSP-BTC,WAVES-BTC,EGT-BTC,CTXC-ETH,IDT-ETH,STK-BTC,WICC-BTC,UTK-ETH,CRO-HT,LXT-BTC,GSC-ETH,OMG-BTC,XRP-HT,DGB-BTC,IOST-USDT,CVNT-ETH,GAS-BTC,HIT-BTC,CKB-USDT,ARPA-HT,RUFF-USDT,HC-BTC,WTC-ETH,MDS-USDT,ABT-ETH,ALGO-ETH,BIFI-BTC,KNC-BTC,TT-BTC,LET-BTC,NKN-USDT,PAY-BTC,DTA-USDT,AE-BTC,UC-BTC,VSYS-USDT,USDT-HUSD,EOS-BTC,STEEM-BTC,DOGE-BTC,NODE-HT,MDS-ETH,CRE-BTC,GNT-USDT,UIP-ETH,AST-BTC,XEM-BTC,ZEN-ETH,EDU-ETH,MEX-ETH,EKT-BTC,CVC-USDT,WAXP-ETH,REQ-ETH,OST-BTC,STORJ-USDT,SBTC-BTC,DGD-ETH,SC-ETH,WTC-USDT,THETA-BTC,DTA-ETH,BCV-BTC,SNC-BTC,RSR-HT,KAN-BTC,ELA-BTC,ATOM-BTC,BKBT-BTC,FSN-USDT,EM-USDT,WPR-BTC,TOP-BTC,BTS-BTC,EGCC-ETH,MTL-BTC,GNT-ETH,SEELE-BTC,EVX-BTC,FTI-ETH,BAT-USDT,MT-HT,LOL-HT,ICX-ETH,LOOM-BTC,ZJLT-BTC,XLM-USDT,OGO-BTC,DOCK-BTC,CHAT-ETH,DAT-ETH,ETC-USDT,HPT-BTC,BHT-HT" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "XBT-USD", - "available": "ETH-GBP,XRP-USD,DAI-EUR,LSK-USD,BAT-EUR,BCH-EUR,EOS-ETH,GNO-EUR,ETH-CAD,XRP-JPY,ADA-ETH,DAI-USD,DASH-EUR,GNO-USD,LSK-XBT,ETH-EUR,ZEC-EUR,DASH-XBT,EOS-EUR,ETH-CHF,SC-ETH,SC-USD,WAVES-EUR,XBT-USD,ADA-EUR,LINK-USD,NANO-EUR,PAXG-USD,SC-EUR,WAVES-ETH,REP-USD,EOS-XBT,ETC-ETH,XMR-USD,LTC-USD,MLN-XBT,XTZ-CAD,XBT-GBP,ADA-CAD,XTZ-EUR,ETH-JPY,XTZ-USD,XDG-XBT,XLM-EUR,ATOM-USD,ATOM-XBT,OMG-EUR,ZEC-JPY,ADA-XBT,GNO-ETH,LINK-XBT,ETC-EUR,BCH-XBT,QTUM-ETH,XBT-CHF,LTC-EUR,ETH-DAI,LSK-EUR,NANO-USD,QTUM-XBT,XRP-XBT,ZEC-USD,BAT-ETH,LINK-ETH,XBT-CAD,BAT-USD,GNO-XBT,ICX-XBT,PAXG-ETH,DAI-USDT,NANO-ETH,OMG-ETH,WAVES-XBT,ZEC-XBT,BAT-XBT,NANO-XBT,XBT-JPY,DASH-USD,ICX-ETH,LSK-ETH,QTUM-CAD,REP-XBT,XMR-XBT,XRP-EUR,ATOM-CAD,OMG-USD,LTC-XBT,MLN-ETH,XTZ-ETH,EOS-USD,ICX-EUR,SC-XBT,ETC-USD,BCH-USD,ICX-USD,QTUM-USD,ETH-XBT,ETH-USD,OMG-XBT,PAXG-EUR,REP-EUR,ADA-USD,USDT-USD,XMR-EUR,XRP-CAD,ATOM-EUR,ETC-XBT,XBT-EUR,XLM-USD,ATOM-ETH,LINK-EUR,PAXG-XBT,WAVES-USD,REP-ETH,XLM-XBT,QTUM-EUR,XTZ-XBT" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + { + "name": "ITBIT", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD,SGD", + "currencyPairs": { + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + }, + "useGlobalFormat": true, + "lastUpdated": 1591062026, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "XBTUSD,XBTSGD", + "available": "XBTUSD,XBTSGD" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresSecret": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": {}, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": false, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "credentials": { - "key": "Key", - "secret": "Secret" + { + "name": "Kraken", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "EUR,USD,CAD,GBP,JPY", + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "separator": "," + }, + "configFormat": { + "uppercase": true, + "delimiter": "-", + "separator": "," + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "XBT-USD", + "available": "ETH-GBP,XRP-USD,DAI-EUR,LSK-USD,BAT-EUR,BCH-EUR,EOS-ETH,GNO-EUR,ETH-CAD,XRP-JPY,ADA-ETH,DAI-USD,DASH-EUR,GNO-USD,LSK-XBT,ETH-EUR,ZEC-EUR,DASH-XBT,EOS-EUR,ETH-CHF,SC-ETH,SC-USD,WAVES-EUR,XBT-USD,ADA-EUR,LINK-USD,NANO-EUR,PAXG-USD,SC-EUR,WAVES-ETH,REP-USD,EOS-XBT,ETC-ETH,XMR-USD,LTC-USD,MLN-XBT,XTZ-CAD,XBT-GBP,ADA-CAD,XTZ-EUR,ETH-JPY,XTZ-USD,XDG-XBT,XLM-EUR,ATOM-USD,ATOM-XBT,OMG-EUR,ZEC-JPY,ADA-XBT,GNO-ETH,LINK-XBT,ETC-EUR,BCH-XBT,QTUM-ETH,XBT-CHF,LTC-EUR,ETH-DAI,LSK-EUR,NANO-USD,QTUM-XBT,XRP-XBT,ZEC-USD,BAT-ETH,LINK-ETH,XBT-CAD,BAT-USD,GNO-XBT,ICX-XBT,PAXG-ETH,DAI-USDT,NANO-ETH,OMG-ETH,WAVES-XBT,ZEC-XBT,BAT-XBT,NANO-XBT,XBT-JPY,DASH-USD,ICX-ETH,LSK-ETH,QTUM-CAD,REP-XBT,XMR-XBT,XRP-EUR,ATOM-CAD,OMG-USD,LTC-XBT,MLN-ETH,XTZ-ETH,EOS-USD,ICX-EUR,SC-XBT,ETC-USD,BCH-USD,ICX-USD,QTUM-USD,ETH-XBT,ETH-USD,OMG-XBT,PAXG-EUR,REP-EUR,ADA-USD,USDT-USD,XMR-EUR,XRP-CAD,ATOM-EUR,ETC-XBT,XBT-EUR,XLM-USD,ATOM-ETH,LINK-EUR,PAXG-XBT,WAVES-USD,REP-ETH,XLM-XBT,QTUM-EUR,XTZ-XBT" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresBase64DecodeSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true, - "requiresBase64DecodeSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + { + "name": "Kucoin", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 150000000000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "assetTypes": [ + "spot", + "margin", + "futures" + ], + "pairs": { + "spot": { + "assetEnabled": true, + "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,XMR-BTC", + "available": "BTC-USDT,MHC-ETH,MHC-BTC,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MHC-USDT,XMR-BTC,XMR-ETH,RIF-BTC,MTV-BTC,MTV-ETH,CRO-BTC,MTV-USDT,KMD-BTC,KMD-USDT,RFOX-USDT,TEL-USDT,TT-USDT,AERGO-USDT,XMR-USDT,TRX-KCS,ATOM-BTC,ATOM-ETH,ATOM-USDT,ATOM-KCS,ETN-USDT,FTM-USDT,TOMO-USDT,VSYS-USDT,OCEAN-BTC,OCEAN-ETH,CHR-BTC,CHR-USDT,FX-BTC,FX-ETH,NIM-BTC,NIM-ETH,COTI-BTC,COTI-USDT,NRG-ETH,BNB-BTC,BNB-USDT,JAR-BTC,JAR-USDT,ALGO-BTC,ALGO-ETH,ALGO-USDT,XEM-BTC,XEM-USDT,CIX100-USDT,XTZ-BTC,XTZ-USDT,ZEC-BTC,ZEC-USDT,ADA-BTC,ADA-USDT,REV-USDT,WXT-BTC,WXT-USDT,FORESTPLUS-BTC,FORESTPLUS-USDT,BOLT-BTC,BOLT-USDT,ARPA-USDT,CHZ-BTC,CHZ-USDT,DAPPT-BTC,DAPPT-USDT,NOIA-BTC,NOIA-USDT,WIN-BTC,WIN-USDT,DERO-BTC,DERO-USDT,BTT-USDT,EOSC-USDT,ENQ-BTC,ENQ-USDT,ONE-BTC,ONE-USDT,TOKO-BTC,TOKO-USDT,VID-BTC,VID-USDT,LUNA-USDT,SXP-BTC,SXP-USDT,AKRO-BTC,AKRO-USDT,ROOBEE-BTC,WIN-TRX,MAP-BTC,MAP-USDT,AMPL-BTC,AMPL-USDT,DAG-USDT,POL-USDT,ARX-USDT,NWC-BTC,NWC-USDT,BEPRO-BTC,BEPRO-USDT,VRA-BTC,VRA-USDT,KSM-BTC,KSM-USDT,DASH-USDT,SUTER-USDT,ACOIN-USDT,SUTER-BTC,SENSO-USDT,PRE-BTC,XDB-USDT,SYLO-USDT,WOM-USDT,SENSO-BTC,DGB-USDT,LYXE-USDT,LYXE-ETH,XDB-BTC,STX-BTC,STX-USDT,XSR-USDT,COMP-USDT,CRO-USDT,KAI-USDT,KAI-BTC,WEST-BTC,WEST-USDT,EWT-BTC,WAVES-USDT,WAVES-BTC,ORN-USDT,AMPL-ETH,BNS-USDT,MKR-USDT,SUKU-BTC,MLK-BTC,MLK-USDT,JST-USDT,KAI-ETH,SUKU-USDT,DIA-USDT,DIA-BTC,LINK-BTC,LINK-USDT,DOT-USDT,DOT-BTC,SHA-BTC,SHA-USDT,EWT-USDT,USDJ-USDT,EFX-BTC,CKB-BTC,CKB-USDT,UMA-USDT,ALEPH-USDT,VELO-USDT,SUN-USDT,BUY-USDT,YFI-USDT,OXEN-USDT,UNI-USDT,UOS-USDT,UOS-BTC,NIM-USDT,DEGO-USDT,DEGO-ETH,UDOO-ETH,RFUEL-USDT,FIL-USDT,UBX-ETH,REAP-USDT,AAVE-USDT,AAVE-BTC,TONE-BTC,TONE-ETH,ELF-ETH,AERGO-BTC,IOST-ETH,KCS-USDT,SNX-ETH,TOMO-ETH,KCS-ETH,DRGN-BTC,WAN-ETH,NULS-ETH,AXPR-ETH,POWR-BTC,QTUM-BTC,MANA-BTC,TEL-BTC,XYO-ETH,AXPR-BTC,ETN-BTC,COV-ETH,VET-BTC,KCS-BTC,CAPP-ETH,ONT-BTC,DRGN-ETH,DAG-ETH,TOMO-BTC,WAN-BTC,KNC-ETH,CRPT-ETH,LTC-USDT,BAX-ETH,BSV-USDT,DENT-ETH,AION-ETH,LYM-ETH,TRAC-ETH,ENJ-BTC,WAXP-BTC,DGB-BTC,ELA-BTC,ZIL-BTC,BSV-BTC,XLM-USDT,IOTX-ETH,SOUL-BTC,DOCK-BTC,AMB-ETH,TRX-BTC,XRP-TUSD,NULS-BTC,ETH-DAI,LSK-BTC,GMB-ETH,GMB-BTC,NEO-ETH,OMG-ETH,BTC-TUSD,KAT-USDT,KNC-BTC,ELF-BTC,MANA-ETH,ETC-USDT,ONT-ETH,MKR-BTC,KAT-BTC,XRP-USDC,XYO-BTC,SNT-ETH,ZRX-BTC,LOOM-ETH,AION-BTC,POWR-ETH,OLT-ETH,OLT-BTC,SNT-BTC,TRAC-BTC,XLM-ETH,ETH-USDT,BSV-ETH,TRX-ETH,ETN-ETH,AOA-USDT,BCD-BTC,DENT-BTC,DOCK-ETH,KEY-BTC,EOS-KCS,XLM-BTC,ADB-ETH,TIME-ETH,CVC-BTC,LSK-ETH,QKC-BTC,AMB-BTC,USDT-TUSD,ETC-ETH,XRP-BTC,NEO-KCS,SNX-USDT,CRPT-BTC,IOTX-BTC,LTC-ETH,XRP-KCS,ADB-BTC,LTC-KCS,TEL-ETH,DCR-ETH,LYM-USDT,USDT-USDC,ETH-USDC,DAG-BTC,AVA-BTC,BTC-USDT,WAXP-ETH,XRP-USDT,KEY-ETH,VET-ETH,FTM-BTC,USDT-DAI,QKC-ETH,ETH-BTC,MAN-BTC,CPC-ETH,TRX-USDT,BTC-DAI,ONT-USDT,DASH-ETH,BAX-BTC,AVA-ETH,LOOM-BTC,MVP-BTC,MKR-ETH,COV-BTC,CPC-BTC,REQ-ETH,EOS-BTC,LTC-BTC,XRP-ETH,CAPP-BTC,FTM-ETH,BCD-ETH,ZRX-ETH,DGB-ETH,VET-USDT,REQ-BTC,UTK-BTC,PLAY-BTC,UTK-ETH,SNX-BTC,MVP-ETH,NEO-BTC,SOUL-ETH,NEO-USDT,ELA-ETH,OMG-BTC,TIME-BTC,AOA-BTC,ETC-BTC,DCR-BTC,BTC-USDC,ENJ-ETH,IOST-BTC,DASH-BTC,EOS-USDT,EOS-ETH,ZIL-ETH,ETH-TUSD,GAS-BTC,LYM-BTC,BCH-BTC,VSYS-BTC,BCH-USDT,MKR-DAI,SOLVE-BTC,GRIN-BTC,GRIN-USDT,UQC-BTC,UQC-ETH,OPCT-BTC,OPCT-ETH,PRE-USDT,SHR-BTC,SHR-USDT,UBXT-USDT,ROSE-USDT,USDC-USDT,CTI-USDT,CTI-ETH,ETH2-ETH,BUX-BTC,XHV-USDT,PLU-USDT,GRT-USDT,CAS-BTC,CAS-USDT,MSWAP-BTC,MSWAP-USDT,GOM2-BTC,GOM2-USDT,REVV-BTC,REVV-USDT,LON-USDT,1INCH-USDT,LOC-USDT,API3-USDT,UNFI-USDT,HTR-USDT,FRONT-USDT,FRONT-BTC,WBTC-BTC,WBTC-ETH,MIR-USDT,LTC-USDC,BCH-USDC,HYDRA-USDT,DFI-USDT,DFI-BTC,CRV-USDT,SUSHI-USDT,FRM-USDT,EOS-USDC,BSV-USDC,ZEN-USDT,CUDOS-USDT,ADA-USDC,REN-USDT,LRC-USDT,LINK-USDC,KLV-USDT,KLV-BTC,BOA-USDT,THETA-USDT,QNT-USDT,BAT-USDT,DOGE-USDT,DOGE-USDC,DAO-USDT,STRONG-USDT,TRIAS-USDT,TRIAS-BTC,DOGE-BTC,MITX-BTC,MITX-USDT,CAKE-USDT,ORAI-USDT,ZEE-USDT,LTX-USDT,LTX-BTC,MASK-USDT,KLV-TRX,IDEA-USDT,PHA-USDT,PHA-ETH,BCH-KCS,SRK-USDT,SRK-BTC,ADA-KCS,HTR-BTC,BSV-KCS,DOT-KCS,LINK-KCS,MIR-KCS,BNB-KCS,XLM-KCS,VET-KCS,SWINGBY-USDT,SWINGBY-BTC,XHV-BTC,DASH-KCS,UNI-KCS,AAVE-KCS,DOGE-KCS,ZEC-KCS,XTZ-KCS,GRT-KCS,ALGO-KCS,EWT-KCS,GAS-USDT,AVAX-USDT,AVAX-BTC,KRL-BTC,KRL-USDT,POLK-USDT,POLK-BTC,ENJ-USDT,MANA-USDT,RNDR-USDT,RNDR-BTC,RLY-USDT,ANC-USDT,SKEY-USDT,LAYER-USDT,TARA-USDT,TARA-ETH,IOST-USDT,DYP-USDT,DYP-ETH,XYM-USDT,XYM-BTC,PCX-USDT,PCX-BTC,ORBS-USDT,ORBS-BTC,BTC3L-USDT,BTC3S-USDT,ETH3L-USDT,ETH3S-USDT,ANKR-USDT,DSLA-USDT,DSLA-BTC,SAND-USDT,VAI-USDT,XCUR-USDT,XCUR-BTC,FLUX-USDT,OMG-USDT,ZIL-USDT,DODO-USDT,MAN-USDT,BAX-USDT,BOSON-USDT,BOSON-ETH,PUNDIX-USDT,PUNDIX-BTC,WAXP-USDT,HT-USDT,PDEX-USDT,LABS-USDT,LABS-ETH,GMB-USDT,PHNX-USDT,PHNX-BTC,HAI-USDT,EQZ-USDT,FORTH-USDT,HORD-USDT,CGG-USDT,UBX-USDT,GHX-USDT,TCP-USDT,STND-USDT,STND-ETH,TOWER-USDT,TOWER-BTC,ACE-USDT,LOCG-USDT,CARD-USDT,FLY-USDT,CWS-USDT,XDC-USDT,XDC-ETH,STRK-BTC,STRK-ETH,SHIB-USDT,POLX-USDT,KDA-USDT,KDA-BTC,ICP-USDT,ICP-BTC,STC-USDT,STC-BTC,GOVI-USDT,GOVI-BTC,FKX-USDT,CELO-USDT,CELO-BTC,CUSD-USDT,CUSD-BTC,FCL-USDT,MATIC-USDT,MATIC-BTC,ELA-USDT,CRPT-USDT,OPCT-USDT,OGN-USDT,OGN-BTC,OUSD-USDT,OUSD-BTC,TLOS-USDT,TLOS-BTC,YOP-USDT,YOP-ETH,GLQ-USDT,GLQ-BTC,MXC-USDT,ERSDL-USDT,HOTCROSS-USDT,ADA3L-USDT,ADA3S-USDT,HYVE-USDT,HYVE-BTC,DAPPX-USDT,KONO-USDT,PRQ-USDT,MAHA-USDT,MAHA-BTC,FEAR-USDT,PYR-USDT,PYR-BTC,PROM-USDT,PROM-BTC,GLCH-USDT,UNO-USDT,ALBT-USDT,ALBT-ETH,XCAD-USDT,EOS3L-USDT,EOS3S-USDT,BCH3L-USDT,BCH3S-USDT,ELON-USDT,APL-USDT,FCL-ETH,VEED-USDT,VEED-BTC,DIVI-USDT,PDEX-BTC,JUP-USDT,JUP-ETH,POLS-USDT,POLS-BTC,LPOOL-USDT,LPOOL-BTC,LSS-USDT,VET3L-USDT,VET3S-USDT,LTC3L-USDT,LTC3S-USDT,ABBC-USDT,ABBC-BTC,KOK-USDT,ROSN-USDT,DORA-USDT,DORA-BTC,ZCX-USDT,ZCX-BTC,NORD-USDT,GMEE-USDT,SFUND-USDT,XAVA-USDT,AI-USDT,ALPACA-USDT,IOI-USDT,NFT-USDT,NFT-TRX,MNST-USDT,MEM-USDT,AGIX-USDT,AGIX-BTC,AGIX-ETH,CQT-USDT,AIOZ-USDT,MARSH-USDT,HAPI-USDT,MODEFI-USDT,MODEFI-BTC,YFDAI-USDT,YFDAI-BTC,GENS-USDT,FORM-USDT,ARRR-USDT,ARRR-BTC,TOKO-KCS,EXRD-USDT,NGM-USDT,LPT-USDT,STMX-USDT,ASD-USDT,BOND-USDT,HAI-BTC,SOUL-USDT,2CRZ-USDT,NEAR-USDT,NEAR-BTC,DFYN-USDT,OOE-USDT,CFG-USDT,CFG-BTC,AXS-USDT,CLV-USDT,ROUTE-USDT,KAR-USDT,EFX-USDT,XDC-BTC,SHFT-USDT,PMON-USDT,DPET-USDT,ERG-USDT,ERG-BTC,SOL-USDT,SLP-USDT,LITH-USDT,LITH-ETH,XCH-USDT,HAKA-USDT,LAYER-BTC,MTL-USDT,MTL-BTC,IOTX-USDT,GALA-USDT,REQ-USDT,TXA-USDT,TXA-USDC,CIRUS-USDT,QI-USDT,QI-BTC,ODDZ-USDT,PNT-USDT,PNT-BTC,XPR-USDT,XPR-BTC,TRIBE-USDT,SHFT-BTC,MOVR-USDT,MOVR-ETH,WOO-USDT,WILD-USDT,QRDO-USDT,QRDO-ETH,SDN-USDT,SDN-ETH,MAKI-USDT,MAKI-BTC,REP-USDT,REP-BTC,REP-ETH,BNT-USDT,BNT-BTC,BNT-ETH,OXT-USDT,OXT-BTC,OXT-ETH,BAL-USDT,BAL-BTC,BAL-ETH,STORJ-USDT,STORJ-BTC,STORJ-ETH,YGG-USDT,NDAU-USDT,SDAO-USDT,SDAO-ETH,XRP3L-USDT,XRP3S-USDT,SKL-USDT,SKL-BTC,NMR-USDT,NMR-BTC,IXS-USDT,TRB-USDT,TRB-BTC,DYDX-USDT,XYO-USDT,GTC-USDT,GTC-BTC,EQX-USDT,EQX-BTC,RLC-USDT,RLC-BTC,XPRT-USDT,EGLD-USDT,EGLD-BTC,HBAR-USDT,HBAR-BTC,DOGE3L-USDT,DOGE3S-USDT,FLOW-USDT,FLOW-BTC,NKN-USDT,NKN-BTC,PBX-USDT,SOL3L-USDT,SOL3S-USDT,MLN-USDT,MLN-BTC,XNL-USDT,SOLVE-USDT,WNCG-USDT,WNCG-BTC,DMTR-USDT,LINK3L-USDT,LINK3S-USDT,DOT3L-USDT,DOT3S-USDT,CTSI-USDT,CTSI-BTC,ALICE-USDT,ALICE-BTC,ALICE-ETH,OPUL-USDT,ILV-USDT,BAND-USDT,BAND-BTC,FTT-USDT,FTT-BTC,DVPN-USDT,SKU-USDT,SKU-BTC,EDG-USDT,SLIM-USDT,TLM-USDT,TLM-BTC,TLM-ETH,DEXE-USDT,DEXE-BTC,DEXE-ETH,MATTER-USDT,CUDOS-BTC,RUNE-USDT,RUNE-BTC,RMRK-USDT,BMON-USDT,C98-USDT,BLOK-USDT,SOLR-USDT,ATOM3L-USDT,ATOM3S-USDT,UNI3L-USDT,UNI3S-USDT,WSIENNA-USDT,PUSH-USDT,PUSH-BTC,FORM-ETH,NTVRK-USDT,NTVRK-USDC,AXS3L-USDT,AXS3S-USDT,FTM3L-USDT,FTM3S-USDT,FLAME-USDT,AGLD-USDT,NAKA-USDT,YLD-USDT,TONE-USDT,REEF-USDT,REEF-BTC,TIDAL-USDT,TVK-USDT,TVK-BTC,INJ-USDT,INJ-BTC,BNB3L-USDT,BNB3S-USDT,MATIC3L-USDT,MATIC3S-USDT,NFTB-USDT,VEGA-USDT,VEGA-ETH,ALPHA-USDT,ALPHA-BTC,BADGER-USDT,BADGER-BTC,UNO-BTC,ZKT-USDT,AR-USDT,AR-BTC,XVS-USDT,XVS-BTC,JASMY-USDT,PERP-USDT,PERP-BTC,GHST-USDT,GHST-BTC,SCLP-USDT,SCLP-BTC,SUPER-USDT,SUPER-BTC,CPOOL-USDT,HERO-USDT,BASIC-USDT,XED-USDT,XED-BTC,AURY-USDT,SWASH-USDT,LTO-USDT,LTO-BTC,BUX-USDT,MTRG-USDT,DREAMS-USDT,SHIB-DOGE,QUICK-USDT,QUICK-BTC,TRU-USDT,TRU-BTC,WRX-USDT,WRX-BTC,TKO-USDT,TKO-BTC,SUSHI3L-USDT,SUSHI3S-USDT,NEAR3L-USDT,NEAR3S-USDT,DATA-USDT,DATA-BTC,NORD-BTC,ISP-USDT,CERE-USDT,SHILL-USDT,HEGIC-USDT,HEGIC-BTC,ERN-USDT,ERN-BTC,FTG-USDT,PAXG-USDT,PAXG-BTC,AUDIO-USDT,AUDIO-BTC,ENS-USDT,AAVE3L-USDT,AAVE3S-USDT,SAND3L-USDT,SAND3S-USDT,XTM-USDT,MNW-USDT,FXS-USDT,FXS-BTC,ATA-USDT,ATA-BTC,VXV-USDT,LRC-BTC,LRC-ETH,DPR-USDT,CWAR-USDT,CWAR-BTC,FLUX-BTC,EDG-BTC,PBR-USDT,WNXM-USDT,WNXM-BTC,ANT-USDT,ANT-BTC,COV-USDT,SWP-USDT,TWT-USDT,TWT-BTC,OM-USDT,OM-BTC,ADX-USDT,AVAX3L-USDT,AVAX3S-USDT,MANA3L-USDT,MANA3S-USDT,GLM-USDT,GLM-BTC,BAKE-USDT,BAKE-BTC,BAKE-ETH,NUM-USDT,VLX-USDT,VLX-BTC,TRADE-USDT,TRADE-BTC,1EARTH-USDT,MONI-USDT,LIKE-USDT,MFT-USDT,MFT-BTC,LIT-USDT,LIT-BTC,KAVA-USDT,SFP-USDT,SFP-BTC,BURGER-USDT,BURGER-BTC,ILA-USDT,CREAM-USDT,CREAM-BTC,RSR-USDT,RSR-BTC,BUY-BTC,IMX-USDT,GODS-USDT,KMA-USDT,SRM-USDT,SRM-BTC,POLC-USDT,XTAG-USDT,MNET-USDT,NGC-USDT,HARD-USDT,GALAX3L-USDT,GALAX3S-USDT,UNIC-USDT,POND-USDT,POND-BTC,VR-USDT,EPIK-USDT,NGL-USDT,NGL-BTC,KDON-USDT,PEL-USDT,CIRUS-ETH,LINA-USDT,LINA-BTC,KLAY-USDT,KLAY-BTC,CREDI-USDT,TRVL-USDT,LACE-USDT,LACE-ETH,ARKER-USDT,BONDLY-USDT,BONDLY-ETH,XEC-USDT,HEART-USDT,HEART-BTC,UNB-USDT,GAFI-USDT,KOL-USDT,KOL-ETH,H3RO3S-USDT,FALCONS-USDT,UFO-USDT,CHMB-USDT,GEEQ-USDT,ORC-USDT,RACEFI-USDT,PEOPLE-USDT,ADS-USDT,ADS-BTC,OCEAN-USDT,SOS-USDT,WHALE-USDT,TIME-USDT,CWEB-USDT,IOTA-USDT,IOTA-BTC,OOKI-USDT,OOKI-BTC,HNT-USDT,HNT-BTC,GGG-USDT,POWR-USDT,REVU-USDT,CLH-USDT,PLGR-USDT,GLMR-USDT,GLMR-BTC,LOVE-USDT,CTC-USDT,CTC-BTC,GARI-USDT,FRR-USDT,ASTR-USDT,ASTR-BTC,ERTHA-USDT,FCON-USDT,ACA-USDT,ACA-BTC,MTS-USDT,ROAR-USDT,HBB-USDT,SURV-USDT,CVX-USDT,AMP-USDT,ACT-USDT,MJT-USDT,MJT-KCS,SHX-USDT,SHX-BTC,STARLY-USDT,ONSTON-USDT,RANKER-USDT,WMT-USDT,XNO-USDT,XNO-BTC,MARS4-USDT,TFUEL-USDT,TFUEL-BTC,METIS-USDT,LAVAX-USDT,WAL-USDT,BULL-USDT,SON-USDT,MELOS-USDT,APE-USDT,GMT-USDT,BICO-USDT,STG-USDT,LMR-USDT,LMR-BTC,LOKA-USDT,URUS-USDT,JAM-USDT,JAM-ETH,BNC-USDT,LBP-USDT,CFX-USDT,LOOKS-USDT,XCN-USDT,XCN-BTC,KP3R-USDT,TITAN-USDT,INDI-USDT,UPO-USDT,SPELL-USDT,SLCL-USDT,CEEK-USDT,VEMP-USDT,BETA-USDT,NHCT-USDT,ARNM-USDT,FRA-USDT,VISION-USDT,COCOS-USDT,ALPINE-USDT,BNX-USDT,ZBC-USDT,WOOP-USDT,T-USDT,NYM-USDT,VOXEL-USDT,VOXEL-ETH,PSTAKE-USDT,SPA-USDT,SPA-ETH,SYNR-USDT,DAR-USDT,DAR-BTC,MV-USDT,XDEFI-USDT,RACA-USDT,XWG-USDT,HAWK-USDT,TRVL-BTC,SWFTC-USDT,IDEX-USDT,BRWL-USDT,PLATO-USDT,TAUM-USDT,CELR-USDT,AURORA-USDT,POSI-USDT,COOHA-USDT,KNC-USDT,EPK-USDT,PLD-USDT,PSL-USDT,PKF-USDT,OVR-USDT,SYS-USDT,SYS-BTC,BRISE-USDT,DG-USDT,EPX-USDT,GST-USDT,PLY-USDT,GAL-USDT,BSW-USDT,FITFI-USDT,FSN-USDT,H2O-USDT,GMM-USDT,AKT-USDT,SIN-USDT,AUSD-USDT,BOBA-USDT,KARA-USDT,BFC-USDT,BIFI-USDT,DFA-USDT,KYL-USDT,FCD-USDT,MBL-USDT,CELT-USDT,DUSK-USDT,USDD-USDT,USDD-USDC,FITFI-USDC,MBOX-USDT,MBOX-BTC,APE-USDC,AVAX-USDC,SHIB-USDC,XCN-USDC,TRX-USDC,NEAR-USDC,MATIC-USDC,FTM-USDC,ZIL-USDC,SOL-USDC,MLS-USDT,AFK-USDT,AFK-USDC,ACH-USDT,SCRT-USDT,SCRT-BTC,APE3L-USDT,APE3S-USDT,STORE-USDT,STORE-ETH,GMT3L-USDT,GMT3S-USDT,CCD-USDT,DOSE-USDC,LUNC-USDT,LUNC-USDC,USTC-USDT,USTC-USDC,GMT-USDC,VRA-USDC,DOT-USDC,RUNE-USDC,ATOM-USDC,BNB-USDC,JASMY-USDC,KCS-USDC,KDA-USDC,ALGO-USDC,LUNA-USDC,OP-USDT,OP-USDC,JASMY3L-USDT,JASMY3S-USDT,EVER-USDT,MOOV-USDT,IHC-USDT,ICX-USDT,ICX-ETH,BTC-BRL,ETH-BRL,USDT-BRL,WELL-USDT,FORT-USDT,USDP-USDT,USDD-TRX,CSPR-USDT,CSPR-ETH,WEMIX-USDT,REV3L-USDT,OLE-USDT,LDO-USDT,LDO-USDC,CULT-USDT,SWFTC-USDC,FIDA-USDT,BUSD-USDT,RBP-USDT,SRBP-USDT,HIBAYC-USDT,BUSD-USDC,OGV-USDT,WOMBAT-USDT,HIPUNKS-USDT,FT-USDT,ETC-USDC,HIENS4-USDT,EGAME-USDT,EGAME-BTC,STEPWATCH-USDT,HISAND33-USDT,DC-USDT,NEER-USDT,RVN-USDT,HIENS3-USDT,MC-USDT,PEEL-USDT,PEEL-BTC,SDL-USDT,SDL-BTC,SWEAT-USDT,HIODBS-USDT,CMP-USDT,PIX-USDT,MPLX-USDT,HIDOODLES-USDT,ETHW-USDT,QUARTZ-USDT,ACQ-USDT,ACQ-USDC,AOG-USDT,HIMAYC-USDT,PRMX-USDT,RED-USDT,PUMLX-USDT,XETA-USDT,GEM-USDT,DERC-USDT,P00LS-USDT,P00LS-USDC,KICKS-USDT,TRIBL-USDT,GMX-USDT,HIOD-USDT,POKT-USDT,EFI-USDT,APT-USDT,BBC-USDT,EUL-USDT,TON-USDT,PIAS-USDT,HIMEEBITS-USDT,HISQUIGGLE-USDT,XCV-USDT,HFT-USDT,HFT-USDC,ECOX-USDT,AMB-USDT,AZERO-USDT,HIFIDENZA-USDT,BEAT-USDT", + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + } + }, + "margin": { + "assetEnabled": true, + "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MTV-BTC,ETH-BTC", + "available": "BTC-USDT,MHC-ETH,MHC-BTC,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MHC-USDT,XMR-BTC,XMR-ETH,RIF-BTC,MTV-BTC,MTV-ETH,CRO-BTC,MTV-USDT,KMD-BTC,KMD-USDT,RFOX-USDT,TEL-USDT,TT-USDT,AERGO-USDT,XMR-USDT,TRX-KCS,ATOM-BTC,ATOM-ETH,ATOM-USDT,ATOM-KCS,ETN-USDT,FTM-USDT,TOMO-USDT,VSYS-USDT,OCEAN-BTC,OCEAN-ETH,CHR-BTC,CHR-USDT,FX-BTC,FX-ETH,NIM-BTC,NIM-ETH,COTI-BTC,COTI-USDT,NRG-ETH,BNB-BTC,BNB-USDT,JAR-BTC,JAR-USDT,ALGO-BTC,ALGO-ETH,ALGO-USDT,XEM-BTC,XEM-USDT,CIX100-USDT,XTZ-BTC,XTZ-USDT,ZEC-BTC,ZEC-USDT,ADA-BTC,ADA-USDT,REV-USDT,WXT-BTC,WXT-USDT,FORESTPLUS-BTC,FORESTPLUS-USDT,BOLT-BTC,BOLT-USDT,ARPA-USDT,CHZ-BTC,CHZ-USDT,DAPPT-BTC,DAPPT-USDT,NOIA-BTC,NOIA-USDT,WIN-BTC,WIN-USDT,DERO-BTC,DERO-USDT,BTT-USDT,EOSC-USDT,ENQ-BTC,ENQ-USDT,ONE-BTC,ONE-USDT,TOKO-BTC,TOKO-USDT,VID-BTC,VID-USDT,LUNA-USDT,SXP-BTC,SXP-USDT,AKRO-BTC,AKRO-USDT,ROOBEE-BTC,WIN-TRX,MAP-BTC,MAP-USDT,AMPL-BTC,AMPL-USDT,DAG-USDT,POL-USDT,ARX-USDT,NWC-BTC,NWC-USDT,BEPRO-BTC,BEPRO-USDT,VRA-BTC,VRA-USDT,KSM-BTC,KSM-USDT,DASH-USDT,SUTER-USDT,ACOIN-USDT,SUTER-BTC,SENSO-USDT,PRE-BTC,XDB-USDT,SYLO-USDT,WOM-USDT,SENSO-BTC,DGB-USDT,LYXE-USDT,LYXE-ETH,XDB-BTC,STX-BTC,STX-USDT,XSR-USDT,COMP-USDT,CRO-USDT,KAI-USDT,KAI-BTC,WEST-BTC,WEST-USDT,EWT-BTC,WAVES-USDT,WAVES-BTC,ORN-USDT,AMPL-ETH,BNS-USDT,MKR-USDT,SUKU-BTC,MLK-BTC,MLK-USDT,JST-USDT,KAI-ETH,SUKU-USDT,DIA-USDT,DIA-BTC,LINK-BTC,LINK-USDT,DOT-USDT,DOT-BTC,SHA-BTC,SHA-USDT,EWT-USDT,USDJ-USDT,EFX-BTC,CKB-BTC,CKB-USDT,UMA-USDT,ALEPH-USDT,VELO-USDT,SUN-USDT,BUY-USDT,YFI-USDT,OXEN-USDT,UNI-USDT,UOS-USDT,UOS-BTC,NIM-USDT,DEGO-USDT,DEGO-ETH,UDOO-ETH,RFUEL-USDT,FIL-USDT,UBX-ETH,REAP-USDT,AAVE-USDT,AAVE-BTC,TONE-BTC,TONE-ETH,ELF-ETH,AERGO-BTC,IOST-ETH,KCS-USDT,SNX-ETH,TOMO-ETH,KCS-ETH,DRGN-BTC,WAN-ETH,NULS-ETH,AXPR-ETH,POWR-BTC,QTUM-BTC,MANA-BTC,TEL-BTC,XYO-ETH,AXPR-BTC,ETN-BTC,COV-ETH,VET-BTC,KCS-BTC,CAPP-ETH,ONT-BTC,DRGN-ETH,DAG-ETH,TOMO-BTC,WAN-BTC,KNC-ETH,CRPT-ETH,LTC-USDT,BAX-ETH,BSV-USDT,DENT-ETH,AION-ETH,LYM-ETH,TRAC-ETH,ENJ-BTC,WAXP-BTC,DGB-BTC,ELA-BTC,ZIL-BTC,BSV-BTC,XLM-USDT,IOTX-ETH,SOUL-BTC,DOCK-BTC,AMB-ETH,TRX-BTC,XRP-TUSD,NULS-BTC,ETH-DAI,LSK-BTC,GMB-ETH,GMB-BTC,NEO-ETH,OMG-ETH,BTC-TUSD,KAT-USDT,KNC-BTC,ELF-BTC,MANA-ETH,ETC-USDT,ONT-ETH,MKR-BTC,KAT-BTC,XRP-USDC,XYO-BTC,SNT-ETH,ZRX-BTC,LOOM-ETH,AION-BTC,POWR-ETH,OLT-ETH,OLT-BTC,SNT-BTC,TRAC-BTC,XLM-ETH,ETH-USDT,BSV-ETH,TRX-ETH,ETN-ETH,AOA-USDT,BCD-BTC,DENT-BTC,DOCK-ETH,KEY-BTC,EOS-KCS,XLM-BTC,ADB-ETH,TIME-ETH,CVC-BTC,LSK-ETH,QKC-BTC,AMB-BTC,USDT-TUSD,ETC-ETH,XRP-BTC,NEO-KCS,SNX-USDT,CRPT-BTC,IOTX-BTC,LTC-ETH,XRP-KCS,ADB-BTC,LTC-KCS,TEL-ETH,DCR-ETH,LYM-USDT,USDT-USDC,ETH-USDC,DAG-BTC,AVA-BTC,BTC-USDT,WAXP-ETH,XRP-USDT,KEY-ETH,VET-ETH,FTM-BTC,USDT-DAI,QKC-ETH,ETH-BTC,MAN-BTC,CPC-ETH,TRX-USDT,BTC-DAI,ONT-USDT,DASH-ETH,BAX-BTC,AVA-ETH,LOOM-BTC,MVP-BTC,MKR-ETH,COV-BTC,CPC-BTC,REQ-ETH,EOS-BTC,LTC-BTC,XRP-ETH,CAPP-BTC,FTM-ETH,BCD-ETH,ZRX-ETH,DGB-ETH,VET-USDT,REQ-BTC,UTK-BTC,PLAY-BTC,UTK-ETH,SNX-BTC,MVP-ETH,NEO-BTC,SOUL-ETH,NEO-USDT,ELA-ETH,OMG-BTC,TIME-BTC,AOA-BTC,ETC-BTC,DCR-BTC,BTC-USDC,ENJ-ETH,IOST-BTC,DASH-BTC,EOS-USDT,EOS-ETH,ZIL-ETH,ETH-TUSD,GAS-BTC,LYM-BTC,BCH-BTC,VSYS-BTC,BCH-USDT,MKR-DAI,SOLVE-BTC,GRIN-BTC,GRIN-USDT,UQC-BTC,UQC-ETH,OPCT-BTC,OPCT-ETH,PRE-USDT,SHR-BTC,SHR-USDT,UBXT-USDT,ROSE-USDT,USDC-USDT,CTI-USDT,CTI-ETH,ETH2-ETH,BUX-BTC,XHV-USDT,PLU-USDT,GRT-USDT,CAS-BTC,CAS-USDT,MSWAP-BTC,MSWAP-USDT,GOM2-BTC,GOM2-USDT,REVV-BTC,REVV-USDT,LON-USDT,1INCH-USDT,LOC-USDT,API3-USDT,UNFI-USDT,HTR-USDT,FRONT-USDT,FRONT-BTC,WBTC-BTC,WBTC-ETH,MIR-USDT,LTC-USDC,BCH-USDC,HYDRA-USDT,DFI-USDT,DFI-BTC,CRV-USDT,SUSHI-USDT,FRM-USDT,EOS-USDC,BSV-USDC,ZEN-USDT,CUDOS-USDT,ADA-USDC,REN-USDT,LRC-USDT,LINK-USDC,KLV-USDT,KLV-BTC,BOA-USDT,THETA-USDT,QNT-USDT,BAT-USDT,DOGE-USDT,DOGE-USDC,DAO-USDT,STRONG-USDT,TRIAS-USDT,TRIAS-BTC,DOGE-BTC,MITX-BTC,MITX-USDT,CAKE-USDT,ORAI-USDT,ZEE-USDT,LTX-USDT,LTX-BTC,MASK-USDT,KLV-TRX,IDEA-USDT,PHA-USDT,PHA-ETH,BCH-KCS,SRK-USDT,SRK-BTC,ADA-KCS,HTR-BTC,BSV-KCS,DOT-KCS,LINK-KCS,MIR-KCS,BNB-KCS,XLM-KCS,VET-KCS,SWINGBY-USDT,SWINGBY-BTC,XHV-BTC,DASH-KCS,UNI-KCS,AAVE-KCS,DOGE-KCS,ZEC-KCS,XTZ-KCS,GRT-KCS,ALGO-KCS,EWT-KCS,GAS-USDT,AVAX-USDT,AVAX-BTC,KRL-BTC,KRL-USDT,POLK-USDT,POLK-BTC,ENJ-USDT,MANA-USDT,RNDR-USDT,RNDR-BTC,RLY-USDT,ANC-USDT,SKEY-USDT,LAYER-USDT,TARA-USDT,TARA-ETH,IOST-USDT,DYP-USDT,DYP-ETH,XYM-USDT,XYM-BTC,PCX-USDT,PCX-BTC,ORBS-USDT,ORBS-BTC,BTC3L-USDT,BTC3S-USDT,ETH3L-USDT,ETH3S-USDT,ANKR-USDT,DSLA-USDT,DSLA-BTC,SAND-USDT,VAI-USDT,XCUR-USDT,XCUR-BTC,FLUX-USDT,OMG-USDT,ZIL-USDT,DODO-USDT,MAN-USDT,BAX-USDT,BOSON-USDT,BOSON-ETH,PUNDIX-USDT,PUNDIX-BTC,WAXP-USDT,HT-USDT,PDEX-USDT,LABS-USDT,LABS-ETH,GMB-USDT,PHNX-USDT,PHNX-BTC,HAI-USDT,EQZ-USDT,FORTH-USDT,HORD-USDT,CGG-USDT,UBX-USDT,GHX-USDT,TCP-USDT,STND-USDT,STND-ETH,TOWER-USDT,TOWER-BTC,ACE-USDT,LOCG-USDT,CARD-USDT,FLY-USDT,CWS-USDT,XDC-USDT,XDC-ETH,STRK-BTC,STRK-ETH,SHIB-USDT,POLX-USDT,KDA-USDT,KDA-BTC,ICP-USDT,ICP-BTC,STC-USDT,STC-BTC,GOVI-USDT,GOVI-BTC,FKX-USDT,CELO-USDT,CELO-BTC,CUSD-USDT,CUSD-BTC,FCL-USDT,MATIC-USDT,MATIC-BTC,ELA-USDT,CRPT-USDT,OPCT-USDT,OGN-USDT,OGN-BTC,OUSD-USDT,OUSD-BTC,TLOS-USDT,TLOS-BTC,YOP-USDT,YOP-ETH,GLQ-USDT,GLQ-BTC,MXC-USDT,ERSDL-USDT,HOTCROSS-USDT,ADA3L-USDT,ADA3S-USDT,HYVE-USDT,HYVE-BTC,DAPPX-USDT,KONO-USDT,PRQ-USDT,MAHA-USDT,MAHA-BTC,FEAR-USDT,PYR-USDT,PYR-BTC,PROM-USDT,PROM-BTC,GLCH-USDT,UNO-USDT,ALBT-USDT,ALBT-ETH,XCAD-USDT,EOS3L-USDT,EOS3S-USDT,BCH3L-USDT,BCH3S-USDT,ELON-USDT,APL-USDT,FCL-ETH,VEED-USDT,VEED-BTC,DIVI-USDT,PDEX-BTC,JUP-USDT,JUP-ETH,POLS-USDT,POLS-BTC,LPOOL-USDT,LPOOL-BTC,LSS-USDT,VET3L-USDT,VET3S-USDT,LTC3L-USDT,LTC3S-USDT,ABBC-USDT,ABBC-BTC,KOK-USDT,ROSN-USDT,DORA-USDT,DORA-BTC,ZCX-USDT,ZCX-BTC,NORD-USDT,GMEE-USDT,SFUND-USDT,XAVA-USDT,AI-USDT,ALPACA-USDT,IOI-USDT,NFT-USDT,NFT-TRX,MNST-USDT,MEM-USDT,AGIX-USDT,AGIX-BTC,AGIX-ETH,CQT-USDT,AIOZ-USDT,MARSH-USDT,HAPI-USDT,MODEFI-USDT,MODEFI-BTC,YFDAI-USDT,YFDAI-BTC,GENS-USDT,FORM-USDT,ARRR-USDT,ARRR-BTC,TOKO-KCS,EXRD-USDT,NGM-USDT,LPT-USDT,STMX-USDT,ASD-USDT,BOND-USDT,HAI-BTC,SOUL-USDT,2CRZ-USDT,NEAR-USDT,NEAR-BTC,DFYN-USDT,OOE-USDT,CFG-USDT,CFG-BTC,AXS-USDT,CLV-USDT,ROUTE-USDT,KAR-USDT,EFX-USDT,XDC-BTC,SHFT-USDT,PMON-USDT,DPET-USDT,ERG-USDT,ERG-BTC,SOL-USDT,SLP-USDT,LITH-USDT,LITH-ETH,XCH-USDT,HAKA-USDT,LAYER-BTC,MTL-USDT,MTL-BTC,IOTX-USDT,GALA-USDT,REQ-USDT,TXA-USDT,TXA-USDC,CIRUS-USDT,QI-USDT,QI-BTC,ODDZ-USDT,PNT-USDT,PNT-BTC,XPR-USDT,XPR-BTC,TRIBE-USDT,SHFT-BTC,MOVR-USDT,MOVR-ETH,WOO-USDT,WILD-USDT,QRDO-USDT,QRDO-ETH,SDN-USDT,SDN-ETH,MAKI-USDT,MAKI-BTC,REP-USDT,REP-BTC,REP-ETH,BNT-USDT,BNT-BTC,BNT-ETH,OXT-USDT,OXT-BTC,OXT-ETH,BAL-USDT,BAL-BTC,BAL-ETH,STORJ-USDT,STORJ-BTC,STORJ-ETH,YGG-USDT,NDAU-USDT,SDAO-USDT,SDAO-ETH,XRP3L-USDT,XRP3S-USDT,SKL-USDT,SKL-BTC,NMR-USDT,NMR-BTC,IXS-USDT,TRB-USDT,TRB-BTC,DYDX-USDT,XYO-USDT,GTC-USDT,GTC-BTC,EQX-USDT,EQX-BTC,RLC-USDT,RLC-BTC,XPRT-USDT,EGLD-USDT,EGLD-BTC,HBAR-USDT,HBAR-BTC,DOGE3L-USDT,DOGE3S-USDT,FLOW-USDT,FLOW-BTC,NKN-USDT,NKN-BTC,PBX-USDT,SOL3L-USDT,SOL3S-USDT,MLN-USDT,MLN-BTC,XNL-USDT,SOLVE-USDT,WNCG-USDT,WNCG-BTC,DMTR-USDT,LINK3L-USDT,LINK3S-USDT,DOT3L-USDT,DOT3S-USDT,CTSI-USDT,CTSI-BTC,ALICE-USDT,ALICE-BTC,ALICE-ETH,OPUL-USDT,ILV-USDT,BAND-USDT,BAND-BTC,FTT-USDT,FTT-BTC,DVPN-USDT,SKU-USDT,SKU-BTC,EDG-USDT,SLIM-USDT,TLM-USDT,TLM-BTC,TLM-ETH,DEXE-USDT,DEXE-BTC,DEXE-ETH,MATTER-USDT,CUDOS-BTC,RUNE-USDT,RUNE-BTC,RMRK-USDT,BMON-USDT,C98-USDT,BLOK-USDT,SOLR-USDT,ATOM3L-USDT,ATOM3S-USDT,UNI3L-USDT,UNI3S-USDT,WSIENNA-USDT,PUSH-USDT,PUSH-BTC,FORM-ETH,NTVRK-USDT,NTVRK-USDC,AXS3L-USDT,AXS3S-USDT,FTM3L-USDT,FTM3S-USDT,FLAME-USDT,AGLD-USDT,NAKA-USDT,YLD-USDT,TONE-USDT,REEF-USDT,REEF-BTC,TIDAL-USDT,TVK-USDT,TVK-BTC,INJ-USDT,INJ-BTC,BNB3L-USDT,BNB3S-USDT,MATIC3L-USDT,MATIC3S-USDT,NFTB-USDT,VEGA-USDT,VEGA-ETH,ALPHA-USDT,ALPHA-BTC,BADGER-USDT,BADGER-BTC,UNO-BTC,ZKT-USDT,AR-USDT,AR-BTC,XVS-USDT,XVS-BTC,JASMY-USDT,PERP-USDT,PERP-BTC,GHST-USDT,GHST-BTC,SCLP-USDT,SCLP-BTC,SUPER-USDT,SUPER-BTC,CPOOL-USDT,HERO-USDT,BASIC-USDT,XED-USDT,XED-BTC,AURY-USDT,SWASH-USDT,LTO-USDT,LTO-BTC,BUX-USDT,MTRG-USDT,DREAMS-USDT,SHIB-DOGE,QUICK-USDT,QUICK-BTC,TRU-USDT,TRU-BTC,WRX-USDT,WRX-BTC,TKO-USDT,TKO-BTC,SUSHI3L-USDT,SUSHI3S-USDT,NEAR3L-USDT,NEAR3S-USDT,DATA-USDT,DATA-BTC,NORD-BTC,ISP-USDT,CERE-USDT,SHILL-USDT,HEGIC-USDT,HEGIC-BTC,ERN-USDT,ERN-BTC,FTG-USDT,PAXG-USDT,PAXG-BTC,AUDIO-USDT,AUDIO-BTC,ENS-USDT,AAVE3L-USDT,AAVE3S-USDT,SAND3L-USDT,SAND3S-USDT,XTM-USDT,MNW-USDT,FXS-USDT,FXS-BTC,ATA-USDT,ATA-BTC,VXV-USDT,LRC-BTC,LRC-ETH,DPR-USDT,CWAR-USDT,CWAR-BTC,FLUX-BTC,EDG-BTC,PBR-USDT,WNXM-USDT,WNXM-BTC,ANT-USDT,ANT-BTC,COV-USDT,SWP-USDT,TWT-USDT,TWT-BTC,OM-USDT,OM-BTC,ADX-USDT,AVAX3L-USDT,AVAX3S-USDT,MANA3L-USDT,MANA3S-USDT,GLM-USDT,GLM-BTC,BAKE-USDT,BAKE-BTC,BAKE-ETH,NUM-USDT,VLX-USDT,VLX-BTC,TRADE-USDT,TRADE-BTC,1EARTH-USDT,MONI-USDT,LIKE-USDT,MFT-USDT,MFT-BTC,LIT-USDT,LIT-BTC,KAVA-USDT,SFP-USDT,SFP-BTC,BURGER-USDT,BURGER-BTC,ILA-USDT,CREAM-USDT,CREAM-BTC,RSR-USDT,RSR-BTC,BUY-BTC,IMX-USDT,GODS-USDT,KMA-USDT,SRM-USDT,SRM-BTC,POLC-USDT,XTAG-USDT,MNET-USDT,NGC-USDT,HARD-USDT,GALAX3L-USDT,GALAX3S-USDT,UNIC-USDT,POND-USDT,POND-BTC,VR-USDT,EPIK-USDT,NGL-USDT,NGL-BTC,KDON-USDT,PEL-USDT,CIRUS-ETH,LINA-USDT,LINA-BTC,KLAY-USDT,KLAY-BTC,CREDI-USDT,TRVL-USDT,LACE-USDT,LACE-ETH,ARKER-USDT,BONDLY-USDT,BONDLY-ETH,XEC-USDT,HEART-USDT,HEART-BTC,UNB-USDT,GAFI-USDT,KOL-USDT,KOL-ETH,H3RO3S-USDT,FALCONS-USDT,UFO-USDT,CHMB-USDT,GEEQ-USDT,ORC-USDT,RACEFI-USDT,PEOPLE-USDT,ADS-USDT,ADS-BTC,OCEAN-USDT,SOS-USDT,WHALE-USDT,TIME-USDT,CWEB-USDT,IOTA-USDT,IOTA-BTC,OOKI-USDT,OOKI-BTC,HNT-USDT,HNT-BTC,GGG-USDT,POWR-USDT,REVU-USDT,CLH-USDT,PLGR-USDT,GLMR-USDT,GLMR-BTC,LOVE-USDT,CTC-USDT,CTC-BTC,GARI-USDT,FRR-USDT,ASTR-USDT,ASTR-BTC,ERTHA-USDT,FCON-USDT,ACA-USDT,ACA-BTC,MTS-USDT,ROAR-USDT,HBB-USDT,SURV-USDT,CVX-USDT,AMP-USDT,ACT-USDT,MJT-USDT,MJT-KCS,SHX-USDT,SHX-BTC,STARLY-USDT,ONSTON-USDT,RANKER-USDT,WMT-USDT,XNO-USDT,XNO-BTC,MARS4-USDT,TFUEL-USDT,TFUEL-BTC,METIS-USDT,LAVAX-USDT,WAL-USDT,BULL-USDT,SON-USDT,MELOS-USDT,APE-USDT,GMT-USDT,BICO-USDT,STG-USDT,LMR-USDT,LMR-BTC,LOKA-USDT,URUS-USDT,JAM-USDT,JAM-ETH,BNC-USDT,LBP-USDT,CFX-USDT,LOOKS-USDT,XCN-USDT,XCN-BTC,KP3R-USDT,TITAN-USDT,INDI-USDT,UPO-USDT,SPELL-USDT,SLCL-USDT,CEEK-USDT,VEMP-USDT,BETA-USDT,NHCT-USDT,ARNM-USDT,FRA-USDT,VISION-USDT,COCOS-USDT,ALPINE-USDT,BNX-USDT,ZBC-USDT,WOOP-USDT,T-USDT,NYM-USDT,VOXEL-USDT,VOXEL-ETH,PSTAKE-USDT,SPA-USDT,SPA-ETH,SYNR-USDT,DAR-USDT,DAR-BTC,MV-USDT,XDEFI-USDT,RACA-USDT,XWG-USDT,HAWK-USDT,TRVL-BTC,SWFTC-USDT,IDEX-USDT,BRWL-USDT,PLATO-USDT,TAUM-USDT,CELR-USDT,AURORA-USDT,POSI-USDT,COOHA-USDT,KNC-USDT,EPK-USDT,PLD-USDT,PSL-USDT,PKF-USDT,OVR-USDT,SYS-USDT,SYS-BTC,BRISE-USDT,DG-USDT,EPX-USDT,GST-USDT,PLY-USDT,GAL-USDT,BSW-USDT,FITFI-USDT,FSN-USDT,H2O-USDT,GMM-USDT,AKT-USDT,SIN-USDT,AUSD-USDT,BOBA-USDT,KARA-USDT,BFC-USDT,BIFI-USDT,DFA-USDT,KYL-USDT,FCD-USDT,MBL-USDT,CELT-USDT,DUSK-USDT,USDD-USDT,USDD-USDC,FITFI-USDC,MBOX-USDT,MBOX-BTC,APE-USDC,AVAX-USDC,SHIB-USDC,XCN-USDC,TRX-USDC,NEAR-USDC,MATIC-USDC,FTM-USDC,ZIL-USDC,SOL-USDC,MLS-USDT,AFK-USDT,AFK-USDC,ACH-USDT,SCRT-USDT,SCRT-BTC,APE3L-USDT,APE3S-USDT,STORE-USDT,STORE-ETH,GMT3L-USDT,GMT3S-USDT,CCD-USDT,DOSE-USDC,LUNC-USDT,LUNC-USDC,USTC-USDT,USTC-USDC,GMT-USDC,VRA-USDC,DOT-USDC,RUNE-USDC,ATOM-USDC,BNB-USDC,JASMY-USDC,KCS-USDC,KDA-USDC,ALGO-USDC,LUNA-USDC,OP-USDT,OP-USDC,JASMY3L-USDT,JASMY3S-USDT,EVER-USDT,MOOV-USDT,IHC-USDT,ICX-USDT,ICX-ETH,BTC-BRL,ETH-BRL,USDT-BRL,WELL-USDT,FORT-USDT,USDP-USDT,USDD-TRX,CSPR-USDT,CSPR-ETH,WEMIX-USDT,REV3L-USDT,OLE-USDT,LDO-USDT,LDO-USDC,CULT-USDT,SWFTC-USDC,FIDA-USDT,BUSD-USDT,RBP-USDT,SRBP-USDT,HIBAYC-USDT,BUSD-USDC,OGV-USDT,WOMBAT-USDT,HIPUNKS-USDT,FT-USDT,ETC-USDC,HIENS4-USDT,EGAME-USDT,EGAME-BTC,STEPWATCH-USDT,HISAND33-USDT,DC-USDT,NEER-USDT,RVN-USDT,HIENS3-USDT,MC-USDT,PEEL-USDT,PEEL-BTC,SDL-USDT,SDL-BTC,SWEAT-USDT,HIODBS-USDT,CMP-USDT,PIX-USDT,MPLX-USDT,HIDOODLES-USDT,ETHW-USDT,QUARTZ-USDT,ACQ-USDT,ACQ-USDC,AOG-USDT,HIMAYC-USDT,PRMX-USDT,RED-USDT,PUMLX-USDT,XETA-USDT,GEM-USDT,DERC-USDT,P00LS-USDT,P00LS-USDC,KICKS-USDT,TRIBL-USDT,GMX-USDT,HIOD-USDT,POKT-USDT,EFI-USDT,APT-USDT,BBC-USDT,EUL-USDT,TON-USDT,PIAS-USDT,HIMEEBITS-USDT,HISQUIGGLE-USDT,XCV-USDT,HFT-USDT,HFT-USDC,ECOX-USDT,AMB-USDT,AZERO-USDT,HIFIDENZA-USDT,BEAT-USDT", + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + } + }, + "futures": { + "assetEnabled": true, + "enabled": "ETH_USDCM,XBT_USDCM,SOL_USDTM", + "available": "SDTM,SUSHI_USDTM,XLM_USDTM,1INCH_USDTM,ZEC_USDTM,DASH_USDTM,DOT_USDM,XRP_USDM,AAVE_USDTM,KSM_USDTM,DOGE_USDTM,VET_USDTM,BNB_USDTM,SXP_USDTM,SOL_USDTM,CRV_USDTM,ALGO_USDTM,AVAX_USDTM,FTM_USDTM,MATIC_USDTM,THETA_USDTM,ATOM_USDTM,CHZ_USDTM,ENJ_USDTM,MANA_USDTM,DENT_USDTM,OCEAN_USDTM,BAT_USDTM,XEM_USDTM,QTUM_USDTM,XTZ_USDTM,SNX_USDTM,NEO_USDTM,ONT_USDTM,XMR_USDTM,COMP_USDTM,ETC_USDTM,WAVES_USDTM,BAND_USDTM,MKR_USDTM,RVN_USDTM,DGB_USDTM,SHIB_USDTM,ICP_USDTM,DYDX_USDTM,AXS_USDTM,HBAR_USDTM,EGLD_USDTM,ALICE_USDTM,YGG_USDTM,NEAR_USDTM,SAND_USDTM,C98_USDTM,ONE_USDTM,VRA_USDTM,GALA_USDTM,CHR_USDTM,LRC_USDTM,FLOW_USDTM,RNDR_USDTM,IOTX_USDTM,CRO_USDTM,WAXP_USDTM,PEOPLE_USDTM,OMG_USDTM,LINA_USDTM,IMX_USDTM,CELR_USDTM,ENS_USDTM,CELO_USDTM,CTSI_USDTM,ARPA_USDTM,KNC_USDTM,ROSE_USDTM,AGLD_USDTM,APE_USDTM,JASMY_USDTM,ZIL_USDTM,GMT_USDTM,RUNE_USDTM,LOOKS_USDTM,AUDIO_USDTM,KDA_USDTM,KAVA_USDTM,BAL_USDTM,GAL_USDTM,LUNA_USDTM,LUNC_USDTM,OP_USDTM,XCN_USDTM,UNFI_USDTM,LIT_USDTM,DUSK_USDTM,STORJ_USDTM,RSR_USDTM,OGN_USDTM,TRB_USDTM,PERP_USDTM,KLAY_USDTM,ANKR_USDTM,LDO_USDTM,WOO_USDTM,REN_USDTM,CVC_USDTM,INJ_USDTM,APT_USDTM,MASK_USDTM,REEF_USDTM,TON_USDTM,MAGIC_USDTM,CFX_USDTM,AGIX_USDTM,FXS_USDTM,FET_USDTM,AR_USDTM,GMX_USDTM,BLUR_USDTM,ASTR_USDTM,HIGH_USDTM,ACH_USDTM,STX_USDTM,SSV_USDTM,FLOKI_USDTM,CKB_USDTM,TRU_USDTM,QNT_USDTM,ETH_USDCM,MINA_USDTM,USDC_USDTM,T_USDTM,LQTY_USDTM,ARB_USDTM,DAR_USDTM,ID_USDTM,STG_USDTM,JOE_USDTM,RDNT_USDTM,DODO_USDTM,PAXG_USDTM,ZRX_USDTM,ICX_USDTM,HFT_USDTM,NKN_USDTM,HOOK_USDTM,ANT_USDTM,DC_USDTM,BEL_USDTM,SUI_USDTM,PEPE_USDTM,IDEX_USDTM,GNS_USDTM,CETUS_USDTM,KAS_USDTM,ORDI_USDTM,WOJAK_USDTM,POGAI_USDTM,UMA_USDTM,RAD_USDTM,XBT_USDCM,PHB_USDTM,FTT_USDTM,10000LADYS_USDTM,LEVER_USDTM,TURBO_USDTM,TOMO_USDTM,BOB_USDTM,KEY_USDTM,EDU_USDTM,MTL_USDTM,FLUX_USDTM,COMBO_USDTM,AMB_USDTM,ALPHA_USDTM,SFP_USDTM,MAV_USDTM,MDT_USDTM,XEC_USDTM,XVG_USDTM,1000PEPE2_USDTM,PENDLE_USDTM,STMX_USDTM,WLD_USDTM,LPT_USDTM,GTC_USDTM,BNT_USDTM,OXT_USDTM,BLZ_USDTM,SEI_USDTM,BAKE_USDTM,CYBER_USDTM,NMR_USDTM,FLM_USDTM,SPELL_USDTM,ARK_USDTM,XBT_MU23,XBT_MZ23", + "requestFormat": { + "uppercase": true, + "delimiter": "" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + } + } + } + }, + "api": { + "authenticatedSupport": true, + "authenticatedWebsocketApiSupport": true, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret", + "clientID": "ClientID" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": true - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "Kucoin", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 150000000000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "assetTypes": [ - "spot", - "margin", - "futures" - ], - "pairs": { + "name": "Okcoin", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "-" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { "spot": { - "assetEnabled": true, - "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,XMR-BTC", - "available": "BTC-USDT,MHC-ETH,MHC-BTC,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MHC-USDT,XMR-BTC,XMR-ETH,RIF-BTC,MTV-BTC,MTV-ETH,CRO-BTC,MTV-USDT,KMD-BTC,KMD-USDT,RFOX-USDT,TEL-USDT,TT-USDT,AERGO-USDT,XMR-USDT,TRX-KCS,ATOM-BTC,ATOM-ETH,ATOM-USDT,ATOM-KCS,ETN-USDT,FTM-USDT,TOMO-USDT,VSYS-USDT,OCEAN-BTC,OCEAN-ETH,CHR-BTC,CHR-USDT,FX-BTC,FX-ETH,NIM-BTC,NIM-ETH,COTI-BTC,COTI-USDT,NRG-ETH,BNB-BTC,BNB-USDT,JAR-BTC,JAR-USDT,ALGO-BTC,ALGO-ETH,ALGO-USDT,XEM-BTC,XEM-USDT,CIX100-USDT,XTZ-BTC,XTZ-USDT,ZEC-BTC,ZEC-USDT,ADA-BTC,ADA-USDT,REV-USDT,WXT-BTC,WXT-USDT,FORESTPLUS-BTC,FORESTPLUS-USDT,BOLT-BTC,BOLT-USDT,ARPA-USDT,CHZ-BTC,CHZ-USDT,DAPPT-BTC,DAPPT-USDT,NOIA-BTC,NOIA-USDT,WIN-BTC,WIN-USDT,DERO-BTC,DERO-USDT,BTT-USDT,EOSC-USDT,ENQ-BTC,ENQ-USDT,ONE-BTC,ONE-USDT,TOKO-BTC,TOKO-USDT,VID-BTC,VID-USDT,LUNA-USDT,SXP-BTC,SXP-USDT,AKRO-BTC,AKRO-USDT,ROOBEE-BTC,WIN-TRX,MAP-BTC,MAP-USDT,AMPL-BTC,AMPL-USDT,DAG-USDT,POL-USDT,ARX-USDT,NWC-BTC,NWC-USDT,BEPRO-BTC,BEPRO-USDT,VRA-BTC,VRA-USDT,KSM-BTC,KSM-USDT,DASH-USDT,SUTER-USDT,ACOIN-USDT,SUTER-BTC,SENSO-USDT,PRE-BTC,XDB-USDT,SYLO-USDT,WOM-USDT,SENSO-BTC,DGB-USDT,LYXE-USDT,LYXE-ETH,XDB-BTC,STX-BTC,STX-USDT,XSR-USDT,COMP-USDT,CRO-USDT,KAI-USDT,KAI-BTC,WEST-BTC,WEST-USDT,EWT-BTC,WAVES-USDT,WAVES-BTC,ORN-USDT,AMPL-ETH,BNS-USDT,MKR-USDT,SUKU-BTC,MLK-BTC,MLK-USDT,JST-USDT,KAI-ETH,SUKU-USDT,DIA-USDT,DIA-BTC,LINK-BTC,LINK-USDT,DOT-USDT,DOT-BTC,SHA-BTC,SHA-USDT,EWT-USDT,USDJ-USDT,EFX-BTC,CKB-BTC,CKB-USDT,UMA-USDT,ALEPH-USDT,VELO-USDT,SUN-USDT,BUY-USDT,YFI-USDT,OXEN-USDT,UNI-USDT,UOS-USDT,UOS-BTC,NIM-USDT,DEGO-USDT,DEGO-ETH,UDOO-ETH,RFUEL-USDT,FIL-USDT,UBX-ETH,REAP-USDT,AAVE-USDT,AAVE-BTC,TONE-BTC,TONE-ETH,ELF-ETH,AERGO-BTC,IOST-ETH,KCS-USDT,SNX-ETH,TOMO-ETH,KCS-ETH,DRGN-BTC,WAN-ETH,NULS-ETH,AXPR-ETH,POWR-BTC,QTUM-BTC,MANA-BTC,TEL-BTC,XYO-ETH,AXPR-BTC,ETN-BTC,COV-ETH,VET-BTC,KCS-BTC,CAPP-ETH,ONT-BTC,DRGN-ETH,DAG-ETH,TOMO-BTC,WAN-BTC,KNC-ETH,CRPT-ETH,LTC-USDT,BAX-ETH,BSV-USDT,DENT-ETH,AION-ETH,LYM-ETH,TRAC-ETH,ENJ-BTC,WAXP-BTC,DGB-BTC,ELA-BTC,ZIL-BTC,BSV-BTC,XLM-USDT,IOTX-ETH,SOUL-BTC,DOCK-BTC,AMB-ETH,TRX-BTC,XRP-TUSD,NULS-BTC,ETH-DAI,LSK-BTC,GMB-ETH,GMB-BTC,NEO-ETH,OMG-ETH,BTC-TUSD,KAT-USDT,KNC-BTC,ELF-BTC,MANA-ETH,ETC-USDT,ONT-ETH,MKR-BTC,KAT-BTC,XRP-USDC,XYO-BTC,SNT-ETH,ZRX-BTC,LOOM-ETH,AION-BTC,POWR-ETH,OLT-ETH,OLT-BTC,SNT-BTC,TRAC-BTC,XLM-ETH,ETH-USDT,BSV-ETH,TRX-ETH,ETN-ETH,AOA-USDT,BCD-BTC,DENT-BTC,DOCK-ETH,KEY-BTC,EOS-KCS,XLM-BTC,ADB-ETH,TIME-ETH,CVC-BTC,LSK-ETH,QKC-BTC,AMB-BTC,USDT-TUSD,ETC-ETH,XRP-BTC,NEO-KCS,SNX-USDT,CRPT-BTC,IOTX-BTC,LTC-ETH,XRP-KCS,ADB-BTC,LTC-KCS,TEL-ETH,DCR-ETH,LYM-USDT,USDT-USDC,ETH-USDC,DAG-BTC,AVA-BTC,BTC-USDT,WAXP-ETH,XRP-USDT,KEY-ETH,VET-ETH,FTM-BTC,USDT-DAI,QKC-ETH,ETH-BTC,MAN-BTC,CPC-ETH,TRX-USDT,BTC-DAI,ONT-USDT,DASH-ETH,BAX-BTC,AVA-ETH,LOOM-BTC,MVP-BTC,MKR-ETH,COV-BTC,CPC-BTC,REQ-ETH,EOS-BTC,LTC-BTC,XRP-ETH,CAPP-BTC,FTM-ETH,BCD-ETH,ZRX-ETH,DGB-ETH,VET-USDT,REQ-BTC,UTK-BTC,PLAY-BTC,UTK-ETH,SNX-BTC,MVP-ETH,NEO-BTC,SOUL-ETH,NEO-USDT,ELA-ETH,OMG-BTC,TIME-BTC,AOA-BTC,ETC-BTC,DCR-BTC,BTC-USDC,ENJ-ETH,IOST-BTC,DASH-BTC,EOS-USDT,EOS-ETH,ZIL-ETH,ETH-TUSD,GAS-BTC,LYM-BTC,BCH-BTC,VSYS-BTC,BCH-USDT,MKR-DAI,SOLVE-BTC,GRIN-BTC,GRIN-USDT,UQC-BTC,UQC-ETH,OPCT-BTC,OPCT-ETH,PRE-USDT,SHR-BTC,SHR-USDT,UBXT-USDT,ROSE-USDT,USDC-USDT,CTI-USDT,CTI-ETH,ETH2-ETH,BUX-BTC,XHV-USDT,PLU-USDT,GRT-USDT,CAS-BTC,CAS-USDT,MSWAP-BTC,MSWAP-USDT,GOM2-BTC,GOM2-USDT,REVV-BTC,REVV-USDT,LON-USDT,1INCH-USDT,LOC-USDT,API3-USDT,UNFI-USDT,HTR-USDT,FRONT-USDT,FRONT-BTC,WBTC-BTC,WBTC-ETH,MIR-USDT,LTC-USDC,BCH-USDC,HYDRA-USDT,DFI-USDT,DFI-BTC,CRV-USDT,SUSHI-USDT,FRM-USDT,EOS-USDC,BSV-USDC,ZEN-USDT,CUDOS-USDT,ADA-USDC,REN-USDT,LRC-USDT,LINK-USDC,KLV-USDT,KLV-BTC,BOA-USDT,THETA-USDT,QNT-USDT,BAT-USDT,DOGE-USDT,DOGE-USDC,DAO-USDT,STRONG-USDT,TRIAS-USDT,TRIAS-BTC,DOGE-BTC,MITX-BTC,MITX-USDT,CAKE-USDT,ORAI-USDT,ZEE-USDT,LTX-USDT,LTX-BTC,MASK-USDT,KLV-TRX,IDEA-USDT,PHA-USDT,PHA-ETH,BCH-KCS,SRK-USDT,SRK-BTC,ADA-KCS,HTR-BTC,BSV-KCS,DOT-KCS,LINK-KCS,MIR-KCS,BNB-KCS,XLM-KCS,VET-KCS,SWINGBY-USDT,SWINGBY-BTC,XHV-BTC,DASH-KCS,UNI-KCS,AAVE-KCS,DOGE-KCS,ZEC-KCS,XTZ-KCS,GRT-KCS,ALGO-KCS,EWT-KCS,GAS-USDT,AVAX-USDT,AVAX-BTC,KRL-BTC,KRL-USDT,POLK-USDT,POLK-BTC,ENJ-USDT,MANA-USDT,RNDR-USDT,RNDR-BTC,RLY-USDT,ANC-USDT,SKEY-USDT,LAYER-USDT,TARA-USDT,TARA-ETH,IOST-USDT,DYP-USDT,DYP-ETH,XYM-USDT,XYM-BTC,PCX-USDT,PCX-BTC,ORBS-USDT,ORBS-BTC,BTC3L-USDT,BTC3S-USDT,ETH3L-USDT,ETH3S-USDT,ANKR-USDT,DSLA-USDT,DSLA-BTC,SAND-USDT,VAI-USDT,XCUR-USDT,XCUR-BTC,FLUX-USDT,OMG-USDT,ZIL-USDT,DODO-USDT,MAN-USDT,BAX-USDT,BOSON-USDT,BOSON-ETH,PUNDIX-USDT,PUNDIX-BTC,WAXP-USDT,HT-USDT,PDEX-USDT,LABS-USDT,LABS-ETH,GMB-USDT,PHNX-USDT,PHNX-BTC,HAI-USDT,EQZ-USDT,FORTH-USDT,HORD-USDT,CGG-USDT,UBX-USDT,GHX-USDT,TCP-USDT,STND-USDT,STND-ETH,TOWER-USDT,TOWER-BTC,ACE-USDT,LOCG-USDT,CARD-USDT,FLY-USDT,CWS-USDT,XDC-USDT,XDC-ETH,STRK-BTC,STRK-ETH,SHIB-USDT,POLX-USDT,KDA-USDT,KDA-BTC,ICP-USDT,ICP-BTC,STC-USDT,STC-BTC,GOVI-USDT,GOVI-BTC,FKX-USDT,CELO-USDT,CELO-BTC,CUSD-USDT,CUSD-BTC,FCL-USDT,MATIC-USDT,MATIC-BTC,ELA-USDT,CRPT-USDT,OPCT-USDT,OGN-USDT,OGN-BTC,OUSD-USDT,OUSD-BTC,TLOS-USDT,TLOS-BTC,YOP-USDT,YOP-ETH,GLQ-USDT,GLQ-BTC,MXC-USDT,ERSDL-USDT,HOTCROSS-USDT,ADA3L-USDT,ADA3S-USDT,HYVE-USDT,HYVE-BTC,DAPPX-USDT,KONO-USDT,PRQ-USDT,MAHA-USDT,MAHA-BTC,FEAR-USDT,PYR-USDT,PYR-BTC,PROM-USDT,PROM-BTC,GLCH-USDT,UNO-USDT,ALBT-USDT,ALBT-ETH,XCAD-USDT,EOS3L-USDT,EOS3S-USDT,BCH3L-USDT,BCH3S-USDT,ELON-USDT,APL-USDT,FCL-ETH,VEED-USDT,VEED-BTC,DIVI-USDT,PDEX-BTC,JUP-USDT,JUP-ETH,POLS-USDT,POLS-BTC,LPOOL-USDT,LPOOL-BTC,LSS-USDT,VET3L-USDT,VET3S-USDT,LTC3L-USDT,LTC3S-USDT,ABBC-USDT,ABBC-BTC,KOK-USDT,ROSN-USDT,DORA-USDT,DORA-BTC,ZCX-USDT,ZCX-BTC,NORD-USDT,GMEE-USDT,SFUND-USDT,XAVA-USDT,AI-USDT,ALPACA-USDT,IOI-USDT,NFT-USDT,NFT-TRX,MNST-USDT,MEM-USDT,AGIX-USDT,AGIX-BTC,AGIX-ETH,CQT-USDT,AIOZ-USDT,MARSH-USDT,HAPI-USDT,MODEFI-USDT,MODEFI-BTC,YFDAI-USDT,YFDAI-BTC,GENS-USDT,FORM-USDT,ARRR-USDT,ARRR-BTC,TOKO-KCS,EXRD-USDT,NGM-USDT,LPT-USDT,STMX-USDT,ASD-USDT,BOND-USDT,HAI-BTC,SOUL-USDT,2CRZ-USDT,NEAR-USDT,NEAR-BTC,DFYN-USDT,OOE-USDT,CFG-USDT,CFG-BTC,AXS-USDT,CLV-USDT,ROUTE-USDT,KAR-USDT,EFX-USDT,XDC-BTC,SHFT-USDT,PMON-USDT,DPET-USDT,ERG-USDT,ERG-BTC,SOL-USDT,SLP-USDT,LITH-USDT,LITH-ETH,XCH-USDT,HAKA-USDT,LAYER-BTC,MTL-USDT,MTL-BTC,IOTX-USDT,GALA-USDT,REQ-USDT,TXA-USDT,TXA-USDC,CIRUS-USDT,QI-USDT,QI-BTC,ODDZ-USDT,PNT-USDT,PNT-BTC,XPR-USDT,XPR-BTC,TRIBE-USDT,SHFT-BTC,MOVR-USDT,MOVR-ETH,WOO-USDT,WILD-USDT,QRDO-USDT,QRDO-ETH,SDN-USDT,SDN-ETH,MAKI-USDT,MAKI-BTC,REP-USDT,REP-BTC,REP-ETH,BNT-USDT,BNT-BTC,BNT-ETH,OXT-USDT,OXT-BTC,OXT-ETH,BAL-USDT,BAL-BTC,BAL-ETH,STORJ-USDT,STORJ-BTC,STORJ-ETH,YGG-USDT,NDAU-USDT,SDAO-USDT,SDAO-ETH,XRP3L-USDT,XRP3S-USDT,SKL-USDT,SKL-BTC,NMR-USDT,NMR-BTC,IXS-USDT,TRB-USDT,TRB-BTC,DYDX-USDT,XYO-USDT,GTC-USDT,GTC-BTC,EQX-USDT,EQX-BTC,RLC-USDT,RLC-BTC,XPRT-USDT,EGLD-USDT,EGLD-BTC,HBAR-USDT,HBAR-BTC,DOGE3L-USDT,DOGE3S-USDT,FLOW-USDT,FLOW-BTC,NKN-USDT,NKN-BTC,PBX-USDT,SOL3L-USDT,SOL3S-USDT,MLN-USDT,MLN-BTC,XNL-USDT,SOLVE-USDT,WNCG-USDT,WNCG-BTC,DMTR-USDT,LINK3L-USDT,LINK3S-USDT,DOT3L-USDT,DOT3S-USDT,CTSI-USDT,CTSI-BTC,ALICE-USDT,ALICE-BTC,ALICE-ETH,OPUL-USDT,ILV-USDT,BAND-USDT,BAND-BTC,FTT-USDT,FTT-BTC,DVPN-USDT,SKU-USDT,SKU-BTC,EDG-USDT,SLIM-USDT,TLM-USDT,TLM-BTC,TLM-ETH,DEXE-USDT,DEXE-BTC,DEXE-ETH,MATTER-USDT,CUDOS-BTC,RUNE-USDT,RUNE-BTC,RMRK-USDT,BMON-USDT,C98-USDT,BLOK-USDT,SOLR-USDT,ATOM3L-USDT,ATOM3S-USDT,UNI3L-USDT,UNI3S-USDT,WSIENNA-USDT,PUSH-USDT,PUSH-BTC,FORM-ETH,NTVRK-USDT,NTVRK-USDC,AXS3L-USDT,AXS3S-USDT,FTM3L-USDT,FTM3S-USDT,FLAME-USDT,AGLD-USDT,NAKA-USDT,YLD-USDT,TONE-USDT,REEF-USDT,REEF-BTC,TIDAL-USDT,TVK-USDT,TVK-BTC,INJ-USDT,INJ-BTC,BNB3L-USDT,BNB3S-USDT,MATIC3L-USDT,MATIC3S-USDT,NFTB-USDT,VEGA-USDT,VEGA-ETH,ALPHA-USDT,ALPHA-BTC,BADGER-USDT,BADGER-BTC,UNO-BTC,ZKT-USDT,AR-USDT,AR-BTC,XVS-USDT,XVS-BTC,JASMY-USDT,PERP-USDT,PERP-BTC,GHST-USDT,GHST-BTC,SCLP-USDT,SCLP-BTC,SUPER-USDT,SUPER-BTC,CPOOL-USDT,HERO-USDT,BASIC-USDT,XED-USDT,XED-BTC,AURY-USDT,SWASH-USDT,LTO-USDT,LTO-BTC,BUX-USDT,MTRG-USDT,DREAMS-USDT,SHIB-DOGE,QUICK-USDT,QUICK-BTC,TRU-USDT,TRU-BTC,WRX-USDT,WRX-BTC,TKO-USDT,TKO-BTC,SUSHI3L-USDT,SUSHI3S-USDT,NEAR3L-USDT,NEAR3S-USDT,DATA-USDT,DATA-BTC,NORD-BTC,ISP-USDT,CERE-USDT,SHILL-USDT,HEGIC-USDT,HEGIC-BTC,ERN-USDT,ERN-BTC,FTG-USDT,PAXG-USDT,PAXG-BTC,AUDIO-USDT,AUDIO-BTC,ENS-USDT,AAVE3L-USDT,AAVE3S-USDT,SAND3L-USDT,SAND3S-USDT,XTM-USDT,MNW-USDT,FXS-USDT,FXS-BTC,ATA-USDT,ATA-BTC,VXV-USDT,LRC-BTC,LRC-ETH,DPR-USDT,CWAR-USDT,CWAR-BTC,FLUX-BTC,EDG-BTC,PBR-USDT,WNXM-USDT,WNXM-BTC,ANT-USDT,ANT-BTC,COV-USDT,SWP-USDT,TWT-USDT,TWT-BTC,OM-USDT,OM-BTC,ADX-USDT,AVAX3L-USDT,AVAX3S-USDT,MANA3L-USDT,MANA3S-USDT,GLM-USDT,GLM-BTC,BAKE-USDT,BAKE-BTC,BAKE-ETH,NUM-USDT,VLX-USDT,VLX-BTC,TRADE-USDT,TRADE-BTC,1EARTH-USDT,MONI-USDT,LIKE-USDT,MFT-USDT,MFT-BTC,LIT-USDT,LIT-BTC,KAVA-USDT,SFP-USDT,SFP-BTC,BURGER-USDT,BURGER-BTC,ILA-USDT,CREAM-USDT,CREAM-BTC,RSR-USDT,RSR-BTC,BUY-BTC,IMX-USDT,GODS-USDT,KMA-USDT,SRM-USDT,SRM-BTC,POLC-USDT,XTAG-USDT,MNET-USDT,NGC-USDT,HARD-USDT,GALAX3L-USDT,GALAX3S-USDT,UNIC-USDT,POND-USDT,POND-BTC,VR-USDT,EPIK-USDT,NGL-USDT,NGL-BTC,KDON-USDT,PEL-USDT,CIRUS-ETH,LINA-USDT,LINA-BTC,KLAY-USDT,KLAY-BTC,CREDI-USDT,TRVL-USDT,LACE-USDT,LACE-ETH,ARKER-USDT,BONDLY-USDT,BONDLY-ETH,XEC-USDT,HEART-USDT,HEART-BTC,UNB-USDT,GAFI-USDT,KOL-USDT,KOL-ETH,H3RO3S-USDT,FALCONS-USDT,UFO-USDT,CHMB-USDT,GEEQ-USDT,ORC-USDT,RACEFI-USDT,PEOPLE-USDT,ADS-USDT,ADS-BTC,OCEAN-USDT,SOS-USDT,WHALE-USDT,TIME-USDT,CWEB-USDT,IOTA-USDT,IOTA-BTC,OOKI-USDT,OOKI-BTC,HNT-USDT,HNT-BTC,GGG-USDT,POWR-USDT,REVU-USDT,CLH-USDT,PLGR-USDT,GLMR-USDT,GLMR-BTC,LOVE-USDT,CTC-USDT,CTC-BTC,GARI-USDT,FRR-USDT,ASTR-USDT,ASTR-BTC,ERTHA-USDT,FCON-USDT,ACA-USDT,ACA-BTC,MTS-USDT,ROAR-USDT,HBB-USDT,SURV-USDT,CVX-USDT,AMP-USDT,ACT-USDT,MJT-USDT,MJT-KCS,SHX-USDT,SHX-BTC,STARLY-USDT,ONSTON-USDT,RANKER-USDT,WMT-USDT,XNO-USDT,XNO-BTC,MARS4-USDT,TFUEL-USDT,TFUEL-BTC,METIS-USDT,LAVAX-USDT,WAL-USDT,BULL-USDT,SON-USDT,MELOS-USDT,APE-USDT,GMT-USDT,BICO-USDT,STG-USDT,LMR-USDT,LMR-BTC,LOKA-USDT,URUS-USDT,JAM-USDT,JAM-ETH,BNC-USDT,LBP-USDT,CFX-USDT,LOOKS-USDT,XCN-USDT,XCN-BTC,KP3R-USDT,TITAN-USDT,INDI-USDT,UPO-USDT,SPELL-USDT,SLCL-USDT,CEEK-USDT,VEMP-USDT,BETA-USDT,NHCT-USDT,ARNM-USDT,FRA-USDT,VISION-USDT,COCOS-USDT,ALPINE-USDT,BNX-USDT,ZBC-USDT,WOOP-USDT,T-USDT,NYM-USDT,VOXEL-USDT,VOXEL-ETH,PSTAKE-USDT,SPA-USDT,SPA-ETH,SYNR-USDT,DAR-USDT,DAR-BTC,MV-USDT,XDEFI-USDT,RACA-USDT,XWG-USDT,HAWK-USDT,TRVL-BTC,SWFTC-USDT,IDEX-USDT,BRWL-USDT,PLATO-USDT,TAUM-USDT,CELR-USDT,AURORA-USDT,POSI-USDT,COOHA-USDT,KNC-USDT,EPK-USDT,PLD-USDT,PSL-USDT,PKF-USDT,OVR-USDT,SYS-USDT,SYS-BTC,BRISE-USDT,DG-USDT,EPX-USDT,GST-USDT,PLY-USDT,GAL-USDT,BSW-USDT,FITFI-USDT,FSN-USDT,H2O-USDT,GMM-USDT,AKT-USDT,SIN-USDT,AUSD-USDT,BOBA-USDT,KARA-USDT,BFC-USDT,BIFI-USDT,DFA-USDT,KYL-USDT,FCD-USDT,MBL-USDT,CELT-USDT,DUSK-USDT,USDD-USDT,USDD-USDC,FITFI-USDC,MBOX-USDT,MBOX-BTC,APE-USDC,AVAX-USDC,SHIB-USDC,XCN-USDC,TRX-USDC,NEAR-USDC,MATIC-USDC,FTM-USDC,ZIL-USDC,SOL-USDC,MLS-USDT,AFK-USDT,AFK-USDC,ACH-USDT,SCRT-USDT,SCRT-BTC,APE3L-USDT,APE3S-USDT,STORE-USDT,STORE-ETH,GMT3L-USDT,GMT3S-USDT,CCD-USDT,DOSE-USDC,LUNC-USDT,LUNC-USDC,USTC-USDT,USTC-USDC,GMT-USDC,VRA-USDC,DOT-USDC,RUNE-USDC,ATOM-USDC,BNB-USDC,JASMY-USDC,KCS-USDC,KDA-USDC,ALGO-USDC,LUNA-USDC,OP-USDT,OP-USDC,JASMY3L-USDT,JASMY3S-USDT,EVER-USDT,MOOV-USDT,IHC-USDT,ICX-USDT,ICX-ETH,BTC-BRL,ETH-BRL,USDT-BRL,WELL-USDT,FORT-USDT,USDP-USDT,USDD-TRX,CSPR-USDT,CSPR-ETH,WEMIX-USDT,REV3L-USDT,OLE-USDT,LDO-USDT,LDO-USDC,CULT-USDT,SWFTC-USDC,FIDA-USDT,BUSD-USDT,RBP-USDT,SRBP-USDT,HIBAYC-USDT,BUSD-USDC,OGV-USDT,WOMBAT-USDT,HIPUNKS-USDT,FT-USDT,ETC-USDC,HIENS4-USDT,EGAME-USDT,EGAME-BTC,STEPWATCH-USDT,HISAND33-USDT,DC-USDT,NEER-USDT,RVN-USDT,HIENS3-USDT,MC-USDT,PEEL-USDT,PEEL-BTC,SDL-USDT,SDL-BTC,SWEAT-USDT,HIODBS-USDT,CMP-USDT,PIX-USDT,MPLX-USDT,HIDOODLES-USDT,ETHW-USDT,QUARTZ-USDT,ACQ-USDT,ACQ-USDC,AOG-USDT,HIMAYC-USDT,PRMX-USDT,RED-USDT,PUMLX-USDT,XETA-USDT,GEM-USDT,DERC-USDT,P00LS-USDT,P00LS-USDC,KICKS-USDT,TRIBL-USDT,GMX-USDT,HIOD-USDT,POKT-USDT,EFI-USDT,APT-USDT,BBC-USDT,EUL-USDT,TON-USDT,PIAS-USDT,HIMEEBITS-USDT,HISQUIGGLE-USDT,XCV-USDT,HFT-USDT,HFT-USDC,ECOX-USDT,AMB-USDT,AZERO-USDT,HIFIDENZA-USDT,BEAT-USDT", - "requestFormat": { - "uppercase": true, - "delimiter": "-" - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - } + "enabled": "BTC-USD", + "available": "BTC-USD,LTC-USD,ETH-USD,ETC-USD,TUSD-USD,BCH-USD,EOS-USD,XRP-USD,TRX-USD,BSV-USD,USDT-USD,USDK-USD,XLM-USD,ADA-USD,BAT-USD,DCR-USD,EURS-USD,HBAR-USD,PAX-USD,USDC-USD,ZEC-USD,BTC-USDT,BTC-SGD,ETH-SGD,BTC-EUR,BTC-EURS,ETH-EUR,BCH-EUR,EURS-EUR" }, "margin": { - "assetEnabled": true, - "enabled": "BTC-USDT,ETH-USDT,LTC-USDT,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MTV-BTC,ETH-BTC", - "available": "BTC-USDT,MHC-ETH,MHC-BTC,OXEN-BTC,OXEN-ETH,NRG-BTC,AVA-USDT,FET-BTC,FET-ETH,ANKR-BTC,MHC-USDT,XMR-BTC,XMR-ETH,RIF-BTC,MTV-BTC,MTV-ETH,CRO-BTC,MTV-USDT,KMD-BTC,KMD-USDT,RFOX-USDT,TEL-USDT,TT-USDT,AERGO-USDT,XMR-USDT,TRX-KCS,ATOM-BTC,ATOM-ETH,ATOM-USDT,ATOM-KCS,ETN-USDT,FTM-USDT,TOMO-USDT,VSYS-USDT,OCEAN-BTC,OCEAN-ETH,CHR-BTC,CHR-USDT,FX-BTC,FX-ETH,NIM-BTC,NIM-ETH,COTI-BTC,COTI-USDT,NRG-ETH,BNB-BTC,BNB-USDT,JAR-BTC,JAR-USDT,ALGO-BTC,ALGO-ETH,ALGO-USDT,XEM-BTC,XEM-USDT,CIX100-USDT,XTZ-BTC,XTZ-USDT,ZEC-BTC,ZEC-USDT,ADA-BTC,ADA-USDT,REV-USDT,WXT-BTC,WXT-USDT,FORESTPLUS-BTC,FORESTPLUS-USDT,BOLT-BTC,BOLT-USDT,ARPA-USDT,CHZ-BTC,CHZ-USDT,DAPPT-BTC,DAPPT-USDT,NOIA-BTC,NOIA-USDT,WIN-BTC,WIN-USDT,DERO-BTC,DERO-USDT,BTT-USDT,EOSC-USDT,ENQ-BTC,ENQ-USDT,ONE-BTC,ONE-USDT,TOKO-BTC,TOKO-USDT,VID-BTC,VID-USDT,LUNA-USDT,SXP-BTC,SXP-USDT,AKRO-BTC,AKRO-USDT,ROOBEE-BTC,WIN-TRX,MAP-BTC,MAP-USDT,AMPL-BTC,AMPL-USDT,DAG-USDT,POL-USDT,ARX-USDT,NWC-BTC,NWC-USDT,BEPRO-BTC,BEPRO-USDT,VRA-BTC,VRA-USDT,KSM-BTC,KSM-USDT,DASH-USDT,SUTER-USDT,ACOIN-USDT,SUTER-BTC,SENSO-USDT,PRE-BTC,XDB-USDT,SYLO-USDT,WOM-USDT,SENSO-BTC,DGB-USDT,LYXE-USDT,LYXE-ETH,XDB-BTC,STX-BTC,STX-USDT,XSR-USDT,COMP-USDT,CRO-USDT,KAI-USDT,KAI-BTC,WEST-BTC,WEST-USDT,EWT-BTC,WAVES-USDT,WAVES-BTC,ORN-USDT,AMPL-ETH,BNS-USDT,MKR-USDT,SUKU-BTC,MLK-BTC,MLK-USDT,JST-USDT,KAI-ETH,SUKU-USDT,DIA-USDT,DIA-BTC,LINK-BTC,LINK-USDT,DOT-USDT,DOT-BTC,SHA-BTC,SHA-USDT,EWT-USDT,USDJ-USDT,EFX-BTC,CKB-BTC,CKB-USDT,UMA-USDT,ALEPH-USDT,VELO-USDT,SUN-USDT,BUY-USDT,YFI-USDT,OXEN-USDT,UNI-USDT,UOS-USDT,UOS-BTC,NIM-USDT,DEGO-USDT,DEGO-ETH,UDOO-ETH,RFUEL-USDT,FIL-USDT,UBX-ETH,REAP-USDT,AAVE-USDT,AAVE-BTC,TONE-BTC,TONE-ETH,ELF-ETH,AERGO-BTC,IOST-ETH,KCS-USDT,SNX-ETH,TOMO-ETH,KCS-ETH,DRGN-BTC,WAN-ETH,NULS-ETH,AXPR-ETH,POWR-BTC,QTUM-BTC,MANA-BTC,TEL-BTC,XYO-ETH,AXPR-BTC,ETN-BTC,COV-ETH,VET-BTC,KCS-BTC,CAPP-ETH,ONT-BTC,DRGN-ETH,DAG-ETH,TOMO-BTC,WAN-BTC,KNC-ETH,CRPT-ETH,LTC-USDT,BAX-ETH,BSV-USDT,DENT-ETH,AION-ETH,LYM-ETH,TRAC-ETH,ENJ-BTC,WAXP-BTC,DGB-BTC,ELA-BTC,ZIL-BTC,BSV-BTC,XLM-USDT,IOTX-ETH,SOUL-BTC,DOCK-BTC,AMB-ETH,TRX-BTC,XRP-TUSD,NULS-BTC,ETH-DAI,LSK-BTC,GMB-ETH,GMB-BTC,NEO-ETH,OMG-ETH,BTC-TUSD,KAT-USDT,KNC-BTC,ELF-BTC,MANA-ETH,ETC-USDT,ONT-ETH,MKR-BTC,KAT-BTC,XRP-USDC,XYO-BTC,SNT-ETH,ZRX-BTC,LOOM-ETH,AION-BTC,POWR-ETH,OLT-ETH,OLT-BTC,SNT-BTC,TRAC-BTC,XLM-ETH,ETH-USDT,BSV-ETH,TRX-ETH,ETN-ETH,AOA-USDT,BCD-BTC,DENT-BTC,DOCK-ETH,KEY-BTC,EOS-KCS,XLM-BTC,ADB-ETH,TIME-ETH,CVC-BTC,LSK-ETH,QKC-BTC,AMB-BTC,USDT-TUSD,ETC-ETH,XRP-BTC,NEO-KCS,SNX-USDT,CRPT-BTC,IOTX-BTC,LTC-ETH,XRP-KCS,ADB-BTC,LTC-KCS,TEL-ETH,DCR-ETH,LYM-USDT,USDT-USDC,ETH-USDC,DAG-BTC,AVA-BTC,BTC-USDT,WAXP-ETH,XRP-USDT,KEY-ETH,VET-ETH,FTM-BTC,USDT-DAI,QKC-ETH,ETH-BTC,MAN-BTC,CPC-ETH,TRX-USDT,BTC-DAI,ONT-USDT,DASH-ETH,BAX-BTC,AVA-ETH,LOOM-BTC,MVP-BTC,MKR-ETH,COV-BTC,CPC-BTC,REQ-ETH,EOS-BTC,LTC-BTC,XRP-ETH,CAPP-BTC,FTM-ETH,BCD-ETH,ZRX-ETH,DGB-ETH,VET-USDT,REQ-BTC,UTK-BTC,PLAY-BTC,UTK-ETH,SNX-BTC,MVP-ETH,NEO-BTC,SOUL-ETH,NEO-USDT,ELA-ETH,OMG-BTC,TIME-BTC,AOA-BTC,ETC-BTC,DCR-BTC,BTC-USDC,ENJ-ETH,IOST-BTC,DASH-BTC,EOS-USDT,EOS-ETH,ZIL-ETH,ETH-TUSD,GAS-BTC,LYM-BTC,BCH-BTC,VSYS-BTC,BCH-USDT,MKR-DAI,SOLVE-BTC,GRIN-BTC,GRIN-USDT,UQC-BTC,UQC-ETH,OPCT-BTC,OPCT-ETH,PRE-USDT,SHR-BTC,SHR-USDT,UBXT-USDT,ROSE-USDT,USDC-USDT,CTI-USDT,CTI-ETH,ETH2-ETH,BUX-BTC,XHV-USDT,PLU-USDT,GRT-USDT,CAS-BTC,CAS-USDT,MSWAP-BTC,MSWAP-USDT,GOM2-BTC,GOM2-USDT,REVV-BTC,REVV-USDT,LON-USDT,1INCH-USDT,LOC-USDT,API3-USDT,UNFI-USDT,HTR-USDT,FRONT-USDT,FRONT-BTC,WBTC-BTC,WBTC-ETH,MIR-USDT,LTC-USDC,BCH-USDC,HYDRA-USDT,DFI-USDT,DFI-BTC,CRV-USDT,SUSHI-USDT,FRM-USDT,EOS-USDC,BSV-USDC,ZEN-USDT,CUDOS-USDT,ADA-USDC,REN-USDT,LRC-USDT,LINK-USDC,KLV-USDT,KLV-BTC,BOA-USDT,THETA-USDT,QNT-USDT,BAT-USDT,DOGE-USDT,DOGE-USDC,DAO-USDT,STRONG-USDT,TRIAS-USDT,TRIAS-BTC,DOGE-BTC,MITX-BTC,MITX-USDT,CAKE-USDT,ORAI-USDT,ZEE-USDT,LTX-USDT,LTX-BTC,MASK-USDT,KLV-TRX,IDEA-USDT,PHA-USDT,PHA-ETH,BCH-KCS,SRK-USDT,SRK-BTC,ADA-KCS,HTR-BTC,BSV-KCS,DOT-KCS,LINK-KCS,MIR-KCS,BNB-KCS,XLM-KCS,VET-KCS,SWINGBY-USDT,SWINGBY-BTC,XHV-BTC,DASH-KCS,UNI-KCS,AAVE-KCS,DOGE-KCS,ZEC-KCS,XTZ-KCS,GRT-KCS,ALGO-KCS,EWT-KCS,GAS-USDT,AVAX-USDT,AVAX-BTC,KRL-BTC,KRL-USDT,POLK-USDT,POLK-BTC,ENJ-USDT,MANA-USDT,RNDR-USDT,RNDR-BTC,RLY-USDT,ANC-USDT,SKEY-USDT,LAYER-USDT,TARA-USDT,TARA-ETH,IOST-USDT,DYP-USDT,DYP-ETH,XYM-USDT,XYM-BTC,PCX-USDT,PCX-BTC,ORBS-USDT,ORBS-BTC,BTC3L-USDT,BTC3S-USDT,ETH3L-USDT,ETH3S-USDT,ANKR-USDT,DSLA-USDT,DSLA-BTC,SAND-USDT,VAI-USDT,XCUR-USDT,XCUR-BTC,FLUX-USDT,OMG-USDT,ZIL-USDT,DODO-USDT,MAN-USDT,BAX-USDT,BOSON-USDT,BOSON-ETH,PUNDIX-USDT,PUNDIX-BTC,WAXP-USDT,HT-USDT,PDEX-USDT,LABS-USDT,LABS-ETH,GMB-USDT,PHNX-USDT,PHNX-BTC,HAI-USDT,EQZ-USDT,FORTH-USDT,HORD-USDT,CGG-USDT,UBX-USDT,GHX-USDT,TCP-USDT,STND-USDT,STND-ETH,TOWER-USDT,TOWER-BTC,ACE-USDT,LOCG-USDT,CARD-USDT,FLY-USDT,CWS-USDT,XDC-USDT,XDC-ETH,STRK-BTC,STRK-ETH,SHIB-USDT,POLX-USDT,KDA-USDT,KDA-BTC,ICP-USDT,ICP-BTC,STC-USDT,STC-BTC,GOVI-USDT,GOVI-BTC,FKX-USDT,CELO-USDT,CELO-BTC,CUSD-USDT,CUSD-BTC,FCL-USDT,MATIC-USDT,MATIC-BTC,ELA-USDT,CRPT-USDT,OPCT-USDT,OGN-USDT,OGN-BTC,OUSD-USDT,OUSD-BTC,TLOS-USDT,TLOS-BTC,YOP-USDT,YOP-ETH,GLQ-USDT,GLQ-BTC,MXC-USDT,ERSDL-USDT,HOTCROSS-USDT,ADA3L-USDT,ADA3S-USDT,HYVE-USDT,HYVE-BTC,DAPPX-USDT,KONO-USDT,PRQ-USDT,MAHA-USDT,MAHA-BTC,FEAR-USDT,PYR-USDT,PYR-BTC,PROM-USDT,PROM-BTC,GLCH-USDT,UNO-USDT,ALBT-USDT,ALBT-ETH,XCAD-USDT,EOS3L-USDT,EOS3S-USDT,BCH3L-USDT,BCH3S-USDT,ELON-USDT,APL-USDT,FCL-ETH,VEED-USDT,VEED-BTC,DIVI-USDT,PDEX-BTC,JUP-USDT,JUP-ETH,POLS-USDT,POLS-BTC,LPOOL-USDT,LPOOL-BTC,LSS-USDT,VET3L-USDT,VET3S-USDT,LTC3L-USDT,LTC3S-USDT,ABBC-USDT,ABBC-BTC,KOK-USDT,ROSN-USDT,DORA-USDT,DORA-BTC,ZCX-USDT,ZCX-BTC,NORD-USDT,GMEE-USDT,SFUND-USDT,XAVA-USDT,AI-USDT,ALPACA-USDT,IOI-USDT,NFT-USDT,NFT-TRX,MNST-USDT,MEM-USDT,AGIX-USDT,AGIX-BTC,AGIX-ETH,CQT-USDT,AIOZ-USDT,MARSH-USDT,HAPI-USDT,MODEFI-USDT,MODEFI-BTC,YFDAI-USDT,YFDAI-BTC,GENS-USDT,FORM-USDT,ARRR-USDT,ARRR-BTC,TOKO-KCS,EXRD-USDT,NGM-USDT,LPT-USDT,STMX-USDT,ASD-USDT,BOND-USDT,HAI-BTC,SOUL-USDT,2CRZ-USDT,NEAR-USDT,NEAR-BTC,DFYN-USDT,OOE-USDT,CFG-USDT,CFG-BTC,AXS-USDT,CLV-USDT,ROUTE-USDT,KAR-USDT,EFX-USDT,XDC-BTC,SHFT-USDT,PMON-USDT,DPET-USDT,ERG-USDT,ERG-BTC,SOL-USDT,SLP-USDT,LITH-USDT,LITH-ETH,XCH-USDT,HAKA-USDT,LAYER-BTC,MTL-USDT,MTL-BTC,IOTX-USDT,GALA-USDT,REQ-USDT,TXA-USDT,TXA-USDC,CIRUS-USDT,QI-USDT,QI-BTC,ODDZ-USDT,PNT-USDT,PNT-BTC,XPR-USDT,XPR-BTC,TRIBE-USDT,SHFT-BTC,MOVR-USDT,MOVR-ETH,WOO-USDT,WILD-USDT,QRDO-USDT,QRDO-ETH,SDN-USDT,SDN-ETH,MAKI-USDT,MAKI-BTC,REP-USDT,REP-BTC,REP-ETH,BNT-USDT,BNT-BTC,BNT-ETH,OXT-USDT,OXT-BTC,OXT-ETH,BAL-USDT,BAL-BTC,BAL-ETH,STORJ-USDT,STORJ-BTC,STORJ-ETH,YGG-USDT,NDAU-USDT,SDAO-USDT,SDAO-ETH,XRP3L-USDT,XRP3S-USDT,SKL-USDT,SKL-BTC,NMR-USDT,NMR-BTC,IXS-USDT,TRB-USDT,TRB-BTC,DYDX-USDT,XYO-USDT,GTC-USDT,GTC-BTC,EQX-USDT,EQX-BTC,RLC-USDT,RLC-BTC,XPRT-USDT,EGLD-USDT,EGLD-BTC,HBAR-USDT,HBAR-BTC,DOGE3L-USDT,DOGE3S-USDT,FLOW-USDT,FLOW-BTC,NKN-USDT,NKN-BTC,PBX-USDT,SOL3L-USDT,SOL3S-USDT,MLN-USDT,MLN-BTC,XNL-USDT,SOLVE-USDT,WNCG-USDT,WNCG-BTC,DMTR-USDT,LINK3L-USDT,LINK3S-USDT,DOT3L-USDT,DOT3S-USDT,CTSI-USDT,CTSI-BTC,ALICE-USDT,ALICE-BTC,ALICE-ETH,OPUL-USDT,ILV-USDT,BAND-USDT,BAND-BTC,FTT-USDT,FTT-BTC,DVPN-USDT,SKU-USDT,SKU-BTC,EDG-USDT,SLIM-USDT,TLM-USDT,TLM-BTC,TLM-ETH,DEXE-USDT,DEXE-BTC,DEXE-ETH,MATTER-USDT,CUDOS-BTC,RUNE-USDT,RUNE-BTC,RMRK-USDT,BMON-USDT,C98-USDT,BLOK-USDT,SOLR-USDT,ATOM3L-USDT,ATOM3S-USDT,UNI3L-USDT,UNI3S-USDT,WSIENNA-USDT,PUSH-USDT,PUSH-BTC,FORM-ETH,NTVRK-USDT,NTVRK-USDC,AXS3L-USDT,AXS3S-USDT,FTM3L-USDT,FTM3S-USDT,FLAME-USDT,AGLD-USDT,NAKA-USDT,YLD-USDT,TONE-USDT,REEF-USDT,REEF-BTC,TIDAL-USDT,TVK-USDT,TVK-BTC,INJ-USDT,INJ-BTC,BNB3L-USDT,BNB3S-USDT,MATIC3L-USDT,MATIC3S-USDT,NFTB-USDT,VEGA-USDT,VEGA-ETH,ALPHA-USDT,ALPHA-BTC,BADGER-USDT,BADGER-BTC,UNO-BTC,ZKT-USDT,AR-USDT,AR-BTC,XVS-USDT,XVS-BTC,JASMY-USDT,PERP-USDT,PERP-BTC,GHST-USDT,GHST-BTC,SCLP-USDT,SCLP-BTC,SUPER-USDT,SUPER-BTC,CPOOL-USDT,HERO-USDT,BASIC-USDT,XED-USDT,XED-BTC,AURY-USDT,SWASH-USDT,LTO-USDT,LTO-BTC,BUX-USDT,MTRG-USDT,DREAMS-USDT,SHIB-DOGE,QUICK-USDT,QUICK-BTC,TRU-USDT,TRU-BTC,WRX-USDT,WRX-BTC,TKO-USDT,TKO-BTC,SUSHI3L-USDT,SUSHI3S-USDT,NEAR3L-USDT,NEAR3S-USDT,DATA-USDT,DATA-BTC,NORD-BTC,ISP-USDT,CERE-USDT,SHILL-USDT,HEGIC-USDT,HEGIC-BTC,ERN-USDT,ERN-BTC,FTG-USDT,PAXG-USDT,PAXG-BTC,AUDIO-USDT,AUDIO-BTC,ENS-USDT,AAVE3L-USDT,AAVE3S-USDT,SAND3L-USDT,SAND3S-USDT,XTM-USDT,MNW-USDT,FXS-USDT,FXS-BTC,ATA-USDT,ATA-BTC,VXV-USDT,LRC-BTC,LRC-ETH,DPR-USDT,CWAR-USDT,CWAR-BTC,FLUX-BTC,EDG-BTC,PBR-USDT,WNXM-USDT,WNXM-BTC,ANT-USDT,ANT-BTC,COV-USDT,SWP-USDT,TWT-USDT,TWT-BTC,OM-USDT,OM-BTC,ADX-USDT,AVAX3L-USDT,AVAX3S-USDT,MANA3L-USDT,MANA3S-USDT,GLM-USDT,GLM-BTC,BAKE-USDT,BAKE-BTC,BAKE-ETH,NUM-USDT,VLX-USDT,VLX-BTC,TRADE-USDT,TRADE-BTC,1EARTH-USDT,MONI-USDT,LIKE-USDT,MFT-USDT,MFT-BTC,LIT-USDT,LIT-BTC,KAVA-USDT,SFP-USDT,SFP-BTC,BURGER-USDT,BURGER-BTC,ILA-USDT,CREAM-USDT,CREAM-BTC,RSR-USDT,RSR-BTC,BUY-BTC,IMX-USDT,GODS-USDT,KMA-USDT,SRM-USDT,SRM-BTC,POLC-USDT,XTAG-USDT,MNET-USDT,NGC-USDT,HARD-USDT,GALAX3L-USDT,GALAX3S-USDT,UNIC-USDT,POND-USDT,POND-BTC,VR-USDT,EPIK-USDT,NGL-USDT,NGL-BTC,KDON-USDT,PEL-USDT,CIRUS-ETH,LINA-USDT,LINA-BTC,KLAY-USDT,KLAY-BTC,CREDI-USDT,TRVL-USDT,LACE-USDT,LACE-ETH,ARKER-USDT,BONDLY-USDT,BONDLY-ETH,XEC-USDT,HEART-USDT,HEART-BTC,UNB-USDT,GAFI-USDT,KOL-USDT,KOL-ETH,H3RO3S-USDT,FALCONS-USDT,UFO-USDT,CHMB-USDT,GEEQ-USDT,ORC-USDT,RACEFI-USDT,PEOPLE-USDT,ADS-USDT,ADS-BTC,OCEAN-USDT,SOS-USDT,WHALE-USDT,TIME-USDT,CWEB-USDT,IOTA-USDT,IOTA-BTC,OOKI-USDT,OOKI-BTC,HNT-USDT,HNT-BTC,GGG-USDT,POWR-USDT,REVU-USDT,CLH-USDT,PLGR-USDT,GLMR-USDT,GLMR-BTC,LOVE-USDT,CTC-USDT,CTC-BTC,GARI-USDT,FRR-USDT,ASTR-USDT,ASTR-BTC,ERTHA-USDT,FCON-USDT,ACA-USDT,ACA-BTC,MTS-USDT,ROAR-USDT,HBB-USDT,SURV-USDT,CVX-USDT,AMP-USDT,ACT-USDT,MJT-USDT,MJT-KCS,SHX-USDT,SHX-BTC,STARLY-USDT,ONSTON-USDT,RANKER-USDT,WMT-USDT,XNO-USDT,XNO-BTC,MARS4-USDT,TFUEL-USDT,TFUEL-BTC,METIS-USDT,LAVAX-USDT,WAL-USDT,BULL-USDT,SON-USDT,MELOS-USDT,APE-USDT,GMT-USDT,BICO-USDT,STG-USDT,LMR-USDT,LMR-BTC,LOKA-USDT,URUS-USDT,JAM-USDT,JAM-ETH,BNC-USDT,LBP-USDT,CFX-USDT,LOOKS-USDT,XCN-USDT,XCN-BTC,KP3R-USDT,TITAN-USDT,INDI-USDT,UPO-USDT,SPELL-USDT,SLCL-USDT,CEEK-USDT,VEMP-USDT,BETA-USDT,NHCT-USDT,ARNM-USDT,FRA-USDT,VISION-USDT,COCOS-USDT,ALPINE-USDT,BNX-USDT,ZBC-USDT,WOOP-USDT,T-USDT,NYM-USDT,VOXEL-USDT,VOXEL-ETH,PSTAKE-USDT,SPA-USDT,SPA-ETH,SYNR-USDT,DAR-USDT,DAR-BTC,MV-USDT,XDEFI-USDT,RACA-USDT,XWG-USDT,HAWK-USDT,TRVL-BTC,SWFTC-USDT,IDEX-USDT,BRWL-USDT,PLATO-USDT,TAUM-USDT,CELR-USDT,AURORA-USDT,POSI-USDT,COOHA-USDT,KNC-USDT,EPK-USDT,PLD-USDT,PSL-USDT,PKF-USDT,OVR-USDT,SYS-USDT,SYS-BTC,BRISE-USDT,DG-USDT,EPX-USDT,GST-USDT,PLY-USDT,GAL-USDT,BSW-USDT,FITFI-USDT,FSN-USDT,H2O-USDT,GMM-USDT,AKT-USDT,SIN-USDT,AUSD-USDT,BOBA-USDT,KARA-USDT,BFC-USDT,BIFI-USDT,DFA-USDT,KYL-USDT,FCD-USDT,MBL-USDT,CELT-USDT,DUSK-USDT,USDD-USDT,USDD-USDC,FITFI-USDC,MBOX-USDT,MBOX-BTC,APE-USDC,AVAX-USDC,SHIB-USDC,XCN-USDC,TRX-USDC,NEAR-USDC,MATIC-USDC,FTM-USDC,ZIL-USDC,SOL-USDC,MLS-USDT,AFK-USDT,AFK-USDC,ACH-USDT,SCRT-USDT,SCRT-BTC,APE3L-USDT,APE3S-USDT,STORE-USDT,STORE-ETH,GMT3L-USDT,GMT3S-USDT,CCD-USDT,DOSE-USDC,LUNC-USDT,LUNC-USDC,USTC-USDT,USTC-USDC,GMT-USDC,VRA-USDC,DOT-USDC,RUNE-USDC,ATOM-USDC,BNB-USDC,JASMY-USDC,KCS-USDC,KDA-USDC,ALGO-USDC,LUNA-USDC,OP-USDT,OP-USDC,JASMY3L-USDT,JASMY3S-USDT,EVER-USDT,MOOV-USDT,IHC-USDT,ICX-USDT,ICX-ETH,BTC-BRL,ETH-BRL,USDT-BRL,WELL-USDT,FORT-USDT,USDP-USDT,USDD-TRX,CSPR-USDT,CSPR-ETH,WEMIX-USDT,REV3L-USDT,OLE-USDT,LDO-USDT,LDO-USDC,CULT-USDT,SWFTC-USDC,FIDA-USDT,BUSD-USDT,RBP-USDT,SRBP-USDT,HIBAYC-USDT,BUSD-USDC,OGV-USDT,WOMBAT-USDT,HIPUNKS-USDT,FT-USDT,ETC-USDC,HIENS4-USDT,EGAME-USDT,EGAME-BTC,STEPWATCH-USDT,HISAND33-USDT,DC-USDT,NEER-USDT,RVN-USDT,HIENS3-USDT,MC-USDT,PEEL-USDT,PEEL-BTC,SDL-USDT,SDL-BTC,SWEAT-USDT,HIODBS-USDT,CMP-USDT,PIX-USDT,MPLX-USDT,HIDOODLES-USDT,ETHW-USDT,QUARTZ-USDT,ACQ-USDT,ACQ-USDC,AOG-USDT,HIMAYC-USDT,PRMX-USDT,RED-USDT,PUMLX-USDT,XETA-USDT,GEM-USDT,DERC-USDT,P00LS-USDT,P00LS-USDC,KICKS-USDT,TRIBL-USDT,GMX-USDT,HIOD-USDT,POKT-USDT,EFI-USDT,APT-USDT,BBC-USDT,EUL-USDT,TON-USDT,PIAS-USDT,HIMEEBITS-USDT,HISQUIGGLE-USDT,XCV-USDT,HFT-USDT,HFT-USDC,ECOX-USDT,AMB-USDT,AZERO-USDT,HIFIDENZA-USDT,BEAT-USDT", - "requestFormat": { - "uppercase": true, - "delimiter": "-" - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - } - }, - "futures" : { - "assetEnabled" : true, - "enabled" : "ETH_USDCM,XBT_USDCM,SOL_USDTM", - "available" : "SDTM,SUSHI_USDTM,XLM_USDTM,1INCH_USDTM,ZEC_USDTM,DASH_USDTM,DOT_USDM,XRP_USDM,AAVE_USDTM,KSM_USDTM,DOGE_USDTM,VET_USDTM,BNB_USDTM,SXP_USDTM,SOL_USDTM,CRV_USDTM,ALGO_USDTM,AVAX_USDTM,FTM_USDTM,MATIC_USDTM,THETA_USDTM,ATOM_USDTM,CHZ_USDTM,ENJ_USDTM,MANA_USDTM,DENT_USDTM,OCEAN_USDTM,BAT_USDTM,XEM_USDTM,QTUM_USDTM,XTZ_USDTM,SNX_USDTM,NEO_USDTM,ONT_USDTM,XMR_USDTM,COMP_USDTM,ETC_USDTM,WAVES_USDTM,BAND_USDTM,MKR_USDTM,RVN_USDTM,DGB_USDTM,SHIB_USDTM,ICP_USDTM,DYDX_USDTM,AXS_USDTM,HBAR_USDTM,EGLD_USDTM,ALICE_USDTM,YGG_USDTM,NEAR_USDTM,SAND_USDTM,C98_USDTM,ONE_USDTM,VRA_USDTM,GALA_USDTM,CHR_USDTM,LRC_USDTM,FLOW_USDTM,RNDR_USDTM,IOTX_USDTM,CRO_USDTM,WAXP_USDTM,PEOPLE_USDTM,OMG_USDTM,LINA_USDTM,IMX_USDTM,CELR_USDTM,ENS_USDTM,CELO_USDTM,CTSI_USDTM,ARPA_USDTM,KNC_USDTM,ROSE_USDTM,AGLD_USDTM,APE_USDTM,JASMY_USDTM,ZIL_USDTM,GMT_USDTM,RUNE_USDTM,LOOKS_USDTM,AUDIO_USDTM,KDA_USDTM,KAVA_USDTM,BAL_USDTM,GAL_USDTM,LUNA_USDTM,LUNC_USDTM,OP_USDTM,XCN_USDTM,UNFI_USDTM,LIT_USDTM,DUSK_USDTM,STORJ_USDTM,RSR_USDTM,OGN_USDTM,TRB_USDTM,PERP_USDTM,KLAY_USDTM,ANKR_USDTM,LDO_USDTM,WOO_USDTM,REN_USDTM,CVC_USDTM,INJ_USDTM,APT_USDTM,MASK_USDTM,REEF_USDTM,TON_USDTM,MAGIC_USDTM,CFX_USDTM,AGIX_USDTM,FXS_USDTM,FET_USDTM,AR_USDTM,GMX_USDTM,BLUR_USDTM,ASTR_USDTM,HIGH_USDTM,ACH_USDTM,STX_USDTM,SSV_USDTM,FLOKI_USDTM,CKB_USDTM,TRU_USDTM,QNT_USDTM,ETH_USDCM,MINA_USDTM,USDC_USDTM,T_USDTM,LQTY_USDTM,ARB_USDTM,DAR_USDTM,ID_USDTM,STG_USDTM,JOE_USDTM,RDNT_USDTM,DODO_USDTM,PAXG_USDTM,ZRX_USDTM,ICX_USDTM,HFT_USDTM,NKN_USDTM,HOOK_USDTM,ANT_USDTM,DC_USDTM,BEL_USDTM,SUI_USDTM,PEPE_USDTM,IDEX_USDTM,GNS_USDTM,CETUS_USDTM,KAS_USDTM,ORDI_USDTM,WOJAK_USDTM,POGAI_USDTM,UMA_USDTM,RAD_USDTM,XBT_USDCM,PHB_USDTM,FTT_USDTM,10000LADYS_USDTM,LEVER_USDTM,TURBO_USDTM,TOMO_USDTM,BOB_USDTM,KEY_USDTM,EDU_USDTM,MTL_USDTM,FLUX_USDTM,COMBO_USDTM,AMB_USDTM,ALPHA_USDTM,SFP_USDTM,MAV_USDTM,MDT_USDTM,XEC_USDTM,XVG_USDTM,1000PEPE2_USDTM,PENDLE_USDTM,STMX_USDTM,WLD_USDTM,LPT_USDTM,GTC_USDTM,BNT_USDTM,OXT_USDTM,BLZ_USDTM,SEI_USDTM,BAKE_USDTM,CYBER_USDTM,NMR_USDTM,FLM_USDTM,SPELL_USDTM,ARK_USDTM,XBT_MU23,XBT_MZ23", - "requestFormat": { - "uppercase": true, - "delimiter": "" - }, - "configFormat": { - "uppercase": true, - "delimiter": "_" - } + "enabled": "BTC-USD", + "available": "BTC-USD" } - } - }, - "api": { - "authenticatedSupport": true, - "authenticatedWebsocketApiSupport": true, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret", - "clientID": "ClientID" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} - }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": true - } - }, - "bankAccounts": [ - { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] -}, - { - "name": "Okcoin", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": true, - "delimiter": "-" - }, - "configFormat": { - "uppercase": true, - "delimiter": "-" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC-USD", - "available": "BTC-USD,LTC-USD,ETH-USD,ETC-USD,TUSD-USD,BCH-USD,EOS-USD,XRP-USD,TRX-USD,BSV-USD,USDT-USD,USDK-USD,XLM-USD,ADA-USD,BAT-USD,DCR-USD,EURS-USD,HBAR-USD,PAX-USD,USDC-USD,ZEC-USD,BTC-USDT,BTC-SGD,ETH-SGD,BTC-EUR,BTC-EURS,ETH-EUR,BCH-EUR,EURS-EUR" - }, - "margin": { - "enabled": "BTC-USD", - "available": "BTC-USD" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true, - "requiresClientID": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true, + "requiresClientID": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": true - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "Poloniex", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": true, - "delimiter": "_" - }, - "configFormat": { - "uppercase": true, - "delimiter": "_" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC_LTC,BTC_ETH,BTC_DOGE,BTC_DASH,BTC_XRP", - "available": "USDC_GRIN,BTC_BCN,BTC_DGB,BTC_XMR,USDT_STR,BTC_SC,BTC_ZRX,USDC_XMR,BTC_TRX,BTC_STR,BTC_SNT,USDT_QTUM,USDC_BTC,BTC_NMR,BTC_DASH,BTC_NXT,USDT_LTC,BTC_DCR,USDT_ZRX,USDC_ZEC,USDT_REP,USDT_BAT,BTC_MANA,USDC_BCHABC,USDC_STR,BTC_XRP,USDT_ETH,BTC_REP,USDT_EOS,USDC_ATOM,USDT_XRP,BTC_ETH,USDT_LSK,USDT_SC,USDT_MANA,USDC_ETC,USDC_ETH,BTC_BTS,BTC_LTC,BTC_ETC,BTC_OMG,BTC_STORJ,USDC_XRP,USDT_GRIN,BTC_QTUM,BTC_MAID,BTC_XEM,USDT_BTC,USDT_DASH,ETH_REP,BTC_ZEC,BTC_STRAT,USDC_LTC,BTC_FOAM,USDC_TRX,BTC_DOGE,BTC_VIA,BTC_VTC,ETH_ETC,USDT_ETC,ETH_EOS,USDC_BCHSV,USDT_NXT,USDT_XMR,BTC_ARDR,BTC_CVC,ETH_BAT,USDC_DOGE,BTC_XPM,BTC_LOOM,BTC_LPT,USDC_EOS,USDT_DGB,USDT_BCHSV,BTC_OMNI,ETH_ZEC,BTC_EOS,BTC_KNC,BTC_BCHSV,BTC_POLY,USDC_DASH,USDT_GNT,BTC_BCHABC,BTC_GRIN,BTC_ATOM,USDT_ATOM,USDT_BCHABC,BTC_LSK,ETH_ZRX,BTC_GAS,BTC_BAT,BTC_BNT,USDT_TRX,BTC_FCT,USDT_ZEC,BTC_GNT,USDT_DOGE,USDC_USDT" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + "name": "Poloniex", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_LTC,BTC_ETH,BTC_DOGE,BTC_DASH,BTC_XRP", + "available": "USDC_GRIN,BTC_BCN,BTC_DGB,BTC_XMR,USDT_STR,BTC_SC,BTC_ZRX,USDC_XMR,BTC_TRX,BTC_STR,BTC_SNT,USDT_QTUM,USDC_BTC,BTC_NMR,BTC_DASH,BTC_NXT,USDT_LTC,BTC_DCR,USDT_ZRX,USDC_ZEC,USDT_REP,USDT_BAT,BTC_MANA,USDC_BCHABC,USDC_STR,BTC_XRP,USDT_ETH,BTC_REP,USDT_EOS,USDC_ATOM,USDT_XRP,BTC_ETH,USDT_LSK,USDT_SC,USDT_MANA,USDC_ETC,USDC_ETH,BTC_BTS,BTC_LTC,BTC_ETC,BTC_OMG,BTC_STORJ,USDC_XRP,USDT_GRIN,BTC_QTUM,BTC_MAID,BTC_XEM,USDT_BTC,USDT_DASH,ETH_REP,BTC_ZEC,BTC_STRAT,USDC_LTC,BTC_FOAM,USDC_TRX,BTC_DOGE,BTC_VIA,BTC_VTC,ETH_ETC,USDT_ETC,ETH_EOS,USDC_BCHSV,USDT_NXT,USDT_XMR,BTC_ARDR,BTC_CVC,ETH_BAT,USDC_DOGE,BTC_XPM,BTC_LOOM,BTC_LPT,USDC_EOS,USDT_DGB,USDT_BCHSV,BTC_OMNI,ETH_ZEC,BTC_EOS,BTC_KNC,BTC_BCHSV,BTC_POLY,USDC_DASH,USDT_GNT,BTC_BCHABC,BTC_GRIN,BTC_ATOM,USDT_ATOM,USDT_BCHABC,BTC_LSK,ETH_ZRX,BTC_GAS,BTC_BAT,BTC_BNT,USDT_TRX,BTC_FCT,USDT_ZEC,BTC_GNT,USDT_DOGE,USDC_USDT" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "Yobit", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": false, - "delimiter": "_", - "separator": "-" - }, - "configFormat": { - "uppercase": true, - "delimiter": "_" - }, - "useGlobalFormat": true, - "lastUpdated": 1566798411, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "LTC_BTC,ETH_BTC,BTC_USD,DASH_BTC", - "available": "DASH_BTC,WAVES_BTC,LSK_BTC,LIZA_BTC,BCC_BTC,ETH_BTC,LTC_BTC,TRX_BTC,DOGE_BTC,VNTX_BTC,SW_BTC,ZEC_BTC,DASH_ETH,WAVES_ETH,LSK_ETH,LIZA_ETH,BCC_ETH,LTC_ETH,TRX_ETH,DOGE_ETH,VNTX_ETH,SW_ETH,ZEC_ETH,DASH_DOGE,WAVES_DOGE,LSK_DOGE,LIZA_DOGE,BCC_DOGE,LTC_DOGE,TRX_DOGE,VNTX_DOGE,SW_DOGE,ZEC_DOGE,DASH_USD,WAVES_USD,LSK_USD,LIZA_USD,BCC_USD,LTC_USD,TRX_USD,VNTX_USD,SW_USD,ZEC_USD,ETH_USD,BTC_USD,DASH_RUR,WAVES_BTC,WAVES_RUR,LSK_RUR,LIZA_RUR,BCC_RUR,LTC_RUR,TRX_RUR,VNTX_RUR,SW_RUR,ETH_RUR,ZEC_RUR" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": false, - "websocketCapabilities": {} + "name": "Yobit", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_", + "separator": "-" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "lastUpdated": 1566798411, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "LTC_BTC,ETH_BTC,BTC_USD,DASH_BTC", + "available": "DASH_BTC,WAVES_BTC,LSK_BTC,LIZA_BTC,BCC_BTC,ETH_BTC,LTC_BTC,TRX_BTC,DOGE_BTC,VNTX_BTC,SW_BTC,ZEC_BTC,DASH_ETH,WAVES_ETH,LSK_ETH,LIZA_ETH,BCC_ETH,LTC_ETH,TRX_ETH,DOGE_ETH,VNTX_ETH,SW_ETH,ZEC_ETH,DASH_DOGE,WAVES_DOGE,LSK_DOGE,LIZA_DOGE,BCC_DOGE,LTC_DOGE,TRX_DOGE,VNTX_DOGE,SW_DOGE,ZEC_DOGE,DASH_USD,WAVES_USD,LSK_USD,LIZA_USD,BCC_USD,LTC_USD,TRX_USD,VNTX_USD,SW_USD,ZEC_USD,ETH_USD,BTC_USD,DASH_RUR,WAVES_BTC,WAVES_RUR,LSK_RUR,LIZA_RUR,BCC_RUR,LTC_RUR,TRX_RUR,VNTX_RUR,SW_RUR,ETH_RUR,ZEC_RUR" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": false, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": false, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": false, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "ZB", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": false, - "delimiter": "_" - }, - "configFormat": { - "uppercase": true, - "delimiter": "_" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC_USDT,ETH_USDT", - "available": "TRUE_BTC,LTC_USDT,ACC_USDT,MANA_BTC,GRIN_USDT,HC_USDT,ADA_BTC,SLT_BTC,TRX_QC,CRO_USDT,BCHABC_USDT,MANA_QC,OMG_USDT,BTS_QC,TRUE_USDT,BCW_QC,TOPC_QC,ZB_BTC,SAFE_QC,BTH_USDT,MCO_USDT,UBTC_QC,XEM_USDT,BTC_QC,B91_QC,BCW_USDT,MCO_QC,BTP_QC,CHAT_USDT,VSYS_BTC,HSR_USDT,DDM_USDT,MANA_USDT,XLM_BTC,ETC_QC,KAN_QC,ZRX_USDT,TUSD_USDT,SBTC_USDT,NXWC_USDT,BRC_BTC,PDX_BTC,XUC_QC,ETH_QC,EOSDAC_USDT,BTN_USDT,GNT_QC,BRC_USDT,LTC_BTC,SUB_QC,INK_USDT,EOSDAC_QC,TRUE_QC,XLM_USDT,BTS_BTC,HLC_QC,YTNB_USDT,AE_BTC,PDX_QC,DOGE_USDT,XWC_USDT,ADA_QC,BSV_USDT,XEM_BTC,DDM_QC,LBTC_USDT,SAFE_USDT,OMG_QC,EDO_USDT,XMR_QC,MITH_QC,TV_QC,TOPC_USDT,CRO_QC,LVN_USDT,HOTC_QC,TRX_USDT,XLM_QC,HLC_USDT,QUN_USDT,BTM_BTC,TV_USDT,NEO_BTC,QTUM_USDT,ZRX_QC,SNT_USDT,XWC_QC,HC_QC,ENTC_USDT,XTZ_USDT,BITCNY_QC,EPC_QC,KAN_BTC,AAA_QC,PAX_USDT,BCHABC_QC,QTUM_BTC,HPY_QC,GNT_USDT,BCD_QC,SLT_QC,BAT_BTC,HC_BTC,BCHSV_QC,HSR_BTC,AE_QC,HX_QC,INK_QC,LBTC_QC,BDS_QC,XRP_BTC,VSYS_ZB,ETC_USDT,OMG_BTC,1ST_USDT,SLT_USDT,BAR_USDT,NEO_USDT,BCD_USDT,MTL_USDT,XEM_QC,BSV_QC,BTP_USDT,TRX_BTC,XRP_QC,ETZ_QC,LVN_QC,BTM_QC,DASH_BTC,BTN_QC,LBTC_BTC,EPC_BTC,FN_QC,PAX_QC,NWT_USDT,XMR_USDT,ICX_BTC,1ST_QC,BCX_USDT,EOS_USDT,KNC_QC,EOS_BTC,BTM_USDT,USDT_QC,BAT_USDT,NEO_QC,LTC_QC,ETZ_USDT,CDC_QC,ICX_QC,RCN_USDT,BTC_USDT,BRC_QC,TSR_USDT,ETH_BTC,GRIN_QC,SNT_QC,ICX_USDT,ETC_BTC,TV_BTC,ADA_USDT,GNT_BTC,CDC_USDT,B91_USDT,DASH_QC,PDX_USDT,GRAM_USDT,GRAM_QC,HSR_QC,HOTC_USDT,XRP_USDT,VSYS_QC,LEO_USDT,HX_USDT,QTUM_QC,ZB_QC,ETH_USDT,BCHSV_USDT,QUN_QC,BCX_QC,ZB_USDT,HPY_USDT,DOGE_BTC,BCX_BTC,SNT_BTC,DASH_USDT,ZRX_BTC,BTH_QC,KNC_USDT,UBTC_USDT,BTS_USDT,BITE_BTC,DOGE_QC,BAT_QC,EOS_QC,GRAM_BTC" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} + "name": "ZB", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": false, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot" + ], + "pairs": { + "spot": { + "enabled": "BTC_USDT,ETH_USDT", + "available": "TRUE_BTC,LTC_USDT,ACC_USDT,MANA_BTC,GRIN_USDT,HC_USDT,ADA_BTC,SLT_BTC,TRX_QC,CRO_USDT,BCHABC_USDT,MANA_QC,OMG_USDT,BTS_QC,TRUE_USDT,BCW_QC,TOPC_QC,ZB_BTC,SAFE_QC,BTH_USDT,MCO_USDT,UBTC_QC,XEM_USDT,BTC_QC,B91_QC,BCW_USDT,MCO_QC,BTP_QC,CHAT_USDT,VSYS_BTC,HSR_USDT,DDM_USDT,MANA_USDT,XLM_BTC,ETC_QC,KAN_QC,ZRX_USDT,TUSD_USDT,SBTC_USDT,NXWC_USDT,BRC_BTC,PDX_BTC,XUC_QC,ETH_QC,EOSDAC_USDT,BTN_USDT,GNT_QC,BRC_USDT,LTC_BTC,SUB_QC,INK_USDT,EOSDAC_QC,TRUE_QC,XLM_USDT,BTS_BTC,HLC_QC,YTNB_USDT,AE_BTC,PDX_QC,DOGE_USDT,XWC_USDT,ADA_QC,BSV_USDT,XEM_BTC,DDM_QC,LBTC_USDT,SAFE_USDT,OMG_QC,EDO_USDT,XMR_QC,MITH_QC,TV_QC,TOPC_USDT,CRO_QC,LVN_USDT,HOTC_QC,TRX_USDT,XLM_QC,HLC_USDT,QUN_USDT,BTM_BTC,TV_USDT,NEO_BTC,QTUM_USDT,ZRX_QC,SNT_USDT,XWC_QC,HC_QC,ENTC_USDT,XTZ_USDT,BITCNY_QC,EPC_QC,KAN_BTC,AAA_QC,PAX_USDT,BCHABC_QC,QTUM_BTC,HPY_QC,GNT_USDT,BCD_QC,SLT_QC,BAT_BTC,HC_BTC,BCHSV_QC,HSR_BTC,AE_QC,HX_QC,INK_QC,LBTC_QC,BDS_QC,XRP_BTC,VSYS_ZB,ETC_USDT,OMG_BTC,1ST_USDT,SLT_USDT,BAR_USDT,NEO_USDT,BCD_USDT,MTL_USDT,XEM_QC,BSV_QC,BTP_USDT,TRX_BTC,XRP_QC,ETZ_QC,LVN_QC,BTM_QC,DASH_BTC,BTN_QC,LBTC_BTC,EPC_BTC,FN_QC,PAX_QC,NWT_USDT,XMR_USDT,ICX_BTC,1ST_QC,BCX_USDT,EOS_USDT,KNC_QC,EOS_BTC,BTM_USDT,USDT_QC,BAT_USDT,NEO_QC,LTC_QC,ETZ_USDT,CDC_QC,ICX_QC,RCN_USDT,BTC_USDT,BRC_QC,TSR_USDT,ETH_BTC,GRIN_QC,SNT_QC,ICX_USDT,ETC_BTC,TV_BTC,ADA_USDT,GNT_BTC,CDC_USDT,B91_USDT,DASH_QC,PDX_USDT,GRAM_USDT,GRAM_QC,HSR_QC,HOTC_USDT,XRP_USDT,VSYS_QC,LEO_USDT,HX_USDT,QTUM_QC,ZB_QC,ETH_USDT,BCHSV_USDT,QUN_QC,BCX_QC,ZB_USDT,HPY_USDT,DOGE_BTC,BCX_BTC,SNT_BTC,DASH_USDT,ZRX_BTC,BTH_QC,KNC_USDT,UBTC_USDT,BTS_USDT,BITE_BTC,DOGE_QC,BAT_QC,EOS_QC,GRAM_BTC" + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] - }, - { - "name": "Bitmex", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "assetTypes": [ - "perpetualcontract", - "futures", - "downsideprofitcontract", - "upsideprofitcontract" - ], - "pairs": { - "downsideprofitcontract": { - "enabled": "XBT7D_D95", - "available": "XBT7D_D95", - "requestFormat": { - "uppercase": true, - "delimiter": "_" - }, - "configFormat": { - "uppercase": true, - "delimiter": "_" - } - }, - "futures": { - "enabled": "BCHZ19", - "available": "XRPZ19,BCHZ19,ADAZ19,EOSZ19,TRXZ19,XBTZ19,ETHZ19,LTCZ19", - "requestFormat": { - "uppercase": true - }, - "configFormat": { - "uppercase": true - } - }, - "perpetualcontract": { - "enabled": "ETHUSD", - "available": "XBTUSD,ETHUSD", - "requestFormat": { - "uppercase": true - }, - "configFormat": { - "uppercase": true - } - }, - "upsideprofitcontract": { - "enabled": "XBT7D_U105", - "available": "XBT7D_U105", - "requestFormat": { - "uppercase": true, - "delimiter": "_" - }, - "configFormat": { - "uppercase": true, - "delimiter": "_" - } - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true - }, - "websocketAPI": true, - "websocketCapabilities": {} - }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false + "name": "Bitmex", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "assetTypes": [ + "perpetualcontract", + "futures", + "downsideprofitcontract", + "upsideprofitcontract" + ], + "pairs": { + "downsideprofitcontract": { + "enabled": "XBT7D_D95", + "available": "XBT7D_D95", + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + } + }, + "futures": { + "enabled": "BCHZ19", + "available": "XRPZ19,BCHZ19,ADAZ19,EOSZ19,TRXZ19,XBTZ19,ETHZ19,LTCZ19", + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + } + }, + "perpetualcontract": { + "enabled": "ETHUSD", + "available": "XBTUSD,ETHUSD", + "requestFormat": { + "uppercase": true + }, + "configFormat": { + "uppercase": true + } + }, + "upsideprofitcontract": { + "enabled": "XBT7D_U105", + "available": "XBT7D_U105", + "requestFormat": { + "uppercase": true, + "delimiter": "_" + }, + "configFormat": { + "uppercase": true, + "delimiter": "_" + } + } + } + }, + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": false + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] } - }, - "bankAccounts": [ + ], + "bankAccounts": [ { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" + "enabled": false, + "bankName": "test", + "bankAddress": "test", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "TestAccount", + "accountNumber": "0234", + "swiftCode": "91272837", + "iban": "98218738671897", + "supportedCurrencies": "USD", + "supportedExchanges": "Kraken,Bitstamp" } - ] - } - ], - "bankAccounts": [ - { - "enabled": false, - "bankName": "test", - "bankAddress": "test", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "TestAccount", - "accountNumber": "0234", - "swiftCode": "91272837", - "iban": "98218738671897", - "supportedCurrencies": "USD", - "supportedExchanges": "Kraken,Bitstamp" - } - ] -} \ No newline at end of file + ] +}