From 6105071114e6db35be6e149b1c26af7e2f28a180 Mon Sep 17 00:00:00 2001 From: Samuael A <39623015+samuael@users.noreply.github.com> Date: Wed, 27 Sep 2023 05:09:38 +0000 Subject: [PATCH] exchanges: Add Kucoin support (#1102) * init * updates config * wrapper configuration * updates exchange readme * adds SendAuthHTTPRequest and SendHTTPRequest * adds ratelimit file * adds test case and minor fixes * improve error handling * update testcases and improve GetSymbols API * adds SPOT API's * minor fix * WIP * WIP * adds test case * adds check in test case * fixes in Auth. HTTP * improvements * adds trade, kline support and testcases * adds SPOT API and testcases for same * adds SPOT API and testcases * adds SPOT API and testcase * WIP * adds API's * adds API's * adds test cases * adds comment to exported data types * adds API and test cases * adds API * adds API * rearrange functions * WIP: adds API * adds API for Post Order SPOT * adds API and few fixes * fixes * WIP * WIP * add PostBulkOrder API and its test case * fix issues * adds cancel order APIs and test cases for same * add minor test fixes * add API * adds API * fixes * add API * adds API and test cases * fix test * adds API * adds test * fix test * adds API and test * adds deposit API and test cases * WIP * adds API and test cases * WIP * WIP * add public future API and test cases * WIP * remove v2 API and replace them with v1 * update test cases * adds future order API and test cases * adds futures order API * adds API * add API and test cases * adds API and test cases * adds API and test cases * adds API and test cases * Adding wrapper functions * Fix on wrapper function * Adding websocket support * Complete addressing WS push datas * Adding spot push data unit tests * adding futures websocket push data handlers * Adding futures websocket push data handlers * Added unit tests * Updating unit tests * Updating wrapper and unit test functions * Adding missing wrapper functions and code cleaning up * Resolved linter issues * Fixing websocket issues * Fixing websocket issues * Slight fix on config_example file * Minor update * Basic nits updates * Fix minor linter issues * Minor update * Minor unit test update * Minor unit test update * Code update and linter issues fix * Removed unnecessary type conversion codes * Monor update based on review comment * Fix based on review comments * Adding rate-limiter * Websocket update and overall minor fixes * Removed IsAssetTypeEnabled method implementation * Fix connection and formatting issues * Updating orderbook issues * Very minor label fix * Minor error returning fix * code cleaning up and minor spelling fix * Updates on unit test * Update on unit tests and slight code structure * unit test update * orderbook update and minor fix * fix on race * Mini linter fix * fix minor parameter and unit test issues * handler funcs and models update * Fixing websocket and unit test issues * order side string for active orders * Fix on websocket and unit tests * Minor type changes * Minor Orderbook fix and unit test update * Small fix on orderbook * Updating orderbook functionality * FIx on websocket orderbook handlers * Small update on kucoin websocket * fix missed review comments * fix based on review comments * Updating websocket orderbook and fixing unit tests * Minor fixes * unit test update * Updating unit test according to enabled asset type * toggle canManipulateRealOrders const * Unit test update * Fix minor issues * minor fix * documentation fix * wrapper coverage and unused params fix * testing and minor changes * documentation, websocket and unit test update * minor linter fix * Websocket spot/margin subscription update * minor ticker update fix * minor fixes on endpoints * timestamp and number convert method and unit tests * timestamp convert minor update * minor type and conversion fix * create a common timestamp convert and fix minor issues * linter and ticker fix * Updating unit tests and order placing endpoint methods * Added a pairs check * Fix config test error * rm unused error variable * Fix source of linter issue * code update: convert, wrapper and websocket fix * minor code update * Websocket code and unit tests update * Websocket ticker ask/bid type change and small error msg fix * docs update * fix: websocket orderbook handling * change orderbook channel to marketOrderbookLevel2Channels and fix websocket orderbook update * Minor func rename and reciever change * Minor orderbook unit test issue fix * comment: about why we used a random delimiter '-' for futures * update config files and FetchTradablePair func for futures pairs * futures config pairs update * remove ConnextionMonitorDelay from websocket setup * fix on types and futures pair conversion * updating config pairs * change NewPairFromString to DeriveFrom * unit tests update * unit tests update * Added TickerBatching * added GetStandardConfig to GetDefaultConfig --------- Co-authored-by: Jaydeep Rajpurohit --- .gitignore | 2 +- CONTRIBUTORS | 6 +- README.md | 15 +- .../exchanges_trade_readme.tmpl | 1 + .../root_templates/root_readme.tmpl | 1 + .../wrapperconfig.json | 5 + common/convert/convert.go | 54 + common/convert/convert_test.go | 83 + config_example.json | 107 + currency/code_types.go | 8 +- docs/ADD_NEW_EXCHANGE.md | 2 + docs/MULTICHAIN_TRANSFER_SUPPORT.md | 1 + docs/OHLCV.md | 3 +- engine/exchange_manager.go | 3 + exchanges/kucoin/README.md | 140 + exchanges/kucoin/kucoin.go | 1851 +++++++++++++ exchanges/kucoin/kucoin_convert.go | 67 + exchanges/kucoin/kucoin_futures.go | 821 ++++++ exchanges/kucoin/kucoin_futures_types.go | 451 +++ exchanges/kucoin/kucoin_ratelimit.go | 244 ++ exchanges/kucoin/kucoin_test.go | 2421 +++++++++++++++++ exchanges/kucoin/kucoin_types.go | 1498 ++++++++++ exchanges/kucoin/kucoin_websocket.go | 1912 +++++++++++++ exchanges/kucoin/kucoin_wrapper.go | 1502 ++++++++++ exchanges/order/order_types.go | 11 + exchanges/support.go | 1 + exchanges/trade/README.md | 1 + testdata/configtest.json | 107 + testdata/exchangelist.csv | 1 + 29 files changed, 11306 insertions(+), 13 deletions(-) create mode 100644 exchanges/kucoin/README.md create mode 100644 exchanges/kucoin/kucoin.go create mode 100644 exchanges/kucoin/kucoin_convert.go create mode 100644 exchanges/kucoin/kucoin_futures.go create mode 100644 exchanges/kucoin/kucoin_futures_types.go create mode 100644 exchanges/kucoin/kucoin_ratelimit.go create mode 100644 exchanges/kucoin/kucoin_test.go create mode 100644 exchanges/kucoin/kucoin_types.go create mode 100644 exchanges/kucoin/kucoin_websocket.go create mode 100644 exchanges/kucoin/kucoin_wrapper.go diff --git a/.gitignore b/.gitignore index 4b5259d5..ab1aa87d 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,4 @@ __debug_bin # Coverage reports coverage.txt -wrapperconfig.json \ No newline at end of file +wrapperconfig.json diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 728f268a..907db755 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -6,8 +6,8 @@ gloriousCode | https://github.com/gloriousCode dependabot[bot] | https://github.com/apps/dependabot dependabot-preview[bot] | https://github.com/apps/dependabot-preview xtda | https://github.com/xtda -lrascao | https://github.com/lrascao gbjk | https://github.com/gbjk +lrascao | https://github.com/lrascao Rots | https://github.com/Rots vazha | https://github.com/vazha ydm | https://github.com/ydm @@ -19,12 +19,12 @@ 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 dackroyd | https://github.com/dackroyd cranktakular | https://github.com/cranktakular khcchiu | https://github.com/khcchiu -samuael | https://github.com/samuael -woshidama323 | https://github.com/woshidama323 yangrq1018 | https://github.com/yangrq1018 +woshidama323 | https://github.com/woshidama323 crackcomm | https://github.com/crackcomm azhang | https://github.com/azhang andreygrehov | https://github.com/andreygrehov diff --git a/README.md b/README.md index 7d389628..31e90575 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | Huobi.Pro | Yes | Yes | NA | | ItBit | Yes | NA | No | | Kraken | Yes | Yes | NA | +| Kucoin | Yes | Yes | NA | | Lbank | Yes | No | NA | | Okcoin | Yes | Yes | No | | Okx | Yes | Yes | NA | @@ -143,14 +144,14 @@ Binaries will be published once the codebase reaches a stable condition. |User|Contribution Amount| |--|--| -| [thrasher-](https://github.com/thrasher-) | 682 | -| [shazbert](https://github.com/shazbert) | 299 | -| [gloriousCode](https://github.com/gloriousCode) | 217 | -| [dependabot[bot]](https://github.com/apps/dependabot) | 202 | +| [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 | | [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 88 | | [xtda](https://github.com/xtda) | 47 | +| [gbjk](https://github.com/gbjk) | 35 | | [lrascao](https://github.com/lrascao) | 27 | -| [gbjk](https://github.com/gbjk) | 26 | | [Rots](https://github.com/Rots) | 15 | | [vazha](https://github.com/vazha) | 15 | | [ydm](https://github.com/ydm) | 15 | @@ -162,12 +163,12 @@ Binaries will be published once the codebase reaches a stable condition. | [geseq](https://github.com/geseq) | 8 | | [Beadko](https://github.com/Beadko) | 6 | | [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 | -| [samuael](https://github.com/samuael) | 5 | +| [yangrq1018](https://github.com/yangrq1018) | 4 | | [woshidama323](https://github.com/woshidama323) | 3 | -| [yangrq1018](https://github.com/yangrq1018) | 3 | | [crackcomm](https://github.com/crackcomm) | 3 | | [azhang](https://github.com/azhang) | 2 | | [andreygrehov](https://github.com/andreygrehov) | 2 | diff --git a/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl b/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl index f9063d07..51b79314 100644 --- a/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl +++ b/cmd/documentation/exchanges_templates/exchanges_trade_readme.tmpl @@ -62,6 +62,7 @@ _b in this context is an `IBotExchange` implemented struct_ | Huobi.Pro | Yes | Yes | No | | ItBit | Yes | NA | No | | Kraken | Yes | Yes | No | +| Kucoin | Yes | No | Yes | | Lbank | Yes | No | Yes | | Okcoin | Yes | Yes | Yes | | Okx | Yes | Yes | Yes | diff --git a/cmd/documentation/root_templates/root_readme.tmpl b/cmd/documentation/root_templates/root_readme.tmpl index 8fea1ee2..d1346219 100644 --- a/cmd/documentation/root_templates/root_readme.tmpl +++ b/cmd/documentation/root_templates/root_readme.tmpl @@ -40,6 +40,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | Huobi.Pro | Yes | Yes | NA | | ItBit | Yes | NA | No | | Kraken | Yes | Yes | NA | +| Kucoin | Yes | Yes | NA | | Lbank | Yes | No | NA | | Okcoin | Yes | Yes | No | | Okx | Yes | Yes | NA | diff --git a/cmd/exchange_wrapper_issues/wrapperconfig.json b/cmd/exchange_wrapper_issues/wrapperconfig.json index 6b09b519..8ca64f4f 100644 --- a/cmd/exchange_wrapper_issues/wrapperconfig.json +++ b/cmd/exchange_wrapper_issues/wrapperconfig.json @@ -136,6 +136,11 @@ "secret": "Secret", "otpSecret": "-" }, + "kucoin":{ + "key": "Key", + "secret": "Secret", + "otpSecret": "-" + }, "lbank": { "key": "Key", "secret": "Secret", diff --git a/common/convert/convert.go b/common/convert/convert.go index dc91a96d..070f1fed 100644 --- a/common/convert/convert.go +++ b/common/convert/convert.go @@ -2,6 +2,7 @@ package convert import ( "bytes" + "encoding/json" "errors" "fmt" "math" @@ -242,3 +243,56 @@ func (f StringToFloat64) Float64() float64 { func (f StringToFloat64) Decimal() decimal.Decimal { return decimal.NewFromFloat(float64(f)) } + +// ExchangeTime provides timestamp to time conversion method. +type ExchangeTime time.Time + +// UnmarshalJSON is custom type json unmarshaller for ExchangeTime +func (k *ExchangeTime) UnmarshalJSON(data []byte) error { + var timestamp interface{} + err := json.Unmarshal(data, ×tamp) + if err != nil { + return err + } + var standard int64 + switch value := timestamp.(type) { + case string: + if value == "" { + // Setting the time to zero value because some timestamp fields could return an empty string while there is no error + // So, in such cases, Time returns zero timestamp. + break + } + standard, err = strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + case int64: + standard = value + case float64: + // Warning: converting float64 to int64 instance may create loss of precision in the timestamp information. + // be aware or consider customizing this section if found necessary. + standard = int64(value) + case nil: + // for some exchange timestamp fields, if the timestamp information is not specified, + // the data is 'nil' instead of zero value string or integer value. + default: + return fmt.Errorf("unsupported timestamp type %T", timestamp) + } + + switch { + case standard == 0: + *k = ExchangeTime(time.Time{}) + case standard >= 1e13: + *k = ExchangeTime(time.Unix(standard/1e9, standard%1e9)) + case standard > 9999999999: + *k = ExchangeTime(time.UnixMilli(standard)) + default: + *k = ExchangeTime(time.Unix(standard, 0)) + } + return nil +} + +// Time returns a time.Time instance from ExchangeTime instance object. +func (k ExchangeTime) Time() time.Time { + return time.Time(k) +} diff --git a/common/convert/convert_test.go b/common/convert/convert_test.go index c84318ac..c77b2123 100644 --- a/common/convert/convert_test.go +++ b/common/convert/convert_test.go @@ -400,3 +400,86 @@ func BenchmarkStringToFloat64(b *testing.B) { } } } + +func TestExchangeTimeUnmarshalJSON(t *testing.T) { + t.Parallel() + unmarshaledResult := &struct { + Timestamp ExchangeTime `json:"ts"` + }{} + data1 := `{"ts":""}` + result := time.Time{} + err := json.Unmarshal([]byte(data1), &unmarshaledResult) + if err != nil { + t.Fatal(err) + } else if !unmarshaledResult.Timestamp.Time().Equal(result) { + t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result) + } + data2 := `{"ts":"1685564775371"}` + result = time.UnixMilli(1685564775371) + err = json.Unmarshal([]byte(data2), &unmarshaledResult) + if err != nil { + t.Fatal(err) + } else if !unmarshaledResult.Timestamp.Time().Equal(result) { + t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result) + } + data3 := `{"ts":1685564775371}` + err = json.Unmarshal([]byte(data3), &unmarshaledResult) + if err != nil { + t.Fatal(err) + } else if !unmarshaledResult.Timestamp.Time().Equal(result) { + t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result) + } + data4 := `{"ts":"1685564775"}` + result = time.Unix(1685564775, 0) + err = json.Unmarshal([]byte(data4), &unmarshaledResult) + if err != nil { + t.Fatal(err) + } else if !unmarshaledResult.Timestamp.Time().Equal(result) { + t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result) + } + data5 := `{"ts":1685564775}` + err = json.Unmarshal([]byte(data5), &unmarshaledResult) + if err != nil { + t.Fatal(err) + } else if !unmarshaledResult.Timestamp.Time().Equal(result) { + t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result) + } + data6 := `{"ts":"1685564775371320000"}` + result = time.Unix(int64(1685564775371320000)/1e9, int64(1685564775371320000)%1e9) + err = json.Unmarshal([]byte(data6), &unmarshaledResult) + if err != nil { + t.Fatal(err) + } else if !unmarshaledResult.Timestamp.Time().Equal(result) { + t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result) + } + data7 := `{"ts":"abcdefg"}` + err = json.Unmarshal([]byte(data7), &unmarshaledResult) + if err == nil { + t.Fatal("expecting error but found nil") + } + data8 := `{"ts":0}` + result = time.Time{} + err = json.Unmarshal([]byte(data8), &unmarshaledResult) + if err != nil { + t.Fatal(err) + } else if !unmarshaledResult.Timestamp.Time().Equal(result) { + t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result) + } +} + +// 2239239 516.1 ns/op 424 B/op 9 allocs/op +func BenchmarkExchangeTimeUnmarshaling(b *testing.B) { + unmarshaledResult := &struct { + Timestamp ExchangeTime `json:"ts"` + }{} + data5 := `{"ts":1685564775}` + result := time.Unix(1685564775, 0) + var err error + for i := 0; i < b.N; i++ { + if err = json.Unmarshal([]byte(data5), &unmarshaledResult); err != nil { + b.Fatal(err) + } else if !unmarshaledResult.Timestamp.Time().Equal(result) { + b.Fatalf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result) + } + } +} diff --git a/config_example.json b/config_example.json index 9b5ca286..a1544fda 100644 --- a/config_example.json +++ b/config_example.json @@ -1343,6 +1343,113 @@ } ] }, + { + "name": "Kucoin", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "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", + "available": "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", + "available": "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": "" + } + ] + }, { "name": "EXMO", "enabled": true, diff --git a/currency/code_types.go b/currency/code_types.go index 5a57fc8d..8f9dbe34 100644 --- a/currency/code_types.go +++ b/currency/code_types.go @@ -1679,7 +1679,6 @@ var ( YFI = NewCode("YFI") BAL = NewCode("BAL") UMA = NewCode("UMA") - KDA = NewCode("KDA") SNX = NewCode("SNX") CRV = NewCode("CRV") OXT = NewCode("OXT") @@ -3008,6 +3007,13 @@ var ( USDFL = NewCode("USDFL") FLUSD = NewCode("FLUSD") DUSD = NewCode("DUSD") + USDD = NewCode("USDD") + KDA = NewCode("KDA") + XCN = NewCode("XCN") + TEL = NewCode("TEL") + XDC = NewCode("XDC") + MHC = NewCode("MHC") + OXEN = NewCode("OXEN") STETH = NewCode("STETH") stables = Currencies{ diff --git a/docs/ADD_NEW_EXCHANGE.md b/docs/ADD_NEW_EXCHANGE.md index 4fe35527..3abf11a2 100644 --- a/docs/ADD_NEW_EXCHANGE.md +++ b/docs/ADD_NEW_EXCHANGE.md @@ -215,6 +215,7 @@ Yes means supported, No means not yet implemented and NA means protocol unsuppor | Huobi.Pro | Yes | Yes | NA | | ItBit | Yes | NA | No | | Kraken | Yes | Yes | NA | +| Kucoin | Yes | Yes | No | | Lbank | Yes | No | NA | | Okcoin | Yes | Yes | No | | Okx | Yes | Yes | NA | @@ -245,6 +246,7 @@ var Exchanges = []string{ "huobi", "itbit", "kraken", + "kucoin", "lbank", "okcoin", "okx", diff --git a/docs/MULTICHAIN_TRANSFER_SUPPORT.md b/docs/MULTICHAIN_TRANSFER_SUPPORT.md index e3fd859f..ccb33596 100644 --- a/docs/MULTICHAIN_TRANSFER_SUPPORT.md +++ b/docs/MULTICHAIN_TRANSFER_SUPPORT.md @@ -63,6 +63,7 @@ $ ./gctcli withdrawcryptofunds --exchange=binance --currency=USDT --address=TJU9 | Huobi.Pro | Yes | Yes | | | ItBit | No | No | | | Kraken | Yes | Yes | Front-end and API don't match total available transfer chains | +| Kucoin | Yes | Yes | | | Lbank | No | No | | | Okcoin | Yes | Yes | | | Okx | Yes | Yes | | diff --git a/docs/OHLCV.md b/docs/OHLCV.md index f636badd..2565fe96 100644 --- a/docs/OHLCV.md +++ b/docs/OHLCV.md @@ -84,7 +84,8 @@ A helper tool [cmd/dbseed](../cmd/dbseed/README.md) has been created for assisti | HitBTC | Y | | Huobi | Y | | itBIT | | -| Kraken | Y | +| Kraken | Y | +| Kucoin | Y | | lBank | Y | | Okcoin | Y | | Okx | Y | diff --git a/engine/exchange_manager.go b/engine/exchange_manager.go index 9ca9b068..fb5f66bd 100644 --- a/engine/exchange_manager.go +++ b/engine/exchange_manager.go @@ -28,6 +28,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/huobi" "github.com/thrasher-corp/gocryptotrader/exchanges/itbit" "github.com/thrasher-corp/gocryptotrader/exchanges/kraken" + "github.com/thrasher-corp/gocryptotrader/exchanges/kucoin" "github.com/thrasher-corp/gocryptotrader/exchanges/lbank" "github.com/thrasher-corp/gocryptotrader/exchanges/okcoin" "github.com/thrasher-corp/gocryptotrader/exchanges/okx" @@ -196,6 +197,8 @@ func (m *ExchangeManager) NewExchangeByName(name string) (exchange.IBotExchange, exch = new(itbit.ItBit) case "kraken": exch = new(kraken.Kraken) + case "kucoin": + exch = new(kucoin.Kucoin) case "lbank": exch = new(lbank.Lbank) case "okcoin": diff --git a/exchanges/kucoin/README.md b/exchanges/kucoin/README.md new file mode 100644 index 00000000..ab42b51c --- /dev/null +++ b/exchanges/kucoin/README.md @@ -0,0 +1,140 @@ +# GoCryptoTrader package Kucoin + + + + +[![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/kucoin) +[![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 kucoin 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) + +## Kucoin Exchange + +### Current Features + ++ REST Support ++ Websocket Support + +### How to enable + ++ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example) + ++ Individual package example below: + +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### How to do REST public/private calls + ++ If enabled via "configuration".json file the exchange will be added to the +IBotExchange array in the ```go var bot Bot``` and you will only be able to use +the wrapper interface functions for accessing exchange data. View routines.go +for an example of integration usage with GoCryptoTrader. Rudimentary example +below: + +main.go +```go +var b exchange.IBotExchange + +for i := range bot.Exchanges { + if bot.Exchanges[i].GetName() == "Kucoin" { + b = bot.Exchanges[i] + } +} + +// Public calls - wrapper functions + +// Fetches current ticker information +tick, err := b.FetchTicker() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := b.FetchOrderbook() +if err != nil { + // Handle error +} + +// Private calls - wrapper functions - make sure your APIKEY and APISECRET are +// set and AuthenticatedAPISupport is set to true + +// Fetches current account information +accountInfo, err := b.GetAccountInfo() +if err != nil { + // Handle error +} +``` + ++ If enabled via individually importing package, rudimentary example below: + +```go +// Public calls + +// Fetches current ticker information +ticker, err := b.GetTicker() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := b.GetOrderBook() +if err != nil { + // Handle error +} + +// Private calls - make sure your APIKEY and APISECRET are set and +// AuthenticatedAPISupport is set to true + +// GetUserInfo returns account info +accountInfo, err := b.GetUserInfo(...) +if err != nil { + // Handle error +} + +// Submits an order and the exchange and returns its tradeID +tradeID, err := b.Trade(...) +if err != nil { + // Handle error +} +``` + +### How to do Websocket public/private calls + +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/exchanges/kucoin/kucoin.go b/exchanges/kucoin/kucoin.go new file mode 100644 index 00000000..0995e854 --- /dev/null +++ b/exchanges/kucoin/kucoin.go @@ -0,0 +1,1851 @@ +package kucoin + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" +) + +// Kucoin is the overarching type across this package +type Kucoin struct { + exchange.Base + obm *orderbookManager +} + +var locker sync.Mutex + +const ( + kucoinAPIURL = "https://api.kucoin.com/api" + kucoinAPIKeyVersion = "2" + + // Public endpoints + kucoinGetSymbols = "/v2/symbols" + kucoinGetTicker = "/v1/market/orderbook/level1" + kucoinGetAllTickers = "/v1/market/allTickers" + kucoinGet24hrStats = "/v1/market/stats" + kucoinGetMarketList = "/v1/markets" + kucoinGetPartOrderbook20 = "/v1/market/orderbook/level2_20" + kucoinGetPartOrderbook100 = "/v1/market/orderbook/level2_100" + kucoinGetTradeHistory = "/v1/market/histories" + kucoinGetKlines = "/v1/market/candles" + kucoinGetCurrencies = "/v1/currencies" + kucoinGetCurrency = "/v2/currencies/" + kucoinGetFiatPrice = "/v1/prices" + kucoinGetMarkPrice = "/v1/mark-price/%s/current" + kucoinGetMarginConfiguration = "/v1/margin/config" + kucoinGetServerTime = "/v1/timestamp" + kucoinGetServiceStatus = "/v1/status" + + // Authenticated endpoints + kucoinGetOrderbook = "/v3/market/orderbook/level2" + kucoinGetMarginAccount = "/v1/margin/account" + kucoinGetMarginRiskLimit = "/v1/risk/limit/strategy" + kucoinBorrowOrder = "/v1/margin/borrow" + kucoinGetOutstandingRecord = "/v1/margin/borrow/outstanding" + kucoinGetRepaidRecord = "/v1/margin/borrow/repaid" + kucoinOneClickRepayment = "/v1/margin/repay/all" + kucoinRepaySingleOrder = "/v1/margin/repay/single" + kucoinLendOrder = "/v1/margin/lend" + kucoinSetAutoLend = "/v1/margin/toggle-auto-lend" + kucoinGetActiveOrder = "/v1/margin/lend/active" + kucoinGetLendHistory = "/v1/margin/lend/done" + kucoinGetUnsettleLendOrder = "/v1/margin/lend/trade/unsettled" + kucoinGetSettleLendOrder = "/v1/margin/lend/trade/settled" + kucoinGetAccountLendRecord = "/v1/margin/lend/assets" + kucoinGetLendingMarketData = "/v1/margin/market" + kucoinGetMarginTradeData = "/v1/margin/trade/last" + + kucoinGetIsolatedMarginPairConfig = "/v1/isolated/symbols" + kucoinGetIsolatedMarginAccountInfo = "/v1/isolated/accounts" + kucoinGetSingleIsolatedMarginAccountInfo = "/v1/isolated/account/" + kucoinInitiateIsolatedMarginBorrowing = "/v1/isolated/borrow" + kucoinGetIsolatedOutstandingRepaymentRecords = "/v1/isolated/borrow/outstanding" + kucoinGetIsolatedMarginRepaymentRecords = "/v1/isolated/borrow/repaid" + kucoinInitiateIsolatedMarginQuickRepayment = "/v1/isolated/repay/all" + kucoinInitiateIsolatedMarginSingleRepayment = "/v1/isolated/repay/single" + + kucoinPostOrder = "/v1/orders" + kucoinPostMarginOrder = "/v1/margin/order" + kucoinPostBulkOrder = "/v1/orders/multi" + kucoinOrderByID = "/v1/orders/" // used by CancelSingleOrder and GetOrderByID + kucoinOrderByClientOID = "/v1/order/client-order/" // used by CancelOrderByClientOID and GetOrderByClientOID + kucoinOrders = "/v1/orders" // used by CancelAllOpenOrders and GetOrders + kucoinGetRecentOrders = "/v1/limit/orders" + + kucoinGetFills = "/v1/fills" + kucoinGetRecentFills = "/v1/limit/fills" + + kucoinStopOrder = "/v1/stop-order" + kucoinStopOrderByID = "/v1/stop-order/" + kucoinCancelAllStopOrder = "/v1/stop-order/cancel" + kucoinGetStopOrderByClientID = "/v1/stop-order/queryOrderByClientOid" + kucoinCancelStopOrderByClientID = "/v1/stop-order/cancelOrderByClientOid" + + // user info endpoints + kucoinSubUserCreated = "/v2/sub/user/created" + kucoinSubUser = "/v2/sub/user" + + kucoinSubAccountSpotAPIs = "/v1/sub/api-key" + kucoinUpdateModifySubAccountSpotAPIs = "/v1/sub/api-key/update" + + // account + kucoinAccount = "/v1/accounts" + kucoinGetAccount = "/v1/accounts/" + kucoinGetAccountLedgers = "/v1/accounts/ledgers" + kucoinUserInfo = "/v2/user-info" + kucoinGetSubAccountBalance = "/v1/sub-accounts/" + kucoinGetAggregatedSubAccountBalance = "/v1/sub-accounts" + kucoinGetTransferableBalance = "/v1/accounts/transferable" + kucoinTransferMainToSubAccount = "/v2/accounts/sub-transfer" + kucoinInnerTransfer = "/v2/accounts/inner-transfer" + + // deposit + kucoinGetDepositAddressesV2 = "/v2/deposit-addresses" + kucoinGetDepositAddressV1 = "/v1/deposit-addresses" + kucoinGetDepositList = "/v1/deposits" + kucoinGetHistoricalDepositList = "/v1/hist-deposits" + + // withdrawal + kucoinWithdrawal = "/v1/withdrawals" + kucoinGetHistoricalWithdrawalList = "/v1/hist-withdrawals" + kucoinGetWithdrawalQuotas = "/v1/withdrawals/quotas" + kucoinCancelWithdrawal = "/v1/withdrawals/" + + kucoinBasicFee = "/v1/base-fee" + kucoinTradingFee = "/v1/trade-fees" +) + +// GetSymbols gets pairs details on the exchange +func (ku *Kucoin) GetSymbols(ctx context.Context, ccy string) ([]SymbolInfo, error) { + params := url.Values{} + if ccy != "" { + params.Set("market", ccy) + } + var resp []SymbolInfo + return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, common.EncodeURLValues(kucoinGetSymbols, params), &resp) +} + +// GetTicker gets pair ticker information +func (ku *Kucoin) GetTicker(ctx context.Context, pair string) (*Ticker, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("symbol", pair) + var resp *Ticker + err := ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, common.EncodeURLValues(kucoinGetTicker, params), &resp) + if err != nil { + return nil, err + } + if resp == nil { + return nil, common.ErrNoResponse + } + return resp, nil +} + +// GetTickers gets all trading pair ticker information including 24h volume +func (ku *Kucoin) GetTickers(ctx context.Context) (*TickersResponse, error) { + var resp *TickersResponse + return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, kucoinGetAllTickers, &resp) +} + +// Get24hrStats get the statistics of the specified pair in the last 24 hours +func (ku *Kucoin) Get24hrStats(ctx context.Context, pair string) (*Stats24hrs, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("symbol", pair) + var resp *Stats24hrs + return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, common.EncodeURLValues(kucoinGet24hrStats, params), &resp) +} + +// GetMarketList get the transaction currency for the entire trading market +func (ku *Kucoin) GetMarketList(ctx context.Context) ([]string, error) { + var resp []string + return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, kucoinGetMarketList, &resp) +} + +func processOB(ob [][2]string) ([]orderbook.Item, error) { + o := make([]orderbook.Item, len(ob)) + for x := range ob { + amount, err := strconv.ParseFloat(ob[x][1], 64) + if err != nil { + return nil, err + } + price, err := strconv.ParseFloat(ob[x][0], 64) + if err != nil { + return nil, err + } + o[x] = orderbook.Item{ + Price: price, + Amount: amount, + } + } + return o, nil +} + +func constructOrderbook(o *orderbookResponse) (*Orderbook, error) { + var ( + s Orderbook + err error + ) + s.Bids, err = processOB(o.Bids) + if err != nil { + return nil, err + } + s.Asks, err = processOB(o.Asks) + if err != nil { + return nil, err + } + s.Time = o.Time.Time() + if o.Sequence != "" { + s.Sequence, err = strconv.ParseInt(o.Sequence, 10, 64) + if err != nil { + return nil, err + } + } + return &s, err +} + +// GetPartOrderbook20 gets orderbook for a specified pair with depth 20 +func (ku *Kucoin) GetPartOrderbook20(ctx context.Context, pair string) (*Orderbook, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("symbol", pair) + var o *orderbookResponse + err := ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, common.EncodeURLValues(kucoinGetPartOrderbook20, params), &o) + if err != nil { + return nil, err + } + return constructOrderbook(o) +} + +// GetPartOrderbook100 gets orderbook for a specified pair with depth 100 +func (ku *Kucoin) GetPartOrderbook100(ctx context.Context, pair string) (*Orderbook, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("symbol", pair) + var o *orderbookResponse + err := ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, common.EncodeURLValues(kucoinGetPartOrderbook100, params), &o) + if err != nil { + return nil, err + } + return constructOrderbook(o) +} + +// GetOrderbook gets full orderbook for a specified pair +func (ku *Kucoin) GetOrderbook(ctx context.Context, pair string) (*Orderbook, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("symbol", pair) + var o *orderbookResponse + err := ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, retrieveFullOrderbookEPL, http.MethodGet, common.EncodeURLValues(kucoinGetOrderbook, params), nil, &o) + if err != nil { + return nil, err + } + return constructOrderbook(o) +} + +// GetTradeHistory gets trade history of the specified pair +func (ku *Kucoin) GetTradeHistory(ctx context.Context, pair string) ([]Trade, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("symbol", pair) + var resp []Trade + return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, common.EncodeURLValues(kucoinGetTradeHistory, params), &resp) +} + +// GetKlines gets kline of the specified pair +func (ku *Kucoin) GetKlines(ctx context.Context, pair, period string, start, end time.Time) ([]Kline, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("symbol", pair) + if period == "" { + return nil, errors.New("period can not be empty") + } + if !common.StringDataContains(validPeriods, period) { + return nil, errors.New("invalid period") + } + params.Set("type", period) + if !start.IsZero() { + params.Set("startAt", strconv.FormatInt(start.Unix(), 10)) + } + if !end.IsZero() { + params.Set("endAt", strconv.FormatInt(end.Unix(), 10)) + } + var resp [][7]string + err := ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, common.EncodeURLValues(kucoinGetKlines, params), &resp) + if err != nil { + return nil, err + } + klines := make([]Kline, len(resp)) + for i := range resp { + t, err := strconv.ParseInt(resp[i][0], 10, 64) + if err != nil { + return nil, err + } + klines[i].StartTime = time.Unix(t, 0) + klines[i].Open, err = strconv.ParseFloat(resp[i][1], 64) + if err != nil { + return nil, err + } + klines[i].Close, err = strconv.ParseFloat(resp[i][2], 64) + if err != nil { + return nil, err + } + klines[i].High, err = strconv.ParseFloat(resp[i][3], 64) + if err != nil { + return nil, err + } + klines[i].Low, err = strconv.ParseFloat(resp[i][4], 64) + if err != nil { + return nil, err + } + klines[i].Volume, err = strconv.ParseFloat(resp[i][5], 64) + if err != nil { + return nil, err + } + klines[i].Amount, err = strconv.ParseFloat(resp[i][6], 64) + if err != nil { + return nil, err + } + } + return klines, nil +} + +// GetCurrencies gets list of currencies +func (ku *Kucoin) GetCurrencies(ctx context.Context) ([]Currency, error) { + var resp []Currency + return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, kucoinGetCurrencies, &resp) +} + +// GetCurrencyDetail gets currency detail using currency code and chain information. +func (ku *Kucoin) GetCurrencyDetail(ctx context.Context, ccy, chain string) (*CurrencyDetail, error) { + if ccy == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + if chain != "" { + params.Set("chain", chain) + } + var resp *CurrencyDetail + return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, common.EncodeURLValues(kucoinGetCurrency+strings.ToUpper(ccy), params), &resp) +} + +// GetFiatPrice gets fiat prices of currencies, default base currency is USD +func (ku *Kucoin) GetFiatPrice(ctx context.Context, base, currencies string) (map[string]string, error) { + params := url.Values{} + if base != "" { + params.Set("base", base) + } + if currencies != "" { + params.Set("currencies", currencies) + } + var resp map[string]string + return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, common.EncodeURLValues(kucoinGetFiatPrice, params), &resp) +} + +// GetMarkPrice gets index price of the specified pair +func (ku *Kucoin) GetMarkPrice(ctx context.Context, pair string) (*MarkPrice, error) { + if pair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + var resp *MarkPrice + return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, fmt.Sprintf(kucoinGetMarkPrice, pair), &resp) +} + +// GetMarginConfiguration gets configure info of the margin +func (ku *Kucoin) GetMarginConfiguration(ctx context.Context) (*MarginConfiguration, error) { + var resp *MarginConfiguration + return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, kucoinGetMarginConfiguration, &resp) +} + +// GetMarginAccount gets configure info of the margin +func (ku *Kucoin) GetMarginAccount(ctx context.Context) (*MarginAccounts, error) { + var resp *MarginAccounts + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinGetMarginAccount, nil, &resp) +} + +// GetMarginRiskLimit gets cross/isolated margin risk limit, default model is cross margin +func (ku *Kucoin) GetMarginRiskLimit(ctx context.Context, marginModel string) ([]MarginRiskLimit, error) { + params := url.Values{} + if marginModel != "" { + params.Set("marginModel", marginModel) + } + var resp []MarginRiskLimit + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, retrieveMarginAccountEPL, http.MethodGet, common.EncodeURLValues(kucoinGetMarginRiskLimit, params), nil, &resp) +} + +// PostBorrowOrder used to post borrow order +func (ku *Kucoin) PostBorrowOrder(ctx context.Context, ccy, orderType, term string, size, maxRate float64) (*PostBorrowOrderResp, error) { + if ccy == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + if orderType == "" { + return nil, errors.New("orderType can not be empty") + } + if size == 0 { + return nil, errors.New("size can not be zero") + } + params := make(map[string]interface{}) + params["currency"] = strings.ToUpper(ccy) + params["type"] = orderType + params["size"] = strconv.FormatFloat(size, 'f', -1, 64) + if maxRate != 0 { + params["maxRate"] = strconv.FormatFloat(maxRate, 'f', -1, 64) + } + if term != "" { + params["term"] = term + } + var resp *PostBorrowOrderResp + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinBorrowOrder, params, &resp) +} + +// GetBorrowOrder gets borrow order information +func (ku *Kucoin) GetBorrowOrder(ctx context.Context, orderID string) (*BorrowOrder, error) { + if orderID == "" { + return nil, errors.New("empty orderID") + } + params := url.Values{} + params.Set("orderId", orderID) + var resp *BorrowOrder + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinBorrowOrder, params), nil, &resp) +} + +// GetOutstandingRecord gets outstanding record information +func (ku *Kucoin) GetOutstandingRecord(ctx context.Context, ccy string) (*OutstandingRecordResponse, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + var resp *OutstandingRecordResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetOutstandingRecord, params), nil, &resp) +} + +// GetRepaidRecord gets repaid record information +func (ku *Kucoin) GetRepaidRecord(ctx context.Context, ccy string) (*RepaidRecordsResponse, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + var resp *RepaidRecordsResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetRepaidRecord, params), nil, &resp) +} + +// OneClickRepayment used to complete repayment in single go +func (ku *Kucoin) OneClickRepayment(ctx context.Context, ccy, sequence string, size float64) error { + if ccy == "" { + return currency.ErrCurrencyCodeEmpty + } + params := make(map[string]interface{}) + params["currency"] = ccy + if sequence == "" { + return errors.New("sequence can not be empty") + } + params["sequence"] = sequence + if size == 0 { + return errors.New("size can not be zero") + } + params["size"] = strconv.FormatFloat(size, 'f', -1, 64) + return ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinOneClickRepayment, params, &struct{}{}) +} + +// SingleOrderRepayment used to repay single order +func (ku *Kucoin) SingleOrderRepayment(ctx context.Context, ccy, tradeID string, size float64) error { + if ccy == "" { + return currency.ErrCurrencyCodeEmpty + } + params := make(map[string]interface{}) + params["currency"] = ccy + if tradeID == "" { + return errors.New("tradeId can not be empty") + } + params["tradeId"] = tradeID + if size == 0 { + return errors.New("size can not be zero") + } + params["size"] = strconv.FormatFloat(size, 'f', -1, 64) + return ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinRepaySingleOrder, params, &struct{}{}) +} + +// PostLendOrder used to create lend order +func (ku *Kucoin) PostLendOrder(ctx context.Context, ccy string, dailyInterestRate, size float64, term int64) (string, error) { + if ccy == "" { + return "", currency.ErrCurrencyPairEmpty + } + params := make(map[string]interface{}) + params["currency"] = ccy + if dailyInterestRate == 0 { + return "", errors.New("dailyIntRate can not be zero") + } + params["dailyIntRate"] = strconv.FormatFloat(dailyInterestRate, 'f', -1, 64) + if size == 0 { + return "", errors.New("size can not be zero") + } + params["size"] = strconv.FormatFloat(size, 'f', -1, 64) + if term == 0 { + return "", errors.New("term can not be zero") + } + params["term"] = strconv.FormatInt(term, 10) + resp := struct { + OrderID string `json:"orderId"` + Error + }{} + return resp.OrderID, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinLendOrder, params, &resp) +} + +// CancelLendOrder used to cancel lend order +func (ku *Kucoin) CancelLendOrder(ctx context.Context, orderID string) error { + resp := struct { + Error + }{} + return ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodDelete, kucoinLendOrder+"/"+orderID, nil, &resp) +} + +// SetAutoLend used to set up the automatic lending for a specified currency +func (ku *Kucoin) SetAutoLend(ctx context.Context, ccy string, dailyInterestRate, retainSize float64, term int64, isEnable bool) error { + if ccy == "" { + return currency.ErrCurrencyCodeEmpty + } + params := make(map[string]interface{}) + params["currency"] = ccy + if dailyInterestRate == 0 { + return errors.New("dailyIntRate can not be zero") + } + params["dailyIntRate"] = strconv.FormatFloat(dailyInterestRate, 'f', -1, 64) + if retainSize == 0 { + return errors.New("retainSize can not be zero") + } + params["retainSize"] = strconv.FormatFloat(retainSize, 'f', -1, 64) + if term == 0 { + return errors.New("term can not be zero") + } + params["term"] = strconv.FormatInt(term, 10) + params["isEnable"] = isEnable + resp := struct { + Error + }{} + return ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinSetAutoLend, params, &resp) +} + +// GetActiveOrder gets active lend orders +func (ku *Kucoin) GetActiveOrder(ctx context.Context, ccy string) ([]LendOrder, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + resp := struct { + Data []LendOrder `json:"items"` + Error + }{} + return resp.Data, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetActiveOrder, params), nil, &resp) +} + +// GetLendHistory gets lend orders +func (ku *Kucoin) GetLendHistory(ctx context.Context, ccy string) ([]LendOrderHistory, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + resp := struct { + Data []LendOrderHistory `json:"items"` + Error + }{} + return resp.Data, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetLendHistory, params), nil, &resp) +} + +// GetUnsettledLendOrder gets outstanding lend order list +func (ku *Kucoin) GetUnsettledLendOrder(ctx context.Context, ccy string) ([]UnsettleLendOrder, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + resp := struct { + Data []UnsettleLendOrder `json:"items"` + Error + }{} + return resp.Data, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetUnsettleLendOrder, params), nil, &resp) +} + +// GetSettledLendOrder gets settle lend orders +func (ku *Kucoin) GetSettledLendOrder(ctx context.Context, ccy string) ([]SettleLendOrder, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + resp := struct { + Data []SettleLendOrder `json:"items"` + Error + }{} + return resp.Data, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetSettleLendOrder, params), nil, &resp) +} + +// GetAccountLendRecord get the lending history of the main account +func (ku *Kucoin) GetAccountLendRecord(ctx context.Context, ccy string) ([]LendRecord, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + var resp []LendRecord + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetAccountLendRecord, params), nil, &resp) +} + +// GetLendingMarketData get the lending market data +func (ku *Kucoin) GetLendingMarketData(ctx context.Context, ccy string, term int64) ([]LendMarketData, error) { + if ccy == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + params.Set("currency", ccy) + if term != 0 { + params.Set("term", strconv.FormatInt(term, 10)) + } + var resp []LendMarketData + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetLendingMarketData, params), nil, &resp) +} + +// GetMarginTradeData get the last 300 fills in the lending and borrowing market +func (ku *Kucoin) GetMarginTradeData(ctx context.Context, ccy string) ([]MarginTradeData, error) { + if ccy == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + params.Set("currency", ccy) + var resp []MarginTradeData + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetMarginTradeData, params), nil, &resp) +} + +// GetIsolatedMarginPairConfig get the current isolated margin trading pair configuration +func (ku *Kucoin) GetIsolatedMarginPairConfig(ctx context.Context) ([]IsolatedMarginPairConfig, error) { + var resp []IsolatedMarginPairConfig + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinGetIsolatedMarginPairConfig, nil, &resp) +} + +// GetIsolatedMarginAccountInfo get all isolated margin accounts of the current user +func (ku *Kucoin) GetIsolatedMarginAccountInfo(ctx context.Context, balanceCurrency string) (*IsolatedMarginAccountInfo, error) { + params := url.Values{} + if balanceCurrency != "" { + params.Set("balanceCurrency", balanceCurrency) + } + var resp *IsolatedMarginAccountInfo + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetIsolatedMarginAccountInfo, params), nil, &resp) +} + +// GetSingleIsolatedMarginAccountInfo get single isolated margin accounts of the current user +func (ku *Kucoin) GetSingleIsolatedMarginAccountInfo(ctx context.Context, symbol string) (*AssetInfo, error) { + if symbol == "" { + return nil, errors.New("symbol can not be empty") + } + var resp *AssetInfo + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinGetSingleIsolatedMarginAccountInfo+symbol, nil, &resp) +} + +// InitiateIsolatedMarginBorrowing initiates isolated margin borrowing +func (ku *Kucoin) InitiateIsolatedMarginBorrowing(ctx context.Context, symbol, ccy, borrowStrategy, period string, size, maxRate int64) (*IsolatedMarginBorrowing, error) { + if symbol == "" { + return nil, errors.New("symbol can not be empty") + } + if ccy == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + params := make(map[string]interface{}) + params["symbol"] = symbol + params["currency"] = ccy + if borrowStrategy == "" { + return nil, errors.New("borrowStrategy can not be empty") + } + params["borrowStrategy"] = borrowStrategy + if size == 0 { + return nil, errors.New("size can not be zero") + } + params["size"] = strconv.FormatInt(size, 10) + + if period != "" { + params["period"] = period + } + if maxRate == 0 { + params["maxRate"] = strconv.FormatInt(maxRate, 10) + } + var resp *IsolatedMarginBorrowing + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinInitiateIsolatedMarginBorrowing, params, &resp) +} + +// GetIsolatedOutstandingRepaymentRecords get the outstanding repayment records of isolated margin positions +func (ku *Kucoin) GetIsolatedOutstandingRepaymentRecords(ctx context.Context, symbol, ccy string, pageSize, currentPage int64) (*OutstandingRepaymentRecordsResponse, error) { + params := url.Values{} + if symbol != "" { + params.Set("symbol", symbol) + } + if ccy != "" { + params.Set("currency", ccy) + } + if pageSize != 0 { + params.Set("pageSize", strconv.FormatInt(pageSize, 10)) + } + if currentPage != 0 { + params.Set("currentPage", strconv.FormatInt(currentPage, 10)) + } + var resp *OutstandingRepaymentRecordsResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetIsolatedOutstandingRepaymentRecords, params), nil, &resp) +} + +// GetIsolatedMarginRepaymentRecords get the repayment records of isolated margin positions +func (ku *Kucoin) GetIsolatedMarginRepaymentRecords(ctx context.Context, symbol, ccy string, pageSize, currentPage int64) (*CompletedRepaymentRecordsResponse, error) { + params := url.Values{} + if symbol != "" { + params.Set("symbol", symbol) + } + if ccy != "" { + params.Set("currency", ccy) + } + if pageSize != 0 { + params.Set("pageSize", strconv.FormatInt(pageSize, 10)) + } + if currentPage != 0 { + params.Set("currentPage", strconv.FormatInt(currentPage, 10)) + } + var resp *CompletedRepaymentRecordsResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetIsolatedMarginRepaymentRecords, params), nil, &resp) +} + +// InitiateIsolatedMarginQuickRepayment is used to initiate quick repayment for isolated margin accounts +func (ku *Kucoin) InitiateIsolatedMarginQuickRepayment(ctx context.Context, symbol, ccy, seqStrategy string, size int64) error { + if symbol == "" { + return currency.ErrCurrencyPairEmpty + } + if size == 0 { + return errors.New("size can not be zero") + } + if seqStrategy == "" { + return errors.New("seqStrategy can not be empty") + } + if ccy == "" { + return currency.ErrCurrencyCodeEmpty + } + params := make(map[string]interface{}) + params["symbol"] = symbol + params["currency"] = ccy + params["seqStrategy"] = seqStrategy + params["size"] = strconv.FormatInt(size, 10) + resp := struct { + Error + }{} + return ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinInitiateIsolatedMarginQuickRepayment, params, &resp) +} + +// InitiateIsolatedMarginSingleRepayment is used to initiate quick repayment for single margin accounts +func (ku *Kucoin) InitiateIsolatedMarginSingleRepayment(ctx context.Context, symbol, ccy, loanID string, size int64) error { + if symbol == "" { + return currency.ErrCurrencyPairEmpty + } + params := make(map[string]interface{}) + params["symbol"] = symbol + if ccy == "" { + return currency.ErrCurrencyCodeEmpty + } + params["currency"] = ccy + if loanID == "" { + return errors.New("loanId can not be empty") + } + params["loanId"] = loanID + if size == 0 { + return errors.New("size can not be zero") + } + params["size"] = strconv.FormatInt(size, 10) + resp := struct { + Error + }{} + return ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinInitiateIsolatedMarginSingleRepayment, params, &resp) +} + +// GetCurrentServerTime gets the server time +func (ku *Kucoin) GetCurrentServerTime(ctx context.Context) (time.Time, error) { + resp := struct { + Timestamp convert.ExchangeTime `json:"data"` + Error + }{} + err := ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, kucoinGetServerTime, &resp) + if err != nil { + return time.Time{}, err + } + return resp.Timestamp.Time(), nil +} + +// GetServiceStatus gets the service status +func (ku *Kucoin) GetServiceStatus(ctx context.Context) (*ServiceStatus, error) { + var resp *ServiceStatus + return resp, ku.SendHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, kucoinGetServiceStatus, &resp) +} + +// PostOrder used to place two types of orders: limit and market +// Note: use this only for SPOT trades +func (ku *Kucoin) PostOrder(ctx context.Context, arg *SpotOrderParam) (string, error) { + if arg.ClientOrderID == "" { + return "", errInvalidClientOrderID + } + if arg.Side == "" { + return "", order.ErrSideIsInvalid + } + if arg.Symbol.IsEmpty() { + return "", fmt.Errorf("%w, empty symbol", currency.ErrCurrencyPairEmpty) + } + switch arg.OrderType { + case "limit", "": + if arg.Price <= 0 { + return "", fmt.Errorf("%w, price =%.3f", errInvalidPrice, arg.Price) + } + if arg.Size <= 0 { + return "", errInvalidSize + } + if arg.VisibleSize < 0 { + return "", fmt.Errorf("%w, visible size must be non-zero positive value", errInvalidSize) + } + case "market": + if arg.Size == 0 && arg.Funds == 0 { + return "", errSizeOrFundIsRequired + } + default: + return "", fmt.Errorf("%w %s", order.ErrTypeIsInvalid, arg.OrderType) + } + resp := struct { + OrderID string `json:"orderId"` + Error + }{} + return resp.OrderID, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, placeOrderEPL, http.MethodPost, kucoinPostOrder, &arg, &resp) +} + +// PostMarginOrder used to place two types of margin orders: limit and market +func (ku *Kucoin) PostMarginOrder(ctx context.Context, arg *MarginOrderParam) (*PostMarginOrderResp, error) { + if arg.ClientOrderID == "" { + return nil, errInvalidClientOrderID + } + if arg.Side == "" { + return nil, order.ErrSideIsInvalid + } + if arg.Symbol.IsEmpty() { + return nil, fmt.Errorf("%w, empty symbol", currency.ErrCurrencyPairEmpty) + } + arg.OrderType = strings.ToLower(arg.OrderType) + switch arg.OrderType { + case "limit", "": + if arg.Price <= 0 { + return nil, fmt.Errorf("%w, price=%.3f", errInvalidPrice, arg.Price) + } + if arg.Size <= 0 { + return nil, errInvalidSize + } + if arg.VisibleSize < 0 { + return nil, fmt.Errorf("%w, visible size must be non-zero positive value", errInvalidSize) + } + case "market": + sum := arg.Size + arg.Funds + if sum <= 0 || (sum != arg.Size && sum != arg.Funds) { + return nil, fmt.Errorf("%w, either 'size' or 'funds' has to be set, but not both", errSizeOrFundIsRequired) + } + default: + return nil, fmt.Errorf("%w %s", order.ErrTypeIsInvalid, arg.OrderType) + } + resp := struct { + PostMarginOrderResp + Error + }{} + return &resp.PostMarginOrderResp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, placeMarginOrdersEPL, http.MethodPost, kucoinPostMarginOrder, &arg, &resp) +} + +// PostBulkOrder used to place 5 orders at the same time. The order type must be a limit order of the same symbol +// Note: it supports only SPOT trades +// Note: To check if order was posted successfully, check status field in response +func (ku *Kucoin) PostBulkOrder(ctx context.Context, symbol string, orderList []OrderRequest) ([]PostBulkOrderResp, error) { + if symbol == "" { + return nil, errors.New("symbol can not be empty") + } + for i := range orderList { + if orderList[i].ClientOID == "" { + return nil, errors.New("clientOid can not be empty") + } + if orderList[i].Side == "" { + return nil, errors.New("side can not be empty") + } + if orderList[i].Price <= 0 { + return nil, errors.New("price must be positive") + } + if orderList[i].Size <= 0 { + return nil, errors.New("size must be positive") + } + } + params := make(map[string]interface{}) + params["symbol"] = symbol + params["orderList"] = orderList + resp := &struct { + Data []PostBulkOrderResp `json:"data"` + }{} + return resp.Data, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, placeBulkOrdersEPL, http.MethodPost, kucoinPostBulkOrder, params, &resp) +} + +// CancelSingleOrder used to cancel single order previously placed +func (ku *Kucoin) CancelSingleOrder(ctx context.Context, orderID string) ([]string, error) { + if orderID == "" { + return nil, errors.New("orderID can not be empty") + } + resp := struct { + CancelledOrderIDs []string `json:"cancelledOrderIds"` + Error + }{} + return resp.CancelledOrderIDs, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, cancelOrderEPL, http.MethodDelete, kucoinOrderByID+orderID, nil, &resp) +} + +// CancelOrderByClientOID used to cancel order via the clientOid +func (ku *Kucoin) CancelOrderByClientOID(ctx context.Context, orderID string) (*CancelOrderResponse, error) { + var resp *CancelOrderResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodDelete, kucoinOrderByClientOID+orderID, nil, &resp) +} + +// CancelAllOpenOrders used to cancel all order based upon the parameters passed +func (ku *Kucoin) CancelAllOpenOrders(ctx context.Context, symbol, tradeType string) ([]string, error) { + params := url.Values{} + if symbol != "" { + params.Set("symbol", symbol) + } + if tradeType != "" { + params.Set("tradeType", tradeType) + } + resp := struct { + CancelledOrderIDs []string `json:"cancelledOrderIds"` + Error + }{} + return resp.CancelledOrderIDs, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, cancelAllOrdersEPL, http.MethodDelete, common.EncodeURLValues(kucoinOrders, params), nil, &resp) +} + +// ListOrders gets the user order list +func (ku *Kucoin) ListOrders(ctx context.Context, status, symbol, side, orderType, tradeType string, startAt, endAt time.Time) (*OrdersListResponse, error) { + params := fillParams(symbol, side, orderType, tradeType, startAt, endAt) + if status != "" { + params.Set("status", status) + } + var resp *OrdersListResponse + err := ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, listOrdersEPL, http.MethodGet, common.EncodeURLValues(kucoinOrders, params), nil, &resp) + if err != nil { + return nil, err + } + return resp, nil +} + +func fillParams(symbol, side, orderType, tradeType string, startAt, endAt time.Time) url.Values { + params := url.Values{} + if symbol != "" { + params.Set("symbol", symbol) + } + if side != "" { + params.Set("side", side) + } + if orderType != "" { + params.Set("type", orderType) + } + if tradeType != "" { + params.Set("tradeType", tradeType) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + return params +} + +// GetRecentOrders get orders in the last 24 hours. +func (ku *Kucoin) GetRecentOrders(ctx context.Context) ([]OrderDetail, error) { + var resp []OrderDetail + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinGetRecentOrders, nil, &resp) +} + +// GetOrderByID get a single order info by order ID +func (ku *Kucoin) GetOrderByID(ctx context.Context, orderID string) (*OrderDetail, error) { + if orderID == "" { + return nil, errors.New("orderID can not be empty") + } + var resp *OrderDetail + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinOrderByID+orderID, nil, &resp) +} + +// GetOrderByClientSuppliedOrderID get a single order info by client order ID +func (ku *Kucoin) GetOrderByClientSuppliedOrderID(ctx context.Context, clientOID string) (*OrderDetail, error) { + if clientOID == "" { + return nil, errors.New("client order ID can not be empty") + } + var resp *OrderDetail + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinOrderByClientOID+clientOID, nil, &resp) +} + +// GetFills get fills +func (ku *Kucoin) GetFills(ctx context.Context, orderID, symbol, side, orderType, tradeType string, startAt, endAt time.Time) (*ListFills, error) { + params := fillParams(symbol, side, orderType, tradeType, startAt, endAt) + if orderID != "" { + params.Set("orderId", orderID) + } + var resp *ListFills + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, listFillsEPL, http.MethodGet, common.EncodeURLValues(kucoinGetFills, params), nil, &resp) +} + +// GetRecentFills get a list of 1000 fills in last 24 hours +func (ku *Kucoin) GetRecentFills(ctx context.Context) ([]Fill, error) { + var resp []Fill + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinGetRecentFills, nil, &resp) +} + +// PostStopOrder used to place two types of stop orders: limit and market +func (ku *Kucoin) PostStopOrder(ctx context.Context, clientOID, side, symbol, orderType, remark, stop, stp, tradeType, timeInForce string, size, price, stopPrice, cancelAfter, visibleSize, funds float64, postOnly, hidden, iceberg bool) (string, error) { + params := make(map[string]interface{}) + if clientOID == "" { + return "", errors.New("clientOid can not be empty") + } + params["clientOid"] = clientOID + if side == "" { + return "", errors.New("side can not be empty") + } + params["side"] = side + if symbol == "" { + return "", fmt.Errorf("%w, empty symbol", currency.ErrCurrencyPairEmpty) + } + params["symbol"] = symbol + if remark != "" { + params["remark"] = remark + } + if stop != "" { + params["stop"] = stop + if stopPrice <= 0 { + return "", errors.New("stopPrice is required") + } + params["stopPrice"] = strconv.FormatFloat(stopPrice, 'f', -1, 64) + } + if stp != "" { + params["stp"] = stp + } + if tradeType != "" { + params["tradeType"] = tradeType + } + orderType = strings.ToLower(orderType) + switch orderType { + case "limit", "": + if price <= 0 { + return "", errors.New("price is required") + } + params["price"] = strconv.FormatFloat(price, 'f', -1, 64) + if size <= 0 { + return "", errors.New("size can not be zero or negative") + } + params["size"] = strconv.FormatFloat(size, 'f', -1, 64) + if timeInForce != "" { + params["timeInForce"] = timeInForce + } + if cancelAfter > 0 && timeInForce == "GTT" { + params["cancelAfter"] = strconv.FormatFloat(cancelAfter, 'f', -1, 64) + } + params["postOnly"] = postOnly + params["hidden"] = hidden + params["iceberg"] = iceberg + if visibleSize > 0 { + params["visibleSize"] = strconv.FormatFloat(visibleSize, 'f', -1, 64) + } + case "market": + switch { + case size > 0: + params["size"] = strconv.FormatFloat(size, 'f', -1, 64) + case funds > 0: + params["funds"] = strconv.FormatFloat(funds, 'f', -1, 64) + default: + return "", errSizeOrFundIsRequired + } + default: + return "", fmt.Errorf("%w, order type: %s", order.ErrTypeIsInvalid, orderType) + } + if orderType != "" { + params["type"] = orderType + } + resp := struct { + OrderID string `json:"orderId"` + Error + }{} + return resp.OrderID, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, placeOrderEPL, http.MethodPost, kucoinStopOrder, params, &resp) +} + +// CancelStopOrder used to cancel single stop order previously placed +func (ku *Kucoin) CancelStopOrder(ctx context.Context, orderID string) ([]string, error) { + if orderID == "" { + return nil, errors.New("orderID can not be empty") + } + resp := struct { + Data []string `json:"cancelledOrderIds"` + Error + }{} + return resp.Data, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodDelete, kucoinStopOrderByID+orderID, nil, &resp) +} + +// CancelStopOrders used to cancel all order based upon the parameters passed +func (ku *Kucoin) CancelStopOrders(ctx context.Context, symbol, tradeType, orderIDs string) ([]string, error) { + params := url.Values{} + if symbol != "" { + params.Set("symbol", symbol) + } + if tradeType != "" { + params.Set("tradeType", tradeType) + } + if orderIDs != "" { + params.Set("orderIds", orderIDs) + } + resp := struct { + CancelledOrderIDs []string `json:"cancelledOrderIds"` + Error + }{} + return resp.CancelledOrderIDs, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodDelete, common.EncodeURLValues(kucoinCancelAllStopOrder, params), nil, &resp) +} + +// GetStopOrder used to cancel single stop order previously placed +func (ku *Kucoin) GetStopOrder(ctx context.Context, orderID string) (*StopOrder, error) { + if orderID == "" { + return nil, errors.New("orderID can not be empty") + } + resp := struct { + StopOrder + Error + }{} + return &resp.StopOrder, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinStopOrderByID+orderID, nil, &resp) +} + +// ListStopOrders get all current untriggered stop orders +func (ku *Kucoin) ListStopOrders(ctx context.Context, symbol, side, orderType, tradeType, orderIDs string, startAt, endAt time.Time, currentPage, pageSize int64) (*StopOrderListResponse, error) { + params := fillParams(symbol, side, orderType, tradeType, startAt, endAt) + if orderIDs != "" { + params.Set("orderIds", orderIDs) + } + if currentPage != 0 { + params.Set("currentPage", strconv.FormatInt(currentPage, 10)) + } + if pageSize != 0 { + params.Set("pageSize", strconv.FormatInt(pageSize, 10)) + } + var resp *StopOrderListResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinStopOrder, params), nil, &resp) +} + +// GetStopOrderByClientID get a stop order information via the clientOID +func (ku *Kucoin) GetStopOrderByClientID(ctx context.Context, symbol, clientOID string) ([]StopOrder, error) { + if clientOID == "" { + return nil, errors.New("clientOID can not be empty") + } + params := url.Values{} + params.Set("clientOid", clientOID) + if symbol != "" { + params.Set("symbol", symbol) + } + var resp []StopOrder + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetStopOrderByClientID, params), nil, &resp) +} + +// CancelStopOrderByClientID used to cancel a stop order via the clientOID. +func (ku *Kucoin) CancelStopOrderByClientID(ctx context.Context, symbol, clientOID string) (*CancelOrderResponse, error) { + if clientOID == "" { + return nil, errors.New("clientOID can not be empty") + } + params := url.Values{} + params.Set("clientOid", clientOID) + if symbol != "" { + params.Set("symbol", symbol) + } + var resp *CancelOrderResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodDelete, common.EncodeURLValues(kucoinCancelStopOrderByClientID, params), nil, &resp) +} + +// CreateSubUser creates a new sub-user for the account. +func (ku *Kucoin) CreateSubUser(ctx context.Context, subAccountName, password, remarks, access string) (*SubAccount, error) { + params := make(map[string]interface{}) + if regexp.MustCompile("^[a-zA-Z0-9]{7-32}$").MatchString(subAccountName) { + return nil, errors.New("invalid sub-account name") + } + if regexp.MustCompile("^[a-zA-Z0-9]{7-24}$").MatchString(password) { + return nil, errInvalidPassPhraseInstance + } + params["subName"] = subAccountName + params["password"] = password + if remarks != "" { + params["remarks"] = remarks + } + if access != "" { + params["access"] = access + } + var resp *SubAccount + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinSubUserCreated, params, &resp) +} + +// GetSubAccountSpotAPIList used to obtain a list of Spot APIs pertaining to a sub-account. +func (ku *Kucoin) GetSubAccountSpotAPIList(ctx context.Context, subAccountName, apiKeys string) (*SubAccountResponse, error) { + params := url.Values{} + if subAccountRegExp.MatchString(subAccountName) { + return nil, errInvalidSubAccountName + } + params.Set("subName", subAccountName) + if apiKeys != "" { + params.Set("apiKey", apiKeys) + } + var resp SubAccountResponse + return &resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinSubAccountSpotAPIs, params), nil, &resp) +} + +// CreateSpotAPIsForSubAccount can be used to create Spot APIs for sub-accounts. +func (ku *Kucoin) CreateSpotAPIsForSubAccount(ctx context.Context, arg *SpotAPISubAccountParams) (*SpotAPISubAccount, error) { + if subAccountRegExp.MatchString(arg.SubAccountName) { + return nil, errInvalidSubAccountName + } + if subAccountPassphraseRegExp.MatchString(arg.Passphrase) { + return nil, fmt.Errorf("%w, must contain 7-32 characters. cannot contain any spaces", errInvalidPassPhraseInstance) + } + if arg.Remark == "" { + return nil, errors.New("remark is required") + } + var resp *SpotAPISubAccount + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinSubAccountSpotAPIs, &arg, &resp) +} + +// ModifySubAccountSpotAPIs modifies sub-account Spot APIs. +func (ku *Kucoin) ModifySubAccountSpotAPIs(ctx context.Context, arg *SpotAPISubAccountParams) (*SpotAPISubAccount, error) { + if subAccountRegExp.MatchString(arg.SubAccountName) { + return nil, errInvalidSubAccountName + } + if subAccountPassphraseRegExp.MatchString(arg.Passphrase) { + return nil, fmt.Errorf("%w, must contain 7-32 characters. cannot contain any spaces", errInvalidPassPhraseInstance) + } + if arg.Remark == "" { + return nil, errors.New("remark is required") + } + var resp *SpotAPISubAccount + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPut, kucoinUpdateModifySubAccountSpotAPIs, &arg, &resp) +} + +// DeleteSubAccountSpotAPI delete sub-account Spot APIs. +func (ku *Kucoin) DeleteSubAccountSpotAPI(ctx context.Context, apiKey, passphrase, subAccountName string) (*DeleteSubAccountResponse, error) { + if subAccountRegExp.MatchString(subAccountName) { + return nil, errInvalidSubAccountName + } + if subAccountPassphraseRegExp.MatchString(passphrase) { + return nil, fmt.Errorf("%w, must contain 7-32 characters. cannot contain any spaces", errInvalidPassPhraseInstance) + } + if apiKey == "" { + return nil, errors.New("apiKey is required") + } + params := url.Values{} + params.Set("apiKey", apiKey) + params.Set("passphrase", passphrase) + params.Set("subName", subAccountName) + var resp *DeleteSubAccountResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodDelete, common.EncodeURLValues(kucoinSubAccountSpotAPIs, params), nil, &resp) +} + +// GetUserInfoOfAllSubAccounts get the user info of all sub-users via this interface. +func (ku *Kucoin) GetUserInfoOfAllSubAccounts(ctx context.Context) (*SubAccountResponse, error) { + var resp *SubAccountResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinSubUser, nil, &resp) +} + +// GetPaginatedListOfSubAccounts to retrieve a paginated list of sub-accounts. Pagination is required. +func (ku *Kucoin) GetPaginatedListOfSubAccounts(ctx context.Context, currentPage, pageSize int64) (*SubAccountResponse, error) { + params := url.Values{} + if pageSize > 0 { + params.Set("pageSize", strconv.FormatInt(pageSize, 10)) + } + if currentPage > 0 { + params.Set("currentPage", strconv.FormatInt(currentPage, 10)) + } + var resp *SubAccountResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinSubUser, params), nil, &resp) +} + +// GetAllAccounts get all accounts +func (ku *Kucoin) GetAllAccounts(ctx context.Context, ccy, accountType string) ([]AccountInfo, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + if accountType != "" { + params.Set("type", accountType) + } + var resp []AccountInfo + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinAccount, params), nil, &resp) +} + +// GetAccount get information of single account +func (ku *Kucoin) GetAccount(ctx context.Context, accountID string) (*AccountInfo, error) { + var resp *AccountInfo + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinGetAccount+accountID, nil, &resp) +} + +// GetAccountLedgers get the history of deposit/withdrawal of all accounts, supporting inquiry of various currencies +func (ku *Kucoin) GetAccountLedgers(ctx context.Context, ccy, direction, bizType string, startAt, endAt time.Time) (*AccountLedgerResponse, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + if direction != "" { + params.Set("direction", direction) + } + if bizType != "" { + params.Set("bizType", bizType) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + var resp *AccountLedgerResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, retrieveAccountLedgerEPL, http.MethodGet, common.EncodeURLValues(kucoinGetAccountLedgers, params), nil, &resp) +} + +// GetAccountSummaryInformation this can be used to obtain account summary information. +func (ku *Kucoin) GetAccountSummaryInformation(ctx context.Context) (*AccountSummaryInformation, error) { + var resp *AccountSummaryInformation + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinUserInfo, nil, &resp) +} + +// GetSubAccountBalance get account info of a sub-user specified by the subUserID +func (ku *Kucoin) GetSubAccountBalance(ctx context.Context, subUserID string, includeBaseAmount bool) (*SubAccountInfo, error) { + params := url.Values{} + if includeBaseAmount { + params.Set("includeBaseAmount", "true") + } + var resp *SubAccountInfo + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetSubAccountBalance+subUserID, params), nil, &resp) +} + +// GetAggregatedSubAccountBalance get the account info of all sub-users +func (ku *Kucoin) GetAggregatedSubAccountBalance(ctx context.Context) ([]SubAccountInfo, error) { + var resp []SubAccountInfo + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, kucoinGetAggregatedSubAccountBalance, nil, &resp) +} + +// GetPaginatedSubAccountInformation this endpoint can be used to get paginated sub-account information. Pagination is required. +func (ku *Kucoin) GetPaginatedSubAccountInformation(ctx context.Context, currentPage, pageSize int64) ([]SubAccountInfo, error) { + params := url.Values{} + if currentPage != 0 { + params.Set("currentPage", strconv.FormatInt(currentPage, 10)) + } + if pageSize != 0 { + params.Set("pageSize", strconv.FormatInt(pageSize, 10)) + } + var resp []SubAccountInfo + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetAggregatedSubAccountBalance, params), nil, &resp) +} + +// GetTransferableBalance get the transferable balance of a specified account +func (ku *Kucoin) GetTransferableBalance(ctx context.Context, ccy, accountType, tag string) (*TransferableBalanceInfo, error) { + if ccy == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + params.Set("currency", ccy) + if accountType == "" { + return nil, errors.New("accountType can not be empty") + } + params.Set("type", accountType) + if tag != "" { + params.Set("tag", tag) + } + var resp *TransferableBalanceInfo + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetTransferableBalance, params), nil, &resp) +} + +// TransferMainToSubAccount used to transfer funds from main account to sub-account +func (ku *Kucoin) TransferMainToSubAccount(ctx context.Context, clientOID, ccy, amount, direction, accountType, subAccountType, subUserID string) (string, error) { + if clientOID == "" { + return "", errors.New("clientOID can not be empty") + } + if ccy == "" { + return "", currency.ErrCurrencyPairEmpty + } + if amount == "" { + return "", errors.New("amount can not be empty") + } + if direction == "" { + return "", errors.New("direction can not be empty") + } + if subUserID == "" { + return "", errors.New("subUserID can not be empty") + } + params := make(map[string]interface{}) + params["clientOid"] = clientOID + params["currency"] = ccy + params["amount"] = amount + params["direction"] = direction + if accountType != "" { + params["accountType"] = accountType + } + if subAccountType != "" { + params["subAccountType"] = subAccountType + } + params["subUserId"] = subUserID + resp := struct { + OrderID string `json:"orderId"` + }{} + return resp.OrderID, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, masterSubUserTransferEPL, http.MethodPost, kucoinTransferMainToSubAccount, params, &resp) +} + +// MakeInnerTransfer used to transfer funds between accounts internally +func (ku *Kucoin) MakeInnerTransfer(ctx context.Context, clientOID, ccy, from, to, amount, fromTag, toTag string) (string, error) { + if clientOID == "" { + return "", errors.New("clientOID can not be empty") + } + if ccy == "" { + return "", currency.ErrCurrencyPairEmpty + } + if amount == "" { + return "", errors.New("amount can not be empty") + } + if from == "" { + return "", errors.New("from can not be empty") + } + if to == "" { + return "", errors.New("to can not be empty") + } + params := make(map[string]interface{}) + params["clientOid"] = clientOID + params["currency"] = ccy + params["amount"] = amount + params["from"] = from + params["to"] = to + if fromTag != "" { + params["fromTag"] = fromTag + } + if toTag != "" { + params["toTag"] = toTag + } + resp := struct { + OrderID string `json:"orderId"` + }{} + return resp.OrderID, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinInnerTransfer, params, &resp) +} + +// CreateDepositAddress create a deposit address for a currency you intend to deposit +func (ku *Kucoin) CreateDepositAddress(ctx context.Context, ccy, chain string) (*DepositAddress, error) { + if ccy == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + params := make(map[string]interface{}) + params["currency"] = ccy + if chain != "" { + params["chain"] = chain + } + var resp *DepositAddress + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinGetDepositAddressV1, params, &resp) +} + +// GetDepositAddressesV2 get all deposit addresses for the currency you intend to deposit +func (ku *Kucoin) GetDepositAddressesV2(ctx context.Context, ccy string) ([]DepositAddress, error) { + if ccy == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + params.Set("currency", ccy) + var resp []DepositAddress + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetDepositAddressesV2, params), nil, &resp) +} + +// GetDepositAddressV1 get a deposit address for the currency you intend to deposit +func (ku *Kucoin) GetDepositAddressV1(ctx context.Context, ccy, chain string) (*DepositAddress, error) { + if ccy == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + params.Set("currency", ccy) + if chain != "" { + params.Set("chain", chain) + } + var resp DepositAddress + return &resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetDepositAddressV1, params), nil, &resp) +} + +// GetDepositList get deposit list items and sorted to show the latest first +func (ku *Kucoin) GetDepositList(ctx context.Context, ccy, status string, startAt, endAt time.Time) (*DepositResponse, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + if status != "" { + params.Set("status", status) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + var resp *DepositResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, retrieveDepositListEPL, http.MethodGet, common.EncodeURLValues(kucoinGetDepositList, params), nil, &resp) +} + +// GetHistoricalDepositList get historical deposit list items +func (ku *Kucoin) GetHistoricalDepositList(ctx context.Context, ccy, status string, startAt, endAt time.Time) (*HistoricalDepositWithdrawalResponse, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + if status != "" { + params.Set("status", status) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + var resp *HistoricalDepositWithdrawalResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, retrieveV1HistoricalDepositListEPL, http.MethodGet, common.EncodeURLValues(kucoinGetHistoricalDepositList, params), nil, &resp) +} + +// GetWithdrawalList get withdrawal list items +func (ku *Kucoin) GetWithdrawalList(ctx context.Context, ccy, status string, startAt, endAt time.Time) (*WithdrawalsResponse, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + if status != "" { + params.Set("status", status) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + var resp *WithdrawalsResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, retrieveWithdrawalListEPL, http.MethodGet, common.EncodeURLValues(kucoinWithdrawal, params), nil, &resp) +} + +// GetHistoricalWithdrawalList get historical withdrawal list items +func (ku *Kucoin) GetHistoricalWithdrawalList(ctx context.Context, ccy, status string, startAt, endAt time.Time, currentPage, pageSize int64) (*HistoricalDepositWithdrawalResponse, error) { + params := url.Values{} + if ccy != "" { + params.Set("currency", ccy) + } + if status != "" { + params.Set("status", status) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + if currentPage != 0 { + params.Set("currentPage", strconv.FormatInt(currentPage, 10)) + } + if pageSize != 0 { + params.Set("pageSize", strconv.FormatInt(pageSize, 10)) + } + var resp *HistoricalDepositWithdrawalResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, retrieveV1HistoricalWithdrawalListEPL, http.MethodGet, common.EncodeURLValues(kucoinGetHistoricalWithdrawalList, params), nil, &resp) +} + +// GetWithdrawalQuotas get withdrawal quota details +func (ku *Kucoin) GetWithdrawalQuotas(ctx context.Context, ccy, chain string) (*WithdrawalQuota, error) { + if ccy == "" { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + params.Set("currency", ccy) + if chain != "" { + params.Set("chain", chain) + } + var resp *WithdrawalQuota + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinGetWithdrawalQuotas, params), nil, &resp) +} + +// ApplyWithdrawal create a withdrawal request +// The endpoint was deprecated for futures, please transfer assets from the FUTURES account to the MAIN account first, and then withdraw from the MAIN account +func (ku *Kucoin) ApplyWithdrawal(ctx context.Context, ccy, address, memo, remark, chain, feeDeductType string, isInner bool, amount float64) (string, error) { + if ccy == "" { + return "", currency.ErrCurrencyPairEmpty + } + params := make(map[string]interface{}) + params["currency"] = ccy + if address == "" { + return "", errors.New("address can not be empty") + } + params["address"] = address + if amount == 0 { + return "", errors.New("amount can not be empty") + } + params["amount"] = amount + if memo != "" { + params["memo"] = memo + } + params["isInner"] = isInner + if remark != "" { + params["remark"] = remark + } + if chain != "" { + params["chain"] = chain + } + if feeDeductType != "" { + params["feeDeductType"] = feeDeductType + } + resp := struct { + WithdrawalID string `json:"withdrawalId"` + Error + }{} + return resp.WithdrawalID, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, kucoinWithdrawal, params, &resp) +} + +// CancelWithdrawal used to cancel a withdrawal request +func (ku *Kucoin) CancelWithdrawal(ctx context.Context, withdrawalID string) error { + return ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodDelete, kucoinCancelWithdrawal+withdrawalID, nil, &struct{}{}) +} + +// GetBasicFee get basic fee rate of users +func (ku *Kucoin) GetBasicFee(ctx context.Context, currencyType string) (*Fees, error) { + params := url.Values{} + if currencyType != "" { + params.Set("currencyType", currencyType) + } + var resp *Fees + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinBasicFee, params), nil, &resp) +} + +// 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) + } + var resp []Fees + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodGet, common.EncodeURLValues(kucoinTradingFee, params), nil, &resp) +} + +// SendHTTPRequest sends an unauthenticated HTTP request +func (ku *Kucoin) SendHTTPRequest(ctx context.Context, ePath exchange.URL, epl request.EndpointLimit, path string, result interface{}) error { + value := reflect.ValueOf(result) + if value.Kind() != reflect.Pointer { + return errInvalidResultInterface + } + resp, okay := result.(UnmarshalTo) + if !okay { + resp = &Response{Data: result} + } + endpointPath, err := ku.API.Endpoints.GetURL(ePath) + if err != nil { + return err + } + err = ku.SendPayload(ctx, epl, func() (*request.Item, error) { + return &request.Item{ + Method: http.MethodGet, + Path: endpointPath + path, + Result: resp, + Verbose: ku.Verbose, + HTTPDebugging: ku.HTTPDebugging, + HTTPRecording: ku.HTTPRecording}, nil + }, request.UnauthenticatedRequest) + if err != nil { + return err + } + if result == nil { + return errNoValidResponseFromServer + } + return resp.GetError() +} + +// SendAuthHTTPRequest sends an authenticated HTTP request +// Request parameters are added to path variable for GET and DELETE request and for other requests its passed in params variable +func (ku *Kucoin) SendAuthHTTPRequest(ctx context.Context, ePath exchange.URL, epl request.EndpointLimit, method, path string, arg, result interface{}) error { + value := reflect.ValueOf(result) + if value.Kind() != reflect.Pointer { + return errInvalidResultInterface + } + creds, err := ku.GetCredentials(ctx) + if err != nil { + return err + } + resp, okay := result.(UnmarshalTo) + if !okay { + resp = &Response{Data: result} + } + endpointPath, err := ku.API.Endpoints.GetURL(ePath) + if err != nil { + return err + } + if value.IsNil() || value.Kind() != reflect.Pointer { + return fmt.Errorf("%w receiver has to be non-nil pointer", errInvalidResponseReceiver) + } + err = ku.SendPayload(ctx, epl, func() (*request.Item, error) { + var ( + body io.Reader + payload []byte + ) + if arg != nil { + payload, err = json.Marshal(arg) + if err != nil { + return nil, err + } + body = bytes.NewBuffer(payload) + } + timeStamp := strconv.FormatInt(time.Now().UnixMilli(), 10) + var signHash, passPhraseHash []byte + signHash, err = crypto.GetHMAC(crypto.HashSHA256, []byte(timeStamp+method+"/api"+path+string(payload)), []byte(creds.Secret)) + if err != nil { + return nil, err + } + passPhraseHash, err = crypto.GetHMAC(crypto.HashSHA256, []byte(creds.ClientID), []byte(creds.Secret)) + if err != nil { + return nil, err + } + headers := map[string]string{ + "KC-API-KEY": creds.Key, + "KC-API-SIGN": crypto.Base64Encode(signHash), + "KC-API-TIMESTAMP": timeStamp, + "KC-API-PASSPHRASE": crypto.Base64Encode(passPhraseHash), + "KC-API-KEY-VERSION": kucoinAPIKeyVersion, + "Content-Type": "application/json", + } + return &request.Item{ + Method: method, + Path: endpointPath + path, + Headers: headers, + Body: body, + Result: &resp, + Verbose: ku.Verbose, + HTTPDebugging: ku.HTTPDebugging, + HTTPRecording: ku.HTTPRecording}, nil + }, request.AuthenticatedRequest) + if err != nil { + return err + } + if result == nil { + return errNoValidResponseFromServer + } + return resp.GetError() +} + +func (ku *Kucoin) intervalToString(interval kline.Interval) (string, error) { + switch interval { + case kline.OneMin: + return "1min", nil + case kline.ThreeMin: + return "3min", nil + case kline.FiveMin: + return "5min", nil + case kline.FifteenMin: + return "15min", nil + case kline.ThirtyMin: + return "30min", nil + case kline.OneHour: + return "1hour", nil + case kline.TwoHour: + return "2hour", nil + case kline.FourHour: + return "4hour", nil + case kline.SixHour: + return "6hour", nil + case kline.EightHour: + return "8hour", nil + case kline.TwelveHour: + return "12hour", nil + case kline.OneDay: + return "1day", nil + case kline.OneWeek: + return "1week", nil + default: + return "", kline.ErrUnsupportedInterval + } +} + +func (ku *Kucoin) stringToOrderStatus(status string) (order.Status, error) { + switch status { + case "match": + return order.Filled, nil + case "open": + return order.Open, nil + case "done": + return order.Closed, nil + default: + return order.StringToOrderStatus(status) + } +} + +func (ku *Kucoin) accountTypeToString(a asset.Item) string { + switch a { + case asset.Spot: + return "trade" + case asset.Margin: + return "margin" + case asset.Empty: + return "" + default: + return "main" + } +} + +func (ku *Kucoin) accountToTradeTypeString(a asset.Item, marginMode string) string { + switch a { + case asset.Spot: + return "TRADE" + case asset.Margin: + if strings.EqualFold(marginMode, "isolated") { + return "MARGIN_ISOLATED_TRADE" + } + return "MARGIN_TRADE" + default: + return "" + } +} + +func (ku *Kucoin) orderTypeToString(orderType order.Type) (string, error) { + switch orderType { + case order.AnyType, order.UnknownType: + return "", nil + case order.Market, order.Limit: + return orderType.Lower(), nil + default: + return "", order.ErrUnsupportedOrderType + } +} + +func (ku *Kucoin) orderSideString(side order.Side) (string, error) { + switch side { + case order.Buy, order.Sell: + return side.Lower(), nil + case order.AnySide: + return "", nil + default: + return "", fmt.Errorf("%w, side:%s", order.ErrSideIsInvalid, side.String()) + } +} diff --git a/exchanges/kucoin/kucoin_convert.go b/exchanges/kucoin/kucoin_convert.go new file mode 100644 index 00000000..098476a1 --- /dev/null +++ b/exchanges/kucoin/kucoin_convert.go @@ -0,0 +1,67 @@ +package kucoin + +import ( + "encoding/json" + "fmt" + "strconv" +) + +// UnmarshalJSON valid data to SubAccountsResponse of return nil if the data is empty list. +// this is added to handle the empty list returned when there are no accounts. +func (a *SubAccountsResponse) UnmarshalJSON(data []byte) error { + var result interface{} + err := json.Unmarshal(data, &result) + if err != nil { + return err + } + var ok bool + if a, ok = result.(*SubAccountsResponse); ok { + if a == nil { + return errNoValidResponseFromServer + } + return nil + } else if _, ok := result.([]interface{}); ok { + return nil + } + return fmt.Errorf("%w can not unmarshal to SubAccountsResponse", errMalformedData) +} + +// kucoinNumber unmarshals and extract numeric value from a byte slice. +type kucoinNumber float64 + +// Float64 returns an float64 value from kucoinNumeric instance +func (a *kucoinNumber) Float64() float64 { + return float64(*a) +} + +// UnmarshalJSON decerializes integer and string data having an integer value to int64 +func (a *kucoinNumber) UnmarshalJSON(data []byte) error { + var value interface{} + err := json.Unmarshal(data, &value) + if err != nil { + return err + } + switch val := value.(type) { + case float64: + *a = kucoinNumber(val) + case float32: + *a = kucoinNumber(val) + case string: + if val == "" { + *a = kucoinNumber(0) // setting empty string value to zero to reset previous value if exist. + return nil + } + value, err := strconv.ParseFloat(val, 64) + if err != nil { + return err + } + *a = kucoinNumber(value) + case int64: + *a = kucoinNumber(val) + case int32: + *a = kucoinNumber(val) + default: + return fmt.Errorf("unsupported input numeric type %T", value) + } + return nil +} diff --git a/exchanges/kucoin/kucoin_futures.go b/exchanges/kucoin/kucoin_futures.go new file mode 100644 index 00000000..ca83c371 --- /dev/null +++ b/exchanges/kucoin/kucoin_futures.go @@ -0,0 +1,821 @@ +package kucoin + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" +) + +const ( + kucoinFuturesAPIURL = "https://api-futures.kucoin.com/api" + kucoinWebsocketURL = "wss://ws-api.kucoin.com/endpoint" + + // Public market endpoints + kucoinFuturesOpenContracts = "/v1/contracts/active" + kucoinFuturesContract = "/v1/contracts/" + kucoinFuturesRealTimeTicker = "/v1/ticker" + kucoinFuturesFullOrderbook = "/v1/level2/snapshot" + kucoinFuturesPartOrderbook20 = "/v1/level2/depth20" + kucoinFuturesPartOrderbook100 = "/v1/level2/depth100" + kucoinFuturesTradeHistory = "/v1/trade/history" + kucoinFuturesInterestRate = "/v1/interest/query" + kucoinFuturesIndex = "/v1/index/query" + kucoinFuturesMarkPrice = "/v1/mark-price/%s/current" + kucoinFuturesPremiumIndex = "/v1/premium/query" + kucoinFuturesFundingRate = "/v1/funding-rate/%s/current" + kucoinFuturesServerTime = "/v1/timestamp" + kucoinFuturesServiceStatus = "/v1/status" + kucoinFuturesKline = "/v1/kline/query" + + // Authenticated endpoints + kucoinFuturesOrder = "/v1/orders" + kucoinFuturesCancelOrder = "/v1/orders/" + kucoinFuturesStopOrder = "/v1/stopOrders" + kucoinFuturesRecentCompletedOrder = "/v1/recentDoneOrders" + kucoinFuturesGetOrderDetails = "/v1/orders/" + kucoinFuturesGetOrderDetailsByClientID = "/v1/orders/byClientOid" + + kucoinFuturesFills = "/v1/fills" + kucoinFuturesRecentFills = "/v1/recentFills" + kucoinFuturesOpenOrderStats = "/v1/openOrderStatistics" + kucoinFuturesPosition = "/v1/position" + kucoinFuturesPositionList = "/v1/positions" + kucoinFuturesSetAutoDeposit = "/v1/position/margin/auto-deposit-status" + kucoinFuturesAddMargin = "/v1/position/margin/deposit-margin" + kucoinFuturesRiskLimitLevel = "/v1/contracts/risk-limit/" + kucoinFuturesUpdateRiskLmitLevel = "/v1/position/risk-limit-level/change" + kucoinFuturesFundingHistory = "/v1/funding-history" + + kucoinFuturesAccountOverview = "/v1/account-overview" + kucoinFuturesTransactionHistory = "/v1/transaction-history" + kucoinFuturesSubAccountAPI = "/v1/sub/api-key" + kucoinFuturesDepositAddress = "/v1/deposit-address" + kucoinFuturesDepositsList = "/v1/deposit-list" + kucoinFuturesWithdrawalLimit = "/v1/withdrawals/quotas" + kucoinFuturesWithdrawalList = "/v1/withdrawal-list" + kucoinFuturesCancelWithdrawal = "/v1/withdrawals/" + kucoinFuturesTransferFundtoMainAccount = "/v3/transfer-out" + kucoinFuturesTransferFundtoFuturesAccount = "/v1/transfer-in" + kucoinFuturesTransferOutList = "/v1/transfer-list" + kucoinFuturesCancelTransferOut = "/v1/cancel/transfer-out" +) + +// GetFuturesOpenContracts gets all open futures contract with its details +func (ku *Kucoin) GetFuturesOpenContracts(ctx context.Context) ([]Contract, error) { + var resp []Contract + return resp, ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, kucoinFuturesOpenContracts, &resp) +} + +// GetFuturesContract get contract details +func (ku *Kucoin) GetFuturesContract(ctx context.Context, symbol string) (*Contract, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + var resp *Contract + return resp, ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, kucoinFuturesContract+symbol, &resp) +} + +// GetFuturesRealTimeTicker get real time ticker +func (ku *Kucoin) GetFuturesRealTimeTicker(ctx context.Context, symbol string) (*FuturesTicker, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params := url.Values{} + params.Set("symbol", symbol) + var resp *FuturesTicker + return resp, ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, common.EncodeURLValues(kucoinFuturesRealTimeTicker, params), &resp) +} + +// GetFuturesOrderbook gets full orderbook for a specified symbol +func (ku *Kucoin) GetFuturesOrderbook(ctx context.Context, symbol string) (*Orderbook, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params := url.Values{} + params.Set("symbol", symbol) + var o futuresOrderbookResponse + err := ku.SendHTTPRequest(ctx, exchange.RestFutures, futuresRetrieveFullOrderbookLevel2EPL, common.EncodeURLValues(kucoinFuturesFullOrderbook, params), &o) + if err != nil { + return nil, err + } + return constructFuturesOrderbook(&o) +} + +// GetFuturesPartOrderbook20 gets orderbook for a specified symbol with depth 20 +func (ku *Kucoin) GetFuturesPartOrderbook20(ctx context.Context, symbol string) (*Orderbook, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params := url.Values{} + params.Set("symbol", symbol) + var o futuresOrderbookResponse + err := ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, common.EncodeURLValues(kucoinFuturesPartOrderbook20, params), &o) + if err != nil { + return nil, err + } + return constructFuturesOrderbook(&o) +} + +// GetFuturesPartOrderbook100 gets orderbook for a specified symbol with depth 100 +func (ku *Kucoin) GetFuturesPartOrderbook100(ctx context.Context, symbol string) (*Orderbook, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params := url.Values{} + params.Set("symbol", symbol) + var o futuresOrderbookResponse + err := ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, common.EncodeURLValues(kucoinFuturesPartOrderbook100, params), &o) + if err != nil { + return nil, err + } + return constructFuturesOrderbook(&o) +} + +// GetFuturesTradeHistory get last 100 trades for symbol +func (ku *Kucoin) GetFuturesTradeHistory(ctx context.Context, symbol string) ([]FuturesTrade, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params := url.Values{} + params.Set("symbol", symbol) + var resp []FuturesTrade + return resp, ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, common.EncodeURLValues(kucoinFuturesTradeHistory, params), &resp) +} + +// GetFuturesInterestRate get interest rate +func (ku *Kucoin) GetFuturesInterestRate(ctx context.Context, symbol string, startAt, endAt time.Time, reverse, forward bool, offset, maxCount int64) (*FundingInterestRateResponse, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params := url.Values{} + params.Set("symbol", symbol) + + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + params.Set("reverse", strconv.FormatBool(reverse)) + params.Set("forward", strconv.FormatBool(forward)) + if offset != 0 { + params.Set("offset", strconv.FormatInt(offset, 10)) + } + if maxCount != 0 { + params.Set("maxCount", strconv.FormatInt(maxCount, 10)) + } + var resp *FundingInterestRateResponse + return resp, ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, common.EncodeURLValues(kucoinFuturesInterestRate, params), &resp) +} + +// GetFuturesIndexList retrieves futures index information for a symbol +func (ku *Kucoin) GetFuturesIndexList(ctx context.Context, symbol string, startAt, endAt time.Time, reverse, forward bool, offset, maxCount int64) (*FuturesIndexResponse, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params := url.Values{} + params.Set("symbol", symbol) + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + params.Set("reverse", strconv.FormatBool(reverse)) + params.Set("forward", strconv.FormatBool(forward)) + if offset != 0 { + params.Set("offset", strconv.FormatInt(offset, 10)) + } + if maxCount != 0 { + params.Set("maxCount", strconv.FormatInt(maxCount, 10)) + } + var resp *FuturesIndexResponse + return resp, ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, common.EncodeURLValues(kucoinFuturesIndex, params), &resp) +} + +// GetFuturesCurrentMarkPrice get current mark price +func (ku *Kucoin) GetFuturesCurrentMarkPrice(ctx context.Context, symbol string) (*FuturesMarkPrice, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + var resp *FuturesMarkPrice + return resp, ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, fmt.Sprintf(kucoinFuturesMarkPrice, symbol), &resp) +} + +// GetFuturesPremiumIndex get premium index +func (ku *Kucoin) GetFuturesPremiumIndex(ctx context.Context, symbol string, startAt, endAt time.Time, reverse, forward bool, offset, maxCount int64) (*FuturesInterestRateResponse, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params := url.Values{} + params.Set("symbol", symbol) + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + params.Set("reverse", strconv.FormatBool(reverse)) + params.Set("forward", strconv.FormatBool(forward)) + if offset != 0 { + params.Set("offset", strconv.FormatInt(offset, 10)) + } + if maxCount != 0 { + params.Set("maxCount", strconv.FormatInt(maxCount, 10)) + } + var resp *FuturesInterestRateResponse + return resp, ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, common.EncodeURLValues(kucoinFuturesPremiumIndex, params), &resp) +} + +// GetFuturesCurrentFundingRate get current funding rate +func (ku *Kucoin) GetFuturesCurrentFundingRate(ctx context.Context, symbol string) (*FuturesFundingRate, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + var resp *FuturesFundingRate + return resp, ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, fmt.Sprintf(kucoinFuturesFundingRate, symbol), &resp) +} + +// GetFuturesServerTime get server time +func (ku *Kucoin) GetFuturesServerTime(ctx context.Context) (time.Time, error) { + resp := struct { + Data convert.ExchangeTime `json:"data"` + Error + }{} + err := ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, kucoinFuturesServerTime, &resp) + if err != nil { + return time.Time{}, err + } + return resp.Data.Time(), nil +} + +// GetFuturesServiceStatus get service status +func (ku *Kucoin) GetFuturesServiceStatus(ctx context.Context) (*FuturesServiceStatus, error) { + var resp *FuturesServiceStatus + return resp, ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, kucoinFuturesServiceStatus, &resp) +} + +// GetFuturesKline get contract's kline data +func (ku *Kucoin) GetFuturesKline(ctx context.Context, granularity int64, symbol string, from, to time.Time) ([]FuturesKline, error) { + if granularity == 0 { + return nil, errors.New("granularity can not be empty") + } + if !common.StringDataContains(validGranularity, strconv.FormatInt(granularity, 10)) { + return nil, errors.New("invalid granularity") + } + params := url.Values{} + // The granularity (granularity parameter of K-line) represents the number of minutes, the available granularity scope is: 1,5,15,30,60,120,240,480,720,1440,10080. Requests beyond the above range will be rejected. + params.Set("granularity", strconv.FormatInt(granularity, 10)) + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params.Set("symbol", symbol) + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.UnixMilli(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.UnixMilli(), 10)) + } + var resp [][6]float64 + err := ku.SendHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, common.EncodeURLValues(kucoinFuturesKline, params), &resp) + if err != nil { + return nil, err + } + kline := make([]FuturesKline, len(resp)) + for i := range resp { + kline[i] = FuturesKline{ + StartTime: time.UnixMilli(int64(resp[i][0])), + Open: resp[i][1], + High: resp[i][2], + Low: resp[i][3], + Close: resp[i][4], + Volume: resp[i][5], + } + } + return kline, nil +} + +// PostFuturesOrder used to place two types of futures orders: limit and market +func (ku *Kucoin) PostFuturesOrder(ctx context.Context, arg *FuturesOrderParam) (string, error) { + if arg.Leverage < 0.01 { + return "", fmt.Errorf("%w must be greater than 0.01", errInvalidLeverage) + } + if arg.ClientOrderID == "" { + return "", errInvalidClientOrderID + } + if arg.Side == "" { + return "", fmt.Errorf("%w, empty order side", order.ErrSideIsInvalid) + } + if arg.Symbol.IsEmpty() { + return "", currency.ErrCurrencyPairEmpty + } + if arg.Stop != "" { + if arg.StopPriceType == "" { + return "", errInvalidStopPriceType + } + if arg.StopPrice <= 0 { + return "", fmt.Errorf("%w, stopPrice is required", errInvalidPrice) + } + } + switch arg.OrderType { + case "limit", "": + if arg.Price <= 0 { + return "", fmt.Errorf("%w %f", errInvalidPrice, arg.Price) + } + if arg.Size <= 0 { + return "", fmt.Errorf("%w, must be non-zero positive value", errInvalidSize) + } + if arg.VisibleSize < 0 { + return "", fmt.Errorf("%w, visible size must be non-zero positive value", errInvalidSize) + } + case "market": + if arg.Size <= 0 { + return "", fmt.Errorf("%w, market size must be > 0", errInvalidSize) + } + default: + return "", fmt.Errorf("%w, order type= %s", order.ErrTypeIsInvalid, arg.OrderType) + } + resp := struct { + OrderID string `json:"orderId"` + }{} + return resp.OrderID, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, futuresPlaceOrderEPL, http.MethodPost, kucoinFuturesOrder, &arg, &resp) +} + +// CancelFuturesOrder used to cancel single order previously placed including a stop order +func (ku *Kucoin) CancelFuturesOrder(ctx context.Context, orderID string) ([]string, error) { + resp := struct { + CancelledOrderIDs []string `json:"cancelledOrderIds"` + }{} + + if orderID == "" { + return resp.CancelledOrderIDs, errors.New("orderID can't be empty") + } + return resp.CancelledOrderIDs, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, futuresCancelAnOrderEPL, http.MethodDelete, kucoinFuturesCancelOrder+orderID, nil, &resp) +} + +// CancelAllFuturesOpenOrders used to cancel all futures order excluding stop orders +func (ku *Kucoin) CancelAllFuturesOpenOrders(ctx context.Context, symbol string) ([]string, error) { + params := url.Values{} + if symbol != "" { + params.Set("symbol", symbol) + } + resp := struct { + CancelledOrderIDs []string `json:"cancelledOrderIds"` + }{} + return resp.CancelledOrderIDs, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, futuresLimitOrderMassCancelationEPL, http.MethodDelete, common.EncodeURLValues(kucoinFuturesOrder, params), nil, &resp) +} + +// CancelAllFuturesStopOrders used to cancel all untriggered stop orders +func (ku *Kucoin) CancelAllFuturesStopOrders(ctx context.Context, symbol string) ([]string, error) { + params := url.Values{} + if symbol != "" { + params.Set("symbol", symbol) + } + resp := struct { + CancelledOrderIDs []string `json:"cancelledOrderIds"` + }{} + return resp.CancelledOrderIDs, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodDelete, common.EncodeURLValues(kucoinFuturesStopOrder, params), nil, &resp) +} + +// GetFuturesOrders gets the user current futures order list +func (ku *Kucoin) GetFuturesOrders(ctx context.Context, status, symbol, side, orderType string, startAt, endAt time.Time) (*FutureOrdersResponse, error) { + params := url.Values{} + if status != "" { + params.Set("status", status) + } + if symbol != "" { + params.Set("symbol", symbol) + } + if side != "" { + params.Set("side", side) + } + if orderType != "" { + params.Set("type", orderType) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + var resp *FutureOrdersResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, futuresRetrieveOrderListEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesOrder, params), nil, &resp) +} + +// GetUntriggeredFuturesStopOrders gets the untriggered stop orders list +func (ku *Kucoin) GetUntriggeredFuturesStopOrders(ctx context.Context, symbol, side, orderType string, startAt, endAt time.Time) (*FutureOrdersResponse, error) { + params := url.Values{} + if symbol != "" { + params.Set("symbol", symbol) + } + if side != "" { + params.Set("side", side) + } + if orderType != "" { + params.Set("type", orderType) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + var resp *FutureOrdersResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesStopOrder, params), nil, &resp) +} + +// GetFuturesRecentCompletedOrders gets list of recent 1000 orders in the last 24 hours +func (ku *Kucoin) GetFuturesRecentCompletedOrders(ctx context.Context) ([]FuturesOrder, error) { + var resp []FuturesOrder + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, kucoinFuturesRecentCompletedOrder, nil, &resp) +} + +// GetFuturesOrderDetails gets single order details by order ID +func (ku *Kucoin) GetFuturesOrderDetails(ctx context.Context, orderID string) (*FuturesOrder, error) { + var resp *FuturesOrder + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, kucoinFuturesGetOrderDetails+orderID, nil, &resp) +} + +// GetFuturesOrderDetailsByClientID gets single order details by client ID +func (ku *Kucoin) GetFuturesOrderDetailsByClientID(ctx context.Context, clientID string) (*FuturesOrder, error) { + if clientID == "" { + return nil, errors.New("clientID can't be empty") + } + params := url.Values{} + params.Set("clientOid", clientID) + var resp *FuturesOrder + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesGetOrderDetailsByClientID, params), nil, &resp) +} + +// GetFuturesFills gets list of recent fills +func (ku *Kucoin) GetFuturesFills(ctx context.Context, orderID, symbol, side, orderType string, startAt, endAt time.Time) (*FutureFillsResponse, error) { + params := url.Values{} + if orderID != "" { + params.Set("orderId", orderID) + } + if symbol != "" { + params.Set("symbol", symbol) + } + if side != "" { + params.Set("side", side) + } + if orderType != "" { + params.Set("type", orderType) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + var resp *FutureFillsResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, futuresRetrieveFillsEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesFills, params), nil, &resp) +} + +// GetFuturesRecentFills gets list of 1000 recent fills in the last 24 hrs +func (ku *Kucoin) GetFuturesRecentFills(ctx context.Context) ([]FuturesFill, error) { + var resp []FuturesFill + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, futuresRecentFillsEPL, http.MethodGet, kucoinFuturesRecentFills, nil, &resp) +} + +// GetFuturesOpenOrderStats gets the total number and value of the all your active orders +func (ku *Kucoin) GetFuturesOpenOrderStats(ctx context.Context, symbol string) (*FuturesOpenOrderStats, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params := url.Values{} + params.Set("symbol", symbol) + var resp *FuturesOpenOrderStats + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesOpenOrderStats, params), nil, &resp) +} + +// GetFuturesPosition gets the position details of a specified position +func (ku *Kucoin) GetFuturesPosition(ctx context.Context, symbol string) (*FuturesPosition, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params := url.Values{} + params.Set("symbol", symbol) + var resp *FuturesPosition + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesPosition, params), nil, &resp) +} + +// GetFuturesPositionList gets the list of position with details +func (ku *Kucoin) GetFuturesPositionList(ctx context.Context) ([]FuturesPosition, error) { + var resp []FuturesPosition + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, futuresRetrievePositionListEPL, http.MethodGet, kucoinFuturesPositionList, nil, &resp) +} + +// SetAutoDepositMargin enable/disable of auto-deposit margin +func (ku *Kucoin) SetAutoDepositMargin(ctx context.Context, symbol string, status bool) (bool, error) { + params := make(map[string]interface{}) + if symbol == "" { + return false, errors.New("symbol can't be empty") + } + params["symbol"] = symbol + params["status"] = status + var resp bool + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodPost, kucoinFuturesSetAutoDeposit, params, &resp) +} + +// AddMargin is used to add margin manually +func (ku *Kucoin) AddMargin(ctx context.Context, symbol, uniqueID string, margin float64) (*FuturesPosition, error) { + params := make(map[string]interface{}) + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params["symbol"] = symbol + if uniqueID == "" { + return nil, errors.New("uniqueID can't be empty") + } + params["bizNo"] = uniqueID + if margin <= 0 { + return nil, errors.New("margin can't be zero or negative") + } + params["margin"] = strconv.FormatFloat(margin, 'f', -1, 64) + var resp *FuturesPosition + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodPost, kucoinFuturesAddMargin, params, &resp) +} + +// GetFuturesRiskLimitLevel gets information about risk limit level of a specific contract +func (ku *Kucoin) GetFuturesRiskLimitLevel(ctx context.Context, symbol string) ([]FuturesRiskLimitLevel, error) { + var resp []FuturesRiskLimitLevel + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, kucoinFuturesRiskLimitLevel+symbol, nil, &resp) +} + +// FuturesUpdateRiskLmitLevel is used to adjustment the risk limit level +func (ku *Kucoin) FuturesUpdateRiskLmitLevel(ctx context.Context, symbol string, level int64) (bool, error) { + params := make(map[string]interface{}) + if symbol == "" { + return false, errors.New("symbol can't be empty") + } + params["symbol"] = symbol + params["level"] = strconv.FormatInt(level, 10) + var resp bool + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodPost, kucoinFuturesUpdateRiskLmitLevel, params, &resp) +} + +// GetFuturesFundingHistory gets information about funding history +func (ku *Kucoin) GetFuturesFundingHistory(ctx context.Context, symbol string, offset, maxCount int64, reverse, forward bool, startAt, endAt time.Time) (*FuturesFundingHistoryResponse, error) { + if symbol == "" { + return nil, errors.New("symbol can't be empty") + } + params := url.Values{} + params.Set("symbol", symbol) + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + params.Set("reverse", strconv.FormatBool(reverse)) + params.Set("forward", strconv.FormatBool(forward)) + if offset != 0 { + params.Set("offset", strconv.FormatInt(offset, 10)) + } + if maxCount != 0 { + params.Set("maxCount", strconv.FormatInt(maxCount, 10)) + } + var resp *FuturesFundingHistoryResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, futuresRetrieveFundingHistoryEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesFundingHistory, params), nil, &resp) +} + +// GetFuturesAccountOverview gets future account overview +func (ku *Kucoin) GetFuturesAccountOverview(ctx context.Context, currency string) (FuturesAccount, error) { + params := url.Values{} + if currency != "" { + params.Set("currency", currency) + } + resp := FuturesAccount{} + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, futuresRetrieveAccountOverviewEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesAccountOverview, params), nil, &resp) +} + +// GetFuturesTransactionHistory gets future transaction history +func (ku *Kucoin) GetFuturesTransactionHistory(ctx context.Context, currency, txType string, offset, maxCount int64, forward bool, startAt, endAt time.Time) (*FuturesTransactionHistoryResponse, error) { + params := url.Values{} + if currency != "" { + params.Set("currency", currency) + } + if txType != "" { + params.Set("type", txType) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + params.Set("forward", strconv.FormatBool(forward)) + if offset != 0 { + params.Set("offset", strconv.FormatInt(offset, 10)) + } + if maxCount != 0 { + params.Set("maxCount", strconv.FormatInt(maxCount, 10)) + } + var resp *FuturesTransactionHistoryResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, futuresRetrieveTransactionHistoryEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesTransactionHistory, params), nil, &resp) +} + +// CreateFuturesSubAccountAPIKey is used to create Futures APIs for sub-accounts +func (ku *Kucoin) CreateFuturesSubAccountAPIKey(ctx context.Context, ipWhitelist, passphrase, permission, remark, subName string) (*APIKeyDetail, error) { + params := make(map[string]interface{}) + if ipWhitelist != "" { + params["ipWhitelist"] = ipWhitelist + } + if passphrase == "" { + return nil, errors.New("passphrase can't be empty") + } + params["passphrase"] = passphrase + if permission != "" { + params["permission"] = permission + } + if remark == "" { + return nil, errors.New("remark can't be empty") + } + params["remark"] = remark + if subName == "" { + return nil, errors.New("subName can't be empty") + } + params["subName"] = subName + var resp *APIKeyDetail + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodPost, kucoinFuturesSubAccountAPI, params, &resp) +} + +// GetFuturesDepositAddress gets deposit address for currency +func (ku *Kucoin) GetFuturesDepositAddress(ctx context.Context, currency string) (*DepositAddress, error) { + if currency == "" { + return nil, errors.New("currency can't be empty") + } + params := url.Values{} + params.Set("currency", currency) + var resp *DepositAddress + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesDepositAddress, params), nil, &resp) +} + +// GetFuturesDepositsList gets deposits list +func (ku *Kucoin) GetFuturesDepositsList(ctx context.Context, currency, status string, startAt, endAt time.Time) (*FuturesDepositDetailsResponse, error) { + params := url.Values{} + if currency != "" { + params.Set("currency", currency) + } + if status != "" { + params.Set("status", status) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + var resp *FuturesDepositDetailsResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesDepositsList, params), nil, &resp) +} + +// GetFuturesWithdrawalLimit gets withdrawal limits for currency +func (ku *Kucoin) GetFuturesWithdrawalLimit(ctx context.Context, currency string) (*FuturesWithdrawalLimit, error) { + if currency == "" { + return nil, errors.New("currency can't be empty") + } + params := url.Values{} + params.Set("currency", currency) + var resp *FuturesWithdrawalLimit + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesWithdrawalLimit, params), nil, &resp) +} + +// GetFuturesWithdrawalList gets withdrawal list +func (ku *Kucoin) GetFuturesWithdrawalList(ctx context.Context, currency, status string, startAt, endAt time.Time) (*FuturesWithdrawalsListResponse, error) { + params := url.Values{} + if currency != "" { + params.Set("currency", currency) + } + if status != "" { + params.Set("status", status) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + var resp *FuturesWithdrawalsListResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesWithdrawalList, params), nil, &resp) +} + +// CancelFuturesWithdrawal is used to cancel withdrawal request of only PROCESSING status +func (ku *Kucoin) CancelFuturesWithdrawal(ctx context.Context, withdrawalID string) (bool, error) { + var resp bool + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodDelete, kucoinFuturesCancelWithdrawal+withdrawalID, nil, &resp) +} + +// TransferFuturesFundsToMainAccount helps in transferring funds from futures to main/trade account +func (ku *Kucoin) TransferFuturesFundsToMainAccount(ctx context.Context, amount float64, currency, recAccountType string) (*TransferRes, error) { + params := make(map[string]interface{}) + if amount <= 0 { + return nil, errors.New("amount can't be zero or negative") + } + params["amount"] = amount + if currency == "" { + return nil, errors.New("currency can't be empty") + } + params["currency"] = currency + if recAccountType == "" { + return nil, errors.New("recAccountType can't be empty") + } + params["recAccountType"] = recAccountType + var resp *TransferRes + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodPost, kucoinFuturesTransferFundtoMainAccount, params, &resp) +} + +// TransferFundsToFuturesAccount helps in transferring funds from payee account to futures account +func (ku *Kucoin) TransferFundsToFuturesAccount(ctx context.Context, amount float64, currency, payAccountType string) error { + params := make(map[string]interface{}) + if amount <= 0 { + return errors.New("amount can't be zero or negative") + } + params["amount"] = amount + if currency == "" { + return errors.New("currency can't be empty") + } + params["currency"] = currency + if payAccountType == "" { + return errors.New("payAccountType can't be empty") + } + params["payAccountType"] = payAccountType + resp := struct { + Error + }{} + return ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodPost, kucoinFuturesTransferFundtoFuturesAccount, params, &resp) +} + +// GetFuturesTransferOutList gets list of transfer out +func (ku *Kucoin) GetFuturesTransferOutList(ctx context.Context, currency, status string, startAt, endAt time.Time) (*TransferListsResponse, error) { + if currency == "" { + return nil, errors.New("currency can't be empty") + } + params := url.Values{} + params.Set("currency", currency) + if status != "" { + params.Set("status", status) + } + if !startAt.IsZero() { + params.Set("startAt", strconv.FormatInt(startAt.UnixMilli(), 10)) + } + if !endAt.IsZero() { + params.Set("endAt", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + var resp *TransferListsResponse + return resp, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodGet, common.EncodeURLValues(kucoinFuturesTransferOutList, params), nil, &resp) +} + +// CancelFuturesTransferOut is used to cancel transfer out request of only PROCESSING status +func (ku *Kucoin) CancelFuturesTransferOut(ctx context.Context, applyID string) error { + if applyID == "" { + return errors.New("applyID can't be empty") + } + params := url.Values{} + params.Set("applyId", applyID) + resp := struct { + Error + }{} + return ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodDelete, common.EncodeURLValues(kucoinFuturesCancelTransferOut, params), nil, &resp) +} + +func processFuturesOB(ob [][2]float64) []orderbook.Item { + o := make([]orderbook.Item, len(ob)) + for x := range ob { + o[x] = orderbook.Item{ + Price: ob[x][0], + Amount: ob[x][1], + } + } + return o +} + +func constructFuturesOrderbook(o *futuresOrderbookResponse) (*Orderbook, error) { + var ( + s Orderbook + err error + ) + s.Bids = processFuturesOB(o.Bids) + if err != nil { + return nil, err + } + s.Asks = processFuturesOB(o.Asks) + if err != nil { + return nil, err + } + s.Sequence = o.Sequence + s.Time = o.Time.Time() + return &s, err +} diff --git a/exchanges/kucoin/kucoin_futures_types.go b/exchanges/kucoin/kucoin_futures_types.go new file mode 100644 index 00000000..d8980941 --- /dev/null +++ b/exchanges/kucoin/kucoin_futures_types.go @@ -0,0 +1,451 @@ +package kucoin + +import ( + "time" + + "github.com/thrasher-corp/gocryptotrader/common/convert" +) + +var ( + validGranularity = []string{ + "1", "5", "15", "30", "60", "120", "240", "480", "720", "1440", "10080", + } +) + +// Contract store contract details +type Contract struct { + Symbol string `json:"symbol"` + RootSymbol string `json:"rootSymbol"` + ContractType string `json:"type"` + FirstOpenDate convert.ExchangeTime `json:"firstOpenDate"` + ExpireDate convert.ExchangeTime `json:"expireDate"` + SettleDate convert.ExchangeTime `json:"settleDate"` + BaseCurrency string `json:"baseCurrency"` + QuoteCurrency string `json:"quoteCurrency"` + SettleCurrency string `json:"settleCurrency"` + MaxOrderQty float64 `json:"maxOrderQty"` + MaxPrice float64 `json:"maxPrice"` + LotSize float64 `json:"lotSize"` + TickSize float64 `json:"tickSize"` + IndexPriceTickSize float64 `json:"indexPriceTickSize"` + Multiplier float64 `json:"multiplier"` + InitialMargin float64 `json:"initialMargin"` + MaintainMargin float64 `json:"maintainMargin"` + MaxRiskLimit float64 `json:"maxRiskLimit"` + MinRiskLimit float64 `json:"minRiskLimit"` + RiskStep float64 `json:"riskStep"` + MakerFeeRate float64 `json:"makerFeeRate"` + TakerFeeRate float64 `json:"takerFeeRate"` + TakerFixFee float64 `json:"takerFixFee"` + MakerFixFee float64 `json:"makerFixFee"` + SettlementFee float64 `json:"settlementFee"` + IsDeleverage bool `json:"isDeleverage"` + IsQuanto bool `json:"isQuanto"` + IsInverse bool `json:"isInverse"` + MarkMethod string `json:"markMethod"` + FairMethod string `json:"fairMethod"` + FundingBaseSymbol string `json:"fundingBaseSymbol"` + FundingQuoteSymbol string `json:"fundingQuoteSymbol"` + FundingRateSymbol string `json:"fundingRateSymbol"` + IndexSymbol string `json:"indexSymbol"` + SettlementSymbol string `json:"settlementSymbol"` + Status string `json:"status"` + FundingFeeRate float64 `json:"fundingFeeRate"` + PredictedFundingFeeRate float64 `json:"predictedFundingFeeRate"` + OpenInterest string `json:"openInterest"` + TurnoverOf24h float64 `json:"turnoverOf24h"` + VolumeOf24h float64 `json:"volumeOf24h"` + MarkPrice float64 `json:"markPrice"` + IndexPrice float64 `json:"indexPrice"` + LastTradePrice float64 `json:"lastTradePrice"` + NextFundingRateTime float64 `json:"nextFundingRateTime"` + MaxLeverage float64 `json:"maxLeverage"` + SourceExchanges []string `json:"sourceExchanges"` + PremiumsSymbol1M string `json:"premiumsSymbol1M"` + PremiumsSymbol8H string `json:"premiumsSymbol8H"` + FundingBaseSymbol1M string `json:"fundingBaseSymbol1M"` + FundingQuoteSymbol1M string `json:"fundingQuoteSymbol1M"` + LowPrice float64 `json:"lowPrice"` + HighPrice float64 `json:"highPrice"` + PriceChgPct float64 `json:"priceChgPct"` + PriceChg float64 `json:"priceChg"` +} + +// FuturesTicker stores ticker data +type FuturesTicker struct { + Sequence int64 `json:"sequence"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Size float64 `json:"size"` + Price float64 `json:"price"` + BestBidSize float64 `json:"bestBidSize"` + BestBidPrice float64 `json:"bestBidPrice"` + BestAskSize float64 `json:"bestAskSize"` + BestAskPrice float64 `json:"bestAskPrice"` + TradeID string `json:"tradeId"` + FilledTime convert.ExchangeTime `json:"time"` +} + +type futuresOrderbookResponse struct { + Asks [][2]float64 `json:"asks"` + Bids [][2]float64 `json:"bids"` + Time convert.ExchangeTime `json:"ts"` + Sequence int64 `json:"sequence"` + Symbol string `json:"symbol"` +} + +// FuturesTrade stores trade data +type FuturesTrade struct { + Sequence int64 `json:"sequence"` + TradeID string `json:"tradeId"` + TakerOrderID string `json:"takerOrderId"` + MakerOrderID string `json:"makerOrderId"` + Price float64 `json:"price,string"` + Size float64 `json:"size"` + Side string `json:"side"` + FilledTime convert.ExchangeTime `json:"ts"` +} + +// FuturesInterestRate stores interest rate data +type FuturesInterestRate struct { + Symbol string `json:"symbol"` + TimePoint convert.ExchangeTime `json:"timePoint"` + Value float64 `json:"value"` + Granularity int64 `json:"granularity"` +} + +// Decomposition stores decomposition data +type Decomposition struct { + Exchange string `json:"exchange"` + Price float64 `json:"price"` + Weight float64 `json:"weight"` +} + +// FuturesIndex stores index data +type FuturesIndex struct { + FuturesInterestRate + DecompositionList []Decomposition `json:"decompositionList"` +} + +// FuturesMarkPrice stores mark price data +type FuturesMarkPrice struct { + FuturesInterestRate + IndexPrice float64 `json:"indexPrice"` +} + +// FuturesFundingRate stores funding rate data +type FuturesFundingRate struct { + FuturesInterestRate + PredictedValue float64 `json:"predictedValue"` +} + +// FuturesKline stores kline data +type FuturesKline struct { + StartTime time.Time + Open float64 + Close float64 + High float64 + Low float64 + Volume float64 +} + +// FutureOrdersResponse represents a future order response list detail. +type FutureOrdersResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []FuturesOrder `json:"items"` +} + +// FuturesOrder represents futures order information +type FuturesOrder struct { + ID string `json:"id"` + Symbol string `json:"symbol"` + OrderType string `json:"type"` + Side string `json:"side"` + Price float64 `json:"price,string"` + Size float64 `json:"size"` + Value float64 `json:"value,string"` + DealValue float64 `json:"dealValue,string"` + DealSize float64 `json:"dealSize"` + Stp string `json:"stp"` + Stop string `json:"stop"` + StopPriceType string `json:"stopPriceType"` + StopTriggered bool `json:"stopTriggered"` + StopPrice float64 `json:"stopPrice,string"` + TimeInForce string `json:"timeInForce"` + PostOnly bool `json:"postOnly"` + Hidden bool `json:"hidden"` + Iceberg bool `json:"iceberg"` + Leverage float64 `json:"leverage,string"` + ForceHold bool `json:"forceHold"` + CloseOrder bool `json:"closeOrder"` + VisibleSize float64 `json:"visibleSize"` + ClientOid string `json:"clientOid"` + Remark string `json:"remark"` + Tags string `json:"tags"` + IsActive bool `json:"isActive"` + CancelExist bool `json:"cancelExist"` + CreatedAt convert.ExchangeTime `json:"createdAt"` + UpdatedAt convert.ExchangeTime `json:"updatedAt"` + EndAt convert.ExchangeTime `json:"endAt"` + OrderTime convert.ExchangeTime `json:"orderTime"` + SettleCurrency string `json:"settleCurrency"` + Status string `json:"status"` + FilledValue float64 `json:"filledValue,string"` + FilledSize float64 `json:"filledSize"` + ReduceOnly bool `json:"reduceOnly"` +} + +// FutureFillsResponse represents a future fills list response detail. +type FutureFillsResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []FuturesFill `json:"items"` +} + +// FuturesFill represents list of recent fills for futures orders. +type FuturesFill struct { + Symbol string `json:"symbol"` + TradeID string `json:"tradeId"` + OrderID string `json:"orderId"` + Side string `json:"side"` + Liquidity string `json:"liquidity"` + ForceTaker bool `json:"forceTaker"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + Value float64 `json:"value,string"` + FeeRate float64 `json:"feeRate,string"` + FixFee float64 `json:"fixFee,string"` + FeeCurrency string `json:"feeCurrency"` + Stop string `json:"stop"` + Fee float64 `json:"fee,string"` + OrderType string `json:"orderType"` + TradeType string `json:"tradeType"` + CreatedAt convert.ExchangeTime `json:"createdAt"` + SettleCurrency string `json:"settleCurrency"` + TradeTime convert.ExchangeTime `json:"tradeTime"` +} + +// FuturesOpenOrderStats represents futures open order summary stats information. +type FuturesOpenOrderStats struct { + OpenOrderBuySize int64 `json:"openOrderBuySize"` + OpenOrderSellSize int64 `json:"openOrderSellSize"` + OpenOrderBuyCost float64 `json:"openOrderBuyCost,string"` + OpenOrderSellCost float64 `json:"openOrderSellCost,string"` + SettleCurrency string `json:"settleCurrency"` +} + +// FuturesPosition represents futures position detailed information. +type FuturesPosition struct { + ID string `json:"id"` + Symbol string `json:"symbol"` + AutoDeposit bool `json:"autoDeposit"` + MaintMarginReq float64 `json:"maintMarginReq"` + RiskLimit int64 `json:"riskLimit"` + RealLeverage float64 `json:"realLeverage"` + CrossMode bool `json:"crossMode"` + ADLRankingPercentile float64 `json:"delevPercentage"` + OpeningTimestamp convert.ExchangeTime `json:"openingTimestamp"` + CurrentTimestamp convert.ExchangeTime `json:"currentTimestamp"` + CurrentQty int64 `json:"currentQty"` + CurrentCost float64 `json:"currentCost"` // Current position value + CurrentComm float64 `json:"currentComm"` // Current commission + UnrealisedCost float64 `json:"unrealisedCost"` + RealisedGrossCost float64 `json:"realisedGrossCost"` + RealisedCost float64 `json:"realisedCost"` + IsOpen bool `json:"isOpen"` + MarkPrice float64 `json:"markPrice"` + MarkValue float64 `json:"markValue"` + PosCost float64 `json:"posCost"` // Position value + PosCross float64 `json:"posCross"` // Added margin + PosInit float64 `json:"posInit"` // Leverage margin + PosComm float64 `json:"posComm"` // Bankruptcy cost + PosLoss float64 `json:"posLoss"` // Funding fees paid out + PosMargin float64 `json:"posMargin"` // Position margin + PosMaint float64 `json:"posMaint"` // Maintenance margin + MaintMargin float64 `json:"maintMargin"` + RealisedGrossPnl float64 `json:"realisedGrossPnl"` + RealisedPnl float64 `json:"realisedPnl"` + UnrealisedPnl float64 `json:"unrealisedPnl"` + UnrealisedPnlPcnt float64 `json:"unrealisedPnlPcnt"` + UnrealisedRoePcnt float64 `json:"unrealisedRoePcnt"` + AvgEntryPrice float64 `json:"avgEntryPrice"` + LiquidationPrice float64 `json:"liquidationPrice"` + BankruptPrice float64 `json:"bankruptPrice"` + SettleCurrency string `json:"settleCurrency"` + MaintainMargin float64 `json:"maintainMargin"` + RiskLimitLevel int64 `json:"riskLimitLevel"` +} + +// FuturesRiskLimitLevel represents futures risk limit level information. +type FuturesRiskLimitLevel struct { + Symbol string `json:"symbol"` + Level int64 `json:"level"` + MaxRiskLimit float64 `json:"maxRiskLimit"` + MinRiskLimit float64 `json:"minRiskLimit"` + MaxLeverage float64 `json:"maxLeverage"` + InitialMargin float64 `json:"initialMargin"` + MaintainMargin float64 `json:"maintainMargin"` +} + +// FuturesFundingHistory represents futures funding information. +type FuturesFundingHistory struct { + ID string `json:"id"` + Symbol string `json:"symbol"` + Time convert.ExchangeTime `json:"timePoint"` + FundingRate float64 `json:"fundingRate"` + MarkPrice float64 `json:"markPrice"` + PositionQty float64 `json:"positionQty"` + PositionCost float64 `json:"positionCost"` + Funding float64 `json:"funding"` + SettleCurrency string `json:"settleCurrency"` +} + +// FuturesAccount holds futures account detail information. +type FuturesAccount struct { + AccountEquity float64 `json:"accountEquity"` // marginBalance + Unrealised PNL + UnrealisedPNL float64 `json:"unrealisedPNL"` // unrealised profit and loss + MarginBalance float64 `json:"marginBalance"` // positionMargin + orderMargin + frozenFunds + availableBalance - unrealisedPNL + PositionMargin float64 `json:"positionMargin"` + OrderMargin float64 `json:"orderMargin"` + FrozenFunds float64 `json:"frozenFunds"` // frozen funds for withdrawal and out-transfer + AvailableBalance float64 `json:"availableBalance"` + Currency string `json:"currency"` +} + +// FuturesTransactionHistory represents a transaction history +type FuturesTransactionHistory struct { + Time convert.ExchangeTime `json:"time"` + Type string `json:"type"` + Amount float64 `json:"amount"` + Fee float64 `json:"fee"` + AccountEquity float64 `json:"accountEquity"` + Status string `json:"status"` + Remark string `json:"remark"` + Offset int64 `json:"offset"` + Currency string `json:"currency"` +} + +// APIKeyDetail represents the API key detail +type APIKeyDetail struct { + SubName string `json:"subName"` + Remark string `json:"remark"` + APIKey string `json:"apiKey"` + APISecret string `json:"apiSecret"` + Passphrase string `json:"passphrase"` + Permission string `json:"permission"` + IPWhitelist string `json:"ipWhitelist"` + CreateAt convert.ExchangeTime `json:"createdAt"` +} + +// FuturesDepositDetailsResponse represents a futures deposits list detail response. +type FuturesDepositDetailsResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []FuturesDepositDetail `json:"items"` +} + +// FuturesDepositDetail represents futures deposit detail information. +type FuturesDepositDetail struct { + Currency string `json:"currency"` + Status string `json:"status"` + Address string `json:"address"` + IsInner bool `json:"isInner"` + Amount float64 `json:"amount"` + Fee float64 `json:"fee"` + WalletTxID string `json:"walletTxId"` + CreatedAt convert.ExchangeTime `json:"createdAt"` +} + +// FuturesWithdrawalLimit represents withdrawal limit information. +type FuturesWithdrawalLimit struct { + Currency string `json:"currency"` + ChainID string `json:"chainId"` + LimitAmount float64 `json:"limitAmount"` + UsedAmount float64 `json:"usedAmount"` + RemainAmount float64 `json:"remainAmount"` + AvailableAmount float64 `json:"availableAmount"` + WithdrawMinFee float64 `json:"withdrawMinFee"` + InnerWithdrawMinFee float64 `json:"innerWithdrawMinFee"` + WithdrawMinSize float64 `json:"withdrawMinSize"` + IsWithdrawEnabled bool `json:"isWithdrawEnabled"` + Precision float64 `json:"precision"` +} + +// FuturesWithdrawalsListResponse represents a list of futures Withdrawal history instance. +type FuturesWithdrawalsListResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []FuturesWithdrawalHistory `json:"items"` +} + +// FuturesWithdrawalHistory represents a list of Futures withdrawal history. +type FuturesWithdrawalHistory struct { + WithdrawalID string `json:"withdrawalId"` + Currency string `json:"currency"` + Status string `json:"status"` + Address string `json:"address"` + Memo string `json:"memo"` + IsInner bool `json:"isInner"` + Amount float64 `json:"amount"` + Fee float64 `json:"fee"` + WalletTxID string `json:"walletTxId"` + CreatedAt convert.ExchangeTime `json:"createdAt"` + Remark string `json:"remark"` + Reason string `json:"reason"` +} + +// TransferBase represents transfer base information. +type TransferBase struct { + ApplyID string `json:"applyId"` + Currency string `json:"currency"` + RecRemark string `json:"recRemark"` + RecSystem string `json:"recSystem"` + Status string `json:"status"` + Amount float64 `json:"amount,string"` + Reason string `json:"reason"` + CreatedAt convert.ExchangeTime `json:"createdAt"` + Remark string `json:"remark"` +} + +// TransferRes represents a transfer response +type TransferRes struct { + TransferBase + BizNo string `json:"bizNo"` + PayAccountType string `json:"payAccountType"` + PayTag string `json:"payTag"` + RecAccountType string `json:"recAccountType"` + RecTag string `json:"recTag"` + Fee float64 `json:"fee,string"` + Serial int64 `json:"sn"` + UpdatedAt convert.ExchangeTime `json:"updatedAt"` +} + +// TransferListsResponse represents a transfer lists detail. +type TransferListsResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []Transfer `json:"items"` +} + +// Transfer represents a transfer detail. +type Transfer struct { + TransferBase + Offset int64 `json:"offset"` +} + +// FuturesServiceStatus represents service status. +type FuturesServiceStatus struct { + Status string `json:"status"` + Message string `json:"msg"` +} diff --git a/exchanges/kucoin/kucoin_ratelimit.go b/exchanges/kucoin/kucoin_ratelimit.go new file mode 100644 index 00000000..7f8a52b4 --- /dev/null +++ b/exchanges/kucoin/kucoin_ratelimit.go @@ -0,0 +1,244 @@ +package kucoin + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "golang.org/x/time/rate" +) + +const ( + threeSecondsInterval = time.Second * 3 + oneMinuteInterval = time.Minute +) + +// RateLimit implements the request.Limiter interface +type RateLimit struct { + RetrieveAccountLedger *rate.Limiter + MasterSubUserTransfer *rate.Limiter + RetrieveDepositList *rate.Limiter + RetrieveV1HistoricalDepositList *rate.Limiter + RetrieveWithdrawalList *rate.Limiter + RetrieveV1HistoricalWithdrawalList *rate.Limiter + PlaceOrder *rate.Limiter + PlaceMarginOrders *rate.Limiter + PlaceBulkOrders *rate.Limiter + CancelOrder *rate.Limiter + CancelAllOrders *rate.Limiter + ListOrders *rate.Limiter + ListFills *rate.Limiter + RetrieveFullOrderbook *rate.Limiter + RetrieveMarginAccount *rate.Limiter + SpotRate *rate.Limiter + FuturesRate *rate.Limiter + + FRetrieveAccountOverviewRate *rate.Limiter + FRetrieveTransactionHistoryRate *rate.Limiter + FPlaceOrderRate *rate.Limiter + FCancelAnOrderRate *rate.Limiter + FLimitOrderMassCancelationRate *rate.Limiter + FRetrieveOrderListRate *rate.Limiter + FRetrieveFillsRate *rate.Limiter + FRecentFillsRate *rate.Limiter + FRetrievePositionListRate *rate.Limiter + FRetrieveFundingHistoryRate *rate.Limiter + FRetrieveFullOrderbookLevel2Rate *rate.Limiter +} + +// rate of request per interval +const ( + retrieveAccountLedgerRate = 18 + masterSubUserTransferRate = 3 + retrieveDepositListRate = 6 + retrieveV1HistoricalDepositListRate = 6 + retrieveWithdrawalListRate = 6 + retrieveV1HistoricalWithdrawalListRate = 6 + placeOrderRate = 45 + placeMarginOrdersRate = 45 + placeBulkOrdersRate = 3 + cancelOrderRate = 60 + cancelAllOrdersRate = 3 + listOrdersRate = 30 + listFillsRate = 9 + retrieveFullOrderbookRate = 30 + retrieveMarginAccountRate = 1 + + futuresRetrieveAccountOverviewRate = 30 + futuresRetrieveTransactionHistoryRate = 9 + futuresPlaceOrderRate = 30 + futuresCancelAnOrderRate = 40 + futuresLimitOrderMassCancelationRate = 9 + futuresRetrieveOrderListRate = 30 + futuresRetrieveFillsRate = 9 + futuresRecentFillsRate = 9 + futuresRetrievePositionListRate = 9 + futuresRetrieveFundingHistoryRate = 9 + futuresRetrieveFullOrderbookLevel2Rate = 30 + + defaultSpotRate = 1200 + defaultFuturesRate = 1200 +) + +const ( + // for spot endpoints + retrieveAccountLedgerEPL request.EndpointLimit = iota + masterSubUserTransferEPL + retrieveDepositListEPL + retrieveV1HistoricalDepositListEPL + retrieveWithdrawalListEPL + retrieveV1HistoricalWithdrawalListEPL + placeOrderEPL + placeMarginOrdersEPL + placeBulkOrdersEPL + cancelOrderEPL + cancelAllOrdersEPL + listOrdersEPL + listFillsEPL + retrieveFullOrderbookEPL + retrieveMarginAccountEPL + defaultSpotEPL + + // for futures endpoints + futuresRetrieveAccountOverviewEPL + futuresRetrieveTransactionHistoryEPL + futuresPlaceOrderEPL + futuresCancelAnOrderEPL + futuresLimitOrderMassCancelationEPL + futuresRetrieveOrderListEPL + futuresRetrieveFillsEPL + futuresRecentFillsEPL + futuresRetrievePositionListEPL + futuresRetrieveFundingHistoryEPL + futuresRetrieveFullOrderbookLevel2EPL + defaultFuturesEPL +) + +// Limit executes rate limiting functionality for Kucoin +func (r *RateLimit) Limit(ctx context.Context, epl request.EndpointLimit) error { + var limiter *rate.Limiter + var tokens int + switch epl { + case retrieveAccountLedgerEPL: + return r.RetrieveAccountLedger.Wait(ctx) + case masterSubUserTransferEPL: + return r.MasterSubUserTransfer.Wait(ctx) + case retrieveDepositListEPL: + return r.RetrieveDepositList.Wait(ctx) + case retrieveV1HistoricalDepositListEPL: + return r.RetrieveV1HistoricalDepositList.Wait(ctx) + case retrieveWithdrawalListEPL: + return r.RetrieveWithdrawalList.Wait(ctx) + case retrieveV1HistoricalWithdrawalListEPL: + return r.RetrieveV1HistoricalWithdrawalList.Wait(ctx) + case placeOrderEPL: + return r.PlaceOrder.Wait(ctx) + case placeMarginOrdersEPL: + return r.PlaceMarginOrders.Wait(ctx) + case placeBulkOrdersEPL: + return r.PlaceBulkOrders.Wait(ctx) + case cancelOrderEPL: + return r.CancelOrder.Wait(ctx) + case cancelAllOrdersEPL: + return r.CancelAllOrders.Wait(ctx) + case listOrdersEPL: + return r.ListOrders.Wait(ctx) + case listFillsEPL: + return r.ListFills.Wait(ctx) + case retrieveFullOrderbookEPL: + return r.RetrieveFullOrderbook.Wait(ctx) + case retrieveMarginAccountEPL: + return r.RetrieveMarginAccount.Wait(ctx) + case futuresRetrieveAccountOverviewEPL: + return r.FRetrieveAccountOverviewRate.Wait(ctx) + case futuresRetrieveTransactionHistoryEPL: + return r.FRetrieveTransactionHistoryRate.Wait(ctx) + case futuresPlaceOrderEPL: + return r.FPlaceOrderRate.Wait(ctx) + case futuresCancelAnOrderEPL: + return r.FCancelAnOrderRate.Wait(ctx) + case futuresLimitOrderMassCancelationEPL: + return r.FLimitOrderMassCancelationRate.Wait(ctx) + case futuresRetrieveOrderListEPL: + return r.FRetrieveOrderListRate.Wait(ctx) + case futuresRetrieveFillsEPL: + return r.FRetrieveFillsRate.Wait(ctx) + case futuresRecentFillsEPL: + return r.FRecentFillsRate.Wait(ctx) + case futuresRetrievePositionListEPL: + return r.FRetrievePositionListRate.Wait(ctx) + case futuresRetrieveFundingHistoryEPL: + return r.FRetrieveFundingHistoryRate.Wait(ctx) + case futuresRetrieveFullOrderbookLevel2EPL: + return r.FRetrieveFullOrderbookLevel2Rate.Wait(ctx) + case defaultSpotEPL: + limiter, tokens = r.SpotRate, 1 + case defaultFuturesEPL: + limiter, tokens = r.FuturesRate, 1 + default: + return errors.New("endpoint rate limit functionality not found") + } + var finalDelay time.Duration + var reserves = make([]*rate.Reservation, tokens) + for i := 0; i < tokens; i++ { + // Consume tokens 1 at a time as this avoids needing burst capacity in the limiter, + // which would otherwise allow the rate limit to be exceeded over short periods + reserves[i] = limiter.Reserve() + finalDelay = reserves[i].Delay() + } + + if dl, ok := ctx.Deadline(); ok && dl.Before(time.Now().Add(finalDelay)) { + // Cancel all potential reservations to free up rate limiter if deadline + // is exceeded. + for x := range reserves { + reserves[x].Cancel() + } + return fmt.Errorf("rate limit delay of %s will exceed deadline: %w", + finalDelay, + context.DeadlineExceeded) + } + + time.Sleep(finalDelay) + return nil +} + +// SetRateLimit returns a RateLimit instance, which implements the request.Limiter interface. +func SetRateLimit() *RateLimit { + return &RateLimit{ + // spot specific rate limiters + RetrieveAccountLedger: request.NewRateLimit(threeSecondsInterval, retrieveAccountLedgerRate), + MasterSubUserTransfer: request.NewRateLimit(threeSecondsInterval, masterSubUserTransferRate), + RetrieveDepositList: request.NewRateLimit(threeSecondsInterval, retrieveDepositListRate), + RetrieveV1HistoricalDepositList: request.NewRateLimit(threeSecondsInterval, retrieveV1HistoricalDepositListRate), + RetrieveWithdrawalList: request.NewRateLimit(threeSecondsInterval, retrieveWithdrawalListRate), + RetrieveV1HistoricalWithdrawalList: request.NewRateLimit(threeSecondsInterval, retrieveV1HistoricalWithdrawalListRate), + PlaceOrder: request.NewRateLimit(threeSecondsInterval, placeOrderRate), + PlaceMarginOrders: request.NewRateLimit(threeSecondsInterval, placeMarginOrdersRate), + PlaceBulkOrders: request.NewRateLimit(threeSecondsInterval, placeBulkOrdersRate), + CancelOrder: request.NewRateLimit(threeSecondsInterval, cancelOrderRate), + CancelAllOrders: request.NewRateLimit(threeSecondsInterval, cancelAllOrdersRate), + ListOrders: request.NewRateLimit(threeSecondsInterval, listOrdersRate), + ListFills: request.NewRateLimit(threeSecondsInterval, listFillsRate), + RetrieveFullOrderbook: request.NewRateLimit(threeSecondsInterval, retrieveFullOrderbookRate), + RetrieveMarginAccount: request.NewRateLimit(threeSecondsInterval, retrieveMarginAccountRate), + + // default spot and futures rates + SpotRate: request.NewRateLimit(oneMinuteInterval, defaultSpotRate), + FuturesRate: request.NewRateLimit(oneMinuteInterval, defaultFuturesRate), + + // futures specific rate limiters + FRetrieveAccountOverviewRate: request.NewRateLimit(threeSecondsInterval, futuresRetrieveAccountOverviewRate), + FRetrieveTransactionHistoryRate: request.NewRateLimit(threeSecondsInterval, futuresRetrieveTransactionHistoryRate), + FPlaceOrderRate: request.NewRateLimit(threeSecondsInterval, futuresPlaceOrderRate), + FCancelAnOrderRate: request.NewRateLimit(threeSecondsInterval, futuresCancelAnOrderRate), + FLimitOrderMassCancelationRate: request.NewRateLimit(threeSecondsInterval, futuresLimitOrderMassCancelationRate), + FRetrieveOrderListRate: request.NewRateLimit(threeSecondsInterval, futuresRetrieveOrderListRate), + FRetrieveFillsRate: request.NewRateLimit(threeSecondsInterval, futuresRetrieveFillsRate), + FRecentFillsRate: request.NewRateLimit(threeSecondsInterval, futuresRecentFillsRate), + FRetrievePositionListRate: request.NewRateLimit(threeSecondsInterval, futuresRetrievePositionListRate), + FRetrieveFundingHistoryRate: request.NewRateLimit(threeSecondsInterval, futuresRetrieveFundingHistoryRate), + FRetrieveFullOrderbookLevel2Rate: request.NewRateLimit(threeSecondsInterval, futuresRetrieveFullOrderbookLevel2Rate), + } +} diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go new file mode 100644 index 00000000..4e52f364 --- /dev/null +++ b/exchanges/kucoin/kucoin_test.go @@ -0,0 +1,2421 @@ +package kucoin + +import ( + "context" + "encoding/json" + "errors" + "log" + "os" + "testing" + "time" + + "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/margin" + "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/buffer" + "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" +) + +// Please supply your own keys here to do authenticated endpoint testing +const ( + apiKey = "" + apiSecret = "" + passPhrase = "" + canManipulateRealOrders = false + + assetNotEnabled = "asset %v not enabled" + spotAndMarginAssetNotEnabled = "neither spot nor margin asset is enabled" +) + +var ( + ku = &Kucoin{} + spotTradablePair, marginTradablePair, futuresTradablePair currency.Pair +) + +func TestMain(m *testing.M) { + ku.SetDefaults() + cfg := config.GetConfig() + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal(err) + } + + exchCfg, err := cfg.GetExchangeConfig("Kucoin") + if err != nil { + log.Fatal(err) + } + + exchCfg.API.AuthenticatedSupport = true + exchCfg.API.AuthenticatedWebsocketSupport = true + + exchCfg.API.Credentials.Key = apiKey + exchCfg.API.Credentials.Secret = apiSecret + exchCfg.API.Credentials.ClientID = passPhrase + if apiKey != "" && apiSecret != "" && passPhrase != "" { + ku.Websocket.SetCanUseAuthenticatedEndpoints(true) + } + + ku.SetDefaults() + ku.Websocket = sharedtestvalues.NewTestWebsocket() + ku.Websocket.Orderbook = buffer.Orderbook{} + err = ku.Setup(exchCfg) + if err != nil { + log.Fatal(err) + } + request.MaxRequestJobs = 100 + ku.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + ku.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() + setupWS() + getFirstTradablePairOfAssets() + os.Exit(m.Run()) +} + +// Spot asset test cases starts from here +func TestGetSymbols(t *testing.T) { + t.Parallel() + _, err := ku.GetSymbols(context.Background(), "") + if err != nil { + t.Error("GetSymbols() error", err) + } + _, err = ku.GetSymbols(context.Background(), currency.BTC.String()) + if err != nil { + t.Error("GetSymbols() error", err) + } +} + +func TestGetTicker(t *testing.T) { + t.Parallel() + _, err := ku.GetTicker(context.Background(), "BTC-USDT") + if err != nil { + t.Error("GetTicker() error", err) + } +} + +func TestGetAllTickers(t *testing.T) { + t.Parallel() + _, err := ku.GetTickers(context.Background()) + if err != nil { + t.Error("GetAllTickers() error", err) + } +} + +func TestGet24hrStats(t *testing.T) { + t.Parallel() + _, err := ku.Get24hrStats(context.Background(), "BTC-USDT") + if err != nil { + t.Error("Get24hrStats() error", err) + } +} + +func TestGetMarketList(t *testing.T) { + t.Parallel() + _, err := ku.GetMarketList(context.Background()) + if err != nil { + t.Error("GetMarketList() error", err) + } +} + +func TestGetPartOrderbook20(t *testing.T) { + t.Parallel() + _, err := ku.GetPartOrderbook20(context.Background(), "BTC-USDT") + if err != nil { + t.Error("GetPartOrderbook20() error", err) + } +} + +func TestGetPartOrderbook100(t *testing.T) { + t.Parallel() + _, err := ku.GetPartOrderbook100(context.Background(), "BTC-USDT") + if err != nil { + t.Error("GetPartOrderbook100() error", err) + } +} + +func TestGetOrderbook(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetOrderbook(context.Background(), "BTC-USDT") + if err != nil { + t.Error("GetOrderbook() error", err) + } +} + +func TestGetTradeHistory(t *testing.T) { + t.Parallel() + _, err := ku.GetTradeHistory(context.Background(), "BTC-USDT") + if err != nil { + t.Error("GetTradeHistory() error", err) + } +} + +func TestGetKlines(t *testing.T) { + t.Parallel() + _, err := ku.GetKlines(context.Background(), "BTC-USDT", "1week", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetKlines() error", err) + } + _, err = ku.GetKlines(context.Background(), "BTC-USDT", "5min", time.Now().Add(-time.Hour*1), time.Now()) + if err != nil { + t.Error("GetKlines() error", err) + } +} + +func TestGetCurrencies(t *testing.T) { + t.Parallel() + _, err := ku.GetCurrencies(context.Background()) + if err != nil { + t.Error("GetCurrencies() error", err) + } +} + +func TestGetCurrency(t *testing.T) { + t.Parallel() + _, err := ku.GetCurrencyDetail(context.Background(), "BTC", "") + if err != nil { + t.Error("GetCurrency() error", err) + } + + _, err = ku.GetCurrencyDetail(context.Background(), "BTC", "ETH") + if err != nil { + t.Error("GetCurrency() error", err) + } +} + +func TestGetFiatPrice(t *testing.T) { + t.Parallel() + _, err := ku.GetFiatPrice(context.Background(), "", "") + if err != nil { + t.Error("GetFiatPrice() error", err) + } + + _, err = ku.GetFiatPrice(context.Background(), "EUR", "ETH,BTC") + if err != nil { + t.Error("GetFiatPrice() error", err) + } +} + +func TestGetMarkPrice(t *testing.T) { + t.Parallel() + _, err := ku.GetMarkPrice(context.Background(), "USDT-BTC") + if err != nil { + t.Error("GetMarkPrice() error", err) + } +} + +func TestGetMarginConfiguration(t *testing.T) { + t.Parallel() + _, err := ku.GetMarginConfiguration(context.Background()) + if err != nil { + t.Error("GetMarginConfiguration() error", err) + } +} + +func TestGetMarginAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetMarginAccount(context.Background()) + if err != nil { + t.Error("GetMarginAccount() error", err) + } +} + +func TestGetMarginRiskLimit(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetMarginRiskLimit(context.Background(), "cross") + if err != nil { + t.Error("GetMarginRiskLimit() error", err) + } + + _, err = ku.GetMarginRiskLimit(context.Background(), "isolated") + if err != nil { + t.Error("GetMarginRiskLimit() error", err) + } +} + +func TestPostBorrowOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.PostBorrowOrder(context.Background(), "USDT", "FOK", "", 10, 0) + if err != nil { + t.Error("PostBorrowOrder() error", err) + } + + _, err = ku.PostBorrowOrder(context.Background(), "USDT", "IOC", "7,14,28", 10, 0.05) + if err != nil { + t.Error("PostBorrowOrder() error", err) + } +} + +const borrowOrderJSON = `{"orderId": "a2111213","currency": "USDT","size": "1.009","filled": 1.009,"matchList": [{"currency": "USDT","dailyIntRate": "0.001","size": "12.9","term": 7,"timestamp": "1544657947759","tradeId": "1212331"}],"status": "DONE"}` + +func TestGetBorrowOrder(t *testing.T) { + t.Parallel() + var resp *BorrowOrder + err := json.Unmarshal([]byte(borrowOrderJSON), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err = ku.GetBorrowOrder(context.Background(), "orderID") + if err != nil { + t.Error("GetBorrowOrder() error", err) + } +} + +const outstandingRecordResponseJSON = `{"currentPage": 0, "pageSize": 0, "totalNum": 0, "totalPage": 0, "items": [ { "tradeId": "1231141", "currency": "USDT", "accruedInterest": "0.22121", "dailyIntRate": "0.0021", "liability": "1.32121", "maturityTime": "1544657947759", "principal": "1.22121", "repaidSize": "0", "term": 7, "createdAt": "1544657947759" } ] }` + +func TestGetOutstandingRecord(t *testing.T) { + t.Parallel() + var resp *OutstandingRecordResponse + err := json.Unmarshal([]byte(outstandingRecordResponseJSON), &resp) + if err != nil { + t.Error(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err = ku.GetOutstandingRecord(context.Background(), "BTC") + if err != nil { + t.Error("GetOutstandingRecord() error", err) + } +} + +const repaidRecordJSON = `{"pageSize": 0, "totalNum": 0, "totalPage": 0, "currentPage": 0, "items": [ { "tradeId": "1231141", "currency": "USDT", "dailyIntRate": "0.0021", "interest": "0.22121", "principal": "1.22121", "repaidSize": "0", "repayTime": "1544657947759", "term": 7 } ] }` + +func TestGetRepaidRecord(t *testing.T) { + t.Parallel() + var resp *RepaidRecordsResponse + err := json.Unmarshal([]byte(repaidRecordJSON), &resp) + if err != nil { + t.Error(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err = ku.GetRepaidRecord(context.Background(), "BTC") + if err != nil { + t.Error("GetRepaidRecord() error", err) + } +} + +func TestOneClickRepayment(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + err := ku.OneClickRepayment(context.Background(), "BTC", "RECENTLY_EXPIRE_FIRST", 2.5) + if err != nil { + t.Error("OneClickRepayment() error", err) + } +} + +func TestSingleOrderRepayment(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + err := ku.SingleOrderRepayment(context.Background(), "BTC", "fa3e34c980062c10dad74016", 2.5) + if err != nil { + t.Error("SingleOrderRepayment() error", err) + } +} + +func TestPostLendOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.PostLendOrder(context.Background(), "BTC", 0.0001, 5, 7) + if err != nil { + t.Error("PostLendOrder() error", err) + } +} + +func TestCancelLendOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + err := ku.CancelLendOrder(context.Background(), "OrderID") + if err != nil { + t.Error("CancelLendOrder() error", err) + } +} + +func TestSetAutoLend(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + err := ku.SetAutoLend(context.Background(), "BTC", 0.0002, 0.005, 7, true) + if err != nil { + t.Error("SetAutoLend() error", err) + } +} + +const activeOrderResponseJSON = `[ { "orderId": "5da59f5ef943c033b2b643e4", "currency": "BTC", "size": "0.51", "filledSize": "0", "dailyIntRate": "0.0001", "term": 7, "createdAt": 1571135326913 } ]` + +func TestGetActiveOrder(t *testing.T) { + t.Parallel() + var resp []LendOrder + err := json.Unmarshal([]byte(activeOrderResponseJSON), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err = ku.GetActiveOrder(context.Background(), "") + if err != nil { + t.Error("GetActiveOrder() error", err) + } + + _, err = ku.GetActiveOrder(context.Background(), "BTC") + if err != nil { + t.Error("GetActiveOrder() error", err) + } +} + +func TestGetLendHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetLendHistory(context.Background(), "") + if err != nil { + t.Error("GetLendHistory() error", err) + } + _, err = ku.GetLendHistory(context.Background(), "BTC") + if err != nil { + t.Error("GetLendHistory() error", err) + } +} + +const activeLentOrderResponseJSON = `[ { "tradeId": "5da6dba0f943c0c81f5d5db5", "currency": "BTC", "size": "0.51", "accruedInterest": "0", "repaid": "0.10999968", "dailyIntRate": "0.0001", "term": 14, "maturityTime": 1572425888958 } ]` + +func TestGetUnsettleLendOrder(t *testing.T) { + t.Parallel() + var resp []UnsettleLendOrder + err := json.Unmarshal([]byte(activeLentOrderResponseJSON), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err = ku.GetUnsettledLendOrder(context.Background(), "") + if err != nil { + t.Error("GetUnsettledLendOrder() error", err) + } + + _, err = ku.GetUnsettledLendOrder(context.Background(), "BTC") + if err != nil { + t.Error("GetUnsettledLendOrder() error", err) + } +} + +const settledLendOrderResponseJSON = `[{ "tradeId": "5da59fe6f943c033b2b6440b", "currency": "BTC", "size": "0.51", "interest": "0.00004899", "repaid": "0.510041641", "dailyIntRate": "0.0001", "term": 7, "settledAt": 1571216254767, "note": "The account of the borrowers reached a negative balance, and the system has supplemented the loss via the insurance fund. Deposit funds: 0.51." } ]` + +func TestGetSettleLendOrder(t *testing.T) { + t.Parallel() + var resp []SettleLendOrder + err := json.Unmarshal([]byte(settledLendOrderResponseJSON), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err = ku.GetSettledLendOrder(context.Background(), "") + if err != nil { + t.Error("GetSettledLendOrder() error", err) + } + _, err = ku.GetSettledLendOrder(context.Background(), "BTC") + if err != nil { + t.Error("GetSettledLendOrder() error", err) + } +} + +func TestGetAccountLendRecord(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetAccountLendRecord(context.Background(), "") + if err != nil { + t.Error("GetAccountLendRecord() error", err) + } + _, err = ku.GetAccountLendRecord(context.Background(), "BTC") + if err != nil { + t.Error("GetAccountLendRecord() error", err) + } +} + +func TestGetLendingMarketData(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetLendingMarketData(context.Background(), "BTC", 0) + if err != nil { + t.Error("GetLendingMarketData() error", err) + } + _, err = ku.GetLendingMarketData(context.Background(), "BTC", 7) + if err != nil { + t.Error("GetLendingMarketData() error", err) + } +} + +func TestGetMarginTradeData(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetMarginTradeData(context.Background(), "BTC") + if err != nil { + t.Error("GetMarginTradeData() error", err) + } +} + +func TestGetIsolatedMarginPairConfig(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetIsolatedMarginPairConfig(context.Background()) + if err != nil { + t.Error("GetIsolatedMarginPairConfig() error", err) + } +} + +func TestGetIsolatedMarginAccountInfo(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetIsolatedMarginAccountInfo(context.Background(), "") + if err != nil { + t.Error("GetIsolatedMarginAccountInfo() error", err) + } + _, err = ku.GetIsolatedMarginAccountInfo(context.Background(), "USDT") + if err != nil { + t.Error("GetIsolatedMarginAccountInfo() error", err) + } +} + +func TestGetSingleIsolatedMarginAccountInfo(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetSingleIsolatedMarginAccountInfo(context.Background(), "BTC-USDT") + if err != nil { + t.Error("GetSingleIsolatedMarginAccountInfo() error", err) + } +} + +func TestInitiateIsolateMarginBorrowing(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.InitiateIsolatedMarginBorrowing(context.Background(), "BTC-USDT", "USDT", "FOK", "", 10, 0) + if err != nil { + t.Error("InitiateIsolateMarginBorrowing() error", err) + } +} + +func TestGetIsolatedOutstandingRepaymentRecords(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetIsolatedOutstandingRepaymentRecords(context.Background(), "", "", 0, 0) + if err != nil { + t.Error("GetIsolatedOutstandingRepaymentRecords() error", err) + } + _, err = ku.GetIsolatedOutstandingRepaymentRecords(context.Background(), "BTC-USDT", "USDT", 0, 0) + if err != nil { + t.Error("GetIsolatedOutstandingRepaymentRecords() error", err) + } +} + +func TestGetIsolatedMarginRepaymentRecords(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetIsolatedMarginRepaymentRecords(context.Background(), "", "", 0, 0) + if err != nil { + t.Error("GetIsolatedMarginRepaymentRecords() error", err) + } + _, err = ku.GetIsolatedMarginRepaymentRecords(context.Background(), "BTC-USDT", "USDT", 0, 0) + if err != nil { + t.Error("GetIsolatedMarginRepaymentRecords() error", err) + } +} + +func TestInitiateIsolatedMarginQuickRepayment(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + err := ku.InitiateIsolatedMarginQuickRepayment(context.Background(), "BTC-USDT", "USDT", "RECENTLY_EXPIRE_FIRST", 10) + if err != nil { + t.Error("InitiateIsolatedMarginQuickRepayment() error", err) + } +} + +func TestInitiateIsolatedMarginSingleRepayment(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + err := ku.InitiateIsolatedMarginSingleRepayment(context.Background(), "BTC-USDT", "USDT", "628c570f7818320001d52b69", 10) + if err != nil { + t.Error("InitiateIsolatedMarginSingleRepayment() error", err) + } +} + +func TestGetCurrentServerTime(t *testing.T) { + t.Parallel() + _, err := ku.GetCurrentServerTime(context.Background()) + if err != nil { + t.Error("GetCurrentServerTime() error", err) + } +} + +func TestGetServiceStatus(t *testing.T) { + t.Parallel() + _, err := ku.GetServiceStatus(context.Background()) + if err != nil { + t.Error("GetServiceStatus() error", err) + } +} + +func TestPostOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + + // default order type is limit + _, err := ku.PostOrder(context.Background(), &SpotOrderParam{ + ClientOrderID: ""}) + if !errors.Is(err, errInvalidClientOrderID) { + t.Errorf("PostOrder() expected %v, but found %v", errInvalidClientOrderID, err) + } + _, err = ku.PostOrder(context.Background(), &SpotOrderParam{ + ClientOrderID: "5bd6e9286d99522a52e458de", 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, + 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", + 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", + 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 + _, err = ku.PostOrder(context.Background(), &SpotOrderParam{ + ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", + Symbol: spotTradablePair, + OrderType: "market", Remark: "remark", Size: 0.1}) + if err != nil { + t.Error("PostOrder() error", err) + } +} + +func TestPostMarginOrder(t *testing.T) { + t.Parallel() + // default order type is limit + _, err := ku.PostMarginOrder(context.Background(), &MarginOrderParam{ + ClientOrderID: ""}) + if !errors.Is(err, errInvalidClientOrderID) { + t.Errorf("PostMarginOrder() expected %v, but found %v", errInvalidClientOrderID, err) + } + _, err = ku.PostMarginOrder(context.Background(), &MarginOrderParam{ + ClientOrderID: "5bd6e9286d99522a52e458de", Symbol: marginTradablePair, + OrderType: ""}) + if !errors.Is(err, order.ErrSideIsInvalid) { + t.Errorf("PostMarginOrder() expected %v, but found %v", order.ErrSideIsInvalid, err) + } + _, err = ku.PostMarginOrder(context.Background(), &MarginOrderParam{ + ClientOrderID: "5bd6e9286d99522a52e458de", Symbol: currency.EMPTYPAIR, + Size: 0.1, Side: "buy", Price: 234565}) + if !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Errorf("PostMarginOrder() expected %v, but found %v", currency.ErrCurrencyPairEmpty, err) + } + _, err = ku.PostMarginOrder(context.Background(), &MarginOrderParam{ + ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", + Symbol: marginTradablePair, + OrderType: "limit", Size: 0.1}) + if !errors.Is(err, errInvalidPrice) { + t.Errorf("PostMarginOrder() expected %v, but found %v", errInvalidPrice, err) + } + _, err = ku.PostMarginOrder(context.Background(), &MarginOrderParam{ + ClientOrderID: "5bd6e9286d99522a52e458de", Symbol: marginTradablePair, Side: "buy", + OrderType: "limit", Price: 234565}) + if !errors.Is(err, errInvalidSize) { + t.Errorf("PostMarginOrder() expected %v, but found %v", errInvalidSize, err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + // default order type is limit and margin mode is cross + _, err = ku.PostMarginOrder(context.Background(), + &MarginOrderParam{ + ClientOrderID: "5bd6e9286d99522a52e458de", + Side: "buy", Symbol: marginTradablePair, + Price: 1000, Size: 0.1, PostOnly: true}) + if err != nil { + t.Error("PostMarginOrder() error", err) + } + + // market isolated order + _, err = ku.PostMarginOrder(context.Background(), + &MarginOrderParam{ + ClientOrderID: "5bd6e9286d99522a52e458de", + Side: "buy", Symbol: marginTradablePair, + OrderType: "market", Funds: 1234, + Remark: "remark", MarginMode: "cross", Price: 1000, PostOnly: true, AutoBorrow: true}) + if err != nil { + t.Error("PostMarginOrder() error", err) + } +} + +func TestPostBulkOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + + req := []OrderRequest{ + { + ClientOID: "3d07008668054da6b3cb12e432c2b13a", + Side: "buy", + Type: "limit", + Price: 1000, + Size: 0.01, + }, + { + ClientOID: "37245dbe6e134b5c97732bfb36cd4a9d", + Side: "buy", + Type: "limit", + Price: 1000, + Size: 0.01, + }, + } + + _, err := ku.PostBulkOrder(context.Background(), "BTC-USDT", req) + if err != nil { + t.Error("PostBulkOrder() error", err) + } +} + +func TestCancelSingleOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + + _, err := ku.CancelSingleOrder(context.Background(), "5bd6e9286d99522a52e458de") + if err != nil { + t.Error("CancelSingleOrder() error", err) + } +} + +func TestCancelOrderByClientOID(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + + _, err := ku.CancelOrderByClientOID(context.Background(), "5bd6e9286d99522a52e458de") + if err != nil { + t.Error("CancelOrderByClientOID() error", err) + } +} + +func TestCancelAllOpenOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + + _, err := ku.CancelAllOpenOrders(context.Background(), "", "") + if err != nil { + t.Error("CancelAllOpenOrders() error", err) + } +} + +const ordersListResponseJSON = `{"currentPage": 1, "pageSize": 1, "totalNum": 153408, "totalPage": 153408, "items": [ { "id": "5c35c02703aa673ceec2a168", "symbol": "BTC-USDT", "opType": "DEAL", "type": "limit", "side": "buy", "price": "10", "size": "2", "funds": "0", "dealFunds": "0.166", "dealSize": "2", "fee": "0", "feeCurrency": "USDT", "stp": "", "stop": "", "stopTriggered": false, "stopPrice": "0", "timeInForce": "GTC", "postOnly": false, "hidden": false, "iceberg": false, "visibleSize": "0", "cancelAfter": 0, "channel": "IOS", "clientOid": "", "remark": "", "tags": "", "isActive": false, "cancelExist": false, "createdAt": 1547026471000, "tradeType": "TRADE" } ] }` + +func TestGetOrders(t *testing.T) { + t.Parallel() + var resp *OrdersListResponse + err := json.Unmarshal([]byte(ordersListResponseJSON), &resp) + if err != nil { + t.Error(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + + _, err = ku.ListOrders(context.Background(), "", "", "", "", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetOrders() error", err) + } +} + +func TestGetRecentOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetRecentOrders(context.Background()) + if err != nil { + t.Error("GetRecentOrders() error", err) + } +} + +func TestGetOrderByID(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetOrderByID(context.Background(), "5c35c02703aa673ceec2a168") + if err != nil { + t.Error("GetOrderByID() error", err) + } +} + +func TestGetOrderByClientOID(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetOrderByClientSuppliedOrderID(context.Background(), "6d539dc614db312") + if err != nil { + t.Error("GetOrderByClientOID() error", err) + } +} + +func TestGetFills(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFills(context.Background(), "", "", "", "", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetFills() error", err) + } + _, err = ku.GetFills(context.Background(), "5c35c02703aa673ceec2a168", "BTC-USDT", "buy", "limit", "TRADE", time.Now().Add(-time.Hour*12), time.Now()) + if err != nil { + t.Error("GetFills() error", err) + } +} + +const limitFillsResponseJSON = `[{ "counterOrderId":"5db7ee769797cf0008e3beea", "createdAt":1572335233000, "fee":"0.946357371456", "feeCurrency":"USDT", "feeRate":"0.001", "forceTaker":true, "funds":"946.357371456", "liquidity":"taker", "orderId":"5db7ee805d53620008dce1ba", "price":"9466.8", "side":"buy", "size":"0.09996592", "stop":"", "symbol":"BTC-USDT", "tradeId":"5db7ee8054c05c0008069e21", "tradeType":"MARGIN_TRADE", "type":"market" }, { "counterOrderId":"5db7ee4b5d53620008dcde8e", "createdAt":1572335207000, "fee":"0.94625", "feeCurrency":"USDT", "feeRate":"0.001", "forceTaker":true, "funds":"946.25", "liquidity":"taker", "orderId":"5db7ee675d53620008dce01e", "price":"9462.5", "side":"sell", "size":"0.1", "stop":"", "symbol":"BTC-USDT", "tradeId":"5db7ee6754c05c0008069e03", "tradeType":"MARGIN_TRADE", "type":"market" }]` + +func TestGetRecentFills(t *testing.T) { + t.Parallel() + var resp []Fill + err := json.Unmarshal([]byte(limitFillsResponseJSON), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err = ku.GetRecentFills(context.Background()) + if err != nil { + t.Error("GetRecentFills() error", err) + } +} + +func TestPostStopOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.PostStopOrder(context.Background(), "5bd6e9286d99522a52e458de", "buy", "BTC-USDT", "", "", "entry", "CO", "TRADE", "", 0.1, 1, 10, 0, 0, 0, true, false, false) + if err != nil { + t.Error("PostStopOrder() error", err) + } +} + +func TestCancelStopOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.CancelStopOrder(context.Background(), "5bd6e9286d99522a52e458de") + if err != nil { + t.Error("CancelStopOrder() error", err) + } +} + +func TestCancelAllStopOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.CancelStopOrders(context.Background(), "", "", "") + if err != nil { + t.Error("CancelAllStopOrder() error", err) + } +} + +const stopOrderResponseJSON = `{"id": "vs8hoo8q2ceshiue003b67c0", "symbol": "KCS-USDT", "userId": "60fe4956c43cbc0006562c2c", "status": "NEW", "type": "limit", "side": "buy", "price": "0.01000000000000000000", "size": "0.01000000000000000000", "funds": null, "stp": null, "timeInForce": "GTC", "cancelAfter": -1, "postOnly": false, "hidden": false, "iceberg": false, "visibleSize": null, "channel": "API", "clientOid": "40e0eb9efe6311eb8e58acde48001122", "remark": null, "tags": null, "orderTime": 1629098781127530345, "domainId": "kucoin", "tradeSource": "USER", "tradeType": "TRADE", "feeCurrency": "USDT", "takerFeeRate": "0.00200000000000000000", "makerFeeRate": "0.00200000000000000000", "createdAt": 1629098781128, "stop": "loss", "stopTriggerTime": null, "stopPrice": "10.00000000000000000000" }` + +func TestGetStopOrder(t *testing.T) { + t.Parallel() + var resp *StopOrder + err := json.Unmarshal([]byte(stopOrderResponseJSON), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err = ku.GetStopOrder(context.Background(), "5bd6e9286d99522a52e458de") + if err != nil { + t.Error("GetStopOrder() error", err) + } +} + +func TestGetAllStopOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.ListStopOrders(context.Background(), "", "", "", "", "", time.Time{}, time.Time{}, 0, 0) + if err != nil { + t.Error("GetAllStopOrder() error", err) + } +} + +func TestGetStopOrderByClientID(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetStopOrderByClientID(context.Background(), "", "5bd6e9286d99522a52e458de") + if err != nil { + t.Error("GetStopOrderByClientID() error", err) + } +} + +func TestCancelStopOrderByClientID(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.CancelStopOrderByClientID(context.Background(), "", "5bd6e9286d99522a52e458de") + if err != nil { + t.Error("CancelStopOrderByClientID() error", err) + } +} + +func TestGetAllAccounts(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + + _, err := ku.GetAllAccounts(context.Background(), "", "") + if err != nil { + t.Error("GetAllAccounts() error", err) + } +} + +func TestGetAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + + _, err := ku.GetAccount(context.Background(), "62fcd1969474ea0001fd20e4") + if err != nil { + t.Error("GetAccount() error", err) + } +} + +const accountLedgerResponseJSON = `{"currentPage": 1, "pageSize": 50, "totalNum": 2, "totalPage": 1, "items": [ { "id": "611a1e7c6a053300067a88d9", "currency": "USDT", "amount": "10.00059547", "fee": "0", "balance": "0", "accountType": "MAIN", "bizType": "Loans Repaid", "direction": "in", "createdAt": 1629101692950, "context": "{\"borrowerUserId\":\"601ad03e50dc810006d242ea\",\"loanRepayDetailNo\":\"611a1e7cc913d000066cf7ec\"}" }, { "id": "611a18bc6a0533000671e1bf", "currency": "USDT", "amount": "10.00059547", "fee": "0", "balance": "0", "accountType": "MAIN", "bizType": "Loans Repaid", "direction": "in", "createdAt": 1629100220843, "context": "{\"borrowerUserId\":\"5e3f4623dbf52d000800292f\",\"loanRepayDetailNo\":\"611a18bc7255c200063ea545\"}" } ] }` + +func TestGetAccountLedgers(t *testing.T) { + t.Parallel() + var resp *AccountLedgerResponse + err := json.Unmarshal([]byte(accountLedgerResponseJSON), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err = ku.GetAccountLedgers(context.Background(), "", "", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetAccountLedgers() error", err) + } +} + +func TestGetAccountSummaryInformation(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + if _, err := ku.GetAccountSummaryInformation(context.Background()); err != nil { + t.Error(err) + } +} + +func TestGetSubAccountBalance(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetSubAccountBalance(context.Background(), "62fcd1969474ea0001fd20e4", false) + if err != nil { + t.Error("GetSubAccountBalance() error", err) + } +} + +func TestGetAggregatedSubAccountBalance(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetAggregatedSubAccountBalance(context.Background()) + if err != nil { + t.Error("GetAggregatedSubAccountBalance() error", err) + } +} + +func TestGetPaginatedSubAccountInformation(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetPaginatedSubAccountInformation(context.Background(), 0, 10) + if err != nil { + t.Error("GetPaginatedSubAccountInformation() error", err) + } +} + +func TestGetTransferableBalance(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + + _, err := ku.GetTransferableBalance(context.Background(), "BTC", "MAIN", "") + if err != nil { + t.Error("GetTransferableBalance() error", err) + } +} + +func TestTransferMainToSubAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.TransferMainToSubAccount(context.Background(), "62fcd1969474ea0001fd20e4", "BTC", "1", "OUT", "", "", "5caefba7d9575a0688f83c45") + if err != nil { + t.Error("TransferMainToSubAccount() error", err) + } +} + +func TestMakeInnerTransfer(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.MakeInnerTransfer(context.Background(), "62fcd1969474ea0001fd20e4", "BTC", "trade", "main", "1", "", "") + if err != nil { + t.Error("MakeInnerTransfer() error", err) + } +} + +func TestCreateDepositAddress(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.CreateDepositAddress(context.Background(), "BTC", "") + if err != nil { + t.Error("CreateDepositAddress() error", err) + } + + _, err = ku.CreateDepositAddress(context.Background(), "USDT", "TRC20") + if err != nil { + t.Error("CreateDepositAddress() error", err) + } +} + +func TestGetDepositAddressV2(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetDepositAddressesV2(context.Background(), "BTC") + if err != nil { + t.Error("GetDepositAddressV2() error", err) + } +} + +func TestGetDepositAddressesV1(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetDepositAddressV1(context.Background(), "BTC", "") + if err != nil { + t.Error("GetDepositAddressV1() error", err) + } +} + +const depositResponseJSON = `{"currentPage": 1, "pageSize": 50, "totalNum": 1, "totalPage": 1, "items": [ { "currency": "XRP", "chain": "xrp", "status": "SUCCESS", "address": "rNFugeoj3ZN8Wv6xhuLegUBBPXKCyWLRkB", "memo": "1919537769", "isInner": false, "amount": "20.50000000", "fee": "0.00000000", "walletTxId": "2C24A6D5B3E7D5B6AA6534025B9B107AC910309A98825BF5581E25BEC94AD83B@e8902757998fc352e6c9d8890d18a71c", "createdAt": 1666600519000, "updatedAt": 1666600549000, "remark": "Deposit" } ] }` + +func TestGetDepositList(t *testing.T) { + t.Parallel() + var resp DepositResponse + err := json.Unmarshal([]byte(depositResponseJSON), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err = ku.GetDepositList(context.Background(), "", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetDepositList() error", err) + } +} + +const historicalDepositResponseJSON = `{"currentPage":1, "pageSize":1, "totalNum":9, "totalPage":9, "items":[ { "currency":"BTC", "createAt":1528536998, "amount":"0.03266638", "walletTxId":"55c643bc2c68d6f17266383ac1be9e454038864b929ae7cee0bc408cc5c869e8@12ffGWmMMD1zA1WbFm7Ho3JZ1w6NYXjpFk@234", "isInner":false, "status":"SUCCESS" } ] }` + +func TestGetHistoricalDepositList(t *testing.T) { + t.Parallel() + var resp *HistoricalDepositWithdrawalResponse + err := json.Unmarshal([]byte(historicalDepositResponseJSON), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err = ku.GetHistoricalDepositList(context.Background(), "", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetHistoricalDepositList() error", err) + } +} + +func TestGetWithdrawalList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + + _, err := ku.GetWithdrawalList(context.Background(), "", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetWithdrawalList() error", err) + } +} + +func TestGetHistoricalWithdrawalList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + + _, err := ku.GetHistoricalWithdrawalList(context.Background(), "", "", time.Time{}, time.Time{}, 0, 0) + if err != nil { + t.Error("GetHistoricalWithdrawalList() error", err) + } +} + +func TestGetWithdrawalQuotas(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + + _, err := ku.GetWithdrawalQuotas(context.Background(), "BTC", "") + if err != nil { + t.Error("GetWithdrawalQuotas() error", err) + } +} + +func TestApplyWithdrawal(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.ApplyWithdrawal(context.Background(), "ETH", "0x597873884BC3a6C10cB6Eb7C69172028Fa85B25A", "", "", "", "", false, 1) + if err != nil { + t.Error("ApplyWithdrawal() error", err) + } +} + +func TestCancelWithdrawal(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + err := ku.CancelWithdrawal(context.Background(), "5bffb63303aa675e8bbe18f9") + if err != nil { + t.Error("CancelWithdrawal() error", err) + } +} + +func TestGetBasicFee(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetBasicFee(context.Background(), "1") + if err != nil { + t.Error("GetBasicFee() error", err) + } +} + +func TestGetTradingFee(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + + _, err := ku.GetTradingFee(context.Background(), "BTC-USDT") + if err != nil { + t.Error("GetTradingFee() error", err) + } +} + +// futures +func TestGetFuturesOpenContracts(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesOpenContracts(context.Background()) + if err != nil { + t.Error("GetFuturesOpenContracts() error", err) + } +} + +func TestGetFuturesContract(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesContract(context.Background(), "XBTUSDTM") + if err != nil { + t.Error("GetFuturesContract() error", err) + } +} + +func TestGetFuturesRealTimeTicker(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesRealTimeTicker(context.Background(), "XBTUSDTM") + if err != nil { + t.Error("GetFuturesRealTimeTicker() error", err) + } +} + +func TestGetFuturesOrderbook(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesOrderbook(context.Background(), futuresTradablePair.String()) + if err != nil { + t.Error("GetFuturesOrderbook() error", err) + } +} + +func TestGetFuturesPartOrderbook20(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesPartOrderbook20(context.Background(), "XBTUSDTM") + if err != nil { + t.Error("GetFuturesPartOrderbook20() error", err) + } +} + +func TestGetFuturesPartOrderbook100(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesPartOrderbook100(context.Background(), "XBTUSDTM") + if err != nil { + t.Error("GetFuturesPartOrderbook100() error", err) + } +} + +func TestGetFuturesTradeHistory(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesTradeHistory(context.Background(), "XBTUSDTM") + if err != nil { + t.Error("GetFuturesTradeHistory() error", err) + } +} + +func TestGetFuturesInterestRate(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesInterestRate(context.Background(), "XBTUSDTM", time.Time{}, time.Time{}, false, false, 0, 0) + if err != nil { + t.Error("GetFuturesInterestRate() error", err) + } +} + +func TestGetFuturesIndexList(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesIndexList(context.Background(), futuresTradablePair.String(), time.Time{}, time.Time{}, false, false, 0, 10) + if err != nil { + t.Error(err) + } +} + +func TestGetFuturesCurrentMarkPrice(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesCurrentMarkPrice(context.Background(), "XBTUSDTM") + if err != nil { + t.Error("GetFuturesCurrentMarkPrice() error", err) + } +} + +func TestGetFuturesPremiumIndex(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesPremiumIndex(context.Background(), "XBTUSDTM", time.Time{}, time.Time{}, false, false, 0, 0) + if err != nil { + t.Error("GetFuturesPremiumIndex() error", err) + } +} + +func TestGetFuturesCurrentFundingRate(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesCurrentFundingRate(context.Background(), "XBTUSDTM") + if err != nil { + t.Error("GetFuturesCurrentFundingRate() error", err) + } +} + +func TestGetFuturesServerTime(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesServerTime(context.Background()) + if err != nil { + t.Error("GetFuturesServerTime() error", err) + } +} + +func TestGetFuturesServiceStatus(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesServiceStatus(context.Background()) + if err != nil { + t.Error("GetFuturesServiceStatus() error", err) + } +} + +func TestGetFuturesKline(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesKline(context.Background(), int64(kline.ThirtyMin.Duration().Minutes()), "XBTUSDTM", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetFuturesKline() error", err) + } +} + +func TestPostFuturesOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de"}) + if !errors.Is(err, errInvalidLeverage) { + t.Errorf("PostFuturesOrder expected %v, but found %v", errInvalidLeverage, err) + } + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{Side: "buy", Leverage: 0.02}) + if !errors.Is(err, errInvalidClientOrderID) { + t.Errorf("PostFuturesOrder expected %v, but found %v", errInvalidClientOrderID, err) + } + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Leverage: 0.02}) + if !errors.Is(err, order.ErrSideIsInvalid) { + t.Errorf("PostFuturesOrder expected %v, but found %v", order.ErrSideIsInvalid, err) + } + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Leverage: 0.02}) + if !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Errorf("PostFuturesOrder expected %v, but found %v", currency.ErrCurrencyPairEmpty, err) + } + + // With Stop order configuration + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", + Stop: "up", StopPriceType: "", TimeInForce: "", Size: 1, Price: 1000, StopPrice: 0, Leverage: 0.02, VisibleSize: 0}) + if !errors.Is(err, errInvalidStopPriceType) { + t.Errorf("PostFuturesOrder expected %v, but found %v", errInvalidStopPriceType, err) + } + + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", + Stop: "up", StopPriceType: "TP", TimeInForce: "", Size: 1, Price: 1000, StopPrice: 0, Leverage: 0.02, VisibleSize: 0}) + if !errors.Is(err, errInvalidPrice) { + t.Errorf("PostFuturesOrder expected %v, but found %v", errInvalidPrice, err) + } + + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", + Stop: "up", StopPriceType: "TP", StopPrice: 123456, TimeInForce: "", Size: 1, Price: 1000, Leverage: 0.02, VisibleSize: 0}) + if err != nil { + t.Errorf("PostFuturesOrder expected %v", err) + } + + // Limit Orders + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, + OrderType: "limit", Remark: "10", Leverage: 0.02}) + if !errors.Is(err, errInvalidPrice) { + t.Errorf("PostFuturesOrder expected %v, but found %v", errInvalidPrice, err) + } + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", Price: 1000, Leverage: 0.02, VisibleSize: 0}) + if !errors.Is(err, errInvalidSize) { + t.Errorf("PostFuturesOrder expected %v, but found %v", errInvalidSize, err) + } + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", + Size: 1, Price: 1000, Leverage: 0.02, VisibleSize: 0}) + if err != nil { + t.Error(err) + } + + // Market Orders + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, + OrderType: "market", Remark: "10", Leverage: 0.02}) + if !errors.Is(err, errInvalidSize) { + t.Errorf("PostFuturesOrder expected %v, but found %v", errInvalidSize, err) + } + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "market", Remark: "10", + Size: 1, Leverage: 0.02, VisibleSize: 0}) + if !errors.Is(err, errInvalidSize) { + t.Errorf("PostFuturesOrder expected %v, but found %v", errInvalidSize, err) + } + + _, err = ku.PostFuturesOrder(context.Background(), &FuturesOrderParam{ + ClientOrderID: "5bd6e9286d99522a52e458de", + Side: "buy", + Symbol: futuresTradablePair, + OrderType: "limit", + Remark: "10", + Stop: "", + StopPriceType: "", + TimeInForce: "", + Size: 1, + Price: 1000, + StopPrice: 0, + Leverage: 0.02, + VisibleSize: 0}) + if err != nil { + t.Error("PostFuturesOrder() error", err) + } +} + +func TestCancelFuturesOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + + _, err := ku.CancelFuturesOrder(context.Background(), "5bd6e9286d99522a52e458de") + if err != nil { + t.Error("CancelFuturesOrder() error", err) + } +} + +func TestCancelAllFuturesOpenOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + + _, err := ku.CancelAllFuturesOpenOrders(context.Background(), "XBTUSDM") + if err != nil { + t.Error("CancelAllFuturesOpenOrders() error", err) + } +} + +func TestCancelAllFuturesStopOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.CancelAllFuturesStopOrders(context.Background(), "XBTUSDM") + if err != nil { + t.Error("CancelAllFuturesStopOrders() error", err) + } +} + +func TestGetFuturesOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesOrders(context.Background(), "", "", "", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetFuturesOrders() error", err) + } +} + +func TestGetUntriggeredFuturesStopOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetUntriggeredFuturesStopOrders(context.Background(), "", "", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetUntriggeredFuturesStopOrders() error", err) + } +} + +func TestGetFuturesRecentCompletedOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesRecentCompletedOrders(context.Background()) + if err != nil { + t.Error("GetFuturesRecentCompletedOrders() error", err) + } +} + +func TestGetFuturesOrderDetails(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesOrderDetails(context.Background(), "5cdfc138b21023a909e5ad55") + if err != nil { + t.Error("GetFuturesOrderDetails() error", err) + } +} + +func TestGetFuturesOrderDetailsByClientID(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesOrderDetailsByClientID(context.Background(), "eresc138b21023a909e5ad59") + if err != nil { + t.Error("GetFuturesOrderDetailsByClientID() error", err) + } +} + +func TestGetFuturesFills(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesFills(context.Background(), "", "", "", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetFuturesFills() error", err) + } +} + +func TestGetFuturesRecentFills(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesRecentFills(context.Background()) + if err != nil { + t.Error("GetFuturesRecentFills() error", err) + } +} + +func TestGetFuturesOpenOrderStats(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesOpenOrderStats(context.Background(), "XBTUSDM") + if err != nil { + t.Error("GetFuturesOpenOrderStats() error", err) + } +} + +func TestGetFuturesPosition(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesPosition(context.Background(), "XBTUSDM") + if err != nil { + t.Error("GetFuturesPosition() error", err) + } +} + +func TestGetFuturesPositionList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesPositionList(context.Background()) + if err != nil { + t.Error("GetFuturesPositionList() error", err) + } +} + +func TestSetAutoDepositMargin(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.SetAutoDepositMargin(context.Background(), "ADAUSDTM", true) + if err != nil { + t.Error("SetAutoDepositMargin() error", err) + } +} + +func TestAddMargin(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.AddMargin(context.Background(), "XBTUSDTM", "6200c9b83aecfb000152dasfdee", 1) + if err != nil { + t.Error("AddMargin() error", err) + } +} + +func TestGetFuturesRiskLimitLevel(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + + _, err := ku.GetFuturesRiskLimitLevel(context.Background(), "ADAUSDTM") + if err != nil { + t.Error("GetFuturesRiskLimitLevel() error", err) + } +} + +func TestUpdateRiskLmitLevel(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.FuturesUpdateRiskLmitLevel(context.Background(), "ADASUDTM", 2) + if err != nil { + t.Error("UpdateRiskLmitLevel() error", err) + } +} + +func TestGetFuturesFundingHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesFundingHistory(context.Background(), futuresTradablePair.String(), 0, 0, true, true, time.Time{}, time.Time{}) + if err != nil { + t.Error("GetFuturesFundingHistory() error", err) + } +} + +func TestGetFuturesAccountOverview(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesAccountOverview(context.Background(), "") + if err != nil { + t.Error("GetFuturesAccountOverview() error", err) + } +} + +func TestGetFuturesTransactionHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesTransactionHistory(context.Background(), "", "", 0, 0, true, time.Time{}, time.Time{}) + if err != nil { + t.Error("GetFuturesTransactionHistory() error", err) + } +} + +func TestCreateFuturesSubAccountAPIKey(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err := ku.CreateFuturesSubAccountAPIKey(context.Background(), "", "passphrase", "", "remark", "subAccName") + if err != nil { + t.Error("CreateFuturesSubAccountAPIKey() error", err) + } +} + +func TestGetFuturesDepositAddress(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesDepositAddress(context.Background(), "XBT") + if err != nil { + t.Error("GetFuturesDepositAddress() error", err) + } +} + +func TestGetFuturesDepositsList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesDepositsList(context.Background(), "", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetFuturesDepositsList() error", err) + } +} + +func TestGetFuturesWithdrawalLimit(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesWithdrawalLimit(context.Background(), "XBT") + if err != nil { + t.Error("GetFuturesWithdrawalLimit() error", err) + } +} + +func TestGetFuturesWithdrawalList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesWithdrawalList(context.Background(), "", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetFuturesWithdrawalList() error", err) + } +} + +func TestCancelFuturesWithdrawal(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + + _, err := ku.CancelFuturesWithdrawal(context.Background(), "5cda659603aa67131f305f7e") + if err != nil { + t.Error("CancelFuturesWithdrawal() error", err) + } +} + +func TestTransferFuturesFundsToMainAccount(t *testing.T) { + t.Parallel() + var resp *TransferRes + err := json.Unmarshal([]byte(transferFuturesFundsResponseJSON), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + _, err = ku.TransferFuturesFundsToMainAccount(context.Background(), 1, "USDT", "MAIN") + if err != nil { + t.Error("TransferFuturesFundsToMainAccount() error", err) + } +} + +func TestTransferFundsToFuturesAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + err := ku.TransferFundsToFuturesAccount(context.Background(), 1, "USDT", "MAIN") + if err != nil { + t.Error("TransferFundsToFuturesAccount() error", err) + } +} + +func TestGetFuturesTransferOutList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetFuturesTransferOutList(context.Background(), "USDT", "", time.Time{}, time.Time{}) + if err != nil { + t.Error("GetFuturesTransferOutList() error", err) + } +} + +func TestCancelFuturesTransferOut(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + err := ku.CancelFuturesTransferOut(context.Background(), "5cd53be30c19fc3754b60928") + if err != nil { + t.Error("CancelFuturesTransferOut() error", err) + } +} + +func TestFetchTradablePairs(t *testing.T) { + t.Parallel() + _, err := ku.FetchTradablePairs(context.Background(), asset.Futures) + if err != nil { + t.Error(err) + } + _, err = ku.FetchTradablePairs(context.Background(), asset.Spot) + if err != nil { + t.Error(err) + } + _, err = ku.FetchTradablePairs(context.Background(), asset.Margin) + if err != nil { + t.Error(err) + } +} + +func TestUpdateOrderbook(t *testing.T) { + t.Parallel() + if _, err := ku.UpdateOrderbook(context.Background(), futuresTradablePair, asset.Futures); err != nil { + t.Error(err) + } + if _, err := ku.UpdateOrderbook(context.Background(), marginTradablePair, asset.Margin); err != nil { + t.Error(err) + } + if _, err := ku.UpdateOrderbook(context.Background(), spotTradablePair, asset.Spot); err != nil { + t.Error(err) + } +} +func TestUpdateTickers(t *testing.T) { + t.Parallel() + err := ku.UpdateTickers(context.Background(), asset.Spot) + if err != nil { + t.Fatal(err) + } + err = ku.UpdateTickers(context.Background(), asset.Margin) + if err != nil { + t.Fatal(err) + } + err = ku.UpdateTickers(context.Background(), asset.Futures) + if err != nil { + t.Fatal(err) + } + err = ku.UpdateTickers(context.Background(), asset.Empty) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatal(err) + } +} +func TestUpdateTicker(t *testing.T) { + t.Parallel() + var err error + _, err = ku.UpdateTicker(context.Background(), spotTradablePair, asset.Spot) + if err != nil { + t.Fatal(err) + } + _, err = ku.UpdateTicker(context.Background(), marginTradablePair, asset.Margin) + if err != nil { + t.Fatal(err) + } + _, err = ku.UpdateTicker(context.Background(), futuresTradablePair, asset.Futures) + if err != nil { + t.Fatal(err) + } +} + +func TestFetchTicker(t *testing.T) { + t.Parallel() + _, err := ku.FetchTicker(context.Background(), spotTradablePair, asset.Spot) + if err != nil { + t.Fatal(err) + } + if _, err = ku.FetchTicker(context.Background(), marginTradablePair, asset.Margin); err != nil { + t.Error(err) + } + if _, err = ku.FetchTicker(context.Background(), futuresTradablePair, asset.Futures); err != nil { + t.Error(err) + } +} + +func TestFetchOrderbook(t *testing.T) { + t.Parallel() + if _, err := ku.FetchOrderbook(context.Background(), spotTradablePair, asset.Spot); err != nil { + t.Error(err) + } + if _, err := ku.FetchOrderbook(context.Background(), marginTradablePair, asset.Margin); err != nil { + t.Error(err) + } + if _, err := ku.FetchOrderbook(context.Background(), futuresTradablePair, asset.Futures); err != nil { + t.Error(err) + } +} + +func TestGetHistoricCandles(t *testing.T) { + startTime := time.Now().Add(-time.Hour * 4) + endTime := time.Now().Add(-time.Hour * 3) + var err error + _, err = ku.GetHistoricCandles(context.Background(), futuresTradablePair, asset.Futures, kline.OneHour, startTime, endTime) + if err != nil { + t.Fatal(err) + } + _, err = ku.GetHistoricCandles(context.Background(), spotTradablePair, asset.Spot, kline.OneHour, startTime, time.Now()) + if err != nil { + t.Fatal(err) + } + _, err = ku.GetHistoricCandles(context.Background(), marginTradablePair, asset.Margin, kline.OneHour, startTime, time.Now()) + if err != nil { + t.Fatal(err) + } +} + +func TestGetHistoricCandlesExtended(t *testing.T) { + startTime := time.Now().Add(-time.Hour * 4) + endTime := time.Now().Add(-time.Hour * 1) + var err error + _, err = ku.GetHistoricCandlesExtended(context.Background(), spotTradablePair, asset.Spot, kline.OneHour, startTime, endTime) + if err != nil { + t.Fatal(err) + } + _, err = ku.GetHistoricCandlesExtended(context.Background(), spotTradablePair, asset.Spot, kline.FiveMin, startTime, endTime) + if err != nil { + t.Error(err) + } + _, err = ku.GetHistoricCandlesExtended(context.Background(), marginTradablePair, asset.Margin, kline.OneHour, startTime, endTime) + if err != nil { + t.Fatal(err) + } + _, err = ku.GetHistoricCandlesExtended(context.Background(), futuresTradablePair, asset.Futures, kline.FiveMin, startTime, endTime) + if err != nil { + t.Error(err) + } +} + +func TestGetServerTime(t *testing.T) { + t.Parallel() + _, err := ku.GetServerTime(context.Background(), asset.Spot) + if err != nil { + t.Error(err) + } + _, err = ku.GetServerTime(context.Background(), asset.Futures) + if err != nil { + t.Error(err) + } + _, err = ku.GetServerTime(context.Background(), asset.Margin) + if err != nil { + t.Error(err) + } +} + +func TestGetRecentTrades(t *testing.T) { + t.Parallel() + _, err := ku.GetRecentTrades(context.Background(), futuresTradablePair, asset.Futures) + if err != nil { + t.Error(err) + } + _, err = ku.GetRecentTrades(context.Background(), spotTradablePair, asset.Spot) + if err != nil { + t.Error(err) + } + _, err = ku.GetRecentTrades(context.Background(), marginTradablePair, asset.Margin) + if err != nil { + t.Error(err) + } +} + +func TestGetOrderHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + var enabledPairs currency.Pairs + var getOrdersRequest order.MultiOrderRequest + var err error + enabledPairs, err = ku.GetEnabledPairs(asset.Futures) + if err != nil { + t.Fatal(err) + } + getOrdersRequest = order.MultiOrderRequest{ + Type: order.Limit, + Pairs: append([]currency.Pair{currency.NewPair(currency.BTC, currency.USDT)}, enabledPairs[:3]...), + AssetType: asset.Futures, + Side: order.AnySide, + } + _, err = ku.GetOrderHistory(context.Background(), &getOrdersRequest) + if err != nil { + t.Error(err) + } + getOrdersRequest.Pairs = []currency.Pair{} + _, err = ku.GetOrderHistory(context.Background(), &getOrdersRequest) + if err != nil { + t.Error(err) + } + getOrdersRequest = order.MultiOrderRequest{ + Type: order.Limit, + Pairs: []currency.Pair{spotTradablePair}, + AssetType: asset.Spot, + Side: order.Sell, + } + _, err = ku.GetOrderHistory(context.Background(), &getOrdersRequest) + if err != nil { + t.Error(err) + } + getOrdersRequest.Pairs = []currency.Pair{} + _, err = ku.GetOrderHistory(context.Background(), &getOrdersRequest) + if err != nil { + t.Error(err) + } + getOrdersRequest.AssetType = asset.Margin + getOrdersRequest.Pairs = currency.Pairs{marginTradablePair} + _, err = ku.GetOrderHistory(context.Background(), &getOrdersRequest) + if err != nil { + t.Error(err) + } +} + +func TestGetActiveOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + var getOrdersRequest order.MultiOrderRequest + var enabledPairs currency.Pairs + var err error + enabledPairs, err = ku.GetEnabledPairs(asset.Spot) + if err != nil { + t.Fatal(err) + } + getOrdersRequest = order.MultiOrderRequest{ + Type: order.Limit, + Pairs: enabledPairs, + AssetType: asset.Spot, + Side: order.Buy, + } + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); err != nil { + t.Error("Kucoin GetActiveOrders() error", err) + } + getOrdersRequest.Pairs = []currency.Pair{} + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); err != nil { + t.Error("Kucoin GetActiveOrders() error", err) + } + getOrdersRequest.Type = order.Market + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); err != nil { + t.Error("Kucoin GetActiveOrders() error", err) + } + getOrdersRequest.Type = order.OCO + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); !errors.Is(err, order.ErrUnsupportedOrderType) { + t.Error("Kucoin GetActiveOrders() error", err) + } + enabledPairs, err = ku.GetEnabledPairs(asset.Spot) + if err != nil { + t.Fatal(err) + } + getOrdersRequest = order.MultiOrderRequest{ + Type: order.Limit, + Pairs: enabledPairs, + AssetType: asset.Margin, + Side: order.Buy, + } + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); err != nil { + t.Error("Kucoin GetActiveOrders() error", err) + } + getOrdersRequest.Pairs = []currency.Pair{} + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); err != nil { + t.Error("Kucoin GetActiveOrders() error", err) + } + getOrdersRequest.Type = order.Market + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); err != nil { + t.Error("Kucoin GetActiveOrders() error", err) + } + getOrdersRequest.Type = order.OCO + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); !errors.Is(err, order.ErrUnsupportedOrderType) { + t.Errorf("expected %v, but found %v", order.ErrUnsupportedOrderType, err) + } + enabledPairs, err = ku.GetEnabledPairs(asset.Futures) + if err != nil { + t.Fatal(err) + } + getOrdersRequest = order.MultiOrderRequest{ + Type: order.Limit, + Pairs: enabledPairs, + AssetType: asset.Futures, + Side: order.Buy, + } + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); err != nil { + t.Error("Kucoin GetActiveOrders() error", err) + } + getOrdersRequest.Pairs = []currency.Pair{} + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); err != nil { + t.Error("Kucoin GetActiveOrders() error", err) + } + getOrdersRequest.Type = order.StopLimit + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); err != nil { + t.Error("Kucoin GetActiveOrders() error", err) + } + getOrdersRequest.Type = order.OCO + if _, err = ku.GetActiveOrders(context.Background(), &getOrdersRequest); !errors.Is(err, order.ErrUnsupportedOrderType) { + t.Errorf("expected %v, but found %v", order.ErrUnsupportedOrderType, err) + } +} + +func TestGetFeeByType(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + if _, err := ku.GetFeeByType(context.Background(), &exchange.FeeBuilder{ + Amount: 1, + FeeType: exchange.CryptocurrencyTradeFee, + Pair: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), currency.DashDelimiter), + PurchasePrice: 1, + FiatCurrency: currency.USD, + BankTransactionType: exchange.WireTransfer, + }); err != nil { + t.Error(err) + } +} + +func TestValidateCredentials(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + assetTypes := ku.CurrencyPairs.GetAssetTypes(true) + for _, at := range assetTypes { + if err := ku.ValidateCredentials(context.Background(), at); err != nil { + t.Error(err) + } + } +} + +func TestGetInstanceServers(t *testing.T) { + t.Parallel() + if _, err := ku.GetInstanceServers(context.Background()); err != nil { + t.Error(err) + } +} + +func TestGetAuthenticatedServersInstances(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetAuthenticatedInstanceServers(context.Background()) + if err != nil { + t.Error(err) + } +} + +var websocketPushDatas = map[string]string{ + "SymbolTickerPushDataJSON": `{"type": "message","topic": "/market/ticker:FET-BTC","subject": "trade.ticker","data": {"bestAsk": "0.000018679","bestAskSize": "258.4609","bestBid": "0.000018622","bestBidSize": "68.5961","price": "0.000018628","sequence": "38509148","size": "8.943","time": 1677321643926}}`, + "AllSymbolsTickerPushDataJSON": `{"type": "message","topic": "/market/ticker:all","subject": "FTM-ETH","data": {"bestAsk": "0.0002901","bestAskSize": "3514.4978","bestBid": "0.0002894","bestBidSize": "65.536","price": "0.0002894","sequence": "186911324","size": "150","time": 1677320967673}}`, + "MarketTradeSnapshotPushDataJSON": `{"type": "message","topic": "/market/snapshot:BTC","subject": "trade.snapshot","data": {"sequence": "5701753771","data": {"averagePrice": 21736.73225440,"baseCurrency": "BTC","board": 1,"buy": 21423,"changePrice": -556.80000000000000000000,"changeRate": -0.0253,"close": 21423.1,"datetime": 1676310802092,"high": 22030.70000000000000000000,"lastTradedPrice": 21423.1,"low": 21407.00000000000000000000,"makerCoefficient": 1.000000,"makerFeeRate": 0.001,"marginTrade": true,"mark": 0,"market": "USDS","markets": ["USDS"],"open": 21979.90000000000000000000,"quoteCurrency": "USDT","sell": 21423.1,"sort": 100,"symbol": "BTC-USDT","symbolCode": "BTC-USDT","takerCoefficient": 1.000000,"takerFeeRate": 0.001,"trading": true,"vol": 6179.80570155000000000000,"volValue": 133988049.45570351500000000000}}}`, + "Orderbook Level 2 PushDataJSON": `{"type": "message","topic": "/spotMarket/level2Depth5:ETH-USDT","subject": "level2","data": {"asks": [[ "21612.7", "0.32307467"],[ "21613.1", "0.1581911"],[ "21613.2", "1.37156153"],[ "21613.3", "2.58327302"],[ "21613.4", "0.00302088"]],"bids": [[ "21612.6", "2.34316818"],[ "21612.3", "0.5771615"],[ "21612.2", "0.21605964"],[ "21612.1", "0.22894841"],[ "21611.6", "0.29251003"]],"timestamp": 1676319909635}}`, + "TradeCandlesUpdatePushDataJSON": `{"type":"message","topic":"/market/candles:BTC-USDT_1hour","subject":"trade.candles.update","data":{"symbol":"BTC-USDT","candles":["1589968800","9786.9","9740.8","9806.1","9732","27.45649579","268280.09830877"],"time":1589970010253893337}}`, + "SymbolSnapshotPushDataJSON": `{"type": "message","topic": "/market/snapshot:KCS-BTC","subject": "trade.snapshot","data": {"sequence": "1545896669291","data": {"trading": true,"symbol": "KCS-BTC","buy": 0.00011,"sell": 0.00012, "sort": 100, "volValue": 3.13851792584, "baseCurrency": "KCS", "market": "BTC", "quoteCurrency": "BTC", "symbolCode": "KCS-BTC", "datetime": 1548388122031, "high": 0.00013, "vol": 27514.34842, "low": 0.0001, "changePrice": -1.0e-5, "changeRate": -0.0769, "lastTradedPrice": 0.00012, "board": 0, "mark": 0 } }}`, + "MatchExecutionPushDataJSON": `{"type":"message","topic":"/market/match:BTC-USDT","subject":"trade.l3match","data":{"sequence":"1545896669145","type":"match","symbol":"BTC-USDT","side":"buy","price":"0.08200000000000000000","size":"0.01022222000000000000","tradeId":"5c24c5da03aa673885cd67aa","takerOrderId":"5c24c5d903aa6772d55b371e","makerOrderId":"5c2187d003aa677bd09d5c93","time":"1545913818099033203"}}`, + "IndexPricePushDataJSON": `{"id":"","type":"message","topic":"/indicator/index:USDT-BTC","subject":"tick","data":{"symbol": "USDT-BTC","granularity": 5000,"timestamp": 1551770400000,"value": 0.0001092}}`, + "MarkPricePushDataJSON": `{"type":"message","topic":"/indicator/markPrice:USDT-BTC","subject":"tick","data":{"symbol": "USDT-BTC","granularity": 5000,"timestamp": 1551770400000,"value": 0.0001093}}`, + "Orderbook ChangePushDataJSON": `{"type":"message","topic":"/margin/fundingBook:USDT","subject":"funding.update","data":{"annualIntRate":0.0547,"currency":"USDT","dailyIntRate":0.00015,"sequence":87611418,"side":"lend","size":25040,"term":7,"ts":1671005721087508735}}`, + "Order ChangeStateOpenPushDataJSON": `{"type":"message","topic":"/spotMarket/tradeOrders","subject":"orderChange","channelType":"private","data":{"symbol":"KCS-USDT","orderType":"limit","side":"buy","orderId":"5efab07953bdea00089965d2","type":"open","orderTime":1593487481683297666,"size":"0.1","filledSize":"0","price":"0.937","clientOid":"1593487481000906","remainSize":"0.1","status":"open","ts":1593487481683297666}}`, + "Order ChangeStateMatchPushDataJSON": `{"type":"message","topic":"/spotMarket/tradeOrders","subject":"orderChange","channelType":"private","data":{"symbol":"KCS-USDT","orderType":"limit","side":"sell","orderId":"5efab07953bdea00089965fa","liquidity":"taker","type":"match","orderTime":1593487482038606180,"size":"0.1","filledSize":"0.1","price":"0.938","matchPrice":"0.96738","matchSize":"0.1","tradeId":"5efab07a4ee4c7000a82d6d9","clientOid":"1593487481000313","remainSize":"0","status":"match","ts":1593487482038606180}}`, + "Order ChangeStateFilledPushDataJSON": `{"type":"message","topic":"/spotMarket/tradeOrders","subject":"orderChange","channelType":"private","data":{"symbol":"KCS-USDT","orderType":"limit","side":"sell","orderId":"5efab07953bdea00089965fa","type":"filled","orderTime":1593487482038606180,"size":"0.1","filledSize":"0.1","price":"0.938","clientOid":"1593487481000313","remainSize":"0","status":"done","ts":1593487482038606180}}`, + "Order ChangeStateCancelledPushDataJSON": `{"type":"message","topic":"/spotMarket/tradeOrders","subject":"orderChange","channelType":"private","data":{"symbol":"KCS-USDT","orderType":"limit","side":"buy","orderId":"5efab07953bdea00089965d2","type":"canceled","orderTime":1593487481683297666,"size":"0.1","filledSize":"0","price":"0.937","clientOid":"1593487481000906","remainSize":"0","status":"done","ts":1593487481893140844}}`, + "Order ChangeStateUpdatePushDataJSON": `{"type":"message","topic":"/spotMarket/tradeOrders","subject":"orderChange","channelType":"private","data":{"symbol":"KCS-USDT","orderType":"limit","side":"buy","orderId":"5efab13f53bdea00089971df","type":"update","oldSize":"0.1","orderTime":1593487679693183319,"size":"0.06","filledSize":"0","price":"0.937","clientOid":"1593487679000249","remainSize":"0.06","status":"open","ts":1593487682916117521}}`, + "AccountBalanceNoticePushDataJSON": `{"type": "message","topic": "/account/balance","subject": "account.balance","channelType":"private","data": {"total": "88","available": "88","availableChange": "88","currency": "KCS","hold": "0","holdChange": "0","relationEvent": "trade.hold","relationEventId": "5c21e80303aa677bd09d7dff","relationContext": {"symbol":"BTC-USDT","tradeId":"5e6a5dca9e16882a7d83b7a4","orderId":"5ea10479415e2f0009949d54"},"time": "1545743136994"}}`, + "DebtRatioChangePushDataJSON": `{"type":"message","topic":"/margin/position","subject":"debt.ratio","channelType":"private","data": {"debtRatio": 0.7505,"totalDebt": "21.7505","debtList": {"BTC": "1.21","USDT": "2121.2121","EOS": "0"},"timestamp": 1553846081210}}`, + "PositionStatusChangeEventPushDataJSON": `{"type":"message","topic":"/margin/position","subject":"position.status","channelType":"private","data": {"type": "FROZEN_FL","timestamp": 1553846081210}}`, + "MarginTradeOrderEntersEventPushDataJSON": `{"type": "message","topic": "/margin/loan:BTC","subject": "order.open","channelType":"private","data": { "currency": "BTC", "orderId": "ac928c66ca53498f9c13a127a60e8", "dailyIntRate": 0.0001, "term": 7, "size": 1, "side": "lend", "ts": 1553846081210004941}}`, + "MarginTradeOrderUpdateEventPushDataJSON": `{"type": "message","topic": "/margin/loan:BTC","subject": "order.update","channelType":"private","data": { "currency": "BTC", "orderId": "ac928c66ca53498f9c13a127a60e8", "dailyIntRate": 0.0001, "term": 7, "size": 1, "lentSize": 0.5, "side": "lend", "ts": 1553846081210004941}}`, + "MarginTradeOrderDoneEventPushDataJSON": `{"type": "message","topic": "/margin/loan:BTC","subject": "order.done","channelType":"private","data": { "currency": "BTC", "orderId": "ac928c66ca53498f9c13a127a60e8", "reason": "filled", "side": "lend", "ts": 1553846081210004941 }}`, + "StopOrderEventPushDataJSON": `{"type":"message","topic":"/spotMarket/advancedOrders","subject":"stopOrder","channelType":"private","data":{"createdAt":1589789942337,"orderId":"5ec244f6a8a75e0009958237","orderPrice":"0.00062","orderType":"stop","side":"sell","size":"1","stop":"entry","stopPrice":"0.00062","symbol":"KCS-BTC","tradeType":"TRADE","triggerSuccess":true,"ts":1589790121382281286,"type":"triggered"}}`, + "Public Futures TickerPushDataJSON": `{"subject": "tickerV2","topic": "/contractMarket/tickerV2:ETHUSDCM","data": {"symbol": "ETHUSDCM","bestBidSize": 795,"bestBidPrice": 3200.00,"bestAskPrice": 3600.00,"bestAskSize": 284,"ts": 1553846081210004941}}`, + "Public Futures TickerV1PushDataJSON": `{"subject": "ticker","topic": "/contractMarket/ticker:ETHUSDCM","data": {"symbol": "ETHUSDCM","sequence": 45,"side": "sell","price": 3600.00,"size": 16,"tradeId": "5c9dcf4170744d6f5a3d32fb","bestBidSize": 795,"bestBidPrice": 3200.00,"bestAskPrice": 3600.00,"bestAskSize": 284,"ts": 1553846081210004941}}`, + "Public Futures Level2OrderbookPushDataJSON": `{"subject": "level2", "topic": "/contractMarket/level2:ETHUSDCM", "type": "message", "data": { "sequence": 18, "change": "5000.0,sell,83","timestamp": 1551770400000}}`, + "Public Futures ExecutionDataJSON": `{"type": "message","topic": "/contractMarket/execution:ETHUSDCM","subject": "match","data": {"makerUserId": "6287c3015c27f000017d0c2f","symbol": "ETHUSDCM","sequence": 31443494,"side": "buy","size": 35,"price": 23083.00000000,"takerOrderId": "63f94040839d00000193264b","makerOrderId": "63f94036839d0000019310c3","takerUserId": "6133f817230d8d000607b941","tradeId": "63f940400000650065f4996f","ts": 1677279296134648869}}`, + "PublicFuturesOrderbookWithDepth5PushDataJSON": `{ "type": "message", "topic": "/contractMarket/level2Depth5:ETHUSDCM", "subject": "level2", "data": { "sequence": 1672332328701, "asks": [[ 23149, 13703],[ 23150, 1460],[ 23151.00000000, 941],[ 23152, 4591],[ 23153, 4107] ], "bids": [[ 23148.00000000, 22801],[23147.0,4766],[ 23146, 1388],[ 23145.00000000, 2593],[ 23144.00000000, 6286] ], "ts": 1677280435684, "timestamp": 1677280435684 }}`, + "Private PositionSettlementPushDataJSON": `{"userId": "xbc453tg732eba53a88ggyt8c","topic": "/contract/position:ETHUSDCM","subject": "position.settlement","data": {"fundingTime": 1551770400000,"qty": 100,"markPrice": 3610.85,"fundingRate": -0.002966,"fundingFee": -296,"ts": 1547697294838004923,"settleCurrency": "XBT"}}`, + "Futures PositionChangePushDataJSON": `{ "userId": "5cd3f1a7b7ebc19ae9558591","topic": "/contract/position:ETHUSDCM", "subject": "position.change", "data": {"markPrice": 7947.83,"markValue": 0.00251640,"maintMargin": 0.00252044,"realLeverage": 10.06,"unrealisedPnl": -0.00014735,"unrealisedRoePcnt": -0.0553,"unrealisedPnlPcnt": -0.0553,"delevPercentage": 0.52,"currentTimestamp": 1558087175068,"settleCurrency": "XBT"}}`, + "Futures PositionChangeWithChangeReasonPushDataJSON": `{ "type": "message","userId": "5c32d69203aa676ce4b543c7","channelType": "private","topic": "/contract/position:ETHUSDCM", "subject": "position.change", "data": {"realisedGrossPnl": 0E-8,"symbol":"ETHUSDCM","crossMode": false,"liquidationPrice": 1000000.0,"posLoss": 0E-8,"avgEntryPrice": 7508.22,"unrealisedPnl": -0.00014735,"markPrice": 7947.83,"posMargin": 0.00266779,"autoDeposit": false,"riskLimit": 100000,"unrealisedCost": 0.00266375,"posComm": 0.00000392,"posMaint": 0.00001724,"posCost": 0.00266375,"maintMarginReq": 0.005,"bankruptPrice": 1000000.0,"realisedCost": 0.00000271,"markValue": 0.00251640,"posInit": 0.00266375,"realisedPnl": -0.00000253,"maintMargin": 0.00252044,"realLeverage": 1.06,"changeReason": "positionChange","currentCost": 0.00266375,"openingTimestamp": 1558433191000,"currentQty": -20,"delevPercentage": 0.52,"currentComm": 0.00000271,"realisedGrossCost": 0E-8,"isOpen": true,"posCross": 1.2E-7,"currentTimestamp": 1558506060394,"unrealisedRoePcnt": -0.0553,"unrealisedPnlPcnt": -0.0553,"settleCurrency": "XBT"}}`, + "Futures WithdrawalAmountTransferOutAmountEventPushDataJSON": `{ "userId": "xbc453tg732eba53a88ggyt8c","topic": "/contractAccount/wallet","subject": "withdrawHold.change","data": {"withdrawHold": 5923,"currency":"USDT","timestamp": 1553842862614}}`, + "Futures AvailableBalanceChangePushData": `{ "userId": "xbc453tg732eba53a88ggyt8c","topic": "/contractAccount/wallet","subject": "availableBalance.change","data": {"availableBalance": 5923,"holdBalance": 2312,"currency":"USDT","timestamp": 1553842862614}}`, + "Futures OrderMarginChangePushDataJSON": `{ "userId": "xbc453tg732eba53a88ggyt8c","topic": "/contractAccount/wallet","subject": "orderMargin.change","data": {"orderMargin": 5923,"currency":"USDT","timestamp": 1553842862614}}`, + "Futures StopOrderPushDataJSON": `{"userId": "5cd3f1a7b7ebc19ae9558591","topic": "/contractMarket/advancedOrders", "subject": "stopOrder","data": {"orderId": "5cdfc138b21023a909e5ad55","symbol": "ETHUSDCM","type": "open","orderType":"stop","side":"buy","size":"1000","orderPrice":"9000","stop":"up","stopPrice":"9100","stopPriceType":"TP","triggerSuccess": true,"error": "error.createOrder.accountBalanceInsufficient","createdAt": 1558074652423,"ts":1558074652423004000}}`, + "Futures TradeOrdersPushDataJSON": `{"type": "message","topic": "/contractMarket/tradeOrders","subject": "orderChange","channelType": "private","data": {"orderId": "5cdfc138b21023a909e5ad55","symbol": "ETHUSDCM","type": "match","status": "open","matchSize": "","matchPrice": "","orderType": "limit","side": "buy","price": "3600","size": "20000","remainSize": "20001","filledSize":"20000","canceledSize": "0","tradeId": "5ce24c16b210233c36eexxxx","clientOid": "5ce24c16b210233c36ee321d","orderTime": 1545914149935808589,"oldSize ": "15000","liquidity": "maker","ts": 1545914149935808589}}`, + "TransactionStaticsPushDataJSON": `{ "topic": "/contractMarket/snapshot:ETHUSDCM","subject": "snapshot.24h","data": {"volume": 30449670, "turnover": 845169919063,"lastPrice": 3551, "priceChgPct": 0.0043, "ts": 1547697294838004923} }`, + "Futures EndFundingFeeSettlementPushDataJSON": `{ "type":"message","topic": "/contract/announcement","subject": "funding.end","data": {"symbol": "ETHUSDCM", "fundingTime": 1551770400000,"fundingRate": -0.002966, "timestamp": 1551770410000 }}`, + "Futures StartFundingFeeSettlementPushDataJSON": `{ "topic": "/contract/announcement","subject": "funding.begin","data": {"symbol": "ETHUSDCM","fundingTime": 1551770400000,"fundingRate": -0.002966,"timestamp": 1551770400000}}`, + "Futures FundingRatePushDataJSON": `{ "topic": "/contract/instrument:ETHUSDCM","subject": "funding.rate","data": {"granularity": 60000,"fundingRate": -0.002966,"timestamp": 1551770400000}}`, + "Futures MarkIndexPricePushDataJSON": `{ "topic": "/contract/instrument:ETHUSDCM","subject": "mark.index.price","data": {"granularity": 1000,"indexPrice": 4000.23,"markPrice": 4010.52,"timestamp": 1551770400000}}`, + "Orderbook Market Level2": `{ "type": "message", "topic": "/market/level2:BTC-USDT", "subject": "trade.l2update", "data": { "changes": { "asks": [ [ "18906", "0.00331", "14103845" ], [ "18907.3", "0.58751503", "14103844" ] ], "bids": [ [ "18891.9", "0.15688", "14103847" ] ] }, "sequenceEnd": 14103847, "sequenceStart": 14103844, "symbol": "BTC-USDT", "time": 1663747970273 } }`, +} + +func TestPushData(t *testing.T) { + for key, val := range websocketPushDatas { + err := ku.wsHandleData([]byte(val)) + if err != nil { + t.Errorf("%s: %v", key, err) + } + } +} + +func TestGenerateDefaultSubscriptions(t *testing.T) { + t.Parallel() + if _, err := ku.GenerateDefaultSubscriptions(); err != nil { + t.Error(err) + } +} + +func TestGetAvailableTransferChains(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + if _, err := ku.GetAvailableTransferChains(context.Background(), currency.BTC); err != nil { + t.Error(err) + } +} + +func TestGetWithdrawalsHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + if _, err := ku.GetWithdrawalsHistory(context.Background(), currency.BTC, asset.Futures); err != nil { + t.Error(err) + } + if _, err := ku.GetWithdrawalsHistory(context.Background(), currency.BTC, asset.Spot); err != nil { + t.Error(err) + } + if _, err := ku.GetWithdrawalsHistory(context.Background(), currency.BTC, asset.Margin); !errors.Is(err, asset.ErrNotSupported) { + t.Error(err) + } +} + +func TestGetOrderInfo(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + var err error + _, err = ku.GetOrderInfo(context.Background(), "123", futuresTradablePair, asset.Futures) + if err != nil { + t.Errorf("expected %s, but found %v", "Order does not exist", err) + } + _, err = ku.GetOrderInfo(context.Background(), "123", futuresTradablePair, asset.Spot) + if err != nil { + t.Errorf("expected %s, but found %v", "Order does not exist", err) + } + _, err = ku.GetOrderInfo(context.Background(), "123", futuresTradablePair, asset.Margin) + if err != nil { + t.Errorf("expected %s, but found %v", "Order does not exist", err) + } +} + +func TestGetDepositAddress(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + if _, err := ku.GetDepositAddress(context.Background(), currency.BTC, "", ""); err != nil && !errors.Is(err, errNoDepositAddress) { + t.Error(err) + } +} + +func TestWithdrawCryptocurrencyFunds(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + withdrawCryptoRequest := withdraw.Request{ + Exchange: ku.Name, + Amount: 0.00000000001, + Currency: currency.BTC, + Crypto: withdraw.CryptoRequest{ + Address: core.BitcoinDonationAddress, + }, + } + if _, err := ku.WithdrawCryptocurrencyFunds(context.Background(), &withdrawCryptoRequest); err != nil { + t.Error(err) + } +} + +func TestSubmitOrder(t *testing.T) { + t.Parallel() + orderSubmission := &order.Submit{ + Pair: spotTradablePair, + Exchange: ku.Name, + Side: order.Bid, + Type: order.Limit, + Price: 1, + Amount: 100000, + ClientOrderID: "myOrder", + AssetType: asset.Spot, + } + _, err := ku.SubmitOrder(context.Background(), orderSubmission) + if !errors.Is(err, order.ErrSideIsInvalid) { + t.Errorf("expected %v, but found %v", asset.ErrNotSupported, err) + } + orderSubmission.Side = order.Buy + orderSubmission.AssetType = asset.Options + _, err = ku.SubmitOrder(context.Background(), orderSubmission) + if !errors.Is(err, asset.ErrNotSupported) { + t.Errorf("expected %v, but found %v", asset.ErrNotSupported, err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + orderSubmission.AssetType = asset.Spot + orderSubmission.Side = order.Buy + orderSubmission.Pair = spotTradablePair + _, err = ku.SubmitOrder(context.Background(), orderSubmission) + if err != order.ErrTypeIsInvalid { + t.Errorf("expected %v, but found %v", order.ErrTypeIsInvalid, err) + } + orderSubmission.AssetType = asset.Spot + orderSubmission.Pair = spotTradablePair + _, err = ku.SubmitOrder(context.Background(), orderSubmission) + if err != nil { + t.Error(err) + } + orderSubmission.AssetType = asset.Margin + orderSubmission.Pair = marginTradablePair + _, err = ku.SubmitOrder(context.Background(), orderSubmission) + if err != nil { + t.Error(err) + } + orderSubmission.AssetType = asset.Margin + orderSubmission.Pair = marginTradablePair + orderSubmission.MarginType = margin.Isolated + _, err = ku.SubmitOrder(context.Background(), orderSubmission) + if err != nil { + t.Error(err) + } + orderSubmission.AssetType = asset.Futures + orderSubmission.Pair = futuresTradablePair + _, err = ku.SubmitOrder(context.Background(), orderSubmission) + if !errors.Is(err, errInvalidLeverage) { + t.Error(err) + } + orderSubmission.Leverage = 0.01 + _, err = ku.SubmitOrder(context.Background(), orderSubmission) + if err != nil { + t.Error(err) + } +} + +func TestCancelOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + var orderCancellation = &order.Cancel{ + OrderID: "1", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: spotTradablePair, + AssetType: asset.Spot, + } + if err := ku.CancelOrder(context.Background(), orderCancellation); err != nil { + t.Error(err) + } + orderCancellation.Pair = marginTradablePair + orderCancellation.AssetType = asset.Margin + if err := ku.CancelOrder(context.Background(), orderCancellation); err != nil { + t.Error(err) + } + orderCancellation.Pair = futuresTradablePair + orderCancellation.AssetType = asset.Futures + if err := ku.CancelOrder(context.Background(), orderCancellation); err != nil { + t.Error(err) + } +} + +func TestCancelAllOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + if _, err := ku.CancelAllOrders(context.Background(), &order.Cancel{ + AssetType: asset.Futures, + MarginType: margin.Isolated, + }); err != nil { + t.Error(err) + } + if _, err := ku.CancelAllOrders(context.Background(), &order.Cancel{ + AssetType: asset.Margin, + MarginType: margin.Isolated, + }); err != nil { + t.Error(err) + } + if _, err := ku.CancelAllOrders(context.Background(), &order.Cancel{ + AssetType: asset.Spot, + MarginType: margin.Isolated, + }); err != nil { + t.Error(err) + } +} + +func TestGeneratePayloads(t *testing.T) { + t.Parallel() + subscriptions, err := ku.GenerateDefaultSubscriptions() + if err != nil { + t.Error(err) + } + payload, err := ku.generatePayloads(subscriptions, "subscribe") + if err != nil { + t.Error(err) + } + if len(payload) != len(subscriptions) { + t.Error("derived payload is not same as generated channel subscription instances") + } +} + +const ( + subUserResponseJSON = `{"userId":"635002438793b80001dcc8b3", "uid":62356, "subName":"margin01", "status":2, "type":4, "access":"Margin", "createdAt":1666187844000, "remarks":null }` + positionSettlementPushData = `{"userId": "xbc453tg732eba53a88ggyt8c", "topic": "/contract/position:XBTUSDM", "subject": "position.settlement", "data": { "fundingTime": 1551770400000, "qty": 100, "markPrice": 3610.85, "fundingRate": -0.002966, "fundingFee": -296, "ts": 1547697294838004923, "settleCurrency": "XBT" } }` + transferFuturesFundsResponseJSON = `{"applyId": "620a0bbefeaa6a000110e833", "bizNo": "620a0bbefeaa6a000110e832", "payAccountType": "CONTRACT", "payTag": "DEFAULT", "remark": "", "recAccountType": "MAIN", "recTag": "DEFAULT", "recRemark": "", "recSystem": "KUCOIN", "status": "PROCESSING", "currency": "USDT", "amount": "0.001", "fee": "0", "sn": 889048787670001, "reason": "", "createdAt": 1644825534000, "updatedAt": 1644825534000 }` + modifySubAccountSpotAPIs = `{"subName": "AAAAAAAAAA0007", "remark": "remark", "apiKey": "630325e0e750870001829864", "apiSecret": "110f31fc-61c5-4baf-a29f-3f19a62bbf5d", "passphrase": "passphrase", "permission": "General", "ipWhitelist": "", "createdAt": 1661150688000 }` +) + +func TestCreateSubUser(t *testing.T) { + t.Parallel() + var resp *SubAccount + err := json.Unmarshal([]byte(subUserResponseJSON), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + if _, err := ku.CreateSubUser(context.Background(), "SamuaelTee1", "sdfajdlkad", "", ""); err != nil { + t.Error(err) + } +} + +func TestGetSubAccountSpotAPIList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + if _, err := ku.GetSubAccountSpotAPIList(context.Background(), "sam", ""); err != nil { + t.Error(err) + } +} + +func TestCreateSpotAPIsForSubAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + if _, err := ku.CreateSpotAPIsForSubAccount(context.Background(), &SpotAPISubAccountParams{ + SubAccountName: "gocryptoTrader1", + Passphrase: "mysecretPassphrase123", + Remark: "123456", + }); err != nil { + t.Error(err) + } +} + +func TestModifySubAccountSpotAPIs(t *testing.T) { + t.Parallel() + var resp SpotAPISubAccount + err := json.Unmarshal([]byte(modifySubAccountSpotAPIs), &resp) + if err != nil { + t.Fatal(err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + if _, err := ku.ModifySubAccountSpotAPIs(context.Background(), &SpotAPISubAccountParams{ + SubAccountName: "gocryptoTrader1", + Passphrase: "mysecretPassphrase123", + Remark: "123456", + }); err != nil { + t.Error(err) + } +} + +func TestDeleteSubAccountSpotAPI(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + if _, err := ku.DeleteSubAccountSpotAPI(context.Background(), apiKey, "mysecretPassphrase123", "gocryptoTrader1"); err != nil { + t.Error(err) + } +} + +func TestGetUserInfoOfAllSubAccounts(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + if _, err := ku.GetUserInfoOfAllSubAccounts(context.Background()); err != nil { + t.Error(err) + } +} + +func TestGetPaginatedListOfSubAccounts(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + if _, err := ku.GetPaginatedListOfSubAccounts(context.Background(), 1, 100); err != nil { + t.Error(err) + } +} + +func setupWS() { + if !ku.Websocket.IsEnabled() { + return + } + if !sharedtestvalues.AreAPICredentialsSet(ku) { + ku.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + err := ku.WsConnect() + if err != nil { + log.Fatal(err) + } +} + +func TestGetFundingHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.GetAccountFundingHistory(context.Background()) + if err != nil { + t.Error(err) + } +} + +func getFirstTradablePairOfAssets() { + if err := ku.UpdateTradablePairs(context.Background(), true); err != nil { + log.Fatalf("Kucoin error while updating tradable pairs. %v", err) + } + enabledPairs, err := ku.GetEnabledPairs(asset.Spot) + if err != nil { + log.Fatalf("Kucoin %v, trying to get %v enabled pairs error", err, asset.Spot) + } + spotTradablePair = enabledPairs[0] + enabledPairs, err = ku.GetEnabledPairs(asset.Margin) + if err != nil { + log.Fatalf("Kucoin %v, trying to get %v enabled pairs error", err, asset.Margin) + } + marginTradablePair = enabledPairs[0] + enabledPairs, err = ku.GetEnabledPairs(asset.Futures) + if err != nil { + log.Fatalf("Kucoin %v, trying to get %v enabled pairs error", err, asset.Futures) + } + futuresTradablePair = enabledPairs[0] + futuresTradablePair.Delimiter = "" +} + +func TestFetchAccountInfo(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + var err error + _, err = ku.FetchAccountInfo(context.Background(), asset.Spot) + if err != nil { + t.Fatal(err) + } + _, err = ku.FetchAccountInfo(context.Background(), asset.Margin) + if err != nil { + t.Fatal(err) + } + _, err = ku.FetchAccountInfo(context.Background(), asset.Futures) + if err != nil { + t.Fatal(err) + } +} + +func TestKucoinNumberUnmarshal(t *testing.T) { + t.Parallel() + data := &struct { + Number kucoinNumber `json:"number"` + }{} + data1 := `{"number": 123.33}` + err := json.Unmarshal([]byte(data1), &data) + if err != nil { + t.Fatal(err) + } else if data.Number.Float64() != 123.33 { + t.Errorf("expecting %.2f, got %.2f", 123.33, data.Number) + } + data2 := `{"number": "123.33"}` + err = json.Unmarshal([]byte(data2), &data) + if err != nil { + t.Fatal(err) + } else if data.Number.Float64() != 123.33 { + t.Errorf("expecting %.2f, got %.2f", 123.33, data.Number) + } + data3 := `{"number": ""}` + err = json.Unmarshal([]byte(data3), &data) + if err != nil { + t.Fatal(err) + } else if data.Number.Float64() != 0 { + t.Errorf("expecting %d, got %.2f", 0, data.Number) + } + data4 := `{"number": "123"}` + err = json.Unmarshal([]byte(data4), &data) + if err != nil { + t.Fatal(err) + } else if data.Number.Float64() != 123 { + t.Errorf("expecting %d, got %.2f", 123, data.Number) + } + data5 := `{"number": 0}` + err = json.Unmarshal([]byte(data5), &data) + if err != nil { + t.Fatal(err) + } else if data.Number.Float64() != 0 { + t.Errorf("expecting %d, got %.2f", 0, data.Number) + } + data6 := `{"number": 123789}` + err = json.Unmarshal([]byte(data6), &data) + if err != nil { + t.Fatal(err) + } else if data.Number.Float64() != 123789 { + t.Errorf("expecting %d, got %.2f", 123789, data.Number) + } + data7 := `{"number": 12321312312312312}` + err = json.Unmarshal([]byte(data7), &data) + if err != nil { + t.Fatal(err) + } else if data.Number.Float64() != float64(12321312312312312) { + t.Errorf("expecting %.f, got %.2f", float64(12321312312312312), data.Number) + } +} + +func TestUpdateAccountInfo(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku) + _, err := ku.UpdateAccountInfo(context.Background(), asset.Spot) + if err != nil { + t.Error("Kucoin UpdateAccountInfo() error", err) + } + _, err = ku.UpdateAccountInfo(context.Background(), asset.Futures) + if err != nil { + t.Error("Kucoin UpdateAccountInfo() error", err) + } + _, err = ku.UpdateAccountInfo(context.Background(), asset.Margin) + if err != nil { + t.Error("Kucoin UpdateAccountInfo() error", err) + } +} + +const ( + orderbookLevel5PushData = `{"type": "message","topic": "/spotMarket/level2Depth50:BTC-USDT","subject": "level2","data": {"asks": [["21621.7","3.03206193"],["21621.8","1.00048239"],["21621.9","0.29558803"],["21622","0.0049653"],["21622.4","0.06177582"],["21622.9","0.39664116"],["21623.7","0.00803466"],["21624.2","0.65405"],["21624.3","0.34661426"],["21624.6","0.00035589"],["21624.9","0.61282048"],["21625.2","0.16421424"],["21625.4","0.90107014"],["21625.5","0.73484442"],["21625.9","0.04"],["21626.2","0.28569324"],["21626.4","0.18403701"],["21627.1","0.06503999"],["21627.2","0.56105832"],["21627.7","0.10649999"],["21628.1","2.66459953"],["21628.2","0.32"],["21628.5","0.27605551"],["21628.6","1.59482596"],["21628.9","0.16"],["21629.8","0.08"],["21630","0.04"],["21631.6","0.1"],["21631.8","0.0920185"],["21633.6","0.00447983"],["21633.7","0.00015044"],["21634.3","0.32193346"],["21634.4","0.00004"],["21634.5","0.1"],["21634.6","0.0002865"],["21635.6","0.12069941"],["21635.8","0.00117158"],["21636","0.00072816"],["21636.5","0.98611492"],["21636.6","0.00007521"],["21637.2","0.00699999"],["21637.6","0.00017129"],["21638","0.00013035"],["21638.1","0.05"],["21638.5","0.92427"],["21639.2","1.84998696"],["21639.3","0.04827233"],["21640","0.56255996"],["21640.9","0.8"],["21641","0.12"]],"bids": [["21621.6","0.40949924"],["21621.5","0.27703279"],["21621.3","0.04"],["21621.1","0.0086"],["21621","0.6653104"],["21620.9","0.35435999"],["21620.8","0.37224309"],["21620.5","0.416184"],["21620.3","0.24"],["21619.6","0.13883999"],["21619.5","0.21053355"],["21618.7","0.2"],["21618.6","0.001"],["21618.5","0.2258151"],["21618.4","0.06503999"],["21618.3","0.00370056"],["21618","0.12067842"],["21617.7","0.34844131"],["21617.6","0.92845495"],["21617.5","0.66460535"],["21617","0.01"],["21616.7","0.0004624"],["21616.4","0.02"],["21615.6","0.04828251"],["21615","0.59065665"],["21614.4","0.00227"],["21614.3","0.1"],["21613","0.32193346"],["21612.9","0.0028638"],["21612.6","0.1"],["21612.5","0.92539"],["21610.7","0.08208616"],["21610.6","0.00967666"],["21610.3","0.12"],["21610.2","0.00611126"],["21609.9","0.00226344"],["21609.8","0.00315812"],["21609.1","0.00547218"],["21608.6","0.09793157"],["21608.5","0.00437793"],["21608.4","1.85013454"],["21608.1","0.00366647"],["21607.9","0.00611595"],["21607.7","0.83263561"],["21607.6","0.00368919"],["21607.5","0.00280702"],["21607.1","0.66610849"],["21606.8","0.00364164"],["21606.2","0.80351642"],["21605.7","0.075"]],"timestamp": 1676319280783}}` + wsOrderbookData = `{"changes":{"asks":[["21621.7","3.03206193",""],["21621.8","1.00048239",""],["21621.9","0.29558803",""],["21622","0.0049653",""],["21622.4","0.06177582",""],["21622.9","0.39664116",""],["21623.7","0.00803466",""],["21624.2","0.65405",""],["21624.3","0.34661426",""],["21624.6","0.00035589",""],["21624.9","0.61282048",""],["21625.2","0.16421424",""],["21625.4","0.90107014",""],["21625.5","0.73484442",""],["21625.9","0.04",""],["21626.2","0.28569324",""],["21626.4","0.18403701",""],["21627.1","0.06503999",""],["21627.2","0.56105832",""],["21627.7","0.10649999",""],["21628.1","2.66459953",""],["21628.2","0.32",""],["21628.5","0.27605551",""],["21628.6","1.59482596",""],["21628.9","0.16",""],["21629.8","0.08",""],["21630","0.04",""],["21631.6","0.1",""],["21631.8","0.0920185",""],["21633.6","0.00447983",""],["21633.7","0.00015044",""],["21634.3","0.32193346",""],["21634.4","0.00004",""],["21634.5","0.1",""],["21634.6","0.0002865",""],["21635.6","0.12069941",""],["21635.8","0.00117158",""],["21636","0.00072816",""],["21636.5","0.98611492",""],["21636.6","0.00007521",""],["21637.2","0.00699999",""],["21637.6","0.00017129",""],["21638","0.00013035",""],["21638.1","0.05",""],["21638.5","0.92427",""],["21639.2","1.84998696",""],["21639.3","0.04827233",""],["21640","0.56255996",""],["21640.9","0.8",""],["21641","0.12",""]],"bids":[["21621.6","0.40949924",""],["21621.5","0.27703279",""],["21621.3","0.04",""],["21621.1","0.0086",""],["21621","0.6653104",""],["21620.9","0.35435999",""],["21620.8","0.37224309",""],["21620.5","0.416184",""],["21620.3","0.24",""],["21619.6","0.13883999",""],["21619.5","0.21053355",""],["21618.7","0.2",""],["21618.6","0.001",""],["21618.5","0.2258151",""],["21618.4","0.06503999",""],["21618.3","0.00370056",""],["21618","0.12067842",""],["21617.7","0.34844131",""],["21617.6","0.92845495",""],["21617.5","0.66460535",""],["21617","0.01",""],["21616.7","0.0004624",""],["21616.4","0.02",""],["21615.6","0.04828251",""],["21615","0.59065665",""],["21614.4","0.00227",""],["21614.3","0.1",""],["21613","0.32193346",""],["21612.9","0.0028638",""],["21612.6","0.1",""],["21612.5","0.92539",""],["21610.7","0.08208616",""],["21610.6","0.00967666",""],["21610.3","0.12",""],["21610.2","0.00611126",""],["21609.9","0.00226344",""],["21609.8","0.00315812",""],["21609.1","0.00547218",""],["21608.6","0.09793157",""],["21608.5","0.00437793",""],["21608.4","1.85013454",""],["21608.1","0.00366647",""],["21607.9","0.00611595",""],["21607.7","0.83263561",""],["21607.6","0.00368919",""],["21607.5","0.00280702",""],["21607.1","0.66610849",""],["21606.8","0.00364164",""],["21606.2","0.80351642",""],["21605.7","0.075",""]]},"sequenceEnd":1676319280783,"sequenceStart":0,"symbol":"BTC-USDT","time":1676319280783}` +) + +func TestProcessOrderbook(t *testing.T) { + t.Parallel() + response := &WsOrderbook{} + err := json.Unmarshal([]byte(wsOrderbookData), &response) + if err != nil { + t.Error(err) + } + _, err = ku.UpdateLocalBuffer(response, asset.Spot) + if err != nil { + t.Error(err) + } + err = ku.processOrderbook([]byte(orderbookLevel5PushData), "BTC-USDT") + if err != nil { + t.Error(err) + } + err = ku.wsHandleData([]byte(orderbookLevel5PushData)) + if err != nil { + t.Error(err) + } +} + +func TestSeedLocalCache(t *testing.T) { + t.Parallel() + pair, err := currency.NewPairFromString("ETH-USDT") + if err != nil { + t.Error(err) + } + err = ku.SeedLocalCache(context.Background(), pair, asset.Margin) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/kucoin/kucoin_types.go b/exchanges/kucoin/kucoin_types.go new file mode 100644 index 00000000..11082992 --- /dev/null +++ b/exchanges/kucoin/kucoin_types.go @@ -0,0 +1,1498 @@ +package kucoin + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" + "strconv" + "time" + + "github.com/thrasher-corp/gocryptotrader/common/convert" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" +) + +var ( + validPeriods = []string{ + "1min", "3min", "5min", "15min", "30min", "1hour", "2hour", "4hour", "6hour", "8hour", "12hour", "1day", "1week", + } + + errInvalidResponseReceiver = errors.New("invalid response receiver") + errInvalidPrice = errors.New("invalid price") + errInvalidStopPriceType = errors.New("stopPriceType is required") + errInvalidSize = errors.New("invalid size") + errMalformedData = errors.New("malformed data") + errNoDepositAddress = errors.New("no deposit address found") + errMultipleDepositAddress = errors.New("multiple deposit addresses") + errInvalidResultInterface = errors.New("result interface has to be pointer") + errInvalidSubAccountName = errors.New("invalid sub-account name") + errInvalidPassPhraseInstance = errors.New("invalid passphrase string") + errNoValidResponseFromServer = errors.New("no valid response from server") + 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") + 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}$") +) + +// UnmarshalTo acts as interface to exchange API response +type UnmarshalTo interface { + GetError() error +} + +// Error defines all error information for each request +type Error struct { + Code string `json:"code"` + Msg string `json:"msg"` +} + +// GetError checks and returns an error if it is supplied. +func (e Error) GetError() error { + code, err := strconv.ParseInt(e.Code, 10, 64) + if err != nil { + return err + } + switch code { + case 200000, 200: + return nil + default: + return fmt.Errorf("code: %s message: %s", e.Code, e.Msg) + } +} + +// SymbolInfo stores symbol information +type SymbolInfo struct { + Symbol string `json:"symbol"` + Name string `json:"name"` + BaseCurrency string `json:"baseCurrency"` + QuoteCurrency string `json:"quoteCurrency"` + FeeCurrency string `json:"feeCurrency"` + Market string `json:"market"` + BaseMinSize float64 `json:"baseMinSize,string"` + QuoteMinSize float64 `json:"quoteMinSize,string"` + BaseMaxSize float64 `json:"baseMaxSize,string"` + QuoteMaxSize float64 `json:"quoteMaxSize,string"` + BaseIncrement float64 `json:"baseIncrement,string"` + QuoteIncrement float64 `json:"quoteIncrement,string"` + PriceIncrement float64 `json:"priceIncrement,string"` + PriceLimitRate float64 `json:"priceLimitRate,string"` + MinFunds float64 `json:"minFunds,string"` + IsMarginEnabled bool `json:"isMarginEnabled"` + EnableTrading bool `json:"enableTrading"` +} + +// Ticker stores ticker data +type Ticker struct { + Sequence string `json:"sequence"` + BestAsk float64 `json:"bestAsk,string"` + Size float64 `json:"size,string"` + Price float64 `json:"price,string"` + BestBidSize float64 `json:"bestBidSize,string"` + BestBid float64 `json:"bestBid,string"` + BestAskSize float64 `json:"bestAskSize,string"` + Time convert.ExchangeTime `json:"time"` +} + +type tickerInfoBase struct { + Symbol string `json:"symbol"` + Buy float64 `json:"buy,string"` + Sell float64 `json:"sell,string"` + ChangeRate float64 `json:"changeRate,string"` + ChangePrice float64 `json:"changePrice,string"` + High float64 `json:"high,string"` + Low float64 `json:"low,string"` + Volume float64 `json:"vol,string"` + VolumeValue float64 `json:"volValue,string"` + Last float64 `json:"last,string"` + AveragePrice float64 `json:"averagePrice,string"` + TakerFeeRate float64 `json:"takerFeeRate,string"` + MakerFeeRate float64 `json:"makerFeeRate,string"` + TakerCoefficient float64 `json:"takerCoefficient,string"` + MakerCoefficient float64 `json:"makerCoefficient,string"` +} + +// TickerInfo stores ticker information +type TickerInfo struct { + tickerInfoBase + SymbolName string `json:"symbolName"` +} + +// Stats24hrs stores 24 hrs statistics +type Stats24hrs struct { + tickerInfoBase + Time convert.ExchangeTime `json:"time"` +} + +// Orderbook stores the orderbook data +type Orderbook struct { + Sequence int64 + Bids []orderbook.Item + Asks []orderbook.Item + Time time.Time +} + +type orderbookResponse struct { + Asks [][2]string `json:"asks"` + Bids [][2]string `json:"bids"` + Time convert.ExchangeTime `json:"time"` + Sequence string `json:"sequence"` +} + +// Trade stores trade data +type Trade struct { + Sequence string `json:"sequence"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + Side string `json:"side"` + Time convert.ExchangeTime `json:"time"` +} + +// Kline stores kline data +type Kline struct { + StartTime time.Time + Open float64 + Close float64 + High float64 + Low float64 + Volume float64 // Transaction volume + Amount float64 // Transaction amount +} + +type currencyBase struct { + Currency string `json:"currency"` // a unique currency code that will never change + Name string `json:"name"` // will change after renaming + FullName string `json:"fullName"` + Precision int64 `json:"precision"` + Confirms int64 `json:"confirms"` + ContractAddress string `json:"contractAddress"` + IsMarginEnabled bool `json:"isMarginEnabled"` + IsDebitEnabled bool `json:"isDebitEnabled"` +} + +// Currency stores currency data +type Currency struct { + currencyBase + WithdrawalMinSize float64 `json:"withdrawalMinSize,string"` + WithdrawalMinFee float64 `json:"withdrawalMinFee,string"` + IsWithdrawEnabled bool `json:"isWithdrawEnabled"` + IsDepositEnabled bool `json:"isDepositEnabled"` +} + +// Chain stores blockchain data +type Chain struct { + Name string `json:"chainName"` + Confirms int64 `json:"confirms"` + ContractAddress string `json:"contractAddress"` + WithdrawalMinSize float64 `json:"withdrawalMinSize,string"` + WithdrawalMinFee float64 `json:"withdrawalMinFee,string"` + IsWithdrawEnabled bool `json:"isWithdrawEnabled"` + IsDepositEnabled bool `json:"isDepositEnabled"` +} + +// CurrencyDetail stores currency details +type CurrencyDetail struct { + currencyBase + Chains []Chain `json:"chains"` +} + +// MarkPrice stores mark price data +type MarkPrice struct { + Symbol string `json:"symbol"` + Granularity int64 `json:"granularity"` + TimePoint convert.ExchangeTime `json:"timePoint"` + Value float64 `json:"value"` +} + +// MarginConfiguration stores margin configuration +type MarginConfiguration struct { + CurrencyList []string `json:"currencyList"` + WarningDebtRatio float64 `json:"warningDebtRatio,string"` + LiqDebtRatio float64 `json:"liqDebtRatio,string"` + MaxLeverage float64 `json:"maxLeverage"` +} + +// MarginAccount stores margin account data +type MarginAccount struct { + AvailableBalance float64 `json:"availableBalance,string"` + Currency string `json:"currency"` + HoldBalance float64 `json:"holdBalance,string"` + Liability float64 `json:"liability,string"` + MaxBorrowSize float64 `json:"maxBorrowSize,string"` + TotalBalance float64 `json:"totalBalance,string"` +} + +// MarginAccounts stores margin accounts data +type MarginAccounts struct { + Accounts []MarginAccount `json:"accounts"` + DebtRatio float64 `json:"debtRatio,string"` +} + +// MarginRiskLimit stores margin risk limit +type MarginRiskLimit struct { + Currency string `json:"currency"` + MaximumBorrowAmount float64 `json:"borrowMaxAmount,string"` + MaxumumBuyAmount float64 `json:"buyMaxAmount,string"` + MaximumHoldAmount float64 `json:"holdMaxAmount,string"` + Precision int64 `json:"precision"` +} + +// PostBorrowOrderResp stores borrow order response +type PostBorrowOrderResp struct { + OrderID string `json:"orderId"` + Currency string `json:"currency"` +} + +// BorrowOrder stores borrow order +type BorrowOrder struct { + OrderID string `json:"orderId"` + Currency string `json:"currency"` + Size float64 `json:"size,string"` + Filled float64 `json:"filled"` + MatchList []struct { + Currency string `json:"currency"` + DailyIntRate float64 `json:"dailyIntRate,string"` + Size float64 `json:"size,string"` + Term int64 `json:"term"` + Timestamp convert.ExchangeTime `json:"timestamp"` + TradeID string `json:"tradeId"` + } `json:"matchList"` + Status string `json:"status"` +} + +type baseRecord struct { + TradeID string `json:"tradeId"` + Currency string `json:"currency"` + DailyIntRate float64 `json:"dailyIntRate,string"` + Principal float64 `json:"principal,string"` + RepaidSize float64 `json:"repaidSize,string"` + Term int64 `json:"term"` +} + +// OutstandingRecordResponse represents outstanding record detail. +type OutstandingRecordResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNumber int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []OutstandingRecord `json:"items"` // lists +} + +// OutstandingRecord stores outstanding record +type OutstandingRecord struct { + baseRecord + AccruedInterest float64 `json:"accruedInterest,string"` + Liability float64 `json:"liability,string"` + MaturityTime convert.ExchangeTime `json:"maturityTime"` + CreatedAt convert.ExchangeTime `json:"createdAt"` +} + +// RepaidRecordsResponse stores list of repaid record details. +type RepaidRecordsResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNumber int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []RepaidRecord `json:"items"` +} + +// RepaidRecord stores repaid record +type RepaidRecord struct { + baseRecord + Interest float64 `json:"interest,string"` + RepayTime convert.ExchangeTime `json:"repayTime"` +} + +// LendOrder stores lend order +type LendOrder struct { + OrderID string `json:"orderId"` + Currency string `json:"currency"` + Size float64 `json:"size,string"` + FilledSize float64 `json:"filledSize,string"` + DailyIntRate float64 `json:"dailyIntRate,string"` + Term int64 `json:"term"` + CreatedAt convert.ExchangeTime `json:"createdAt"` +} + +// LendOrderHistory stores lend order history +type LendOrderHistory struct { + LendOrder + Status string `json:"status"` +} + +// UnsettleLendOrder stores unsettle lend order +type UnsettleLendOrder struct { + TradeID string `json:"tradeId"` + Currency string `json:"currency"` + Size float64 `json:"size,string"` + AccruedInterest float64 `json:"accruedInterest,string"` + Repaid float64 `json:"repaid,string"` + DailyIntRate float64 `json:"dailyIntRate,string"` + Term int64 `json:"term"` + MaturityTime convert.ExchangeTime `json:"maturityTime"` +} + +// SettleLendOrder stores settled lend order +type SettleLendOrder struct { + TradeID string `json:"tradeId"` + Currency string `json:"currency"` + Size float64 `json:"size,string"` + Interest float64 `json:"interest,string"` + Repaid float64 `json:"repaid,string"` + DailyIntRate float64 `json:"dailyIntRate,string"` + Term int64 `json:"term"` + SettledAt convert.ExchangeTime `json:"settledAt"` + Note string `json:"note"` +} + +// LendRecord stores lend record +type LendRecord struct { + Currency string `json:"currency"` + Outstanding float64 `json:"outstanding,string"` + FilledSize float64 `json:"filledSize,string"` + AccruedInterest float64 `json:"accruedInterest,string"` + RealizedProfit float64 `json:"realizedProfit,string"` + IsAutoLend bool `json:"isAutoLend"` +} + +// LendMarketData stores lend market data +type LendMarketData struct { + DailyIntRate float64 `json:"dailyIntRate,string"` + Term int64 `json:"term"` + Size float64 `json:"size,string"` +} + +// MarginTradeData stores margin trade data +type MarginTradeData struct { + TradeID string `json:"tradeId"` + Currency string `json:"currency"` + Size float64 `json:"size,string"` + DailyIntRate float64 `json:"dailyIntRate,string"` + Term int64 `json:"term"` + Timestamp convert.ExchangeTime `json:"timestamp"` +} + +// IsolatedMarginPairConfig current isolated margin trading pair configuration +type IsolatedMarginPairConfig struct { + Symbol string `json:"symbol"` + SymbolName string `json:"symbolName"` + BaseCurrency string `json:"baseCurrency"` + QuoteCurrency string `json:"quoteCurrency"` + MaxLeverage int64 `json:"maxLeverage"` + LiquidationDebtRatio float64 `json:"flDebtRatio,string"` + TradeEnable bool `json:"tradeEnable"` + AutoRenewMaxDebtRatio float64 `json:"autoRenewMaxDebtRatio,string"` + BaseBorrowEnable bool `json:"baseBorrowEnable"` + QuoteBorrowEnable bool `json:"quoteBorrowEnable"` + BaseTransferInEnable bool `json:"baseTransferInEnable"` + QuoteTransferInEnable bool `json:"quoteTransferInEnable"` +} + +type baseAsset struct { + Currency string `json:"currency"` + TotalBalance float64 `json:"totalBalance,string"` + HoldBalance float64 `json:"holdBalance,string"` + AvailableBalance float64 `json:"availableBalance,string"` + Liability float64 `json:"liability,string"` + Interest float64 `json:"interest,string"` + BorrowableAmount float64 `json:"borrowableAmount,string"` +} + +// AssetInfo holds asset information for an instrument. +type AssetInfo struct { + Symbol string `json:"symbol"` + Status string `json:"status"` + DebtRatio float64 `json:"debtRatio,string"` + BaseAsset baseAsset `json:"baseAsset"` + QuoteAsset baseAsset `json:"quoteAsset"` +} + +// IsolatedMarginAccountInfo holds isolated margin accounts of the current user +type IsolatedMarginAccountInfo struct { + TotalConversionBalance float64 `json:"totalConversionBalance,string"` + LiabilityConversionBalance float64 `json:"liabilityConversionBalance,string"` + Assets []AssetInfo `json:"assets"` +} + +type baseRepaymentRecord struct { + LoanID string `json:"loanId"` + Symbol string `json:"symbol"` + Currency string `json:"currency"` + PrincipalTotal float64 `json:"principalTotal,string"` + InterestBalance float64 `json:"interestBalance,string"` + CreatedAt convert.ExchangeTime `json:"createdAt"` + Period int64 `json:"period"` + RepaidSize float64 `json:"repaidSize,string"` + DailyInterestRate float64 `json:"dailyInterestRate,string"` +} + +// OutstandingRepaymentRecordsResponse represents an outstanding repayment records of isolated margin positions list +type OutstandingRepaymentRecordsResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []OutstandingRepaymentRecord `json:"items"` +} + +// OutstandingRepaymentRecord represents an outstanding repayment records of isolated margin positions +type OutstandingRepaymentRecord struct { + baseRepaymentRecord + LiabilityBalance float64 `json:"liabilityBalance,string"` + MaturityTime convert.ExchangeTime `json:"maturityTime"` +} + +// ServiceStatus represents a service status message. +type ServiceStatus struct { + Status string `json:"status"` + Message string `json:"msg"` +} + +// CompletedRepaymentRecordsResponse represents a completed payment records list. +type CompletedRepaymentRecordsResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []CompletedRepaymentRecord `json:"items"` +} + +// CompletedRepaymentRecord represents repayment records of isolated margin positions +type CompletedRepaymentRecord struct { + baseRepaymentRecord + RepayFinishAt convert.ExchangeTime `json:"repayFinishAt"` +} + +// PostMarginOrderResp represents response data for placing margin orders +type PostMarginOrderResp struct { + OrderID string `json:"orderId"` + BorrowSize float64 `json:"borrowSize"` + LoanApplyID string `json:"loanApplyId"` +} + +// OrderRequest represents place order request parameters +type OrderRequest struct { + ClientOID string `json:"clientOid"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Type string `json:"type,omitempty"` // optional + Remark string `json:"remark,omitempty"` // optional + Stop string `json:"stop,omitempty"` // optional + StopPrice float64 `json:"stopPrice,string,omitempty"` // optional + STP string `json:"stp,omitempty"` // optional + Price float64 `json:"price,string,omitempty"` + Size float64 `json:"size,string,omitempty"` + TimeInForce string `json:"timeInForce,omitempty"` // optional + CancelAfter int64 `json:"cancelAfter,omitempty"` // optional + PostOnly bool `json:"postOnly,omitempty"` // optional + Hidden bool `json:"hidden,omitempty"` // optional + Iceberg bool `json:"iceberg,omitempty"` // optional + VisibleSize string `json:"visibleSize,omitempty"` // optional +} + +// PostBulkOrderResp response data for submitting a bulk order +type PostBulkOrderResp struct { + OrderRequest + Channel string `json:"channel"` + ID string `json:"id"` + Status string `json:"status"` + FailMsg string `json:"failMsg"` +} + +// OrdersListResponse represents an order list response. +type OrdersListResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []OrderDetail `json:"items"` +} + +// OrderDetail represents order detail +type OrderDetail struct { + OrderRequest + Channel string `json:"channel"` + ID string `json:"id"` + OperationType string `json:"opType"` // operation type: DEAL + Funds string `json:"funds"` + DealFunds string `json:"dealFunds"` + DealSize float64 `json:"dealSize,string"` + Fee float64 `json:"fee,string"` + FeeCurrency string `json:"feeCurrency"` + StopTriggered bool `json:"stopTriggered"` + Tags string `json:"tags"` + IsActive bool `json:"isActive"` + CancelExist bool `json:"cancelExist"` + CreatedAt convert.ExchangeTime `json:"createdAt"` + TradeType string `json:"tradeType"` +} + +// ListFills represents fills response list detail. +type ListFills struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNumber int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []Fill `json:"items"` +} + +// Fill represents order fills for margin and spot orders. +type Fill struct { + Symbol string `json:"symbol"` + TradeID string `json:"tradeId"` + OrderID string `json:"orderId"` + CounterOrderID string `json:"counterOrderId"` + Side string `json:"side"` + Liquidity string `json:"liquidity"` + ForceTaker bool `json:"forceTaker"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + Funds float64 `json:"funds,string"` + Fee float64 `json:"fee,string"` + FeeRate float64 `json:"feeRate,string"` + FeeCurrency string `json:"feeCurrency"` + Stop string `json:"stop"` + OrderType string `json:"type"` + CreatedAt convert.ExchangeTime `json:"createdAt"` + TradeType string `json:"tradeType"` +} + +// StopOrderListResponse represents a list of spot orders details. +type StopOrderListResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNumber int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []StopOrder `json:"items"` +} + +// StopOrder holds a stop order detail +type StopOrder struct { + OrderRequest + ID string `json:"id"` + UserID string `json:"userId"` + Status string `json:"status"` + Funds float64 `json:"funds,string"` + Channel string `json:"channel"` + Tags string `json:"tags"` + DomainID string `json:"domainId"` + TradeSource string `json:"tradeSource"` + TradeType string `json:"tradeType"` + FeeCurrency string `json:"feeCurrency"` + TakerFeeRate string `json:"takerFeeRate"` + MakerFeeRate string `json:"makerFeeRate"` + CreatedAt convert.ExchangeTime `json:"createdAt"` + OrderTime convert.ExchangeTime `json:"orderTime"` + StopTriggerTime convert.ExchangeTime `json:"stopTriggerTime"` +} + +type baseAccount struct { + Currency string `json:"currency"` + Balance float64 `json:"balance,string"` + Available float64 `json:"available,string"` + Holds float64 `json:"holds,string"` +} + +// AccountInfo represents account information +type AccountInfo struct { + baseAccount + ID string `json:"id"` + Type string `json:"type"` +} + +// LedgerInfo represents account ledger information. +type LedgerInfo struct { + ID string `json:"id"` + Currency string `json:"currency"` + Amount float64 `json:"amount,string"` + Fee float64 `json:"fee,string"` + Balance float64 `json:"balance,string"` + AccountType string `json:"accountType"` + BizType string `json:"bizType"` + Direction string `json:"direction"` + CreatedAt convert.ExchangeTime `json:"createdAt"` + Context string `json:"context"` +} + +// MainAccountInfo represents main account detailed information. +type MainAccountInfo struct { + baseAccount + BaseCurrency string `json:"baseCurrency"` + BaseCurrencyPrice float64 `json:"baseCurrencyPrice,string"` + BaseAmount float64 `json:"baseAmount,string"` +} + +// AccountSummaryInformation represents account summary information detail. +type AccountSummaryInformation struct { + Data struct { + Level float64 `json:"level"` + SubQuantity float64 `json:"subQuantity"` + MaxDefaultSubQuantity float64 `json:"maxDefaultSubQuantity"` + MaxSubQuantity float64 `json:"maxSubQuantity"` + SpotSubQuantity float64 `json:"spotSubQuantity"` + MarginSubQuantity float64 `json:"marginSubQuantity"` + FuturesSubQuantity float64 `json:"futuresSubQuantity"` + MaxSpotSubQuantity float64 `json:"maxSpotSubQuantity"` + MaxMarginSubQuantity float64 `json:"maxMarginSubQuantity"` + MaxFuturesSubQuantity float64 `json:"maxFuturesSubQuantity"` + } `json:"data"` + Code string `json:"code"` +} + +// SubAccountsResponse represents a sub-accounts items response instance. +type SubAccountsResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNumber int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []SubAccountInfo `json:"items"` +} + +// SubAccountInfo holds subaccount data for main, spot(trade), and margin accounts. +type SubAccountInfo struct { + SubUserID string `json:"subUserId"` + SubName string `json:"subName"` + MainAccounts []MainAccountInfo `json:"mainAccounts"` + TradeAccounts []MainAccountInfo `json:"tradeAccounts"` + MarginAccounts []MainAccountInfo `json:"marginAccounts"` +} + +// TransferableBalanceInfo represents transferable balance information +type TransferableBalanceInfo struct { + baseAccount + Transferable float64 `json:"transferable,string"` +} + +// DepositAddress represents deposit address information for Spot and Margin trading. +type DepositAddress struct { + Address string `json:"address"` + Memo string `json:"memo"` + Chain string `json:"chain"` + ContractAddress string `json:"contractAddress"` // missing in case of futures +} + +type baseDeposit struct { + Currency string `json:"currency"` + WalletTxID string `json:"walletTxId"` + IsInner bool `json:"isInner"` + Status string `json:"status"` +} + +// DepositResponse represents a detailed response for list of deposit. +type DepositResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []Deposit `json:"items"` +} + +// Deposit represents deposit address and detail and timestamp information. +type Deposit struct { + baseDeposit + Amount float64 `json:"amount,string"` + Address string `json:"address"` + Memo string `json:"memo"` + Fee float64 `json:"fee,string"` + Remark string `json:"remark"` + CreatedAt convert.ExchangeTime + UpdatedAt convert.ExchangeTime +} + +// HistoricalDepositWithdrawalResponse represents deposit and withdrawal funding items details. +type HistoricalDepositWithdrawalResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []HistoricalDepositWithdrawal `json:"items"` +} + +// HistoricalDepositWithdrawal represents deposit and withdrawal funding item. +type HistoricalDepositWithdrawal struct { + baseDeposit + Amount float64 `json:"amount,string"` + CreatedAt convert.ExchangeTime `json:"createAt"` +} + +// WithdrawalsResponse represents a withdrawals list of items details. +type WithdrawalsResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []Withdrawal `json:"items"` +} + +// Withdrawal represents withdrawal funding information. +type Withdrawal struct { + Deposit + ID string `json:"id"` +} + +// WithdrawalQuota represents withdrawal quota detail information. +type WithdrawalQuota struct { + Currency string `json:"currency"` + LimitBTCAmount float64 `json:"limitBTCAmount,string"` + UsedBTCAmount float64 `json:"usedBTCAmount,string"` + RemainAmount float64 `json:"remainAmount,string"` + AvailableAmount float64 `json:"availableAmount,string"` + WithdrawMinFee float64 `json:"withdrawMinFee,string"` + InnerWithdrawMinFee float64 `json:"innerWithdrawMinFee,string"` + WithdrawMinSize float64 `json:"withdrawMinSize,string"` + IsWithdrawEnabled bool `json:"isWithdrawEnabled"` + Precision int64 `json:"precision"` + Chain string `json:"chain"` +} + +// Fees represents taker and maker fee information a symbol. +type Fees struct { + Symbol string `json:"symbol"` + TakerFeeRate float64 `json:"takerFeeRate,string"` + MakerFeeRate float64 `json:"makerFeeRate,string"` +} + +// WSInstanceServers response connection token and websocket instance server information. +type WSInstanceServers struct { + Token string `json:"token"` + InstanceServers []InstanceServer `json:"instanceServers"` +} + +// InstanceServer represents a single websocket instance server information. +type InstanceServer struct { + Endpoint string `json:"endpoint"` + Encrypt bool `json:"encrypt"` + Protocol string `json:"protocol"` + PingInterval int64 `json:"pingInterval"` + PingTimeout int64 `json:"pingTimeout"` +} + +// WSConnMessages represents response messages ping, pong, and welcome message structures. +type WSConnMessages struct { + ID string `json:"id"` + Type string `json:"type"` +} + +// WsSubscriptionInput represents a subscription information structure. +type WsSubscriptionInput struct { + ID string `json:"id"` + Type string `json:"type"` + Topic string `json:"topic"` + PrivateChannel bool `json:"privateChannel"` + Response bool `json:"response,omitempty"` +} + +// WsPushData represents a push data from a server. +type WsPushData struct { + ID string `json:"id"` + Type string `json:"type"` + Topic string `json:"topic"` + UserID string `json:"userId"` + Subject string `json:"subject"` + ChannelType string `json:"channelType"` + Data json.RawMessage `json:"data"` +} + +// WsTicker represents a ticker push data from server. +type WsTicker struct { + Sequence string `json:"sequence"` + BestAsk float64 `json:"bestAsk,string"` + Size float64 `json:"size,string"` + BestBidSize float64 `json:"bestBidSize,string"` + Price float64 `json:"price,string"` + BestAskSize float64 `json:"bestAskSize,string"` + BestBid float64 `json:"bestBid,string"` + Timestamp convert.ExchangeTime `json:"time"` +} + +// WsSpotTicker represents a spot ticker push data. +type WsSpotTicker struct { + Sequence kucoinNumber `json:"sequence"` + Data WsSpotTickerDetail `json:"data"` +} + +// WsSpotTickerDetail represents the detail of a spot ticker data. +// This represents all websocket ticker information pushed as a result of susbcription to /market/snapshot:{symbol}, and /market/snapshot:{currency,market} +type WsSpotTickerDetail struct { + AveragePrice float64 `json:"averagePrice"` + BaseCurrency string `json:"baseCurrency"` + Board int64 `json:"board"` + Buy float64 `json:"buy"` + ChangePrice float64 `json:"changePrice"` + ChangeRate float64 `json:"changeRate"` + Close float64 `json:"close"` + Datetime convert.ExchangeTime `json:"datetime"` + High float64 `json:"high"` + LastTradedPrice float64 `json:"lastTradedPrice"` + Low float64 `json:"low"` + MakerCoefficient float64 `json:"makerCoefficient"` + MakerFeeRate float64 `json:"makerFeeRate"` + MarginTrade bool `json:"marginTrade"` + Mark float64 `json:"mark"` + Market string `json:"market"` + Markets []string `json:"markets"` + Open float64 `json:"open"` + QuoteCurrency string `json:"quoteCurrency"` + Sell float64 `json:"sell"` + Sort int64 `json:"sort"` + Symbol string `json:"symbol"` + SymbolCode string `json:"symbolCode"` + TakerCoefficient float64 `json:"takerCoefficient"` + TakerFeeRate float64 `json:"takerFeeRate"` + Trading bool `json:"trading"` + Vol float64 `json:"vol"` + VolValue float64 `json:"volValue"` +} + +// WsOrderbook represents orderbook information. +type WsOrderbook struct { + Changes OrderbookChanges `json:"changes"` + SequenceEnd int64 `json:"sequenceEnd"` + SequenceStart int64 `json:"sequenceStart"` + Symbol string `json:"symbol"` + TimeMS convert.ExchangeTime `json:"time"` +} + +// OrderbookChanges represents orderbook ask and bid changes. +type OrderbookChanges struct { + Asks [][]string `json:"asks"` + Bids [][]string `json:"bids"` +} + +// WsOrderbookDepth represents orderbook information. +type WsOrderbookDepth struct { + Asks [][]string `json:"asks"` + Bids [][]string `json:"bids"` + Symbol string `json:"symbol"` + TimeMS convert.ExchangeTime `json:"timestamp"` +} + +// WsCandlestickData represents candlestick information push data for a symbol. +type WsCandlestickData struct { + Symbol string `json:"symbol"` + Candles [7]string `json:"candles"` + Time convert.ExchangeTime `json:"time"` +} + +// WsCandlestick represents candlestick information push data for a symbol. +type WsCandlestick struct { + Symbol string `json:"symbol"` + Candles struct { + StartTime time.Time + OpenPrice float64 + ClosePrice float64 + HighPrice float64 + LowPrice float64 + TransactionVolume float64 + TransactionAmount float64 + } `json:"candles"` + Time time.Time `json:"time"` +} + +func (a *WsCandlestickData) getCandlestickData() (*WsCandlestick, error) { + cand := &WsCandlestick{ + Symbol: a.Symbol, + Time: a.Time.Time(), + } + timeStamp, err := strconv.ParseInt(a.Candles[0], 10, 64) + if err != nil { + return nil, err + } + cand.Candles.StartTime = time.UnixMilli(timeStamp) + cand.Candles.OpenPrice, err = strconv.ParseFloat(a.Candles[1], 64) + if err != nil { + return nil, err + } + cand.Candles.ClosePrice, err = strconv.ParseFloat(a.Candles[2], 64) + if err != nil { + return nil, err + } + cand.Candles.HighPrice, err = strconv.ParseFloat(a.Candles[3], 64) + if err != nil { + return nil, err + } + cand.Candles.LowPrice, err = strconv.ParseFloat(a.Candles[4], 64) + if err != nil { + return nil, err + } + cand.Candles.TransactionVolume, err = strconv.ParseFloat(a.Candles[5], 64) + if err != nil { + return nil, err + } + cand.Candles.TransactionAmount, err = strconv.ParseFloat(a.Candles[6], 64) + if err != nil { + return nil, err + } + return cand, nil +} + +// WsTrade represents a trade push data. +type WsTrade struct { + Sequence string `json:"sequence"` + Type string `json:"type"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + TradeID string `json:"tradeId"` + TakerOrderID string `json:"takerOrderId"` + MakerOrderID string `json:"makerOrderId"` + Time convert.ExchangeTime `json:"time"` +} + +// WsPriceIndicator represents index price or mark price indicator push data. +type WsPriceIndicator struct { + Symbol string `json:"symbol"` + Granularity float64 `json:"granularity"` + Timestamp convert.ExchangeTime `json:"timestamp"` + Value float64 `json:"value"` +} + +// WsMarginFundingBook represents order book changes on margin. +type WsMarginFundingBook struct { + Sequence int64 `json:"sequence"` + Currency string `json:"currency"` + DailyInterestRate float64 `json:"dailyIntRate"` + AnnualInterestRate float64 `json:"annualIntRate"` + Term int64 `json:"term"` + Size float64 `json:"size"` + Side string `json:"side"` + Timestamp convert.ExchangeTime `json:"ts"` // In Nanosecond + +} + +// WsTradeOrder represents a private trade order push data. +type WsTradeOrder struct { + Symbol string `json:"symbol"` + OrderType string `json:"orderType"` + Side string `json:"side"` + OrderID string `json:"orderId"` + Type string `json:"type"` + OrderTime convert.ExchangeTime `json:"orderTime"` + Size float64 `json:"size,string"` + FilledSize float64 `json:"filledSize,string"` + Price float64 `json:"price,string"` + ClientOid string `json:"clientOid"` + RemainSize float64 `json:"remainSize,string"` + Status string `json:"status"` + Timestamp convert.ExchangeTime `json:"ts"` + Liquidity string `json:"liquidity"` + MatchPrice string `json:"matchPrice"` + MatchSize string `json:"matchSize"` + TradeID string `json:"tradeId"` + OldSize string `json:"oldSize"` +} + +// WsAccountBalance represents a Account Balance push data. +type WsAccountBalance struct { + Total float64 `json:"total,string"` + Available float64 `json:"available,string"` + AvailableChange float64 `json:"availableChange,string"` + Currency string `json:"currency"` + Hold float64 `json:"hold,string"` + HoldChange float64 `json:"holdChange,string"` + RelationEvent string `json:"relationEvent"` + RelationEventID string `json:"relationEventId"` + RelationContext struct { + Symbol string `json:"symbol"` + TradeID string `json:"tradeId"` + OrderID string `json:"orderId"` + } `json:"relationContext"` + Time convert.ExchangeTime `json:"time"` +} + +// WsDebtRatioChange represents a push data +type WsDebtRatioChange struct { + DebtRatio float64 `json:"debtRatio"` + TotalDebt float64 `json:"totalDebt,string"` + DebtList map[string]string `json:"debtList"` + Timestamp convert.ExchangeTime `json:"timestamp"` +} + +// WsPositionStatus represents a position status push data. +type WsPositionStatus struct { + Type string `json:"type"` + TimestampMS convert.ExchangeTime `json:"timestamp"` +} + +// WsMarginTradeOrderEntersEvent represents a push data to the lenders +// when the order enters the order book or when the order is executed. +type WsMarginTradeOrderEntersEvent struct { + Currency string `json:"currency"` + OrderID string `json:"orderId"` // Trade ID + DailyIntRate float64 `json:"dailyIntRate"` // Daily interest rate. + Term int64 `json:"term"` // Term (Unit: Day) + Size float64 `json:"size"` // Size + LentSize float64 `json:"lentSize"` // Size executed -- filled when the subject is order.update + Side string `json:"side"` // Lend or borrow. Currently, only "Lend" is available + Timestamp convert.ExchangeTime `json:"ts"` // Timestamp (nanosecond) +} + +// WsMarginTradeOrderDoneEvent represents a push message to the lenders when the order is completed. +type WsMarginTradeOrderDoneEvent struct { + Currency string `json:"currency"` + OrderID string `json:"orderId"` + Reason string `json:"reason"` + Side string `json:"side"` + Timestamp convert.ExchangeTime `json:"ts"` +} + +// WsStopOrder represents a stop order. +// When a stop order is received by the system, you will receive a message with "open" type. +// It means that this order entered the system and waited to be triggered. +type WsStopOrder struct { + CreatedAt convert.ExchangeTime `json:"createdAt"` + OrderID string `json:"orderId"` + OrderPrice float64 `json:"orderPrice,string"` + OrderType string `json:"orderType"` + Side string `json:"side"` + Size float64 `json:"size,string"` + Stop string `json:"stop"` + StopPrice float64 `json:"stopPrice,string"` + Symbol string `json:"symbol"` + TradeType string `json:"tradeType"` + TriggerSuccess bool `json:"triggerSuccess"` + Timestamp convert.ExchangeTime `json:"ts"` + Type string `json:"type"` +} + +// WsFuturesTicker represents a futures ticker push data. +type WsFuturesTicker struct { + Symbol string `json:"symbol"` + Sequence int64 `json:"sequence"` + Side string `json:"side"` + FilledPrice float64 `json:"price"` + FilledSize float64 `json:"size"` + TradeID string `json:"tradeId"` + BestBidSize float64 `json:"bestBidSize"` + BestBidPrice kucoinNumber `json:"bestBidPrice"` + BestAskPrice kucoinNumber `json:"bestAskPrice"` + BestAskSize float64 `json:"bestAskSize"` + FilledTime convert.ExchangeTime `json:"ts"` +} + +// WsFuturesOrderbokInfo represents Level 2 order book information. +type WsFuturesOrderbokInfo struct { + Sequence int64 `json:"sequence"` + Change string `json:"change"` + Timestamp convert.ExchangeTime `json:"timestamp"` +} + +// WsFuturesExecutionData represents execution data for symbol. +type WsFuturesExecutionData struct { + Sequence int64 `json:"sequence"` + FilledQuantity float64 `json:"matchSize"` // Filled quantity + UnfilledQuantity float64 `json:"size"` + FilledPrice float64 `json:"price"` + TradeID string `json:"tradeId"` + MakerUserID string `json:"makerUserId"` + Symbol string `json:"symbol"` + Side string `json:"side"` + TakerOrderID string `json:"takerOrderId"` + MakerOrderID string `json:"makerOrderId"` + TakerUserID string `json:"takerUserId"` + Timestamp convert.ExchangeTime `json:"ts"` +} + +// WsOrderbookLevel5 represents an orderbook push data with depth level 5. +type WsOrderbookLevel5 struct { + Sequence int64 `json:"sequence"` + Asks []orderbook.Item `json:"asks"` + Bids []orderbook.Item `json:"bids"` + PushTimestamp convert.ExchangeTime `json:"ts"` + Timestamp convert.ExchangeTime `json:"timestamp"` +} + +// WsOrderbookLevel5Response represents a response data for an orderbook push data with depth level 5. +type WsOrderbookLevel5Response struct { + Timestamp convert.ExchangeTime `json:"timestamp"` + Sequence int64 `json:"sequence"` + Bids [][2]kucoinNumber `json:"bids"` + Asks [][2]kucoinNumber `json:"asks"` + PushTimestamp convert.ExchangeTime `json:"ts"` +} + +// ExtractOrderbookItems returns WsOrderbookLevel5 instance from WsOrderbookLevel5Response +func (a *WsOrderbookLevel5Response) ExtractOrderbookItems() *WsOrderbookLevel5 { + resp := WsOrderbookLevel5{ + Timestamp: a.Timestamp, + Sequence: a.Sequence, + PushTimestamp: a.PushTimestamp, + } + resp.Asks = make([]orderbook.Item, len(a.Asks)) + for x := range a.Asks { + resp.Asks[x] = orderbook.Item{ + Price: a.Asks[x][0].Float64(), + Amount: a.Asks[x][1].Float64(), + } + } + resp.Bids = make([]orderbook.Item, len(a.Bids)) + for x := range a.Bids { + resp.Bids[x] = orderbook.Item{ + Price: a.Bids[x][0].Float64(), + Amount: a.Bids[x][1].Float64(), + } + } + return &resp +} + +// WsFundingRate represents the funding rate push data information through the websocket channel. +type WsFundingRate struct { + Symbol string `json:"symbol"` + Granularity int64 `json:"granularity"` + FundingRate float64 `json:"fundingRate"` + Timestamp convert.ExchangeTime `json:"timestamp"` +} + +// WsFuturesMarkPriceAndIndexPrice represents mark price and index price information. +type WsFuturesMarkPriceAndIndexPrice struct { + Symbol string `json:"symbol"` + Granularity int64 `json:"granularity"` + IndexPrice float64 `json:"indexPrice"` + MarkPrice float64 `json:"markPrice"` + Timestamp convert.ExchangeTime `json:"timestamp"` +} + +// WsFuturesFundingBegin represents the Start Funding Fee Settlement. +type WsFuturesFundingBegin struct { + Subject string `json:"subject"` + Symbol string `json:"symbol"` + FundingTime convert.ExchangeTime `json:"fundingTime"` + FundingRate float64 `json:"fundingRate"` + Timestamp convert.ExchangeTime `json:"timestamp"` +} + +// WsFuturesTransactionStatisticsTimeEvent represents transaction statistics data. +type WsFuturesTransactionStatisticsTimeEvent struct { + Symbol string `json:"symbol"` + Volume24H float64 `json:"volume"` + Turnover24H float64 `json:"turnover"` + LastPrice int64 `json:"lastPrice"` + PriceChangePercentage24H float64 `json:"priceChgPct"` + SnapshotTime convert.ExchangeTime `json:"ts"` +} + +// WsFuturesTradeOrder represents trade order information according to the market. +type WsFuturesTradeOrder struct { + OrderID string `json:"orderId"` + Symbol string `json:"symbol"` + Type string `json:"type"` // Message Type: "open", "match", "filled", "canceled", "update" + Status string `json:"status"` // Order Status: "match", "open", "done" + MatchSize string `json:"matchSize"` // Match Size (when the type is "match") + MatchPrice string `json:"matchPrice"` // Match Price (when the type is "match") + OrderType string `json:"orderType"` // Order Type, "market" indicates market order, "limit" indicates limit order + Side string `json:"side"` // Trading direction,include buy and sell + OrderPrice float64 `json:"price,string"` + OrderSize float64 `json:"size,string"` + RemainSize float64 `json:"remainSize,string"` + FilledSize float64 `json:"filledSize,string"` // Remaining Size for Trading + CanceledSize float64 `json:"canceledSize,string"` // In the update message, the Size of order reduced + TradeID string `json:"tradeId"` // Trade ID (when the type is "match") + ClientOid string `json:"clientOid"` // Client supplied order id. + OrderTime convert.ExchangeTime `json:"orderTime"` + OldSize string `json:"oldSize "` // Size Before Update (when the type is "update") + TradingDirection string `json:"liquidity"` // Liquidity, Trading direction, buy or sell in taker + Timestamp convert.ExchangeTime `json:"ts"` +} + +// WsStopOrderLifecycleEvent represents futures stop order lifecycle event. +type WsStopOrderLifecycleEvent struct { + OrderID string `json:"orderId"` + Symbol string `json:"symbol"` + Type string `json:"type"` + OrderType string `json:"orderType"` + Side string `json:"side"` + Size float64 `json:"size,string"` + OrderPrice float64 `json:"orderPrice,string"` + Stop string `json:"stop"` + StopPrice float64 `json:"stopPrice,string"` + StopPriceType string `json:"stopPriceType"` + TriggerSuccess bool `json:"triggerSuccess"` + Error string `json:"error"` + CreatedAt convert.ExchangeTime `json:"createdAt"` + Timestamp convert.ExchangeTime `json:"ts"` +} + +// WsFuturesOrderMarginEvent represents an order margin account balance event. +type WsFuturesOrderMarginEvent struct { + OrderMargin float64 `json:"orderMargin"` + Currency string `json:"currency"` + Timestamp convert.ExchangeTime `json:"timestamp"` +} + +// WsFuturesAvailableBalance represents an available balance push data for futures account. +type WsFuturesAvailableBalance struct { + AvailableBalance float64 `json:"availableBalance"` + HoldBalance float64 `json:"holdBalance"` + Currency string `json:"currency"` + Timestamp convert.ExchangeTime `json:"timestamp"` +} + +// WsFuturesWithdrawalAmountAndTransferOutAmountEvent represents Withdrawal Amount & Transfer-Out Amount Event push data. +type WsFuturesWithdrawalAmountAndTransferOutAmountEvent struct { + WithdrawHold float64 `json:"withdrawHold"` // Current frozen amount for withdrawal + Currency string `json:"currency"` + Timestamp convert.ExchangeTime `json:"timestamp"` +} + +// WsFuturesPosition represents futures account position change event. +type WsFuturesPosition struct { + RealisedGrossPnl float64 `json:"realisedGrossPnl"` // Accumulated realised profit and loss + Symbol string `json:"symbol"` + CrossMode bool `json:"crossMode"` // Cross mode or not + LiquidationPrice float64 `json:"liquidationPrice"` // Liquidation price + PosLoss float64 `json:"posLoss"` // Manually added margin amount + AvgEntryPrice float64 `json:"avgEntryPrice"` // Average entry price + UnrealisedPnl float64 `json:"unrealisedPnl"` // Unrealised profit and loss + MarkPrice float64 `json:"markPrice"` // Mark price + PosMargin float64 `json:"posMargin"` // Position margin + AutoDeposit bool `json:"autoDeposit"` // Auto deposit margin or not + RiskLimit float64 `json:"riskLimit"` + UnrealisedCost float64 `json:"unrealisedCost"` // Unrealised value + PosComm float64 `json:"posComm"` // Bankruptcy cost + PosMaint float64 `json:"posMaint"` // Maintenance margin + PosCost float64 `json:"posCost"` // Position value + MaintMarginReq float64 `json:"maintMarginReq"` // Maintenance margin rate + BankruptPrice float64 `json:"bankruptPrice"` // Bankruptcy price + RealisedCost float64 `json:"realisedCost"` // Currently accumulated realised position value + MarkValue float64 `json:"markValue"` // Mark value + PosInit float64 `json:"posInit"` // Position margin + RealisedPnl float64 `json:"realisedPnl"` // Realised profit and loss + MaintMargin float64 `json:"maintMargin"` // Position margin + RealLeverage float64 `json:"realLeverage"` // Leverage of the order + ChangeReason string `json:"changeReason"` // changeReason:marginChange、positionChange、liquidation、autoAppendMarginStatusChange、adl + CurrentCost float64 `json:"currentCost"` // Current position value + OpeningTimestamp convert.ExchangeTime `json:"openingTimestamp"` // Open time + CurrentQty float64 `json:"currentQty"` // Current position + DelevPercentage float64 `json:"delevPercentage"` // ADL ranking percentile + CurrentComm float64 `json:"currentComm"` // Current commission + RealisedGrossCost float64 `json:"realisedGrossCost"` // Accumulated realised gross profit value + IsOpen bool `json:"isOpen"` // Opened position or not + PosCross float64 `json:"posCross"` // Manually added margin + CurrentTimestamp convert.ExchangeTime `json:"currentTimestamp"` // Current timestamp + UnrealisedRoePcnt float64 `json:"unrealisedRoePcnt"` // Rate of return on investment + UnrealisedPnlPcnt float64 `json:"unrealisedPnlPcnt"` // Position profit and loss ratio + SettleCurrency string `json:"settleCurrency"` // Currency used to clear and settle the trades +} + +// WsFuturesMarkPricePositionChanges represents futures account position change caused by mark price. +type WsFuturesMarkPricePositionChanges struct { + MarkPrice float64 `json:"markPrice"` // Mark price + MarkValue float64 `json:"markValue"` // Mark value + MaintMargin float64 `json:"maintMargin"` // Position margin + RealLeverage float64 `json:"realLeverage"` // Leverage of the order + UnrealisedPnl float64 `json:"unrealisedPnl"` // Unrealised profit and lost + UnrealisedRoePcnt float64 `json:"unrealisedRoePcnt"` // Rate of return on investment + UnrealisedPnlPcnt float64 `json:"unrealisedPnlPcnt"` // Position profit and loss ratio + DelevPercentage float64 `json:"delevPercentage"` // ADL ranking percentile + CurrentTimestamp convert.ExchangeTime `json:"currentTimestamp"` // Current timestamp + SettleCurrency string `json:"settleCurrency"` // Currency used to clear and settle the trades +} + +// WsFuturesPositionFundingSettlement represents futures account position funding settlement push data. +type WsFuturesPositionFundingSettlement struct { + PositionSize float64 `json:"qty"` + MarkPrice float64 `json:"markPrice"` + FundingRate float64 `json:"fundingRate"` + FundingFee float64 `json:"fundingFee"` + FundingTime convert.ExchangeTime `json:"fundingTime"` + CurrentTimestamp convert.ExchangeTime `json:"ts"` + SettleCurrency string `json:"settleCurrency"` +} + +// IsolatedMarginBorrowing represents response data for initiating isolated margin borrowing. +type IsolatedMarginBorrowing struct { + OrderID string `json:"orderId"` + Currency string `json:"currency"` + ActualSize float64 `json:"actualSize,string"` +} + +// Response represents response model and implements UnmarshalTo interface. +type Response struct { + Data interface{} `json:"data"` + Error +} + +// CancelOrderResponse represents cancel order response model. +type CancelOrderResponse struct { + CancelledOrderID string `json:"cancelledOrderId"` + ClientOID string `json:"clientOid"` + Error +} + +// AccountLedgerResponse represents the account ledger response detailed information +type AccountLedgerResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []LedgerInfo `json:"items"` +} + +// SpotAPISubAccountParams parameters for Spot APIs for sub-accounts +type SpotAPISubAccountParams struct { + SubAccountName string `json:"subName"` + Passphrase string `json:"passphrase"` + Remark string `json:"remark"` + Permission string `json:"permission,omitempty"` // Permissions(Only "General" and "Trade" permissions can be set, such as "General, Trade". The default is "General") + IPWhitelist string `json:"ipWhitelist,omitempty"` // IP whitelist(You may add up to 20 IPs. Use a halfwidth comma to each IP) + Expire int64 `json:"expire,string,omitempty"` // API expiration time; Never expire(default)-1,30Day30,90Day90,180Day180,360Day360 +} + +// SubAccountResponse represents the sub-user detail. +type SubAccountResponse struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + Items []SubAccount `json:"items"` +} + +// SubAccount represents sub-user +type SubAccount struct { + UserID string `json:"userId"` + SubName string `json:"subName"` + Type int64 `json:"type"` //type:1-rebot or type:0-nomal + Remarks string `json:"remarks"` + UID int64 `json:"uid"` + Status int64 `json:"status"` + Access string `json:"access"` + CreatedAt convert.ExchangeTime `json:"createdAt"` +} + +// SubAccountCreatedResponse represents the sub-account response. +type SubAccountCreatedResponse struct { + UID int64 `json:"uid"` + SubName string `json:"subName"` + Remarks string `json:"remarks"` + Access string `json:"access"` +} + +// SpotAPISubAccount represents a Spot APIs for sub-accounts. +type SpotAPISubAccount struct { + SubName string `json:"subName"` + Remark string `json:"remark"` + APIKey string `json:"apiKey"` + APISecret string `json:"apiSecret"` + Passphrase string `json:"passphrase"` + Permission string `json:"permission"` + IPWhitelist string `json:"ipWhitelist"` + CreatedAt convert.ExchangeTime `json:"createdAt"` +} + +// DeleteSubAccountResponse represents delete sub-account response. +type DeleteSubAccountResponse struct { + SubAccountName string `json:"subName"` + APIKey string `json:"apiKey"` +} + +// ConnectionMessage represents a connection and subscription status message. +type ConnectionMessage struct { + ID string `json:"id"` + Type string `json:"type"` +} + +// TickersResponse represents list of tickers and update timestamp information. +type TickersResponse struct { + Time convert.ExchangeTime `json:"time"` + Tickers []TickerInfo `json:"ticker"` +} + +// FundingInterestRateResponse represents a funding interest rate list response information. +type FundingInterestRateResponse struct { + List []FuturesInterestRate `json:"dataList"` + HasMore bool `json:"hasMore"` +} + +// FuturesIndexResponse represents a response data for futures indexes. +type FuturesIndexResponse struct { + List []FuturesIndex `json:"dataList"` + HasMore bool `json:"hasMore"` +} + +// FuturesInterestRateResponse represents a futures interest rate list response. +type FuturesInterestRateResponse struct { + List []FuturesInterestRate `json:"dataList"` + HasMore bool `json:"hasMore"` +} + +// FuturesTransactionHistoryResponse represents a futures transaction history response. +type FuturesTransactionHistoryResponse struct { + List []FuturesTransactionHistory `json:"dataList"` + HasMore bool `json:"hasMore"` +} + +// FuturesFundingHistoryResponse represents funding history response for futures account. +type FuturesFundingHistoryResponse struct { + DataList []FuturesFundingHistory `json:"dataList"` + HasMore bool `json:"hasMore"` +} + +// FuturesOrderParam represents a query parameter for placing future oorder +type FuturesOrderParam struct { + ClientOrderID string `json:"clientOid"` + Side string `json:"side"` + Symbol currency.Pair `json:"symbol,omitempty"` + OrderType string `json:"type"` + Remark string `json:"remark,omitempty"` + Stop string `json:"stp,omitempty"` // [optional] Either `down` or `up`. Requires stopPrice and stopPriceType to be defined + StopPriceType string `json:"stopPriceType,omitempty"` // [optional] Either TP, IP or MP, Need to be defined if stop is specified. `TP` for trade price, `MP` for Mark price, and "IP" for index price. + TimeInForce string `json:"timeInForce,omitempty"` + Size float64 `json:"size,omitempty,string"` + Price float64 `json:"price,string,omitempty"` + StopPrice float64 `json:"stopPrice,omitempty,string"` + Leverage float64 `json:"leverage,omitempty,string"` + VisibleSize float64 `json:"visibleSize,omitempty,string"` + ReduceOnly bool `json:"reduceOnly,omitempty"` + CloseOrder bool `json:"closeOrder,omitempty"` + ForceHold bool `json:"forceHold,omitempty"` + PostOnly bool `json:"postOnly,omitempty"` + Hidden bool `json:"hidden,omitempty"` + Iceberg bool `json:"iceberg,omitempty"` +} + +// SpotOrderParam represents the spot place order request parameters. +type SpotOrderParam struct { + ClientOrderID string `json:"clientOid"` + Side string `json:"side"` + Symbol currency.Pair `json:"symbol"` + OrderType string `json:"type,omitempty"` + TradeType string `json:"tradeType,omitempty"` // [Optional] The type of trading : TRADE(Spot Trade), MARGIN_TRADE (Margin Trade). Default is TRADE. + Remark string `json:"remark,omitempty"` + SelfTradePrevention string `json:"stp,omitempty"` // [Optional] self trade prevention , CN, CO, CB or DC. `CN` for Cancel newest, `DC` for Decrease and Cancel, `CO` for cancel oldest, and `CB` for Cancel both + TimeInForce string `json:"timeInForce,omitempty"` // [Optional] GTC, GTT, IOC, or FOK (default is GTC) + PostOnly bool `json:"postOnly,omitempty"` + Hidden bool `json:"hidden,omitempty"` + Iceberg bool `json:"iceberg,omitempty"` + ReduceOnly bool `json:"reduceOnly,omitempty"` + CancelAfter int64 `json:"cancelAfter,omitempty"` + Size float64 `json:"size,omitempty,string"` + Price float64 `json:"price,string,omitempty"` + VisibleSize float64 `json:"visibleSize,omitempty,string"` + Funds float64 `json:"funds,string,omitempty"` +} + +// MarginOrderParam represents the margin place order request parameters. +type MarginOrderParam struct { + ClientOrderID string `json:"clientOid"` + Side string `json:"side"` + Symbol currency.Pair `json:"symbol"` + OrderType string `json:"type,omitempty"` + TradeType string `json:"tradeType,omitempty"` // [Optional] The type of trading : TRADE(Spot Trade), MARGIN_TRADE (Margin Trade). Default is TRADE. + Remark string `json:"remark,omitempty"` + SelfTradePrevention string `json:"stp,omitempty"` // [Optional] self trade prevention , CN, CO, CB or DC. `CN` for Cancel newest, `DC` for Decrease and Cancel, `CO` for cancel oldest, and `CB` for Cancel both + MarginMode string `json:"marginModel,omitempty"` // [Optional] The type of trading, including cross (cross mode) and isolated (isolated mode). It is set at cross by default. + AutoBorrow bool `json:"autoBorrow,omitempty"` // [Optional] Auto-borrow to place order. The system will first borrow you funds at the optimal interest rate and then place an order for you. Currently autoBorrow parameter only supports cross mode, not isolated mode. When add this param, stop profit and stop loss are not supported + Size float64 `json:"size,omitempty,string"` + Price float64 `json:"price,string,omitempty"` + TimeInForce string `json:"timeInForce,omitempty"` // [Optional] GTC, GTT, IOC, or FOK (default is GTC) + CancelAfter int64 `json:"cancelAfter,omitempty"` // [Optional] cancel after n seconds, requires timeInForce to be GTT + PostOnly bool `json:"postOnly,omitempty"` + Hidden bool `json:"hidden,omitempty"` + Iceberg bool `json:"iceberg,omitempty"` + VisibleSize float64 `json:"visibleSize,omitempty,string"` + Funds float64 `json:"funds,string,omitempty"` +} diff --git a/exchanges/kucoin/kucoin_websocket.go b/exchanges/kucoin/kucoin_websocket.go new file mode 100644 index 00000000..67461522 --- /dev/null +++ b/exchanges/kucoin/kucoin_websocket.go @@ -0,0 +1,1912 @@ +package kucoin + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "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/orderbook" + "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/exchanges/trade" + "github.com/thrasher-corp/gocryptotrader/log" +) + +var fetchedFuturesSnapshotOrderbook map[string]bool + +const ( + publicBullets = "/v1/bullet-public" + privateBullets = "/v1/bullet-private" + + // spot channels + marketTickerChannel = "/market/ticker:%s" // /market/ticker:{symbol},{symbol}... + marketAllTickersChannel = "/market/ticker:all" + marketTickerSnapshotChannel = "/market/snapshot:%s" // /market/snapshot:{symbol} + marketTickerSnapshotForCurrencyChannel = "/market/snapshot:" // /market/snapshot:{market} <--- market represents a currency + marketOrderbookLevel2Channels = "/market/level2:%s" // /market/level2:{symbol},{symbol}... + marketOrderbookLevel2to5Channel = "/spotMarket/level2Depth5:%s" // /spotMarket/level2Depth5:{symbol},{symbol}... + marketOrderbokLevel2To50Channel = "/spotMarket/level2Depth50:%s" // /spotMarket/level2Depth50:{symbol},{symbol}... + marketCandlesChannel = "/market/candles:%s_%s" // /market/candles:{symbol}_{type} + marketMatchChannel = "/market/match:%s" // /market/match:{symbol},{symbol}... + indexPriceIndicatorChannel = "/indicator/index:%s" // /indicator/index:{symbol0},{symbol1}.. + markPriceIndicatorChannel = "/indicator/markPrice:%s" // /indicator/markPrice:{symbol0},{symbol1}... + marginFundingbookChangeChannel = "/margin/fundingBook:%s" // /margin/fundingBook:{currency0},{currency1}... + + // Private channel + + privateSpotTradeOrders = "/spotMarket/tradeOrders" + accountBalanceChannel = "/account/balance" + marginPositionChannel = "/margin/position" + marginLoanChannel = "/margin/loan:%s" // /margin/loan:{currency} + spotMarketAdvancedChannel = "/spotMarket/advancedOrders" + + // futures channels + + futuresTickerV2Channel = "/contractMarket/tickerV2:%s" // /contractMarket/tickerV2:{symbol} + futuresTickerChannel = "/contractMarket/ticker:%s" // /contractMarket/ticker:{symbol} + futuresOrderbookLevel2Channel = "/contractMarket/level2:%s" // /contractMarket/level2:{symbol} + futuresExecutionDataChannel = "/contractMarket/execution:%s" // /contractMarket/execution:{symbol} + futuresOrderbookLevel2Depth5Channel = "/contractMarket/level2Depth5:%s" // /contractMarket/level2Depth5:{symbol} + futuresOrderbookLevel2Depth50Channel = "/contractMarket/level2Depth50:%s" // /contractMarket/level2Depth50:{symbol} + futuresContractMarketDataChannel = "/contract/instrument:%s" // /contract/instrument:{symbol} + futuresSystemAnnouncementChannel = "/contract/announcement" + futuresTrasactionStatisticsTimerEventChannel = "/contractMarket/snapshot:%s" // /contractMarket/snapshot:{symbol} + + // futures private channels + + futuresTradeOrdersBySymbolChannel = "/contractMarket/tradeOrders:%s" // /contractMarket/tradeOrders:{symbol} + futuresTradeOrderChannel = "/contractMarket/tradeOrders" + futuresStopOrdersLifecycleEventChannel = "/contractMarket/advancedOrders" + futuresAccountBalanceEventChannel = "/contractAccount/wallet" + futuresPositionChangeEventChannel = "/contract/position:%s" // /contract/position:{symbol} +) + +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 +) + +var requiredSubscriptionIDS map[string]bool +var requiredSubscriptionIDSLock sync.Mutex + +// checkRequiredSubscriptionID check whether the id included in the required subscription ids list. +func (ku *Kucoin) checkRequiredSubscriptionID(id string) bool { + if len(requiredSubscriptionIDS) > 0 { + if requiredSubscriptionIDS[id] { + requiredSubscriptionIDSLock.Lock() + delete(requiredSubscriptionIDS, id) + requiredSubscriptionIDSLock.Unlock() + return true + } + } + return false +} + +// WsConnect creates a new websocket connection. +func (ku *Kucoin) WsConnect() error { + if !ku.Websocket.IsEnabled() || !ku.IsEnabled() { + return errors.New(stream.WebsocketNotEnabled) + } + fetchedFuturesSnapshotOrderbook = map[string]bool{} + var dialer websocket.Dialer + dialer.HandshakeTimeout = ku.Config.HTTPTimeout + dialer.Proxy = http.ProxyFromEnvironment + var instances *WSInstanceServers + _, err := ku.GetCredentials(context.Background()) + if err != nil { + ku.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + if ku.Websocket.CanUseAuthenticatedEndpoints() { + instances, err = ku.GetAuthenticatedInstanceServers(context.Background()) + if err != nil { + ku.Websocket.DataHandler <- err + ku.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + } + if instances == nil { + instances, err = ku.GetInstanceServers(context.Background()) + if err != nil { + return err + } + } + if len(instances.InstanceServers) == 0 { + return errors.New("no websocket instance server found") + } + ku.Websocket.Conn.SetURL(instances.InstanceServers[0].Endpoint + "?token=" + instances.Token) + err = ku.Websocket.Conn.Dial(&dialer, http.Header{}) + if err != nil { + return fmt.Errorf("%v - Unable to connect to Websocket. Error: %s", ku.Name, err) + } + ku.Websocket.Wg.Add(1) + go ku.wsReadData() + if err != nil { + return err + } + ku.Websocket.Conn.SetupPingHandler(stream.PingHandler{ + Delay: time.Millisecond * time.Duration(instances.InstanceServers[0].PingTimeout), + Message: []byte(`{"type":"ping"}`), + MessageType: websocket.TextMessage, + }) + + ku.setupOrderbookManager() + return nil +} + +// GetInstanceServers retrieves the server list and temporary public token +func (ku *Kucoin) GetInstanceServers(ctx context.Context) (*WSInstanceServers, error) { + response := struct { + Data WSInstanceServers `json:"data"` + Error + }{} + return &(response.Data), ku.SendPayload(ctx, request.Unset, func() (*request.Item, error) { + endpointPath, err := ku.API.Endpoints.GetURL(exchange.RestSpot) + if err != nil { + return nil, err + } + return &request.Item{ + Method: http.MethodPost, + Path: endpointPath + publicBullets, + Result: &response, + Verbose: ku.Verbose, + HTTPDebugging: ku.HTTPDebugging, + HTTPRecording: ku.HTTPRecording}, nil + }, request.UnauthenticatedRequest) +} + +// GetAuthenticatedInstanceServers retrieves server instances for authenticated users. +func (ku *Kucoin) GetAuthenticatedInstanceServers(ctx context.Context) (*WSInstanceServers, error) { + response := struct { + Data *WSInstanceServers `json:"data"` + Error + }{} + err := ku.SendAuthHTTPRequest(ctx, exchange.RestSpot, defaultSpotEPL, http.MethodPost, privateBullets, nil, &response) + if err != nil && strings.Contains(err.Error(), "400003") { + return response.Data, ku.SendAuthHTTPRequest(ctx, exchange.RestFutures, defaultFuturesEPL, http.MethodPost, privateBullets, nil, &response) + } + return response.Data, err +} + +// wsReadData receives and passes on websocket messages for processing +func (ku *Kucoin) wsReadData() { + defer ku.Websocket.Wg.Done() + for { + resp := ku.Websocket.Conn.ReadMessage() + if resp.Raw == nil { + return + } + err := ku.wsHandleData(resp.Raw) + if err != nil { + ku.Websocket.DataHandler <- err + } + } +} + +func (ku *Kucoin) wsHandleData(respData []byte) error { + resp := WsPushData{} + err := json.Unmarshal(respData, &resp) + if err != nil { + return err + } else if resp.ID != "" { + if ku.checkRequiredSubscriptionID(resp.ID) { + if !ku.Websocket.Match.IncomingWithData(resp.ID, respData) { + return fmt.Errorf("can not match subscription message with signature ID:%s", resp.ID) + } + } + return nil + } + if resp.Type == "pong" || resp.Type == "welcome" { + return nil + } + topicInfo := strings.Split(resp.Topic, ":") + switch { + case strings.HasPrefix(marketAllTickersChannel, topicInfo[0]), + strings.HasPrefix(marketTickerChannel, topicInfo[0]): + var instruments string + if topicInfo[1] == "all" { + instruments = resp.Subject + } else { + instruments = topicInfo[1] + } + return ku.processTicker(resp.Data, instruments) + case strings.HasPrefix(marketTickerSnapshotChannel, topicInfo[0]) || + strings.HasPrefix(marketTickerSnapshotForCurrencyChannel, topicInfo[0]): + return ku.processMarketSnapshot(resp.Data, topicInfo[1]) + case strings.HasPrefix(marketOrderbookLevel2Channels, topicInfo[0]): + return ku.processOrderbookWithDepth(respData, topicInfo[1]) + case strings.HasPrefix(marketOrderbookLevel2to5Channel, topicInfo[0]), + strings.HasPrefix(marketOrderbokLevel2To50Channel, topicInfo[0]): + return ku.processOrderbook(resp.Data, topicInfo[1]) + case strings.HasPrefix(marketCandlesChannel, topicInfo[0]): + symbolAndInterval := strings.Split(topicInfo[1], currency.UnderscoreDelimiter) + if len(symbolAndInterval) != 2 { + return errMalformedData + } + return ku.processCandlesticks(resp.Data, symbolAndInterval[0], symbolAndInterval[1]) + case strings.HasPrefix(marketMatchChannel, topicInfo[0]): + return ku.processTradeData(resp.Data, topicInfo[1]) + case strings.HasPrefix(indexPriceIndicatorChannel, topicInfo[0]): + var response WsPriceIndicator + return ku.processData(resp.Data, &response) + case strings.HasPrefix(markPriceIndicatorChannel, topicInfo[0]): + var response WsPriceIndicator + return ku.processData(resp.Data, &response) + case strings.HasPrefix(marginFundingbookChangeChannel, topicInfo[0]): + var response WsMarginFundingBook + return ku.processData(resp.Data, &response) + case strings.HasPrefix(privateSpotTradeOrders, topicInfo[0]): + return ku.processOrderChangeEvent(resp.Data) + case strings.HasPrefix(accountBalanceChannel, topicInfo[0]): + return ku.processAccountBalanceChange(resp.Data) + case strings.HasPrefix(marginPositionChannel, topicInfo[0]): + if resp.Subject == "debt.ratio" { + var response WsDebtRatioChange + return ku.processData(resp.Data, &response) + } + var response WsPositionStatus + return ku.processData(resp.Data, &response) + case strings.HasPrefix(marginLoanChannel, topicInfo[0]) && resp.Subject == "order.done": + var response WsMarginTradeOrderDoneEvent + return ku.processData(resp.Data, &response) + case strings.HasPrefix(marginLoanChannel, topicInfo[0]): + return ku.processMarginLendingTradeOrderEvent(resp.Data) + case strings.HasPrefix(spotMarketAdvancedChannel, topicInfo[0]): + return ku.processStopOrderEvent(resp.Data) + case strings.HasPrefix(futuresTickerV2Channel, topicInfo[0]), + strings.HasPrefix(futuresTickerChannel, topicInfo[0]): + return ku.processFuturesTickerV2(resp.Data) + case strings.HasPrefix(futuresOrderbookLevel2Channel, topicInfo[0]): + if !fetchedFuturesSnapshotOrderbook[topicInfo[1]] { + fetchedFuturesSnapshotOrderbook[topicInfo[1]] = true + var enabledPairs currency.Pairs + enabledPairs, err = ku.GetEnabledPairs(asset.Futures) + if err != nil { + return err + } + var cp currency.Pair + cp, err = enabledPairs.DeriveFrom(topicInfo[1]) + if err != nil { + return err + } + var orderbooks *orderbook.Base + orderbooks, err = ku.FetchOrderbook(context.Background(), cp, asset.Futures) + if err != nil { + return err + } + err = ku.Websocket.Orderbook.LoadSnapshot(orderbooks) + if err != nil { + return err + } + } + return ku.processFuturesOrderbookLevel2(resp.Data, topicInfo[1]) + case strings.HasPrefix(futuresExecutionDataChannel, topicInfo[0]): + var response WsFuturesExecutionData + return ku.processData(resp.Data, &response) + case strings.HasPrefix(futuresOrderbookLevel2Depth5Channel, topicInfo[0]), + strings.HasPrefix(futuresOrderbookLevel2Depth50Channel, topicInfo[0]): + if !fetchedFuturesSnapshotOrderbook[topicInfo[1]] { + fetchedFuturesSnapshotOrderbook[topicInfo[1]] = true + var enabledPairs currency.Pairs + enabledPairs, err = ku.GetEnabledPairs(asset.Futures) + if err != nil { + return err + } + cp, err := enabledPairs.DeriveFrom(topicInfo[1]) + if err != nil { + return err + } + orderbooks, err := ku.FetchOrderbook(context.Background(), cp, asset.Futures) + if err != nil { + return err + } + err = ku.Websocket.Orderbook.LoadSnapshot(orderbooks) + if err != nil { + return err + } + } + return ku.processFuturesOrderbookLevel5(resp.Data, topicInfo[1]) + case strings.HasPrefix(futuresContractMarketDataChannel, topicInfo[0]): + if resp.Subject == "mark.index.price" { + return ku.processFuturesMarkPriceAndIndexPrice(resp.Data, topicInfo[1]) + } else if resp.Subject == "funding.rate" { + return ku.processFuturesFundingData(resp.Data, topicInfo[1]) + } + case strings.HasPrefix(futuresSystemAnnouncementChannel, topicInfo[0]): + return ku.processFuturesSystemAnnouncement(resp.Data, resp.Subject) + case strings.HasPrefix(futuresTrasactionStatisticsTimerEventChannel, topicInfo[0]): + return ku.processFuturesTransactionStatistics(resp.Data, topicInfo[1]) + case strings.HasPrefix(futuresTradeOrdersBySymbolChannel, topicInfo[0]), + strings.HasPrefix(futuresTradeOrderChannel, topicInfo[0]): + return ku.processFuturesPrivateTradeOrders(resp.Data) + case strings.HasPrefix(futuresStopOrdersLifecycleEventChannel, topicInfo[0]): + return ku.processFuturesStopOrderLifecycleEvent(resp.Data) + case strings.HasPrefix(futuresAccountBalanceEventChannel, topicInfo[0]): + switch resp.Subject { + case "orderMargin.change": + var response WsFuturesOrderMarginEvent + return ku.processData(resp.Data, &response) + case "availableBalance.change": + return ku.processFuturesAccountBalanceEvent(resp.Data) + case "withdrawHold.change": + var response WsFuturesWithdrawalAmountAndTransferOutAmountEvent + return ku.processData(resp.Data, &response) + } + case strings.HasPrefix(futuresPositionChangeEventChannel, topicInfo[0]): + if resp.Subject == "position.change" { + if resp.ChannelType == "private" { + var response WsFuturesPosition + return ku.processData(resp.Data, &response) + } + var response WsFuturesMarkPricePositionChanges + return ku.processData(resp.Data, &response) + } else if resp.Subject == "position.settlement" { + var response WsFuturesPositionFundingSettlement + return ku.processData(resp.Data, &response) + } + default: + ku.Websocket.DataHandler <- stream.UnhandledMessageWarning{ + Message: ku.Name + stream.UnhandledMessage + string(respData), + } + return errors.New("push data not handled") + } + return nil +} + +func (ku *Kucoin) processData(respData []byte, resp interface{}) error { + if err := json.Unmarshal(respData, &resp); err != nil { + return err + } + ku.Websocket.DataHandler <- resp + return nil +} + +func (ku *Kucoin) processFuturesAccountBalanceEvent(respData []byte) error { + resp := WsFuturesAvailableBalance{} + if err := json.Unmarshal(respData, &resp); err != nil { + return err + } + ku.Websocket.DataHandler <- account.Change{ + Exchange: ku.Name, + Currency: currency.NewCode(resp.Currency), + Asset: asset.Futures, + Amount: resp.AvailableBalance, + } + return nil +} + +func (ku *Kucoin) processFuturesStopOrderLifecycleEvent(respData []byte) error { + resp := WsStopOrderLifecycleEvent{} + err := json.Unmarshal(respData, &resp) + if err != nil { + return err + } + var enabledPairs currency.Pairs + enabledPairs, err = ku.GetEnabledPairs(asset.Futures) + if err != nil { + return err + } + pair, err := enabledPairs.DeriveFrom(resp.Symbol) + if err != nil { + return err + } + oType, err := order.StringToOrderType(resp.OrderType) + if err != nil { + return err + } + side, err := order.StringToOrderSide(resp.Side) + if err != nil { + return err + } + ku.Websocket.DataHandler <- &order.Detail{ + Price: resp.OrderPrice, + TriggerPrice: resp.StopPrice, + Amount: resp.Size, + Exchange: ku.Name, + OrderID: resp.OrderID, + Type: oType, + Side: side, + AssetType: asset.Futures, + Date: resp.CreatedAt.Time(), + LastUpdated: resp.Timestamp.Time(), + Pair: pair, + } + return nil +} + +func (ku *Kucoin) processFuturesPrivateTradeOrders(respData []byte) error { + resp := WsFuturesTradeOrder{} + if err := json.Unmarshal(respData, &resp); err != nil { + return err + } + oType, err := order.StringToOrderType(resp.OrderType) + if err != nil { + return err + } + oStatus, err := ku.stringToOrderStatus(resp.Status) + if err != nil { + return err + } + var enabledPairs currency.Pairs + enabledPairs, err = ku.GetEnabledPairs(asset.Futures) + if err != nil { + return err + } + pair, err := enabledPairs.DeriveFrom(resp.Symbol) + if err != nil { + return err + } + side, err := order.StringToOrderSide(resp.Side) + if err != nil { + return err + } + ku.Websocket.DataHandler <- &order.Detail{ + Type: oType, + Status: oStatus, + Pair: pair, + Side: side, + Amount: resp.OrderSize, + Price: resp.OrderPrice, + Exchange: ku.Name, + ExecutedAmount: resp.FilledSize, + RemainingAmount: resp.RemainSize, + ClientOrderID: resp.ClientOid, + OrderID: resp.TradeID, + AssetType: asset.Futures, + LastUpdated: resp.OrderTime.Time(), + } + return nil +} + +func (ku *Kucoin) processFuturesTransactionStatistics(respData []byte, instrument string) error { + resp := WsFuturesTransactionStatisticsTimeEvent{} + if err := json.Unmarshal(respData, &resp); err != nil { + return err + } + resp.Symbol = instrument + return nil +} + +func (ku *Kucoin) processFuturesSystemAnnouncement(respData []byte, subject string) error { + resp := WsFuturesFundingBegin{} + if err := json.Unmarshal(respData, &resp); err != nil { + return err + } + resp.Subject = subject + ku.Websocket.DataHandler <- &resp + return nil +} + +func (ku *Kucoin) processFuturesFundingData(respData []byte, instrument string) error { + resp := WsFundingRate{} + if err := json.Unmarshal(respData, &resp); err != nil { + return err + } + resp.Symbol = instrument + ku.Websocket.DataHandler <- &resp + return nil +} + +func (ku *Kucoin) processFuturesMarkPriceAndIndexPrice(respData []byte, instrument string) error { + resp := WsFuturesMarkPriceAndIndexPrice{} + if err := json.Unmarshal(respData, &resp); err != nil { + return err + } + resp.Symbol = instrument + ku.Websocket.DataHandler <- &resp + return nil +} + +func (ku *Kucoin) processFuturesOrderbookLevel5(respData []byte, instrument string) error { + response := WsOrderbookLevel5Response{} + if err := json.Unmarshal(respData, &response); err != nil { + return err + } + resp := response.ExtractOrderbookItems() + enabledPairs, err := ku.GetEnabledPairs(asset.Futures) + if err != nil { + return err + } + cp, err := enabledPairs.DeriveFrom(instrument) + if err != nil { + return err + } + return ku.Websocket.Orderbook.Update(&orderbook.Update{ + UpdateID: resp.Sequence, + UpdateTime: resp.Timestamp.Time(), + Asset: asset.Futures, + Bids: resp.Bids, + Asks: resp.Asks, + Pair: cp, + }) +} + +func (ku *Kucoin) processFuturesOrderbookLevel2(respData []byte, instrument string) error { + resp := WsFuturesOrderbokInfo{} + if err := json.Unmarshal(respData, &resp); err != nil { + return err + } + detail, err := ku.GetFuturesPartOrderbook100(context.Background(), instrument) + if err != nil { + return err + } + enabledPairs, err := ku.GetEnabledPairs(asset.Futures) + if err != nil { + return err + } + pair, err := enabledPairs.DeriveFrom(instrument) + if err != nil { + return err + } + base := orderbook.Update{ + UpdateTime: detail.Time, + Pair: pair, + Asset: asset.Futures, + Asks: detail.Asks, + Bids: detail.Bids, + } + return ku.Websocket.Orderbook.Update(&base) +} + +func (ku *Kucoin) processFuturesTickerV2(respData []byte) error { + resp := WsFuturesTicker{} + if err := json.Unmarshal(respData, &resp); err != nil { + return err + } + enabledPairs, err := ku.GetEnabledPairs(asset.Futures) + if err != nil { + return err + } + pair, err := enabledPairs.DeriveFrom(resp.Symbol) + if err != nil { + return err + } + ku.Websocket.DataHandler <- &ticker.Price{ + AssetType: asset.Futures, + Last: resp.FilledPrice, + Volume: resp.FilledSize, + LastUpdated: resp.FilledTime.Time(), + ExchangeName: ku.Name, + Pair: pair, + Ask: resp.BestAskPrice.Float64(), + Bid: resp.BestBidPrice.Float64(), + AskSize: resp.BestAskSize, + BidSize: resp.BestBidSize, + } + return nil +} + +func (ku *Kucoin) processStopOrderEvent(respData []byte) error { + resp := WsStopOrder{} + err := json.Unmarshal(respData, &resp) + if err != nil { + return err + } + var pair currency.Pair + pair, err = currency.NewPairFromString(resp.Symbol) + if err != nil { + return err + } + oType, err := order.StringToOrderType(resp.OrderType) + if err != nil { + return err + } + side, err := order.StringToOrderSide(resp.Side) + if err != nil { + return err + } + ku.Websocket.DataHandler <- &order.Detail{ + Price: resp.OrderPrice, + TriggerPrice: resp.StopPrice, + Amount: resp.Size, + Exchange: ku.Name, + OrderID: resp.OrderID, + Type: oType, + Side: side, + AssetType: asset.Spot, + Date: resp.CreatedAt.Time(), + LastUpdated: resp.Timestamp.Time(), + Pair: pair, + } + return nil +} + +func (ku *Kucoin) processMarginLendingTradeOrderEvent(respData []byte) error { + resp := WsMarginTradeOrderEntersEvent{} + if err := json.Unmarshal(respData, &resp); err != nil { + return err + } + ku.Websocket.DataHandler <- resp + return nil +} + +func (ku *Kucoin) processAccountBalanceChange(respData []byte) error { + response := WsAccountBalance{} + err := json.Unmarshal(respData, &response) + if err != nil { + return err + } + ku.Websocket.DataHandler <- account.Change{ + Exchange: ku.Name, + Currency: currency.NewCode(response.Currency), + Asset: asset.Futures, + Amount: response.Available, + } + return nil +} + +func (ku *Kucoin) processOrderChangeEvent(respData []byte) error { + response := WsTradeOrder{} + err := json.Unmarshal(respData, &response) + if err != nil { + return err + } + oType, err := order.StringToOrderType(response.OrderType) + if err != nil { + return err + } + oStatus, err := ku.stringToOrderStatus(response.Status) + if err != nil { + return err + } + pair, err := currency.NewPairFromString(response.Symbol) + if err != nil { + return err + } + side, err := order.StringToOrderSide(response.Side) + 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 + } + return nil +} + +func (ku *Kucoin) processTradeData(respData []byte, instrument string) error { + response := WsTrade{} + err := json.Unmarshal(respData, &response) + if err != nil { + return err + } + saveTradeData := ku.IsSaveTradeDataEnabled() + if !saveTradeData && + !ku.IsTradeFeedEnabled() { + return nil + } + pair, err := currency.NewPairFromString(instrument) + if err != nil { + return err + } + side, err := order.StringToOrderSide(response.Side) + 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) + if err != nil { + return err + } + } + return nil +} + +func (ku *Kucoin) processTicker(respData []byte, instrument string) error { + response := WsTicker{} + err := json.Unmarshal(respData, &response) + if err != nil { + return err + } + pair, err := currency.NewPairFromString(instrument) + 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 + } + return nil +} + +func (ku *Kucoin) processCandlesticks(respData []byte, instrument, intervalString string) error { + pair, err := currency.NewPairFromString(instrument) + if err != nil { + return err + } + response := WsCandlestickData{} + err = json.Unmarshal(respData, &response) + if err != nil { + return err + } + resp, err := response.getCandlestickData() + 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 + } + return nil +} + +func (ku *Kucoin) processOrderbookWithDepth(respData []byte, instrument string) error { + pair, err := currency.NewPairFromString(instrument) + if err != nil { + return err + } + result := struct { + Result *WsOrderbook `json:"data"` + }{} + err = json.Unmarshal(respData, &result) + if err != nil { + 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) + 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, + err) + } + } + return nil +} + +// UpdateLocalBuffer updates orderbook buffer and checks status if the book is Initial Sync being via the REST +// protocol. +func (ku *Kucoin) UpdateLocalBuffer(wsdp *WsOrderbook, assetType asset.Item) (bool, error) { + enabledPairs, err := ku.GetEnabledPairs(assetType) + if err != nil { + return false, err + } + + format, err := ku.GetPairFormat(assetType, true) + if err != nil { + return false, err + } + + currencyPair, err := currency.NewPairFromFormattedPairs(wsdp.Symbol, + enabledPairs, + format) + if err != nil { + return false, err + } + err = ku.obm.stageWsUpdate(wsdp, currencyPair, assetType) + if err != nil { + init, err2 := ku.obm.checkIsInitialSync(currencyPair, assetType) + if err2 != nil { + return false, err2 + } + return init, err + } + + err = ku.applyBufferUpdate(currencyPair, assetType) + if err != nil { + ku.flushAndCleanup(currencyPair, assetType) + } + + return false, err +} + +func (ku *Kucoin) processOrderbook(respData []byte, symbol string) error { + response := &WsOrderbook{} + err := json.Unmarshal(respData, &response) + if err != nil { + return err + } + response.Symbol = symbol + var pair currency.Pair + pair, err = currency.NewPairFromString(symbol) + if err != nil { + 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) + if err != nil { + if init { + return nil + } + return fmt.Errorf("%v - UpdateLocalCache for asset type %v error: %s", + ku.Name, + asset.Margin, + err) + } + } + return nil +} + +func (ku *Kucoin) processMarketSnapshot(respData []byte, instrument string) error { + response := WsSpotTicker{} + err := json.Unmarshal(respData, &response) + if err != nil { + return err + } + pair, err := currency.NewPairFromString(instrument) + 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 + } + return nil +} + +// Subscribe sends a websocket message to receive data from the channel +func (ku *Kucoin) Subscribe(subscriptions []stream.ChannelSubscription) error { + return ku.handleSubscriptions(subscriptions, "subscribe") +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (ku *Kucoin) Unsubscribe(subscriptions []stream.ChannelSubscription) error { + return ku.handleSubscriptions(subscriptions, "unsubscribe") +} + +func (ku *Kucoin) handleSubscriptions(subscriptions []stream.ChannelSubscription, operation string) error { + if requiredSubscriptionIDS == nil { + requiredSubscriptionIDS = map[string]bool{} + } + payloads, err := ku.generatePayloads(subscriptions, operation) + if err != nil { + return err + } + var errs error + for x := range payloads { + err = ku.Websocket.Conn.SendJSONMessage(payloads[x]) + if err != nil { + errs = common.AppendError(errs, err) + continue + } + ku.Websocket.AddSuccessfulSubscriptions(subscriptions[x]) + } + return errs +} + +// getChannelsAssetType returns the asset type to which the subscription channel belongs to +// or returns an error otherwise. +func (ku *Kucoin) getChannelsAssetType(channelName string) (asset.Item, error) { + switch channelName { + case futuresTickerV2Channel, futuresTickerChannel, futuresOrderbookLevel2Channel, futuresExecutionDataChannel, futuresOrderbookLevel2Depth5Channel, futuresOrderbookLevel2Depth50Channel, futuresContractMarketDataChannel, futuresSystemAnnouncementChannel, futuresTrasactionStatisticsTimerEventChannel, futuresTradeOrdersBySymbolChannel, futuresTradeOrderChannel, futuresStopOrdersLifecycleEventChannel, futuresAccountBalanceEventChannel, futuresPositionChangeEventChannel: + return asset.Futures, nil + case marketTickerChannel, marketAllTickersChannel, + marketTickerSnapshotChannel, marketTickerSnapshotForCurrencyChannel, + marketOrderbookLevel2Channels, marketOrderbookLevel2to5Channel, + marketOrderbokLevel2To50Channel, marketCandlesChannel, + marketMatchChannel, indexPriceIndicatorChannel, + markPriceIndicatorChannel, marginFundingbookChangeChannel, + privateSpotTradeOrders, accountBalanceChannel, + marginPositionChannel, marginLoanChannel, + spotMarketAdvancedChannel: + return asset.Spot, nil + default: + return asset.Empty, errors.New("channel not supported") + } +} + +// GenerateDefaultSubscriptions Adds default subscriptions to websocket. +func (ku *Kucoin) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) { + channels := []string{} + if ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil || ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { + channels = append(channels, + marketTickerChannel, + marketMatchChannel, + marketOrderbookLevel2Channels) + } + if ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { + channels = append(channels, + marginFundingbookChangeChannel) + } + if ku.CurrencyPairs.IsAssetEnabled(asset.Futures) == nil { + channels = append(channels, + futuresTickerV2Channel, + futuresOrderbookLevel2Depth50Channel) + } + var subscriptions []stream.ChannelSubscription + if ku.Websocket.CanUseAuthenticatedEndpoints() { + if ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil { + channels = append(channels, + accountBalanceChannel, + ) + } + if ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { + channels = append(channels, + marginPositionChannel, + marginLoanChannel, + ) + } + if ku.CurrencyPairs.IsAssetEnabled(asset.Futures) == nil { + channels = append(channels, + // futures authenticated channels + futuresTradeOrdersBySymbolChannel, + futuresTradeOrderChannel, + futuresStopOrdersLifecycleEventChannel, + futuresAccountBalanceEventChannel) + } + } + var err error + var spotPairs currency.Pairs + if ku.CurrencyPairs.IsAssetEnabled(asset.Spot) == nil { + spotPairs, err = ku.GetEnabledPairs(asset.Spot) + if err != nil { + return nil, err + } + } + var marginPairs currency.Pairs + if ku.CurrencyPairs.IsAssetEnabled(asset.Margin) == nil { + marginPairs, err = ku.GetEnabledPairs(asset.Margin) + if err != nil { + return nil, err + } + } + + var futuresPairs currency.Pairs + if ku.CurrencyPairs.IsAssetEnabled(asset.Futures) == nil { + futuresPairs, err = ku.GetEnabledPairs(asset.Futures) + if err != nil { + return nil, err + } + } + marginLoanCurrencyCheckMap := map[currency.Code]bool{} + for x := range channels { + switch channels[x] { + case accountBalanceChannel, marginPositionChannel, + futuresTradeOrderChannel, futuresStopOrdersLifecycleEventChannel, + spotMarketAdvancedChannel, privateSpotTradeOrders, + marketAllTickersChannel, futuresSystemAnnouncementChannel, + futuresAccountBalanceEventChannel: + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channels[x], + }) + case marketTickerSnapshotChannel, + marketOrderbookLevel2Channels, + marketTickerSnapshotForCurrencyChannel, + marketOrderbookLevel2to5Channel, + marketOrderbokLevel2To50Channel, + marketTickerChannel: + subscribedPairsMap := map[string]bool{} + for b := range spotPairs { + if okay := subscribedPairsMap[spotPairs[b].String()]; okay { + continue + } + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channels[x], + Asset: asset.Spot, + Currency: spotPairs[b], + }) + subscribedPairsMap[spotPairs[b].String()] = true + } + for b := range marginPairs { + if okay := subscribedPairsMap[marginPairs[b].String()]; okay { + continue + } + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channels[x], + Asset: asset.Margin, + Currency: marginPairs[b], + }) + subscribedPairsMap[marginPairs[b].String()] = true + } + case indexPriceIndicatorChannel, + markPriceIndicatorChannel, + marketMatchChannel: + pairs := currency.Pairs{} + for p := range spotPairs { + pairs = pairs.Add(spotPairs[p]) + } + for p := range marginPairs { + pairs = pairs.Add(marginPairs[p]) + } + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channels[x], + Asset: asset.Spot, + Params: map[string]interface{}{"symbols": pairs.Join()}, + }) + case marketCandlesChannel: + subscribedPairsMap := map[string]bool{} + for p := range spotPairs { + if okay := subscribedPairsMap[spotPairs[p].String()]; okay { + continue + } + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channels[x], + Asset: asset.Spot, + Currency: spotPairs[p], + Params: map[string]interface{}{"interval": kline.FifteenMin}, + }) + subscribedPairsMap[spotPairs[p].String()] = true + } + for p := range marginPairs { + if okay := subscribedPairsMap[marginPairs[p].String()]; okay { + continue + } + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channels[x], + Asset: asset.Margin, + Currency: marginPairs[p], + Params: map[string]interface{}{"interval": kline.FifteenMin}, + }) + subscribedPairsMap[marginPairs[p].String()] = true + } + case marginLoanChannel: + for b := range marginPairs { + if !marginLoanCurrencyCheckMap[marginPairs[b].Quote] { + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channels[x], + Currency: currency.Pair{Base: marginPairs[b].Quote}, + }) + marginLoanCurrencyCheckMap[marginPairs[b].Quote] = true + } + if !marginLoanCurrencyCheckMap[marginPairs[b].Base] { + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channels[x], + Currency: currency.Pair{Base: marginPairs[b].Base}, + }) + marginLoanCurrencyCheckMap[marginPairs[b].Base] = true + } + } + case marginFundingbookChangeChannel: + currencyExist := map[currency.Code]bool{} + for b := range marginPairs { + okay := currencyExist[marginPairs[b].Base] + if !okay { + currencyExist[marginPairs[b].Base] = true + } + okay = currencyExist[marginPairs[b].Quote] + if !okay { + currencyExist[marginPairs[b].Quote] = true + } + } + var currencies string + for b := range currencyExist { + currencies += b.String() + "," + } + currencies = strings.TrimSuffix(currencies, ",") + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channels[x], + Params: map[string]interface{}{"currencies": currencies}, + }) + case futuresTickerV2Channel, + futuresTickerChannel, + futuresExecutionDataChannel, + futuresOrderbookLevel2Channel, + futuresOrderbookLevel2Depth5Channel, + futuresOrderbookLevel2Depth50Channel, + futuresContractMarketDataChannel, + futuresTradeOrdersBySymbolChannel, + futuresPositionChangeEventChannel, + futuresTrasactionStatisticsTimerEventChannel: + for b := range futuresPairs { + futuresPairs[b], err = ku.FormatExchangeCurrency(futuresPairs[b], asset.Futures) + if err != nil { + continue + } + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channels[x], + Asset: asset.Futures, + Currency: futuresPairs[b], + }) + } + } + } + return subscriptions, nil +} + +func (ku *Kucoin) generatePayloads(subscriptions []stream.ChannelSubscription, operation string) ([]WsSubscriptionInput, error) { + payloads := make([]WsSubscriptionInput, 0, len(subscriptions)) + marketTickerSnapshotForCurrencyChannelCurrencyFilter := map[currency.Code]int{} + for x := range subscriptions { + var err error + var a asset.Item + a, err = ku.getChannelsAssetType(subscriptions[x].Channel) + if err != nil { + return nil, err + } + if !subscriptions[x].Currency.IsEmpty() { + subscriptions[x].Currency, err = ku.FormatExchangeCurrency(subscriptions[x].Currency, a) + if err != nil { + return nil, err + } + } + if subscriptions[x].Asset == asset.Futures { + subscriptions[x].Currency, err = ku.FormatExchangeCurrency(subscriptions[x].Currency, asset.Futures) + if err != nil { + continue + } + } + switch subscriptions[x].Channel { + case marketTickerChannel, + marketOrderbookLevel2Channels, + marketOrderbookLevel2to5Channel, + marketOrderbokLevel2To50Channel, + indexPriceIndicatorChannel, + marketMatchChannel, + markPriceIndicatorChannel: + symbols, okay := subscriptions[x].Params["symbols"].(string) + if !okay { + if subscriptions[x].Currency.IsEmpty() { + return nil, errors.New("symbols not passed") + } + symbols = subscriptions[x].Currency.String() + } + payloads = append(payloads, WsSubscriptionInput{ + ID: strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10), + Type: operation, + Topic: fmt.Sprintf(subscriptions[x].Channel, symbols), + Response: true, + }) + case marketAllTickersChannel, + privateSpotTradeOrders, + accountBalanceChannel, + marginPositionChannel, + spotMarketAdvancedChannel, + futuresTradeOrderChannel, + futuresStopOrdersLifecycleEventChannel, + futuresAccountBalanceEventChannel, futuresSystemAnnouncementChannel: + input := WsSubscriptionInput{ + ID: strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10), + Type: operation, + Topic: subscriptions[x].Channel, + Response: true, + } + switch subscriptions[x].Channel { + case futuresTradeOrderChannel, + futuresStopOrdersLifecycleEventChannel, + futuresAccountBalanceEventChannel, + privateSpotTradeOrders, + accountBalanceChannel, + marginPositionChannel, + spotMarketAdvancedChannel: + input.PrivateChannel = true + } + payloads = append(payloads, input) + case marketTickerSnapshotChannel, futuresPositionChangeEventChannel, + futuresTradeOrdersBySymbolChannel, futuresTrasactionStatisticsTimerEventChannel, + futuresContractMarketDataChannel, futuresOrderbookLevel2Depth50Channel, + futuresOrderbookLevel2Depth5Channel, futuresExecutionDataChannel, + futuresOrderbookLevel2Channel, futuresTickerChannel, + futuresTickerV2Channel: // Symbols + item := WsSubscriptionInput{ + ID: strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10), + Type: operation, + Topic: fmt.Sprintf(subscriptions[x].Channel, subscriptions[x].Currency.String()), + Response: true, + } + switch subscriptions[x].Channel { + case futuresPositionChangeEventChannel, + futuresTradeOrdersBySymbolChannel: + item.PrivateChannel = true + } + payloads = append(payloads, item) + case marketTickerSnapshotForCurrencyChannel, + marginLoanChannel: + // 3 means the Currency is used by both switch cases + // 2 means the currency is used by channel = marginLoanChannel + // 1 if used by marketTickerSnapshotForCurrencyChannel + if stat := marketTickerSnapshotForCurrencyChannelCurrencyFilter[subscriptions[x].Currency.Base]; stat == 3 || (stat == 2 && subscriptions[x].Channel == marginLoanChannel) || stat == 1 { + continue + } + input := WsSubscriptionInput{} + if subscriptions[x].Channel == marginLoanChannel { + input.PrivateChannel = true + marketTickerSnapshotForCurrencyChannelCurrencyFilter[subscriptions[x].Currency.Base] += 2 + } else { + marketTickerSnapshotForCurrencyChannelCurrencyFilter[subscriptions[x].Currency.Base]++ + subscriptions[x].Channel += "%s" + } + input.ID = strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10) + input.Type = operation + input.Topic = fmt.Sprintf(subscriptions[x].Channel, subscriptions[x].Currency.Base.Upper().String()) + input.Response = true + payloads = append(payloads, input) + case marketCandlesChannel: + interval, err := ku.intervalToString(subscriptions[x].Params["interval"].(kline.Interval)) + if err != nil { + return nil, err + } + payloads = append(payloads, WsSubscriptionInput{ + ID: strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10), + Type: operation, + Topic: fmt.Sprintf(subscriptions[x].Channel, subscriptions[x].Currency.Upper().String(), interval), + Response: true, + }) + case marginFundingbookChangeChannel: + currencies, okay := subscriptions[x].Params["currencies"].(string) + if !okay { + return nil, errors.New("currencies not passed") + } + payloads = append(payloads, WsSubscriptionInput{ + ID: strconv.FormatInt(ku.Websocket.Conn.GenerateMessageID(false), 10), + Type: operation, + Topic: fmt.Sprintf(subscriptions[x].Channel, currencies), + Response: true, + }) + } + } + return payloads, nil +} + +// 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 *WsOrderbook + fetchingBook bool + initialSync bool + needsFetchingBook bool + lastUpdateID int64 +} + +// job defines a synchronisation job that tells a go routine to fetch an +// orderbook via the REST protocol +type job struct { + Pair currency.Pair + AssetType asset.Item +} + +func (ku *Kucoin) setupOrderbookManager() { + locker.Lock() + defer locker.Unlock() + if ku.obm == nil { + ku.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. + ku.obm.Mutex.Lock() + for _, m1 := range ku.obm.state { + for _, m2 := range m1 { + for _, idk := range m2 { + idk.initialSync = true + idk.needsFetchingBook = true + idk.lastUpdateID = 0 + } + } + } + ku.obm.Mutex.Unlock() + } + for i := 0; i < maxWSOrderbookWorkers; i++ { + // 10 workers for synchronising book + ku.SynchroniseWebsocketOrderbook() + } +} + +// ProcessUpdate processes the websocket orderbook update +func (ku *Kucoin) ProcessUpdate(cp currency.Pair, a asset.Item, ws *WsOrderbook) error { + updateBid := make([]orderbook.Item, len(ws.Changes.Bids)) + for i := range ws.Changes.Bids { + p, err := strconv.ParseFloat(ws.Changes.Bids[i][0], 64) + if err != nil { + return err + } + a, err := strconv.ParseFloat(ws.Changes.Bids[i][1], 64) + if err != nil { + return err + } + var sequence int64 + if len(ws.Changes.Bids[i]) > 2 && ws.Changes.Bids[i][2] != "" { + sequence, err = strconv.ParseInt(ws.Changes.Bids[i][2], 10, 64) + if err != nil { + return err + } + } + updateBid[i] = orderbook.Item{Price: p, Amount: a, ID: sequence} + } + + updateAsk := make([]orderbook.Item, len(ws.Changes.Asks)) + for i := range ws.Changes.Asks { + p, err := strconv.ParseFloat(ws.Changes.Asks[i][0], 64) + if err != nil { + return err + } + a, err := strconv.ParseFloat(ws.Changes.Asks[i][1], 64) + if err != nil { + return err + } + var sequence int64 + if len(ws.Changes.Asks[i]) > 2 && ws.Changes.Asks[i][2] != "" { + sequence, err = strconv.ParseInt(ws.Changes.Asks[i][2], 10, 64) + if err != nil { + return err + } + } + updateAsk[i] = orderbook.Item{Price: p, Amount: a, ID: sequence} + } + + return ku.Websocket.Orderbook.Update(&orderbook.Update{ + Bids: updateBid, + Asks: updateAsk, + Pair: cp, + UpdateID: ws.SequenceEnd, + UpdateTime: ws.TimeMS.Time(), + Asset: a, + }) +} + +// 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 (ku *Kucoin) applyBufferUpdate(pair currency.Pair, assetType asset.Item) error { + fetching, needsFetching, err := ku.obm.handleFetchingBook(pair, assetType) + if err != nil { + return err + } + if fetching { + return nil + } + if needsFetching { + if ku.Verbose { + log.Debugf(log.WebsocketMgr, "%s Orderbook: Fetching via REST\n", ku.Name) + } + return ku.obm.fetchBookViaREST(pair, assetType) + } + + recent, err := ku.Websocket.Orderbook.GetOrderbook(pair, assetType) + if err != nil { + log.Errorf( + log.WebsocketMgr, + "%s error fetching recent orderbook when applying updates: %s\n", + ku.Name, + err) + } + + if recent != nil { + err = ku.obm.checkAndProcessUpdate(ku.ProcessUpdate, pair, assetType, recent) + if err != nil { + log.Errorf( + log.WebsocketMgr, + "%s error processing update - initiating new orderbook sync via REST: %s\n", + ku.Name, + err) + err = ku.obm.setNeedsFetchingBook(pair, assetType) + if err != nil { + return err + } + } + } + + return nil +} + +// setNeedsFetchingBook completes the book fetching initiation. +func (o *orderbookManager) setNeedsFetchingBook(pair currency.Pair, assetType asset.Item) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][assetType] + if !ok { + return fmt.Errorf("could not match pair %s and asset type %s in hash table", + pair, + assetType) + } + state.needsFetchingBook = true + return nil +} + +// SynchroniseWebsocketOrderbook synchronises full orderbook for currency pair +// asset +func (ku *Kucoin) SynchroniseWebsocketOrderbook() { + ku.Websocket.Wg.Add(1) + go func() { + defer ku.Websocket.Wg.Done() + for { + select { + case <-ku.Websocket.ShutdownC: + for { + select { + case <-ku.obm.jobs: + default: + return + } + } + case j := <-ku.obm.jobs: + err := ku.processJob(j.Pair, j.AssetType) + if err != nil { + log.Errorf(log.WebsocketMgr, + "%s processing websocket orderbook error %v", + ku.Name, err) + } + } + } + }() +} + +// SeedLocalCache seeds depth data +func (ku *Kucoin) SeedLocalCache(ctx context.Context, p currency.Pair, assetType asset.Item) error { + var ob *Orderbook + var err error + ob, err = ku.GetPartOrderbook100(ctx, p.String()) + if err != nil { + return err + } + if ob.Sequence <= 0 { + return fmt.Errorf("%w p", errMissingOrderbookSequence) + } + return ku.SeedLocalCacheWithBook(p, ob, assetType) +} + +// SeedLocalCacheWithBook seeds the local orderbook cache +func (ku *Kucoin) SeedLocalCacheWithBook(p currency.Pair, orderbookNew *Orderbook, assetType asset.Item) error { + newOrderBook := orderbook.Base{ + Pair: p, + Asset: assetType, + Exchange: ku.Name, + LastUpdated: time.Now(), + LastUpdateID: orderbookNew.Sequence, + VerifyOrderbook: ku.CanVerifyOrderbook, + Bids: make(orderbook.Items, len(orderbookNew.Bids)), + Asks: make(orderbook.Items, len(orderbookNew.Asks)), + } + for i := range orderbookNew.Bids { + newOrderBook.Bids[i] = orderbook.Item{ + Amount: orderbookNew.Bids[i].Amount, + Price: orderbookNew.Bids[i].Price, + } + } + for i := range orderbookNew.Asks { + newOrderBook.Asks[i] = orderbook.Item{ + Amount: orderbookNew.Asks[i].Amount, + Price: orderbookNew.Asks[i].Price, + } + } + return ku.Websocket.Orderbook.LoadSnapshot(&newOrderBook) +} + +// processJob fetches and processes orderbook updates +func (ku *Kucoin) processJob(p currency.Pair, assetType asset.Item) error { + err := ku.SeedLocalCache(context.TODO(), p, assetType) + if err != nil { + err = ku.obm.stopFetchingBook(p, assetType) + if err != nil { + return err + } + return fmt.Errorf("%s %s seeding local cache for orderbook error: %v", + p, assetType, err) + } + + err = ku.obm.stopFetchingBook(p, assetType) + if err != nil { + return err + } + + // Immediately apply the buffer updates so we don't wait for a + // new update to initiate this. + err = ku.applyBufferUpdate(p, assetType) + if err != nil { + ku.flushAndCleanup(p, assetType) + return err + } + return nil +} + +// flushAndCleanup flushes orderbook and clean local cache +func (ku *Kucoin) flushAndCleanup(p currency.Pair, assetType asset.Item) { + errClean := ku.Websocket.Orderbook.FlushOrderbook(p, assetType) + if errClean != nil { + log.Errorf(log.WebsocketMgr, + "%s flushing websocket error: %v", + ku.Name, + errClean) + } + errClean = ku.obm.cleanup(p, assetType) + if errClean != nil { + log.Errorf(log.WebsocketMgr, "%s cleanup websocket error: %v", + ku.Name, + errClean) + } +} + +// stageWsUpdate stages websocket update to roll through updates that need to +// be applied to a fetched orderbook via REST. +func (o *orderbookManager) stageWsUpdate(u *WsOrderbook, 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 *WsOrderbook, maxWSUpdateBuffer), + fetchingBook: false, + initialSync: true, + needsFetchingBook: true, + } + m2[a] = state + } + + if state.lastUpdateID != 0 && u.SequenceStart > state.lastUpdateID+1 { + // Apply the new Level 2 data flow to the local snapshot to ensure that sequenceStart(new)<=sequenceEnd+1(old) and sequenceEnd(new) > sequenceEnd(old) + return fmt.Errorf("websocket orderbook synchronisation failure for pair %s and asset %s", pair, a) + } + state.lastUpdateID = u.SequenceEnd + 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) + } +} + +// handleFetchingBook checks if a full book is being fetched or needs to be +// fetched +func (o *orderbookManager) handleFetchingBook(pair currency.Pair, assetType asset.Item) (fetching, needsFetching bool, err error) { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][assetType] + if !ok { + return false, + false, + fmt.Errorf("check is fetching book cannot match currency pair %s asset type %s", + pair, + assetType) + } + + if state.fetchingBook { + return true, false, nil + } + + if state.needsFetchingBook { + state.needsFetchingBook = false + state.fetchingBook = true + return false, true, nil + } + return false, false, nil +} + +// stopFetchingBook completes the book fetching. +func (o *orderbookManager) stopFetchingBook(pair currency.Pair, assetType asset.Item) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][assetType] + if !ok { + return fmt.Errorf("could not match pair %s and asset type %s in hash table", + pair, + assetType) + } + if !state.fetchingBook { + return fmt.Errorf("fetching book already set to false for %s %s", + pair, + assetType) + } + state.fetchingBook = false + return nil +} + +// completeInitialSync sets if an asset type has completed its initial sync +func (o *orderbookManager) completeInitialSync(pair currency.Pair, assetType asset.Item) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][assetType] + if !ok { + return fmt.Errorf("complete initial sync cannot match currency pair %s asset type %s", + pair, + assetType) + } + if !state.initialSync { + return fmt.Errorf("initital sync already set to false for %s %s", + pair, + assetType) + } + state.initialSync = false + return nil +} + +// checkIsInitialSync checks status if the book is Initial Sync being via the REST +// protocol. +func (o *orderbookManager) checkIsInitialSync(pair currency.Pair, assetType asset.Item) (bool, error) { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][assetType] + if !ok { + return false, + fmt.Errorf("checkIsInitialSync of orderbook cannot match currency pair %s asset type %s", + pair, + assetType) + } + 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, assetType asset.Item) error { + o.Lock() + defer o.Unlock() + + state, ok := o.state[pair.Base][pair.Quote][assetType] + if !ok { + return fmt.Errorf("fetch book via rest cannot match currency pair %s asset type %s", + pair, + assetType) + } + + state.initialSync = true + state.fetchingBook = true + + select { + case o.jobs <- job{pair, assetType}: + return nil + default: + return fmt.Errorf("%s %s book synchronisation channel blocked up", + pair, + assetType) + } +} + +func (o *orderbookManager) checkAndProcessUpdate(processor func(currency.Pair, asset.Item, *WsOrderbook) error, pair currency.Pair, assetType asset.Item, recent *orderbook.Base) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][assetType] + if !ok { + return fmt.Errorf("could not match pair [%s] asset type [%s] in hash table to process websocket orderbook update", + pair, assetType) + } + + // 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, assetType, d) + if err != nil { + return fmt.Errorf("%s %s processing update error: %w", + pair, assetType, err) + } + } + default: + break buffer + } + } + return nil +} + +// validate checks for correct update alignment +func (u *update) validate(updt *WsOrderbook, recent *orderbook.Base) (bool, error) { + if updt.SequenceEnd <= 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.SequenceStart > id || updt.SequenceEnd < id { + return false, fmt.Errorf("initial websocket orderbook sync failure for pair %s and asset %s", + recent.Pair, + recent.Asset) + } + u.initialSync = false + } + return true, nil +} + +// cleanup cleans up buffer and reset fetch and init +func (o *orderbookManager) cleanup(pair currency.Pair, assetType asset.Item) error { + o.Lock() + state, ok := o.state[pair.Base][pair.Quote][assetType] + if !ok { + o.Unlock() + return fmt.Errorf("cleanup cannot match %s %s to hash table", + pair, + assetType) + } + +bufferEmpty: + for { + select { + case <-state.buffer: + // bleed and discard buffer + default: + break bufferEmpty + } + } + o.Unlock() + // disable rest orderbook synchronisation + _ = o.stopFetchingBook(pair, assetType) + _ = o.completeInitialSync(pair, assetType) + _ = o.stopNeedsFetchingBook(pair, assetType) + return nil +} + +// stopNeedsFetchingBook completes the book fetching initiation. +func (o *orderbookManager) stopNeedsFetchingBook(pair currency.Pair, assetType asset.Item) error { + o.Lock() + defer o.Unlock() + state, ok := o.state[pair.Base][pair.Quote][assetType] + if !ok { + return fmt.Errorf("could not match pair %s and asset type %s in hash table", + pair, + assetType) + } + if !state.needsFetchingBook { + return fmt.Errorf("needs fetching book already set to false for %s %s", + pair, + assetType) + } + state.needsFetchingBook = false + 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 + } + assetPairEnabled[assetTypes[i]] = pairs.Contains(cp, true) + } + return assetPairEnabled +} diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go new file mode 100644 index 00000000..e7af5c82 --- /dev/null +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -0,0 +1,1502 @@ +package kucoin + +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/kline" + "github.com/thrasher-corp/gocryptotrader/exchanges/margin" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/exchanges/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/trade" + "github.com/thrasher-corp/gocryptotrader/log" + "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" +) + +// GetDefaultConfig returns a default exchange config +func (ku *Kucoin) GetDefaultConfig(ctx context.Context) (*config.Exchange, error) { + ku.SetDefaults() + exchCfg, err := ku.GetStandardConfig() + if err != nil { + return nil, err + } + + err = ku.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if ku.Features.Supports.RESTCapabilities.AutoPairUpdates { + err := ku.UpdateTradablePairs(ctx, true) + if err != nil { + return nil, err + } + } + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for Kucoin +func (ku *Kucoin) SetDefaults() { + ku.Name = "Kucoin" + ku.Enabled = true + ku.Verbose = false + + ku.API.CredentialsValidator.RequiresKey = true + ku.API.CredentialsValidator.RequiresSecret = true + ku.API.CredentialsValidator.RequiresClientID = true + + spot := currency.PairStore{ + RequestFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}, + ConfigFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}, + } + futures := currency.PairStore{ + RequestFormat: ¤cy.PairFormat{Uppercase: true}, + ConfigFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter}, + } + err := ku.StoreAssetPairFormat(asset.Spot, spot) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + err = ku.StoreAssetPairFormat(asset.Margin, spot) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + err = ku.StoreAssetPairFormat(asset.Futures, futures) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + ku.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: true, + RESTCapabilities: protocol.Features{ + TickerFetching: true, + TickerBatching: true, + OrderbookFetching: true, + AutoPairUpdates: true, + AccountInfo: true, + CryptoWithdrawal: true, + SubmitOrder: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + CancelOrders: true, + TradeFetching: true, + UserTradeHistory: true, + KlineFetching: true, + DepositHistory: true, + WithdrawalHistory: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + AccountInfo: true, + GetOrders: true, + TradeFetching: true, + KlineFetching: true, + GetOrder: true, + }, + WithdrawPermissions: exchange.AutoWithdrawCrypto, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + Kline: kline.ExchangeCapabilitiesEnabled{ + Intervals: kline.DeployExchangeIntervals( + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.EightHour}, + kline.IntervalCapacity{Interval: kline.TwelveHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, + kline.IntervalCapacity{Interval: kline.OneWeek}, + ), + GlobalResultLimit: 1500, + }, + }, + } + ku.Requester, err = request.New(ku.Name, + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), + request.WithLimiter(SetRateLimit())) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + + ku.API.Endpoints = ku.NewEndpoints() + err = ku.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{ + exchange.RestSpot: kucoinAPIURL, + exchange.RestFutures: kucoinFuturesAPIURL, + exchange.WebsocketSpot: kucoinWebsocketURL, + }) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + ku.Websocket = stream.New() + ku.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + ku.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + ku.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup takes in the supplied exchange configuration details and sets params +func (ku *Kucoin) Setup(exch *config.Exchange) error { + err := exch.Validate() + if err != nil { + return err + } + if !exch.Enabled { + ku.SetEnabled(false) + return nil + } + err = ku.SetupDefaults(exch) + if err != nil { + return err + } + + wsRunningEndpoint, err := ku.API.Endpoints.GetURL(exchange.WebsocketSpot) + if err != nil { + return err + } + err = ku.Websocket.Setup( + &stream.WebsocketSetup{ + ExchangeConfig: exch, + DefaultURL: kucoinWebsocketURL, + RunningURL: wsRunningEndpoint, + Connector: ku.WsConnect, + Subscriber: ku.Subscribe, + Unsubscriber: ku.Unsubscribe, + GenerateSubscriptions: ku.GenerateDefaultSubscriptions, + Features: &ku.Features.Supports.WebsocketCapabilities, + OrderbookBufferConfig: buffer.Config{ + SortBuffer: true, + SortBufferByUpdateIDs: true, + UpdateIDProgression: true, + }, + TradeFeed: ku.Features.Enabled.TradeFeed, + }) + if err != nil { + return err + } + return ku.Websocket.SetupNewConnection(stream.ConnectionSetup{ + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + RateLimit: 500, + }) +} + +// Start starts the Kucoin go routine +func (ku *Kucoin) Start(ctx context.Context, wg *sync.WaitGroup) error { + if wg == nil { + return fmt.Errorf("%T %w", wg, common.ErrNilPointer) + } + wg.Add(1) + go func() { + ku.Run(ctx) + wg.Done() + }() + return nil +} + +// Run implements the Kucoin wrapper +func (ku *Kucoin) Run(ctx context.Context) { + if ku.Verbose { + log.Debugf(log.ExchangeSys, + "%s Websocket: %s.", + ku.Name, + common.IsEnabled(ku.Websocket.IsEnabled())) + ku.PrintEnabledPairs() + } + + if !ku.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := ku.UpdateTradablePairs(ctx, true) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + ku.Name, + err) + } +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (ku *Kucoin) FetchTradablePairs(ctx context.Context, assetType asset.Item) (currency.Pairs, error) { + var cp currency.Pair + switch assetType { + case asset.Futures: + myPairs, err := ku.GetFuturesOpenContracts(ctx) + if err != nil { + return nil, err + } + pairs := make(currency.Pairs, 0, len(myPairs)) + for x := range myPairs { + if strings.ToLower(myPairs[x].Status) != "open" { //nolint:gocritic // strings.ToLower is faster + continue + } + cp, err = currency.NewPairFromStrings(myPairs[x].BaseCurrency, myPairs[x].Symbol[len(myPairs[x].BaseCurrency):]) + if err != nil { + return nil, err + } + pairs = pairs.Add(cp) + } + configFormat, err := ku.GetPairFormat(asset.Futures, false) + if err != nil { + return nil, err + } + return pairs.Format(configFormat), nil + case asset.Spot, asset.Margin: + myPairs, err := ku.GetSymbols(ctx, "") + if err != nil { + return nil, err + } + pairs := make(currency.Pairs, 0, len(myPairs)) + for x := range myPairs { + if !myPairs[x].EnableTrading { + continue + } + cp, err = currency.NewPairFromString(strings.ToUpper(myPairs[x].Name)) + if err != nil { + return nil, err + } + pairs = pairs.Add(cp) + } + return pairs, nil + default: + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) + } +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them in the exchanges config +func (ku *Kucoin) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error { + assets := ku.GetAssetTypes(true) + for a := range assets { + pairs, err := ku.FetchTradablePairs(ctx, assets[a]) + if err != nil { + return err + } + if len(pairs) == 0 { + return fmt.Errorf("%v; no tradable pairs", currency.ErrCurrencyPairsEmpty) + } + err = ku.UpdatePairs(pairs, assets[a], false, forceUpdate) + if err != nil { + return err + } + } + return nil +} + +// UpdateTicker updates and returns the ticker for a currency pair +func (ku *Kucoin) UpdateTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) { + p, err := ku.FormatExchangeCurrency(p, assetType) + if err != nil { + return nil, err + } + if err := ku.UpdateTickers(ctx, assetType); err != nil { + return nil, err + } + return ticker.GetTicker(ku.Name, p, assetType) +} + +// UpdateTickers updates all currency pairs of a given asset type +func (ku *Kucoin) UpdateTickers(ctx context.Context, assetType asset.Item) error { + switch assetType { + case asset.Futures: + ticks, err := ku.GetFuturesOpenContracts(ctx) + if err != nil { + return err + } + pairs, err := ku.GetEnabledPairs(asset.Futures) + if err != nil { + return err + } + for x := range ticks { + var pair currency.Pair + pair, err = currency.NewPairFromStrings(ticks[x].BaseCurrency, ticks[x].Symbol[len(ticks[x].BaseCurrency):]) + if err != nil { + return err + } + if !pairs.Contains(pair, true) { + continue + } + err = ticker.ProcessTicker(&ticker.Price{ + Last: ticks[x].LastTradePrice, + High: ticks[x].HighPrice, + Low: ticks[x].LowPrice, + Volume: ticks[x].VolumeOf24h, + Pair: pair, + ExchangeName: ku.Name, + AssetType: assetType, + }) + if err != nil { + return err + } + } + return nil + case asset.Spot, asset.Margin: + ticks, err := ku.GetTickers(ctx) + if err != nil { + return err + } + pairs, err := ku.GetEnabledPairs(assetType) + if err != nil { + return err + } + for t := range ticks.Tickers { + pair, err := currency.NewPairFromString(ticks.Tickers[t].Symbol) + if err != nil { + return err + } + 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) + if err != nil { + return err + } + } + } + default: + return fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) + } + return nil +} + +// FetchTicker returns the ticker for a currency pair +func (ku *Kucoin) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) { + p, err := ku.FormatExchangeCurrency(p, assetType) + if err != nil { + return nil, err + } + tickerNew, err := ticker.GetTicker(ku.Name, p, assetType) + if err != nil { + return ku.UpdateTicker(ctx, p, assetType) + } + return tickerNew, nil +} + +// FetchOrderbook returns orderbook base on the currency pair +func (ku *Kucoin) FetchOrderbook(ctx context.Context, pair currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + pair, err := ku.FormatExchangeCurrency(pair, assetType) + if err != nil { + return nil, err + } + ob, err := orderbook.Get(ku.Name, pair, assetType) + if err != nil { + return ku.UpdateOrderbook(ctx, pair, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (ku *Kucoin) UpdateOrderbook(ctx context.Context, pair currency.Pair, assetType asset.Item) (*orderbook.Base, error) { + err := ku.CurrencyPairs.IsAssetEnabled(assetType) + if err != nil { + return nil, err + } + pair, err = ku.FormatExchangeCurrency(pair, assetType) + if err != nil { + return nil, err + } + var ordBook *Orderbook + switch assetType { + case asset.Futures: + ordBook, err = ku.GetFuturesOrderbook(ctx, pair.String()) + case asset.Spot, asset.Margin: + if ku.IsRESTAuthenticationSupported() && ku.AreCredentialsValid(ctx) { + ordBook, err = ku.GetOrderbook(ctx, pair.String()) + if err != nil { + return nil, err + } + } else { + ordBook, err = ku.GetPartOrderbook100(ctx, pair.String()) + } + default: + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) + } + if err != nil { + return nil, err + } + + book := &orderbook.Base{ + Exchange: ku.Name, + Pair: pair, + Asset: assetType, + VerifyOrderbook: ku.CanVerifyOrderbook, + Asks: ordBook.Asks, + Bids: ordBook.Bids, + } + err = book.Process() + if err != nil { + return book, err + } + return orderbook.Get(ku.Name, pair, assetType) +} + +// UpdateAccountInfo retrieves balances for all enabled currencies +func (ku *Kucoin) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { + holding := account.Holdings{ + Exchange: ku.Name, + } + err := ku.CurrencyPairs.IsAssetEnabled(assetType) + if err != nil { + return holding, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) + } + switch assetType { + case asset.Futures: + accountH, err := ku.GetFuturesAccountOverview(ctx, "") + if err != nil { + return account.Holdings{}, err + } + holding.Accounts = append(holding.Accounts, account.SubAccount{ + AssetType: assetType, + Currencies: []account.Balance{{ + Currency: currency.NewCode(accountH.Currency), + Total: accountH.AvailableBalance + accountH.FrozenFunds, + Hold: accountH.FrozenFunds, + Free: accountH.AvailableBalance, + }}, + }) + case asset.Spot, asset.Margin: + accountH, err := ku.GetAllAccounts(ctx, "", ku.accountTypeToString(assetType)) + if err != nil { + return account.Holdings{}, err + } + for x := range accountH { + holding.Accounts = append(holding.Accounts, account.SubAccount{ + AssetType: assetType, + Currencies: []account.Balance{ + { + Currency: currency.NewCode(accountH[x].Currency), + Total: accountH[x].Balance, + Hold: accountH[x].Holds, + Free: accountH[x].Available, + }}, + }) + } + default: + return holding, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) + } + return holding, nil +} + +// FetchAccountInfo retrieves balances for all enabled currencies +func (ku *Kucoin) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { + creds, err := ku.GetCredentials(ctx) + if err != nil { + return account.Holdings{}, err + } + acc, err := account.GetHoldings(ku.Name, creds, assetType) + if err != nil { + return ku.UpdateAccountInfo(ctx, assetType) + } + return acc, nil +} + +// GetAccountFundingHistory returns funding history, deposits and +// withdrawals +func (ku *Kucoin) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) { + withdrawalsData, err := ku.GetWithdrawalList(ctx, "", "", time.Time{}, time.Time{}) + if err != nil { + return nil, err + } + depositsData, err := ku.GetHistoricalDepositList(ctx, "", "", time.Time{}, time.Time{}) + if err != nil { + return nil, err + } + fundingData := make([]exchange.FundingHistory, len(withdrawalsData.Items)+len(depositsData.Items)) + for x := range depositsData.Items { + fundingData[x] = exchange.FundingHistory{ + Timestamp: depositsData.Items[x].CreatedAt.Time(), + ExchangeName: ku.Name, + TransferType: "deposit", + CryptoTxID: depositsData.Items[x].WalletTxID, + Status: depositsData.Items[x].Status, + Amount: depositsData.Items[x].Amount, + Currency: depositsData.Items[x].Currency, + } + } + length := len(depositsData.Items) + for x := range withdrawalsData.Items { + fundingData[length+x] = exchange.FundingHistory{ + Fee: withdrawalsData.Items[x].Fee, + Timestamp: withdrawalsData.Items[x].UpdatedAt.Time(), + ExchangeName: ku.Name, + TransferType: "withdrawal", + CryptoToAddress: withdrawalsData.Items[x].Address, + CryptoTxID: withdrawalsData.Items[x].WalletTxID, + Status: withdrawalsData.Items[x].Status, + Amount: withdrawalsData.Items[x].Amount, + Currency: withdrawalsData.Items[x].Currency, + TransferID: withdrawalsData.Items[x].ID, + } + } + return fundingData, nil +} + +// GetWithdrawalsHistory returns previous withdrawals data +func (ku *Kucoin) GetWithdrawalsHistory(ctx context.Context, c currency.Code, a asset.Item) ([]exchange.WithdrawalHistory, error) { + err := ku.CurrencyPairs.IsAssetEnabled(a) + if err != nil { + return nil, err + } + switch a { + case asset.Spot: + var withdrawals *HistoricalDepositWithdrawalResponse + withdrawals, err = ku.GetHistoricalWithdrawalList(ctx, c.String(), "", time.Time{}, time.Time{}, 0, 0) + if err != nil { + return nil, err + } + resp := make([]exchange.WithdrawalHistory, len(withdrawals.Items)) + for x := range withdrawals.Items { + resp[x] = exchange.WithdrawalHistory{ + Status: withdrawals.Items[x].Status, + CryptoTxID: withdrawals.Items[x].WalletTxID, + Timestamp: withdrawals.Items[x].CreatedAt.Time(), + Amount: withdrawals.Items[x].Amount, + TransferType: "withdrawal", + Currency: c.String(), + } + } + return resp, nil + case asset.Futures: + var futuresWithdrawals *FuturesWithdrawalsListResponse + futuresWithdrawals, err = ku.GetFuturesWithdrawalList(ctx, c.String(), "", time.Time{}, time.Time{}) + if err != nil { + return nil, err + } + resp := make([]exchange.WithdrawalHistory, len(futuresWithdrawals.Items)) + for y := range futuresWithdrawals.Items { + resp[y] = exchange.WithdrawalHistory{ + Status: futuresWithdrawals.Items[y].Status, + CryptoTxID: futuresWithdrawals.Items[y].WalletTxID, + Timestamp: futuresWithdrawals.Items[y].CreatedAt.Time(), + Amount: futuresWithdrawals.Items[y].Amount, + Currency: c.String(), + TransferType: "withdrawal", + } + } + return resp, nil + default: + return nil, fmt.Errorf("withdrawal %w for asset type %v", asset.ErrNotSupported, a) + } +} + +// GetRecentTrades returns the most recent trades for a currency and asset +func (ku *Kucoin) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) { + p, err := ku.FormatExchangeCurrency(p, assetType) + if err != nil { + return nil, err + } + var resp []trade.Data + switch assetType { + case asset.Futures: + tradeData, err := ku.GetFuturesTradeHistory(ctx, p.String()) + if err != nil { + return nil, err + } + var side order.Side + for i := range tradeData { + side, err = order.StringToOrderSide(tradeData[0].Side) + if err != nil { + return nil, err + } + resp = append(resp, trade.Data{ + TID: tradeData[i].TradeID, + Exchange: ku.Name, + CurrencyPair: p, + AssetType: assetType, + Price: tradeData[i].Price, + Amount: tradeData[i].Size, + Timestamp: tradeData[i].FilledTime.Time(), + Side: side, + }) + } + case asset.Spot, asset.Margin: + tradeData, err := ku.GetTradeHistory(ctx, p.String()) + if err != nil { + return nil, err + } + var side order.Side + for i := range tradeData { + side, err = order.StringToOrderSide(tradeData[0].Side) + if err != nil { + return nil, err + } + resp = append(resp, trade.Data{ + TID: tradeData[i].Sequence, + Exchange: ku.Name, + CurrencyPair: p, + Side: side, + AssetType: assetType, + Price: tradeData[i].Price, + Amount: tradeData[i].Size, + Timestamp: tradeData[i].Time.Time(), + }) + } + default: + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) + } + if ku.IsSaveTradeDataEnabled() { + err := trade.AddTradesToBuffer(ku.Name, resp...) + if err != nil { + return nil, err + } + } + sort.Sort(trade.ByDate(resp)) + return resp, nil +} + +// GetHistoricTrades returns historic trade data within the timeframe provided +func (ku *Kucoin) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) { + return nil, common.ErrFunctionNotSupported +} + +// SubmitOrder submits a new order +func (ku *Kucoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { + err := s.Validate() + if err != nil { + return nil, err + } + sideString, err := ku.orderSideString(s.Side) + if err != nil { + return nil, err + } + if s.Type != order.UnknownType && s.Type != order.Limit && s.Type != order.Market { + return nil, fmt.Errorf("%w only limit and market are supported", order.ErrTypeIsInvalid) + } + s.Pair, err = ku.FormatExchangeCurrency(s.Pair, s.AssetType) + if err != nil { + return nil, err + } + switch s.AssetType { + case asset.Futures: + if s.Leverage == 0 { + s.Leverage = 1 + } + o, err := ku.PostFuturesOrder(ctx, &FuturesOrderParam{ + ClientOrderID: s.ClientOrderID, Side: sideString, Symbol: s.Pair, + OrderType: s.Type.Lower(), Size: s.Amount, Price: s.Price, StopPrice: s.TriggerPrice, + Leverage: s.Leverage, VisibleSize: 0, ReduceOnly: s.ReduceOnly, + PostOnly: s.PostOnly, Hidden: s.Hidden}) + if err != nil { + return nil, err + } + return s.DeriveSubmitResponse(o) + case asset.Spot: + if s.ClientID != "" && s.ClientOrderID == "" { + s.ClientOrderID = s.ClientID + } + 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}) + if err != nil { + return nil, err + } + return s.DeriveSubmitResponse(o) + case asset.Margin: + o, err := ku.PostMarginOrder(ctx, + &MarginOrderParam{ClientOrderID: s.ClientOrderID, + Side: sideString, Symbol: s.Pair, + OrderType: s.Type.Lower(), MarginMode: marginModeToString(s.MarginType), + Price: s.Price, Size: s.Amount, + VisibleSize: s.Amount, PostOnly: s.PostOnly, + Hidden: s.Hidden, AutoBorrow: s.AutoBorrow}) + if err != nil { + return nil, err + } + ret, err := s.DeriveSubmitResponse(o.OrderID) + if err != nil { + return nil, err + } + ret.BorrowSize = o.BorrowSize + ret.LoanApplyID = o.LoanApplyID + return ret, nil + default: + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, s.AssetType) + } +} + +func marginModeToString(mType margin.Type) string { + switch mType { + case margin.Isolated: + return mType.String() + case margin.Multi: + return "cross" + default: + return "" + } +} + +// ModifyOrder will allow of changing orderbook placement and limit to +// market conversion +func (ku *Kucoin) ModifyOrder(_ context.Context, _ *order.Modify) (*order.ModifyResponse, error) { + return nil, common.ErrFunctionNotSupported +} + +// CancelOrder cancels an order by its corresponding ID number +func (ku *Kucoin) CancelOrder(ctx context.Context, ord *order.Cancel) error { + if ord == nil { + return common.ErrNilPointer + } + err := ku.CurrencyPairs.IsAssetEnabled(ord.AssetType) + if err != nil { + return err + } + err = ord.Validate(ord.StandardCancel()) + if err != nil { + return err + } + switch ord.AssetType { + case asset.Spot, asset.Margin: + if ord.OrderID == "" && ord.ClientOrderID == "" { + return errors.New("either OrderID or ClientSuppliedOrderID must be specified") + } + if ord.OrderID != "" { + _, err = ku.CancelSingleOrder(ctx, ord.OrderID) + } else if ord.ClientOrderID != "" || ord.ClientID != "" { + if ord.ClientID != "" && ord.ClientOrderID == "" { + ord.ClientOrderID = ord.ClientID + } + _, err = ku.CancelOrderByClientOID(ctx, ord.ClientOrderID) + } + return err + case asset.Futures: + _, err := ku.CancelFuturesOrder(ctx, ord.OrderID) + if err != nil { + return err + } + } + return nil +} + +// CancelBatchOrders cancels orders by their corresponding ID numbers +func (ku *Kucoin) CancelBatchOrders(_ context.Context, _ []order.Cancel) (*order.CancelBatchResponse, error) { + return nil, common.ErrFunctionNotSupported +} + +// CancelAllOrders cancels all orders associated with a currency pair +func (ku *Kucoin) CancelAllOrders(ctx context.Context, orderCancellation *order.Cancel) (order.CancelAllResponse, error) { + if orderCancellation == nil { + return order.CancelAllResponse{}, common.ErrNilPointer + } + if err := ku.CurrencyPairs.IsAssetEnabled(orderCancellation.AssetType); err != nil { + return order.CancelAllResponse{}, err + } + result := order.CancelAllResponse{} + err := orderCancellation.Validate() + if err != nil { + return result, err + } + var pairString string + if !orderCancellation.Pair.IsEmpty() { + orderCancellation.Pair, err = ku.FormatExchangeCurrency(orderCancellation.Pair, orderCancellation.AssetType) + if err != nil { + return result, err + } + pairString = orderCancellation.Pair.String() + } + var values []string + switch orderCancellation.AssetType { + case asset.Margin, asset.Spot: + tradeType := ku.accountToTradeTypeString(orderCancellation.AssetType, marginModeToString(orderCancellation.MarginType)) + values, err = ku.CancelAllOpenOrders(ctx, pairString, tradeType) + if err != nil { + return order.CancelAllResponse{}, err + } + case asset.Futures: + values, err = ku.CancelAllFuturesOpenOrders(ctx, orderCancellation.Pair.String()) + if err != nil { + return result, err + } + stopOrders, err := ku.CancelAllFuturesStopOrders(ctx, orderCancellation.Pair.String()) + if err != nil { + return result, err + } + values = append(values, stopOrders...) + default: + return order.CancelAllResponse{}, fmt.Errorf("%w %v", asset.ErrNotSupported, orderCancellation.AssetType) + } + result.Status = map[string]string{} + for x := range values { + result.Status[values[x]] = order.Cancelled.String() + } + return result, nil +} + +// GetOrderInfo returns order information based on order ID +func (ku *Kucoin) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (*order.Detail, error) { + if err := ku.CurrencyPairs.IsAssetEnabled(assetType); err != nil { + return nil, err + } + pair, err := ku.FormatExchangeCurrency(pair, assetType) + if err != nil { + return nil, err + } + switch assetType { + case asset.Futures: + orderDetail, err := ku.GetFuturesOrderDetails(ctx, orderID) + if err != nil { + return nil, err + } + enabledPairs, err := ku.GetEnabledPairs(asset.Futures) + if err != nil { + return nil, err + } + nPair, err := enabledPairs.DeriveFrom(orderDetail.Symbol) + if err != nil { + return nil, err + } + oType, err := order.StringToOrderType(orderDetail.OrderType) + if err != nil { + return nil, err + } + side, err := order.StringToOrderSide(orderDetail.Side) + if err != nil { + return nil, err + } + if !pair.IsEmpty() && !nPair.Equal(pair) { + return nil, fmt.Errorf("order with id %s and currency Pair %v does not exist", orderID, pair) + } + return &order.Detail{ + Exchange: ku.Name, + OrderID: orderDetail.ID, + Pair: pair, + Type: oType, + Side: side, + AssetType: assetType, + ExecutedAmount: orderDetail.DealSize, + RemainingAmount: orderDetail.Size - orderDetail.DealSize, + Amount: orderDetail.Size, + Price: orderDetail.Price, + Date: orderDetail.CreatedAt.Time()}, nil + case asset.Spot, asset.Margin: + orderDetail, err := ku.GetOrderByID(ctx, orderID) + if err != nil { + return nil, err + } + nPair, err := currency.NewPairFromString(orderDetail.Symbol) + if err != nil { + return nil, err + } + oType, err := order.StringToOrderType(orderDetail.Type) + if err != nil { + return nil, err + } + side, err := order.StringToOrderSide(orderDetail.Side) + if err != nil { + return nil, err + } + if !pair.IsEmpty() && !nPair.Equal(pair) { + return nil, fmt.Errorf("order with id %s and currency Pair %v does not exist", orderID, pair) + } + return &order.Detail{ + Exchange: ku.Name, + OrderID: orderDetail.ID, + Pair: pair, + Type: oType, + Side: side, + Fee: orderDetail.Fee, + AssetType: assetType, + ExecutedAmount: orderDetail.DealSize, + RemainingAmount: orderDetail.Size - orderDetail.DealSize, + Amount: orderDetail.Size, + Price: orderDetail.Price, + Date: orderDetail.CreatedAt.Time(), + }, nil + default: + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) + } +} + +// GetDepositAddress returns a deposit address for a specified currency +func (ku *Kucoin) GetDepositAddress(ctx context.Context, c currency.Code, _, _ string) (*deposit.Address, error) { + ad, err := ku.GetDepositAddressesV2(ctx, c.Upper().String()) + if err != nil { + fad, err := ku.GetFuturesDepositAddress(ctx, c.String()) + if err != nil { + return nil, err + } + return &deposit.Address{ + Address: fad.Address, + Chain: fad.Chain, + Tag: fad.Memo, + }, nil + } + if len(ad) > 1 { + return nil, errMultipleDepositAddress + } else if len(ad) == 0 { + return nil, errNoDepositAddress + } + return &deposit.Address{ + Address: ad[0].Address, + Chain: ad[0].Chain, + Tag: ad[0].Memo, + }, nil +} + +// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is +// submitted +// The endpoint was deprecated for futures, please transfer assets from the FUTURES account to the MAIN account first, +// and then withdraw from the MAIN account +func (ku *Kucoin) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { + if err := withdrawRequest.Validate(); err != nil { + return nil, err + } + withdrawalID, err := ku.ApplyWithdrawal(ctx, withdrawRequest.Currency.String(), withdrawRequest.Crypto.Address, withdrawRequest.Crypto.AddressTag, withdrawRequest.Description, withdrawRequest.Crypto.Chain, "INTERNAL", false, withdrawRequest.Amount) + if err != nil { + return nil, err + } + return &withdraw.ExchangeResponse{ + ID: withdrawalID, + }, nil +} + +// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted +func (ku *Kucoin) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { + return nil, common.ErrFunctionNotSupported +} + +// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is +// submitted +func (ku *Kucoin) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { + return nil, common.ErrFunctionNotSupported +} + +func orderTypeToString(oType order.Type) (string, error) { + switch oType { + case order.Limit: + return "limit", nil + case order.Market: + return "market", nil + case order.StopLimit: + return "limit_stop", nil + case order.StopMarket: + return "market_stop", nil + case order.AnyType, order.UnknownType: + return "", nil + default: + return "", order.ErrUnsupportedOrderType + } +} + +// GetActiveOrders retrieves any orders that are active/open +func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error) { + if getOrdersRequest == nil { + return nil, common.ErrNilPointer + } + err := ku.CurrencyPairs.IsAssetEnabled(getOrdersRequest.AssetType) + if err != nil { + return nil, err + } + if getOrdersRequest.Validate() != nil { + return nil, err + } + format, err := ku.GetPairFormat(getOrdersRequest.AssetType, true) + if err != nil { + return nil, err + } + pair := "" + orders := []order.Detail{} + switch getOrdersRequest.AssetType { + case asset.Futures: + if len(getOrdersRequest.Pairs) == 1 { + pair = format.Format(getOrdersRequest.Pairs[0]) + } + sideString, err := ku.orderSideString(getOrdersRequest.Side) + if err != nil { + return nil, err + } + oType, err := orderTypeToString(getOrdersRequest.Type) + if err != nil { + return nil, err + } + futuresOrders, err := ku.GetFuturesOrders(ctx, "active", pair, sideString, oType, getOrdersRequest.StartTime, getOrdersRequest.EndTime) + 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) + if err != nil { + if errors.Is(err, currency.ErrPairNotFound) { + continue + } + return nil, err + } + for i := range getOrdersRequest.Pairs { + if !getOrdersRequest.Pairs[i].Equal(dPair) { + continue + } + side, err := order.StringToOrderSide(futuresOrders.Items[x].Side) + if err != nil { + return nil, err + } + oType, err := order.StringToOrderType(futuresOrders.Items[x].OrderType) + if err != nil { + return nil, fmt.Errorf("asset type: %v order type: %v err: %w", getOrdersRequest.AssetType, getOrdersRequest.Type, err) + } + orders = append(orders, order.Detail{ + OrderID: futuresOrders.Items[x].ID, + Amount: futuresOrders.Items[x].Size, + RemainingAmount: futuresOrders.Items[x].Size - futuresOrders.Items[x].FilledSize, + ExecutedAmount: futuresOrders.Items[x].FilledSize, + Exchange: ku.Name, + Date: futuresOrders.Items[x].CreatedAt.Time(), + LastUpdated: futuresOrders.Items[x].UpdatedAt.Time(), + Price: futuresOrders.Items[x].Price, + Side: side, + Type: oType, + Pair: dPair, + }) + } + } + case asset.Spot, asset.Margin: + if len(getOrdersRequest.Pairs) == 1 { + pair = format.Format(getOrdersRequest.Pairs[0]) + } + sideString, err := ku.orderSideString(getOrdersRequest.Side) + if err != nil { + return nil, err + } + oType, err := ku.orderTypeToString(getOrdersRequest.Type) + if err != nil { + return nil, fmt.Errorf("asset type: %v order type: %v err: %w", getOrdersRequest.AssetType, getOrdersRequest.Type, err) + } + spotOrders, err := ku.ListOrders(ctx, "active", pair, sideString, oType, "", getOrdersRequest.StartTime, getOrdersRequest.EndTime) + 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 spotOrders.Items { + if !spotOrders.Items[x].IsActive { + continue + } + dPair, err := enabledPairs.DeriveFrom(spotOrders.Items[x].Symbol) + if err != nil { + if errors.Is(err, currency.ErrPairNotFound) { + continue + } + return nil, err + } + if len(getOrdersRequest.Pairs) > 0 && !getOrdersRequest.Pairs.Contains(dPair, true) { + continue + } + side, err := order.StringToOrderSide(spotOrders.Items[x].Side) + if err != nil { + return nil, err + } + oType, err := order.StringToOrderType(spotOrders.Items[x].TradeType) + if err != nil { + return nil, err + } + orders = append(orders, order.Detail{ + OrderID: spotOrders.Items[x].ID, + Amount: spotOrders.Items[x].Size, + RemainingAmount: spotOrders.Items[x].Size - spotOrders.Items[x].DealSize, + ExecutedAmount: spotOrders.Items[x].DealSize, + Exchange: ku.Name, + Date: spotOrders.Items[x].CreatedAt.Time(), + Price: spotOrders.Items[x].Price, + Side: side, + Type: oType, + Pair: dPair, + }) + } + default: + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, getOrdersRequest.AssetType) + } + return orders, nil +} + +// GetOrderHistory retrieves account order information +// Can Limit response to specific order status +func (ku *Kucoin) GetOrderHistory(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error) { + if getOrdersRequest == nil { + return nil, common.ErrNilPointer + } + err := ku.CurrencyPairs.IsAssetEnabled(getOrdersRequest.AssetType) + if err != nil { + return nil, err + } + if getOrdersRequest.Validate() != nil { + return nil, err + } + var sideString string + sideString, err = ku.orderSideString(getOrdersRequest.Side) + if err != nil { + return nil, err + } + var orders []order.Detail + var orderSide order.Side + var orderStatus order.Status + var oType order.Type + var pair currency.Pair + switch getOrdersRequest.AssetType { + case asset.Futures: + var futuresOrders *FutureOrdersResponse + var newOrders *FutureOrdersResponse + if len(getOrdersRequest.Pairs) == 0 { + futuresOrders, err = ku.GetFuturesOrders(ctx, "", "", sideString, getOrdersRequest.Type.Lower(), getOrdersRequest.StartTime, getOrdersRequest.EndTime) + if err != nil { + return nil, err + } + } else { + for x := range getOrdersRequest.Pairs { + getOrdersRequest.Pairs[x], err = ku.FormatExchangeCurrency(getOrdersRequest.Pairs[x], getOrdersRequest.AssetType) + if err != nil { + return nil, err + } + newOrders, err = ku.GetFuturesOrders(ctx, "", getOrdersRequest.Pairs[x].String(), sideString, getOrdersRequest.Type.Lower(), getOrdersRequest.StartTime, getOrdersRequest.EndTime) + if err != nil { + return nil, fmt.Errorf("%w while fetching for symbol %s", err, getOrdersRequest.Pairs[x].String()) + } + if futuresOrders == nil { + futuresOrders = newOrders + } else { + futuresOrders.Items = append(futuresOrders.Items, newOrders.Items...) + } + } + } + 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) + if err != nil { + if errors.Is(err, currency.ErrPairNotFound) { + continue + } + return nil, err + } + oType, err = order.StringToOrderType(futuresOrders.Items[i].OrderType) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", ku.Name, err) + } + orders = append(orders, order.Detail{ + Price: futuresOrders.Items[i].Price, + Amount: futuresOrders.Items[i].Size, + ExecutedAmount: futuresOrders.Items[i].DealSize, + RemainingAmount: futuresOrders.Items[i].Size - futuresOrders.Items[i].DealSize, + Date: futuresOrders.Items[i].CreatedAt.Time(), + Exchange: ku.Name, + OrderID: futuresOrders.Items[i].ID, + Side: orderSide, + Status: orderStatus, + Type: oType, + Pair: pair, + }) + orders[i].InferCostsAndTimes() + } + case asset.Spot, asset.Margin: + var responseOrders *OrdersListResponse + var newOrders *OrdersListResponse + if len(getOrdersRequest.Pairs) == 0 { + responseOrders, err = ku.ListOrders(ctx, "", "", sideString, getOrdersRequest.Type.Lower(), "", getOrdersRequest.StartTime, getOrdersRequest.EndTime) + if err != nil { + return nil, err + } + } else { + for x := range getOrdersRequest.Pairs { + newOrders, err = ku.ListOrders(ctx, "", getOrdersRequest.Pairs[x].String(), sideString, getOrdersRequest.Type.Lower(), "", getOrdersRequest.StartTime, getOrdersRequest.EndTime) + if err != nil { + return nil, fmt.Errorf("%w while fetching for symbol %s", err, getOrdersRequest.Pairs[x].String()) + } + if responseOrders == nil { + responseOrders = newOrders + } else { + responseOrders.Items = append(responseOrders.Items, newOrders.Items...) + } + } + } + orders = make([]order.Detail, len(responseOrders.Items)) + for i := range orders { + orderSide, err = order.StringToOrderSide(responseOrders.Items[i].Side) + if err != nil { + return nil, err + } + var orderStatus order.Status + pair, err = currency.NewPairFromString(responseOrders.Items[i].Symbol) + if err != nil { + return nil, err + } + var oType order.Type + oType, err = order.StringToOrderType(responseOrders.Items[i].Type) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", ku.Name, err) + } + orders[i] = order.Detail{ + Price: responseOrders.Items[i].Price, + Amount: responseOrders.Items[i].Size, + ExecutedAmount: responseOrders.Items[i].DealSize, + RemainingAmount: responseOrders.Items[i].Size - responseOrders.Items[i].DealSize, + Date: responseOrders.Items[i].CreatedAt.Time(), + Exchange: ku.Name, + OrderID: responseOrders.Items[i].ID, + Side: orderSide, + Status: orderStatus, + Type: oType, + Pair: pair, + } + orders[i].InferCostsAndTimes() + } + } + return getOrdersRequest.Filter(ku.Name, orders), nil +} + +// GetFeeByType returns an estimate of fee based on the type of transaction +func (ku *Kucoin) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { + if feeBuilder == nil { + return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) + } + if !ku.AreCredentialsValid(ctx) && + feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { + feeBuilder.FeeType = exchange.OfflineTradeFee + } + if feeBuilder.Pair.IsEmpty() { + return 0, currency.ErrCurrencyPairEmpty + } + switch feeBuilder.FeeType { + case exchange.CryptocurrencyWithdrawalFee, + exchange.CryptocurrencyTradeFee: + fee, err := ku.GetTradingFee(ctx, feeBuilder.Pair.String()) + if err != nil { + return 0, err + } + if feeBuilder.IsMaker { + return feeBuilder.Amount * fee[0].MakerFeeRate, nil + } + return feeBuilder.Amount * fee[0].TakerFeeRate, nil + case exchange.OfflineTradeFee: + return feeBuilder.Amount * 0.001, nil + case exchange.CryptocurrencyDepositFee: + return 0, nil + default: + if !feeBuilder.FiatCurrency.IsEmpty() { + fee, err := ku.GetBasicFee(ctx, "1") + if err != nil { + return 0, err + } + if feeBuilder.IsMaker { + return feeBuilder.Amount * fee.MakerFeeRate, nil + } + return feeBuilder.Amount * fee.TakerFeeRate, nil + } + return 0, fmt.Errorf("can't construct fee") + } +} + +// ValidateCredentials validates current credentials used for wrapper +func (ku *Kucoin) ValidateCredentials(ctx context.Context, assetType asset.Item) error { + err := ku.CurrencyPairs.IsAssetEnabled(assetType) + if err != nil { + return err + } + _, err = ku.UpdateAccountInfo(ctx, assetType) + return ku.CheckTransientError(err) +} + +// GetHistoricCandles returns candles between a time period for a set time interval +func (ku *Kucoin) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { + req, err := ku.GetKlineRequest(pair, a, interval, start, end, false) + if err != nil { + return nil, err + } + var timeseries []kline.Candle + switch a { + case asset.Futures: + var candles []FuturesKline + candles, err := ku.GetFuturesKline(ctx, int64(interval.Duration().Minutes()), req.RequestFormatted.String(), req.Start, req.End) + if err != nil { + return nil, err + } + for x := range candles { + timeseries = append( + timeseries, kline.Candle{ + Time: candles[x].StartTime, + Open: candles[x].Open, + High: candles[x].High, + Low: candles[x].Low, + Close: candles[x].Close, + Volume: candles[x].Volume, + }) + } + case asset.Spot, asset.Margin: + intervalString, err := ku.intervalToString(interval) + if err != nil { + return nil, err + } + var candles []Kline + candles, err = ku.GetKlines(ctx, req.RequestFormatted.String(), intervalString, req.Start, req.End) + if err != nil { + return nil, err + } + for x := range candles { + timeseries = append( + timeseries, kline.Candle{ + Time: candles[x].StartTime, + Open: candles[x].Open, + High: candles[x].High, + Low: candles[x].Low, + Close: candles[x].Close, + Volume: candles[x].Volume, + }) + } + } + return req.ProcessResponse(timeseries) +} + +// GetHistoricCandlesExtended returns candles between a time period for a set time interval +func (ku *Kucoin) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { + req, err := ku.GetKlineExtendedRequest(pair, a, interval, start, end) + if err != nil { + return nil, err + } + var timeSeries []kline.Candle + for x := range req.RangeHolder.Ranges { + switch a { + case asset.Futures: + var candles []FuturesKline + candles, err = ku.GetFuturesKline(ctx, int64(interval.Duration().Minutes()), req.RequestFormatted.String(), req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time) + if err != nil { + return nil, err + } + for x := range candles { + timeSeries = append( + timeSeries, kline.Candle{ + Time: candles[x].StartTime, + Open: candles[x].Open, + High: candles[x].High, + Low: candles[x].Low, + Close: candles[x].Close, + Volume: candles[x].Volume, + }) + } + case asset.Spot, asset.Margin: + var intervalString string + intervalString, err = ku.intervalToString(interval) + if err != nil { + return nil, err + } + var candles []Kline + candles, err = ku.GetKlines(ctx, req.RequestFormatted.String(), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time) + if err != nil { + return nil, err + } + for x := range candles { + timeSeries = append( + timeSeries, kline.Candle{ + Time: candles[x].StartTime, + Open: candles[x].Open, + High: candles[x].High, + Low: candles[x].Low, + Close: candles[x].Close, + Volume: candles[x].Volume, + }) + } + } + } + return req.ProcessResponse(timeSeries) +} + +// GetServerTime returns the current exchange server time. +func (ku *Kucoin) GetServerTime(ctx context.Context, a asset.Item) (time.Time, error) { + switch a { + case asset.Spot, asset.Margin: + return ku.GetCurrentServerTime(ctx) + case asset.Futures: + return ku.GetFuturesServerTime(ctx) + default: + return time.Time{}, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + } +} + +// GetAvailableTransferChains returns the available transfer blockchains for the specific +// cryptocurrency +func (ku *Kucoin) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) { + if cryptocurrency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + currencyDetail, err := ku.GetCurrencyDetail(ctx, cryptocurrency.String(), "") + if err != nil { + return nil, err + } + chains := make([]string, 0, len(currencyDetail.Chains)) + for x := range currencyDetail.Chains { + chains = append(chains, currencyDetail.Chains[x].Name) + } + return chains, nil +} + +// ValidateAPICredentials validates current credentials used for wrapper +// functionality +func (ku *Kucoin) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error { + _, err := ku.UpdateAccountInfo(ctx, assetType) + return ku.CheckTransientError(err) +} diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 253d7410..970bc615 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -67,6 +67,11 @@ type Submit struct { TriggerPrice float64 ClientID string // TODO: Shift to credentials ClientOrderID string + + // The system will first borrow you funds at the optimal interest rate and then place an order for you. + // see kucoin_wrapper.go + AutoBorrow bool + // MarginType such as isolated or cross margin for when an exchange // supports margin type definition when submitting an order eg okx MarginType margin.Type @@ -77,6 +82,9 @@ type Submit struct { // RetrieveFeeDelay some exchanges take time to properly save order data // and cannot retrieve fees data immediately RetrieveFeeDelay time.Duration + + // Hidden when enabled orders not displaying in order book. + Hidden bool // TradeMode specifies the trading mode for margin and non-margin orders: see okcoin_wrapper.go TradeMode string } @@ -109,6 +117,9 @@ type SubmitResponse struct { Fee float64 FeeAsset currency.Code Cost float64 + + BorrowSize float64 + LoanApplyID string MarginType margin.Type } diff --git a/exchanges/support.go b/exchanges/support.go index 81ffe9bd..65875fb5 100644 --- a/exchanges/support.go +++ b/exchanges/support.go @@ -34,6 +34,7 @@ var Exchanges = []string{ "huobi", "itbit", "kraken", + "kucoin", "lbank", "okcoin", "okx", diff --git a/exchanges/trade/README.md b/exchanges/trade/README.md index 3969b2f9..ae3f2c40 100644 --- a/exchanges/trade/README.md +++ b/exchanges/trade/README.md @@ -80,6 +80,7 @@ _b in this context is an `IBotExchange` implemented struct_ | Huobi.Pro | Yes | Yes | No | | ItBit | Yes | NA | No | | Kraken | Yes | Yes | No | +| Kucoin | Yes | No | Yes | | Lbank | Yes | No | Yes | | Okcoin | Yes | Yes | Yes | | Okx | Yes | Yes | Yes | diff --git a/testdata/configtest.json b/testdata/configtest.json index dce77574..2bc79f4d 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -1977,6 +1977,113 @@ } ] }, + { + "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", + "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", + "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": "" + } + ] +}, { "name": "Okcoin", "enabled": true, diff --git a/testdata/exchangelist.csv b/testdata/exchangelist.csv index fda1c261..92dfea9f 100644 --- a/testdata/exchangelist.csv +++ b/testdata/exchangelist.csv @@ -17,6 +17,7 @@ hitbtc, huobi, itbit, kraken, +kucoin, lbank, okcoin, okx,