From 3eac6d12bd7b446445cc3624140a2bc5129b5c13 Mon Sep 17 00:00:00 2001 From: Samuael A <39623015+samuael@users.noreply.github.com> Date: Tue, 30 May 2023 04:03:53 +0000 Subject: [PATCH] exchanges: Update GateIO exchange to V4 (#1058) * Adding Public Endpoints and test functions * Adding public endpoints and test functions * Adding private spot endpoints * Adding private endpoints and corresponding tests for margin * Adding Margin Private endpoints * Adding cross margin and flash swap endpoints * Adding futures private endpoints * Adding futures private endpoints and corresponding tests * Adding Options and SubAccount endpoints and their unit tests * Adding Wrapper functions * Complete wrapper functions and corresponding unit test functions * Fixing wrapper issues and adding websocket functions * Update of Spot websocket and adding futures websocket handlers * completed futures WS push data endpoints * Completed Options websocket endpoints * Adding websocket support for delivery futures and slight update on endpoint funcs * Added Delivery websocket support and fix linter issues * Update on Unit tests * fix slight currency format error * Fix slight endpoint tempos * Update on conditional statements and unit tests issues * fixing slight tempos * Slight model and websocket data push method change * Fix unit test tempos and updating models * Fix on code structures and update on unit tests * Slight code fix * Remove print statements * Update on tradable pairs fetch eps * Fix websocket tempos * Adding types to websocket routine manager * Fix slight issues * Slight fixes * Updating wrapper funcs and models * Slight update * Update on test * Update on tradable pairs * update conditional statements * Fixing slight issues * Updating unit tests * Minor fixes depending review comments * Remove redundant method declaration * Adding missing intervals * Updating fetch tradable pairs * update tradable pairs issues * Addressing small tempos * Slight fix on ticker * Minor Fixes * Minor review comment fixes * Unit test and minor code updates * Slight code updates * Minor updates depending review comments * Fixes * Updating incoming message matcher * Fix missing merge issue * Fix minor wrapper issues * Updating ratelimit and other issues * Updating endpoint models and adding missing eps * Update on code structure and models * Minor codespell fixes * Minor update on models * fix unit test panic * Minor race fix * Fix issues in generating signature and unit tests * Minor update on wrapper and unit tests * Minor fix on wrapper * Mini linter issues fix * Minor fix * endpoint fixes and slight update * Minor fixes * Updating exchange functions and unit tests * Unit test and wrapper updates * Remove options candlestick support * Minor unit test and wrapper fix * Unit test update * minor fix on unit test and wrapper * endpoints constants name change * Add minor wrapper issues * endpoint constants update * endpoint url updates * Updating subscriptions * fixing dual mode endpoint methods * minor fix * rm small tempo * Update on websocket orderbook handling * Orderbook and currency pair update * fix linter and test issues * minor helper function update * Fix wrapper coverage and wrapper issues * delete unused variables * Minor fix on ReadData() call * separating websocket handlers * separating websocket handlers * Minor fix on enabled pair * minor fix * check instrument availability in spot * create a separate subscriber for sake of multiple websocket connection * linter fix * minor websocket and gateio endpoints fix * fix nil pointer exception * minor fixes * spelling fix decerializes -> deserializes * fix Bitfinex unit test issues * minor unknown currency pair labling fix * minor currency pair handling fix * slight update on GetDepositAddress wrapper unit test * setting max request job to 200 * fixing numerical and timestamp type convert * fix value overflow error * change method of parsing orderbook price * unifying timestamp conversion types to gateioTime --------- Co-authored-by: Samuael Adnew --- .gitignore | 1 + config_example.json | 41 +- currency/code.go | 2 +- currency/code_types.go | 1 + currency/pair.go | 24 +- currency/pair_test.go | 19 + engine/websocketroutine_manager.go | 58 + exchanges/asset/asset.go | 18 +- exchanges/bitfinex/bitfinex_test.go | 20 +- exchanges/bitfinex/bitfinex_websocket.go | 4 +- exchanges/gateio/gateio.go | 4188 +++++++++++++++-- exchanges/gateio/gateio_convert.go | 133 + exchanges/gateio/gateio_test.go | 3927 +++++++++++++--- exchanges/gateio/gateio_types.go | 3092 ++++++++++-- exchanges/gateio/gateio_websocket.go | 1381 +++--- exchanges/gateio/gateio_wrapper.go | 1947 ++++++-- .../gateio/gateio_ws_delivery_futures.go | 333 ++ exchanges/gateio/gateio_ws_futures.go | 870 ++++ exchanges/gateio/gateio_ws_option.go | 780 +++ exchanges/gateio/ratelimiter.go | 118 + exchanges/kline/kline.go | 10 + exchanges/kline/kline_test.go | 20 + exchanges/kline/kline_types.go | 62 +- exchanges/request/request.go | 2 +- testdata/configtest.json | 173 +- 25 files changed, 14353 insertions(+), 2871 deletions(-) create mode 100644 exchanges/gateio/gateio_convert.go create mode 100644 exchanges/gateio/gateio_ws_delivery_futures.go create mode 100644 exchanges/gateio/gateio_ws_futures.go create mode 100644 exchanges/gateio/gateio_ws_option.go create mode 100644 exchanges/gateio/ratelimiter.go diff --git a/.gitignore b/.gitignore index 0afe8d44..66fd6196 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ __debug_bin # Coverage reports coverage.txt +wrapperconfig.json \ No newline at end of file diff --git a/config_example.json b/config_example.json index 4bc0098e..4a5de200 100644 --- a/config_example.json +++ b/config_example.json @@ -1492,7 +1492,7 @@ "baseCurrencies": "USD", "currencyPairs": { "requestFormat": { - "uppercase": false, + "uppercase": true, "delimiter": "_" }, "configFormat": { @@ -1501,12 +1501,43 @@ }, "useGlobalFormat": true, "assetTypes": [ - "spot" + "spot", + "option", + "futures", + "cross_margin", + "margin", + "delivery" ], "pairs": { "spot": { - "enabled": "BTC_USDT", - "available": "USDT_CNYX,BTC_CNYX,ETH_CNYX,EOS_CNYX,BCH_CNYX,XRP_CNYX,DOGE_CNYX,TIPS_CNYX,BTC_USDC,BTC_PAX,BTC_USDT,BCH_USDT,ETH_USDT,ETC_USDT,QTUM_USDT,LTC_USDT,DASH_USDT,ZEC_USDT,BTM_USDT,EOS_USDT,REQ_USDT,SNT_USDT,OMG_USDT,PAY_USDT,CVC_USDT,ZRX_USDT,TNT_USDT,XMR_USDT,XRP_USDT,DOGE_USDT,BAT_USDT,PST_USDT,BTG_USDT,DPY_USDT,LRC_USDT,STORJ_USDT,RDN_USDT,STX_USDT,KNC_USDT,LINK_USDT,CDT_USDT,AE_USDT,AE_ETH,AE_BTC,CDT_ETH,RDN_ETH,STX_ETH,KNC_ETH,LINK_ETH,REQ_ETH,RCN_ETH,TRX_ETH,ARN_ETH,BNT_ETH,VET_ETH,MCO_ETH,FUN_ETH,DATA_ETH,RLC_ETH,RLC_USDT,ZSC_ETH,WINGS_ETH,MDA_ETH,RCN_USDT,TRX_USDT,VET_USDT,MCO_USDT,FUN_USDT,DATA_USDT,ZSC_USDT,MDA_USDT,XTZ_USDT,XTZ_BTC,XTZ_ETH,GNT_USDT,GNT_ETH,GEM_USDT,GEM_ETH,RFR_USDT,RFR_ETH,DADI_USDT,DADI_ETH,ABT_USDT,ABT_ETH,LEDU_BTC,LEDU_ETH,OST_USDT,OST_ETH,XLM_USDT,XLM_ETH,XLM_BTC,MOBI_USDT,MOBI_ETH,MOBI_BTC,OCN_USDT,OCN_ETH,OCN_BTC,ZPT_USDT,ZPT_ETH,ZPT_BTC,COFI_USDT,COFI_ETH,JNT_USDT,JNT_ETH,JNT_BTC,BLZ_USDT,BLZ_ETH,GXS_USDT,GXS_BTC,MTN_USDT,MTN_ETH,RUFF_USDT,RUFF_ETH,RUFF_BTC,TNC_USDT,TNC_ETH,TNC_BTC,ZIL_USDT,ZIL_ETH,BTO_USDT,BTO_ETH,THETA_USDT,THETA_ETH,DDD_USDT,DDD_ETH,DDD_BTC,MKR_USDT,MKR_ETH,DAI_USDT,SMT_USDT,SMT_ETH,MDT_USDT,MDT_ETH,MDT_BTC,MANA_USDT,MANA_ETH,LUN_USDT,LUN_ETH,SALT_USDT,SALT_ETH,FUEL_USDT,FUEL_ETH,ELF_USDT,ELF_ETH,DRGN_USDT,DRGN_ETH,GTC_USDT,GTC_ETH,GTC_BTC,QLC_USDT,QLC_BTC,QLC_ETH,DBC_USDT,DBC_BTC,DBC_ETH,BNTY_USDT,BNTY_ETH,LEND_USDT,LEND_ETH,ICX_USDT,ICX_ETH,BTF_USDT,BTF_BTC,ADA_USDT,ADA_BTC,LSK_USDT,LSK_BTC,WAVES_USDT,WAVES_BTC,BIFI_USDT,BIFI_BTC,MDS_ETH,MDS_USDT,DGD_USDT,DGD_ETH,QASH_USDT,QASH_ETH,QASH_BTC,POWR_USDT,POWR_ETH,POWR_BTC,FIL_USDT,BCD_USDT,BCD_BTC,SBTC_USDT,SBTC_BTC,GOD_USDT,GOD_BTC,BCX_USDT,BCX_BTC,QSP_USDT,QSP_ETH,INK_BTC,INK_USDT,INK_ETH,INK_QTUM,QBT_QTUM,QBT_ETH,QBT_USDT,TSL_QTUM,TSL_USDT,GNX_USDT,GNX_ETH,NEO_USDT,GAS_USDT,NEO_BTC,GAS_BTC,IOTA_USDT,IOTA_BTC,NAS_USDT,NAS_ETH,NAS_BTC,ETH_BTC,ETC_BTC,ETC_ETH,ZEC_BTC,DASH_BTC,LTC_BTC,BCH_BTC,BTG_BTC,QTUM_BTC,QTUM_ETH,XRP_BTC,DOGE_BTC,XMR_BTC,ZRX_BTC,ZRX_ETH,DNT_ETH,DPY_ETH,OAX_BTC,OAX_USDT,OAX_ETH,REP_ETH,LRC_ETH,LRC_BTC,PST_ETH,BCDN_ETH,BCDN_USDT,TNT_ETH,SNT_ETH,SNT_BTC,BTM_ETH,BTM_BTC,SNET_ETH,SNET_USDT,LLT_SNET,OMG_ETH,OMG_BTC,PAY_ETH,PAY_BTC,BAT_ETH,BAT_BTC,CVC_ETH,STORJ_ETH,STORJ_BTC,EOS_ETH,EOS_BTC,BTS_USDT,BTS_BTC,TIPS_ETH,GT_BTC,GT_USDT,ATOM_BTC,ATOM_USDT,KAVA_USDT,ANKR_USDT,RSR_USDT,RSV_USDT,KAI_USDT,COMP_USDT,SOL_USDT,COTI_USDT,LBK_USDT,BTMX_USDT,XEM_ETH,XEM_USDT,XEM_BTC,BU_USDT,BU_ETH,BU_BTC,HNS_BTC,HNS_USDT,BTC3L_USDT,BTC3S_USDT,ETH3L_USDT,ETH3S_USDT,EOS3L_USDT,EOS3S_USDT,BSV3L_USDT,BSV3S_USDT,BCH3L_USDT,BCH3S_USDT,LTC3L_USDT,LTC3S_USDT,XTZ3L_USDT,XTZ3S_USDT,BCHSV_USDT,BCHSV_CNYX,BCHSV_BTC,RVN_USDT,RVC_USDT,AR_USDT,DCR_USDT,DCR_BTC,BCN_USDT,BCN_BTC,XMC_USDT,XMC_BTC,STEEM_USDT,HIVE_USDT,ATP_USDT,ATP_ETH,NAX_USDT,NAX_ETH,KLAY_USDT,NBOT_ETH,NBOT_USDT,MED_USDT,MED_ETH,GRIN_USDT,GRIN_ETH,GRIN_BTC,BEAM_USDT,BEAM_ETH,BEAM_BTC,HBAR_USDT,OKB_USDT,VTHO_ETH,BTT_USDT,BTT_ETH,BTT_TRX,TFUEL_ETH,TFUEL_USDT,CELR_ETH,CELR_USDT,CS_ETH,CS_USDT,MAN_ETH,MAN_USDT,REM_ETH,REM_USDT,LYM_ETH,LYM_BTC,LYM_USDT,ONG_ETH,ONG_USDT,ONT_ETH,ONT_USDT,BFT_ETH,BFT_USDT,IHT_ETH,IHT_USDT,SENC_ETH,SENC_USDT,TOMO_ETH,TOMO_USDT,ELEC_ETH,ELEC_USDT,HAV_ETH,HAV_USDT,SWTH_ETH,SWTH_USDT,NKN_ETH,NKN_USDT,SOUL_ETH,SOUL_USDT,LRN_ETH,LRN_USDT,EOSDAC_ETH,EOSDAC_USDT,DOCK_USDT,DOCK_ETH,GSE_USDT,GSE_ETH,RATING_USDT,RATING_ETH,HSC_USDT,HSC_ETH,HIT_USDT,HIT_ETH,DX_USDT,DX_ETH,CNNS_ETH,CNNS_USDT,DREP_ETH,DREP_USDT,MBL_USDT,MBL_ETH,GMAT_USDT,GMAT_ETH,MIX_USDT,MIX_ETH,LAMB_USDT,LAMB_ETH,LEO_USDT,LEO_BTC,BTCBULL_USDT,BTCBEAR_USDT,ETHBEAR_USDT,ETHBULL_USDT,EOSBULL_USDT,EOSBEAR_USDT,XRPBEAR_USDT,XRPBULL_USDT,WICC_USDT,WICC_ETH,SERO_USDT,SERO_ETH,VIDY_USDT,VIDY_ETH,KGC_USDT,FTM_USDT,FTM_ETH,COS_USDT,CRO_USDT,ALY_USDT,WIN_USDT,MTV_USDT,ONE_USDT,ARPA_USDT,ARPA_ETH,DILI_USDT,ALGO_USDT,PI_USDT,CKB_USDT,CKB_BTC,CKB_ETH,BKC_USDT,BXC_USDT,BXC_ETH,PAX_USDT,PAX_CNYX,USDC_CNYX,USDC_USDT,TUSD_CNYX,TUSD_USDT,HC_USDT,HC_BTC,HC_ETH,GARD_USDT,GARD_ETH,FTI_USDT,FTI_ETH,SOP_ETH,SOP_USDT,LEMO_USDT,LEMO_ETH,QKC_USDT,QKC_ETH,QKC_BTC,IOTX_USDT,IOTX_ETH,RED_USDT,RED_ETH,LBA_USDT,LBA_ETH,OPEN_USDT,OPEN_ETH,MITH_USDT,MITH_ETH,SKM_USDT,SKM_ETH,XVG_USDT,XVG_BTC,NANO_USDT,NANO_BTC,HT_USDT,BNB_USDT,MET_ETH,MET_USDT,TCT_ETH,TCT_USDT,MXC_USDT,MXC_BTC,MXC_ETH" + "assetEnabled": true, + "enabled": "IHT_ETH,AME_ETH,CEUR_ETH,ALEPH_USDT,OMG_TRY,BTC_TRY,OGN_USDT,ALA_USDT,BTC_USDT", + "available": "10SET_USDT,1ART_USDT,1EARTH_USDT,1INCH3L_USDT,1INCH3S_USDT,1INCH_ETH,1INCH_TRY,1INCH_USD,1INCH_USDT,88MPH_ETH,88MPH_USDT,A5T_ETH,A5T_USDT,AAA_ETH,AAA_USDT,AAG_USDT,AART_ETH,AART_USDT,AAVE3L_USDT,AAVE3S_USDT,AAVE_ETH,AAVE_TRY,AAVE_USD,AAVE_USDT,ABBC_USDT,ABT_ETH,ABT_USDT,ACA_USDT,ACE_USDT,ACH3L_USDT,ACH3S_USDT,ACH_ETH,ACH_USDT,ACM_USDT,ACS_USDT,ACX_USDT,ADA3L_USDT,ADA3S_USDT,ADAPAD_USDT,ADA_BTC,ADA_TRY,ADA_USDT,ADEL_ETH,ADEL_USDT,ADP_ETH,ADP_USDT,ADS_USDT,ADX_ETH,ADX_USDT,AERGO_ETH,AERGO_USDT,AE_BTC,AE_ETH,AE_USDT,AFC_USDT,AGIX_USDT,AGLA_USDT,AGLD_ETH,AGLD_USD,AGLD_USDT,AGS_USDT,AIOZ_ETH,AIOZ_USDT,AIRTNT_USDT,AIR_USDT,AKITA_USDT,AKRO_ETH,AKRO_USD,AKRO_USDT,AKT_ETH,AKT_USDT,ALAYA_ETH,ALAYA_USDT,ALA_USDT,ALCX_ETH,ALCX_USDT,ALD_ETH,ALD_USDT,ALEPH_ETH,ALEPH_USDT,ALGO3L_USDT,ALGO3S_USDT,ALGO_TRY,ALGO_USDT,ALICE3L_USDT,ALICE3S_USDT,ALICE_ETH,ALICE_USDT,ALI_USDT,ALN_ETH,ALN_USDT,ALPACA_ETH,ALPACA_USDT,ALPA_ETH,ALPA_USDT,ALPHA3L_USDT,ALPHA3S_USDT,ALPHA_ETH,ALPHA_USDT,ALPHR_ETH,ALPHR_USDT,ALPH_USDT,ALPINE_USDT,ALTB_USDT,ALU_ETH,ALU_USDT,ALY_USDT,AME_ETH,AME_USDT,AMKT_USDT,AMPL_USDT,AMP_ETH,AMP_TRY,AMP_USDT,AM_USDT,ANC3L_USDT,ANC3S_USDT,ANC_ETH,ANC_USDT,ANGLE_USDT,ANKR_TRY,ANKR_USDT,ANML_USDT,ANT_USDT,AOG_USDT,APE3L_USDT,APE3S_USDT,APE_TRY,APE_USDT,API33L_USDT,API33S_USDT,API3_ETH,API3_USDT,APN_ETH,APN_USDT,APRT_USDT,APT3L_USDT,APT3S_USDT,APT_BTC,APT_ETH,APT_TRY,APT_USDT,APX_USDT,APYS_ETH,APYS_USDT,AQDC_USDT,AQT_USDT,AR3L_USDT,AR3S_USDT,ARB_USDT,ARCX_ETH,ARCX_USDT,ARES_ETH,ARES_USDT,ARGON_ETH,ARGON_USDT,ARG_USDT,ARPA3L_USDT,ARPA3S_USDT,ARPA_ETH,ARPA_USDT,ARRR_ETH,ARRR_USDT,ARSW_USDT,ARTEM_USDT,ARV_USDT,AR_TRY,AR_USDT,ASD_USDT,ASK_USDT,ASM_ETH,ASM_USDT,ASR_USDT,ASS_USDT,ASTO_USDT,ASTR3L_USDT,ASTR3S_USDT,ASTRA_USDT,ASTRO_ETH,ASTRO_USDT,ASTR_BTC,ASTR_ETH,ASTR_USDT,AST_ETH,AST_USDT,ASW_USDT,ATA_ETH,ATA_USDT,ATD_ETH,ATD_USDT,ATEAM_USDT,ATK_ETH,ATK_USDT,ATLAS_USDT,ATM_USDT,ATOLO_USDT,ATOM3L_USDT,ATOM3S_USDT,ATOM_BTC,ATOM_TRY,ATOM_USDT,ATOZ_USDT,ATP_ETH,ATP_USDT,ATS_USDT,AUCTION_ETH,AUCTION_USDT,AUDIO_ETH,AUDIO_USDT,AURORA_ETH,AURORA_USDT,AUTO_ETH,AUTO_USDT,AVAX3L_USDT,AVAX3S_USDT,AVAX_ETH,AVAX_TRY,AVAX_USDT,AVA_USDT,AVT_USDT,AXIS_ETH,AXIS_USDT,AXL_USDT,AXS3L_USDT,AXS3S_USDT,AXS5L_USDT,AXS5S_USDT,AXS_ETH,AXS_TRY,AXS_USD,AXS_USDT,AZERO_USDT,AZY_USDT,B3X_USDT,BABI_USDT,BABYDOGE_USDT,BABY_USDT,BACON_USDT,BAC_ETH,BAC_USDT,BADGER_ETH,BADGER_USDT,BAGS_ETH,BAGS_USDT,BAJU_USDT,BAKED_ETH,BAKED_USDT,BAKE_ETH,BAKE_USDT,BAL3L_USDT,BAL3S_USDT,BAL_ETH,BAL_USDT,BAMBOO_USDT,BAND_ETH,BAND_USDT,BANK_ETH,BANK_USDT,BASE_ETH,BASE_USDT,BAS_ETH,BAS_USDT,BAT3L_USDT,BAT3S_USDT,BAT_BTC,BAT_ETH,BAT_TRY,BAT_USDT,BBANK_BTC,BBANK_ETH,BBANK_USDT,BBC_USDT,BBF_USDT,BCDN_ETH,BCDN_USDT,BCD_BTC,BCD_USDT,BCH3L_USDT,BCH3S_USDT,BCH5L_USDT,BCH5S_USDT,BCH_BTC,BCH_USD,BCH_USDT,BCMC_USDT,BCN_BTC,BCN_USDT,BCX_BTC,BCX_USDT,BDP_ETH,BDP_USDT,BDT_ETH,BDT_USDT,BDX_USDT,BEAM_BTC,BEAM_ETH,BEAM_USDT,BEEFI_ETH,BEEFI_USDT,BEL_ETH,BEL_USDT,BENQI_ETH,BENQI_USDT,BEPRO_ETH,BEPRO_USDT,BERRY_USDT,BETU_ETH,BETU_USDT,BEYOND_USDT,BFC_ETH,BFC_USDT,BFT1_USDT,BFT_ETH,BFT_USDT,BICO_ETH,BICO_USDT,BIFIF_ETH,BIFIF_USDT,BIFI_BTC,BIFI_USDT,BIN_ETH,BIN_USDT,BIRD_ETH,BIRD_USDT,BITCI_USDT,BIT_USDT,BKC_USDT,BLACK_ETH,BLACK_USDT,BLANKV2_ETH,BLANKV2_USDT,BLES_ETH,BLES_USDT,BLIN_USDT,BLOCK_USDT,BLOK_ETH,BLOK_USDT,BLT_USDT,BLUR_USDT,BLY_BTC,BLY_USDT,BLZ_ETH,BLZ_USDT,BMI_ETH,BMI_USDT,BMON_ETH,BMON_USDT,BNB3L_USDT,BNB3S_USDT,BNB_BTC,BNB_TRY,BNB_USD,BNB_USDT,BNC_USDT,BNTY_ETH,BNTY_USDT,BNT_ETH,BNX_USDT,BOA_USDT,BOBA_ETH,BOBA_USDT,BONDLY_ETH,BONDLY_USDT,BOND_ETH,BOND_USDT,BONE_USDT,BONK_USDT,BOO_ETH,BOO_USDT,BORA_ETH,BORA_USDT,BORING_ETH,BORING_USDT,BOSON_ETH,BOSON_USDT,BOX_USDT,BP_USDT,BRISE_USDT,BRKL_USDT,BRT_USDT,BRWL_USDT,BRY_ETH,BRY_USDT,BSCPAD_ETH,BSCPAD_USDT,BSCS_ETH,BSCS_USDT,BSV3L_USDT,BSV3S_USDT,BSV5L_USDT,BSV5S_USDT,BSV_BTC,BSV_USDT,BSW3L_USDT,BSW3S_USDT,BSW_USDT,BS_USDT,BTC3L_USDT,BTC3S_USDT,BTC5L_USDT,BTC5S_USDT,BTCST_ETH,BTCST_USDT,BTC_TRY,BTC_USD,BTC_USDT,BTF_BTC,BTF_USDT,BTG_BTC,BTG_USDT,BTL_USDT,BTM_BTC,BTM_ETH,BTM_USDT,BTO_ETH,BTO_USDT,BTRST_ETH,BTRST_USDT,BTS_BTC,BTS_USDT,BTT_ETH,BTT_TRY,BTT_USDT,BURP_ETH,BURP_USDT,BUSY_USDT,BUY_ETH,BUY_USDT,BVT_USDT,BXC_ETH,BXC_USDT,BXH_ETH,BXH_USDT,BYN_ETH,BYN_USDT,BZZ_ETH,BZZ_USDT,C983L_USDT,C983S_USDT,C98_BTC,C98_USD,C98_USDT,CAKE3L_USDT,CAKE3S_USDT,CAKE_ETH,CAKE_USDT,CANTO_USDT,CAPS_USDT,CARE_USDT,CART_ETH,CART_USDT,CATE_USDT,CATGIRL_USDT,CATHEON_USDT,CBK_BTC,CBK_ETH,CBK_USDT,CEEK_ETH,CEEK_USDT,CELL_ETH,CELL_USDT,CELO_USDT,CELR_ETH,CELR_USDT,CELT_USDT,CEL_ETH,CEL_USD,CEL_USDT,CERE_ETH,CERE_USDT,CEUR_ETH,CEUR_USDT,CFG_BTC,CFG_USDT,CFI_ETH,CFI_USDT,CFX3L_USDT,CFX3S_USDT,CFX_ETH,CFX_USDT,CGG_ETH,CGG_USDT,CHAIN_ETH,CHAIN_USDT,CHAMP_USDT,CHEQ_USDT,CHER_USDT,CHESS_ETH,CHESS_USDT,CHNG_BTC,CHNG_USDT,CHO_USDT,CHR_ETH,CHR_USDT,CHZ3L_USDT,CHZ3S_USDT,CHZ_ETH,CHZ_TRY,CHZ_USD,CHZ_USDT,CIRUS_USDT,CIR_ETH,CIR_USDT,CITY_USDT,CKB_BTC,CKB_ETH,CKB_USDT,CLH_USDT,CLO_USDT,CLV_ETH,CLV_USDT,CMP_USDT,CNAME_USDT,CNNS_ETH,CNNS_USDT,COCOS_USDT,COFIX_USDT,COFI_ETH,COFI_USDT,COMBO_ETH,COMBO_USDT,COMP3L_USDT,COMP3S_USDT,COMP_USD,COMP_USDT,CONV_ETH,CONV_USDT,COOK_ETH,COOK_USDT,CORAL_ETH,CORAL_USDT,COREUM_USDT,CORE_USDT,CORN_USDT,COS_USDT,COTI3L_USDT,COTI3S_USDT,COTI_USDT,COVAL_USDT,COVER_ETH,COVER_USDT,CPAN_USDT,CPOOL_USDT,CQT_ETH,CQT_USD,CQT_USDT,CRAFT_USDT,CRBN_ETH,CRBN_USDT,CREAM_ETH,CREAM_USDT,CREDIT_ETH,CREDIT_USDT,CRE_USDT,CRF_ETH,CRF_USDT,CRO3L_USDT,CRO3S_USDT,CRO_USDT,CRPT_ETH,CRPT_USDT,CRP_ETH,CRP_USDT,CRTS_ETH,CRTS_USDT,CRT_USDT,CRU_ETH,CRU_USDT,CRV3L_USDT,CRV3S_USDT,CRV_BTC,CRV_ETH,CRV_TRY,CRV_USD,CRV_USDT,CRYPTOFI_USDT,CSIX_USDT,CSPR_ETH,CSPR_USDT,CSTR_ETH,CSTR_USDT,CS_ETH,CS_USDT,CTC_USDT,CTG_USDT,CTI_ETH,CTI_USDT,CTK_ETH,CTK_USDT,CTRC_USDT,CTSI_USDT,CTT_USDT,CUDOS_USDT,CULT_USDT,CUMMIES_USDT,CUP_USDT,CUSD_ETH,CUSD_USDT,CVAULTCORE_ETH,CVAULTCORE_USDT,CVC3L_USDT,CVC3S_USDT,CVC_ETH,CVC_USDT,CVP_ETH,CVP_USDT,CVTX_USDT,CVX_ETH,CVX_USDT,CWAR_ETH,CWAR_USDT,CWEB_USDT,CWS_ETH,CWS_USDT,CYS_ETH,CYS_USDT,CZZ_USDT,D2T_USDT,DAFI_ETH,DAFI_USDT,DAG_BTC,DAG_ETH,DAG_USDT,DAI_TRY,DAI_USD,DAI_USDT,DAL_USDT,DANA_USDT,DAO_ETH,DAO_USDT,DARK_USDT,DAR_ETH,DAR_USDT,DASH3L_USDT,DASH3S_USDT,DASH_BTC,DASH_USDT,DATA_ETH,DATA_USDT,DBC_BTC,DBC_ETH,DBC_USDT,DCRN_USDT,DCR_BTC,DCR_USDT,DC_USDT,DDD_BTC,DDD_ETH,DDD_USDT,DDIM_ETH,DDIM_USDT,DDOS_USDT,DEBT_USDT,DEFILAND_ETH,DEFILAND_USDT,DEGO_USDT,DEK_USDT,DELFI_USDT,DENT_ETH,DENT_USDT,DEP_USDT,DERC_USDT,DERI_ETH,DERI_USDT,DESO_USDT,DES_USDT,DEUS_USDT,DEVT_USDT,DEXE_ETH,DEXE_USDT,DFA_USDT,DFI_USDT,DFL_USDT,DFND_USDT,DFYN_USDT,DFY_ETH,DFY_USDT,DF_ETH,DF_USDT,DG_ETH,DG_USDT,DHB_USDT,DHV_ETH,DHV_USDT,DHX_USD,DHX_USDT,DIA_ETH,DIA_USDT,DIGG_ETH,DIGG_USDT,DILI_USDT,DIO_USDT,DIS_ETH,DIS_USDT,DIVER_USDT,DKA_ETH,DKA_USDT,DKS_USDT,DLC_USDT,DMLG_USDT,DMS_ETH,DMS_USDT,DMTR_USDT,DNT_ETH,DNXC_USDT,DOCK_ETH,DOCK_USDT,DODO_ETH,DODO_USDT,DOGA_USDT,DOGE3L_USDT,DOGE3S_USDT,DOGE5L_USDT,DOGE5S_USDT,DOGE_BTC,DOGE_TRY,DOGE_USD,DOGE_USDT,DOGGO_USDT,DOGGY_USDT,DOGNFT_ETH,DOGNFT_USDT,DOG_ETH,DOG_USDT,DOME_USDT,DOMI_USDT,DOP_USDT,DORA_ETH,DORA_USDT,DOSE_ETH,DOSE_USDT,DOS_USDT,DOT3L_USDT,DOT3S_USDT,DOT5L_USDT,DOT5S_USDT,DOT_BTC,DOT_TRY,DOT_USDT,DOWS_ETH,DOWS_USDT,DPET_ETH,DPET_USDT,DPR_ETH,DPR_USDT,DPY_ETH,DPY_USDT,DREP_ETH,DREP_USDT,DRGN_ETH,DRGN_USDT,DSLA_ETH,DSLA_USDT,DUCK2_ETH,DUCK2_USDT,DUCK_ETH,DUCK_USDT,DUNE_USDT,DUSK_ETH,DUSK_USDT,DUST_USDT,DVI_USDT,DVP_ETH,DVP_USDT,DV_USDT,DXCT_ETH,DXCT_USDT,DX_ETH,DX_USDT,DYDX3L_USDT,DYDX3S_USDT,DYDX_ETH,DYDX_TRY,DYDX_USD,DYDX_USDT,DYP_ETH,DYP_USDT,DZOO_USDT,ECOX_USDT,EDEN_ETH,EDEN_USD,EDEN_USDT,EDG_ETH,EDG_USDT,EFI_ETH,EFI_USDT,EGAME_USDT,EGG_ETH,EGG_USDT,EGLD3L_USDT,EGLD3S_USDT,EGLD_ETH,EGLD_USDT,EGS_USDT,EHASH_ETH,EHASH_USDT,EJS_USDT,ELA_USDT,ELEC_ETH,ELEC_USDT,ELF_ETH,ELF_USDT,ELON_USDT,ELT_USDT,ELU_USDT,EMON_USDT,EMPIRE_ETH,EMPIRE_USDT,ENJ3L_USDT,ENJ3S_USDT,ENJ_ETH,ENJ_TRY,ENJ_USD,ENJ_USDT,ENNO_USDT,ENS_ETH,ENS_USDT,ENV_USDT,EOS3L_USDT,EOS3S_USDT,EOS5L_USDT,EOS5S_USDT,EOSDAC_ETH,EOSDAC_USDT,EOS_BTC,EOS_ETH,EOS_TRY,EOS_USDT,EPIK_USDT,EPK_USDT,EPX_ETH,EPX_USDT,EQX_USDT,EQ_USDT,ERG_ETH,ERG_USDT,ERN_ETH,ERN_USDT,ESG_USDT,ESS_ETH,ESS_USDT,ETC3L_USDT,ETC3S_USDT,ETC_BTC,ETC_ETH,ETC_USDT,ETERNAL_USDT,ETH2_ETH,ETH2_USDT,ETH3L_USDT,ETH3S_USDT,ETH5L_USDT,ETH5S_USDT,ETHA_ETH,ETHA_USDT,ETHF_USDT,ETHW_ETH,ETHW_USDT,ETH_BTC,ETH_TRY,ETH_USD,ETH_USDT,EUL_USDT,EURT_USDT,EVA_ETH,EVA_USDT,EVER_USDT,EVRY_USDT,EWT_ETH,EWT_USDT,EZ_ETH,EZ_USDT,F2C_USDT,FALCONS_USDT,FAME_USDT,FAN_ETH,FAN_USDT,FARM_ETH,FARM_USDT,FAR_ETH,FAR_USDT,FCON_USDT,FDC_USDT,FDT_USDT,FEAR_USDT,FEG_USDT,FEI_ETH,FEI_USDT,FER_USDT,FET_ETH,FET_USDT,FEVR_USDT,FIC_USDT,FIDA_ETH,FIDA_USDT,FIL3L_USDT,FIL3S_USDT,FILDA_ETH,FILDA_USDT,FIL_BTC,FIL_ETH,FIL_TRY,FIL_USDT,FINE_ETH,FINE_USDT,FIN_USDT,FIO_ETH,FIO_USDT,FIRE_ETH,FIRE_USDT,FIRO_USDT,FIS_ETH,FIS_USDT,FITFI3L_USDT,FITFI3S_USDT,FITFI_USDT,FIU_USDT,FIWA_USDT,FLM_ETH,FLM_USDT,FLOKI_USDT,FLOW_ETH,FLOW_TRY,FLOW_USDT,FLR_USDT,FLURRY_USDT,FLUX_ETH,FLUX_USDT,FLY_USDT,FNCY_USDT,FNF_USDT,FNZ_USDT,FODL_ETH,FODL_USDT,FOF_USDT,FOREX_ETH,FOREX_USDT,FORM_ETH,FORM_USDT,FORTH_ETH,FORTH_USDT,FORT_USDT,FOR_ETH,FOR_USDT,FOX_ETH,FOX_USDT,FPFT_USDT,FRAX_ETH,FRAX_USDT,FRA_ETH,FRA_USDT,FREE_USDT,FRIN_USDT,FRM_USDT,FROG_ETH,FROG_USDT,FRONT_ETH,FRONT_USDT,FRR_USDT,FSN_ETH,FSN_USDT,FST_ETH,FST_USDT,FTI_ETH,FTI_USDT,FTM3L_USDT,FTM3S_USDT,FTM_ETH,FTM_TRY,FTM_USD,FTM_USDT,FTRB_USDT,FTT3L_USDT,FTT3S_USDT,FTT_ETH,FTT_USD,FTT_USDT,FUEL_ETH,FUEL_USDT,FUN_ETH,FUN_USDT,FUSE_ETH,FUSE_USDT,FXF_ETH,FXF_USDT,FXS_ETH,FXS_USDT,FX_ETH,FX_USDT,GAFI_ETH,GAFI_USDT,GAIA_USDT,GAL3L_USDT,GAL3S_USDT,GALA3L_USDT,GALA3S_USDT,GALA5L_USDT,GALA5S_USDT,GALA_ETH,GALA_TRY,GALA_USDT,GALFAN_USDT,GAL_USDT,GAME_USDT,GAN_USDT,GARD_ETH,GARD_USDT,GARI_ETH,GARI_USDT,GASDAO_USDT,GAS_BTC,GAS_USDT,GBPT_BTC,GBPT_ETH,GBPT_USDT,GCOIN_USDT,GDAO_ETH,GDAO_USDT,GEAR_USDT,GEL_ETH,GEL_USDT,GEM_ETH,GEM_USDT,GENS_USDT,GFI_ETH,GFI_USDT,GFT_USDT,GF_ETH,GF_USDT,GGG_USDT,GGM_USDT,GHNY_USDT,GHST_ETH,GHST_USDT,GITCOIN_ETH,GITCOIN_USDT,GLMR3L_USDT,GLMR3S_USDT,GLMR_ETH,GLMR_USDT,GLM_ETH,GLM_USDT,GLQ_ETH,GLQ_USDT,GMAT_ETH,GMAT_USDT,GMEE_ETH,GMEE_USDT,GMM_USDT,GMPD_USDT,GMT3L_USDT,GMT3S_USDT,GMT_USDT,GMX_USDT,GM_USDT,GNO_ETH,GNO_USDT,GNS_USDT,GNX_ETH,GNX_USDT,GOAL_USDT,GOB_USDT,GOD_BTC,GOD_USDT,GOFX_USDT,GOF_USDT,GOLDMINER_USDT,GOLD_USDT,GOVI_USDT,GOV_USDT,GOZ_USDT,GO_ETH,GO_USDT,GPT_USDT,GQ_USDT,GRAIL_USDT,GRBE_USDT,GRIN_BTC,GRIN_ETH,GRIN_USDT,GRND_USDT,GRT3L_USDT,GRT3S_USDT,GRT_ETH,GRT_USD,GRT_USDT,GRV_USDT,GSE_ETH,GSE_USDT,GST3L_USDT,GST3S_USDT,GST_TRY,GST_USDT,GS_ETH,GS_USDT,GTC_BTC,GTC_ETH,GTC_USDT,GTH_ETH,GTH_USDT,GT_BTC,GT_ETH,GT_USDT,GUM_USDT,GZONE_ETH,GZONE_USDT,HADES_USDT,HAI_ETH,HAI_USDT,HAM_USDT,HAO_BTC,HAO_ETH,HAO_USDT,HAPI_USDT,HARD_ETH,HARD_USDT,HBAR3L_USDT,HBAR3S_USDT,HBAR_USDT,HCT_ETH,HCT_USDT,HC_BTC,HC_ETH,HC_USDT,HDV_USDT,HEART_USDT,HECH_USDT,HEGIC_ETH,HEGIC_USDT,HELLO_USDT,HERA_USDT,HERO_ETH,HERO_USDT,HE_USDT,HFT_ETH,HFT_USDT,HGET_ETH,HGET_USDT,HIBIKI_USDT,HIBS_USDT,HID_USDT,HIFI_ETH,HIFI_USDT,HIGH_USDT,HIT_ETH,HIT_USDT,HIVE_USDT,HMT_ETH,HMT_USDT,HNS_BTC,HNS_USDT,HNT_ETH,HNT_USDT,HOD_USDT,HOGE_USDT,HOOK_USDT,HOPR_ETH,HOPR_USDT,HORD_ETH,HORD_USDT,HOTCROSS_ETH,HOTCROSS_USDT,HOT_ETH,HOT_TRY,HOT_USDT,HPB_ETH,HPB_USDT,HSC_ETH,HSC_USDT,HSF_USDT,HT3L_USDT,HT3S_USDT,HTR_USDT,HT_BTC,HT_USD,HT_USDT,HYDRA_USDT,HYVE_ETH,HYVE_USDT,IAG_USDT,IAZUKI_USDT,IBAYC_USDT,IBFK_USDT,ICE_ETH,ICE_USDT,ICONS_ETH,ICONS_USDT,ICP3L_USDT,ICP3S_USDT,ICP_ETH,ICP_TRY,ICP_USDT,ICX_ETH,ICX_USDT,IDEA_USDT,IDEX_ETH,IDEX_USDT,IDOODLES_USDT,IDV_ETH,IDV_USDT,ID_USDT,IGU_USDT,IHC_USDT,IHT_ETH,IHT_USDT,ILV_ETH,ILV_USDT,IMAYC_USDT,IMPT_USDT,IMX3L_USDT,IMX3S_USDT,IMX_ETH,IMX_USDT,INDI_ETH,INDI_USDT,ING_USDT,INJ_ETH,INJ_USDT,INK_BTC,INK_ETH,INK_USDT,INSUR_ETH,INSUR_USDT,INTER_USDT,INTR_USDT,INV_ETH,INV_USDT,IOEN_ETH,IOEN_USDT,IOI_USDT,IONX_ETH,IONX_USDT,IOST3L_USDT,IOST3S_USDT,IOST_BTC,IOST_USDT,IOTA_BTC,IOTA_USDT,IOTX_ETH,IOTX_USDT,IP3_USDT,IPUNKS_USDT,IRIS_USDT,ISKY_USDT,ISK_USDT,ISP_ETH,ISP_USDT,ITEM_USDT,ITGR_ETH,ITGR_USDT,ITRUMP_USDT,ITSB_USDT,IZI_ETH,IZI_USDT,JAM_USDT,JASMY3L_USDT,JASMY3S_USDT,JASMY_ETH,JASMY_TRY,JASMY_USDT,JFI_USDT,JGN_ETH,JGN_USDT,JOE_ETH,JOE_USDT,JOY_USDT,JST3L_USDT,JST3S_USDT,JST_USDT,JULD_ETH,JULD_USDT,JUV_USDT,K21_ETH,K21_USDT,KABY_USDT,KAI_USDT,KALM_USDT,KAP_USDT,KAR_USDT,KASTA_USDT,KAS_USDT,KAVA3L_USDT,KAVA3S_USDT,KAVA_USDT,KBD_USDT,KBOX_USDT,KCAL_USDT,KDA_BTC,KDA_USDT,KEX_ETH,KEX_USDT,KEY_ETH,KEY_USDT,KFC_USDT,KFT_ETH,KFT_USDT,KGC_USDT,KIBA_USDT,KICKS_USDT,KIF_ETH,KIF_USDT,KILT_USDT,KIMCHI_ETH,KIMCHI_USDT,KINE_ETH,KINE_USDT,KINGSHIB_USDT,KING_USDT,KINT_ETH,KINT_USDT,KIN_USDT,KISHU_USDT,KLAP_USDT,KLAY3L_USDT,KLAY3S_USDT,KLAY_USDT,KLO_USDT,KLV_ETH,KLV_USDT,KMA_USDT,KMON_USDT,KNC_ETH,KNC_USDT,KNIGHT_USDT,KNOT_USDT,KOK_USDT,KONO_ETH,KONO_USDT,KON_USDT,KP3R_ETH,KP3R_USDT,KPAD_ETH,KPAD_USDT,KRL_USDT,KSM3L_USDT,KSM3S_USDT,KSM_USDT,KST_ETH,KST_USDT,KTN_ETH,KTN_USDT,KTON_USDT,KT_USDT,KUBE_USDT,KUB_USDT,KUMA_USDT,KWS_USDT,KYL_ETH,KYL_USDT,KZEN_USDT,LABS_ETH,LABS_USDT,LAMB_ETH,LAMB_USDT,LAND_USDT,LARIX_ETH,LARIX_USDT,LAT_USDT,LAVA_ETH,LAVA_USDT,LAYER_ETH,LAYER_USDT,LAZIO_ETH,LAZIO_USDT,LBA_ETH,LBA_USDT,LBK_USDT,LBLOCK_USDT,LBL_USDT,LDO_ETH,LDO_USDT,LEASH_ETH,LEASH_USDT,LEMD_ETH,LEMD_USDT,LEMN_USDT,LEMO_ETH,LEMO_USDT,LEO_BTC,LEO_USDT,LEVER_USDT,LEV_USDT,LFW_USDT,LGCY_USDT,LGX_USDT,LIEN_ETH,LIEN_USDT,LIFE_ETH,LIFE_USDT,LIKE_ETH,LIKE_USDT,LIME_BTC,LIME_ETH,LIME_USDT,LINA_ETH,LINA_USDT,LINK3L_USDT,LINK3S_USDT,LINK5L_USDT,LINK5S_USDT,LINK_ETH,LINK_TRY,LINK_USD,LINK_USDT,LION_USDT,LIQUIDUS_ETH,LIQUIDUS_USDT,LIQ_USDT,LIT3L_USDT,LIT3S_USDT,LITH_ETH,LITH_USDT,LIT_ETH,LIT_TRY,LIT_USDT,LKR_ETH,LKR_USDT,LLT_SNET,LMR_BTC,LMR_USDT,LM_USDT,LN_BTC,LN_USDT,LOA_USDT,LOCG_ETH,LOCG_USDT,LOKA3L_USDT,LOKA3S_USDT,LOKA_ETH,LOKA_USDT,LON_ETH,LON_USDT,LOOKS3L_USDT,LOOKS3S_USDT,LOOKS_ETH,LOOKS_USDT,LOON_ETH,LOON_USDT,LOOT_USDT,LOVELY_USDT,LOWB_USDT,LPOOL_ETH,LPOOL_USDT,LPT_ETH,LPT_USDT,LQTY_USDT,LRC3L_USDT,LRC3S_USDT,LRC_BTC,LRC_ETH,LRC_TRY,LRC_USDT,LRN_ETH,LRN_USDT,LSK_BTC,LSK_USDT,LSS_ETH,LSS_USDT,LTC3L_USDT,LTC3S_USDT,LTC5L_USDT,LTC5S_USDT,LTC_BTC,LTC_TRY,LTC_USD,LTC_USDT,LTO_ETH,LTO_USDT,LUFFY_ETH,LUFFY_USDT,LUNA_ETH,LUNA_TRY,LUNA_USDT,LUNCH_USDT,LUNC_TRY,LUNC_USDT,LUNR_USDT,LUS_USDT,LYM_BTC,LYM_ETH,LYM_USDT,LYXE_ETH,LYXE_USDT,MAGIC_USDT,MAHA_ETH,MAHA_USDT,MANA3L_USDT,MANA3S_USDT,MANA_ETH,MANA_TRY,MANA_USDT,MAN_ETH,MAN_USDT,MAPE_USDT,MAPS_ETH,MAPS_USDT,MARSH_ETH,MARSH_USDT,MART_USDT,MASK3L_USDT,MASK3S_USDT,MASK_ETH,MASK_TRY,MASK_USDT,MATCH_USDT,MATH_ETH,MATH_USDT,MATIC3L_USDT,MATIC3S_USDT,MATIC_ETH,MATIC_USD,MATIC_USDT,MATTER_ETH,MATTER_USDT,MAT_ETH,MAT_USDT,MBL_ETH,MBL_USDT,MBOX_ETH,MBOX_USDT,MBS_ETH,MBS_USDT,MBX_USDT,MCASH_USDT,MCG_USDT,MCO2_ETH,MCO2_USDT,MCRN_ETH,MCRN_USDT,MCRT_USDT,MC_ETH,MC_USDT,MDAO_USDT,MDA_ETH,MDA_USDT,MDF_ETH,MDF_USDT,MDS_ETH,MDS_USDT,MDT_BTC,MDT_ETH,MDT_USDT,MDX_ETH,MDX_USDT,MEAN_ETH,MEAN_USDT,MED_ETH,MED_USDT,MELI_USDT,MENGO_USDT,MEPAD_USDT,MER_USDT,MESA_ETH,MESA_USDT,METAG_USDT,METAL_USDT,METAN_USDT,METAX_ETH,METAX_USDT,METIS_ETH,METIS_USDT,METO_USDT,MET_USDT,MFOOTBALL_USDT,MGA_USDT,MGG_USDT,MHUNT_USDT,MILO_USDT,MIMIR_ETH,MIMIR_USDT,MINA3L_USDT,MINA3S_USDT,MINA_BTC,MINA_USDT,MINE_USDT,MINI_ETH,MINI_USDT,MINT_USDT,MIR_ETH,MIR_USDT,MIST_ETH,MIST_USDT,MIS_ETH,MIS_USDT,MITH_ETH,MITH_USDT,MIX_ETH,MIX_USDT,MKR3L_USDT,MKR3S_USDT,MKR_ETH,MKR_TRY,MKR_USDT,MLK_USDT,MLN_ETH,MLN_USDT,MLS_USDT,MLT_USDT,ML_USDT,MMM_USDT,MMPRO_USDT,MM_ETH,MM_USDT,MNDE_USDT,MNGO_ETH,MNGO_USDT,MNW_ETH,MNW_USDT,MNY_USDT,MNZ_USDT,MOBI_BTC,MOBI_ETH,MOBI_USDT,MOB_ETH,MOB_USDT,MODA_ETH,MODA_USDT,MOFI_USDT,MOJO_USDT,MOMA_ETH,MOMA_USDT,MONI_USDT,MONS_USDT,MOONEY_USDT,MOON_USDT,MOOO_USDT,MOOV_USDT,MOO_USDT,MOTG_USDT,MOT_USDT,MOVEZ_USDT,MOVR_ETH,MOVR_USDT,MPH_ETH,MPH_USDT,MPI_USDT,MPLX_USDT,MPL_USDT,MQL_USDT,MRCH_ETH,MRCH_USDT,MSOL_ETH,MSOL_USDT,MSU_USDT,MSWAP_USDT,MTA_ETH,MTA_USDT,MTD_USDT,MTG_USDT,MTL3L_USDT,MTL3S_USDT,MTL_ETH,MTL_USDT,MTN_ETH,MTN_USDT,MTRG_USDT,MTR_USDT,MTS_ETH,MTS_USDT,MTV_USDT,MULTI_ETH,MULTI_USDT,MUSE_ETH,MUSE_USDT,MV_USDT,MXC_BTC,MXC_ETH,MXC_USD,MXC_USDT,MYRA_USDT,NAFT_USDT,NANO_BTC,NANO_USDT,NAOS_BTC,NAOS_ETH,NAOS_USDT,NAP_USDT,NAS_BTC,NAS_ETH,NAS_USDT,NAVI_USDT,NAX_ETH,NAX_USDT,NBLU_USDT,NBOT_ETH,NBOT_USDT,NBP_ETH,NBP_USDT,NBS_BTC,NBS_USDT,NBT_USDT,NCT_ETH,NCT_USDT,NEAR3L_USDT,NEAR3S_USDT,NEAR_ETH,NEAR_USDT,NEBL_USDT,NEER_USDT,NEO3L_USDT,NEO3S_USDT,NEO_BTC,NEO_TRY,NEO_USDT,NEST_ETH,NEST_USDT,NEXO_ETH,NEXO_USDT,NEXT_USDT,NFTB_ETH,NFTB_USDT,NFTD_USDT,NFTL_USDT,NFTX_ETH,NFTX_USDT,NFTY_ETH,NFTY_USDT,NFT_USDT,NGL_USDT,NIFT_USDT,NIIFI_USDT,NII_ETH,NII_USDT,NIM_USDT,NKN_ETH,NKN_USDT,NMR_ETH,NMR_USDT,NMT_ETH,NMT_USDT,NOA_USDT,NODL_USDT,NOIA_ETH,NOIA_USDT,NOM_USDT,NORD_ETH,NORD_USDT,NOS_ETH,NOS_USDT,NPT_USDT,NRFB_ETH,NRFB_USDT,NRV_ETH,NRV_USDT,NSBT_BTC,NSBT_ETH,NSBT_USDT,NSDX_USDT,NSURE_ETH,NSURE_USDT,NULS_ETH,NULS_USDT,NUM_USDT,NUX_ETH,NUX_USDT,NVG_USDT,NVIR_USDT,NWC_BTC,NWC_USDT,NXD_USDT,NYM_USDT,NYZO_ETH,NYZO_USDT,O3_ETH,O3_USDT,OAS_USDT,OAX_BTC,OAX_ETH,OAX_USDT,OCC_USDT,OCEAN_USDT,OCN_BTC,OCN_ETH,OCN_USDT,OCTO_ETH,OCTO_USDT,OCT_USDT,ODDZ_ETH,ODDZ_USDT,OGN_ETH,OGN_USDT,OGV_USDT,OG_USDT,OHM_ETH,OHM_USDT,OIN_USDT,OKB3L_USDT,OKB3S_USDT,OKB_USDT,OKT_ETH,OKT_USDT,OLAND_USDT,OLE_USDT,OLT_USDT,OLV_USDT,OLY_USDT,OMG3L_USDT,OMG3S_USDT,OMG_BTC,OMG_ETH,OMG_TRY,OMG_USD,OMG_USDT,OMI_ETH,OMI_USDT,OM_ETH,OM_USDT,ONC_ETH,ONC_USDT,ONE3L_USDT,ONE3S_USDT,ONE_USDT,ONG_ETH,ONG_USDT,ONIT_USDT,ONSTON_USDT,ONS_ETH,ONS_USDT,ONT3L_USDT,ONT3S_USDT,ONT_ETH,ONT_USDT,ONX_ETH,ONX_USDT,OOE_ETH,OOE_USDT,OOKI_USDT,OP3L_USDT,OP3S_USDT,OPA_USDT,OPEN_ETH,OPEN_USDT,OPIUM_ETH,OPIUM_USDT,OPS_ETH,OPS_USDT,OPTIMUS_USDT,OPUL_ETH,OPUL_USDT,OP_ETH,OP_TRY,OP_USDT,ORAI_ETH,ORAI_USDT,ORAO_ETH,ORAO_USDT,ORBR_USDT,ORBS_ETH,ORBS_USDT,ORB_USDT,ORCA_USDT,ORC_USDT,ORION_USDT,ORN_ETH,ORN_USDT,ORO_USDT,ORT_USDT,OSMO_USDT,OST_ETH,OST_USDT,OUSD_USDT,OVO_USDT,OVR_USDT,OXT_ETH,OXT_USDT,OXY_ETH,OXY_USDT,P00LS_USDT,PAF_USDT,PARA_USDT,PAW_USDT,PAY_BTC,PAY_ETH,PAY_USDT,PBR_ETH,PBR_USDT,PBTC35A_ETH,PBTC35A_USDT,PBX_ETH,PBX_USDT,PCNT_ETH,PCNT_USDT,PCX_USDT,PDEX_USDT,PEARL_USDT,PENDLE_ETH,PENDLE_USDT,PEOPLE3L_USDT,PEOPLE3S_USDT,PEOPLE_TRY,PEOPLE_USDT,PERA_USDT,PERC_USDT,PERI_USDT,PERL_ETH,PERL_USDT,PERP_ETH,PERP_USD,PERP_USDT,PET_BTC,PET_ETH,PET_USDT,PHA_USDT,PHB_USDT,PHM_USDT,PHTR_USDT,PIAS_USDT,PICKLE_ETH,PICKLE_USDT,PIG_USDT,PINE_USDT,PING_USDT,PIT_USDT,PIXEL_USDT,PIZA_USDT,PI_BTC,PI_USDT,PKF_ETH,PKF_USDT,PLACE_USDT,PLA_ETH,PLA_USDT,PLCU_USDT,PLSPAD_USDT,PMON_ETH,PMON_USDT,PNG_USDT,PNK_ETH,PNK_USDT,PNL_ETH,PNL_USDT,PNT_ETH,PNT_USDT,POG_USDT,POKT_USDT,POLC_ETH,POLC_USDT,POLIS_USDT,POLI_USDT,POLK_ETH,POLK_USDT,POLS_USDT,POLYDOGE_USDT,POLYPAD_USDT,POLYX_USDT,POND_ETH,POND_USDT,POOL_ETH,POOL_USDT,POPK_USDT,POP_BTC,POP_USDT,PORTO_USDT,PORTX_USDT,PORT_USDT,POR_USDT,POSI_USDT,POT_USDT,POWR_BTC,POWR_ETH,POWR_USDT,PPAD_USDT,PRARE_ETH,PRARE_USDT,PRIDE_USDT,PRIMAL_USDT,PRIME_USDT,PRISM_ETH,PRISM_USDT,PRMX_USDT,PROM_ETH,PROM_USDT,PROPS_ETH,PROPS_USDT,PROS_ETH,PROS_USDT,PRQ_USDT,PRT_ETH,PRT_USDT,PSB_USDT,PSG_ETH,PSG_USDT,PSI_USDT,PSL_USDT,PSP_ETH,PSP_USDT,PSTAKE_USDT,PST_ETH,PST_USDT,PSY_ETH,PSY_USDT,PTS_USDT,PUMLX_USDT,PUNDIX_ETH,PUNDIX_USDT,PUSH_ETH,PUSH_USDT,PVU_ETH,PVU_USDT,PWAR_ETH,PWAR_USDT,PYM_USDT,PYR_ETH,PYR_USDT,QASH_BTC,QASH_ETH,QASH_USDT,QBT_ETH,QBT_USDT,QI_ETH,QI_USDT,QKC_BTC,QKC_ETH,QKC_USDT,QLC_BTC,QLC_ETH,QLC_USDT,QNT_ETH,QNT_USDT,QRDO_BTC,QRDO_ETH,QRDO_USDT,QSP_ETH,QSP_USDT,QTCON_USDT,QTC_ETH,QTC_USDT,QTUM3L_USDT,QTUM3S_USDT,QTUM_BTC,QTUM_ETH,QTUM_USDT,QUACK_USDT,QUICK_ETH,QUICK_USDT,RACA3L_USDT,RACA3S_USDT,RACA_USDT,RADAR_USDT,RAD_ETH,RAD_USDT,RAGE_USDT,RAI_ETH,RAI_USDT,RAM_USDT,RANKER_USDT,RARE_ETH,RARE_USDT,RARI_ETH,RARI_USDT,RATING_ETH,RATING_USDT,RATIO_USDT,RAY_ETH,RAY_USD,RAY_USDT,RAZE_ETH,RAZE_USDT,RAZOR_ETH,RAZOR_USDT,RBC_ETH,RBC_USDT,RBLS_USDT,RBN_ETH,RBN_USDT,RCN_ETH,RCN_USDT,RDF_USDT,RDNT_USDT,RDN_ETH,RDN_USDT,REALM_USDT,REAL_USDT,REAP_USDT,REDTOKEN_USDT,RED_ETH,RED_USDT,REEF_ETH,REEF_USDT,REELT_USDT,REF_USDT,REI_BTC,REI_USDT,REM_ETH,REM_USDT,RENA_USDT,REN_ETH,REN_USD,REN_USDT,REP_ETH,REP_USDT,REQ_ETH,REQ_USDT,REVOLAND_USDT,REVO_BTC,REVO_ETH,REVO_USDT,REVU_USDT,REVV_ETH,REVV_USDT,RFOX_ETH,RFOX_USDT,RFR_ETH,RFR_USDT,RFT_USDT,RFUEL_USDT,RICE_ETH,RICE_USDT,RIDE_USDT,RIF_ETH,RIF_USDT,RIM_USDT,RING_ETH,RING_USDT,RIN_USDT,RITE_USDT,RJV_USDT,RLC_ETH,RLC_USDT,RLY_ETH,RLY_USDT,RMRK_USDT,RNDR_ETH,RNDR_USDT,RNDX_USDT,ROCO_USDT,RON_USDT,ROOBEE_USDT,ROOM_ETH,ROOM_USDT,ROSE3L_USDT,ROSE3S_USDT,ROSE_ETH,ROSE_USDT,ROSN_USDT,ROUTE_USDT,RPL_USDT,RSR_USDT,RSS3_USDT,RSV_USDT,RUFF_BTC,RUFF_ETH,RUFF_USDT,RUNE3L_USDT,RUNE3S_USDT,RUNE_ETH,RUNE_USD,RUNE_USDT,RVC_USDT,RVN_USDT,SAFEMARS_USDT,SAITAMA_USDT,SAITO_USDT,SAKE_ETH,SAKE_USDT,SALT_ETH,SALT_USDT,SAMO_ETH,SAMO_USDT,SAND3L_USDT,SAND3S_USDT,SANDWICH_USDT,SAND_ETH,SAND_TRY,SAND_USD,SAND_USDT,SANTOS_USDT,SAO_USDT,SASHIMI_ETH,SASHIMI_USDT,SAUBER_USDT,SAVG_USDT,SBR_ETH,SBR_USDT,SBTC_BTC,SBTC_USDT,SCCP_USDT,SCLP_ETH,SCLP_USDT,SCNSOL_ETH,SCNSOL_USDT,SCRT_ETH,SCRT_USDT,SCY_ETH,SCY_USDT,SC_ETH,SC_USDT,SDAO_BTC,SDAO_ETH,SDAO_USDT,SDN_BTC,SDN_ETH,SDN_USDT,SD_USDT,SEELE_USDT,SENATE_USDT,SENC_ETH,SENC_USDT,SENSO_ETH,SENSO_USDT,SERO_ETH,SERO_USDT,SFG_USDT,SFIL_USDT,SFI_ETH,SFI_USDT,SFM_USDT,SFP_ETH,SFP_USDT,SFUND_USDT,SGB_USDT,SHARE_ETH,SHARE_USDT,SHFT_ETH,SHFT_USDT,SHIB3L_USDT,SHIB3S_USDT,SHIB5L_USDT,SHIB5S_USDT,SHIB_TRY,SHIB_USD,SHIB_USDT,SHILL_USDT,SHI_USDT,SHOE_USDT,SHOPX_ETH,SHOPX_USDT,SHPING_USDT,SHR_ETH,SHR_USDT,SHX_USDT,SIDUS_USDT,SINGLE_USDT,SIN_USDT,SIS_USDT,SKEB_USDT,SKILL_ETH,SKILL_USDT,SKL3L_USDT,SKL3S_USDT,SKL_USDT,SKM_ETH,SKM_USDT,SKRT_USDT,SKT_USDT,SKU_USDT,SKYRIM_ETH,SKYRIM_USDT,SLC_ETH,SLC_USDT,SLG_USDT,SLICE_ETH,SLICE_USDT,SLIM_ETH,SLIM_USDT,SLK_USDT,SLM_USDT,SLND_ETH,SLND_USDT,SLNV2_ETH,SLNV2_USDT,SLP3L_USDT,SLP3S_USDT,SLP_ETH,SLP_USDT,SLRS_ETH,SLRS_USDT,SMART_USDT,SMTY_ETH,SMTY_USDT,SMT_ETH,SMT_USDT,SNET_ETH,SNET_USDT,SNFT1_USDT,SNFT_USDT,SNK_USDT,SNM_USDT,SNOW_ETH,SNOW_USDT,SNTR_ETH,SNTR_USDT,SNT_BTC,SNT_ETH,SNT_USDT,SNX3L_USDT,SNX3S_USDT,SNX_USDT,SNY_ETH,SNY_USDT,SN_USDT,SOL3L_USDT,SOL3S_USDT,SOLO_BTC,SOLO_USDT,SOLR_ETH,SOLR_USDT,SOL_TRY,SOL_USD,SOL_USDT,SOMM_USDT,SONAR_ETH,SONAR_USDT,SOP_ETH,SOP_USDT,SOS_USDT,SOUL_ETH,SOUL_USDT,SOURCE_ETH,SOURCE_USDT,SOV_BTC,SOV_USDT,SPAY_ETH,SPAY_USDT,SPA_ETH,SPA_USDT,SPELLFIRE_USDT,SPELL_ETH,SPELL_USDT,SPEX_USDT,SPFC_USDT,SPHRI_USDT,SPIRIT_USDT,SPO_USDT,SPS_ETH,SPS_USDT,SPUME_USDT,SQUAD_USDT,SQUIDGROW_USDT,SQUID_ETH,SQUID_USDT,SRG_USDT,SRK_ETH,SRK_USDT,SRM_ETH,SRM_USD,SRM_USDT,SRP_USDT,SRT_USDT,SSV_BTC,SSV_ETH,SSV_USDT,SSX_USDT,STARL_USDT,STAR_ETH,STAR_USDT,STBU_ETH,STBU_USDT,STC_USDT,STEEM_USDT,STEPG_ETH,STEPG_USDT,STEP_ETH,STEP_USD,STEP_USDT,STETH_ETH,STETH_USDT,STG_ETH,STG_USDT,STIK_USDT,STI_USDT,STMX_ETH,STMX_USDT,STND_ETH,STND_USDT,STN_ETH,STN_USDT,STORE_USDT,STORJ_BTC,STORJ_ETH,STORJ_USDT,STOS_ETH,STOS_USDT,STOX_ETH,STOX_USDT,STPT_USDT,STRAX_BTC,STRAX_ETH,STRAX_USDT,STRM_USDT,STRONG_ETH,STRONG_USDT,STRP_ETH,STRP_USDT,STX_ETH,STX_USDT,STZ_USDT,SUDO_USDT,SUKU_BTC,SUKU_ETH,SUKU_USDT,SUNNY_ETH,SUNNY_USDT,SUN_USDT,SUPER_ETH,SUPER_USDT,SUPE_ETH,SUPE_USDT,SUP_USDT,SUSD_ETH,SUSD_USDT,SUSHI3L_USDT,SUSHI3S_USDT,SUSHI_ETH,SUSHI_USD,SUSHI_USDT,SUTER_USDT,SVT_ETH,SVT_USDT,SWAP_ETH,SWAP_USDT,SWASH_USDT,SWAY_USDT,SWEAT_USDT,SWFTC_USD,SWFTC_USDT,SWOP_ETH,SWOP_USDT,SWP_USDT,SWRV_ETH,SWRV_USDT,SWTH_ETH,SWTH_USDT,SXP3L_USDT,SXP3S_USDT,SXP_ETH,SXP_TRY,SXP_USD,SXP_USDT,SYLO_USDT,SYN_ETH,SYN_USDT,SYS_ETH,SYS_USDT,T23_USDT,TABOO_USDT,TAI_USDT,TAKI_USDT,TALK_USDT,TAMA_USDT,TAP_USDT,TARA_BTC,TARA_ETH,TARA_USDT,TARI_USDT,TAUR_USDT,TBE_USDT,TCP_ETH,TCP_USDT,TCT_ETH,TCT_USDT,TDROP_USDT,TEDDY_USDT,TEER_USDT,TEM_USDT,TFUEL_ETH,TFUEL_USDT,THALES_USDT,THEOS_USDT,THETA3L_USDT,THETA3S_USDT,THETA_ETH,THETA_USDT,THE_USDT,THG_USDT,THN_ETH,THN_USDT,TIDAL_ETH,TIDAL_USDT,TIFI_USDT,TIMECHRONO_ETH,TIMECHRONO_USDT,TIME_ETH,TIME_USDT,TIPS_ETH,TIPS_USDT,TIP_USDT,TITA_ETH,TITA_USDT,TKO_ETH,TKO_USDT,TLM_ETH,TLM_USDT,TLOS_BTC,TLOS_USDT,TNC_BTC,TNC_ETH,TNC_USDT,TOKE_ETH,TOKE_USDT,TOMI_USDT,TOMO_ETH,TOMO_USDT,TOMS_USDT,TONC_USDT,TON_ETH,TON_USDT,TOOLS_ETH,TOOLS_USDT,TORN_ETH,TORN_USDT,TOTM_USDT,TPT_ETH,TPT_USDT,TRACE_USDT,TRADE_USDT,TRA_USDT,TRB_ETH,TRB_USDT,TRG_USDT,TRIBE3L_USDT,TRIBE3S_USDT,TRIBE_ETH,TRIBE_USDT,TROY_ETH,TROY_USDT,TRR_USDT,TRU_ETH,TRU_USDT,TRVL_BTC,TRVL_USDT,TRX3L_USDT,TRX3S_USDT,TRX_ETH,TRX_TRY,TRX_USD,TRX_USDT,TSHP_USDT,TSL_USDT,TSUKA_USDT,TTT_USDT,TT_ETH,TT_USDT,TULIP_ETH,TULIP_USDT,TVK_ETH,TVK_USDT,TWITFI_USDT,TWT_ETH,TWT_USDT,TXT_ETH,TXT_USDT,T_ETH,T_USDT,UBXS_USDT,UDO_ETH,UDO_USDT,UFI_USDT,UFO_USDT,UFT_ETH,UFT_USDT,ULU_ETH,ULU_USDT,UMA_TRY,UMA_USDT,UMB_ETH,UMB_USDT,UMEE_USDT,UNCX_USDT,UNDEAD_USDT,UNFI_ETH,UNFI_USDT,UNI3L_USDT,UNI3S_USDT,UNI5L_USDT,UNI5S_USDT,UNISTAKE_ETH,UNISTAKE_USDT,UNI_ETH,UNI_TRY,UNI_USD,UNI_USDT,UNN_ETH,UNN_USDT,UNO_ETH,UNO_USDT,UNQ_USDT,UOS_ETH,UOS_USDT,UPI_USDT,URUS_ETH,URUS_USDT,USDC_USDT,USDD_USDT,USDG_USDT,USDT_TRY,USDT_USD,USD_TRY,USTC_USDT,UTK_ETH,UTK_USDT,VADER_ETH,VADER_USDT,VAI_USDT,VALUE_ETH,VALUE_USDT,VATRENI_USDT,VDR_USDT,VEE_USDT,VEGA_ETH,VEGA_USDT,VELODROME_USDT,VELO_ETH,VELO_USDT,VEMP_USDT,VENT_USDT,VET3L_USDT,VET3S_USDT,VET_ETH,VET_USDT,VGX_ETH,VGX_USDT,VIDYX_ETH,VIDYX_USDT,VIDY_ETH,VIDY_USDT,VINU_TRY,VINU_USDT,VLXPAD_ETH,VLXPAD_USDT,VLX_USDT,VMT_USDT,VOLT_USDT,VOXEL_USDT,VP_USDT,VRA_BTC,VRA_USDT,VRT_ETH,VRT_USDT,VRX_ETH,VRX_USDT,VR_USDT,VSO_ETH,VSO_USDT,VSP_ETH,VSP_USDT,VTG_USDT,VTHO_ETH,VTHO_USDT,VVS_USDT,VXT_USDT,WAGYU_ETH,WAGYU_USDT,WAG_USDT,WALLET_USDT,WALV_USDT,WAM_USDT,WAR_ETH,WAR_USDT,WAS_USDT,WATT_USDT,WAVES3L_USDT,WAVES3S_USDT,WAVES_BTC,WAVES_TRY,WAVES_USDT,WAXL_USDT,WAXP_ETH,WAXP_USDT,WBTC_BTC,WBTC_TRY,WBTC_USDT,WBT_USDT,WEAR_USDT,WELL_USDT,WEMIX_ETH,WEMIX_USDT,WEST_ETH,WEST_USDT,WEX_USDT,WGRT_USDT,WHALE_USDT,WHITE_ETH,WHITE_USDT,WICC_ETH,WICC_USDT,WIKEN_BTC,WIKEN_USDT,WILD_USDT,WING_ETH,WING_USDT,WIN_USDT,WIT_BTC,WIT_ETH,WIT_USDT,WLKN_USDT,WNCG_BTC,WNCG_USDT,WNDR_USDT,WNXM_ETH,WNXM_USDT,WNZ_USDT,WOM_ETH,WOM_USDT,WOO3L_USDT,WOO3S_USDT,WOOF_USDT,WOOP_ETH,WOOP_USDT,WOO_ETH,WOO_USDT,WOZX_ETH,WOZX_USDT,WRT_USDT,WRX_ETH,WRX_USDT,WSG_USDT,WSIENNA_USDT,WSI_USDT,WWY_USDT,WXT_ETH,WXT_USDT,WZM_USDT,WZRD_USDT,XAUT_USDT,XAVA_USDT,XCAD_USDT,XCH_ETH,XCH_USDT,XCN_ETH,XCN_USDT,XCUR_ETH,XCUR_USDT,XCV_ETH,XCV_USDT,XDB_USDT,XDC_ETH,XDC_USDT,XDEFI_USDT,XEC3L_USDT,XEC3S_USDT,XEC_USDT,XED_ETH,XED_USDT,XELS_USDT,XEM_BTC,XEM_ETH,XEM_USDT,XEND_ETH,XEND_USDT,XEN_USDT,XETA_USDT,XET_USDT,XIL_USDT,XLM3L_USDT,XLM3S_USDT,XLM_BTC,XLM_ETH,XLM_TRY,XLM_USDT,XMC_BTC,XMC_USDT,XMON_ETH,XMON_USDT,XMR3L_USDT,XMR3S_USDT,XMR_BTC,XMR_USDT,XNFT_ETH,XNFT_USDT,XNL_USDT,XOR_ETH,XOR_USDT,XPLA_USDT,XPNET_USDT,XPRESS_USDT,XPRT_ETH,XPRT_USDT,XPR_ETH,XPR_USDT,XRD_ETH,XRD_USDT,XRP3L_USDT,XRP3S_USDT,XRP5L_USDT,XRP5S_USDT,XRP_BTC,XRP_TRY,XRP_USD,XRP_USDT,XRUNE_USDT,XTAG_ETH,XTAG_USDT,XTZ3L_USDT,XTZ3S_USDT,XTZ_BTC,XTZ_ETH,XTZ_TRY,XTZ_USDT,XVG_BTC,XVG_USDT,XVS_ETH,XVS_USDT,XWG_USDT,XYM_ETH,XYM_USDT,XYO_ETH,XYO_USDT,XY_USDT,YAM_ETH,YAM_USDT,YCT_USDT,YFDAI_ETH,YFDAI_USDT,YFI3L_USDT,YFI3S_USDT,YFII3L_USDT,YFII3S_USDT,YFII_ETH,YFII_USDT,YFI_ETH,YFI_USD,YFI_USDT,YFX_USDT,YGG_ETH,YGG_USDT,YIELD_ETH,YIELD_USDT,YIN_ETH,YIN_USDT,YLD_USDT,YOOSHI_USDT,ZAM_ETH,ZAM_USDT,ZBC_USDT,ZCN_ETH,ZCN_USDT,ZCX_USDT,ZEC3L_USDT,ZEC3S_USDT,ZEC_BTC,ZEC_USDT,ZEE_ETH,ZEE_USDT,ZEN3L_USDT,ZEN3S_USDT,ZEN_USDT,ZEUM_USDT,ZIG_USDT,ZIL3L_USDT,ZIL3S_USDT,ZIL_ETH,ZIL_USDT,ZKS_ETH,ZKS_USDT,ZLK_ETH,ZLK_USDT,ZLW_ETH,ZLW_USDT,ZMT_USDT,ZODI_ETH,ZODI_USDT,ZONE_USDT,ZOON_USDT,ZPT_BTC,ZPT_ETH,ZPT_USDT,ZRX_BTC,ZRX_ETH,ZRX_USD,ZRX_USDT,ZSC_ETH,ZSC_USDT,ZTG_USDT" + }, + "option": { + "assetEnabled": true, + "enabled": "BTC_USDT-20221028-26000-C,BTC_USDT-20221028-34000-P,BTC_USDT-20221028-40000-C", + "available": "BTC_USDT-20221028-26000-C,BTC_USDT-20221028-34000-P,BTC_USDT-20221028-40000-C,BTC_USDT-20221028-28000-P,BTC_USDT-20221028-34000-C,BTC_USDT-20221028-28000-C,BTC_USDT-20221028-36000-P,BTC_USDT-20221028-50000-P,BTC_USDT-20221028-36000-C,BTC_USDT-20221028-50000-C,BTC_USDT-20221028-21000-P,BTC_USDT-20221028-38000-P,BTC_USDT-20221028-21000-C,BTC_USDT-20221028-38000-C,BTC_USDT-20221028-23000-P,BTC_USDT-20221028-17000-P,BTC_USDT-20221028-23000-C,BTC_USDT-20221028-17000-C,BTC_USDT-20221028-25000-P,BTC_USDT-20221028-19000-P,BTC_USDT-20221028-25000-C,BTC_USDT-20221028-10000-P,BTC_USDT-20221028-19000-C,BTC_USDT-20221028-27000-P,BTC_USDT-20221028-10000-C,BTC_USDT-20221028-27000-C,BTC_USDT-20221028-12000-P,BTC_USDT-20221028-12000-C,BTC_USDT-20221028-20000-P,BTC_USDT-20221028-5000-P,BTC_USDT-20221028-14000-P,BTC_USDT-20221028-20000-C,BTC_USDT-20221028-45000-P,BTC_USDT-20221028-5000-C,BTC_USDT-20221028-14000-C,BTC_USDT-20221028-22000-P,BTC_USDT-20221028-45000-C,BTC_USDT-20221028-16000-P,BTC_USDT-20221028-22000-C,BTC_USDT-20221028-30000-P,BTC_USDT-20221028-16000-C,BTC_USDT-20221028-24000-P,BTC_USDT-20221028-30000-C,BTC_USDT-20221028-18000-P,BTC_USDT-20221028-24000-C,BTC_USDT-20221028-32000-P,BTC_USDT-20221028-18000-C,BTC_USDT-20221028-26000-P,BTC_USDT-20221028-32000-C,BTC_USDT-20221028-40000-P" + }, + "futures": { + "assetEnabled": true, + "enabled": "ETH_USD,BTT_USD,BTM_USD,BCH_USD,ONT_USD,DASH_USD,XLM_USD,LTC_USD,NEO_USD,BNB_USD,XMR_USD,BTC_USD,BTC_USDT", + "available": "ETH_USD,BTT_USD,BTM_USD,BCH_USD,ONT_USD,DASH_USD,XLM_USD,LTC_USD,NEO_USD,BNB_USD,XMR_USD,BTC_USD,TRX_USD,MDA_USD,WAVES_USD,HT_USD,ETC_USD,BSV_USD,ADA_USD,ZRX_USD,ZEC_USD,EOS_USD,XRP_USD,KNC_USDT,TRYB_USDT,MELON_USDT,OOKI_USDT,MNGO_USDT,BIT_USDT,SC_USDT,ZEC_USDT,ICX_USDT,RVN_USDT,BEL_USDT,DUSK_USDT,ALCX_USDT,REEF_USDT,ASTR_USDT,INJ_USDT,CAKE_USDT,CEL_USDT,ONE_USDT,KLAY_USDT,ETH_USDT,RON_USDT,MKISHU_USDT,COTI_USDT,MANA_USDT,MOVR_USDT,BZZ_USDT,KEEP_USDT,OMG_USDT,UNI_USDT,AAVE_USDT,LTC_USDT,RAMP_USDT,CRU_USDT,DENT_USDT,QRDO_USDT,IRIS_USDT,APE_USDT,RAY_USDT,ALPHA_USDT,BNB_USDT,CERE_USDT,STMX_USDT,ATLAS_USDT,XCN_USDT,BCHA_USDT,OKB_USDT,OGN_USDT,ROOK_USDT,TLM_USDT,DOT_USDT,BTM_USDT,MER_USDT,ADA_USDT,ANKR_USDT,ANT_USDT,TRX_USDT,YFII_USDT,MTL_USDT,WIN_USDT,MBABYDOGE_USDT,SAND_USDT,SUN_USDT,STG_USDT,SRM_USDT,LUNC_USDT,BAT_USDT,SOL_USDT,AXS_USDT,MAKITA_USDT,BLZ_USDT,BNT_USDT,ASD_USDT,IOTA_USDT,MTA_USDT,PYR_USDT,RSR_USDT,MKR_USDT,FITFI_USDT,PERP_USDT,ZKS_USDT,COMP_USDT,UST_USDT,WSB_USDT,LINK_USDT,GARI_USDT,CFX_USDT,CHR_USDT,MCB_USDT,DGB_USDT,MBOX_USDT,DYDX_USDT,TRB_USDT,HT_USDT,LUNA_USDT,CTK_USDT,PEARL_USDT,ACA_USDT,OCEAN_USDT,TFUEL_USDT,HOT_USDT,XLM_USDT,FTM_USDT,LPT_USDT,ALGO_USDT,SOS_USDT,SHIB_USDT,BSV_USDT,SFP_USDT,FIL6_USDT,EDEN_USDT,BADGER_USDT,DAR_USDT,ALICE_USDT,XEM_USDT,DEFI_USDT,ICP_USDT,FLUX_USDT,BAKE_USDT,LRC_USDT,CVC_USDT,CRO_USDT,MINA_USDT,LIT_USDT,AST_USDT,AUDIO_USDT,FRONT_USDT,XMR_USDT,ZIL_USDT,CTSI_USDT,AGLD_USDT,YGG_USDT,OP_USDT,ZRX_USDT,GT_USDT,XCH_USDT,VET_USDT,MOB_USDT,BICO_USDT,SLP_USDT,ACH_USDT,AR_USDT,CLV_USDT,IMX_USDT,SPELL_USDT,UNFI_USDT,SUSHI_USDT,FTT_USDT,HIGH_USDT,HNT_USDT,ALT_USDT,YFI_USDT,NEAR_USDT,NKN_USDT,XVS_USDT,BAND_USDT,LOKA_USDT,OXY_USDT,BCH_USDT,TOMO_USDT,WAVES_USDT,FIDA_USDT,DIA_USDT,ANC_USDT,CELO_USDT,CRV_USDT,FLM_USDT,GLMR_USDT,FIL_USDT,PEOPLE_USDT,WAXP_USDT,IOTX_USDT,ATOM_USDT,RLC_USDT,BOBA_USDT,HBAR_USDT,REN_USDT,POLIS_USDT,GMT_USDT,KAVA_USDT,KDA_USDT,GALA_USDT,STORJ_USDT,PUNDIX_USDT,BAL_USDT,XAUG_USDT,GRIN_USDT,SXP_USDT,AKRO_USDT,NEXO_USDT,CKB_USDT,API3_USDT,NEST_USDT,POLY_USDT,ETHW_USDT,TONCOIN_USDT,THETA_USDT,TRIBE_USDT,CREAM_USDT,BTC_USDT,GST_USDT,AMPL_USDT,KIN_USDT,BEAM_USDT,SERO_USDT,KSM_USDT,RAD_USDT,BCD_USDT,OLDLUNA_USDT,QTUM_USDT,WOO_USDT,ATA_USDT,AVAX_USDT,EOS_USDT,SNX_USDT,AUCTION_USDT,XRP_USDT,STEP_USDT,GITCOIN_USDT,MATIC_USDT,LEO_USDT,ONT_USDT,LINA_USDT,DASH_USDT,MASK_USDT,ETC_USDT,JST_USDT,LON_USDT,BSW_USDT,CONV_USDT,SKL_USDT,GAL_USDT,DODO_USDT,GRT_USDT,NU_USDT,TRU_USDT,STX_USDT,CVX_USDT,JASMY_USDT,HIVE_USDT,EXCH_USDT,ROSE_USDT,SUPER_USDT,MAPS_USDT,SCRT_USDT,USTC_USDT,ENJ_USDT,BTS_USDT,LOOKS_USDT,PRQ_USDT,RUNE_USDT,FLOW_USDT,CHZ_USDT,DOGE_USDT,1INCH_USDT,PRIV_USDT,C98_USDT,CSPR_USDT,RACA_USDT,CELR_USDT,ENS_USDT,XEC_USDT,POND_USDT,NYM_USDT,PROM_USDT,LDO_USDT,ZEN_USDT,IOST_USDT,REQ_USDT,RNDR_USDT,QUICK_USDT,VRA_USDT,DEGO_USDT,AIOZ_USDT,ARPA_USDT,POLS_USDT,EGLD_USDT,XTZ_USDT,NFT_USDT,ETH_USD,BTC_USD" + }, + "cross_margin": { + "assetEnabled": true, + "enabled": "ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT,BTC_USDT", + "available": "ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT,TARA_USDT,TRX_USDT,OXY_USDT,LON_USDT,DOGE_USDT,ISP_USDT,TWT_USDT,BAO_USDT,QUACK_USDT,ANT_USDT,VGX_USDT,ARPA_USDT,QUICK_USDT,UTK_USDT,HERO_USDT,WSG_USDT,BICO_USDT,MTV_USDT,VET_USDT,GARI_USDT,BCH_USDT,KLAY_USDT,WING_USDT,BLOK_USDT,SPS_USDT,WIKEN_USDT,WSIENNA_USDT,PUNDIX_USDT,FIC_USDT,ASTR_USDT,FET_USDT,VELO_USDT,BENQI_USDT,CWEB_USDT,RIF_USDT,UNI_USDT,ONG_USDT,ERG_USDT,ALPHA_USDT,CELO_USDT,XVG_USDT,GMAT_USDT,BTS_USDT,DOCK_USDT,GMT_USDT,DIA_USDT,CSPR_USDT,NKN_USDT,STAKE_USDT,SWASH_USDT,XEC_USDT,SWRV_USDT,QRDO_USDT,BLES_USDT,EOS_USDT,GRT_USDT,ASM_USDT,FIL6_USDT,GNO_USDT,EGLD_USDT,XYM_USDT,LOOKS_USDT,LOKA_USDT,BNC_USDT,BAS_USDT,SKL_USDT,STMX_USDT,CVC_USDT,DDOS_USDT,COTI_USDT,AVA_USDT,HMT_USDT,DF_USDT,LPT_USDT,XRP_USDT,TVK_USDT,FEVR_USDT,MBL_USDT,KIN_USDT,SPELL_USDT,MATIC_USDT,FTT_USDT,NMR_USDT,PMON_USDT,BNB_USDT,USDD_USDT,LSS_USDT,MDX_USDT,PRQ_USDT,ALPINE_USDT,DEGO_USDT,OMI_USDT,TIPS_USDT,OCT_USDT,FEI_USDT,UMEE_USDT,CRP_USDT,LION_USDT,YFI_USDT,DASH_USDT,REQ_USDT,SDAO_USDT,PNT_USDT,INSUR_USDT,OOKI_USDT,SUN_USDT,CRPT_USDT,BAC_USDT,DATA_USDT,LRN_USDT,JGN_USDT,KIMCHI_USDT,SUKU_USDT,VRA_USDT,AAVE_USDT,FTI_USDT,LDO_USDT,FRA_USDT,BLANK_USDT,NEAR_USDT,ZKS_USDT,MTRG_USDT,RLY_USDT,TCT_USDT,FLY_USDT,JST_USDT,YFII_USDT,AR_USDT,POLY_USDT,JULD_USDT,SOL_USDT,BZZ_USDT,AXS_USDT,ASD_USDT,XMR_USDT,FTM_USDT,HIT_USDT,LEO_USDT,LIT_USDT,PIG_USDT,COMP_USDT,ELON_USDT,IMX_USDT,EFI_USDT,XVS_USDT,WAVES_USDT,PEOPLE_USDT,SOS_USDT,RUNE_USDT,POLC_USDT,SCLP_USDT,BABYDOGE_USDT,KONO_USDT,SPI_USDT,ETC_USDT,MDA_USDT,MTL_USDT,BCHA_USDT,KISHU_USDT,SUNNY_USDT,PYR_USDT,XTZ_USDT,TRIBE_USDT,AUDIO_USDT,FIRO_USDT,MANA_USDT,OKB_USDT,DOG_USDT,SLP_USDT,KNC_USDT,GAS_USDT,LUNA_USDT,SAFEMARS_USDT,MIR_USDT,DAR_USDT,EGS_USDT,KSM_USDT,ATP_USDT,BIT_USDT,STORJ_USDT,XEM_USDT,QTUM_USDT,AGLD_USDT,RVN_USDT,OXT_USDT,SHFT_USDT,IOTX_USDT,LUNC_USDT,NEXO_USDT,AKITA_USDT,PERP_USDT,ONE_USDT,ETH_USDT,FLUX_USDT,FLOKI_USDT,STX_USDT,ANML_USDT,XPRT_USDT,GALA_USDT,GXS_USDT,TORN_USDT,KAI_USDT,1INCH_USDT,CHR_USDT,GAL_USDT,GLMR_USDT,CTX_USDT,CERE_USDT,CART_USDT,STRAX_USDT,MASK_USDT,MKR_USDT,AVAX_USDT,ENJ_USDT,YAM_USDT,ALPACA_USDT,DODO_USDT,MFT_USDT,CAKE_USDT,RNDR_USDT,CTSI_USDT,GRIN_USDT,MXC_USDT,ONT_USDT,ANKR_USDT,SLIM_USDT,FIL_USDT,CTK_USDT,ASR_USDT,FEG_USDT,SERO_USDT,RSS3_USDT,IRIS_USDT,XCH_USDT,ZRX_USDT,BAND_USDT,BADGER_USDT,DAO_USDT,EPS_USDT,THETA_USDT,BAKE_USDT,SHIB_USDT,MBOX_USDT,NBS_USDT,SNT_USDT,DREP_USDT,NFT_USDT,AUCTION_USDT,BOSON_USDT,O3_USDT,NULS_USDT,OMG_USDT,PEARL_USDT,HAPI_USDT,STG_USDT,IDV_USDT,HORD_USDT,ZIL_USDT,SUPER_USDT,DENT_USDT,REN_USDT,RAI_USDT,ZEN_USDT,ALGO_USDT,BLZ_USDT,BOR_USDT,SC_USDT,HEGIC_USDT,MOB_USDT,DORA_USDT,FOR_USDT,FLOW_USDT,RARI_USDT,DYDX_USDT,ATLAS_USDT,GST_USDT,REEF_USDT,HT_USDT,XYO_USDT,CHESS_USDT,BAT_USDT,NYM_USDT,RAMP_USDT,USDC_USDT,ICP_USDT,EPK_USDT,EXRD_USDT,DOT_USDT,COOK_USDT,CKB_USDT,YGG_USDT,CRU_USDT,ANC_USDT,FIS_USDT,ALCX_USDT,HIGH_USDT,BEAM_USDT,BSW_USDT,STAR_USDT,ROSE_USDT,CNNS_USDT,BZRX_USDT,WOO_USDT,SAFEMOON_USDT,VTHO_USDT,OM_USDT,LAMB_USDT,CHZ_USDT,AIOZ_USDT,EDEN_USDT,POND_USDT,ATOM_USDT,UNFI_USDT,FORTH_USDT,MLN_USDT,NEO_USDT,MOVR_USDT,RLC_USDT,FXS_USDT,ENS_USDT,ATA_USDT,XPR_USDT,NEST_USDT,XLM_USDT,AUTO_USDT,SNX_USDT,OCN_USDT,RSR_USDT,MITH_USDT,KAR_USDT,INJ_USDT,PLA_USDT,CYS_USDT,WAXP_USDT,VOXEL_USDT,CRV_USDT,FITFI_USDT,WHALE_USDT,WRX_USDT,TIDAL_USDT,C98_USDT,HNT_USDT,TONCOIN_USDT,DOGGY_USDT,SYS_USDT,NPXS_USDT,CRO_USDT,LEMD_USDT,RAY_USDT,PERL_USDT,CQT_USDT,CFX_USDT,TOMO_USDT,ACA_USDT,SDN_USDT,OKT_USDT,WILD_USDT,BNX_USDT,TRU_USDT,RACA_USDT,SWEAT_USDT,ACH_USDT,AKRO_USDT,BTM_USDT,TKO_USDT,GT_USDT,OCEAN_USDT,WNCG_USDT,BSV_USDT,GHST_USDT,CELR_USDT,LINA_USDT,SAND_USDT,APE_USDT,WICC_USDT,FIDA_USDT,ADA_USDT,PROPS_USDT,METIS_USDT,KAVA_USDT,AERGO_USDT,CONV_USDT,TFUEL_USDT,FRONT_USDT,API3_USDT,FARM_USDT,AE_USDT,LRC_USDT,IOTA_USDT,RFOX_USDT,PHA_USDT,XCN_USDT,NAS_USDT,KEEP_USDT,VIDY_USDT,HOT_USDT,MINA_USDT,ETHW_USDT,ALICE_USDT,HAI_USDT,BTC_USDT,LTC_USDT,LTO_USDT,DC_USDT,NU_USDT,IOST_USDT,RAD_USDT,POLS_USDT,OP_USDT,WXT_USDT,STR_USDT,YIELD_USDT,GM_USDT,SPA_USDT,BTCST_USDT,WEMIX_USDT,CLV_USDT,ICX_USDT,PET_USDT,STARL_USDT,HBAR_USDT,REDTOKEN_USDT,BTT_USDT,LINK_USDT,TLM_USDT,ARES_USDT,GTC_USDT,SUSHI_USDT,KEY_USDT,ALN_USDT,KDA_USDT,DVI_USDT,SXP_USDT,MAPS_USDT,BCD_USDT,SRM_USDT,WIN_USDT,ZEC_USDT,JASMY_USDT" + }, + "margin": { + "assetEnabled": true, + "enabled": "ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT,BTC_USDT", + "available": "ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT,TARA_USDT,TRX_USDT,OXY_USDT,LON_USDT,DOGE_USDT,ISP_USDT,TWT_USDT,BAO_USDT,QUACK_USDT,ANT_USDT,VGX_USDT,ARPA_USDT,QUICK_USDT,UTK_USDT,HERO_USDT,WSG_USDT,BICO_USDT,MTV_USDT,VET_USDT,GARI_USDT,BCH_USDT,KLAY_USDT,WING_USDT,BLOK_USDT,SPS_USDT,WIKEN_USDT,WSIENNA_USDT,PUNDIX_USDT,FIC_USDT,ASTR_USDT,FET_USDT,VELO_USDT,BENQI_USDT,CWEB_USDT,RIF_USDT,UNI_USDT,ONG_USDT,ERG_USDT,ALPHA_USDT,CELO_USDT,XVG_USDT,GMAT_USDT,BTS_USDT,DOCK_USDT,GMT_USDT,DIA_USDT,CSPR_USDT,NKN_USDT,STAKE_USDT,SWASH_USDT,XEC_USDT,SWRV_USDT,QRDO_USDT,BLES_USDT,EOS_USDT,GRT_USDT,ASM_USDT,FIL6_USDT,GNO_USDT,EGLD_USDT,XYM_USDT,LOOKS_USDT,LOKA_USDT,BNC_USDT,BAS_USDT,SKL_USDT,STMX_USDT,CVC_USDT,DDOS_USDT,COTI_USDT,AVA_USDT,HMT_USDT,DF_USDT,LPT_USDT,XRP_USDT,TVK_USDT,FEVR_USDT,MBL_USDT,KIN_USDT,SPELL_USDT,MATIC_USDT,FTT_USDT,NMR_USDT,PMON_USDT,BNB_USDT,USDD_USDT,LSS_USDT,MDX_USDT,PRQ_USDT,ALPINE_USDT,DEGO_USDT,OMI_USDT,TIPS_USDT,OCT_USDT,FEI_USDT,UMEE_USDT,CRP_USDT,LION_USDT,YFI_USDT,DASH_USDT,REQ_USDT,SDAO_USDT,PNT_USDT,INSUR_USDT,OOKI_USDT,SUN_USDT,CRPT_USDT,BAC_USDT,DATA_USDT,LRN_USDT,JGN_USDT,KIMCHI_USDT,SUKU_USDT,VRA_USDT,AAVE_USDT,FTI_USDT,LDO_USDT,FRA_USDT,BLANK_USDT,NEAR_USDT,ZKS_USDT,MTRG_USDT,RLY_USDT,TCT_USDT,FLY_USDT,JST_USDT,YFII_USDT,AR_USDT,POLY_USDT,JULD_USDT,SOL_USDT,BZZ_USDT,AXS_USDT,ASD_USDT,XMR_USDT,FTM_USDT,HIT_USDT,LEO_USDT,LIT_USDT,PIG_USDT,COMP_USDT,ELON_USDT,IMX_USDT,EFI_USDT,XVS_USDT,WAVES_USDT,PEOPLE_USDT,SOS_USDT,RUNE_USDT,POLC_USDT,SCLP_USDT,BABYDOGE_USDT,KONO_USDT,SPI_USDT,ETC_USDT,MDA_USDT,MTL_USDT,BCHA_USDT,KISHU_USDT,SUNNY_USDT,PYR_USDT,XTZ_USDT,TRIBE_USDT,AUDIO_USDT,FIRO_USDT,MANA_USDT,OKB_USDT,DOG_USDT,SLP_USDT,KNC_USDT,GAS_USDT,LUNA_USDT,SAFEMARS_USDT,MIR_USDT,DAR_USDT,EGS_USDT,KSM_USDT,ATP_USDT,BIT_USDT,STORJ_USDT,XEM_USDT,QTUM_USDT,AGLD_USDT,RVN_USDT,OXT_USDT,SHFT_USDT,IOTX_USDT,LUNC_USDT,NEXO_USDT,AKITA_USDT,PERP_USDT,ONE_USDT,ETH_USDT,FLUX_USDT,FLOKI_USDT,STX_USDT,ANML_USDT,XPRT_USDT,GALA_USDT,GXS_USDT,TORN_USDT,KAI_USDT,1INCH_USDT,CHR_USDT,GAL_USDT,GLMR_USDT,CTX_USDT,CERE_USDT,CART_USDT,STRAX_USDT,MASK_USDT,MKR_USDT,AVAX_USDT,ENJ_USDT,YAM_USDT,ALPACA_USDT,DODO_USDT,MFT_USDT,CAKE_USDT,RNDR_USDT,CTSI_USDT,GRIN_USDT,MXC_USDT,ONT_USDT,ANKR_USDT,SLIM_USDT,FIL_USDT,CTK_USDT,ASR_USDT,FEG_USDT,SERO_USDT,RSS3_USDT,IRIS_USDT,XCH_USDT,ZRX_USDT,BAND_USDT,BADGER_USDT,DAO_USDT,EPS_USDT,THETA_USDT,BAKE_USDT,SHIB_USDT,MBOX_USDT,NBS_USDT,SNT_USDT,DREP_USDT,NFT_USDT,AUCTION_USDT,BOSON_USDT,O3_USDT,NULS_USDT,OMG_USDT,PEARL_USDT,HAPI_USDT,STG_USDT,IDV_USDT,HORD_USDT,ZIL_USDT,SUPER_USDT,DENT_USDT,REN_USDT,RAI_USDT,ZEN_USDT,ALGO_USDT,BLZ_USDT,BOR_USDT,SC_USDT,HEGIC_USDT,MOB_USDT,DORA_USDT,FOR_USDT,FLOW_USDT,RARI_USDT,DYDX_USDT,ATLAS_USDT,GST_USDT,REEF_USDT,HT_USDT,XYO_USDT,CHESS_USDT,BAT_USDT,NYM_USDT,RAMP_USDT,USDC_USDT,ICP_USDT,EPK_USDT,EXRD_USDT,DOT_USDT,COOK_USDT,CKB_USDT,YGG_USDT,CRU_USDT,ANC_USDT,FIS_USDT,ALCX_USDT,HIGH_USDT,BEAM_USDT,BSW_USDT,STAR_USDT,ROSE_USDT,CNNS_USDT,BZRX_USDT,WOO_USDT,SAFEMOON_USDT,VTHO_USDT,OM_USDT,LAMB_USDT,CHZ_USDT,AIOZ_USDT,EDEN_USDT,POND_USDT,ATOM_USDT,UNFI_USDT,FORTH_USDT,MLN_USDT,NEO_USDT,MOVR_USDT,RLC_USDT,FXS_USDT,ENS_USDT,ATA_USDT,XPR_USDT,NEST_USDT,XLM_USDT,AUTO_USDT,SNX_USDT,OCN_USDT,RSR_USDT,MITH_USDT,KAR_USDT,INJ_USDT,PLA_USDT,CYS_USDT,WAXP_USDT,VOXEL_USDT,CRV_USDT,FITFI_USDT,WHALE_USDT,WRX_USDT,TIDAL_USDT,C98_USDT,HNT_USDT,TONCOIN_USDT,DOGGY_USDT,SYS_USDT,NPXS_USDT,CRO_USDT,LEMD_USDT,RAY_USDT,PERL_USDT,CQT_USDT,CFX_USDT,TOMO_USDT,ACA_USDT,SDN_USDT,OKT_USDT,WILD_USDT,BNX_USDT,TRU_USDT,RACA_USDT,SWEAT_USDT,ACH_USDT,AKRO_USDT,BTM_USDT,TKO_USDT,GT_USDT,OCEAN_USDT,WNCG_USDT,BSV_USDT,GHST_USDT,CELR_USDT,LINA_USDT,SAND_USDT,APE_USDT,WICC_USDT,FIDA_USDT,ADA_USDT,PROPS_USDT,METIS_USDT,KAVA_USDT,AERGO_USDT,CONV_USDT,TFUEL_USDT,FRONT_USDT,API3_USDT,FARM_USDT,AE_USDT,LRC_USDT,IOTA_USDT,RFOX_USDT,PHA_USDT,XCN_USDT,NAS_USDT,KEEP_USDT,VIDY_USDT,HOT_USDT,MINA_USDT,ETHW_USDT,ALICE_USDT,HAI_USDT,BTC_USDT,LTC_USDT,LTO_USDT,DC_USDT,NU_USDT,IOST_USDT,RAD_USDT,POLS_USDT,OP_USDT,WXT_USDT,STR_USDT,YIELD_USDT,GM_USDT,SPA_USDT,BTCST_USDT,WEMIX_USDT,CLV_USDT,ICX_USDT,PET_USDT,STARL_USDT,HBAR_USDT,REDTOKEN_USDT,BTT_USDT,LINK_USDT,TLM_USDT,ARES_USDT,GTC_USDT,SUSHI_USDT,KEY_USDT,ALN_USDT,KDA_USDT,DVI_USDT,SXP_USDT,MAPS_USDT,BCD_USDT,SRM_USDT,WIN_USDT,ZEC_USDT,JASMY_USDT" + }, + "delivery": { + "assetEnabled": true, + "enabled": "BTC_USDT_20230630", + "available": "BTC_USD_20221021,BTC_USD_20221014,BTC_USD_20230331,BTC_USD_20221230,BTC_USDT_20221021,BTC_USDT_20221014,BTC_USDT_20230331,BTC_USDT_20221230" } } }, @@ -1539,7 +1570,7 @@ }, "enabled": { "autoPairUpdates": true, - "websocketAPI": false + "websocketAPI": true } }, "bankAccounts": [ diff --git a/currency/code.go b/currency/code.go index 1eebe3ee..2083400c 100644 --- a/currency/code.go +++ b/currency/code.go @@ -187,7 +187,7 @@ func (b *BaseCodes) Register(c string, newRole Role) Code { var format bool // Digits fool upper and lower casing. So find first letter and check case. for x := range c { - if !unicode.IsDigit(rune(c[x])) { + if unicode.IsLetter(rune(c[x])) { format = unicode.IsUpper(rune(c[x])) break } diff --git a/currency/code_types.go b/currency/code_types.go index 21a03ca8..5a3f8258 100644 --- a/currency/code_types.go +++ b/currency/code_types.go @@ -2999,6 +2999,7 @@ var ( USDFL = NewCode("USDFL") FLUSD = NewCode("FLUSD") DUSD = NewCode("DUSD") + STETH = NewCode("STETH") stables = Currencies{ USDT, diff --git a/currency/pair.go b/currency/pair.go index 2a5b2978..a216d86e 100644 --- a/currency/pair.go +++ b/currency/pair.go @@ -84,17 +84,29 @@ func NewPairFromIndex(currencyPair, index string) (Pair, error) { // NewPairFromString converts currency string into a new CurrencyPair // with or without delimiter func NewPairFromString(currencyPair string) (Pair, error) { - for x := range delimiters { - if strings.Contains(currencyPair, delimiters[x]) { - return NewPairDelimiter(currencyPair, delimiters[x]) - } - } if len(currencyPair) < 3 { return EMPTYPAIR, - fmt.Errorf("%w from %s string too short to be a current pair", + fmt.Errorf("%w from %s string too short to be a currency pair", errCannotCreatePair, currencyPair) } + var delimiter string + pairStrings := []string{currencyPair} + for x := range delimiters { + if strings.Contains(pairStrings[0], delimiters[x]) { + values := strings.SplitN(pairStrings[0], delimiters[x], 2) + if delimiter != "" { + values[1] += delimiter + pairStrings[1] + pairStrings = values + } else { + pairStrings = values + } + delimiter = delimiters[x] + } + } + if delimiter != "" { + return Pair{Base: NewCode(pairStrings[0]), Delimiter: delimiter, Quote: NewCode(pairStrings[1])}, nil + } return NewPairFromStrings(currencyPair[0:3], currencyPair[3:]) } diff --git a/currency/pair_test.go b/currency/pair_test.go index b68ecdad..fc1af247 100644 --- a/currency/pair_test.go +++ b/currency/pair_test.go @@ -507,6 +507,25 @@ func TestNewPairFromString(t *testing.T) { actual, expected, ) } + pairMap := map[string]Pair{ + "BTC_USDT-20230630-45000-C": {Base: NewCode("BTC"), Delimiter: UnderscoreDelimiter, Quote: NewCode("USDT-20230630-45000-C")}, + "BTC-USD-221007": {Base: NewCode("BTC"), Delimiter: DashDelimiter, Quote: NewCode("USD-221007")}, + "IHT_ETH": {Base: NewCode("IHT"), Delimiter: UnderscoreDelimiter, Quote: NewCode("ETH")}, + "BTC-USD-220930-30000-P": {Base: NewCode("BTC"), Delimiter: DashDelimiter, Quote: NewCode("USD-220930-30000-P")}, + "XBTUSDTM": {Base: NewCode("XBT"), Delimiter: "", Quote: NewCode("USDTM")}, + "BTC-PERPETUAL": {Base: NewCode("BTC"), Delimiter: DashDelimiter, Quote: NewCode("PERPETUAL")}, + "SOL-21OCT22-20-C": {Base: NewCode("SOL"), Delimiter: DashDelimiter, Quote: NewCode("21OCT22-20-C")}, + "SOL-FS-30DEC22_28OCT22": {Base: NewCode("SOL"), Delimiter: DashDelimiter, Quote: NewCode("FS-30DEC22_28OCT22")}, + } + for key, expectedPair := range pairMap { + pair, err = NewPairFromString(key) + if err != nil { + t.Fatal(err) + } + if !pair.Equal(expectedPair) || pair.Delimiter != expectedPair.Delimiter { + t.Errorf("Pair(): %s was not equal to expected value: %s", pair.String(), expectedPair.String()) + } + } } func TestNewPairFromFormattedPairs(t *testing.T) { diff --git a/engine/websocketroutine_manager.go b/engine/websocketroutine_manager.go index a27d61f9..439eb0f3 100644 --- a/engine/websocketroutine_manager.go +++ b/engine/websocketroutine_manager.go @@ -234,6 +234,24 @@ func (m *WebsocketRoutineManager) websocketDataHandler(exchName string, data int return err } m.syncer.PrintTickerSummary(d, "websocket", err) + case []ticker.Price: + for x := range d { + if m.syncer.IsRunning() { + err := m.syncer.Update(exchName, + d[x].Pair, + d[x].AssetType, + SyncItemTicker, + nil) + if err != nil { + return err + } + } + err := ticker.ProcessTicker(&d[x]) + if err != nil { + return err + } + m.syncer.PrintTickerSummary(&d[x], "websocket", err) + } case stream.KlineData: if m.verbose { log.Infof(log.WebsocketMgr, "%s websocket %s %s kline updated %+v", @@ -242,6 +260,16 @@ func (m *WebsocketRoutineManager) websocketDataHandler(exchName string, data int d.AssetType, d) } + case []stream.KlineData: + for x := range d { + if m.verbose { + log.Infof(log.WebsocketMgr, "%s websocket %s %s kline updated %+v", + exchName, + m.FormatCurrency(d[x].Pair), + d[x].AssetType, + d) + } + } case *orderbook.Depth: base, err := d.Retrieve() if err != nil { @@ -281,6 +309,30 @@ func (m *WebsocketRoutineManager) websocketDataHandler(exchName string, data int } m.printOrderSummary(d, true) } + case []order.Detail: + for x := range d { + if !m.orderManager.Exists(&d[x]) { + err := m.orderManager.Add(&d[x]) + if err != nil { + return err + } + m.printOrderSummary(&d[x], false) + } else { + od, err := m.orderManager.GetByExchangeAndID(d[x].Exchange, d[x].OrderID) + if err != nil { + return err + } + err = od.UpdateOrderFromDetail(&d[x]) + if err != nil { + return err + } + err = m.orderManager.UpdateExistingOrder(od) + if err != nil { + return err + } + m.printOrderSummary(&d[x], true) + } + } case order.ClassificationError: return fmt.Errorf("%w %s", d.Err, d.Error()) case stream.UnhandledMessageWarning: @@ -289,6 +341,12 @@ func (m *WebsocketRoutineManager) websocketDataHandler(exchName string, data int if m.verbose { m.printAccountHoldingsChangeSummary(d) } + case []account.Change: + if m.verbose { + for x := range d { + m.printAccountHoldingsChangeSummary(d[x]) + } + } case []trade.Data: if m.verbose { log.Infof(log.Trade, "%+v", d) diff --git a/exchanges/asset/asset.go b/exchanges/asset/asset.go index 217c63f2..aa2b7003 100644 --- a/exchanges/asset/asset.go +++ b/exchanges/asset/asset.go @@ -23,12 +23,14 @@ const ( Empty Item = 0 Spot Item = 1 << iota Margin + CrossMargin MarginFunding Index Binary PerpetualContract PerpetualSwap Futures + DeliveryFutures UpsideProfitContract DownsideProfitContract CoinMarginedFutures @@ -36,17 +38,19 @@ const ( USDCMarginedFutures Options - futuresFlag = PerpetualContract | PerpetualSwap | Futures | UpsideProfitContract | DownsideProfitContract | CoinMarginedFutures | USDTMarginedFutures | USDCMarginedFutures - supportedFlag = Spot | Margin | MarginFunding | Index | Binary | PerpetualContract | PerpetualSwap | Futures | UpsideProfitContract | DownsideProfitContract | CoinMarginedFutures | USDTMarginedFutures | USDCMarginedFutures | Options + futuresFlag = PerpetualContract | PerpetualSwap | Futures | DeliveryFutures | UpsideProfitContract | DownsideProfitContract | CoinMarginedFutures | USDTMarginedFutures | USDCMarginedFutures + supportedFlag = Spot | Margin | CrossMargin | MarginFunding | Index | Binary | PerpetualContract | PerpetualSwap | Futures | DeliveryFutures | UpsideProfitContract | DownsideProfitContract | CoinMarginedFutures | USDTMarginedFutures | USDCMarginedFutures | Options spot = "spot" margin = "margin" + crossMargin = "cross_margin" // for Gateio exchange marginFunding = "marginfunding" index = "index" binary = "binary" perpetualContract = "perpetualcontract" perpetualSwap = "perpetualswap" futures = "futures" + deliveryFutures = "delivery" upsideProfitContract = "upsideprofitcontract" downsideProfitContract = "downsideprofitcontract" coinMarginedFutures = "coinmarginedfutures" @@ -56,7 +60,7 @@ const ( ) var ( - supportedList = Items{Spot, Margin, MarginFunding, Index, Binary, PerpetualContract, PerpetualSwap, Futures, UpsideProfitContract, DownsideProfitContract, CoinMarginedFutures, USDTMarginedFutures, USDCMarginedFutures, Options} + supportedList = Items{Spot, Margin, CrossMargin, MarginFunding, Index, Binary, PerpetualContract, PerpetualSwap, Futures, DeliveryFutures, UpsideProfitContract, DownsideProfitContract, CoinMarginedFutures, USDTMarginedFutures, USDCMarginedFutures, Options} ) // Supported returns a list of supported asset types @@ -71,6 +75,8 @@ func (a Item) String() string { return spot case Margin: return margin + case CrossMargin: + return crossMargin case MarginFunding: return marginFunding case Index: @@ -83,6 +89,8 @@ func (a Item) String() string { return perpetualSwap case Futures: return futures + case DeliveryFutures: + return deliveryFutures case UpsideProfitContract: return upsideProfitContract case DownsideProfitContract: @@ -170,6 +178,10 @@ func New(input string) (Item, error) { return Margin, nil case marginFunding: return MarginFunding, nil + case crossMargin: + return CrossMargin, nil + case deliveryFutures: + return DeliveryFutures, nil case index: return Index, nil case binary: diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index 18e22411..8116728a 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -1189,7 +1189,7 @@ func TestWsCandleResponse(t *testing.T) { } func TestWsOrderSnapshot(t *testing.T) { - b.WsAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "") pressXToJSON := `[0,"os",[[34930659963,null,1574955083558,"tETHUSD",1574955083558,1574955083573,0.201104,0.201104,"EXCHANGE LIMIT",null,null,null,0,"ACTIVE",null,null,120,0,0,0,null,null,null,0,0,null,null,null,"BFX",null,null,null]]]` err := b.wsHandleData([]byte(pressXToJSON)) if err != nil { @@ -1203,7 +1203,7 @@ func TestWsOrderSnapshot(t *testing.T) { } func TestWsNotifications(t *testing.T) { - b.WsAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "") pressXToJSON := `[0,"n",[1575282446099,"fon-req",null,null,[41238905,null,null,null,-1000,null,null,null,null,null,null,null,null,null,0.002,2,null,null,null,null,null],null,"SUCCESS","Submitting funding bid of 1000.0 USD at 0.2000 for 2 days."]]` err := b.wsHandleData([]byte(pressXToJSON)) if err != nil { @@ -1218,7 +1218,7 @@ func TestWsNotifications(t *testing.T) { } func TestWSFundingOfferSnapshotAndUpdate(t *testing.T) { - b.WsAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "") pressXToJSON := `[0,"fos",[[41237920,"fETH",1573912039000,1573912039000,0.5,0.5,"LIMIT",null,null,0,"ACTIVE",null,null,null,0.0024,2,0,0,null,0,null]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) @@ -1231,7 +1231,7 @@ func TestWSFundingOfferSnapshotAndUpdate(t *testing.T) { } func TestWSFundingCreditSnapshotAndUpdate(t *testing.T) { - b.WsAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "") pressXToJSON := `[0,"fcs",[[26223578,"fUST",1,1575052261000,1575296187000,350,0,"ACTIVE",null,null,null,0,30,1575052261000,1575293487000,0,0,null,0,null,0,"tBTCUST"],[26223711,"fUSD",-1,1575291961000,1575296187000,180,0,"ACTIVE",null,null,null,0.002,7,1575282446000,1575295587000,0,0,null,0,null,0,"tETHUSD"]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) @@ -1244,7 +1244,7 @@ func TestWSFundingCreditSnapshotAndUpdate(t *testing.T) { } func TestWSFundingLoanSnapshotAndUpdate(t *testing.T) { - b.WsAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "") pressXToJSON := `[0,"fls",[[2995442,"fUSD",-1,1575291961000,1575295850000,820,0,"ACTIVE",null,null,null,0.002,7,1575282446000,1575295850000,0,0,null,0,null,0]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) @@ -1257,7 +1257,7 @@ func TestWSFundingLoanSnapshotAndUpdate(t *testing.T) { } func TestWSWalletSnapshot(t *testing.T) { - b.WsAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "") pressXToJSON := `[0,"ws",[["exchange","SAN",19.76,0,null,null,null]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) @@ -1265,7 +1265,7 @@ func TestWSWalletSnapshot(t *testing.T) { } func TestWSBalanceUpdate(t *testing.T) { - b.WsAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "") const pressXToJSON = `[0,"bu",[4131.85,4131.85]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) @@ -1273,7 +1273,7 @@ func TestWSBalanceUpdate(t *testing.T) { } func TestWSMarginInfoUpdate(t *testing.T) { - b.WsAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "") const pressXToJSON = `[0,"miu",["base",[-13.014640000000007,0,49331.70267297,49318.68803297,27]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) @@ -1281,7 +1281,7 @@ func TestWSMarginInfoUpdate(t *testing.T) { } func TestWSFundingInfoUpdate(t *testing.T) { - b.WsAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "") const pressXToJSON = `[0,"fiu",["sym","tETHUSD",[149361.09689202666,149639.26293509,830.0182168075556,895.0658432466332]]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) @@ -1289,7 +1289,7 @@ func TestWSFundingInfoUpdate(t *testing.T) { } func TestWSFundingTrade(t *testing.T) { - b.WsAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "") pressXToJSON := `[0,"fte",[636854,"fUSD",1575282446000,41238905,-1000,0.002,7,null]]` if err := b.wsHandleData([]byte(pressXToJSON)); err != nil { t.Error(err) diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index 7bb742d0..d17f7d4d 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -172,7 +172,7 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error { } if status == "OK" { b.Websocket.DataHandler <- d - b.WsAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "") } else if status == "fail" { if code, ok := d["code"].(string); ok { return fmt.Errorf("websocket unable to AUTH. Error code: %s", @@ -238,7 +238,7 @@ func (b *Bitfinex) wsHandleData(respRaw []byte) error { if err != nil { return err } - case len(pairInfo) == 1: + case len(pairInfo) == 1 && chanInfo.Pair != "": newPair := pairInfo[0] if newPair[0] == 'f' { chanAsset = asset.MarginFunding diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index e9538ff0..a6bc4d85 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -2,6 +2,9 @@ package gateio import ( "context" + "crypto/hmac" + "crypto/sha512" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -9,38 +12,151 @@ import ( "net/url" "strconv" "strings" + "time" - "github.com/thrasher-corp/gocryptotrader/common/convert" - "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" - "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) const ( - gateioTradeURL = "https://api.gateio.io" - gateioMarketURL = "https://data.gateio.io" - gateioAPIVersion = "api2/1" + gateioTradeURL = "https://api.gateio.ws/" + gateioAPIVersion + gateioFuturesTestnetTrading = "https://fx-api-testnet.gateio.ws" + gateioFuturesLiveTradingAlternative = "https://fx-api.gateio.ws/" + gateioAPIVersion + gateioAPIVersion = "api/v4/" - gateioSymbol = "pairs" - gateioMarketInfo = "marketinfo" - gateioKline = "candlestick2" - gateioOrder = "private" - gateioBalances = "private/balances" - gateioCancelOrder = "private/cancelOrder" - gateioCancelAllOrders = "private/cancelAllOrders" - gateioWithdraw = "private/withdraw" - gateioOpenOrders = "private/openOrders" - gateioTradeHistory = "private/tradeHistory" - gateioDepositAddress = "private/depositAddress" - gateioTicker = "ticker" - gateioTrades = "tradeHistory" - gateioTickers = "tickers" - gateioOrderbook = "orderBook" + // SubAccount Endpoints + subAccounts = "sub_accounts" - gateioGenerateAddress = "New address is being generated for you, please wait a moment and refresh this page. " + // Spot + gateioSpotCurrencies = "spot/currencies" + gateioSpotCurrencyPairs = "spot/currency_pairs" + gateioSpotTickers = "spot/tickers" + gateioSpotOrderbook = "spot/order_book" + gateioSpotMarketTrades = "spot/trades" + gateioSpotCandlesticks = "spot/candlesticks" + gateioSpotFeeRate = "spot/fee" + gateioSpotAccounts = "spot/accounts" + gateioSpotBatchOrders = "spot/batch_orders" + gateioSpotOpenOrders = "spot/open_orders" + gateioSpotClosePositionWhenCrossCurrencyDisabledPath = "spot/cross_liquidate_orders" + gateioSpotOrders = "spot/orders" + gateioSpotCancelBatchOrders = "spot/cancel_batch_orders" + gateioSpotMyTrades = "spot/my_trades" + gateioSpotServerTime = "spot/time" + gateioSpotAllCountdown = "spot/countdown_cancel_all" + gateioSpotPriceOrders = "spot/price_orders" + + // Wallets + walletCurrencyChain = "wallet/currency_chains" + walletDepositAddress = "wallet/deposit_address" + walletWithdrawals = "wallet/withdrawals" + walletDeposits = "wallet/deposits" + walletTransfer = "wallet/transfers" + walletSubAccountTransfer = "wallet/sub_account_transfers" + walletInterSubAccountTransfer = "wallet/sub_account_to_sub_account" + walletWithdrawStatus = "wallet/withdraw_status" + walletSubAccountBalance = "wallet/sub_account_balances" + walletSubAccountMarginBalance = "wallet/sub_account_margin_balances" + walletSubAccountFuturesBalance = "wallet/sub_account_futures_balances" + walletSubAccountCrossMarginBalances = "wallet/sub_account_cross_margin_balances" + walletSavedAddress = "wallet/saved_address" + walletTradingFee = "wallet/fee" + walletTotalBalance = "wallet/total_balance" + + // Margin + gateioMarginCurrencyPairs = "margin/currency_pairs" + gateioMarginFundingBook = "margin/funding_book" + gateioMarginAccount = "margin/accounts" + gateioMarginAccountBook = "margin/account_book" + gateioMarginFundingAccounts = "margin/funding_accounts" + gateioMarginLoans = "margin/loans" + gateioMarginMergedLoans = "margin/merged_loans" + gateioMarginLoanRecords = "margin/loan_records" + gateioMarginAutoRepay = "margin/auto_repay" + gateioMarginTransfer = "margin/transferable" + gateioMarginBorrowable = "margin/borrowable" + gateioCrossMarginCurrencies = "margin/cross/currencies" + gateioCrossMarginAccounts = "margin/cross/accounts" + gateioCrossMarginAccountBook = "margin/cross/account_book" + gateioCrossMarginLoans = "margin/cross/loans" + gateioCrossMarginRepayments = "margin/cross/repayments" + gateioCrossMarginTransferable = "margin/cross/transferable" + gateioCrossMarginBorrowable = "margin/cross/borrowable" + + // Options + gateioOptionUnderlyings = "options/underlyings" + gateioOptionExpiration = "options/expirations" + gateioOptionContracts = "options/contracts" + gateioOptionSettlement = "options/settlements" + gateioOptionMySettlements = "options/my_settlements" + gateioOptionsOrderbook = "options/order_book" + gateioOptionsTickers = "options/tickers" + gateioOptionCandlesticks = "options/candlesticks" + gateioOptionUnderlyingCandlesticks = "options/underlying/candlesticks" + gateioOptionsTrades = "options/trades" + gateioOptionAccounts = "options/accounts" + gateioOptionsAccountbook = "options/account_book" + gateioOptionsPosition = "options/positions" + gateioOptionsPositionClose = "options/position_close" + gateioOptionsOrders = "options/orders" + gateioOptionsMyTrades = "options/my_trades" + + // Flash Swap + gateioFlashSwapCurrencies = "flash_swap/currencies" + gateioFlashSwapOrders = "flash_swap/orders" + gateioFlashSwapOrdersPreview = "flash_swap/orders/preview" + + // Withdrawals + withdrawal = "withdrawals" +) + +const ( + utc0TimeZone = "utc0" + utc8TimeZone = "utc8" +) + +var ( + errEmptySettlementCurrency = errors.New("empty settlement currency") + errInvalidOrMissingContractParam = errors.New("invalid or empty contract") + errNoValidResponseFromServer = errors.New("no valid response from server") + errInvalidUnderlying = errors.New("missing underlying") + errInvalidOrderSize = errors.New("invalid order size") + errInvalidOrderID = errors.New("invalid order id") + errInvalidAmount = errors.New("invalid amount") + errInvalidOrEmptySubaccount = errors.New("invalid or empty subaccount") + errInvalidTransferDirection = errors.New("invalid transfer direction") + errInvalidOrderSide = errors.New("invalid order side") + errDifferentAccount = errors.New("account type must be identical for all orders") + errInvalidPrice = errors.New("invalid price") + errNoValidParameterPassed = errors.New("no valid parameter passed") + errInvalidCountdown = errors.New("invalid countdown, Countdown time, in seconds At least 5 seconds, 0 means cancel the countdown") + errInvalidOrderStatus = errors.New("invalid order status") + errInvalidLoanSide = errors.New("invalid loan side, only 'lend' and 'borrow'") + errInvalidLoanID = errors.New("missing loan ID") + errInvalidRepayMode = errors.New("invalid repay mode specified, must be 'all' or 'partial'") + errMissingPreviewID = errors.New("missing required parameter: preview_id") + errChangeHasToBePositive = errors.New("change has to be positive") + errInvalidLeverageValue = errors.New("invalid leverage value") + errInvalidRiskLimit = errors.New("new position risk limit") + errInvalidCountTotalValue = errors.New("invalid \"count_total\" value, supported \"count_total\" values are 0 and 1") + errInvalidTimeInForce = errors.New("invalid time in force value") + errInvalidAutoSizeValue = errors.New("invalid \"auto_size\" value, only \"close_long\" and \"close_short\" are supported") + errTooManyOrderRequest = errors.New("too many order creation request") + errInvalidTimeout = errors.New("invalid timeout, should be in seconds At least 5 seconds, 0 means cancel the countdown") + errNoTickerData = errors.New("no ticker data available") + errOnlyLimitOrderType = errors.New("only order type 'limit' is allowed") + errNilArgument = errors.New("null argument") + errInvalidTimezone = errors.New("invalid timezone") + errMultipleOrders = errors.New("multiple orders passed") + errMissingWithdrawalID = errors.New("missing withdrawal ID") + errInvalidSubAccountUserID = errors.New("sub-account user id is required") + errCannotParseSettlementCurrency = errors.New("cannot derive settlement currency") + errMissingAPIKey = errors.New("missing API key information") ) // Gateio is the overarching type across this package @@ -48,280 +164,878 @@ type Gateio struct { exchange.Base } -// GetSymbols returns all supported symbols -func (g *Gateio) GetSymbols(ctx context.Context) ([]string, error) { - var result []string - urlPath := fmt.Sprintf("/%s/%s", gateioAPIVersion, gateioSymbol) - err := g.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, urlPath, &result) - return result, err +// ***************************************** SubAccounts ******************************** + +// CreateNewSubAccount creates a new sub-account +func (g *Gateio) CreateNewSubAccount(ctx context.Context, arg SubAccountParams) (*SubAccount, error) { + if arg.LoginName == "" { + return nil, errors.New("login name can not be empty") + } + var response *SubAccount + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, http.MethodPost, subAccounts, nil, &arg, &response) } -// GetMarketInfo returns information about all trading pairs, including -// transaction fee, minimum order quantity, price accuracy and so on -func (g *Gateio) GetMarketInfo(ctx context.Context) (MarketInfoResponse, error) { - type response struct { - Result string `json:"result"` - Pairs []interface{} `json:"pairs"` - } - - urlPath := fmt.Sprintf("/%s/%s", gateioAPIVersion, gateioMarketInfo) - var res response - var result MarketInfoResponse - err := g.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, urlPath, &res) - if err != nil { - return result, err - } - - result.Result = res.Result - for _, v := range res.Pairs { - item, ok := v.(map[string]interface{}) - if !ok { - return result, errors.New("unable to type assert item") - } - for itemk, itemv := range item { - pairv, ok := itemv.(map[string]interface{}) - if !ok { - return result, errors.New("unable to type assert pairv") - } - decimalPlaces, ok := pairv["decimal_places"].(float64) - if !ok { - return result, errors.New("unable to type assert decimal_places") - } - minAmount, ok := pairv["min_amount"].(float64) - if !ok { - return result, errors.New("unable to type assert min_amount") - } - fee, ok := pairv["fee"].(float64) - if !ok { - return result, errors.New("unable to type assert fee") - } - result.Pairs = append(result.Pairs, MarketInfoPairsResponse{ - Symbol: itemk, - DecimalPlaces: decimalPlaces, - MinAmount: minAmount, - Fee: fee, - }) - } - } - return result, nil +// GetSubAccounts retrieves list of sub-accounts for given account +func (g *Gateio) GetSubAccounts(ctx context.Context) ([]SubAccount, error) { + var response []SubAccount + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, http.MethodGet, subAccounts, nil, nil, &response) } -// GetLatestSpotPrice returns latest spot price of symbol -// updated every 10 seconds +// GetSingleSubAccount retrieves a single sub-account for given account +func (g *Gateio) GetSingleSubAccount(ctx context.Context, userID string) (*SubAccount, error) { + if userID == "" { + return nil, errors.New("user ID can not be empty") + } + var response *SubAccount + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, http.MethodGet, + subAccounts+"/"+userID, nil, nil, &response) +} + +// CreateAPIKeysOfSubAccount creates a sub-account for the sub-account // -// symbol: string of currency pair -func (g *Gateio) GetLatestSpotPrice(ctx context.Context, symbol string) (float64, error) { - res, err := g.GetTicker(ctx, symbol) - if err != nil { - return 0, err +// name: Permission name (all permissions will be removed if no value is passed) +// >> wallet: wallet, spot: spot/margin, futures: perpetual contract, delivery: delivery, earn: earn, options: options +func (g *Gateio) CreateAPIKeysOfSubAccount(ctx context.Context, arg CreateAPIKeySubAccountParams) (*CreateAPIKeyResponse, error) { + if arg.SubAccountUserID == 0 { + return nil, errInvalidSubAccountUserID } - - return res.Last, nil + if arg.Body == nil { + return nil, errors.New("sub-account key information is required") + } + var resp *CreateAPIKeyResponse + return resp, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPost, "sub_accounts/"+strconv.FormatInt(arg.SubAccountUserID, 10)+"/keys", nil, &arg, &resp) } -// GetTicker returns a ticker for the supplied symbol -// updated every 10 seconds -func (g *Gateio) GetTicker(ctx context.Context, symbol string) (TickerResponse, error) { - urlPath := fmt.Sprintf("/%s/%s/%s", gateioAPIVersion, gateioTicker, symbol) - var res TickerResponse - return res, g.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, urlPath, &res) +// GetAllAPIKeyOfSubAccount list all API Key of the sub-account +func (g *Gateio) GetAllAPIKeyOfSubAccount(ctx context.Context, userID int64) ([]CreateAPIKeyResponse, error) { + var resp []CreateAPIKeyResponse + return resp, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, "sub_accounts/"+strconv.FormatInt(userID, 10)+"/keys", nil, nil, &resp) } -// GetTickers returns tickers for all symbols -func (g *Gateio) GetTickers(ctx context.Context) (map[string]TickerResponse, error) { - urlPath := fmt.Sprintf("/%s/%s", gateioAPIVersion, gateioTickers) - resp := make(map[string]TickerResponse) - err := g.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, urlPath, &resp) +// UpdateAPIKeyOfSubAccount update API key of the sub-account +func (g *Gateio) UpdateAPIKeyOfSubAccount(ctx context.Context, subAccountAPIKey string, arg CreateAPIKeySubAccountParams) error { + return g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPut, "sub_accounts/"+strconv.FormatInt(arg.SubAccountUserID, 10)+"/keys/"+subAccountAPIKey, nil, &arg, nil) +} + +// GetAPIKeyOfSubAccount retrieves the API Key of the sub-account +func (g *Gateio) GetAPIKeyOfSubAccount(ctx context.Context, subAccountUserID int64, apiKey string) (*CreateAPIKeyResponse, error) { + if subAccountUserID == 0 { + return nil, errInvalidSubAccountUserID + } + if apiKey == "" { + return nil, errMissingAPIKey + } + var resp *CreateAPIKeyResponse + return resp, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, "sub_accounts/"+strconv.FormatInt(subAccountUserID, 10)+"/keys/"+apiKey, nil, nil, &resp) +} + +// LockSubAccount locks the sub-account +func (g *Gateio) LockSubAccount(ctx context.Context, subAccountUserID int64) error { + if subAccountUserID == 0 { + return errInvalidSubAccountUserID + } + return g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodPost, "sub_accounts/"+strconv.FormatInt(subAccountUserID, 10)+"/lock", nil, nil, nil) +} + +// UnlockSubAccount locks the sub-account +func (g *Gateio) UnlockSubAccount(ctx context.Context, subAccountUserID int64) error { + if subAccountUserID == 0 { + return errInvalidSubAccountUserID + } + return g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodPost, "sub_accounts/"+strconv.FormatInt(subAccountUserID, 10)+"/unlock", nil, nil, nil) +} + +// ***************************************** Spot ************************************** + +// ListSpotCurrencies to retrieve detailed list of each currency. +func (g *Gateio) ListSpotCurrencies(ctx context.Context) ([]CurrencyInfo, error) { + var resp []CurrencyInfo + return resp, g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, gateioSpotCurrencies, &resp) +} + +// GetCurrencyDetail details of a specific currency. +func (g *Gateio) GetCurrencyDetail(ctx context.Context, ccy currency.Code) (*CurrencyInfo, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + var resp *CurrencyInfo + return resp, g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, + gateioSpotCurrencies+"/"+ccy.String(), &resp) +} + +// ListSpotCurrencyPairs retrieve all currency pairs supported by the exchange. +func (g *Gateio) ListSpotCurrencyPairs(ctx context.Context) ([]CurrencyPairDetail, error) { + var resp []CurrencyPairDetail + return resp, g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, gateioSpotCurrencyPairs, &resp) +} + +// GetCurrencyPairDetail to get details of a specific order for spot/margin accounts. +func (g *Gateio) GetCurrencyPairDetail(ctx context.Context, currencyPair string) (*CurrencyPairDetail, error) { + if currencyPair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + var resp *CurrencyPairDetail + return resp, g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, + gateioSpotCurrencyPairs+"/"+currencyPair, &resp) +} + +// GetTickers retrieve ticker information +// Return only related data if currency_pair is specified; otherwise return all of them +func (g *Gateio) GetTickers(ctx context.Context, currencyPair, timezone string) ([]Ticker, error) { + params := url.Values{} + if currencyPair != "" { + params.Set("currency_pair", currencyPair) + } + if timezone != "" && timezone != utc8TimeZone && timezone != utc0TimeZone { + return nil, errInvalidTimezone + } else if timezone != "" { + params.Set("timezone", timezone) + } + var tickers []Ticker + return tickers, g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, common.EncodeURLValues(gateioSpotTickers, params), &tickers) +} + +// GetTicker retrieves a single ticker information for a currency pair. +func (g *Gateio) GetTicker(ctx context.Context, currencyPair, timezone string) (*Ticker, error) { + if currencyPair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + tickers, err := g.GetTickers(ctx, currencyPair, timezone) if err != nil { return nil, err } - return resp, nil -} - -// GetTrades returns trades for symbols -func (g *Gateio) GetTrades(ctx context.Context, symbol string) (TradeHistory, error) { - urlPath := fmt.Sprintf("/%s/%s/%s", gateioAPIVersion, gateioTrades, symbol) - var resp TradeHistory - err := g.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, urlPath, &resp) - if err != nil { - return TradeHistory{}, err + if len(tickers) > 0 { + return &tickers[0], err } - return resp, nil + return nil, fmt.Errorf("no ticker data found for currency pair %v", currencyPair) } -// GetOrderbook returns the orderbook data for a suppled symbol -func (g *Gateio) GetOrderbook(ctx context.Context, symbol string) (*Orderbook, error) { - urlPath := fmt.Sprintf("/%s/%s/%s", gateioAPIVersion, gateioOrderbook, symbol) - var resp OrderbookResponse - err := g.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, urlPath, &resp) +// GetIntervalString returns a string representation of the interval according to the Gateio exchange representation. +func (g *Gateio) GetIntervalString(interval kline.Interval) (string, error) { + switch interval { + case kline.HundredMilliseconds: + return "100ms", nil + case kline.ThousandMilliseconds: + return "1000ms", nil + case kline.TenSecond: + return "10s", nil + case kline.ThirtySecond: + return "30s", nil + case kline.OneMin: + return "1m", nil + case kline.FiveMin: + return "5m", nil + case kline.FifteenMin: + return "15m", nil + case kline.ThirtyMin: + return "30m", nil + case kline.OneHour: + return "1h", nil + case kline.TwoHour: + return "2h", nil + case kline.FourHour: + return "4h", nil + case kline.EightHour: + return "8h", nil + case kline.TwelveHour: + return "12h", nil + case kline.OneDay: + return "1d", nil + case kline.SevenDay: + return "7d", nil + case kline.OneMonth: + return "30d", nil + default: + return "", kline.ErrUnsupportedInterval + } +} + +// GetIntervalFromString returns a kline.Interval representation of the interval string +func (g *Gateio) GetIntervalFromString(interval string) (kline.Interval, error) { + switch interval { + case "10s": + return kline.TenSecond, nil + case "30s": + return kline.ThirtySecond, nil + case "1m": + return kline.OneMin, nil + case "5m": + return kline.FiveMin, nil + case "15m": + return kline.FifteenMin, nil + case "30m": + return kline.ThirtyMin, nil + case "1h": + return kline.OneHour, nil + case "2h": + return kline.TwoHour, nil + case "4h": + return kline.FourHour, nil + case "8h": + return kline.EightHour, nil + case "12h": + return kline.TwelveHour, nil + case "1d": + return kline.OneDay, nil + case "7d": + return kline.SevenDay, nil + case "30d": + return kline.OneMonth, nil + case "100ms": + return kline.HundredMilliseconds, nil + case "1000ms": + return kline.ThousandMilliseconds, nil + default: + return kline.Interval(0), kline.ErrUnsetInterval + } +} + +// GetOrderbook returns the orderbook data for a suppled currency pair +func (g *Gateio) GetOrderbook(ctx context.Context, pairString, interval string, limit uint64, withOrderbookID bool) (*Orderbook, error) { + if pairString == "" { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("currency_pair", pairString) + if interval != "" { + params.Set("interval", interval) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + params.Set("with_id", strconv.FormatBool(withOrderbookID)) + var response *OrderbookData + err := g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, common.EncodeURLValues(gateioSpotOrderbook, params), &response) if err != nil { return nil, err } - - switch { - case resp.Result != "true": - return nil, errors.New("result was not true") - case len(resp.Asks) == 0: - return nil, errors.New("asks are empty") - case len(resp.Bids) == 0: - return nil, errors.New("bids are empty") - } - - // Asks are in reverse order - ob := Orderbook{ - Result: resp.Result, - Elapsed: resp.Elapsed, - Bids: make([]OrderbookItem, len(resp.Bids)), - Asks: make([]OrderbookItem, 0, len(resp.Asks)), - } - - for x := len(resp.Asks) - 1; x != 0; x-- { - price, err := strconv.ParseFloat(resp.Asks[x][0], 64) - if err != nil { - return nil, err - } - - amount, err := strconv.ParseFloat(resp.Asks[x][1], 64) - if err != nil { - return nil, err - } - - ob.Asks = append(ob.Asks, OrderbookItem{Price: price, Amount: amount}) - } - - for x := range resp.Bids { - price, err := strconv.ParseFloat(resp.Bids[x][0], 64) - if err != nil { - return nil, err - } - - amount, err := strconv.ParseFloat(resp.Bids[x][1], 64) - if err != nil { - return nil, err - } - - ob.Bids[x] = OrderbookItem{Price: price, Amount: amount} - } - return &ob, nil + return response.MakeOrderbook() } -// GetSpotKline returns kline data for the most recent time period -func (g *Gateio) GetSpotKline(ctx context.Context, arg KlinesRequestParams) ([]kline.Candle, error) { - urlPath := fmt.Sprintf("/%s/%s/%s?group_sec=%s&range_hour=%d", - gateioAPIVersion, - gateioKline, - arg.Symbol, - arg.GroupSec, - arg.HourSize) +// GetMarketTrades retrieve market trades +func (g *Gateio) GetMarketTrades(ctx context.Context, pairString currency.Pair, limit uint64, lastID string, reverse bool, from, to time.Time, page uint64) ([]Trade, error) { + params := url.Values{} + if pairString.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + params.Set("currency_pair", pairString.String()) + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if lastID != "" { + params.Set("last_id", lastID) + } + if reverse { + params.Set("reverse", strconv.FormatBool(reverse)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if page != 0 { + params.Set("page", strconv.FormatUint(page, 10)) + } + var response []Trade + return response, g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, + common.EncodeURLValues(gateioSpotMarketTrades, params), &response) +} +// GetCandlesticks retrieves market candlesticks. +func (g *Gateio) GetCandlesticks(ctx context.Context, currencyPair currency.Pair, limit uint64, from, to time.Time, interval kline.Interval) ([]Candlestick, error) { + params := url.Values{} + if currencyPair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + params.Set("currency_pair", currencyPair.String()) + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var err error + if interval.Duration().Microseconds() != 0 { + var intervalString string + intervalString, err = g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params.Set("interval", intervalString) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + var candles [][7]string + err = g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, common.EncodeURLValues(gateioSpotCandlesticks, params), &candles) + if err != nil { + return nil, err + } + if len(candles) == 0 { + return nil, fmt.Errorf("no candlesticks available for instrument %v", currencyPair) + } + candlesticks := make([]Candlestick, len(candles)) + for x := range candles { + timestamp, err := strconv.ParseInt(candles[x][0], 10, 64) + if err != nil { + return nil, err + } + quoteTradingVolume, err := strconv.ParseFloat(candles[x][1], 64) + if err != nil { + return nil, err + } + closePrice, err := strconv.ParseFloat(candles[x][2], 64) + if err != nil { + return nil, err + } + highestPrice, err := strconv.ParseFloat(candles[x][3], 64) + if err != nil { + return nil, err + } + lowestPrice, err := strconv.ParseFloat(candles[x][4], 64) + if err != nil { + return nil, err + } + openPrice, err := strconv.ParseFloat(candles[x][5], 64) + if err != nil { + return nil, err + } + baseCurrencyAmount, err := strconv.ParseFloat(candles[x][6], 64) + if err != nil { + return nil, err + } + candlesticks[x] = Candlestick{ + Timestamp: time.Unix(timestamp, 0), + QuoteCcyVolume: quoteTradingVolume, + ClosePrice: closePrice, + HighestPrice: highestPrice, + LowestPrice: lowestPrice, + OpenPrice: openPrice, + BaseCcyAmount: baseCurrencyAmount, + } + } + return candlesticks, nil +} + +// GetTradingFeeRatio retrieves user trading fee rates +func (g *Gateio) GetTradingFeeRatio(ctx context.Context, currencyPair currency.Pair) (*SpotTradingFeeRate, error) { + params := url.Values{} + if currencyPair.IsPopulated() { + // specify a currency pair to retrieve precise fee rate + params.Set("currency_pair", currencyPair.String()) + } + var response *SpotTradingFeeRate + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioSpotFeeRate, params, nil, &response) +} + +// GetSpotAccounts retrieves spot account. +func (g *Gateio) GetSpotAccounts(ctx context.Context, ccy currency.Code) ([]SpotAccount, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("currency", ccy.String()) + } + var response []SpotAccount + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioSpotAccounts, params, nil, &response) +} + +// CreateBatchOrders Create a batch of orders Batch orders requirements: custom order field text is required At most 4 currency pairs, +// maximum 10 orders each, are allowed in one request No mixture of spot orders and margin orders, i.e. account must be identical for all orders +func (g *Gateio) CreateBatchOrders(ctx context.Context, args []CreateOrderRequestData) ([]SpotOrder, error) { + if len(args) > 10 { + return nil, fmt.Errorf("%w only 10 orders are canceled at once", errMultipleOrders) + } + var err error + for x := range args { + if (x != 0) && args[x-1].Account != args[x].Account { + return nil, errDifferentAccount + } + if args[x].CurrencyPair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + if args[x].Type != "limit" { + return nil, errors.New("only order type limit is allowed") + } + args[x].Side = strings.ToLower(args[x].Side) + if args[x].Side != "buy" && args[x].Side != "sell" { + return nil, errInvalidOrderSide + } + if !strings.EqualFold(args[x].Account, asset.Spot.String()) && + !strings.EqualFold(args[x].Account, asset.CrossMargin.String()) && + !strings.EqualFold(args[x].Account, asset.Margin.String()) { + return nil, errors.New("only spot, margin, and cross_margin area allowed") + } + if args[x].Text == "" { + args[x].Text, err = common.GenerateRandomString(10, common.NumberCharacters) + if err != nil { + return nil, err + } + args[x].Text = "t-" + args[x].Text + } + if args[x].Amount <= 0 { + return nil, errInvalidAmount + } + if args[x].Price <= 0 { + return nil, errInvalidPrice + } + } + var response []SpotOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPost, gateioSpotBatchOrders, nil, &args, &response) +} + +// GateioSpotOpenOrders retrieves all open orders +// List open orders in all currency pairs. +// Note that pagination parameters affect record number in each currency pair's open order list. No pagination is applied to the number of currency pairs returned. All currency pairs with open orders will be returned. +// Spot and margin orders are returned by default. To list cross margin orders, account must be set to cross_margin +func (g *Gateio) GateioSpotOpenOrders(ctx context.Context, page, limit uint64, isCrossMargin bool) ([]SpotOrdersDetail, error) { + params := url.Values{} + if page > 0 { + params.Set("page", strconv.FormatUint(page, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if isCrossMargin { + params.Set("account", asset.CrossMargin.String()) + } + var response []SpotOrdersDetail + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioSpotOpenOrders, params, nil, &response) +} + +// SpotClosePositionWhenCrossCurrencyDisabled set close position when cross-currency is disabled +func (g *Gateio) SpotClosePositionWhenCrossCurrencyDisabled(ctx context.Context, arg *ClosePositionRequestParam) (*SpotOrder, error) { + if arg == nil { + return nil, errNilArgument + } + if arg.CurrencyPair.IsInvalid() { + return nil, currency.ErrCurrencyPairEmpty + } + if arg.Amount <= 0 { + return nil, errInvalidAmount + } + if arg.Price <= 0 { + return nil, errInvalidPrice + } + var response *SpotOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, + http.MethodPost, gateioSpotClosePositionWhenCrossCurrencyDisabledPath, nil, &arg, &response) +} + +// PlaceSpotOrder creates a spot order you can place orders with spot, margin or cross margin account through setting the accountfield. +// It defaults to spot, which means spot account is used to place orders. +func (g *Gateio) PlaceSpotOrder(ctx context.Context, arg *CreateOrderRequestData) (*SpotOrder, error) { + if arg == nil { + return nil, errNilArgument + } + if arg.CurrencyPair.IsInvalid() { + return nil, currency.ErrCurrencyPairEmpty + } + if arg.Type != "limit" { + return nil, errOnlyLimitOrderType + } + arg.Side = strings.ToLower(arg.Side) + if arg.Side != "buy" && arg.Side != "sell" { + return nil, errInvalidOrderSide + } + if !strings.EqualFold(arg.Account, asset.Spot.String()) && + !strings.EqualFold(arg.Account, asset.CrossMargin.String()) && + !strings.EqualFold(arg.Account, asset.Margin.String()) { + return nil, errors.New("only 'spot', 'cross_margin', and 'margin' area allowed") + } + if arg.Text != "" { + arg.Text = "t-" + arg.Text + } else { + randomString, err := common.GenerateRandomString(10, common.NumberCharacters) + if err != nil { + return nil, err + } + arg.Text = "t-" + randomString + } + if arg.Amount <= 0 { + return nil, errInvalidAmount + } + if arg.Price <= 0 { + return nil, errInvalidPrice + } + var response *SpotOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPost, gateioSpotOrders, nil, &arg, &response) +} + +// GetSpotOrders retrieves spot orders. +func (g *Gateio) GetSpotOrders(ctx context.Context, currencyPair currency.Pair, status string, page, limit uint64) ([]SpotOrder, error) { + if currencyPair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("currency_pair", currencyPair.String()) + if status != "" { + params.Set("status", status) + } + if page > 0 { + params.Set("page", strconv.FormatUint(page, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var response []SpotOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodGet, gateioSpotOrders, params, nil, &response) +} + +// CancelAllOpenOrdersSpecifiedCurrencyPair cancel all open orders in specified currency pair +func (g *Gateio) CancelAllOpenOrdersSpecifiedCurrencyPair(ctx context.Context, currencyPair currency.Pair, side order.Side, account asset.Item) ([]SpotOrder, error) { + if currencyPair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("currency_pair", currencyPair.String()) + if side == order.Buy || side == order.Sell { + params.Set("side", strings.ToLower(side.Title())) + } + if account == asset.Spot || account == asset.Margin || account == asset.CrossMargin { + params.Set("account", account.String()) + } + var response []SpotOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, spotCancelOrdersEPL, http.MethodDelete, gateioSpotOrders, params, nil, &response) +} + +// CancelBatchOrdersWithIDList cancels batch orders specifying the order ID and currency pair information +// Multiple currency pairs can be specified, but maximum 20 orders are allowed per request +func (g *Gateio) CancelBatchOrdersWithIDList(ctx context.Context, args []CancelOrderByIDParam) ([]CancelOrderByIDResponse, error) { + var response []CancelOrderByIDResponse + if len(args) == 0 { + return nil, errNoValidParameterPassed + } else if len(args) > 20 { + return nil, fmt.Errorf("%w maximum order size to cancel is 20", errInvalidOrderSize) + } + for x := 0; x < len(args); x++ { + if args[x].CurrencyPair.IsEmpty() || args[x].ID == "" { + return nil, errors.New("currency pair and order ID are required") + } + } + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotCancelOrdersEPL, http.MethodPost, gateioSpotCancelBatchOrders, nil, &args, &response) +} + +// GetSpotOrder retrieves a single spot order using the order id and currency pair information. +func (g *Gateio) GetSpotOrder(ctx context.Context, orderID string, currencyPair currency.Pair, account asset.Item) (*SpotOrder, error) { + if orderID == "" { + return nil, errInvalidOrderID + } + if currencyPair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("currency_pair", currencyPair.String()) + if accountType := account.String(); accountType != "" { + params.Set("account", accountType) + } + var response *SpotOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioSpotOrders+"/"+orderID, params, nil, &response) +} + +// AmendSpotOrder amend an order +// By default, the orders of spot and margin account are updated. +// If you need to modify orders of the cross-margin account, you must specify account as cross_margin. +// For portfolio margin account, only cross_margin account is supported. +func (g *Gateio) AmendSpotOrder(ctx context.Context, orderID string, currencyPair currency.Pair, isCrossMarginAccount bool, arg *PriceAndAmount) (*SpotOrder, error) { + if arg == nil { + return nil, errNilArgument + } + if orderID == "" { + return nil, errInvalidOrderID + } + if currencyPair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("currency_pair", currencyPair.String()) + if isCrossMarginAccount { + params.Set("account", asset.CrossMargin.String()) + } + if arg.Amount != 0 && arg.Price != 0 { + return nil, errors.New("only can chose one of amount or price") + } + var resp *SpotOrder + return resp, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPatch, gateioSpotOrders+"/"+orderID, params, arg, &resp) +} + +// CancelSingleSpotOrder cancels a single order +// Spot and margin orders are cancelled by default. +// If trying to cancel cross margin orders or portfolio margin account are used, account must be set to cross_margin +func (g *Gateio) CancelSingleSpotOrder(ctx context.Context, orderID, currencyPair string, isCrossMarginAccount bool) (*SpotOrder, error) { + if orderID == "" { + return nil, errInvalidOrderID + } + if currencyPair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("currency_pair", currencyPair) + if isCrossMarginAccount { + params.Set("account", asset.CrossMargin.String()) + } + var response *SpotOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, spotCancelOrdersEPL, http.MethodDelete, gateioSpotOrders+"/"+orderID, params, nil, &response) +} + +// GateIOGetPersonalTradingHistory retrieves personal trading history +func (g *Gateio) GateIOGetPersonalTradingHistory(ctx context.Context, currencyPair currency.Pair, + orderID string, page, limit uint64, crossMarginAccount bool, from, to time.Time) ([]SpotPersonalTradeHistory, error) { + params := url.Values{} + if currencyPair.IsPopulated() { + params.Set("currency_pair", currencyPair.String()) + } + if orderID != "" { + params.Set("order_id", orderID) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if page > 0 { + params.Set("page", strconv.FormatUint(page, 10)) + } + if crossMarginAccount { + params.Set("account", asset.CrossMargin.String()) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() && to.After(from) { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + var response []SpotPersonalTradeHistory + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioSpotMyTrades, params, nil, &response) +} + +// GetServerTime retrieves current server time +func (g *Gateio) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) { resp := struct { - Data [][]string `json:"data"` - Result string `json:"result"` + ServerTime int64 `json:"server_time"` + }{} + err := g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, gateioSpotServerTime, &resp) + if err != nil { + return time.Time{}, err + } + return time.Unix(resp.ServerTime, 0), nil +} + +// CountdownCancelorders Countdown cancel orders +// When the timeout set by the user is reached, if there is no cancel or set a new countdown, the related pending orders will be automatically cancelled. +// This endpoint can be called repeatedly to set a new countdown or cancel the countdown. +func (g *Gateio) CountdownCancelorders(ctx context.Context, arg CountdownCancelOrderParam) (*TriggerTimeResponse, error) { + if arg.Timeout <= 0 { + return nil, errInvalidCountdown + } + var response *TriggerTimeResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotCancelOrdersEPL, http.MethodPost, gateioSpotAllCountdown, nil, &arg, &response) +} + +// CreatePriceTriggeredOrder create a price-triggered order +func (g *Gateio) CreatePriceTriggeredOrder(ctx context.Context, arg *PriceTriggeredOrderParam) (*OrderID, error) { + if arg == nil { + return nil, errNilArgument + } + if arg.Put.TimeInForce != gtcTIF && arg.Put.TimeInForce != iocTIF { + return nil, fmt.Errorf("%w, only 'gct' and 'ioc' are supported", errInvalidTimeInForce) + } + if arg.Market.IsEmpty() { + return nil, fmt.Errorf("%w, %s", currency.ErrCurrencyPairEmpty, "field market is required") + } + if arg.Trigger.Price < 0 { + return nil, fmt.Errorf("%w trigger price found %f, but expected trigger_price >=0", errInvalidPrice, arg.Trigger.Price) + } + if arg.Trigger.Rule != "<=" && arg.Trigger.Rule != ">=" { + return nil, fmt.Errorf("invalid price trigger condition or rule '%s' but expected '>=' or '<='", arg.Trigger.Rule) + } + if arg.Trigger.Expiration <= 0 { + return nil, errors.New("invalid expiration(seconds to wait for the condition to be triggered before cancelling the order)") + } + arg.Put.Side = strings.ToLower(arg.Put.Side) + arg.Put.Type = strings.ToLower(arg.Put.Type) + if arg.Put.Type != "limit" { + return nil, errors.New("invalid order type, only order type 'limit' is allowed") + } + if arg.Put.Side != "buy" && arg.Put.Side != "sell" { + return nil, errInvalidOrderSide + } + if arg.Put.Price < 0 { + return nil, fmt.Errorf("%w, %s", errInvalidPrice, "put price has to be greater than 0") + } + if arg.Put.Amount <= 0 { + return nil, errInvalidAmount + } + arg.Put.Account = strings.ToLower(arg.Put.Account) + if arg.Put.Account == "" { + arg.Put.Account = "normal" + } + var response *OrderID + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPost, gateioSpotPriceOrders, nil, &arg, &response) +} + +// GetPriceTriggeredOrderList retrieves price orders created with an order detail and trigger price information. +func (g *Gateio) GetPriceTriggeredOrderList(ctx context.Context, status string, market currency.Pair, account asset.Item, offset, limit uint64) ([]SpotPriceTriggeredOrder, error) { + if status != statusOpen && status != statusFinished { + return nil, fmt.Errorf("%w status %s", errInvalidOrderStatus, status) + } + params := url.Values{} + params.Set("status", status) + if market.IsPopulated() { + params.Set("market", market.String()) + } + if account == asset.CrossMargin { + params.Set("account", account.String()) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + var response []SpotPriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioSpotPriceOrders, params, nil, &response) +} + +// CancelMultipleSpotOpenOrders deletes price triggered orders. +func (g *Gateio) CancelMultipleSpotOpenOrders(ctx context.Context, currencyPair currency.Pair, account asset.Item) ([]SpotPriceTriggeredOrder, error) { + params := url.Values{} + if currencyPair.IsPopulated() { + params.Set("market", currencyPair.String()) + } + switch account { + case asset.Empty: + return nil, asset.ErrNotSupported + case asset.Spot: + params.Set("account", "normal") + default: + params.Set("account", account.String()) + } + var response []SpotPriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotCancelOrdersEPL, http.MethodDelete, gateioSpotPriceOrders, params, nil, &response) +} + +// GetSinglePriceTriggeredOrder get a single order +func (g *Gateio) GetSinglePriceTriggeredOrder(ctx context.Context, orderID string) (*SpotPriceTriggeredOrder, error) { + if orderID == "" { + return nil, errInvalidOrderID + } + var response *SpotPriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioSpotPriceOrders+"/"+orderID, nil, nil, &response) +} + +// CancelPriceTriggeredOrder cancel a price-triggered order +func (g *Gateio) CancelPriceTriggeredOrder(ctx context.Context, orderID string) (*SpotPriceTriggeredOrder, error) { + if orderID == "" { + return nil, errInvalidOrderID + } + var response *SpotPriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotCancelOrdersEPL, http.MethodGet, gateioSpotPriceOrders+"/"+orderID, nil, nil, &response) +} + +// GenerateSignature returns hash for authenticated requests +func (g *Gateio) GenerateSignature(secret, method, path, query string, body interface{}, dtime time.Time) (string, error) { + h := sha512.New() + if body != nil { + val, err := json.Marshal(body) + if err != nil { + return "", err + } + h.Write(val) + } + h.Write(nil) + hashedPayload := hex.EncodeToString(h.Sum(nil)) + t := strconv.FormatInt(dtime.Unix(), 10) + rawQuery, err := url.QueryUnescape(query) + if err != nil { + return "", err + } + msg := method + "\n" + path + "\n" + rawQuery + "\n" + hashedPayload + "\n" + t + mac := hmac.New(sha512.New, []byte(secret)) + mac.Write([]byte(msg)) + return hex.EncodeToString(mac.Sum(nil)), nil +} + +// SendAuthenticatedHTTPRequest sends authenticated requests to the Gateio API +// To use this you must setup an APIKey and APISecret from the exchange +func (g *Gateio) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, epl request.EndpointLimit, method, endpoint string, param url.Values, data, result interface{}) error { + creds, err := g.GetCredentials(ctx) + if err != nil { + return err + } + ePoint, err := g.API.Endpoints.GetURL(ep) + if err != nil { + return err + } + var intermediary json.RawMessage + err = g.SendPayload(ctx, epl, func() (*request.Item, error) { + headers := make(map[string]string) + urlPath := endpoint + timestamp := time.Now() + var paramValue string + if param != nil { + paramValue = param.Encode() + } + var sig string + sig, err = g.GenerateSignature(creds.Secret, method, "/"+gateioAPIVersion+endpoint, paramValue, data, timestamp) + if err != nil { + return nil, err + } + headers["Content-Type"] = "application/json" + headers["KEY"] = creds.Key + headers["TIMESTAMP"] = strconv.FormatInt(timestamp.Unix(), 10) + headers["Accept"] = "application/json" + headers["SIGN"] = sig + urlPath = ePoint + urlPath + if param != nil { + urlPath = common.EncodeURLValues(urlPath, param) + } + var payload string + if data != nil { + var byteData []byte + byteData, err = json.Marshal(data) + if err != nil { + return nil, err + } + payload = string(byteData) + } + return &request.Item{ + Method: method, + Path: urlPath, + Headers: headers, + Body: strings.NewReader(payload), + Result: &intermediary, + AuthRequest: true, + Verbose: g.Verbose, + HTTPDebugging: g.HTTPDebugging, + HTTPRecording: g.HTTPRecording, + }, nil + }) + if err != nil { + return err + } + errCap := struct { + Label string `json:"label"` + Code string `json:"code"` + Message string `json:"message"` }{} - if err := g.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, urlPath, &resp); err != nil { - return nil, err + if err := json.Unmarshal(intermediary, &errCap); err == nil && errCap.Code != "" { + return fmt.Errorf("%s auth request error, code: %s message: %s", + g.Name, + errCap.Label, + errCap.Message) } - if resp.Result != "true" || len(resp.Data) == 0 { - return nil, errors.New("rawKlines unexpected data returned") + if result == nil { + return nil } - - timeSeries := make([]kline.Candle, len(resp.Data)) - for x := range resp.Data { - if len(resp.Data[x]) < 6 { - return nil, fmt.Errorf("unexpected kline data length") - } - otString, err := strconv.ParseFloat(resp.Data[x][0], 64) - if err != nil { - return nil, err - } - orderType, err := convert.TimeFromUnixTimestampFloat(otString) - if err != nil { - return nil, fmt.Errorf("cannot parse Kline.OpenTime. Err: %s", err) - } - _vol, err := convert.FloatFromString(resp.Data[x][1]) - if err != nil { - return nil, fmt.Errorf("cannot parse Kline.Volume. Err: %s", err) - } - _close, err := convert.FloatFromString(resp.Data[x][2]) - if err != nil { - return nil, fmt.Errorf("cannot parse Kline.Close. Err: %s", err) - } - _high, err := convert.FloatFromString(resp.Data[x][3]) - if err != nil { - return nil, fmt.Errorf("cannot parse Kline.High. Err: %s", err) - } - _low, err := convert.FloatFromString(resp.Data[x][4]) - if err != nil { - return nil, fmt.Errorf("cannot parse Kline.Low. Err: %s", err) - } - _open, err := convert.FloatFromString(resp.Data[x][5]) - if err != nil { - return nil, fmt.Errorf("cannot parse Kline.Open. Err: %s", err) - } - timeSeries[x] = kline.Candle{ - Time: orderType, - Volume: _vol, - Close: _close, - High: _high, - Low: _low, - Open: _open, - } - } - return timeSeries, nil -} - -// GetBalances obtains the users account balance -func (g *Gateio) GetBalances(ctx context.Context) (BalancesResponse, error) { - var result BalancesResponse - return result, - g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, gateioBalances, "", &result) -} - -// SpotNewOrder places a new order -func (g *Gateio) SpotNewOrder(ctx context.Context, arg SpotNewOrderRequestParams) (SpotNewOrderResponse, error) { - var result SpotNewOrderResponse - - // Be sure to use the correct price precision before calling this - params := fmt.Sprintf("currencyPair=%s&rate=%s&amount=%s", - arg.Symbol, - strconv.FormatFloat(arg.Price, 'f', -1, 64), - strconv.FormatFloat(arg.Amount, 'f', -1, 64), - ) - - urlPath := fmt.Sprintf("%s/%s", gateioOrder, arg.Type) - return result, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, urlPath, params, &result) -} - -// CancelExistingOrder cancels an order given the supplied orderID and symbol -// orderID order ID number -// symbol trade pair (ltc_btc) -func (g *Gateio) CancelExistingOrder(ctx context.Context, orderID int64, symbol string) (bool, error) { - type response struct { - Result bool `json:"result"` - Code int `json:"code"` - Message string `json:"message"` - } - - var result response - // Be sure to use the correct price precision before calling this - params := fmt.Sprintf("orderNumber=%d¤cyPair=%s", - orderID, - symbol, - ) - err := g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, gateioCancelOrder, params, &result) - if err != nil { - return false, err - } - if !result.Result { - return false, fmt.Errorf("code:%d message:%s", result.Code, result.Message) - } - - return true, nil + return json.Unmarshal(intermediary, result) } // SendHTTPRequest sends an unauthenticated HTTP request -func (g *Gateio) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, result interface{}) error { +func (g *Gateio) SendHTTPRequest(ctx context.Context, ep exchange.URL, epl request.EndpointLimit, path string, result interface{}) error { endpoint, err := g.API.Endpoints.GetURL(ep) if err != nil { return err @@ -334,181 +1048,2771 @@ func (g *Gateio) SendHTTPRequest(ctx context.Context, ep exchange.URL, path stri HTTPDebugging: g.HTTPDebugging, HTTPRecording: g.HTTPRecording, } - return g.SendPayload(ctx, request.Unset, func() (*request.Item, error) { + return g.SendPayload(ctx, epl, func() (*request.Item, error) { return item, nil }) } -// CancelAllExistingOrders all orders for a given symbol and side -// orderType (0: sell,1: buy,-1: unlimited) -func (g *Gateio) CancelAllExistingOrders(ctx context.Context, orderType int64, symbol string) error { - type response struct { - Result bool `json:"result"` - Code int `json:"code"` - Message string `json:"message"` - } +// *********************************** Withdrawals ****************************** - var result response - params := fmt.Sprintf("type=%d¤cyPair=%s", - orderType, - symbol, - ) - err := g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, gateioCancelAllOrders, params, &result) - if err != nil { - return err +// WithdrawCurrency to withdraw a currency. +func (g *Gateio) WithdrawCurrency(ctx context.Context, arg WithdrawalRequestParam) (*WithdrawalResponse, error) { + if arg.Amount <= 0 { + return nil, fmt.Errorf("%w currency amount must be greater than zero", errInvalidAmount) } - - if !result.Result { - return fmt.Errorf("code:%d message:%s", result.Code, result.Message) + if arg.Currency.IsEmpty() { + return nil, fmt.Errorf("%w currency to be withdrawal nust be specified", currency.ErrCurrencyCodeEmpty) } - - return nil + if arg.Chain == "" { + return nil, errors.New("name of the chain used for withdrawal must be specified") + } + var response *WithdrawalResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, withdrawalEPL, http.MethodPost, withdrawal, nil, &arg, &response) } -// GetOpenOrders retrieves all open orders with an optional symbol filter -func (g *Gateio) GetOpenOrders(ctx context.Context, symbol string) (OpenOrdersResponse, error) { - var params string - var result OpenOrdersResponse - - if symbol != "" { - params = fmt.Sprintf("currencyPair=%s", symbol) +// CancelWithdrawalWithSpecifiedID cancels withdrawal with specified ID. +func (g *Gateio) CancelWithdrawalWithSpecifiedID(ctx context.Context, withdrawalID string) (*WithdrawalResponse, error) { + if withdrawalID == "" { + return nil, errMissingWithdrawalID } - - err := g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, gateioOpenOrders, params, &result) - if err != nil { - return result, err - } - - if result.Code > 0 { - return result, fmt.Errorf("code:%d message:%s", result.Code, result.Message) - } - - return result, nil + var response *WithdrawalResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, withdrawalEPL, http.MethodDelete, withdrawal+"/"+withdrawalID, nil, nil, &response) } -// GetTradeHistory retrieves all orders with an optional symbol filter -func (g *Gateio) GetTradeHistory(ctx context.Context, symbol string) (TradeHistoryResponse, error) { - var params string - var result TradeHistoryResponse - params = fmt.Sprintf("currencyPair=%s", symbol) +// *********************************** Wallet *********************************** - err := g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, gateioTradeHistory, params, &result) - if err != nil { - return result, err +// ListCurrencyChain retrieves a list of currency chain name +func (g *Gateio) ListCurrencyChain(ctx context.Context, ccy currency.Code) ([]CurrencyChain, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty } - - if result.Code > 0 { - return result, fmt.Errorf("code:%d message:%s", result.Code, result.Message) - } - - return result, nil + params := url.Values{} + params.Set("currency", ccy.String()) + var resp []CurrencyChain + return resp, g.SendHTTPRequest(ctx, exchange.RestSpot, walletEPL, common.EncodeURLValues(walletCurrencyChain, params), &resp) } -// GenerateSignature returns hash for authenticated requests -func (g *Gateio) GenerateSignature(secret, message string) ([]byte, error) { - return crypto.GetHMAC(crypto.HashSHA512, []byte(message), []byte(secret)) +// GenerateCurrencyDepositAddress generate currency deposit address +func (g *Gateio) GenerateCurrencyDepositAddress(ctx context.Context, ccy currency.Code) (*CurrencyDepositAddressInfo, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + params.Set("currency", ccy.String()) + var response *CurrencyDepositAddressInfo + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, + http.MethodGet, walletDepositAddress, params, nil, &response) } -// SendAuthenticatedHTTPRequest sends authenticated requests to the Gateio API -// To use this you must setup an APIKey and APISecret from the exchange -func (g *Gateio) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, method, endpoint, param string, result interface{}) error { - creds, err := g.GetCredentials(ctx) - if err != nil { - return err +// GetWithdrawalRecords retrieves withdrawal records. Record time range cannot exceed 30 days +func (g *Gateio) GetWithdrawalRecords(ctx context.Context, ccy currency.Code, from, to time.Time, offset, limit uint64) ([]WithdrawalResponse, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("currency", ccy.String()) } - ePoint, err := g.API.Endpoints.GetURL(ep) - if err != nil { - return err + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) } - headers := make(map[string]string) - headers["Content-Type"] = "application/x-www-form-urlencoded" - headers["key"] = creds.Key - - hmac, err := g.GenerateSignature(creds.Secret, param) - if err != nil { - return err + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) } - - headers["sign"] = crypto.HexEncodeToString(hmac) - - urlPath := fmt.Sprintf("%s/%s/%s", ePoint, gateioAPIVersion, endpoint) - - var intermidiary json.RawMessage - item := &request.Item{ - Method: method, - Path: urlPath, - Headers: headers, - Result: &intermidiary, - AuthRequest: true, - Verbose: g.Verbose, - HTTPDebugging: g.HTTPDebugging, - HTTPRecording: g.HTTPRecording, - } - err = g.SendPayload(ctx, request.Unset, func() (*request.Item, error) { - item.Body = strings.NewReader(param) - return item, nil - }) - if err != nil { - return err - } - - errCap := struct { - Result bool `json:"result,string"` - Code int `json:"code"` - Message string `json:"message"` - }{} - - if err := json.Unmarshal(intermidiary, &errCap); err == nil { - if !errCap.Result { - return fmt.Errorf("%s auth request error, code: %d message: %s", - g.Name, - errCap.Code, - errCap.Message) + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + if err := common.StartEndTimeCheck(from, to); err != nil && !to.IsZero() { + return nil, err + } else if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) } } - - return json.Unmarshal(intermidiary, result) + var withdrawals []WithdrawalResponse + return withdrawals, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, + http.MethodGet, walletWithdrawals, params, nil, &withdrawals) } +// GetDepositRecords retrieves deposit records. Record time range cannot exceed 30 days +func (g *Gateio) GetDepositRecords(ctx context.Context, ccy currency.Code, from, to time.Time, offset, limit uint64) ([]DepositRecord, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("currency", ccy.String()) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + if err := common.StartEndTimeCheck(from, to); err != nil { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + } + var depositHistories []DepositRecord + return depositHistories, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, + http.MethodGet, walletDeposits, params, nil, &depositHistories) +} + +// TransferCurrency Transfer between different accounts. Currently support transfers between the following: +// spot - margin, spot - futures(perpetual), spot - delivery +// spot - cross margin, spot - options +func (g *Gateio) TransferCurrency(ctx context.Context, arg *TransferCurrencyParam) (*TransactionIDResponse, error) { + if arg == nil { + return nil, errNilArgument + } + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if !strings.EqualFold(arg.From, asset.Spot.String()) { + return nil, fmt.Errorf("%w, only %s accounts can be used to transfer from", asset.ErrNotSupported, asset.Spot) + } + if !g.isAccountAccepted(arg.To) { + return nil, fmt.Errorf("%w, only %v,%v,%v,%v,%v,and %v are supported", asset.ErrNotSupported, asset.Spot, asset.Margin, asset.Futures, asset.DeliveryFutures, asset.CrossMargin, asset.Options) + } + if arg.Amount < 0 { + return nil, errInvalidAmount + } + var response *TransactionIDResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, http.MethodPost, walletTransfer, nil, &arg, &response) +} + +func (g *Gateio) isAccountAccepted(account string) bool { + if account == "" { + return false + } + acc, err := asset.New(account) + if err != nil { + return false + } + return acc == asset.Spot || acc == asset.Margin || acc == asset.CrossMargin || acc == asset.Futures || acc == asset.DeliveryFutures || acc == asset.Options +} + +func (g *Gateio) assetTypeToString(acc asset.Item) string { + if acc == asset.Options { + return "options" + } + return acc.String() +} + +// SubAccountTransfer to transfer between main and sub accounts +// Support transferring with sub user's spot or futures account. Note that only main user's spot account is used no matter which sub user's account is operated. +func (g *Gateio) SubAccountTransfer(ctx context.Context, arg SubAccountTransferParam) error { + if arg.Currency.IsEmpty() { + return currency.ErrCurrencyCodeEmpty + } + if arg.SubAccount == "" { + return errInvalidOrEmptySubaccount + } + arg.Direction = strings.ToLower(arg.Direction) + if arg.Direction != "to" && arg.Direction != "from" { + return errInvalidTransferDirection + } + if arg.Amount <= 0 { + return errInvalidAmount + } + if arg.SubAccountType != "" && arg.SubAccountType != asset.Spot.String() && arg.SubAccountType != asset.Futures.String() && arg.SubAccountType != asset.CrossMargin.String() { + return fmt.Errorf("%v; only %v,%v, and %v are allowed", asset.ErrNotSupported, asset.Spot, asset.Futures, asset.CrossMargin) + } + return g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, http.MethodPost, walletSubAccountTransfer, nil, &arg, nil) +} + +// GetSubAccountTransferHistory retrieve transfer records between main and sub accounts. +// retrieve transfer records between main and sub accounts. Record time range cannot exceed 30 days +// Note: only records after 2020-04-10 can be retrieved +func (g *Gateio) GetSubAccountTransferHistory(ctx context.Context, subAccountUserID string, from, to time.Time, offset, limit uint64) ([]SubAccountTransferResponse, error) { + params := url.Values{} + if subAccountUserID != "" { + params.Set("sub_uid", subAccountUserID) + } + startingTime, err := time.Parse("2006-Jan-02", "2020-Apr-10") + if err != nil { + return nil, err + } + if err := common.StartEndTimeCheck(startingTime, from); err == nil { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if err := common.StartEndTimeCheck(from, to); err == nil { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var response []SubAccountTransferResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, + http.MethodGet, walletSubAccountTransfer, params, nil, &response) +} + +// SubAccountTransferToSubAccount performs sub-account transfers to sub-account +func (g *Gateio) SubAccountTransferToSubAccount(ctx context.Context, arg *InterSubAccountTransferParams) error { + if arg.Currency.IsEmpty() { + return currency.ErrCurrencyCodeEmpty + } + if arg.SubAccountFromUserID == "" { + return errors.New("sub-account from user-id is required") + } + if arg.SubAccountFromAssetType == asset.Empty { + return errors.New("sub-account to transfer the asset from is required") + } + if arg.SubAccountToUserID == "" { + return errors.New("sub-account to user-id is required") + } + if arg.SubAccountToAssetType == asset.Empty { + return errors.New("sub-account to transfer to is required") + } + if arg.Amount <= 0 { + return errInvalidAmount + } + return g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, http.MethodPost, walletInterSubAccountTransfer, nil, &arg, nil) +} + +// GetWithdrawalStatus retrieves withdrawal status +func (g *Gateio) GetWithdrawalStatus(ctx context.Context, ccy currency.Code) ([]WithdrawalStatus, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("currency", ccy.String()) + } + var response []WithdrawalStatus + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, http.MethodGet, walletWithdrawStatus, params, nil, &response) +} + +// GetSubAccountBalances retrieve sub account balances +func (g *Gateio) GetSubAccountBalances(ctx context.Context, subAccountUserID string) ([]FuturesSubAccountBalance, error) { + params := url.Values{} + if subAccountUserID != "" { + params.Set("sub_uid", subAccountUserID) + } + var response []FuturesSubAccountBalance + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, http.MethodGet, walletSubAccountBalance, params, nil, &response) +} + +// GetSubAccountMarginBalances query sub accounts' margin balances +func (g *Gateio) GetSubAccountMarginBalances(ctx context.Context, subAccountUserID string) ([]SubAccountMarginBalance, error) { + params := url.Values{} + if subAccountUserID != "" { + params.Set("sub_uid", subAccountUserID) + } + var response []SubAccountMarginBalance + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, http.MethodGet, walletSubAccountMarginBalance, params, nil, &response) +} + +// GetSubAccountFuturesBalances retrieves sub accounts' futures account balances +func (g *Gateio) GetSubAccountFuturesBalances(ctx context.Context, subAccountUserID, settle string) ([]FuturesSubAccountBalance, error) { + params := url.Values{} + if subAccountUserID != "" { + params.Set("sub_uid", subAccountUserID) + } + if settle != "" { + params.Set("settle", settle) + } + var response []FuturesSubAccountBalance + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, http.MethodGet, walletSubAccountFuturesBalance, params, nil, &response) +} + +// GetSubAccountCrossMarginBalances query subaccount's cross_margin account info +func (g *Gateio) GetSubAccountCrossMarginBalances(ctx context.Context, subAccountUserID string) ([]SubAccountCrossMarginInfo, error) { + params := url.Values{} + if subAccountUserID != "" { + params.Set("sub_uid", subAccountUserID) + } + var response []SubAccountCrossMarginInfo + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, http.MethodGet, walletSubAccountCrossMarginBalances, params, nil, &response) +} + +// GetSavedAddresses retrieves saved currency address info and related details. +func (g *Gateio) GetSavedAddresses(ctx context.Context, ccy currency.Code, chain string, limit uint64) ([]WalletSavedAddress, error) { + params := url.Values{} + if ccy.IsEmpty() { + return nil, fmt.Errorf("%w address is required", currency.ErrCurrencyPairEmpty) + } + params.Set("currency", ccy.String()) + if chain != "" { + params.Set("chain", chain) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var response []WalletSavedAddress + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, http.MethodGet, walletSavedAddress, params, nil, &response) +} + +// GetPersonalTradingFee retrieves personal trading fee +func (g *Gateio) GetPersonalTradingFee(ctx context.Context, currencyPair currency.Pair, settle string) (*PersonalTradingFee, error) { + params := url.Values{} + if currencyPair.IsPopulated() { + // specify a currency pair to retrieve precise fee rate + params.Set("currency_pair", currencyPair.String()) + } + if settle != "" { + params.Set("settle", settle) + } + var response *PersonalTradingFee + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, http.MethodGet, walletTradingFee, params, nil, &response) +} + +// GetUsersTotalBalance retrieves user's total balances +func (g *Gateio) GetUsersTotalBalance(ctx context.Context, ccy currency.Code) (*UsersAllAccountBalance, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("currency", ccy.String()) + } + var response *UsersAllAccountBalance + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletEPL, http.MethodGet, walletTotalBalance, params, nil, &response) +} + +// ********************************* Margin ******************************************* + +// GetMarginSupportedCurrencyPairs retrieves margin supported currency pairs. +func (g *Gateio) GetMarginSupportedCurrencyPairs(ctx context.Context) ([]MarginCurrencyPairInfo, error) { + var currenciePairsInfo []MarginCurrencyPairInfo + return currenciePairsInfo, g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, gateioMarginCurrencyPairs, ¤ciePairsInfo) +} + +// GetSingleMarginSupportedCurrencyPair retrieves margin supported currency pair detail given the currency pair. +func (g *Gateio) GetSingleMarginSupportedCurrencyPair(ctx context.Context, market currency.Pair) (*MarginCurrencyPairInfo, error) { + if market.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + var currencyPairInfo *MarginCurrencyPairInfo + return currencyPairInfo, g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, gateioMarginCurrencyPairs+"/"+market.String(), ¤cyPairInfo) +} + +// GetOrderbookOfLendingLoans retrieves order book of lending loans for specific currency +func (g *Gateio) GetOrderbookOfLendingLoans(ctx context.Context, ccy currency.Code) ([]OrderbookOfLendingLoan, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + var lendingLoans []OrderbookOfLendingLoan + return lendingLoans, g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, + gateioMarginFundingBook+"?currency="+ccy.String(), &lendingLoans) +} + +// GetMarginAccountList margin account list +func (g *Gateio) GetMarginAccountList(ctx context.Context, currencyPair currency.Pair) ([]MarginAccountItem, error) { + params := url.Values{} + if currencyPair.IsPopulated() { + params.Set("currency_pair", currencyPair.String()) + } + var response []MarginAccountItem + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioMarginAccount, params, nil, &response) +} + +// ListMarginAccountBalanceChangeHistory retrieves margin account balance change history +// Only transferals from and to margin account are provided for now. Time range allows 30 days at most +func (g *Gateio) ListMarginAccountBalanceChangeHistory(ctx context.Context, ccy currency.Code, currencyPair currency.Pair, from, to time.Time, page, limit uint64) ([]MarginAccountBalanceChangeInfo, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("currency", ccy.String()) + } + if currencyPair.IsPopulated() { + params.Set("currency_pair", currencyPair.String()) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() && ((!from.IsZero() && to.After(from)) || from.IsZero()) { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if page > 0 { + params.Set("page", strconv.FormatUint(page, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var response []MarginAccountBalanceChangeInfo + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioMarginAccountBook, params, nil, &response) +} + +// GetMarginFundingAccountList retrieves funding account list +func (g *Gateio) GetMarginFundingAccountList(ctx context.Context, ccy currency.Code) ([]MarginFundingAccountItem, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("currency", ccy.String()) + } + var response []MarginFundingAccountItem + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioMarginFundingAccounts, params, nil, &response) +} + +// MarginLoan represents lend or borrow request +func (g *Gateio) MarginLoan(ctx context.Context, arg *MarginLoanRequestParam) (*MarginLoanResponse, error) { + if arg == nil { + return nil, errNilArgument + } + if arg.Side != sideLend && arg.Side != sideBorrow { + return nil, errInvalidLoanSide + } + if arg.Side == sideBorrow && arg.Rate == 0 { + return nil, errors.New("`rate` is required in borrowing") + } + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if arg.Amount <= 0 { + return nil, errInvalidAmount + } + if arg.Rate != 0 && arg.Rate > 0.002 || arg.Rate < 0.0002 { + return nil, errors.New("invalid loan rate, rate must be between 0.0002 and 0.002") + } + var response *MarginLoanResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPost, gateioMarginLoans, nil, &arg, &response) +} + +// GetMarginAllLoans retrieves all loans (borrow and lending) orders. +func (g *Gateio) GetMarginAllLoans(ctx context.Context, status, side, sortBy string, ccy currency.Code, currencyPair currency.Pair, reverseSort bool, page, limit uint64) ([]MarginLoanResponse, error) { + if side != sideLend && side != sideBorrow { + return nil, fmt.Errorf("%w, only 'lend' and 'borrow' are supported", errInvalidOrderSide) + } + params := url.Values{} + params.Set("side", side) + if status == statusOpen || status == "loaned" || status == statusFinished || status == "auto_repair" { + params.Set("status", status) + } else { + return nil, errors.New("loan status \"status\" is required") + } + if !ccy.IsEmpty() { + params.Set("currency", ccy.String()) + } + if currencyPair.IsPopulated() { + params.Set("currency_pair", currencyPair.String()) + } + if sortBy == "create_time" || sortBy == "rate" { + params.Set("sort_by", sortBy) + } + if reverseSort { + params.Set("reverse_sort", strconv.FormatBool(reverseSort)) + } + if page > 0 { + params.Set("page", strconv.FormatUint(page, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var response []MarginLoanResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioMarginLoans, params, nil, &response) +} + +// MergeMultipleLendingLoans merge multiple lending loans +func (g *Gateio) MergeMultipleLendingLoans(ctx context.Context, ccy currency.Code, ids []string) (*MarginLoanResponse, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if len(ids) < 2 || len(ids) > 20 { + return nil, errors.New("number of loans to be merged must be between [2-20], inclusive") + } + params := url.Values{} + params.Set("currency", ccy.String()) + params.Set("ids", strings.Join(ids, ",")) + var response *MarginLoanResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPost, gateioMarginMergedLoans, params, nil, &response) +} + +// RetriveOneSingleLoanDetail retrieve one single loan detail +// "side" represents loan side: Lend or Borrow +func (g *Gateio) RetriveOneSingleLoanDetail(ctx context.Context, side, loanID string) (*MarginLoanResponse, error) { + if side != sideBorrow && side != sideLend { + return nil, errInvalidLoanSide + } + if loanID == "" { + return nil, errInvalidLoanID + } + params := url.Values{} + params.Set("side", side) + var response *MarginLoanResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioMarginLoans+"/"+loanID+"/", params, nil, &response) +} + +// ModifyALoan Modify a loan +// only auto_renew modification is supported currently +func (g *Gateio) ModifyALoan(ctx context.Context, loanID string, arg *ModifyLoanRequestParam) (*MarginLoanResponse, error) { + if arg == nil { + return nil, errNilArgument + } + if loanID == "" { + return nil, fmt.Errorf("%w, %v", errInvalidLoanID, " loan_id is required") + } + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if arg.Side != sideBorrow && arg.Side != sideLend { + return nil, errInvalidLoanSide + } + if arg.CurrencyPair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + var response *MarginLoanResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPatch, gateioMarginLoans+"/"+loanID, nil, &arg, &response) +} + +// CancelLendingLoan cancels lending loans. only lent loans can be canceled. +func (g *Gateio) CancelLendingLoan(ctx context.Context, ccy currency.Code, loanID string) (*MarginLoanResponse, error) { + if loanID == "" { + return nil, fmt.Errorf("%w, %s", errInvalidLoanID, " loan_id is required") + } + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + params.Set("currency", ccy.String()) + var response *MarginLoanResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodDelete, gateioMarginLoans+"/"+loanID, params, nil, &response) +} + +// RepayALoan execute a loan repay. +func (g *Gateio) RepayALoan(ctx context.Context, loanID string, arg *RepayLoanRequestParam) (*MarginLoanResponse, error) { + if arg == nil { + return nil, errNilArgument + } + if loanID == "" { + return nil, fmt.Errorf("%w, %v", errInvalidLoanID, " loan_id is required") + } + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if arg.CurrencyPair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + if arg.Mode != "all" && arg.Mode != "partial" { + return nil, errInvalidRepayMode + } + if arg.Mode == "partial" && arg.Amount <= 0 { + return nil, fmt.Errorf("%w, repay amount for partial repay mode must be greater than 0", errInvalidAmount) + } + var response *MarginLoanResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPost, gateioMarginLoans+"/"+loanID+"/repayment", nil, &arg, &response) +} + +// ListLoanRepaymentRecords retrieves loan repayment records for specified loan ID +func (g *Gateio) ListLoanRepaymentRecords(ctx context.Context, loanID string) ([]LoanRepaymentRecord, error) { + if loanID == "" { + return nil, fmt.Errorf("%w, %v", errInvalidLoanID, " loan_id is required") + } + var response []LoanRepaymentRecord + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioMarginLoans+"/"+loanID+"/repayment", nil, nil, &response) +} + +// ListRepaymentRecordsOfSpecificLoan retrieves repayment records of specific loan +func (g *Gateio) ListRepaymentRecordsOfSpecificLoan(ctx context.Context, loanID, status string, page, limit uint64) ([]LoanRecord, error) { + if loanID == "" { + return nil, fmt.Errorf("%w, %v", errInvalidLoanID, " loan_id is required") + } + params := url.Values{} + params.Set("loan_id", loanID) + if status == statusLoaned || status == statusFinished { + params.Set("status", status) + } + if page > 0 { + params.Set("page", strconv.FormatUint(page, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var response []LoanRecord + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioMarginLoanRecords, params, nil, &response) +} + +// GetOneSingleLoanRecord get one single loan record +func (g *Gateio) GetOneSingleLoanRecord(ctx context.Context, loanID, loanRecordID string) (*LoanRecord, error) { + if loanID == "" { + return nil, fmt.Errorf("%w, %v", errInvalidLoanID, " loan_id is required") + } + if loanRecordID == "" { + return nil, fmt.Errorf("%w, %v", errInvalidLoanID, " loan_record_id is required") + } + params := url.Values{} + params.Set("loan_id", loanID) + var response *LoanRecord + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioMarginLoanRecords+"/"+loanRecordID, params, nil, &response) +} + +// ModifyALoanRecord modify a loan record +// Only auto_renew modification is supported currently +func (g *Gateio) ModifyALoanRecord(ctx context.Context, loanRecordID string, arg *ModifyLoanRequestParam) (*LoanRecord, error) { + if arg == nil { + return nil, errNilArgument + } + if loanRecordID == "" { + return nil, fmt.Errorf("%w, %v", errInvalidLoanID, " loan_record_id is required") + } + if arg.LoanID == "" { + return nil, fmt.Errorf("%w, %v", errInvalidLoanID, " loan_id is required") + } + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if arg.Side != sideBorrow && arg.Side != sideLend { + return nil, errInvalidLoanSide + } + var response *LoanRecord + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPatch, gateioMarginLoanRecords+"/"+loanRecordID, nil, &arg, &response) +} + +// UpdateUsersAutoRepaymentSetting represents update user's auto repayment setting +func (g *Gateio) UpdateUsersAutoRepaymentSetting(ctx context.Context, statusOn bool) (*OnOffStatus, error) { + var statusStr string + if statusOn { + statusStr = "on" + } else { + statusStr = "off" + } + params := url.Values{} + params.Set("status", statusStr) + var response *OnOffStatus + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPost, gateioMarginAutoRepay, params, nil, &response) +} + +// GetUserAutoRepaymentSetting retrieve user auto repayment setting +func (g *Gateio) GetUserAutoRepaymentSetting(ctx context.Context) (*OnOffStatus, error) { + var response *OnOffStatus + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioMarginAutoRepay, nil, nil, &response) +} + +// GetMaxTransferableAmountForSpecificMarginCurrency get the max transferable amount for a specific margin currency. +func (g *Gateio) GetMaxTransferableAmountForSpecificMarginCurrency(ctx context.Context, ccy currency.Code, currencyPair currency.Pair) (*MaxTransferAndLoanAmount, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + if currencyPair.IsPopulated() { + params.Set("currency_pair", currencyPair.String()) + } + params.Set("currency", ccy.String()) + var response *MaxTransferAndLoanAmount + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioMarginTransfer, params, nil, &response) +} + +// GetMaxBorrowableAmountForSpecificMarginCurrency retrieves the max borrowble amount for specific currency +func (g *Gateio) GetMaxBorrowableAmountForSpecificMarginCurrency(ctx context.Context, ccy currency.Code, currencyPair currency.Pair) (*MaxTransferAndLoanAmount, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + if currencyPair.IsPopulated() { + params.Set("currency_pair", currencyPair.String()) + } + params.Set("currency", ccy.String()) + var response *MaxTransferAndLoanAmount + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioMarginBorrowable, params, nil, &response) +} + +// CurrencySupportedByCrossMargin currencies supported by cross margin. +func (g *Gateio) CurrencySupportedByCrossMargin(ctx context.Context) ([]CrossMarginCurrencies, error) { + var response []CrossMarginCurrencies + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioCrossMarginCurrencies, nil, nil, &response) +} + +// GetCrossMarginSupportedCurrencyDetail retrieve detail of one single currency supported by cross margin +func (g *Gateio) GetCrossMarginSupportedCurrencyDetail(ctx context.Context, ccy currency.Code) (*CrossMarginCurrencies, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + var response *CrossMarginCurrencies + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioCrossMarginCurrencies+"/"+ccy.String(), nil, nil, &response) +} + +// GetCrossMarginAccounts retrieve cross margin account +func (g *Gateio) GetCrossMarginAccounts(ctx context.Context) (*CrossMarginAccount, error) { + var response *CrossMarginAccount + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioCrossMarginAccounts, nil, nil, &response) +} + +// GetCrossMarginAccountChangeHistory retrieve cross margin account change history +// Record time range cannot exceed 30 days +func (g *Gateio) GetCrossMarginAccountChangeHistory(ctx context.Context, ccy currency.Code, from, to time.Time, page, limit uint64, accountChangeType string) ([]CrossMarginAccountHistoryItem, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("currency", ccy.String()) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if page > 0 { + params.Set("page", strconv.FormatUint(page, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if accountChangeType != "" { // "in", "out", "repay", "new_order", "order_fill", "referral_fee", "order_fee", "unknown" are supported + params.Set("type", accountChangeType) + } + var response []CrossMarginAccountHistoryItem + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioCrossMarginAccountBook, params, nil, &response) +} + +// CreateCrossMarginBorrowLoan create a cross margin borrow loan +// Borrow amount cannot be less than currency minimum borrow amount +func (g *Gateio) CreateCrossMarginBorrowLoan(ctx context.Context, arg CrossMarginBorrowLoanParams) (*CrossMarginLoanResponse, error) { + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if arg.Amount <= 0 { + return nil, fmt.Errorf("%w, borrow amount must be greater than 0", errInvalidAmount) + } + var response CrossMarginLoanResponse + return &response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPost, gateioCrossMarginLoans, nil, &arg, &response) +} + +// ExecuteRepayment when the liquidity of the currency is insufficient and the transaction risk is high, the currency will be disabled, +// and funds cannot be transferred.When the available balance of cross-margin is insufficient, the balance of the spot account can be used for repayment. +// Please ensure that the balance of the spot account is sufficient, and system uses cross-margin account for repayment first +func (g *Gateio) ExecuteRepayment(ctx context.Context, arg CurrencyAndAmount) ([]CrossMarginLoanResponse, error) { + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if arg.Amount <= 0 { + return nil, fmt.Errorf("%w, repay amount must be greater than 0", errInvalidAmount) + } + var response []CrossMarginLoanResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPlaceOrdersEPL, http.MethodPost, gateioCrossMarginRepayments, nil, &arg, &response) +} + +// GetCrossMarginRepayments retrieves list of cross margin repayments +func (g *Gateio) GetCrossMarginRepayments(ctx context.Context, ccy currency.Code, loanID string, limit, offset uint64, reverse bool) ([]CrossMarginLoanResponse, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("currency", ccy.String()) + } + if loanID != "" { + params.Set("loanId", loanID) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if reverse { + params.Set("reverse", "true") + } + var response []CrossMarginLoanResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioCrossMarginRepayments, params, nil, &response) +} + +// GetMaxTransferableAmountForSpecificCrossMarginCurrency get the max transferable amount for a specific cross margin currency +func (g *Gateio) GetMaxTransferableAmountForSpecificCrossMarginCurrency(ctx context.Context, ccy currency.Code) (*CurrencyAndAmount, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + var response *CurrencyAndAmount + params.Set("currency", ccy.String()) + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioCrossMarginTransferable, params, nil, &response) +} + +// GetMaxBorrowableAmountForSpecificCrossMarginCurrency returns the max borrowable amount for a specific cross margin currency +func (g *Gateio) GetMaxBorrowableAmountForSpecificCrossMarginCurrency(ctx context.Context, ccy currency.Code) (*CurrencyAndAmount, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + params := url.Values{} + params.Set("currency", ccy.String()) + var response *CurrencyAndAmount + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioCrossMarginBorrowable, params, nil, &response) +} + +// GetCrossMarginBorrowHistory retrieves cross margin borrow history sorted by creation time in descending order by default. +// Set reverse=false to return ascending results. +func (g *Gateio) GetCrossMarginBorrowHistory(ctx context.Context, status uint64, ccy currency.Code, limit, offset uint64, reverse bool) ([]CrossMarginLoanResponse, error) { + if status < 1 || status > 3 { + return nil, fmt.Errorf("%s %v, only allowed status values are 1:failed, 2:borrowed, and 3:repayment", g.Name, errInvalidOrderStatus) + } + params := url.Values{} + params.Set("status", strconv.FormatUint(status, 10)) + if !ccy.IsEmpty() { + params.Set("currency", ccy.String()) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if reverse { + params.Set("reverse", strconv.FormatBool(reverse)) + } + var response []CrossMarginLoanResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioCrossMarginLoans, params, nil, &response) +} + +// GetSingleBorrowLoanDetail retrieve single borrow loan detail +func (g *Gateio) GetSingleBorrowLoanDetail(ctx context.Context, loanID string) (*CrossMarginLoanResponse, error) { + if loanID == "" { + return nil, errInvalidLoanID + } + var response *CrossMarginLoanResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioCrossMarginLoans+"/"+loanID, nil, nil, &response) +} + +// *********************************Futures*************************************** + +// GetAllFutureContracts retrieves list all futures contracts +func (g *Gateio) GetAllFutureContracts(ctx context.Context, settle string) ([]FuturesContract, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + var contracts []FuturesContract + return contracts, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, "futures/"+settle+"/contracts", &contracts) +} + +// GetSingleContract returns a single contract info for the specified settle and Currency Pair (contract << in this case) +func (g *Gateio) GetSingleContract(ctx context.Context, settle, contract string) (*FuturesContract, error) { + if contract == "" { + return nil, currency.ErrCurrencyPairEmpty + } + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + var futureContract *FuturesContract + return futureContract, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, "futures/"+settle+"/contracts/"+contract, &futureContract) +} + +// GetFuturesOrderbook retrieves futures order book data +func (g *Gateio) GetFuturesOrderbook(ctx context.Context, settle, contract, interval string, limit uint64, withOrderbookID bool) (*Orderbook, error) { + if contract == "" { + return nil, currency.ErrCurrencyPairEmpty + } + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + params.Set("contract", contract) + if interval != "" { + params.Set("interval", interval) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if withOrderbookID { + params.Set("with_id", "true") + } + var response *Orderbook + return response, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, common.EncodeURLValues("futures/"+settle+"/order_book", params), &response) +} + +// GetFuturesTradingHistory retrieves futures trading history +func (g *Gateio) GetFuturesTradingHistory(ctx context.Context, settle string, contract currency.Pair, limit, offset uint64, lastID string, from, to time.Time) ([]TradingHistoryItem, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("contract", contract.Upper().String()) + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if lastID != "" { + params.Set("last_id", lastID) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + var response []TradingHistoryItem + return response, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, + common.EncodeURLValues("futures/"+settle+"/trades", params), &response) +} + +// GetFuturesCandlesticks retrieves specified contract candlesticks. +func (g *Gateio) GetFuturesCandlesticks(ctx context.Context, settle, contract string, from, to time.Time, limit uint64, interval kline.Interval) ([]FuturesCandlestick, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract == "" { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("contract", strings.ToUpper(contract)) + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if interval.Duration().Microseconds() != 0 { + intervalString, err := g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params.Set("interval", intervalString) + } + var candlesticks []FuturesCandlestick + return candlesticks, g.SendHTTPRequest(ctx, exchange.RestFutures, perpetualSwapDefaultEPL, + common.EncodeURLValues("futures/"+settle+"/candlesticks", params), + &candlesticks) +} + +// PremiumIndexKLine retrieves premium Index K-Line +// Maximum of 1000 points can be returned in a query. Be sure not to exceed the limit when specifying from, to and interval +func (g *Gateio) PremiumIndexKLine(ctx context.Context, settleCurrency string, contract currency.Pair, from, to time.Time, limit int64, interval kline.Interval) ([]FuturesPremiumIndexKLineResponse, error) { + if settleCurrency == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("contract", contract.String()) + if from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + intervalString, err := g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params.Set("interval", intervalString) + var resp []FuturesPremiumIndexKLineResponse + return resp, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, common.EncodeURLValues("futures/"+settleCurrency+"/premium_index", params), &resp) +} + +// GetFuturesTickers retrieves futures ticker information for a specific settle and contract info. +func (g *Gateio) GetFuturesTickers(ctx context.Context, settle string, contract currency.Pair) ([]FuturesTicker, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + var tickers []FuturesTicker + return tickers, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, common.EncodeURLValues("futures/"+settle+"/tickers", params), &tickers) +} + +// GetFutureFundingRates retrieves funding rate information. +func (g *Gateio) GetFutureFundingRates(ctx context.Context, settle string, contract currency.Pair, limit uint64) ([]FuturesFundingRate, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("contract", contract.String()) + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var rates []FuturesFundingRate + return rates, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, common.EncodeURLValues("futures/"+settle+"/funding_rate", params), &rates) +} + +// GetFuturesInsuranceBalanceHistory retrieves futures insurance balance history +func (g *Gateio) GetFuturesInsuranceBalanceHistory(ctx context.Context, settle string, limit uint64) ([]InsuranceBalance, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var balances []InsuranceBalance + return balances, g.SendHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapDefaultEPL, + common.EncodeURLValues("futures/"+settle+"/insurance", params), + &balances) +} + +// GetFutureStats retrieves futures stats +func (g *Gateio) GetFutureStats(ctx context.Context, settle string, contract currency.Pair, from time.Time, interval kline.Interval, limit uint64) ([]ContractStat, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("contract", contract.String()) + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if int64(interval) != 0 { + intervalString, err := g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params.Set("interval", intervalString) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var stats []ContractStat + return stats, g.SendHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapDefaultEPL, + common.EncodeURLValues("futures/"+settle+"/contract_stats", params), + &stats) +} + +// GetIndexConstituent retrieves index constituents +func (g *Gateio) GetIndexConstituent(ctx context.Context, settle, index string) (*IndexConstituent, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if index == "" { + return nil, currency.ErrCurrencyPairEmpty + } + indexString := strings.ToUpper(index) + var constituents *IndexConstituent + return constituents, g.SendHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapDefaultEPL, + "futures/"+settle+"/index_constituents/"+indexString, + &constituents) +} + +// GetLiquidationHistory retrieves liqudiation history +func (g *Gateio) GetLiquidationHistory(ctx context.Context, settle string, contract currency.Pair, from, to time.Time, limit uint64) ([]LiquidationHistory, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, errInvalidOrMissingContractParam + } + params := url.Values{} + params.Set("contract", contract.String()) + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var histories []LiquidationHistory + return histories, g.SendHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapDefaultEPL, + common.EncodeURLValues("futures/"+settle+"/liq_orders", params), + &histories) +} + +// QueryFuturesAccount retrieves futures account +func (g *Gateio) QueryFuturesAccount(ctx context.Context, settle string) (*FuturesAccount, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + var response *FuturesAccount + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, "futures/"+settle+"/accounts", nil, nil, &response) +} + +// GetFuturesAccountBooks retrieves account books +func (g *Gateio) GetFuturesAccountBooks(ctx context.Context, settle string, limit uint64, from, to time.Time, changingType string) ([]AccountBookItem, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if changingType != "" { + params.Set("type", changingType) + } + var response []AccountBookItem + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, + http.MethodGet, "futures/"+settle+"/account_book", + params, + nil, + &response) +} + +// GetAllFuturesPositionsOfUsers list all positions of users. +func (g *Gateio) GetAllFuturesPositionsOfUsers(ctx context.Context, settle string) (*Position, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + var response *Position + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, "futures/"+settle+"/positions", nil, nil, &response) +} + +// GetSinglePosition returns a single position +func (g *Gateio) GetSinglePosition(ctx context.Context, settle string, contract currency.Pair) (*Position, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + var response *Position + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, + http.MethodPost, "futures/"+settle+"/positions/"+contract.String(), + nil, nil, &response) +} + +// UpdateFuturesPositionMargin represents account position margin for a futures contract. +func (g *Gateio) UpdateFuturesPositionMargin(ctx context.Context, settle string, change float64, contract currency.Pair) (*Position, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + if change <= 0 { + return nil, fmt.Errorf("%w, futures margin change must be positive", errChangeHasToBePositive) + } + params := url.Values{} + params.Set("change", strconv.FormatFloat(change, 'f', -1, 64)) + var response *Position + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapPlaceOrdersEPL, + http.MethodPost, "futures/"+settle+"/positions/"+contract.String()+"/margin", + params, nil, &response) +} + +// UpdateFuturesPositionLeverage update position leverage +func (g *Gateio) UpdateFuturesPositionLeverage(ctx context.Context, settle string, contract currency.Pair, leverage, crossLeverageLimit float64) (*Position, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + if leverage < 0 { + return nil, errInvalidLeverageValue + } + params := url.Values{} + params.Set("leverage", strconv.FormatFloat(leverage, 'f', -1, 64)) + if leverage == 0 && crossLeverageLimit > 0 { + params.Set("cross_leverage_limit", strconv.FormatFloat(crossLeverageLimit, 'f', -1, 64)) + } + var response *Position + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPost, + "futures/"+settle+"/positions/"+contract.String()+"/leverage", params, nil, &response) +} + +// UpdateFuturesPositionRiskLimit updates the position risk limit +func (g *Gateio) UpdateFuturesPositionRiskLimit(ctx context.Context, settle string, contract currency.Pair, riskLimit uint64) (*Position, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + params := url.Values{} + params.Set("risk_limit", strconv.FormatUint(riskLimit, 10)) + var response *Position + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPlaceOrdersEPL, + http.MethodPost, "futures/"+settle+"/positions/"+contract.String()+"/risk_limit", params, nil, &response) +} + +// EnableOrDisableDualMode enable or disable dual mode +// Before setting dual mode, make sure all positions are closed and no orders are open +func (g *Gateio) EnableOrDisableDualMode(ctx context.Context, settle string, dualMode bool) (*DualModeResponse, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + params.Set("dual_mode", strconv.FormatBool(dualMode)) + var response *DualModeResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, + http.MethodGet, "futures/"+settle+"/dual_mode", + params, nil, &response) +} + +// RetrivePositionDetailInDualMode retrieve position detail in dual mode +func (g *Gateio) RetrivePositionDetailInDualMode(ctx context.Context, settle string, contract currency.Pair) ([]Position, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + var response []Position + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "futures/"+settle+"/dual_comp/positions/"+contract.String(), + nil, nil, &response) +} + +// UpdatePositionMarginInDualMode update position margin in dual mode +func (g *Gateio) UpdatePositionMarginInDualMode(ctx context.Context, settle string, contract currency.Pair, change float64, dualSide string) ([]Position, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + params := url.Values{} + params.Set("change", strconv.FormatFloat(change, 'f', -1, 64)) + if dualSide != "dual_long" && dualSide != "dual_short" { + return nil, fmt.Errorf("invalid 'dual_side' should be 'dual_short' or 'dual_long'") + } + params.Set("dual_side", dualSide) + var response []Position + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPlaceOrdersEPL, + http.MethodPost, + "futures/"+settle+"/dual_comp/positions/"+contract.String()+"/margin", + params, nil, &response) +} + +// UpdatePositionLeverageInDualMode update position leverage in dual mode +func (g *Gateio) UpdatePositionLeverageInDualMode(ctx context.Context, settle string, contract currency.Pair, leverage, crossLeverageLimit float64) (*Position, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + if leverage < 0 { + return nil, errInvalidLeverageValue + } + params := url.Values{} + params.Set("leverage", strconv.FormatFloat(leverage, 'f', -1, 64)) + if leverage == 0 && crossLeverageLimit > 0 { + params.Set("cross_leverage_limit", strconv.FormatFloat(crossLeverageLimit, 'f', -1, 64)) + } + var response *Position + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPost, "futures/"+settle+"/dual_comp/positions/"+contract.String()+"/leverage", params, nil, &response) +} + +// UpdatePositionRiskLimitInDualMode update position risk limit in dual mode +func (g *Gateio) UpdatePositionRiskLimitInDualMode(ctx context.Context, settle string, contract currency.Pair, riskLimit float64) ([]Position, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + if riskLimit < 0 { + return nil, errInvalidRiskLimit + } + params := url.Values{} + params.Set("risk_limit", strconv.FormatFloat(riskLimit, 'f', -1, 64)) + var response []Position + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPost, + "futures/"+settle+"/dual_comp/positions/"+contract.String()+"/risk_limit", params, + nil, &response) +} + +// PlaceFuturesOrder creates futures order +// Create a futures order +// Creating futures orders requires size, which is number of contracts instead of currency amount. You can use quanto_multiplier in contract detail response to know how much currency 1 size contract represents +// Zero-filled order cannot be retrieved 10 minutes after order cancellation. You will get a 404 not found for such orders +// Set reduce_only to true can keep the position from changing side when reducing position size +// In single position mode, to close a position, you need to set size to 0 and close to true +// In dual position mode, to close one side position, you need to set auto_size side, reduce_only to true and size to 0 +func (g *Gateio) PlaceFuturesOrder(ctx context.Context, arg *OrderCreateParams) (*Order, error) { + if arg == nil { + return nil, errNilArgument + } + if arg.Contract.IsEmpty() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + if arg.Size == 0 { + return nil, fmt.Errorf("%w, specify positive number to make a bid, and negative number to ask", errInvalidOrderSide) + } + if arg.TimeInForce != gtcTIF && arg.TimeInForce != iocTIF && arg.TimeInForce != pocTIF && arg.TimeInForce != focTIF { + return nil, errInvalidTimeInForce + } + if arg.Price < 0 { + return nil, errInvalidPrice + } + if arg.Text != "" { + arg.Text = "t-" + arg.Text + } else { + randomString, err := common.GenerateRandomString(10, common.NumberCharacters) + if err != nil { + return nil, err + } + arg.Text = "t-" + randomString + } + if arg.AutoSize != "" && (arg.AutoSize == "close_long" || arg.AutoSize == "close_short") { + return nil, errInvalidAutoSizeValue + } + arg.Settle = strings.ToLower(arg.Settle) + if arg.Settle == "" { + return nil, errEmptySettlementCurrency + } + var response *Order + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapPlaceOrdersEPL, + http.MethodPost, + "futures/"+arg.Settle+"/orders", + nil, &arg, &response) +} + +// GetFuturesOrders retrieves list of futures orders +// Zero-filled order cannot be retrieved 10 minutes after order cancellation +func (g *Gateio) GetFuturesOrders(ctx context.Context, contract currency.Pair, status, lastID, settle string, limit, offset uint64, countTotal int64) ([]Order, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + params := url.Values{} + params.Set("contract", contract.String()) + if status != statusOpen && status != statusFinished { + return nil, fmt.Errorf("%w, only 'open' and 'finished' status are supported", errInvalidOrderStatus) + } + params.Set("status", status) + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if lastID != "" { + params.Set("last_id", lastID) + } + if countTotal == 1 && status != statusOpen { + params.Set("count_total", strconv.FormatInt(countTotal, 10)) + } else if countTotal != 0 && countTotal != 1 { + return nil, errInvalidCountTotalValue + } + var response []Order + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, + http.MethodGet, "futures/"+settle+"/orders", + params, nil, &response) +} + +// CancelMultipleFuturesOpenOrders ancel all open orders +// Zero-filled order cannot be retrieved 10 minutes after order cancellation +func (g *Gateio) CancelMultipleFuturesOpenOrders(ctx context.Context, contract currency.Pair, side, settle string) ([]Order, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + params := url.Values{} + if side != "" { + params.Set("side", side) + } + params.Set("contract", contract.String()) + var response []Order + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapCancelOrdersEPL, + http.MethodDelete, "futures/"+settle+"/orders", params, nil, &response) +} + +// PlaceBatchFuturesOrders creates a list of futures orders +// Up to 10 orders per request +// If any of the order's parameters are missing or in the wrong format, all of them will not be executed, and a http status 400 error will be returned directly +// If the parameters are checked and passed, all are executed. Even if there is a business logic error in the middle (such as insufficient funds), it will not affect other execution orders +// The returned result is in array format, and the order corresponds to the orders in the request body +// In the returned result, the succeeded field of type bool indicates whether the execution was successful or not +// If the execution is successful, the normal order content is included; if the execution fails, the label field is included to indicate the cause of the error +// In the rate limiting, each order is counted individually +func (g *Gateio) PlaceBatchFuturesOrders(ctx context.Context, settle string, args []OrderCreateParams) ([]Order, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if len(args) > 10 { + return nil, errTooManyOrderRequest + } + for x := range args { + if args[x].Size == 0 { + return nil, fmt.Errorf("%w, specify positive number to make a bid, and negative number to ask", errInvalidOrderSide) + } + if args[x].TimeInForce != gtcTIF && + args[x].TimeInForce != iocTIF && + args[x].TimeInForce != pocTIF && + args[x].TimeInForce != focTIF { + return nil, errInvalidTimeInForce + } + if args[x].Price > 0 && args[x].TimeInForce == iocTIF { + args[x].Price = 0 + } + if args[x].Price < 0 { + return nil, errInvalidPrice + } + if args[x].Text != "" && !strings.HasPrefix(args[x].Text, "t-") { + args[x].Text = "t-" + args[x].Text + } + if args[x].AutoSize != "" && (args[x].AutoSize == "close_long" || args[x].AutoSize == "close_short") { + return nil, errInvalidAutoSizeValue + } + if args[x].Settle != settleBTC && args[x].Settle != settleUSD && args[x].Settle != settleUSDT { + return nil, errEmptySettlementCurrency + } + } + var response []Order + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, + http.MethodPost, "futures/"+settle+"/batch_orders", + nil, &args, &response) +} + +// GetSingleFuturesOrder retrieves a single order by its identifier +func (g *Gateio) GetSingleFuturesOrder(ctx context.Context, settle, orderID string) (*Order, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if orderID == "" { + return nil, fmt.Errorf("%w, 'order_id' cannot be empty", errInvalidOrderID) + } + var response *Order + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapPrivateEPL, + http.MethodGet, "futures/"+settle+"/orders/"+orderID, + nil, nil, &response) +} + +// CancelSingleFuturesOrder cancel a single order +func (g *Gateio) CancelSingleFuturesOrder(ctx context.Context, settle, orderID string) (*Order, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if orderID == "" { + return nil, fmt.Errorf("%w, 'order_id' cannot be empty", errInvalidOrderID) + } + var response *Order + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapCancelOrdersEPL, http.MethodDelete, + "futures/"+settle+"/orders/"+orderID, nil, nil, &response) +} + +// AmendFuturesOrder amends an existing futures order +func (g *Gateio) AmendFuturesOrder(ctx context.Context, settle, orderID string, arg AmendFuturesOrderParam) (*Order, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if orderID == "" { + return nil, fmt.Errorf("%w, 'order_id' cannot be empty", errInvalidOrderID) + } + if arg.Size <= 0 && arg.Price <= 0 { + return nil, errors.New("missing update 'size' or 'price', please specify 'size' or 'price' or both information") + } + var response *Order + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPut, + "futures/"+settle+"/orders/"+orderID, nil, &arg, &response) +} + +// GetMyPersonalTradingHistory retrieves my personal trading history +func (g *Gateio) GetMyPersonalTradingHistory(ctx context.Context, settle, lastID, orderID string, contract currency.Pair, limit, offset, countTotal uint64) ([]TradingHistoryItem, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if orderID != "" { + params.Set("order", orderID) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if lastID != "" { + params.Set("last_id", lastID) + } + if countTotal == 1 { + params.Set("count_total", strconv.FormatUint(countTotal, 10)) + } + var response []TradingHistoryItem + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "futures/"+settle+"/my_trades", params, nil, &response) +} + +// GetFuturesPositionCloseHistory lists position close history +func (g *Gateio) GetFuturesPositionCloseHistory(ctx context.Context, settle string, contract currency.Pair, limit, offset uint64, from, to time.Time) ([]PositionCloseHistoryResponse, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + var response []PositionCloseHistoryResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "futures/"+settle+"/position_close", params, nil, &response) +} + +// GetFuturesLiquidationHistory list liquidation history +func (g *Gateio) GetFuturesLiquidationHistory(ctx context.Context, settle string, contract currency.Pair, limit uint64, at time.Time) ([]LiquidationHistoryItem, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if !at.IsZero() { + params.Set("at", strconv.FormatInt(at.Unix(), 10)) + } + var response []LiquidationHistoryItem + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "futures/"+settle+"/liquidates", params, nil, &response) +} + +// CountdownCancelOrders represents a trigger time response +func (g *Gateio) CountdownCancelOrders(ctx context.Context, settle string, arg CountdownParams) (*TriggerTimeResponse, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if arg.Timeout < 0 { + return nil, errInvalidTimeout + } + var response *TriggerTimeResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPost, + "futures/"+settle+"/countdown_cancel_all", nil, &arg, &response) +} + +// CreatePriceTriggeredFuturesOrder create a price-triggered order +func (g *Gateio) CreatePriceTriggeredFuturesOrder(ctx context.Context, settle string, arg *FuturesPriceTriggeredOrderParam) (*OrderID, error) { + if arg == nil { + return nil, errNilArgument + } + if settle == "" { + return nil, errEmptySettlementCurrency + } + if arg.Initial.Contract.IsEmpty() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + if arg.Initial.Price < 0 { + return nil, fmt.Errorf("%w, price must be greater than 0", errInvalidPrice) + } + if arg.Initial.TimeInForce != "" && arg.Initial.TimeInForce != gtcTIF && arg.Initial.TimeInForce != iocTIF { + return nil, fmt.Errorf("%w, only time in force value 'gtc' and 'ioc' are supported", errInvalidTimeInForce) + } + if arg.Trigger.StrategyType != 0 && arg.Trigger.StrategyType != 1 { + return nil, errors.New("strategy type must be 0 or 1, 0: by price, and 1: by price gap") + } + if arg.Trigger.Rule != 1 && arg.Trigger.Rule != 2 { + return nil, errors.New("invalid trigger condition('rule') value, rule must be 1 or 2") + } + if arg.Trigger.PriceType != 0 && arg.Trigger.PriceType != 1 && arg.Trigger.PriceType != 2 { + return nil, errors.New("price type must be 0, 1 or 2") + } + if arg.Trigger.OrderType != "" && + arg.Trigger.OrderType != "close-long-order" && + arg.Trigger.OrderType != "close-short-order" && + arg.Trigger.OrderType != "close-long-position" && + arg.Trigger.OrderType != "close-short-position" && + arg.Trigger.OrderType != "plan-close-long-position" && + arg.Trigger.OrderType != "plan-close-short-position" { + return nil, errors.New("invalid order type, only 'close-long-order', 'close-short-order', 'close-long-position', 'close-short-position', 'plan-close-long-position', and 'plan-close-short-position'") + } + var response *OrderID + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPost, + "futures/"+settle+"/price_orders", nil, &arg, &response) +} + +// ListAllFuturesAutoOrders lists all open orders +func (g *Gateio) ListAllFuturesAutoOrders(ctx context.Context, status, settle string, contract currency.Pair, limit, offset uint64) ([]PriceTriggeredOrder, error) { + if status != statusOpen && status != statusFinished { + return nil, fmt.Errorf("%w status: %s", errInvalidOrderStatus, status) + } + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + params.Set("status", status) + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + var response []PriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, + http.MethodGet, + "futures/"+settle+"/price_orders", + params, nil, &response) +} + +// CancelAllFuturesOpenOrders cancels all futures open orders +func (g *Gateio) CancelAllFuturesOpenOrders(ctx context.Context, settle string, contract currency.Pair) ([]PriceTriggeredOrder, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + params := url.Values{} + params.Set("contract", contract.String()) + var response []PriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapCancelOrdersEPL, http.MethodDelete, + "futures/"+settle+"/price_orders", params, nil, &response) +} + +// GetSingleFuturesPriceTriggeredOrder retrieves a single price triggered order +func (g *Gateio) GetSingleFuturesPriceTriggeredOrder(ctx context.Context, settle, orderID string) (*PriceTriggeredOrder, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if orderID == "" { + return nil, errInvalidOrderID + } + var response *PriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, + http.MethodGet, + "futures/"+settle+"/price_orders/"+orderID, nil, nil, &response) +} + +// CancelFuturesPriceTriggeredOrder cancel a price-triggered order +func (g *Gateio) CancelFuturesPriceTriggeredOrder(ctx context.Context, settle, orderID string) (*PriceTriggeredOrder, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if orderID == "" { + return nil, errInvalidOrderID + } + var response *PriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapCancelOrdersEPL, http.MethodDelete, + "futures/"+settle+"/price_orders/"+orderID, nil, nil, &response) +} + +// *************************************** Delivery *************************************** + +// GetAllDeliveryContracts retrieves all futures contracts +func (g *Gateio) GetAllDeliveryContracts(ctx context.Context, settle string) ([]DeliveryContract, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + var contracts []DeliveryContract + return contracts, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, + "delivery/"+settle+"/contracts", &contracts) +} + +// GetSingleDeliveryContracts retrieves a single delivery contract instance. +func (g *Gateio) GetSingleDeliveryContracts(ctx context.Context, settle string, contract currency.Pair) (*DeliveryContract, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + var deliveryContract *DeliveryContract + return deliveryContract, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, + "delivery/"+settle+"/contracts/"+contract.String(), &deliveryContract) +} + +// GetDeliveryOrderbook delivery orderbook +func (g *Gateio) GetDeliveryOrderbook(ctx context.Context, settle, interval string, contract currency.Pair, limit uint64, withOrderbookID bool) (*Orderbook, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, errInvalidOrMissingContractParam + } + params := url.Values{} + params.Set("contract", contract.String()) + if interval != "" { + params.Set("interval", interval) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if withOrderbookID { + params.Set("with_id", strconv.FormatBool(withOrderbookID)) + } + var orderbook *Orderbook + return orderbook, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, common.EncodeURLValues("delivery/"+settle+"/order_book", params), &orderbook) +} + +// GetDeliveryTradingHistory retrieves futures trading history +func (g *Gateio) GetDeliveryTradingHistory(ctx context.Context, settle, lastID string, contract currency.Pair, limit uint64, from, to time.Time) ([]DeliveryTradingHistory, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, errInvalidOrMissingContractParam + } + params := url.Values{} + params.Set("contract", contract.String()) + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if lastID != "" { + params.Set("last_id", lastID) + } + var histories []DeliveryTradingHistory + return histories, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, + common.EncodeURLValues("delivery/"+settle+"/trades", params), &histories) +} + +// GetDeliveryFuturesCandlesticks retrieves specified contract candlesticks +func (g *Gateio) GetDeliveryFuturesCandlesticks(ctx context.Context, settle string, contract currency.Pair, from, to time.Time, limit uint64, interval kline.Interval) ([]FuturesCandlestick, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, errInvalidOrMissingContractParam + } + params := url.Values{} + params.Set("contract", contract.Upper().String()) + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if int64(interval) != 0 { + intervalString, err := g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params.Set("interval", intervalString) + } + var candlesticks []FuturesCandlestick + return candlesticks, g.SendHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapDefaultEPL, + common.EncodeURLValues("delivery/"+settle+"/candlesticks", params), + &candlesticks) +} + +// GetDeliveryFutureTickers retrieves futures ticker information for a specific settle and contract info. +func (g *Gateio) GetDeliveryFutureTickers(ctx context.Context, settle string, contract currency.Pair) ([]FuturesTicker, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + var tickers []FuturesTicker + return tickers, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, common.EncodeURLValues("delivery/"+settle+"/tickers", params), &tickers) +} + +// GetDeliveryInsuranceBalanceHistory retrieves delivery futures insurance balance history +func (g *Gateio) GetDeliveryInsuranceBalanceHistory(ctx context.Context, settle string, limit uint64) ([]InsuranceBalance, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var balances []InsuranceBalance + return balances, g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, + common.EncodeURLValues("delivery/"+settle+"/insurance", params), + &balances) +} + +// GetDeliveryFuturesAccounts retrieves futures account +func (g *Gateio) GetDeliveryFuturesAccounts(ctx context.Context, settle string) (*FuturesAccount, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + var response *FuturesAccount + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, "delivery/"+settle+"/accounts", nil, nil, &response) +} + +// GetDeliveryAccountBooks retrieves account books +func (g *Gateio) GetDeliveryAccountBooks(ctx context.Context, settle string, limit uint64, from, to time.Time, changingType string) ([]AccountBookItem, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if changingType != "" { + params.Set("type", changingType) + } + var response []AccountBookItem + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "delivery/"+settle+"/account_book", + params, nil, &response) +} + +// GetAllDeliveryPositionsOfUser retrieves all positions of user +func (g *Gateio) GetAllDeliveryPositionsOfUser(ctx context.Context, settle string) (*Position, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + var response *Position + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "delivery/"+settle+"/positions", nil, nil, &response) +} + +// GetSingleDeliveryPosition get single position +func (g *Gateio) GetSingleDeliveryPosition(ctx context.Context, settle string, contract currency.Pair) (*Position, error) { + settle = strings.ToLower(settle) + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + var response *Position + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "delivery/"+settle+"/positions/"+contract.String(), + nil, nil, &response) +} + +// UpdateDeliveryPositionMargin updates position margin +func (g *Gateio) UpdateDeliveryPositionMargin(ctx context.Context, settle string, change float64, contract currency.Pair) (*Position, error) { + if settle != settleBTC && settle != settleUSDT { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + if change <= 0 { + return nil, fmt.Errorf("%w, futures margin change must be positive", errChangeHasToBePositive) + } + params := url.Values{} + params.Set("change", strconv.FormatFloat(change, 'f', -1, 64)) + var response *Position + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPost, + "delivery/"+settle+"/positions/"+contract.String()+"/margin", params, nil, &response) +} + +// UpdateDeliveryPositionLeverage updates position leverage +func (g *Gateio) UpdateDeliveryPositionLeverage(ctx context.Context, settle string, contract currency.Pair, leverage float64) (*Position, error) { + if settle != settleBTC && settle != settleUSDT { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + if leverage < 0 { + return nil, errInvalidLeverageValue + } + params := url.Values{} + params.Set("leverage", strconv.FormatFloat(leverage, 'f', -1, 64)) + var response *Position + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPost, + "delivery/"+settle+"/positions/"+contract.String()+"/leverage", + params, nil, &response) +} + +// UpdateDeliveryPositionRiskLimit update position risk limit +func (g *Gateio) UpdateDeliveryPositionRiskLimit(ctx context.Context, settle string, contract currency.Pair, riskLimit uint64) (*Position, error) { + if settle != settleBTC && settle != settleUSDT { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + params := url.Values{} + params.Set("risk_limit", strconv.FormatUint(riskLimit, 10)) + var response *Position + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPost, + "delivery/"+settle+"/positions/"+contract.String()+"/risk_limit", params, nil, &response) +} + +// PlaceDeliveryOrder create a futures order +// Zero-filled order cannot be retrieved 10 minutes after order cancellation +func (g *Gateio) PlaceDeliveryOrder(ctx context.Context, arg *OrderCreateParams) (*Order, error) { + if arg == nil { + return nil, errNilArgument + } + if arg.Contract.IsEmpty() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + if arg.Size == 0 { + return nil, fmt.Errorf("%w, specify positive number to make a bid, and negative number to ask", errInvalidOrderSide) + } + if arg.TimeInForce != gtcTIF && arg.TimeInForce != iocTIF && arg.TimeInForce != pocTIF && arg.TimeInForce != focTIF { + return nil, errInvalidTimeInForce + } + if arg.Price < 0 { + return nil, errInvalidPrice + } + if arg.Text != "" { + arg.Text = "t-" + arg.Text + } else { + randomString, err := common.GenerateRandomString(10, common.NumberCharacters) + if err != nil { + return nil, err + } + arg.Text = "t-" + randomString + } + if arg.AutoSize != "" && (arg.AutoSize == "close_long" || arg.AutoSize == "close_short") { + return nil, errInvalidAutoSizeValue + } + arg.Settle = strings.ToLower(arg.Settle) + if arg.Settle == "" { + return nil, errEmptySettlementCurrency + } + var response *Order + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPost, + "delivery/"+arg.Settle+"/orders", nil, &arg, &response) +} + +// GetDeliveryOrders list futures orders +// Zero-filled order cannot be retrieved 10 minutes after order cancellation +func (g *Gateio) GetDeliveryOrders(ctx context.Context, contract currency.Pair, status, settle, lastID string, limit, offset uint64, countTotal int64) ([]Order, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + params := url.Values{} + params.Set("contract", contract.String()) + if status != statusOpen && status != statusFinished { + return nil, fmt.Errorf("%w, only 'open' and 'finished' status are supported", errInvalidOrderStatus) + } + params.Set("status", status) + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if lastID != "" { + params.Set("last_id", lastID) + } + if countTotal == 1 && status != statusOpen { + params.Set("count_total", strconv.FormatInt(countTotal, 10)) + } else if countTotal != 0 && countTotal != 1 { + return nil, errInvalidCountTotalValue + } + var response []Order + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "delivery/"+settle+"/orders", params, nil, &response) +} + +// CancelMultipleDeliveryOrders cancel all open orders matched +// Zero-filled order cannot be retrieved 10 minutes after order cancellation +func (g *Gateio) CancelMultipleDeliveryOrders(ctx context.Context, contract currency.Pair, side, settle string) ([]Order, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + params := url.Values{} + if side == "ask" || side == "bid" { + params.Set("side", side) + } + params.Set("contract", contract.String()) + var response []Order + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapCancelOrdersEPL, http.MethodDelete, + "delivery/"+settle+"/orders", params, nil, &response) +} + +// GetSingleDeliveryOrder Get a single order +// Zero-filled order cannot be retrieved 10 minutes after order cancellation +func (g *Gateio) GetSingleDeliveryOrder(ctx context.Context, settle, orderID string) (*Order, error) { + if settle != settleBTC && settle != settleUSDT { + return nil, errEmptySettlementCurrency + } + if orderID == "" { + return nil, fmt.Errorf("%w, 'order_id' cannot be empty", errInvalidOrderID) + } + var response *Order + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "delivery/"+settle+"/orders/"+orderID, nil, nil, &response) +} + +// CancelSingleDeliveryOrder cancel a single order +func (g *Gateio) CancelSingleDeliveryOrder(ctx context.Context, settle, orderID string) (*Order, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if orderID == "" { + return nil, fmt.Errorf("%w, 'order_id' cannot be empty", errInvalidOrderID) + } + var response *Order + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapCancelOrdersEPL, http.MethodDelete, + "delivery/"+settle+"/orders/"+orderID, nil, nil, &response) +} + +// GetDeliveryPersonalTradingHistory retrieves personal trading history +func (g *Gateio) GetDeliveryPersonalTradingHistory(ctx context.Context, settle, orderID string, contract currency.Pair, limit, offset, countTotal uint64, lastID string) ([]TradingHistoryItem, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if orderID != "" { + params.Set("order", orderID) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if lastID != "" { + params.Set("last_id", lastID) + } + if countTotal == 1 { + params.Set("count_total", strconv.FormatUint(countTotal, 10)) + } + var response []TradingHistoryItem + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "delivery/"+settle+"/my_trades", params, nil, &response) +} + +// GetDeliveryPositionCloseHistory retrieves position history +func (g *Gateio) GetDeliveryPositionCloseHistory(ctx context.Context, settle string, contract currency.Pair, limit, offset uint64, from, to time.Time) ([]PositionCloseHistoryResponse, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + var response []PositionCloseHistoryResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "delivery/"+settle+"/position_close", params, nil, &response) +} + +// GetDeliveryLiquidationHistory lists liquidation history +func (g *Gateio) GetDeliveryLiquidationHistory(ctx context.Context, settle string, contract currency.Pair, limit uint64, at time.Time) ([]LiquidationHistoryItem, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if !at.IsZero() { + params.Set("at", strconv.FormatInt(at.Unix(), 10)) + } + var response []LiquidationHistoryItem + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "delivery/"+settle+"/liquidates", params, nil, &response) +} + +// GetDeliverySettlementHistory retrieves settlement history +func (g *Gateio) GetDeliverySettlementHistory(ctx context.Context, settle string, contract currency.Pair, limit uint64, at time.Time) ([]SettlementHistoryItem, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if !at.IsZero() { + params.Set("at", strconv.FormatInt(at.Unix(), 10)) + } + var response []SettlementHistoryItem + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "delivery/"+settle+"/settlements", params, nil, &response) +} + +// GetDeliveryPriceTriggeredOrder creates a price-triggered order +func (g *Gateio) GetDeliveryPriceTriggeredOrder(ctx context.Context, settle string, arg *FuturesPriceTriggeredOrderParam) (*OrderID, error) { + if arg == nil { + return nil, errNilArgument + } + if settle == "" { + return nil, errEmptySettlementCurrency + } + if arg.Initial.Contract.IsEmpty() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + if arg.Initial.Price < 0 { + return nil, fmt.Errorf("%w, price must be greater than 0", errInvalidPrice) + } + if arg.Initial.Size <= 0 { + return nil, errors.New("invalid argument: initial.size out of range") + } + if arg.Initial.TimeInForce != "" && + arg.Initial.TimeInForce != gtcTIF && arg.Initial.TimeInForce != iocTIF { + return nil, fmt.Errorf("%w, only time in force value 'gtc' and 'ioc' are supported", errInvalidTimeInForce) + } + if arg.Trigger.StrategyType != 0 && arg.Trigger.StrategyType != 1 { + return nil, fmt.Errorf("strategy type must be 0 or 1, 0: by price, and 1: by price gap") + } + if arg.Trigger.Rule != 1 && arg.Trigger.Rule != 2 { + return nil, fmt.Errorf("invalid trigger condition('rule') value, rule must be 1 or 2") + } + if arg.Trigger.PriceType != 0 && arg.Trigger.PriceType != 1 && arg.Trigger.PriceType != 2 { + return nil, fmt.Errorf("price type must be 0 or 1 or 2") + } + if arg.Trigger.Price <= 0 { + return nil, errors.New("invalid argument: trigger.price") + } + if arg.Trigger.OrderType != "" && + arg.Trigger.OrderType != "close-long-order" && + arg.Trigger.OrderType != "close-short-order" && + arg.Trigger.OrderType != "close-long-position" && + arg.Trigger.OrderType != "close-short-position" && + arg.Trigger.OrderType != "plan-close-long-position" && + arg.Trigger.OrderType != "plan-close-short-position" { + return nil, errors.New("invalid order type, only 'close-long-order', 'close-short-order', 'close-long-position', 'close-short-position', 'plan-close-long-position', and 'plan-close-short-position'") + } + var response *OrderID + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodPost, + "delivery/"+settle+"/price_orders", nil, &arg, &response) +} + +// GetDeliveryAllAutoOrder retrieves all auto orders +func (g *Gateio) GetDeliveryAllAutoOrder(ctx context.Context, status, settle string, contract currency.Pair, limit, offset uint64) ([]PriceTriggeredOrder, error) { + if status != statusOpen && status != statusFinished { + return nil, fmt.Errorf("%w status %s", errInvalidOrderStatus, status) + } + if settle == "" { + return nil, errEmptySettlementCurrency + } + params := url.Values{} + params.Set("status", status) + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + var response []PriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "delivery/"+settle+"/price_orders", params, nil, &response) +} + +// CancelAllDeliveryPriceTriggeredOrder cancels all delivery price triggered orders +func (g *Gateio) CancelAllDeliveryPriceTriggeredOrder(ctx context.Context, settle string, contract currency.Pair) ([]PriceTriggeredOrder, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if contract.IsInvalid() { + return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam) + } + params := url.Values{} + params.Set("contract", contract.String()) + var response []PriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapCancelOrdersEPL, http.MethodDelete, + "delivery/"+settle+"/price_orders", params, nil, &response) +} + +// GetSingleDeliveryPriceTriggeredOrder retrieves a single price triggered order +func (g *Gateio) GetSingleDeliveryPriceTriggeredOrder(ctx context.Context, settle, orderID string) (*PriceTriggeredOrder, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if orderID == "" { + return nil, errInvalidOrderID + } + var response *PriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + "delivery/"+settle+"/price_orders/"+orderID, nil, nil, &response) +} + +// CancelDeliveryPriceTriggeredOrder cancel a price-triggered order +func (g *Gateio) CancelDeliveryPriceTriggeredOrder(ctx context.Context, settle, orderID string) (*PriceTriggeredOrder, error) { + if settle == "" { + return nil, errEmptySettlementCurrency + } + if orderID == "" { + return nil, errInvalidOrderID + } + var response *PriceTriggeredOrder + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapCancelOrdersEPL, http.MethodDelete, + "delivery/"+settle+"/price_orders/"+orderID, nil, nil, &response) +} + +// ********************************** Options *************************************************** + +// GetAllOptionsUnderlyings retrieves all option underlyings +func (g *Gateio) GetAllOptionsUnderlyings(ctx context.Context) ([]OptionUnderlying, error) { + var response []OptionUnderlying + return response, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, gateioOptionUnderlyings, &response) +} + +// GetExpirationTime return the expiration time for the provided underlying. +func (g *Gateio) GetExpirationTime(ctx context.Context, underlying string) (time.Time, error) { + var timestamp []float64 + err := g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, gateioOptionExpiration+"?underlying="+underlying, ×tamp) + if err != nil { + return time.Time{}, err + } + if len(timestamp) == 0 { + return time.Time{}, errNoValidResponseFromServer + } + return time.Unix(int64(timestamp[0]), 0), nil +} + +// GetAllContractOfUnderlyingWithinExpiryDate retrieves list of contracts of the specified underlying and expiry time. +func (g *Gateio) GetAllContractOfUnderlyingWithinExpiryDate(ctx context.Context, underlying string, expTime time.Time) ([]OptionContract, error) { + params := url.Values{} + if underlying == "" { + return nil, errInvalidUnderlying + } + params.Set("underlying", underlying) + if !expTime.IsZero() { + params.Set("expires", strconv.FormatInt(expTime.Unix(), 10)) + } + var contracts []OptionContract + return contracts, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, common.EncodeURLValues(gateioOptionContracts, params), &contracts) +} + +// GetOptionsSpecifiedContractDetail query specified contract detail +func (g *Gateio) GetOptionsSpecifiedContractDetail(ctx context.Context, contract currency.Pair) (*OptionContract, error) { + if contract.IsInvalid() { + return nil, errInvalidOrMissingContractParam + } + var contr *OptionContract + return contr, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, + gateioOptionContracts+"/"+contract.String(), &contr) +} + +// GetSettlementHistory retrieves list of settlement history +func (g *Gateio) GetSettlementHistory(ctx context.Context, underlying string, offset, limit uint64, from, to time.Time) ([]OptionSettlement, error) { + if underlying == "" { + return nil, errInvalidUnderlying + } + params := url.Values{} + params.Set("underlying", underlying) + if offset > 0 { + params.Set("offset", strconv.Itoa(int(offset))) + } + if limit > 0 { + params.Set("limit", strconv.Itoa(int(limit))) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + var settlements []OptionSettlement + return settlements, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, + common.EncodeURLValues(gateioOptionSettlement, params), &settlements) +} + +// GetOptionsSpecifiedContractsSettlement retrieve a single contract settlement detail passing the underlying and contract name +func (g *Gateio) GetOptionsSpecifiedContractsSettlement(ctx context.Context, contract currency.Pair, underlying string, at int64) (*OptionSettlement, error) { + if underlying == "" { + return nil, errInvalidUnderlying + } + if contract.IsInvalid() { + return nil, errInvalidOrMissingContractParam + } + params := url.Values{} + params.Set("underlying", underlying) + params.Set("at", strconv.FormatInt(at, 10)) + var settlement *OptionSettlement + return settlement, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, common.EncodeURLValues(gateioOptionSettlement+"/"+contract.String(), params), &settlement) +} + +// GetMyOptionsSettlements retrieves accounts option settlements. +func (g *Gateio) GetMyOptionsSettlements(ctx context.Context, underlying string, contract currency.Pair, offset, limit uint64, to time.Time) ([]MyOptionSettlement, error) { + if underlying == "" { + return nil, errInvalidUnderlying + } + params := url.Values{} + params.Set("underlying", underlying) + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if to.After(time.Now()) { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if offset > 0 { + params.Set("offset", strconv.Itoa(int(offset))) + } + if limit > 0 { + params.Set("limit", strconv.Itoa(int(limit))) + } + var settlements []MyOptionSettlement + return settlements, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, gateioOptionMySettlements, params, nil, &settlements) +} + +// GetOptionsOrderbook returns the orderbook data for the given contract. +func (g *Gateio) GetOptionsOrderbook(ctx context.Context, contract currency.Pair, interval string, limit uint64, withOrderbookID bool) (*Orderbook, error) { + if contract.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + params := url.Values{} + params.Set("contract", strings.ToUpper(contract.String())) + if interval != "" { + params.Set("interval", interval) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + params.Set("with_id", strconv.FormatBool(withOrderbookID)) + var response *Orderbook + return response, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, common.EncodeURLValues(gateioOptionsOrderbook, params), &response) +} + +// GetOptionAccounts lists option accounts +func (g *Gateio) GetOptionAccounts(ctx context.Context) (*OptionAccount, error) { + var resp *OptionAccount + return resp, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, gateioOptionAccounts, nil, nil, &resp) +} + +// GetAccountChangingHistory retrieves list of account changing history +func (g *Gateio) GetAccountChangingHistory(ctx context.Context, offset, limit uint64, from, to time.Time, changingType string) ([]AccountBook, error) { + params := url.Values{} + if changingType != "" { + params.Set("type", changingType) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() && ((!from.IsZero() && to.After(from)) || to.Before(time.Now())) { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + var accountBook []AccountBook + return accountBook, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, gateioOptionsAccountbook, params, nil, &accountBook) +} + +// GetUsersPositionSpecifiedUnderlying lists user's positions of specified underlying +func (g *Gateio) GetUsersPositionSpecifiedUnderlying(ctx context.Context, underlying string) ([]UsersPositionForUnderlying, error) { + params := url.Values{} + if underlying != "" { + params.Set("underlying", underlying) + } + var response []UsersPositionForUnderlying + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, gateioOptionsPosition, params, nil, &response) +} + +// GetSpecifiedContractPosition retrieves specified contract position +func (g *Gateio) GetSpecifiedContractPosition(ctx context.Context, contract currency.Pair) (*UsersPositionForUnderlying, error) { + if contract.IsInvalid() { + return nil, errInvalidOrMissingContractParam + } + var response *UsersPositionForUnderlying + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, + gateioOptionsPosition+"/"+contract.String(), nil, nil, &response) +} + +// GetUsersLiquidationHistoryForSpecifiedUnderlying retrieves user's liquidation history of specified underlying +func (g *Gateio) GetUsersLiquidationHistoryForSpecifiedUnderlying(ctx context.Context, underlying string, contract currency.Pair) ([]ContractClosePosition, error) { + if underlying == "" { + return nil, errInvalidUnderlying + } + params := url.Values{} + params.Set("underlying", underlying) + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + var response []ContractClosePosition + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, gateioOptionsPositionClose, params, nil, &response) +} + +// PlaceOptionOrder creates an options order +func (g *Gateio) PlaceOptionOrder(ctx context.Context, arg OptionOrderParam) (*OptionOrderResponse, error) { + if arg.Contract == "" { + return nil, errInvalidOrMissingContractParam + } + if arg.OrderSize == 0 { + return nil, errInvalidOrderSize + } + if arg.Iceberg < 0 { + arg.Iceberg = 0 + } + if arg.TimeInForce != gtcTIF && arg.TimeInForce != iocTIF && arg.TimeInForce != pocTIF { + arg.TimeInForce = "" + } + if arg.TimeInForce == iocTIF || arg.Price < 0 { + arg.Price = 0 + } + var err error + arg.Text, err = common.GenerateRandomString(10, common.NumberCharacters) + if err != nil { + return nil, err + } + arg.Text = "t-" + arg.Text + var response *OptionOrderResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPost, + gateioOptionsOrders, nil, &arg, &response) +} + +// GetOptionFuturesOrders retrieves futures orders +func (g *Gateio) GetOptionFuturesOrders(ctx context.Context, contract currency.Pair, underlying, status string, offset, limit uint64, from, to time.Time) ([]OptionOrderResponse, error) { + params := url.Values{} + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if underlying != "" { + params.Set("underlying", underlying) + } + status = strings.ToLower(status) + if status == statusOpen || status == statusFinished { + params.Set("status", status) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() && ((!from.IsZero() && to.After(from)) || to.Before(time.Now())) { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + var response []OptionOrderResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, + http.MethodGet, gateioOptionsOrders, params, nil, &response) +} + +// CancelMultipleOptionOpenOrders cancels all open orders matched +func (g *Gateio) CancelMultipleOptionOpenOrders(ctx context.Context, contract currency.Pair, underlying, side string) ([]OptionOrderResponse, error) { + params := url.Values{} + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if underlying != "" { + params.Set("underlying", underlying) + } + if side != "" { + params.Set("side", side) + } + var response []OptionOrderResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, + exchange.RestSpot, perpetualSwapCancelOrdersEPL, http.MethodDelete, gateioOptionsOrders, params, nil, &response) +} + +// GetSingleOptionOrder retrieves a single option order +func (g *Gateio) GetSingleOptionOrder(ctx context.Context, orderID string) (*OptionOrderResponse, error) { + if orderID == "" { + return nil, errInvalidOrderID + } + var o *OptionOrderResponse + return o, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, gateioOptionsOrders+"/"+orderID, nil, nil, &o) +} + +// CancelOptionSingleOrder cancel a single order. +func (g *Gateio) CancelOptionSingleOrder(ctx context.Context, orderID string) (*OptionOrderResponse, error) { + if orderID == "" { + return nil, errInvalidOrderID + } + var response *OptionOrderResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapCancelOrdersEPL, http.MethodDelete, + "options/orders/"+orderID, nil, nil, &response) +} + +// GetOptionsPersonalTradingHistory retrieves personal tradign histories given the underlying{Required}, contract, and other pagination params. +func (g *Gateio) GetOptionsPersonalTradingHistory(ctx context.Context, underlying string, contract currency.Pair, offset, limit uint64, from, to time.Time) ([]OptionTradingHistory, error) { + if underlying == "" { + return nil, errInvalidUnderlying + } + params := url.Values{} + params.Set("underlying", underlying) + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() && ((!from.IsZero() && to.After(from)) || to.Before(time.Now())) { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + var resp []OptionTradingHistory + return resp, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, gateioOptionsMyTrades, params, nil, &resp) +} + +// GetOptionsTickers lists tickers of options contracts +func (g *Gateio) GetOptionsTickers(ctx context.Context, underlying string) ([]OptionsTicker, error) { + if underlying == "" { + return nil, errInvalidUnderlying + } + underlying = strings.ToUpper(underlying) + var response []OptionsTicker + return response, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, + gateioOptionsTickers+"?underlying="+underlying, &response) +} + +// GetOptionUnderlyingTickers retrieves options underlying ticker +func (g *Gateio) GetOptionUnderlyingTickers(ctx context.Context, underlying string) (*OptionsUnderlyingTicker, error) { + if underlying == "" { + return nil, errInvalidUnderlying + } + var respos *OptionsUnderlyingTicker + return respos, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, + "options/underlying/tickers/"+underlying, &respos) +} + +// GetOptionFuturesCandlesticks retrieves option futures candlesticks +func (g *Gateio) GetOptionFuturesCandlesticks(ctx context.Context, contract currency.Pair, limit uint64, from, to time.Time, interval kline.Interval) ([]FuturesCandlestick, error) { + if contract.IsInvalid() { + return nil, errInvalidOrMissingContractParam + } + params := url.Values{} + params.Set("contract", contract.String()) + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + intervalString, err := g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params.Set("interval", intervalString) + var candles []FuturesCandlestick + return candles, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, + common.EncodeURLValues(gateioOptionCandlesticks, params), &candles) +} + +// GetOptionFuturesMarkPriceCandlesticks retrieves mark price candlesticks of an underlying +func (g *Gateio) GetOptionFuturesMarkPriceCandlesticks(ctx context.Context, underlying string, limit uint64, from, to time.Time, interval kline.Interval) ([]FuturesCandlestick, error) { + if underlying == "" { + return nil, errInvalidUnderlying + } + params := url.Values{} + params.Set("underlying", underlying) + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + if int64(interval) != 0 { + intervalString, err := g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params.Set("interval", intervalString) + } + var candles []FuturesCandlestick + return candles, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, + common.EncodeURLValues(gateioOptionUnderlyingCandlesticks, params), &candles) +} + +// GetOptionsTradeHistory retrieves options trade history +func (g *Gateio) GetOptionsTradeHistory(ctx context.Context, contract /*C is call, while P is put*/ currency.Pair, callType string, offset, limit uint64, from, to time.Time) ([]TradingHistoryItem, error) { + params := url.Values{} + callType = strings.ToUpper(callType) + if callType == "C" || callType == "P" { + params.Set("type", callType) + } + if contract.IsPopulated() { + params.Set("contract", contract.String()) + } + if offset > 0 { + params.Set("offset", strconv.FormatUint(offset, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + if !from.IsZero() { + params.Set("from", strconv.FormatInt(from.Unix(), 10)) + } + if !to.IsZero() { + params.Set("to", strconv.FormatInt(to.Unix(), 10)) + } + var trades []TradingHistoryItem + return trades, g.SendHTTPRequest(ctx, exchange.RestSpot, perpetualSwapDefaultEPL, common.EncodeURLValues(gateioOptionsTrades, params), &trades) +} + +// ********************************** Flash_SWAP ************************* + +// GetSupportedFlashSwapCurrencies retrieves all supported currencies in flash swap +func (g *Gateio) GetSupportedFlashSwapCurrencies(ctx context.Context) ([]SwapCurrencies, error) { + var currencies []SwapCurrencies + return currencies, g.SendHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, gateioFlashSwapCurrencies, ¤cies) +} + +// CreateFlashSwapOrder creates a new flash swap order +// initiate a flash swap preview in advance because order creation requires a preview result +func (g *Gateio) CreateFlashSwapOrder(ctx context.Context, arg FlashSwapOrderParams) (*FlashSwapOrderResponse, error) { + if arg.PreviewID == "" { + return nil, errMissingPreviewID + } + if arg.BuyCurrency.IsEmpty() { + return nil, fmt.Errorf("%w, buy currency can not empty", currency.ErrCurrencyCodeEmpty) + } + if arg.SellCurrency.IsEmpty() { + return nil, fmt.Errorf("%w, sell currency can not empty", currency.ErrCurrencyCodeEmpty) + } + if arg.SellAmount <= 0 { + return nil, fmt.Errorf("%w, sell_amount can not be less than or equal to 0", errInvalidAmount) + } + if arg.BuyAmount <= 0 { + return nil, fmt.Errorf("%w, buy_amount amount can not be less than or equal to 0", errInvalidAmount) + } + var response *FlashSwapOrderResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotDefaultEPL, http.MethodPost, gateioFlashSwapOrders, nil, &arg, &response) +} + +// GetAllFlashSwapOrders retrieves list of flash swap orders filtered by the params +func (g *Gateio) GetAllFlashSwapOrders(ctx context.Context, status int, sellCurrency, buyCurrency currency.Code, reverse bool, limit, page uint64) ([]FlashSwapOrderResponse, error) { + params := url.Values{} + if status == 1 || status == 2 { + params.Set("status", strconv.Itoa(status)) + } + if !sellCurrency.IsEmpty() { + params.Set("sell_currency", sellCurrency.String()) + } + if !buyCurrency.IsEmpty() { + params.Set("buy_currency", buyCurrency.String()) + } + params.Set("reverse", strconv.FormatBool(reverse)) + if page > 0 { + params.Set("page", strconv.FormatUint(page, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatUint(limit, 10)) + } + var response []FlashSwapOrderResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, gateioFlashSwapOrders, params, nil, &response) +} + +// GetSingleFlashSwapOrder get a single flash swap order's detail +func (g *Gateio) GetSingleFlashSwapOrder(ctx context.Context, orderID string) (*FlashSwapOrderResponse, error) { + if orderID == "" { + return nil, fmt.Errorf("%w, flash order order_id must not be empty", errInvalidOrderID) + } + var response *FlashSwapOrderResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodGet, + gateioFlashSwapOrders+"/"+orderID, nil, nil, &response) +} + +// InitiateFlashSwapOrderReview initiate a flash swap order preview +func (g *Gateio) InitiateFlashSwapOrderReview(ctx context.Context, arg FlashSwapOrderParams) (*InitFlashSwapOrderPreviewResponse, error) { + if arg.PreviewID == "" { + return nil, errMissingPreviewID + } + if arg.BuyCurrency.IsEmpty() { + return nil, fmt.Errorf("%w, buy currency can not empty", currency.ErrCurrencyCodeEmpty) + } + if arg.SellCurrency.IsEmpty() { + return nil, fmt.Errorf("%w, sell currency can not empty", currency.ErrCurrencyCodeEmpty) + } + var response *InitFlashSwapOrderPreviewResponse + return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotPrivateEPL, http.MethodPost, gateioFlashSwapOrdersPreview, nil, &arg, &response) +} + +// IsValidPairString returns true if the string represents a valid currency pair +func (g *Gateio) IsValidPairString(currencyPair string) bool { + if len(currencyPair) < 3 { + return false + } + if strings.Contains(currencyPair, g.CurrencyPairs.RequestFormat.Delimiter) { + result := strings.Split(currencyPair, g.CurrencyPairs.RequestFormat.Delimiter) + return len(result) >= 2 + } + return false +} + +// ********************************* Trading Fee calculation ******************************** + // GetFee returns an estimate of fee based on type of transaction func (g *Gateio) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (fee float64, err error) { switch feeBuilder.FeeType { case exchange.CryptocurrencyTradeFee: - feePairs, err := g.GetMarketInfo(ctx) + feePairs, err := g.GetPersonalTradingFee(ctx, feeBuilder.Pair, "") if err != nil { return 0, err } - - currencyPair := feeBuilder.Pair.Base.String() + - feeBuilder.Pair.Delimiter + - feeBuilder.Pair.Quote.String() - - var feeForPair float64 - for _, i := range feePairs.Pairs { - if strings.EqualFold(currencyPair, i.Symbol) { - feeForPair = i.Fee - } + if feeBuilder.IsMaker { + fee = calculateTradingFee(feePairs.MakerFee, + feeBuilder.PurchasePrice, + feeBuilder.Amount) + } else { + fee = calculateTradingFee(feePairs.TakerFee, + feeBuilder.PurchasePrice, + feeBuilder.Amount) } - - if feeForPair == 0 { - return 0, fmt.Errorf("currency '%s' failed to find fee data", - currencyPair) - } - - fee = calculateTradingFee(feeForPair, - feeBuilder.PurchasePrice, - feeBuilder.Amount) - case exchange.CryptocurrencyWithdrawalFee: fee = getCryptocurrencyWithdrawalFee(feeBuilder.Pair.Base) case exchange.OfflineTradeFee: fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount) } - if fee < 0 { fee = 0 } - return fee, nil } @@ -518,72 +3822,38 @@ func getOfflineTradeFee(price, amount float64) float64 { } func calculateTradingFee(feeForPair, purchasePrice, amount float64) float64 { - return (feeForPair / 100) * purchasePrice * amount + return feeForPair * purchasePrice * amount } func getCryptocurrencyWithdrawalFee(c currency.Code) float64 { return WithdrawalFees[c] } -// WithdrawCrypto withdraws cryptocurrency to your selected wallet -func (g *Gateio) WithdrawCrypto(ctx context.Context, curr, address, memo, chain string, amount float64) (*withdraw.ExchangeResponse, error) { - if curr == "" || address == "" || amount <= 0 { - return nil, errors.New("currency, address and amount must be set") +// GetUnderlyingFromCurrencyPair returns an underlying string from a currency pair +func (g *Gateio) GetUnderlyingFromCurrencyPair(p currency.Pair) (currency.Pair, error) { + pairString := strings.Replace(p.Upper().String(), currency.DashDelimiter, currency.UnderscoreDelimiter, -1) + ccies := strings.Split(pairString, currency.UnderscoreDelimiter) + if len(ccies) < 2 { + return currency.EMPTYPAIR, fmt.Errorf("invalid currency pair %v", p) } - - resp := struct { - Result bool `json:"result"` - Message string `json:"message"` - Code int `json:"code"` - }{} - - vals := url.Values{} - vals.Set("currency", strings.ToUpper(curr)) - vals.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) - - // Transaction MEMO has to be entered after the address separated by a space - if memo != "" { - address += " " + memo - } - vals.Set("address", address) - - if chain != "" { - vals.Set("chain", strings.ToUpper(chain)) - } - - err := g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, gateioWithdraw, vals.Encode(), &resp) - if err != nil { - return nil, err - } - if !resp.Result { - return nil, fmt.Errorf("code:%d message:%s", resp.Code, resp.Message) - } - - return &withdraw.ExchangeResponse{ - Status: resp.Message, - }, nil + return currency.Pair{Base: currency.NewCode(ccies[0]), Delimiter: currency.UnderscoreDelimiter, Quote: currency.NewCode(ccies[1])}, nil } - -// GetCryptoDepositAddress returns a deposit address for a cryptocurrency -func (g *Gateio) GetCryptoDepositAddress(ctx context.Context, curr string) (*DepositAddr, error) { - var result DepositAddr - params := fmt.Sprintf("currency=%s", - curr) - - err := g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, gateioDepositAddress, params, &result) - if err != nil { - return nil, err +func (g *Gateio) getSettlementFromCurrency(currencyPair currency.Pair, ignoreUSDSettles bool) (settlement string, err error) { + quote := currencyPair.Quote.Upper().String() + switch { + case strings.HasPrefix(quote, currency.USDT.String()): + return currency.USDT.Item.Lower, nil + case strings.HasPrefix(quote, currency.BTC.String()): + return currency.BTC.Item.Lower, nil + case strings.HasPrefix(quote, currency.USD.String()): + if ignoreUSDSettles { + return currency.BTC.Item.Lower, nil + } + return currency.USD.Item.Lower, nil + case strings.HasPrefix(currencyPair.Base.Upper().String(), currency.BTC.String()): + // some instruments having a BTC base currency uses a BTC settlement + return currency.BTC.Item.Lower, nil + default: + return "", fmt.Errorf("%w %v", errCannotParseSettlementCurrency, currencyPair) } - - if !result.Result { - return nil, fmt.Errorf("code:%d message:%s", result.Code, result.Message) - } - - // For memo/payment ID currencies - if strings.Contains(result.Address, " ") { - split := strings.Split(result.Address, " ") - result.Address = split[0] - result.Tag = split[1] - } - return &result, nil } diff --git a/exchanges/gateio/gateio_convert.go b/exchanges/gateio/gateio_convert.go new file mode 100644 index 00000000..5fc504db --- /dev/null +++ b/exchanges/gateio/gateio_convert.go @@ -0,0 +1,133 @@ +package gateio + +import ( + "encoding/json" + "fmt" + "strconv" + "time" +) + +type gateioTime time.Time + +// UnmarshalJSON deserializes json, and timestamp information. +func (a *gateioTime) UnmarshalJSON(data []byte) error { + var value interface{} + err := json.Unmarshal(data, &value) + if err != nil { + return err + } + var standard int64 + switch val := value.(type) { + case float64: + standard = int64(val) + case int64: + standard = val + case int32: + standard = int64(val) + case string: + if val == "" { + return nil + } + parsedValue, err := strconv.ParseFloat(val, 64) + if err != nil { + return err + } + standard = int64(parsedValue) + default: + return fmt.Errorf("cannot unmarshal %T into gateioTime", val) + } + if standard > 9999999999 { + *a = gateioTime(time.UnixMilli(standard)) + } else { + *a = gateioTime(time.Unix(standard, 0)) + } + return nil +} + +// Time represents a time instance. +func (a *gateioTime) Time() time.Time { return time.Time(*a) } + +type gateioNumericalValue float64 + +// UnmarshalJSON is custom type json unmarshaller for gateioNumericalValue +func (a *gateioNumericalValue) UnmarshalJSON(data []byte) error { + var num interface{} + err := json.Unmarshal(data, &num) + if err != nil { + return err + } + + switch d := num.(type) { + case float64: + *a = gateioNumericalValue(d) + case string: + if d == "" { + *a = gateioNumericalValue(0) + return nil + } + convNum, err := strconv.ParseFloat(d, 64) + if err != nil { + return err + } + *a = gateioNumericalValue(convNum) + } + return nil +} + +// Float64 returns float64 value from gateioNumericalValue instance. +func (a *gateioNumericalValue) Float64() float64 { return float64(*a) } + +// UnmarshalJSON to deserialize timestamp information and create OrderbookItem instance from the list of asks and bids data. +func (a *Orderbook) UnmarshalJSON(data []byte) error { + type Alias Orderbook + type askorbid struct { + Price gateioNumericalValue `json:"p"` + Size float64 `json:"s"` + } + chil := &struct { + *Alias + Current float64 `json:"current"` + Update float64 `json:"update"` + Asks []askorbid `json:"asks"` + Bids []askorbid `json:"bids"` + }{ + Alias: (*Alias)(a), + } + err := json.Unmarshal(data, &chil) + if err != nil { + return err + } + a.Current = time.UnixMilli(int64(chil.Current * 1000)) + a.Update = time.UnixMilli(int64(chil.Update * 1000)) + a.Asks = make([]OrderbookItem, len(chil.Asks)) + a.Bids = make([]OrderbookItem, len(chil.Bids)) + for x := range chil.Asks { + a.Asks[x] = OrderbookItem{ + Amount: chil.Asks[x].Size, + Price: chil.Asks[x].Price.Float64(), + } + } + for x := range chil.Bids { + a.Bids[x] = OrderbookItem{ + Amount: chil.Bids[x].Size, + Price: chil.Bids[x].Price.Float64(), + } + } + return nil +} + +// UnmarshalJSON deserialises the JSON info, including the timestamp +func (a *WsUserPersonalTrade) UnmarshalJSON(data []byte) error { + type Alias WsUserPersonalTrade + chil := &struct { + *Alias + CreateTimeMicroS float64 `json:"create_time_ms,string"` + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, chil); err != nil { + return err + } + a.CreateTimeMicroS = time.UnixMicro(int64(chil.CreateTimeMicroS * 1000)) + return nil +} diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index d374ccc9..24bf6c1b 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -2,26 +2,24 @@ package gateio import ( "context" + "encoding/json" "errors" "log" - "net/http" "os" + "strconv" "sync" "testing" "time" - "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" - "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) @@ -34,7 +32,7 @@ const ( ) var g = &Gateio{} -var wsSetupRan bool +var spotTradablePair, marginTradablePair, crossMarginTradablePair, futuresTradablePair, optionsTradablePair, deliveryFuturesTradablePair currency.Pair func TestMain(m *testing.M) { g.SetDefaults() @@ -56,12 +54,13 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("GateIO setup error", err) } - + request.MaxRequestJobs = 200 + g.Run(context.Background()) + getFirstTradablePairOfAssets() os.Exit(m.Run()) } func TestStart(t *testing.T) { - t.Parallel() err := g.Start(context.Background(), nil) if !errors.Is(err, common.ErrNilPointer) { t.Fatalf("received: '%v' but expected: '%v'", err, common.ErrNilPointer) @@ -74,817 +73,3249 @@ func TestStart(t *testing.T) { testWg.Wait() } -func TestGetSymbols(t *testing.T) { - t.Parallel() - _, err := g.GetSymbols(context.Background()) - if err != nil { - t.Errorf("Gateio TestGetSymbols: %s", err) - } -} - -func TestGetMarketInfo(t *testing.T) { - t.Parallel() - _, err := g.GetMarketInfo(context.Background()) - if err != nil { - t.Errorf("Gateio GetMarketInfo: %s", err) - } -} - -func TestSpotNewOrder(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) - - _, err := g.SpotNewOrder(context.Background(), - SpotNewOrderRequestParams{ - Symbol: "btc_usdt", - Amount: -1, - Price: 100000, - Type: order.Sell.Lower(), - }) - if err != nil { - t.Errorf("Gateio SpotNewOrder: %s", err) - } -} - -func TestCancelExistingOrder(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) - - _, err := g.CancelExistingOrder(context.Background(), 917591554, "btc_usdt") - if err != nil { - t.Errorf("Gateio CancelExistingOrder: %s", err) - } -} - -func TestGetBalances(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, g) - - _, err := g.GetBalances(context.Background()) - if err != nil { - t.Errorf("Gateio GetBalances: %s", err) - } -} - -func TestGetLatestSpotPrice(t *testing.T) { - t.Parallel() - _, err := g.GetLatestSpotPrice(context.Background(), "btc_usdt") - if err != nil { - t.Errorf("Gateio GetLatestSpotPrice: %s", err) - } -} - -func TestGetTicker(t *testing.T) { - t.Parallel() - _, err := g.GetTicker(context.Background(), "btc_usdt") - if err != nil { - t.Errorf("Gateio GetTicker: %s", err) - } -} - -func TestGetTickers(t *testing.T) { - t.Parallel() - _, err := g.GetTickers(context.Background()) - if err != nil { - t.Errorf("Gateio GetTicker: %s", err) - } -} - -func TestGetOrderbook(t *testing.T) { - t.Parallel() - _, err := g.GetOrderbook(context.Background(), "btc_usdt") - if err != nil { - t.Errorf("Gateio GetTicker: %s", err) - } -} - -func TestGetSpotKline(t *testing.T) { - t.Parallel() - _, err := g.GetSpotKline(context.Background(), - KlinesRequestParams{ - Symbol: "btc_usdt", - GroupSec: "5", // 5 minutes or less - HourSize: 1, // 1 hour data - }) - - if err != nil { - t.Errorf("Gateio GetSpotKline: %s", err) - } -} - -func setFeeBuilder() *exchange.FeeBuilder { - return &exchange.FeeBuilder{ - Amount: 1, - FeeType: exchange.CryptocurrencyTradeFee, - Pair: currency.NewPairWithDelimiter(currency.BTC.String(), - currency.USDT.String(), "_"), - IsMaker: false, - PurchasePrice: 1, - FiatCurrency: currency.USD, - BankTransactionType: exchange.WireTransfer, - } -} - -func TestGetTradeHistory(t *testing.T) { - _, err := g.GetTrades(context.Background(), - currency.NewPairWithDelimiter(currency.BTC.String(), - currency.USDT.String(), "_").String()) - if err != nil { - t.Error(err) - } -} - -// TestGetFeeByTypeOfflineTradeFee logic test -func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { - var feeBuilder = setFeeBuilder() - _, err := g.GetFeeByType(context.Background(), feeBuilder) - if err != nil { - t.Fatal(err) - } - if !sharedtestvalues.AreAPICredentialsSet(g) { - if feeBuilder.FeeType != exchange.OfflineTradeFee { - t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) - } - } else { - if feeBuilder.FeeType != exchange.CryptocurrencyTradeFee { - t.Errorf("Expected %v, received %v", exchange.CryptocurrencyTradeFee, feeBuilder.FeeType) - } - } -} - -func TestGetFee(t *testing.T) { - var feeBuilder = setFeeBuilder() - if sharedtestvalues.AreAPICredentialsSet(g) { - // CryptocurrencyTradeFee Basic - if _, err := g.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // CryptocurrencyTradeFee High quantity - feeBuilder = setFeeBuilder() - feeBuilder.Amount = 1000 - feeBuilder.PurchasePrice = 1000 - if _, err := g.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // CryptocurrencyTradeFee IsMaker - feeBuilder = setFeeBuilder() - feeBuilder.IsMaker = true - if _, err := g.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // CryptocurrencyTradeFee Negative purchase price - feeBuilder = setFeeBuilder() - feeBuilder.PurchasePrice = -1000 - if _, err := g.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - } - // CryptocurrencyWithdrawalFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if _, err := g.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // CryptocurrencyWithdrawalFee Invalid currency - feeBuilder = setFeeBuilder() - feeBuilder.Pair.Base = currency.NewCode("hello") - feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if _, err := g.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // CryptocurrencyDepositFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CryptocurrencyDepositFee - if _, err := g.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // InternationalBankDepositFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.InternationalBankDepositFee - if _, err := g.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } - - // InternationalBankWithdrawalFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee - feeBuilder.FiatCurrency = currency.USD - if _, err := g.GetFee(context.Background(), feeBuilder); err != nil { - t.Error(err) - } -} - -func TestFormatWithdrawPermissions(t *testing.T) { - t.Parallel() - expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.NoFiatWithdrawalsText - withdrawPermissions := g.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { - t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) - } -} - -func TestGetActiveOrders(t *testing.T) { - t.Parallel() - var getOrdersRequest = order.GetOrdersRequest{ - Type: order.AnyType, - AssetType: asset.Spot, - Side: order.AnySide, - } - - _, err := g.GetActiveOrders(context.Background(), &getOrdersRequest) - if sharedtestvalues.AreAPICredentialsSet(g) && err != nil { - t.Errorf("Could not get open orders: %s", err) - } else if !sharedtestvalues.AreAPICredentialsSet(g) && err == nil { - t.Error("Expecting an error when no keys are set") - } -} - -func TestGetOrderHistory(t *testing.T) { - t.Parallel() - var getOrdersRequest = order.GetOrdersRequest{ - Type: order.AnyType, - AssetType: asset.Spot, - Side: order.AnySide, - } - - currPair := currency.NewPair(currency.LTC, currency.BTC) - currPair.Delimiter = "_" - getOrdersRequest.Pairs = []currency.Pair{currPair} - - _, err := g.GetOrderHistory(context.Background(), &getOrdersRequest) - if sharedtestvalues.AreAPICredentialsSet(g) && err != nil { - t.Errorf("Could not get order history: %s", err) - } else if !sharedtestvalues.AreAPICredentialsSet(g) && err == nil { - t.Error("Expecting an error when no keys are set") - } -} - -// Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them -// ---------------------------------------------------------------------------------------------------------------------------- -func TestSubmitOrder(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, g, canManipulateRealOrders) - - var orderSubmission = &order.Submit{ - Exchange: g.Name, - Pair: currency.Pair{ - Delimiter: "_", - Base: currency.LTC, - Quote: currency.BTC, - }, - Side: order.Buy, - Type: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", - AssetType: asset.Spot, - } - response, err := g.SubmitOrder(context.Background(), orderSubmission) - if sharedtestvalues.AreAPICredentialsSet(g) && (err != nil || response.Status != order.New) { - t.Errorf("Order failed to be placed: %v", err) - } else if !sharedtestvalues.AreAPICredentialsSet(g) && err == nil { - t.Error("Expecting an error when no keys are set") - } -} - -func TestCancelExchangeOrder(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, g, canManipulateRealOrders) - - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - var orderCancellation = &order.Cancel{ - OrderID: "1", - WalletAddress: core.BitcoinDonationAddress, - AccountID: "1", - Pair: currencyPair, - AssetType: asset.Spot, - } - - err := g.CancelOrder(context.Background(), orderCancellation) - if !sharedtestvalues.AreAPICredentialsSet(g) && err == nil { - t.Error("Expecting an error when no keys are set") - } - if sharedtestvalues.AreAPICredentialsSet(g) && err != nil { - t.Errorf("Could not cancel orders: %v", err) - } -} - func TestCancelAllExchangeOrders(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, g, canManipulateRealOrders) - - currencyPair := currency.NewPair(currency.LTC, currency.BTC) + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + _, err := g.CancelAllOrders(context.Background(), nil) + if !errors.Is(err, order.ErrCancelOrderIsNil) { + t.Error(err) + } var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - Pair: currencyPair, - AssetType: asset.Spot, + Pair: optionsTradablePair, + AssetType: asset.Options, } - - resp, err := g.CancelAllOrders(context.Background(), orderCancellation) - - if !sharedtestvalues.AreAPICredentialsSet(g) && err == nil { - t.Error("Expecting an error when no keys are set") + _, err = g.CancelAllOrders(context.Background(), orderCancellation) + if err != nil { + t.Error(err) } - if sharedtestvalues.AreAPICredentialsSet(g) && err != nil { - t.Errorf("Could not cancel orders: %v", err) + orderCancellation.AssetType = asset.Spot + orderCancellation.Pair = spotTradablePair + _, err = g.CancelAllOrders(context.Background(), orderCancellation) + if err != nil { + t.Error(err) } - - if len(resp.Status) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.Status)) + orderCancellation.Pair = currency.EMPTYPAIR + orderCancellation.AssetType = asset.Margin + _, err = g.CancelAllOrders(context.Background(), orderCancellation) + if !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Error(err) + } + orderCancellation.Pair = marginTradablePair + _, err = g.CancelAllOrders(context.Background(), orderCancellation) + if err != nil { + t.Error(err) + } + orderCancellation.Pair = currency.EMPTYPAIR + orderCancellation.AssetType = asset.CrossMargin + _, err = g.CancelAllOrders(context.Background(), orderCancellation) + if !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Error(err) + } + orderCancellation.Pair = crossMarginTradablePair + _, err = g.CancelAllOrders(context.Background(), orderCancellation) + if err != nil { + t.Error(err) + } + orderCancellation.Pair = currency.EMPTYPAIR + orderCancellation.AssetType = asset.Futures + _, err = g.CancelAllOrders(context.Background(), orderCancellation) + if !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Error(err) + } + orderCancellation.Pair = futuresTradablePair + _, err = g.CancelAllOrders(context.Background(), orderCancellation) + if err != nil { + t.Error(err) + } + orderCancellation.Pair = currency.EMPTYPAIR + orderCancellation.AssetType = asset.DeliveryFutures + _, err = g.CancelAllOrders(context.Background(), orderCancellation) + if !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Error(err) + } + orderCancellation.Pair = deliveryFuturesTradablePair + _, err = g.CancelAllOrders(context.Background(), orderCancellation) + if err != nil { + t.Error(err) } } - func TestGetAccountInfo(t *testing.T) { t.Parallel() - if apiSecret == "" || apiKey == "" { - _, err := g.UpdateAccountInfo(context.Background(), asset.Spot) - if err == nil { - t.Error("GetAccountInfo() Expected error") - } - } else { - _, err := g.UpdateAccountInfo(context.Background(), asset.Spot) - if err != nil { - t.Error("GetAccountInfo() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + _, err := g.UpdateAccountInfo(context.Background(), asset.Spot) + if err != nil { + t.Error("GetAccountInfo() error", err) } -} - -func TestModifyOrder(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, g, canManipulateRealOrders) - - _, err := g.ModifyOrder(context.Background(), - &order.Modify{AssetType: asset.Spot}) - if err == nil { - t.Error("ModifyOrder() Expected error") + if _, err := g.UpdateAccountInfo(context.Background(), asset.Margin); err != nil { + t.Errorf("%s UpdateAccountInfo() error %v", g.Name, err) + } + if _, err := g.UpdateAccountInfo(context.Background(), asset.CrossMargin); err != nil { + t.Errorf("%s UpdateAccountInfo() error %v", g.Name, err) + } + if _, err := g.UpdateAccountInfo(context.Background(), asset.Options); err != nil { + t.Errorf("%s UpdateAccountInfo() error %v", g.Name, err) + } + if _, err := g.UpdateAccountInfo(context.Background(), asset.Futures); err != nil { + t.Errorf("%s UpdateAccountInfo() error %v", g.Name, err) + } + if _, err := g.UpdateAccountInfo(context.Background(), asset.DeliveryFutures); err != nil { + t.Errorf("%s UpdateAccountInfo() error %v", g.Name, err) } } func TestWithdraw(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, g, canManipulateRealOrders) - - _, err := g.WithdrawCryptocurrencyFunds(context.Background(), &withdraw.Request{ + cryptocurrencyChains, err := g.GetAvailableTransferChains(context.Background(), currency.BTC) + if err != nil { + t.Fatal(err) + } else if len(cryptocurrencyChains) == 0 { + t.Fatal("no crypto currency chain available") + } + withdrawCryptoRequest := withdraw.Request{ Exchange: g.Name, - Amount: -1, + Amount: 1, Currency: currency.BTC, Description: "WITHDRAW IT ALL", Crypto: withdraw.CryptoRequest{ Address: core.BitcoinDonationAddress, - }}) - if !sharedtestvalues.AreAPICredentialsSet(g) && err == nil { - t.Error("Expecting an error when no keys are set") + Chain: cryptocurrencyChains[0], + }, } - if sharedtestvalues.AreAPICredentialsSet(g) && err != nil { - t.Errorf("Withdraw failed to be placed: %v", err) + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err = g.WithdrawCryptocurrencyFunds(context.Background(), &withdrawCryptoRequest); err != nil { + t.Errorf("%s WithdrawCryptocurrencyFunds() error: %v", g.Name, err) } } -func TestWithdrawFiat(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, g, canManipulateRealOrders) - - var withdrawFiatRequest = withdraw.Request{} - _, err := g.WithdrawFiatFunds(context.Background(), &withdrawFiatRequest) - if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) - } -} - -func TestWithdrawInternationalBank(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, g, canManipulateRealOrders) - - var withdrawFiatRequest = withdraw.Request{} - _, err := g.WithdrawFiatFundsToInternationalBank(context.Background(), - &withdrawFiatRequest) - if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) - } -} - -func TestGetDepositAddress(t *testing.T) { - t.Parallel() - if sharedtestvalues.AreAPICredentialsSet(g) { - _, err := g.GetDepositAddress(context.Background(), currency.USDT, "", "TRX") - if err != nil { - t.Error("Test Fail - GetDepositAddress error", err) - } - } else { - _, err := g.GetDepositAddress(context.Background(), currency.ETC, "", "") - if err == nil { - t.Error("Test Fail - GetDepositAddress error cannot be nil") - } - } -} func TestGetOrderInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, g) - _, err := g.GetOrderInfo(context.Background(), - "917591554", currency.EMPTYPAIR, asset.Spot) + "917591554", spotTradablePair, asset.Spot) if err != nil { - if err.Error() != "no order found with id 917591554" && err.Error() != "failed to get open orders" { - t.Fatalf("GetOrderInfo() returned an error skipping test: %v", err) - } + t.Errorf("GetOrderInfo() %v", err) } -} - -// TestWsGetBalance dials websocket, sends balance request. -func TestWsGetBalance(t *testing.T) { - if !g.Websocket.IsEnabled() && !g.API.AuthenticatedWebsocketSupport || !sharedtestvalues.AreAPICredentialsSet(g) { - t.Skip(stream.WebsocketNotEnabled) - } - var dialer websocket.Dialer - err := g.Websocket.Conn.Dial(&dialer, http.Header{}) + _, err = g.GetOrderInfo(context.Background(), "917591554", optionsTradablePair, asset.Options) if err != nil { - t.Fatal(err) + t.Errorf("GetOrderInfo() %v", err) } - go g.wsReadData() - err = g.wsServerSignIn(context.Background()) + _, err = g.GetOrderInfo(context.Background(), "917591554", marginTradablePair, asset.Margin) if err != nil { - t.Fatal(err) + t.Errorf("GetOrderInfo() %v", err) } - _, err = g.wsGetBalance([]string{"EOS", "BTC"}) + _, err = g.GetOrderInfo(context.Background(), "917591554", crossMarginTradablePair, asset.CrossMargin) if err != nil { - t.Error(err) + t.Errorf("GetOrderInfo() %v", err) } - _, err = g.wsGetBalance([]string{}) + _, err = g.GetOrderInfo(context.Background(), "917591554", futuresTradablePair, asset.Futures) if err != nil { - t.Error(err) + t.Errorf("GetOrderInfo() %v", err) } -} - -// TestWsGetOrderInfo dials websocket, sends order info request. -func TestWsGetOrderInfo(t *testing.T) { - if !g.Websocket.IsEnabled() && !g.API.AuthenticatedWebsocketSupport || !sharedtestvalues.AreAPICredentialsSet(g) { - t.Skip(stream.WebsocketNotEnabled) - } - var dialer websocket.Dialer - err := g.Websocket.Conn.Dial(&dialer, http.Header{}) + _, err = g.GetOrderInfo(context.Background(), "917591554", deliveryFuturesTradablePair, asset.DeliveryFutures) if err != nil { - t.Fatal(err) - } - go g.wsReadData() - err = g.wsServerSignIn(context.Background()) - if err != nil { - t.Fatal(err) - } - _, err = g.wsGetOrderInfo("EOS_USDT", 0, 100) - if err != nil { - t.Error(err) - } -} - -func setupWSTestAuth(t *testing.T) { - t.Helper() - if wsSetupRan { - return - } - if !g.Websocket.IsEnabled() && !g.API.AuthenticatedWebsocketSupport || !sharedtestvalues.AreAPICredentialsSet(g) { - t.Skip(stream.WebsocketNotEnabled) - } - if err := g.Websocket.Connect(); err != nil { - t.Fatal(err) - } - wsSetupRan = true -} - -// TestWsSubscribe dials websocket, sends a subscribe request. -func TestWsSubscribe(t *testing.T) { - setupWSTestAuth(t) - err := g.Subscribe([]stream.ChannelSubscription{ - { - Channel: "ticker.subscribe", - Currency: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_"), - }, - }) - if err != nil { - t.Error(err) - } - - err = g.Unsubscribe([]stream.ChannelSubscription{ - { - Channel: "ticker.subscribe", - Currency: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_"), - }, - }) - if err != nil { - t.Error(err) - } -} - -func TestWsTicker(t *testing.T) { - pressXToJSON := []byte(`{ - "method": "ticker.update", - "params": - [ - "BTC_USDT", - { - "period": 86400, - "open": "0", - "close": "0", - "high": "0", - "low": "0", - "last": "0.2844", - "change": "0", - "quoteVolume": "0", - "baseVolume": "0" - } - ], - "id": null -}`) - err := g.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } -} - -func TestWsTrade(t *testing.T) { - pressXToJSON := []byte(`{ - "method": "trades.update", - "params": - [ - "BTC_USDT", - [ - { - "id": 7172173, - "time": 1523339279.761838, - "price": "398.59", - "amount": "0.027", - "type": "buy" - } - ] - ], - "id": null - } -`) - err := g.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } -} - -func TestWsDepth(t *testing.T) { - pressXToJSON := []byte(`{ - "method": "depth.update", - "params": [ - true, - { - "asks": [ - [ - "8000.00", - "9.6250" - ] - ], - "bids": [ - [ - "8000.00", - "9.6250" - ] - ] - }, - "BTC_USDT" - ], - "id": null - }`) - err := g.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } -} - -func TestWsKLine(t *testing.T) { - pressXToJSON := []byte(`{ - "method": "kline.update", - "params": - [ - [ - 1492358400, - "7000.00", - "8000.0", - "8100.00", - "6800.00", - "1000.00", - "123456.00", - "BTC_USDT" - ] - ], - "id": null -}`) - err := g.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } -} - -func TestWsOrderUpdate(t *testing.T) { - pressXToJSON := []byte(`{ - "method": "order.update", - "params": [ - 3, - { - "id": 34628963, - "market": "BTC_USDT", - "orderType": 1, - "type": 2, - "user": 602123, - "ctime": 1523013969.6271579, - "mtime": 1523013969.6271579, - "price": "0.1", - "amount": "1000", - "left": "1000", - "filledAmount": "0", - "filledTotal": "0", - "dealFee": "0" - } - ], - "id": null -}`) - err := g.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } -} - -func TestWsBalanceUpdate(t *testing.T) { - pressXToJSON := []byte(`{ - "method": "balance.update", - "params": [{"EOS": {"available": "96.765323611874", "freeze": "11"}}], - "id": 1234 -}`) - err := g.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } -} - -func TestParseTime(t *testing.T) { - // Test REST example - r := convert.TimeFromUnixTimestampDecimal(1574846296.995313).UTC() - if r.Year() != 2019 || - r.Month().String() != "November" || - r.Day() != 27 { - t.Error("unexpected result") - } - - // Test websocket example - r = convert.TimeFromUnixTimestampDecimal(1523887354.256974).UTC() - if r.Year() != 2018 || - r.Month().String() != "April" || - r.Day() != 16 { - t.Error("unexpected result") - } -} - -func TestGetHistoricCandles(t *testing.T) { - t.Parallel() - pair, err := currency.NewPairFromString("BTC_USDT") - if err != nil { - t.Fatal(err) - } - startTime := time.Now().Add(-time.Hour * 6) - _, err = g.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneMin, startTime, time.Now()) - if err != nil { - t.Fatal(err) - } -} - -func TestGetHistoricCandlesExtended(t *testing.T) { - t.Parallel() - pair, err := currency.NewPairFromString("BTC_USDT") - if err != nil { - t.Fatal(err) - } - startTime := time.Now().Add(-time.Hour * 2) - _, err = g.GetHistoricCandlesExtended(context.Background(), pair, asset.Spot, kline.OneMin, startTime, time.Now()) - if !errors.Is(err, common.ErrNotYetImplemented) { - t.Fatal(err) - } -} - -func Test_FormatExchangeKlineInterval(t *testing.T) { - testCases := []struct { - name string - interval kline.Interval - output string - }{ - { - "OneMin", - kline.OneMin, - "60", - }, - { - "OneDay", - kline.OneDay, - "86400", - }, - } - - for x := range testCases { - test := testCases[x] - - t.Run(test.name, func(t *testing.T) { - ret := g.FormatExchangeKlineInterval(test.interval) - - if ret != test.output { - t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret) - } - }) - } -} - -func TestGenerateDefaultSubscriptions(t *testing.T) { - err := g.CurrencyPairs.EnablePair(asset.Spot, currency.NewPair( - currency.LTC, - currency.USDT, - )) - if err != nil { - t.Fatal(err) - } - subs, err := g.GenerateDefaultSubscriptions() - if err != nil { - t.Fatal(err) - } - - payload, err := g.generatePayload(subs) - if err != nil { - t.Fatal(err) - } - - if len(payload) != 4 { - t.Fatal("unexpected payload length") - } -} - -func TestGetRecentTrades(t *testing.T) { - t.Parallel() - currencyPair, err := currency.NewPairFromString("btc_usdt") - if err != nil { - t.Fatal(err) - } - _, err = g.GetRecentTrades(context.Background(), currencyPair, asset.Spot) - if err != nil { - t.Error(err) - } -} - -func TestGetHistoricTrades(t *testing.T) { - t.Parallel() - currencyPair, err := currency.NewPairFromString("btc_usdt") - if err != nil { - t.Fatal(err) - } - _, err = g.GetHistoricTrades(context.Background(), - currencyPair, asset.Spot, time.Now().Add(-time.Minute*15), time.Now()) - if err != nil && err != common.ErrFunctionNotSupported { - t.Error(err) + t.Errorf("GetOrderInfo() %v", err) } } func TestUpdateTicker(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("btc_usdt") + _, err := g.UpdateTicker(context.Background(), optionsTradablePair, asset.Options) + if err != nil { + t.Error(err) + } + _, err = g.UpdateTicker(context.Background(), spotTradablePair, asset.Spot) + if err != nil { + t.Error(err) + } + _, err = g.UpdateTicker(context.Background(), marginTradablePair, asset.Margin) + if err != nil { + t.Error(err) + } + _, err = g.UpdateTicker(context.Background(), crossMarginTradablePair, asset.CrossMargin) + if err != nil { + t.Error(err) + } + _, err = g.UpdateTicker(context.Background(), futuresTradablePair, asset.Futures) + if err != nil { + t.Error(err) + } + _, err = g.UpdateTicker(context.Background(), deliveryFuturesTradablePair, asset.DeliveryFutures) + if err != nil { + t.Error(err) + } +} + +func TestListSpotCurrencies(t *testing.T) { + t.Parallel() + if _, err := g.ListSpotCurrencies(context.Background()); err != nil { + t.Errorf("%s ListAllCurrencies() error %v", g.Name, err) + } +} + +func TestGetCurrencyDetail(t *testing.T) { + t.Parallel() + if _, err := g.GetCurrencyDetail(context.Background(), currency.BTC); err != nil { + t.Errorf("%s GetCurrencyDetail() error %v", g.Name, err) + } +} + +func TestListAllCurrencyPairs(t *testing.T) { + t.Parallel() + if _, err := g.ListSpotCurrencyPairs(context.Background()); err != nil { + t.Errorf("%s ListAllCurrencyPairs() error %v", g.Name, err) + } +} + +func TestGetCurrencyPairDetal(t *testing.T) { + t.Parallel() + if _, err := g.GetCurrencyPairDetail(context.Background(), currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}.String()); err != nil { + t.Errorf("%s GetCurrencyPairDetal() error %v", g.Name, err) + } +} + +func TestGetTickers(t *testing.T) { + t.Parallel() + if _, err := g.GetTickers(context.Background(), "BTC_USDT", ""); err != nil { + t.Errorf("%s GetTickers() error %v", g.Name, err) + } +} + +func TestGetTicker(t *testing.T) { + t.Parallel() + if _, err := g.GetTicker(context.Background(), currency.Pair{Base: currency.BTC, Delimiter: currency.UnderscoreDelimiter, Quote: currency.USDT}.String(), utc8TimeZone); err != nil { + t.Errorf("%s GetTicker() error %v", g.Name, err) + } +} + +func TestGetOrderbook(t *testing.T) { + t.Parallel() + _, err := g.GetOrderbook(context.Background(), spotTradablePair.String(), "0.1", 10, false) + if err != nil { + t.Errorf("%s GetOrderbook() error %v", g.Name, err) + } + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) if err != nil { t.Fatal(err) } - _, err = g.UpdateTicker(context.Background(), cp, asset.Spot) + if _, err = g.GetFuturesOrderbook(context.Background(), settle, futuresTradablePair.String(), "", 10, false); err != nil { + t.Errorf("%s GetOrderbook() error %v", g.Name, err) + } + settle, err = g.getSettlementFromCurrency(deliveryFuturesTradablePair, false) + if err != nil { + t.Fatal(err) + } + if _, err = g.GetDeliveryOrderbook(context.Background(), settle, "0.1", deliveryFuturesTradablePair, 10, false); err != nil { + t.Errorf("%s GetOrderbook() error %v", g.Name, err) + } + if _, err = g.GetOptionsOrderbook(context.Background(), optionsTradablePair, "0.1", 10, false); err != nil { + t.Errorf("%s GetOrderbook() error %v", g.Name, err) + } +} + +func TestGetMarketTrades(t *testing.T) { + t.Parallel() + if _, err := g.GetMarketTrades(context.Background(), spotTradablePair, 0, "", true, time.Time{}, time.Time{}, 1); err != nil { + t.Errorf("%s GetMarketTrades() error %v", g.Name, err) + } +} + +func TestGetCandlesticks(t *testing.T) { + t.Parallel() + if _, err := g.GetCandlesticks(context.Background(), spotTradablePair, 0, time.Time{}, time.Time{}, kline.OneDay); err != nil { + t.Errorf("%s GetCandlesticks() error %v", g.Name, err) + } +} +func TestGetTradingFeeRatio(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetTradingFeeRatio(context.Background(), currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}); err != nil { + t.Errorf("%s GetTradingFeeRatio() error %v", g.Name, err) + } +} + +func TestGetSpotAccounts(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSpotAccounts(context.Background(), currency.BTC); err != nil { + t.Errorf("%s GetSpotAccounts() error %v", g.Name, err) + } +} + +func TestCreateBatchOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CreateBatchOrders(context.Background(), []CreateOrderRequestData{ + { + CurrencyPair: spotTradablePair, + Side: "sell", + Amount: 0.001, + Price: 12349, + Account: g.assetTypeToString(asset.Spot), + Type: "limit", + }, + { + CurrencyPair: currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}, + Side: "buy", + Amount: 1, + Price: 1234567789, + Account: g.assetTypeToString(asset.Spot), + Type: "limit", + }, + }); err != nil { + t.Errorf("%s CreateBatchOrders() error %v", g.Name, err) + } +} + +func TestGetSpotOpenOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GateioSpotOpenOrders(context.Background(), 0, 0, false); err != nil { + t.Errorf("%s GetSpotOpenOrders() error %v", g.Name, err) + } +} + +func TestSpotClosePositionWhenCrossCurrencyDisabled(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.SpotClosePositionWhenCrossCurrencyDisabled(context.Background(), &ClosePositionRequestParam{ + Amount: 0.1, + Price: 1234567384, + CurrencyPair: spotTradablePair, + }); err != nil { + t.Errorf("%s SpotClosePositionWhenCrossCurrencyDisabled() error %v", g.Name, err) + } +} + +func TestCreateSpotOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.PlaceSpotOrder(context.Background(), &CreateOrderRequestData{ + CurrencyPair: spotTradablePair, + Side: "buy", + Amount: 1, + Price: 900000, + Account: g.assetTypeToString(asset.Spot), + Type: "limit", + }); err != nil { + t.Errorf("%s CreateSpotOrder() error %v", g.Name, err) + } +} + +func TestGetSpotOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSpotOrders(context.Background(), currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}, "open", 0, 0); err != nil { + t.Errorf("%s GetSpotOrders() error %v", g.Name, err) + } +} + +func TestCancelAllOpenOrdersSpecifiedCurrencyPair(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelAllOpenOrdersSpecifiedCurrencyPair(context.Background(), spotTradablePair, order.Sell, asset.Empty); err != nil { + t.Errorf("%s CancelAllOpenOrdersSpecifiedCurrencyPair() error %v", g.Name, err) + } +} + +func TestCancelBatchOrdersWithIDList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelBatchOrdersWithIDList(context.Background(), []CancelOrderByIDParam{ + { + CurrencyPair: spotTradablePair, + ID: "1234567", + }, + { + CurrencyPair: currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}, + ID: "something", + }, + }); err != nil { + t.Errorf("%s CancelBatchOrderWithIDList() error %v", g.Name, err) + } +} + +func TestGetSpotOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSpotOrder(context.Background(), "1234", currency.Pair{ + Base: currency.BTC, + Delimiter: currency.UnderscoreDelimiter, + Quote: currency.USDT}, asset.Spot); err != nil { + t.Errorf("%s GetSpotOrder() error %v", g.Name, err) + } +} +func TestAmendSpotOrder(t *testing.T) { + t.Parallel() + _, err := g.AmendSpotOrder(context.Background(), "", spotTradablePair, false, &PriceAndAmount{ + Price: 1000, + }) + if !errors.Is(err, errInvalidOrderID) { + t.Errorf("expecting %v, but found %v", errInvalidOrderID, err) + } + _, err = g.AmendSpotOrder(context.Background(), "123", currency.EMPTYPAIR, false, &PriceAndAmount{ + Price: 1000, + }) + if !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Errorf("expecting %v, but found %v", currency.ErrCurrencyPairEmpty, err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + _, err = g.AmendSpotOrder(context.Background(), "123", spotTradablePair, false, &PriceAndAmount{ + Price: 1000, + }) if err != nil { t.Error(err) } } +func TestCancelSingleSpotOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelSingleSpotOrder(context.Background(), "1234", + spotTradablePair.String(), false); err != nil { + t.Errorf("%s CancelSingleSpotOrder() error %v", g.Name, err) + } +} + +func TestGetPersonalTradingHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GateIOGetPersonalTradingHistory(context.Background(), currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}, "", 0, 0, false, time.Time{}, time.Time{}); err != nil { + t.Errorf("%s GetPersonalTradingHistory() error %v", g.Name, err) + } +} + +func TestGetServerTime(t *testing.T) { + t.Parallel() + if _, err := g.GetServerTime(context.Background(), asset.Spot); err != nil { + t.Errorf("%s GetServerTime() error %v", g.Name, err) + } +} + +func TestCountdownCancelorder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CountdownCancelorders(context.Background(), CountdownCancelOrderParam{ + Timeout: 10, + CurrencyPair: currency.Pair{Base: currency.BTC, Quote: currency.ETH, Delimiter: currency.UnderscoreDelimiter}, + }); err != nil { + t.Errorf("%s CountdownCancelorder() error %v", g.Name, err) + } +} + +func TestCreatePriceTriggeredOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CreatePriceTriggeredOrder(context.Background(), &PriceTriggeredOrderParam{ + Trigger: TriggerPriceInfo{ + Price: 123, + Rule: ">=", + Expiration: 3600, + }, + Put: PutOrderData{ + Type: "limit", + Side: "sell", + Price: 2312312, + Amount: 30, + TimeInForce: "gtc", + }, + Market: currency.Pair{Base: currency.GT, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}, + }); err != nil { + t.Errorf("%s CreatePriceTriggeredOrder() error %v", g.Name, err) + } +} + +func TestGetPriceTriggeredOrderList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetPriceTriggeredOrderList(context.Background(), "open", currency.EMPTYPAIR, asset.Empty, 0, 0); err != nil { + t.Errorf("%s GetPriceTriggeredOrderList() error %v", g.Name, err) + } +} + +func TestCancelAllOpenOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelMultipleSpotOpenOrders(context.Background(), currency.EMPTYPAIR, asset.CrossMargin); err != nil { + t.Errorf("%s CancelAllOpenOrders() error %v", g.Name, err) + } +} + +func TestGetSinglePriceTriggeredOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSinglePriceTriggeredOrder(context.Background(), "1234"); err != nil { + t.Errorf("%s GetSinglePriceTriggeredOrder() error %v", g.Name, err) + } +} + +func TestCancelPriceTriggeredOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.CancelPriceTriggeredOrder(context.Background(), "1234"); err != nil { + t.Errorf("%s CancelPriceTriggeredOrder() error %v", g.Name, err) + } +} + +func TestGetMarginAccountList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetMarginAccountList(context.Background(), currency.EMPTYPAIR); err != nil { + t.Errorf("%s GetMarginAccountList() error %v", g.Name, err) + } +} + +func TestListMarginAccountBalanceChangeHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.ListMarginAccountBalanceChangeHistory(context.Background(), currency.BTC, currency.Pair{ + Base: currency.BTC, + Delimiter: currency.UnderscoreDelimiter, + Quote: currency.USDT}, time.Time{}, time.Time{}, 0, 0); err != nil { + t.Errorf("%s ListMarginAccountBalanceChangeHistory() error %v", g.Name, err) + } +} + +func TestGetMarginFundingAccountList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetMarginFundingAccountList(context.Background(), currency.BTC); err != nil { + t.Errorf("%s GetMarginFundingAccountList %v", g.Name, err) + } +} + +func TestMarginLoan(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.MarginLoan(context.Background(), &MarginLoanRequestParam{ + Side: "borrow", + Amount: 1, + Currency: currency.BTC, + CurrencyPair: currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}, + Days: 10, + Rate: 0.0002, + }); err != nil { + t.Errorf("%s MarginLoan() error %v", g.Name, err) + } +} + +func TestGetMarginAllLoans(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetMarginAllLoans(context.Background(), "open", "lend", "", currency.BTC, currency.Pair{Base: currency.BTC, Delimiter: currency.UnderscoreDelimiter, Quote: currency.USDT}, false, 0, 0); err != nil { + t.Errorf("%s GetMarginAllLoans() error %v", g.Name, err) + } +} + +func TestMergeMultipleLendingLoans(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.MergeMultipleLendingLoans(context.Background(), currency.USDT, []string{"123", "23423"}); err != nil { + t.Errorf("%s MergeMultipleLendingLoans() error %v", g.Name, err) + } +} + +func TestRetriveOneSingleLoanDetail(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.RetriveOneSingleLoanDetail(context.Background(), "borrow", "123"); err != nil { + t.Errorf("%s RetriveOneSingleLoanDetail() error %v", g.Name, err) + } +} + +func TestModifyALoan(t *testing.T) { + t.Parallel() + if _, err := g.ModifyALoan(context.Background(), "1234", &ModifyLoanRequestParam{ + Currency: currency.BTC, + Side: "borrow", + AutoRenew: false, + }); !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Errorf("%s ModifyALoan() error %v", g.Name, err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.ModifyALoan(context.Background(), "1234", &ModifyLoanRequestParam{ + Currency: currency.BTC, + Side: "borrow", + AutoRenew: false, + CurrencyPair: currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}, + }); err != nil { + t.Errorf("%s ModifyALoan() error %v", g.Name, err) + } +} + +func TestCancelLendingLoan(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.CancelLendingLoan(context.Background(), currency.BTC, "1234"); err != nil { + t.Errorf("%s CancelLendingLoan() error %v", g.Name, err) + } +} + +func TestRepayALoan(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.RepayALoan(context.Background(), "1234", &RepayLoanRequestParam{ + CurrencyPair: currency.NewPair(currency.BTC, currency.USDT), + Currency: currency.BTC, + Mode: "all", + }); err != nil { + t.Errorf("%s RepayALoan() error %v", g.Name, err) + } +} + +func TestListLoanRepaymentRecords(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.ListLoanRepaymentRecords(context.Background(), "1234"); err != nil { + t.Errorf("%s LoanRepaymentRecord() error %v", g.Name, err) + } +} + +func TestListRepaymentRecordsOfSpecificLoan(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.ListRepaymentRecordsOfSpecificLoan(context.Background(), "1234", "", 0, 0); err != nil { + t.Errorf("%s error while ListRepaymentRecordsOfSpecificLoan() %v", g.Name, err) + } +} + +func TestGetOneSingleloanRecord(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetOneSingleLoanRecord(context.Background(), "1234", "123"); err != nil { + t.Errorf("%s error while GetOneSingleloanRecord() %v", g.Name, err) + } +} + +func TestModifyALoanRecord(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.ModifyALoanRecord(context.Background(), "1234", &ModifyLoanRequestParam{ + Currency: currency.USDT, + CurrencyPair: currency.NewPair(currency.BTC, currency.USDT), + Side: "lend", + AutoRenew: true, + LoanID: "1234", + }); err != nil { + t.Errorf("%s ModifyALoanRecord() error %v", g.Name, err) + } +} + +func TestUpdateUsersAutoRepaymentSetting(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.UpdateUsersAutoRepaymentSetting(context.Background(), true); err != nil { + t.Errorf("%s UpdateUsersAutoRepaymentSetting() error %v", g.Name, err) + } +} + +func TestGetUserAutoRepaymentSetting(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetUserAutoRepaymentSetting(context.Background()); err != nil { + t.Errorf("%s GetUserAutoRepaymentSetting() error %v", g.Name, err) + } +} + +func TestGetMaxTransferableAmountForSpecificMarginCurrency(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetMaxTransferableAmountForSpecificMarginCurrency(context.Background(), currency.BTC, currency.EMPTYPAIR); err != nil { + t.Errorf("%s GetMaxTransferableAmountForSpecificMarginCurrency() error %v", g.Name, err) + } +} + +func TestGetMaxBorrowableAmountForSpecificMarginCurrency(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetMaxBorrowableAmountForSpecificMarginCurrency(context.Background(), currency.BTC, currency.EMPTYPAIR); err != nil { + t.Errorf("%s GetMaxBorrowableAmountForSpecificMarginCurrency() error %v", g.Name, err) + } +} + +func TestCurrencySupportedByCrossMargin(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.CurrencySupportedByCrossMargin(context.Background()); err != nil { + t.Errorf("%s CurrencySupportedByCrossMargin() error %v", g.Name, err) + } +} + +func TestGetCrossMarginSupportedCurrencyDetail(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetCrossMarginSupportedCurrencyDetail(context.Background(), currency.BTC); err != nil { + t.Errorf("%s GetCrossMarginSupportedCurrencyDetail() error %v", g.Name, err) + } +} + +func TestGetCrossMarginAccounts(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetCrossMarginAccounts(context.Background()); err != nil { + t.Errorf("%s GetCrossMarginAccounts() error %v", g.Name, err) + } +} + +func TestGetCrossMarginAccountChangeHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetCrossMarginAccountChangeHistory(context.Background(), currency.BTC, time.Time{}, time.Time{}, 0, 6, "in"); err != nil { + t.Errorf("%s GetCrossMarginAccountChangeHistory() error %v", g.Name, err) + } +} + +var createCrossMarginBorrowLoanJSON = `{"id": "17", "create_time": 1620381696159, "update_time": 1620381696159, "currency": "EOS", "amount": "110.553635", "text": "web", "status": 2, "repaid": "110.506649705159", "repaid_interest": "0.046985294841", "unpaid_interest": "0.0000074393366667"}` + +func TestCreateCrossMarginBorrowLoan(t *testing.T) { + t.Parallel() + var response CrossMarginLoanResponse + if err := json.Unmarshal([]byte(createCrossMarginBorrowLoanJSON), &response); err != nil { + t.Errorf("%s error while deserializing to CrossMarginBorrowLoanResponse %v", g.Name, err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CreateCrossMarginBorrowLoan(context.Background(), CrossMarginBorrowLoanParams{ + Currency: currency.BTC, + Amount: 3, + }); err != nil { + t.Errorf("%s CreateCrossMarginBorrowLoan() error %v", g.Name, err) + } +} + +func TestGetCrossMarginBorrowHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetCrossMarginBorrowHistory(context.Background(), 1, currency.BTC, 0, 0, false); err != nil { + t.Errorf("%s GetCrossMarginBorrowHistory() error %v", g.Name, err) + } +} + +func TestGetSingleBorrowLoanDetail(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSingleBorrowLoanDetail(context.Background(), "1234"); err != nil { + t.Errorf("%s GetSingleBorrowLoanDetail() error %v", g.Name, err) + } +} + +func TestExecuteRepayment(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.ExecuteRepayment(context.Background(), CurrencyAndAmount{ + Currency: currency.USD, + Amount: 1234.55, + }); err != nil { + t.Errorf("%s ExecuteRepayment() error %v", g.Name, err) + } +} + +func TestGetCrossMarginRepayments(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetCrossMarginRepayments(context.Background(), currency.BTC, "123", 0, 0, false); err != nil { + t.Errorf("%s GetCrossMarginRepayments() error %v", g.Name, err) + } +} + +func TestGetMaxTransferableAmountForSpecificCrossMarginCurrency(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetMaxTransferableAmountForSpecificCrossMarginCurrency(context.Background(), currency.BTC); err != nil { + t.Errorf("%s GetMaxTransferableAmountForSpecificCrossMarginCurrency() error %v", g.Name, err) + } +} + +func TestGetMaxBorrowableAmountForSpecificCrossMarginCurrency(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetMaxBorrowableAmountForSpecificCrossMarginCurrency(context.Background(), currency.BTC); err != nil { + t.Errorf("%s GetMaxBorrowableAmountForSpecificCrossMarginCurrency() error %v", g.Name, err) + } +} + +func TestListCurrencyChain(t *testing.T) { + t.Parallel() + if _, err := g.ListCurrencyChain(context.Background(), currency.BTC); err != nil { + t.Errorf("%s ListCurrencyChain() error %v", g.Name, err) + } +} + +func TestGenerateCurrencyDepositAddress(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GenerateCurrencyDepositAddress(context.Background(), currency.BTC); err != nil { + t.Errorf("%s GenerateCurrencyDepositAddress() error %v", g.Name, err) + } +} + +func TestGetWithdrawalRecords(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetWithdrawalRecords(context.Background(), currency.BTC, time.Time{}, time.Time{}, 0, 0); err != nil { + t.Errorf("%s GetWithdrawalRecords() error %v", g.Name, err) + } +} + +func TestGetDepositRecords(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetDepositRecords(context.Background(), currency.BTC, time.Time{}, time.Time{}, 0, 0); err != nil { + t.Errorf("%s GetDepositRecords() error %v", g.Name, err) + } +} + +func TestTransferCurrency(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.TransferCurrency(context.Background(), &TransferCurrencyParam{ + Currency: currency.BTC, + From: g.assetTypeToString(asset.Spot), + To: g.assetTypeToString(asset.Margin), + Amount: 1202.000, + CurrencyPair: spotTradablePair, + }); err != nil { + t.Errorf("%s TransferCurrency() error %v", g.Name, err) + } +} + +func TestSubAccountTransfer(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if err := g.SubAccountTransfer(context.Background(), SubAccountTransferParam{ + Currency: currency.BTC, + SubAccount: "12222", + Direction: "to", + Amount: 1, + }); err != nil { + t.Errorf("%s SubAccountTransfer() error %v", g.Name, err) + } +} + +func TestGetSubAccountTransferHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.GetSubAccountTransferHistory(context.Background(), "", time.Time{}, time.Time{}, 0, 0); err != nil { + t.Errorf("%s GetSubAccountTransferHistory() error %v", g.Name, err) + } +} + +func TestSubAccountTransferToSubAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if err := g.SubAccountTransferToSubAccount(context.Background(), &InterSubAccountTransferParams{ + Currency: currency.BTC, + SubAccountFromUserID: "1234", + SubAccountFromAssetType: asset.Spot, + SubAccountToUserID: "4567", + SubAccountToAssetType: asset.Spot, + Amount: 1234, + }); err != nil { + t.Error(err) + } +} + +func TestGetWithdrawalStatus(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetWithdrawalStatus(context.Background(), currency.NewCode("")); err != nil { + t.Errorf("%s GetWithdrawalStatus() error %v", g.Name, err) + } +} + +func TestGetSubAccountBalances(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSubAccountBalances(context.Background(), ""); err != nil { + t.Errorf("%s GetSubAccountBalances() error %v", g.Name, err) + } +} + +func TestGetSubAccountMarginBalances(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSubAccountMarginBalances(context.Background(), ""); err != nil { + t.Errorf("%s GetSubAccountMarginBalances() error %v", g.Name, err) + } +} + +func TestGetSubAccountFuturesBalances(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSubAccountFuturesBalances(context.Background(), "", ""); err != nil { + t.Errorf("%s GetSubAccountFuturesBalance() error %v", g.Name, err) + } +} + +func TestGetSubAccountCrossMarginBalances(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSubAccountCrossMarginBalances(context.Background(), ""); err != nil { + t.Errorf("%s GetSubAccountCrossMarginBalances() error %v", g.Name, err) + } +} + +func TestGetSavedAddresses(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSavedAddresses(context.Background(), currency.BTC, "", 0); err != nil { + t.Errorf("%s GetSavedAddresses() error %v", g.Name, err) + } +} + +func TestGetPersonalTradingFee(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetPersonalTradingFee(context.Background(), currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}, ""); err != nil { + t.Errorf("%s GetPersonalTradingFee() error %v", g.Name, err) + } +} + +func TestGetUsersTotalBalance(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetUsersTotalBalance(context.Background(), currency.BTC); err != nil { + t.Errorf("%s GetUsersTotalBalance() error %v", g.Name, err) + } +} + +func TestGetMarginSupportedCurrencyPairs(t *testing.T) { + t.Parallel() + if _, err := g.GetMarginSupportedCurrencyPairs(context.Background()); err != nil { + t.Errorf("%s GetMarginSupportedCurrencyPair() error %v", g.Name, err) + } +} + +func TestGetMarginSupportedCurrencyPair(t *testing.T) { + t.Parallel() + if _, err := g.GetSingleMarginSupportedCurrencyPair(context.Background(), marginTradablePair); err != nil { + t.Errorf("%s GetMarginSupportedCurrencyPair() error %v", g.Name, err) + } +} + +func TestGetOrderbookOfLendingLoans(t *testing.T) { + t.Parallel() + if _, err := g.GetOrderbookOfLendingLoans(context.Background(), currency.BTC); err != nil { + t.Errorf("%s GetOrderbookOfLendingLoans() error %v", g.Name, err) + } +} + +func TestGetAllFutureContracts(t *testing.T) { + t.Parallel() + _, err := g.GetAllFutureContracts(context.Background(), settleUSD) + if err != nil { + t.Errorf("%s GetAllFutureContracts() error %v", g.Name, err) + } + if _, err = g.GetAllFutureContracts(context.Background(), settleUSDT); err != nil { + t.Errorf("%s GetAllFutureContracts() error %v", g.Name, err) + } + if _, err = g.GetAllFutureContracts(context.Background(), settleBTC); err != nil { + t.Errorf("%s GetAllFutureContracts() error %v", g.Name, err) + } +} +func TestGetSingleContract(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err = g.GetSingleContract(context.Background(), settle, futuresTradablePair.String()); err != nil { + t.Errorf("%s GetSingleContract() error %s", g.Name, err) + } +} + +func TestGetFuturesOrderbook(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err := g.GetFuturesOrderbook(context.Background(), settle, futuresTradablePair.String(), "", 0, false); err != nil { + t.Errorf("%s GetFuturesOrderbook() error %v", g.Name, err) + } +} +func TestGetFuturesTradingHistory(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err := g.GetFuturesTradingHistory(context.Background(), settle, futuresTradablePair, 0, 0, "", time.Time{}, time.Time{}); err != nil { + t.Errorf("%s GetFuturesTradingHistory() error %v", g.Name, err) + } +} + +func TestGetFuturesCandlesticks(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err := g.GetFuturesCandlesticks(context.Background(), settle, futuresTradablePair.String(), time.Time{}, time.Time{}, 0, kline.OneWeek); err != nil { + t.Errorf("%s GetFuturesCandlesticks() error %v", g.Name, err) + } +} + +func TestPremiumIndexKLine(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Error(err) + } + if _, err := g.PremiumIndexKLine(context.Background(), settle, futuresTradablePair, time.Time{}, time.Time{}, 0, kline.OneWeek); err != nil { + t.Errorf("%s PremiumIndexKLine() error %v", g.Name, err) + } +} + +func TestGetFutureTickers(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err := g.GetFuturesTickers(context.Background(), settle, futuresTradablePair); err != nil { + t.Errorf("%s GetFuturesTickers() error %v", g.Name, err) + } +} + +func TestGetFutureFundingRates(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err := g.GetFutureFundingRates(context.Background(), settle, futuresTradablePair, 0); err != nil { + t.Errorf("%s GetFutureFundingRates() error %v", g.Name, err) + } +} + +func TestGetFuturesInsuranceBalanceHistory(t *testing.T) { + t.Parallel() + if _, err := g.GetFuturesInsuranceBalanceHistory(context.Background(), settleUSDT, 0); err != nil { + t.Errorf("%s GetFuturesInsuranceBalanceHistory() error %v", g.Name, err) + } +} + +func TestGetFutureStats(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Error(err) + } + if _, err := g.GetFutureStats(context.Background(), settle, futuresTradablePair, time.Time{}, kline.OneHour, 0); err != nil { + t.Errorf("%s GetFutureStats() error %v", g.Name, err) + } +} + +func TestGetIndexConstituent(t *testing.T) { + t.Parallel() + if _, err := g.GetIndexConstituent(context.Background(), settleUSDT, currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}.String()); err != nil { + t.Errorf("%s GetIndexConstituent() error %v", g.Name, err) + } +} + +func TestGetLiquidationHistory(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Error(err) + } + if _, err := g.GetLiquidationHistory(context.Background(), settle, futuresTradablePair, time.Time{}, time.Time{}, 0); err != nil { + t.Errorf("%s GetLiquidationHistory() error %v", g.Name, err) + } +} +func TestQueryFuturesAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.QueryFuturesAccount(context.Background(), settleUSDT); err != nil { + t.Errorf("%s QueryFuturesAccount() error %v", g.Name, err) + } +} + +func TestGetFuturesAccountBooks(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetFuturesAccountBooks(context.Background(), settleUSDT, 0, time.Time{}, time.Time{}, "dnw"); err != nil { + t.Errorf("%s GetFuturesAccountBooks() error %v", g.Name, err) + } +} + +func TestGetAllPositionsOfUsers(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetAllFuturesPositionsOfUsers(context.Background(), settleUSDT); err != nil { + t.Errorf("%s GetAllPositionsOfUsers() error %v", g.Name, err) + } +} + +func TestGetSinglePosition(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSinglePosition(context.Background(), settleUSDT, currency.Pair{Quote: currency.BTC, Base: currency.USDT}); err != nil { + t.Errorf("%s GetSinglePosition() error %v", g.Name, err) + } +} + +func TestUpdatePositionMargin(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err := g.UpdateFuturesPositionMargin(context.Background(), settle, 0.01, futuresTradablePair); err != nil { + t.Errorf("%s UpdatePositionMargin() error %v", g.Name, err) + } +} + +func TestUpdatePositionLeverage(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err := g.UpdateFuturesPositionLeverage(context.Background(), settle, futuresTradablePair, 1, 0); err != nil { + t.Errorf("%s UpdatePositionLeverage() error %v", g.Name, err) + } +} + +func TestUpdatePositionRiskLimit(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err := g.UpdateFuturesPositionRiskLimit(context.Background(), settle, futuresTradablePair, 10); err != nil { + t.Errorf("%s UpdatePositionRiskLimit() error %v", g.Name, err) + } +} + +func TestCreateDeliveryOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(deliveryFuturesTradablePair, false) + if err != nil { + t.Fatal(err) + } + if _, err := g.PlaceDeliveryOrder(context.Background(), &OrderCreateParams{ + Contract: deliveryFuturesTradablePair, + Size: 6024, + Iceberg: 0, + Price: 3765, + Text: "t-my-custom-id", + Settle: settle, + TimeInForce: gtcTIF, + }); err != nil { + t.Errorf("%s CreateDeliveryOrder() error %v", g.Name, err) + } +} + +func TestGetDeliveryOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + settle, err := g.getSettlementFromCurrency(deliveryFuturesTradablePair, false) + if err != nil { + t.Fatal(err) + } + if _, err := g.GetDeliveryOrders(context.Background(), deliveryFuturesTradablePair, "open", settle, "", 0, 0, 1); err != nil { + t.Errorf("%s GetDeliveryOrders() error %v", g.Name, err) + } +} + +func TestCancelAllDeliveryOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(deliveryFuturesTradablePair, false) + if err != nil { + t.Fatal(err) + } + if _, err := g.CancelMultipleDeliveryOrders(context.Background(), deliveryFuturesTradablePair, "ask", settle); err != nil { + t.Errorf("%s CancelAllDeliveryOrders() error %v", g.Name, err) + } +} + +func TestGetSingleDeliveryOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSingleDeliveryOrder(context.Background(), settleUSDT, "123456"); err != nil { + t.Errorf("%s GetSingleDeliveryOrder() error %v", g.Name, err) + } + if _, err := g.GetSingleDeliveryOrder(context.Background(), settleBTC, "123456"); err != nil { + t.Errorf("%s GetSingleDeliveryOrder() error %v", g.Name, err) + } + if _, err := g.GetSingleDeliveryOrder(context.Background(), settleUSD, "123456"); !errors.Is(err, errEmptySettlementCurrency) { + t.Errorf("%s GetSingleDeliveryOrder() expected %v, but found %v", g.Name, errEmptySettlementCurrency, err) + } +} + +func TestCancelSingleDeliveryOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelSingleDeliveryOrder(context.Background(), settleUSDT, "123456"); err != nil { + t.Errorf("%s CancelSingleDeliveryOrder() error %v", g.Name, err) + } + if _, err := g.CancelSingleDeliveryOrder(context.Background(), settleBTC, "123456"); err != nil { + t.Errorf("%s CancelSingleDeliveryOrder() error %v", g.Name, err) + } +} + +func TestGetDeliveryPersonalTradingHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetDeliveryPersonalTradingHistory(context.Background(), settleUSDT, "", deliveryFuturesTradablePair, 0, 0, 1, ""); err != nil { + t.Errorf("%s GetDeliveryPersonalTradingHistory() error %v", g.Name, err) + } +} + +func TestGetDeliveryPositionCloseHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetDeliveryPositionCloseHistory(context.Background(), settleUSDT, deliveryFuturesTradablePair, 0, 0, time.Time{}, time.Time{}); err != nil { + t.Errorf("%s GetDeliveryPositionCloseHistory() error %v", g.Name, err) + } +} + +func TestGetDeliveryLiquidationHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetDeliveryLiquidationHistory(context.Background(), settleUSDT, deliveryFuturesTradablePair, 0, time.Now()); err != nil { + t.Errorf("%s GetDeliveryLiquidationHistory() error %v", g.Name, err) + } +} + +func TestGetDeliverySettlementHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetDeliverySettlementHistory(context.Background(), settleUSDT, deliveryFuturesTradablePair, 0, time.Now()); err != nil { + t.Errorf("%s GetDeliverySettlementHistory() error %v", g.Name, err) + } +} + +func TestGetDeliveryPriceTriggeredOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetDeliveryPriceTriggeredOrder(context.Background(), settleUSDT, &FuturesPriceTriggeredOrderParam{ + Initial: FuturesInitial{ + Price: 1234., + Size: 12, + Contract: deliveryFuturesTradablePair, + }, + Trigger: FuturesTrigger{ + Rule: 1, + OrderType: "close-short-position", + Price: 123400, + }, + }); err != nil { + t.Errorf("%s GetDeliveryPriceTriggeredOrder() error %v", g.Name, err) + } +} + +func TestGetDeliveryAllAutoOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetDeliveryAllAutoOrder(context.Background(), "open", settleUSDT, deliveryFuturesTradablePair, 0, 1); err != nil { + t.Errorf("%s GetDeliveryAllAutoOrder() error %v", g.Name, err) + } +} + +func TestCancelAllDeliveryPriceTriggeredOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(deliveryFuturesTradablePair, false) + if err != nil { + t.Fatal(err) + } + if _, err := g.CancelAllDeliveryPriceTriggeredOrder(context.Background(), settle, deliveryFuturesTradablePair); err != nil { + t.Errorf("%s CancelAllDeliveryPriceTriggeredOrder() error %v", g.Name, err) + } +} + +func TestGetSingleDeliveryPriceTriggeredOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSingleDeliveryPriceTriggeredOrder(context.Background(), settleBTC, "12345"); err != nil { + t.Errorf("%s GetSingleDeliveryPriceTriggeredOrder() error %v", g.Name, err) + } +} + +func TestCancelDeliveryPriceTriggeredOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelDeliveryPriceTriggeredOrder(context.Background(), settleUSDT, "12345"); err != nil { + t.Errorf("%s CancelDeliveryPriceTriggeredOrder() error %v", g.Name, err) + } +} + +func TestEnableOrDisableDualMode(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.EnableOrDisableDualMode(context.Background(), settleBTC, true); err != nil { + t.Errorf("%s EnableOrDisableDualMode() error %v", g.Name, err) + } +} + +func TestRetrivePositionDetailInDualMode(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err = g.RetrivePositionDetailInDualMode(context.Background(), settle, futuresTradablePair); err != nil { + t.Errorf("%s RetrivePositionDetailInDualMode() error %v", g.Name, err) + } +} + +func TestUpdatePositionMarginInDualMode(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err = g.UpdatePositionMarginInDualMode(context.Background(), settle, futuresTradablePair, 0.001, "dual_long"); err != nil { + t.Errorf("%s UpdatePositionMarginInDualMode() error %v", g.Name, err) + } +} +func TestUpdatePositionLeverageInDualMode(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err = g.UpdatePositionLeverageInDualMode(context.Background(), settle, futuresTradablePair, 0.001, 0.001); err != nil { + t.Errorf("%s UpdatePositionLeverageInDualMode() error %v", g.Name, err) + } +} + +func TestUpdatePositionRiskLimitinDualMode(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err = g.UpdatePositionRiskLimitInDualMode(context.Background(), settle, futuresTradablePair, 10); err != nil { + t.Errorf("%s UpdatePositionRiskLimitinDualMode() error %v", g.Name, err) + } +} + +func TestCreateFuturesOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err := g.PlaceFuturesOrder(context.Background(), &OrderCreateParams{ + Contract: futuresTradablePair, + Size: 6024, + Iceberg: 0, + Price: 3765, + TimeInForce: "gtc", + Text: "t-my-custom-id", + Settle: settle, + }); err != nil { + t.Errorf("%s CreateFuturesOrder() error %v", g.Name, err) + } +} + +func TestGetFuturesOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetFuturesOrders(context.Background(), currency.NewPair(currency.BTC, currency.USD), "open", "", settleBTC, 0, 0, 1); err != nil { + t.Errorf("%s GetFuturesOrders() error %v", g.Name, err) + } +} + +func TestCancelMultipleFuturesOpenOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelMultipleFuturesOpenOrders(context.Background(), futuresTradablePair, "ask", settleUSDT); err != nil { + t.Errorf("%s CancelAllOpenOrdersMatched() error %v", g.Name, err) + } +} + +func TestGetSingleFuturesPriceTriggeredOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSingleFuturesPriceTriggeredOrder(context.Background(), settleBTC, "12345"); err != nil { + t.Errorf("%s GetSingleFuturesPriceTriggeredOrder() error %v", g.Name, err) + } +} + +func TestCancelFuturesPriceTriggeredOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelFuturesPriceTriggeredOrder(context.Background(), settleUSDT, "12345"); err != nil { + t.Errorf("%s CancelFuturesPriceTriggeredOrder() error %v", g.Name, err) + } +} + +func TestCreateBatchFuturesOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(futuresTradablePair, true) + if err != nil { + t.Fatal(err) + } + if _, err := g.PlaceBatchFuturesOrders(context.Background(), settleBTC, []OrderCreateParams{ + { + Contract: futuresTradablePair, + Size: 6024, + Iceberg: 0, + Price: 3765, + TimeInForce: "gtc", + Text: "t-my-custom-id", + Settle: settle, + }, + { + Contract: currency.NewPair(currency.BTC, currency.USDT), + Size: 232, + Iceberg: 0, + Price: 376225, + TimeInForce: "gtc", + Text: "t-my-custom-id", + Settle: settleBTC, + }, + }); err != nil { + t.Errorf("%s CreateBatchFuturesOrders() error %v", g.Name, err) + } +} + +func TestGetSingleFuturesOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSingleFuturesOrder(context.Background(), settleBTC, "12345"); err != nil { + t.Errorf("%s GetSingleFuturesOrder() error %v", g.Name, err) + } +} +func TestCancelSingleFuturesOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelSingleFuturesOrder(context.Background(), settleBTC, "12345"); err != nil { + t.Errorf("%s CancelSingleFuturesOrder() error %v", g.Name, err) + } +} +func TestAmendFuturesOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.AmendFuturesOrder(context.Background(), settleBTC, "1234", AmendFuturesOrderParam{ + Price: 12345.990, + }); err != nil { + t.Errorf("%s AmendFuturesOrder() error %v", g.Name, err) + } +} + +func TestGetMyPersonalTradingHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetMyPersonalTradingHistory(context.Background(), settleBTC, "", "", futuresTradablePair, 0, 0, 0); err != nil { + t.Errorf("%s GetMyPersonalTradingHistory() error %v", g.Name, err) + } +} + +func TestGetPositionCloseHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetFuturesPositionCloseHistory(context.Background(), settleBTC, futuresTradablePair, 0, 0, time.Time{}, time.Time{}); err != nil { + t.Errorf("%s GetPositionCloseHistory() error %v", g.Name, err) + } +} + +func TestGetFuturesLiquidationHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetFuturesLiquidationHistory(context.Background(), settleBTC, futuresTradablePair, 0, time.Time{}); err != nil { + t.Errorf("%s GetFuturesLiquidationHistory() error %v", g.Name, err) + } +} + +func TestCountdownCancelOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CountdownCancelOrders(context.Background(), settleBTC, CountdownParams{ + Timeout: 8, + }); err != nil { + t.Errorf("%s CountdownCancelOrders() error %v", g.Name, err) + } +} + +func TestCreatePriceTriggeredFuturesOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(futuresTradablePair, false) + if err != nil { + t.Fatal(err) + } + if _, err := g.CreatePriceTriggeredFuturesOrder(context.Background(), settle, &FuturesPriceTriggeredOrderParam{ + Initial: FuturesInitial{ + Price: 1234., + Size: 2, + Contract: futuresTradablePair, + }, + Trigger: FuturesTrigger{ + Rule: 1, + OrderType: "close-short-position", + }, + }); err != nil { + t.Errorf("%s CreatePriceTriggeredFuturesOrder() error %v", g.Name, err) + } + if _, err := g.CreatePriceTriggeredFuturesOrder(context.Background(), settle, &FuturesPriceTriggeredOrderParam{ + Initial: FuturesInitial{ + Price: 1234., + Size: 1, + Contract: futuresTradablePair, + }, + Trigger: FuturesTrigger{ + Rule: 1, + }, + }); err != nil { + t.Errorf("%s CreatePriceTriggeredFuturesOrder() error %v", g.Name, err) + } +} + +func TestListAllFuturesAutoOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.ListAllFuturesAutoOrders(context.Background(), "open", settleBTC, currency.EMPTYPAIR, 0, 0); err != nil { + t.Errorf("%s ListAllFuturesAutoOrders() error %v", g.Name, err) + } +} + +func TestCancelAllFuturesOpenOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(futuresTradablePair, false) + if err != nil { + t.Fatal(err) + } + if _, err := g.CancelAllFuturesOpenOrders(context.Background(), settle, futuresTradablePair); err != nil { + t.Errorf("%s CancelAllFuturesOpenOrders() error %v", g.Name, err) + } +} + +func TestGetAllDeliveryContracts(t *testing.T) { + t.Parallel() + if _, err := g.GetAllDeliveryContracts(context.Background(), settleUSDT); err != nil { + t.Errorf("%s GetAllDeliveryContracts() error %v", g.Name, err) + } +} + +func TestGetSingleDeliveryContracts(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(deliveryFuturesTradablePair, false) + if err != nil { + t.Skip(err) + } + if _, err := g.GetSingleDeliveryContracts(context.Background(), settle, deliveryFuturesTradablePair); err != nil { + t.Errorf("%s GetSingleDeliveryContracts() error %v", g.Name, err) + } +} + +func TestGetDeliveryOrderbook(t *testing.T) { + t.Parallel() + if _, err := g.GetDeliveryOrderbook(context.Background(), settleUSDT, "0", deliveryFuturesTradablePair, 0, false); err != nil { + t.Errorf("%s GetDeliveryOrderbook() error %v", g.Name, err) + } +} + +func TestGetDeliveryTradingHistory(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(deliveryFuturesTradablePair, false) + if err != nil { + t.Skip(err) + } + if _, err := g.GetDeliveryTradingHistory(context.Background(), settle, "", deliveryFuturesTradablePair, 0, time.Time{}, time.Time{}); err != nil { + t.Errorf("%s GetDeliveryTradingHistory() error %v", g.Name, err) + } +} +func TestGetDeliveryFuturesCandlesticks(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(deliveryFuturesTradablePair, false) + if err != nil { + t.Skip(err) + } + if _, err := g.GetDeliveryFuturesCandlesticks(context.Background(), settle, deliveryFuturesTradablePair, time.Time{}, time.Time{}, 0, kline.OneWeek); err != nil { + t.Errorf("%s GetFuturesCandlesticks() error %v", g.Name, err) + } +} + +func TestGetDeliveryFutureTickers(t *testing.T) { + t.Parallel() + settle, err := g.getSettlementFromCurrency(deliveryFuturesTradablePair, false) + if err != nil { + t.Skip(err) + } + if _, err := g.GetDeliveryFutureTickers(context.Background(), settle, deliveryFuturesTradablePair); err != nil { + t.Errorf("%s GetDeliveryFutureTickers() error %v", g.Name, err) + } +} + +func TestGetDeliveryInsuranceBalanceHistory(t *testing.T) { + t.Parallel() + if _, err := g.GetDeliveryInsuranceBalanceHistory(context.Background(), settleBTC, 0); err != nil { + t.Errorf("%s GetDeliveryInsuranceBalanceHistory() error %v", g.Name, err) + } +} + +func TestQueryDeliveryFuturesAccounts(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetDeliveryFuturesAccounts(context.Background(), settleUSDT); err != nil { + t.Errorf("%s QueryDeliveryFuturesAccounts() error %v", g.Name, err) + } +} +func TestGetDeliveryAccountBooks(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetDeliveryAccountBooks(context.Background(), settleUSDT, 0, time.Time{}, time.Now(), "dnw"); err != nil { + t.Errorf("%s GetDeliveryAccountBooks() error %v", g.Name, err) + } +} + +func TestGetAllDeliveryPositionsOfUser(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetAllDeliveryPositionsOfUser(context.Background(), settleUSDT); err != nil { + t.Errorf("%s GetAllDeliveryPositionsOfUser() error %v", g.Name, err) + } +} + +func TestGetSingleDeliveryPosition(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSingleDeliveryPosition(context.Background(), settleUSDT, deliveryFuturesTradablePair); err != nil { + t.Errorf("%s GetSingleDeliveryPosition() error %v", g.Name, err) + } +} + +func TestUpdateDeliveryPositionMargin(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + settle, err := g.getSettlementFromCurrency(deliveryFuturesTradablePair, false) + if err != nil { + t.Fatal(err) + } + if _, err := g.UpdateDeliveryPositionMargin(context.Background(), settle, 0.001, deliveryFuturesTradablePair); err != nil { + t.Errorf("%s UpdateDeliveryPositionMargin() error %v", g.Name, err) + } +} + +func TestUpdateDeliveryPositionLeverage(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.UpdateDeliveryPositionLeverage(context.Background(), "usdt", deliveryFuturesTradablePair, 0.001); err != nil { + t.Errorf("%s UpdateDeliveryPositionLeverage() error %v", g.Name, err) + } +} + +func TestUpdateDeliveryPositionRiskLimit(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.UpdateDeliveryPositionRiskLimit(context.Background(), "usdt", deliveryFuturesTradablePair, 30); err != nil { + t.Errorf("%s UpdateDeliveryPositionRiskLimit() error %v", g.Name, err) + } +} + +func TestGetAllOptionsUnderlyings(t *testing.T) { + t.Parallel() + if _, err := g.GetAllOptionsUnderlyings(context.Background()); err != nil { + t.Errorf("%s GetAllOptionsUnderlyings() error %v", g.Name, err) + } +} + +func TestGetExpirationTime(t *testing.T) { + t.Parallel() + if _, err := g.GetExpirationTime(context.Background(), "BTC_USDT"); err != nil { + t.Errorf("%s GetExpirationTime() error %v", g.Name, err) + } +} + +func TestGetAllContractOfUnderlyingWithinExpiryDate(t *testing.T) { + t.Parallel() + if _, err := g.GetAllContractOfUnderlyingWithinExpiryDate(context.Background(), "BTC_USDT", time.Time{}); err != nil { + t.Errorf("%s GetAllContractOfUnderlyingWithinExpiryDate() error %v", g.Name, err) + } +} + +func TestGetOptionsSpecifiedContractDetail(t *testing.T) { + t.Parallel() + if _, err := g.GetOptionsSpecifiedContractDetail(context.Background(), optionsTradablePair); err != nil { + t.Errorf("%s GetOptionsSpecifiedContractDetail() error %v", g.Name, err) + } +} + +func TestGetSettlementHistory(t *testing.T) { + t.Parallel() + if _, err := g.GetSettlementHistory(context.Background(), "BTC_USDT", 0, 0, time.Time{}, time.Time{}); err != nil { + t.Errorf("%s GetSettlementHistory() error %v", g.Name, err) + } +} + +func TestGetOptionsSpecifiedSettlementHistory(t *testing.T) { + t.Parallel() + underlying := "BTC_USDT" + optionsSettlement, err := g.GetSettlementHistory(context.Background(), underlying, 0, 1, time.Time{}, time.Time{}) + if err != nil { + t.Fatal(err) + } + cp, err := currency.NewPairFromString(optionsSettlement[0].Contract) + if err != nil { + t.Fatal(err) + } + if _, err := g.GetOptionsSpecifiedContractsSettlement(context.Background(), cp, underlying, optionsSettlement[0].Timestamp.Time().Unix()); err != nil { + t.Errorf("%s GetOptionsSpecifiedContractsSettlement() error %s", g.Name, err) + } +} + +func TestGetSupportedFlashSwapCurrencies(t *testing.T) { + t.Parallel() + if _, err := g.GetSupportedFlashSwapCurrencies(context.Background()); err != nil { + t.Errorf("%s GetSupportedFlashSwapCurrencies() error %v", g.Name, err) + } +} + +const flashSwapOrderResponseJSON = `{"id": 54646, "create_time": 1651116876378, "update_time": 1651116876378, "user_id": 11135567, "sell_currency": "BTC", "sell_amount": "0.01", "buy_currency": "USDT", "buy_amount": "10", "price": "100", "status": 1}` + +func TestCreateFlashSwapOrder(t *testing.T) { + t.Parallel() + var response FlashSwapOrderResponse + if err := json.Unmarshal([]byte(flashSwapOrderResponseJSON), &response); err != nil { + t.Errorf("%s error while deserializing to FlashSwapOrderResponse %v", g.Name, err) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CreateFlashSwapOrder(context.Background(), FlashSwapOrderParams{ + PreviewID: "1234", + SellCurrency: currency.USDT, + BuyCurrency: currency.BTC, + BuyAmount: 34234, + SellAmount: 34234, + }); err != nil { + t.Errorf("%s CreateFlashSwapOrder() error %v", g.Name, err) + } +} + +func TestGetAllFlashSwapOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetAllFlashSwapOrders(context.Background(), 1, currency.EMPTYCODE, currency.EMPTYCODE, true, 0, 0); err != nil { + t.Errorf("%s GetAllFlashSwapOrders() error %v", g.Name, err) + } +} + +func TestGetSingleFlashSwapOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSingleFlashSwapOrder(context.Background(), "1234"); err != nil { + t.Errorf("%s GetSingleFlashSwapOrder() error %v", g.Name, err) + } +} + +func TestInitiateFlashSwapOrderReview(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.InitiateFlashSwapOrderReview(context.Background(), FlashSwapOrderParams{ + PreviewID: "1234", + SellCurrency: currency.USDT, + BuyCurrency: currency.BTC, + SellAmount: 100, + }); err != nil { + t.Errorf("%s InitiateFlashSwapOrderReview() error %v", g.Name, err) + } +} + +func TestGetMyOptionsSettlements(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetMyOptionsSettlements(context.Background(), "BTC_USDT", currency.EMPTYPAIR, 0, 0, time.Time{}); err != nil { + t.Errorf("%s GetMyOptionsSettlements() error %v", g.Name, err) + } +} + +func TestGetOptionAccounts(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetOptionAccounts(context.Background()); err != nil { + t.Errorf("%s GetOptionAccounts() error %v", g.Name, err) + } +} + +func TestGetAccountChangingHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetAccountChangingHistory(context.Background(), 0, 0, time.Time{}, time.Time{}, ""); err != nil { + t.Errorf("%s GetAccountChangingHistory() error %v", g.Name, err) + } +} + +func TestGetUsersPositionSpecifiedUnderlying(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetUsersPositionSpecifiedUnderlying(context.Background(), ""); err != nil { + t.Errorf("%s GetUsersPositionSpecifiedUnderlying() error %v", g.Name, err) + } +} + +func TestGetSpecifiedContractPosition(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + _, err := g.GetSpecifiedContractPosition(context.Background(), currency.EMPTYPAIR) + if err != nil && !errors.Is(err, errInvalidOrMissingContractParam) { + t.Errorf("%s GetSpecifiedContractPosition() error expecting %v, but found %v", g.Name, errInvalidOrMissingContractParam, err) + } + _, err = g.GetSpecifiedContractPosition(context.Background(), optionsTradablePair) + if err != nil { + t.Errorf("%s GetSpecifiedContractPosition() error expecting %v, but found %v", g.Name, errInvalidOrMissingContractParam, err) + } +} + +func TestGetUsersLiquidationHistoryForSpecifiedUnderlying(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetUsersLiquidationHistoryForSpecifiedUnderlying(context.Background(), "BTC_USDT", currency.EMPTYPAIR); err != nil { + t.Errorf("%s GetUsersLiquidationHistoryForSpecifiedUnderlying() error %v", g.Name, err) + } +} + +func TestPlaceOptionOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + _, err := g.PlaceOptionOrder(context.Background(), OptionOrderParam{ + Contract: optionsTradablePair.String(), + OrderSize: -1, + Iceberg: 0, + Text: "-", + TimeInForce: "gtc", + Price: 100, + }) + if err != nil { + t.Errorf("%s PlaceOptionOrder() error %v", g.Name, err) + } +} + +func TestGetOptionFuturesOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetOptionFuturesOrders(context.Background(), currency.EMPTYPAIR, "", "", 0, 0, time.Time{}, time.Time{}); err != nil { + t.Errorf("%s GetOptionFuturesOrders() error %v", g.Name, err) + } +} + +func TestCancelOptionOpenOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelMultipleOptionOpenOrders(context.Background(), optionsTradablePair, "", ""); err != nil { + t.Errorf("%s CancelOptionOpenOrders() error %v", g.Name, err) + } +} +func TestGetSingleOptionOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSingleOptionOrder(context.Background(), ""); err != nil && !errors.Is(errInvalidOrderID, err) { + t.Errorf("%s GetSingleOptionorder() expecting %v, but found %v", g.Name, errInvalidOrderID, err) + } + if _, err := g.GetSingleOptionOrder(context.Background(), "1234"); err != nil { + t.Errorf("%s GetSingleOptionOrder() error %v", g.Name, err) + } +} + +func TestCancelSingleOrder(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelOptionSingleOrder(context.Background(), "1234"); err != nil { + t.Errorf("%s CancelSingleOrder() error %v", g.Name, err) + } +} + +func TestGetOptionsPersonalTradingHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetOptionsPersonalTradingHistory(context.Background(), "BTC_USDT", currency.EMPTYPAIR, 0, 0, time.Time{}, time.Time{}); err != nil { + t.Errorf("%s GetOptionPersonalTradingHistory() error %v", g.Name, err) + } +} + +func TestWithdrawCurrency(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + _, err := g.WithdrawCurrency(context.Background(), WithdrawalRequestParam{}) + if err != nil && !errors.Is(err, errInvalidAmount) { + t.Errorf("%s WithdrawCurrency() expecting error %v, but found %v", g.Name, errInvalidAmount, err) + } + _, err = g.WithdrawCurrency(context.Background(), WithdrawalRequestParam{ + Currency: currency.BTC, + Amount: 0.00000001, + Chain: "BTC", + Address: core.BitcoinDonationAddress, + }) + if err != nil { + t.Errorf("%s WithdrawCurrency() expecting error %v, but found %v", g.Name, errInvalidAmount, err) + } +} + +func TestCancelWithdrawalWithSpecifiedID(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CancelWithdrawalWithSpecifiedID(context.Background(), "1234567"); err != nil { + t.Errorf("%s CancelWithdrawalWithSpecifiedID() error %v", g.Name, err) + } +} + +func TestGetOptionsOrderbook(t *testing.T) { + t.Parallel() + if _, err := g.GetOptionsOrderbook(context.Background(), optionsTradablePair, "0.1", 9, true); err != nil { + t.Errorf("%s GetOptionsFuturesOrderbooks() error %v", g.Name, err) + } +} + +func TestGetOptionsTickers(t *testing.T) { + t.Parallel() + if _, err := g.GetOptionsTickers(context.Background(), "BTC_USDT"); err != nil { + t.Errorf("%s GetOptionsTickers() error %v", g.Name, err) + } +} + +func TestGetOptionUnderlyingTickers(t *testing.T) { + t.Parallel() + if _, err := g.GetOptionUnderlyingTickers(context.Background(), "BTC_USDT"); err != nil { + t.Errorf("%s GetOptionUnderlyingTickers() error %v", g.Name, err) + } +} + +func TestGetOptionFuturesCandlesticks(t *testing.T) { + t.Parallel() + if _, err := g.GetOptionFuturesCandlesticks(context.Background(), optionsTradablePair, 0, time.Now().Add(-time.Hour*10), time.Time{}, kline.ThirtyMin); err != nil { + t.Error(err) + } +} + +func TestGetOptionFuturesMarkPriceCandlesticks(t *testing.T) { + t.Parallel() + if _, err := g.GetOptionFuturesMarkPriceCandlesticks(context.Background(), "BTC_USDT", 0, time.Time{}, time.Time{}, kline.OneMonth); err != nil { + t.Errorf("%s GetOptionFuturesMarkPriceCandlesticks() error %v", g.Name, err) + } +} + +func TestGetOptionsTradeHistory(t *testing.T) { + t.Parallel() + if _, err := g.GetOptionsTradeHistory(context.Background(), optionsTradablePair, "C", 0, 0, time.Time{}, time.Time{}); err != nil { + t.Errorf("%s GetOptionsTradeHistory() error %v", g.Name, err) + } +} + +// Sub-account endpoints + +func TestCreateNewSubAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CreateNewSubAccount(context.Background(), SubAccountParams{ + LoginName: "Sub_Account_for_testing", + }); err != nil { + t.Errorf("%s CreateNewSubAccount() error %v", g.Name, err) + } +} + +func TestGetSubAccounts(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSubAccounts(context.Background()); err != nil { + t.Errorf("%s GetSubAccounts() error %v", g.Name, err) + } +} + +func TestGetSingleSubAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if _, err := g.GetSingleSubAccount(context.Background(), "123423"); err != nil { + t.Errorf("%s GetSingleSubAccount() error %v", g.Name, err) + } +} + +// Wrapper test functions + +func TestFetchTradablePairs(t *testing.T) { + t.Parallel() + _, err := g.FetchTradablePairs(context.Background(), asset.DeliveryFutures) + if err != nil { + t.Errorf("%s FetchTradablePairs() error %v", g.Name, err) + } + if _, err = g.FetchTradablePairs(context.Background(), asset.Options); err != nil { + t.Errorf("%s FetchTradablePairs() error %v", g.Name, err) + } + _, err = g.FetchTradablePairs(context.Background(), asset.Futures) + if err != nil { + t.Errorf("%s FetchTradablePairs() error %v", g.Name, err) + } + if _, err = g.FetchTradablePairs(context.Background(), asset.Margin); err != nil { + t.Errorf("%s FetchTradablePairs() error %v", g.Name, err) + } + _, err = g.FetchTradablePairs(context.Background(), asset.CrossMargin) + if err != nil { + t.Errorf("%s FetchTradablePairs() error %v", g.Name, err) + } + _, err = g.FetchTradablePairs(context.Background(), asset.Spot) + if err != nil { + t.Errorf("%s FetchTradablePairs() error %v", g.Name, err) + } +} + +func TestUpdateTradablePairs(t *testing.T) { + t.Parallel() + if err := g.UpdateTradablePairs(context.Background(), true); err != nil { + t.Errorf("%s UpdateTradablePairs() error %v", g.Name, err) + } +} func TestUpdateTickers(t *testing.T) { - err := g.UpdateTickers(context.Background(), asset.Spot) - if err != nil { - t.Error(err) + t.Parallel() + if err := g.UpdateTickers(context.Background(), asset.DeliveryFutures); err != nil { + t.Errorf("%s UpdateTickers() error %v", g.Name, err) + } + if err := g.UpdateTickers(context.Background(), asset.Futures); err != nil { + t.Errorf("%s UpdateTickers() error %v", g.Name, err) + } + if err := g.UpdateTickers(context.Background(), asset.Spot); err != nil { + t.Errorf("%s UpdateTickers() error %v", g.Name, err) + } + if err := g.UpdateTickers(context.Background(), asset.Options); err != nil { + t.Errorf("%s UpdateTickers() error %v", g.Name, err) + } + if err := g.UpdateTickers(context.Background(), asset.CrossMargin); err != nil { + t.Errorf("%s UpdateTickers() error %v", g.Name, err) + } + if err := g.UpdateTickers(context.Background(), asset.Margin); err != nil { + t.Errorf("%s UpdateTickers() error %v", g.Name, err) } } -func TestGetCryptoDepositAddress(t *testing.T) { +func TestUpdateOrderbook(t *testing.T) { + t.Parallel() + _, err := g.UpdateOrderbook(context.Background(), spotTradablePair, asset.Spot) + if err != nil { + t.Errorf("%s UpdateOrderbook() error %v", g.Name, err) + } + _, err = g.UpdateOrderbook(context.Background(), marginTradablePair, asset.Margin) + if err != nil { + t.Errorf("%s UpdateOrderbook() error %v", g.Name, err) + } + _, err = g.UpdateOrderbook(context.Background(), crossMarginTradablePair, asset.CrossMargin) + if err != nil { + t.Errorf("%s UpdateOrderbook() error %v", g.Name, err) + } + _, err = g.UpdateOrderbook(context.Background(), futuresTradablePair, asset.Futures) + if err != nil { + t.Errorf("%s UpdateOrderbook() error %v", g.Name, err) + } + if _, err = g.UpdateOrderbook(context.Background(), deliveryFuturesTradablePair, asset.DeliveryFutures); err != nil { + t.Errorf("%s UpdateOrderbook() error %v", g.Name, err) + } + if _, err = g.UpdateOrderbook(context.Background(), optionsTradablePair, asset.Options); err != nil { + t.Errorf("%s UpdateOrderbook() error %v", g.Name, err) + } +} + +func TestGetWithdrawalsHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, g) - - _, err := g.GetCryptoDepositAddress(context.Background(), currency.USDT.String()) + if _, err := g.GetWithdrawalsHistory(context.Background(), currency.BTC, asset.Empty); err != nil { + t.Errorf("%s GetWithdrawalsHistory() error %v", g.Name, err) + } +} +func TestGetRecentTrades(t *testing.T) { + t.Parallel() + _, err := g.GetRecentTrades(context.Background(), spotTradablePair, asset.Spot) + if err != nil { + t.Error(err) + } + _, err = g.GetRecentTrades(context.Background(), marginTradablePair, asset.Margin) + if err != nil { + t.Error(err) + } + _, err = g.GetRecentTrades(context.Background(), crossMarginTradablePair, asset.CrossMargin) + if err != nil { + t.Error(err) + } + _, err = g.GetRecentTrades(context.Background(), deliveryFuturesTradablePair, asset.DeliveryFutures) + if err != nil { + t.Error(err) + } + _, err = g.GetRecentTrades(context.Background(), futuresTradablePair, asset.Futures) + if err != nil { + t.Error(err) + } + _, err = g.GetRecentTrades(context.Background(), optionsTradablePair, asset.Options) if err != nil { t.Error(err) } } +func TestSubmitOrder(t *testing.T) { + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + _, err := g.SubmitOrder(context.Background(), &order.Submit{ + Exchange: g.Name, + Pair: crossMarginTradablePair, + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + AssetType: asset.CrossMargin, + }) + if err != nil { + t.Errorf("Order failed to be placed: %v", err) + } + _, err = g.SubmitOrder(context.Background(), &order.Submit{ + Exchange: g.Name, + Pair: spotTradablePair, + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + AssetType: asset.Spot, + }) + if err != nil { + t.Errorf("Order failed to be placed: %v", err) + } + _, err = g.SubmitOrder(context.Background(), &order.Submit{ + Exchange: g.Name, + Pair: optionsTradablePair, + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + AssetType: asset.Options, + }) + if err != nil { + t.Errorf("Order failed to be placed: %v", err) + } + _, err = g.SubmitOrder(context.Background(), &order.Submit{ + Exchange: g.Name, + Pair: deliveryFuturesTradablePair, + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + AssetType: asset.DeliveryFutures, + }) + if err != nil { + t.Errorf("Order failed to be placed: %v", err) + } + _, err = g.SubmitOrder(context.Background(), &order.Submit{ + Exchange: g.Name, + Pair: futuresTradablePair, + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + AssetType: asset.Futures, + }) + if err != nil { + t.Errorf("Order failed to be placed: %v", err) + } + _, err = g.SubmitOrder(context.Background(), &order.Submit{ + Exchange: g.Name, + Pair: marginTradablePair, + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + AssetType: asset.Margin, + }) + if err != nil { + t.Errorf("Order failed to be placed: %v", err) + } +} + +func TestCancelExchangeOrder(t *testing.T) { + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + var orderCancellation = &order.Cancel{ + OrderID: "1", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: currency.NewPair(currency.LTC, currency.BTC), + AssetType: asset.Spot, + } + err := g.CancelOrder(context.Background(), orderCancellation) + if err != nil { + t.Errorf("%s CancelOrder error: %v", g.Name, err) + } + orderCancellation.AssetType = asset.Margin + err = g.CancelOrder(context.Background(), orderCancellation) + if err != nil { + t.Errorf("%s CancelOrder error: %v", g.Name, err) + } + orderCancellation.AssetType = asset.CrossMargin + err = g.CancelOrder(context.Background(), orderCancellation) + if err != nil { + t.Errorf("%s CancelOrder error: %v", g.Name, err) + } + orderCancellation.AssetType = asset.Options + err = g.CancelOrder(context.Background(), orderCancellation) + if err != nil { + t.Errorf("%s CancelOrder error: %v", g.Name, err) + } + orderCancellation.AssetType = asset.Futures + err = g.CancelOrder(context.Background(), orderCancellation) + if err != nil { + t.Errorf("%s CancelOrder error: %v", g.Name, err) + } + orderCancellation.AssetType = asset.DeliveryFutures + err = g.CancelOrder(context.Background(), orderCancellation) + if err != nil { + t.Errorf("%s CancelOrder error: %v", g.Name, err) + } +} + +func TestCancelBatchOrders(t *testing.T) { + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + _, err := g.CancelBatchOrders(context.Background(), []order.Cancel{ + { + OrderID: "1", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: spotTradablePair, + AssetType: asset.Spot, + }, { + OrderID: "2", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: spotTradablePair, + AssetType: asset.Spot, + }}) + if err != nil { + t.Errorf("%s CancelOrder error: %v", g.Name, err) + } + _, err = g.CancelBatchOrders(context.Background(), []order.Cancel{ + { + OrderID: "1", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: futuresTradablePair, + AssetType: asset.Futures, + }, { + OrderID: "2", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: futuresTradablePair, + AssetType: asset.Futures, + }}) + if err != nil { + t.Errorf("%s CancelOrder error: %v", g.Name, err) + } + _, err = g.CancelBatchOrders(context.Background(), []order.Cancel{ + { + OrderID: "1", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: deliveryFuturesTradablePair, + AssetType: asset.DeliveryFutures, + }, { + OrderID: "2", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: deliveryFuturesTradablePair, + AssetType: asset.DeliveryFutures, + }}) + if err != nil { + t.Errorf("%s CancelOrder error: %v", g.Name, err) + } + _, err = g.CancelBatchOrders(context.Background(), []order.Cancel{ + { + OrderID: "1", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: optionsTradablePair, + AssetType: asset.Options, + }, { + OrderID: "2", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: optionsTradablePair, + AssetType: asset.Options, + }}) + if err != nil { + t.Errorf("%s CancelOrder error: %v", g.Name, err) + } + _, err = g.CancelBatchOrders(context.Background(), []order.Cancel{ + { + OrderID: "1", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: marginTradablePair, + AssetType: asset.Margin, + }, { + OrderID: "2", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Pair: marginTradablePair, + AssetType: asset.Margin, + }}) + if err != nil { + t.Errorf("%s CancelOrder error: %v", g.Name, err) + } +} + +func TestGetDepositAddress(t *testing.T) { + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + chains, err := g.GetAvailableTransferChains(context.Background(), currency.BTC) + if err != nil { + t.Fatal(err) + } + for i := range chains { + _, err = g.GetDepositAddress(context.Background(), currency.BTC, "", chains[i]) + if err != nil { + t.Error("Test Fail - GetDepositAddress error", err) + } + } +} + +func TestGetActiveOrders(t *testing.T) { + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + enabledPairs, err := g.GetEnabledPairs(asset.Spot) + if err != nil { + t.Error(err) + } + _, err = g.GetActiveOrders(context.Background(), &order.GetOrdersRequest{ + Pairs: enabledPairs[:2], + Type: order.AnyType, + Side: order.AnySide, + AssetType: asset.Spot, + }) + if err != nil { + t.Errorf(" %s GetActiveOrders() error: %v", g.Name, err) + } + cp, err := currency.NewPairFromString("BTC_USDT") + if err != nil { + t.Error(err) + } + _, err = g.GetActiveOrders(context.Background(), &order.GetOrdersRequest{ + Pairs: []currency.Pair{cp}, + Type: order.AnyType, + Side: order.AnySide, + AssetType: asset.Futures, + }) + if err != nil { + t.Errorf(" %s GetActiveOrders() error: %v", g.Name, err) + } + _, err = g.GetActiveOrders(context.Background(), &order.GetOrdersRequest{ + Pairs: enabledPairs[:2], + Type: order.AnyType, + Side: order.AnySide, + AssetType: asset.Margin, + }) + if err != nil { + t.Errorf(" %s GetActiveOrders() error: %v", g.Name, err) + } + _, err = g.GetActiveOrders(context.Background(), &order.GetOrdersRequest{ + Pairs: enabledPairs[:2], + Type: order.AnyType, + Side: order.AnySide, + AssetType: asset.CrossMargin, + }) + if err != nil { + t.Errorf(" %s GetActiveOrders() error: %v", g.Name, err) + } + _, err = g.GetActiveOrders(context.Background(), &order.GetOrdersRequest{ + Pairs: currency.Pairs{futuresTradablePair}, + Type: order.AnyType, + Side: order.AnySide, + AssetType: asset.Futures, + }) + if err != nil { + t.Errorf(" %s GetActiveOrders() error: %v", g.Name, err) + } + _, err = g.GetActiveOrders(context.Background(), &order.GetOrdersRequest{ + Pairs: currency.Pairs{deliveryFuturesTradablePair}, + Type: order.AnyType, + Side: order.AnySide, + AssetType: asset.DeliveryFutures, + }) + if err != nil { + t.Errorf(" %s GetActiveOrders() error: %v", g.Name, err) + } + _, err = g.GetActiveOrders(context.Background(), &order.GetOrdersRequest{ + Pairs: currency.Pairs{optionsTradablePair}, + Type: order.AnyType, + Side: order.AnySide, + AssetType: asset.Options, + }) + if err != nil { + t.Errorf(" %s GetActiveOrders() error: %v", g.Name, err) + } + if _, err = g.GetActiveOrders(context.Background(), &order.GetOrdersRequest{ + Pairs: currency.Pairs{}, + Type: order.AnyType, + Side: order.AnySide, + AssetType: asset.DeliveryFutures, + }); !errors.Is(err, currency.ErrCurrencyPairsEmpty) { + t.Errorf("%s GetActiveOrders() expecting error %v, but found %v", g.Name, currency.ErrCurrencyPairsEmpty, err) + } +} + +func TestGetOrderHistory(t *testing.T) { + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + var getOrdersRequest = order.GetOrdersRequest{ + Type: order.AnyType, + AssetType: asset.Spot, + Side: order.Buy, + } + enabledPairs, err := g.GetEnabledPairs(asset.Spot) + if err != nil { + t.Fatal(err) + } + getOrdersRequest.Pairs = enabledPairs[:3] + _, err = g.GetOrderHistory(context.Background(), &getOrdersRequest) + if err != nil { + t.Errorf("%s GetOrderhistory() error: %v", g.Name, err) + } + getOrdersRequest.AssetType = asset.Futures + getOrdersRequest.Pairs, err = g.GetEnabledPairs(asset.Futures) + if err != nil { + t.Fatal(err) + } + getOrdersRequest.Pairs = getOrdersRequest.Pairs[len(getOrdersRequest.Pairs)-4:] + _, err = g.GetOrderHistory(context.Background(), &getOrdersRequest) + if err != nil { + t.Errorf("%s GetOrderhistory() error: %v", g.Name, err) + } + getOrdersRequest.AssetType = asset.DeliveryFutures + getOrdersRequest.Pairs, err = g.GetEnabledPairs(asset.DeliveryFutures) + if err != nil { + t.Fatal(err) + } + _, err = g.GetOrderHistory(context.Background(), &getOrdersRequest) + if err != nil { + t.Errorf("%s GetOrderhistory() error: %v", g.Name, err) + } + getOrdersRequest.AssetType = asset.Options + getOrdersRequest.Pairs, err = g.GetEnabledPairs(asset.Options) + if err != nil { + t.Fatal(err) + } + _, err = g.GetOrderHistory(context.Background(), &getOrdersRequest) + if err != nil { + t.Errorf("%s GetOrderhistory() error: %v", g.Name, err) + } +} + +func TestGetHistoricCandles(t *testing.T) { + t.Parallel() + startTime := time.Now().Add(-time.Hour * 10) + if _, err := g.GetHistoricCandles(context.Background(), spotTradablePair, asset.Spot, kline.OneDay, startTime, time.Now()); err != nil { + t.Errorf("%s GetHistoricCandles() error: %v", g.Name, err) + } + if _, err := g.GetHistoricCandles(context.Background(), marginTradablePair, asset.Margin, kline.OneDay, startTime, time.Now()); err != nil { + t.Errorf("%s GetHistoricCandles() error: %v", g.Name, err) + } + if _, err := g.GetHistoricCandles(context.Background(), crossMarginTradablePair, asset.CrossMargin, kline.OneDay, startTime, time.Now()); err != nil { + t.Errorf("%s GetHistoricCandles() error: %v", g.Name, err) + } + if _, err := g.GetHistoricCandles(context.Background(), futuresTradablePair, asset.Futures, kline.OneDay, startTime, time.Now()); err != nil { + t.Errorf("%s GetHistoricCandles() error: %v", g.Name, err) + } + if _, err := g.GetHistoricCandles(context.Background(), deliveryFuturesTradablePair, asset.DeliveryFutures, kline.OneDay, startTime, time.Now()); err != nil { + t.Errorf("%s GetHistoricCandles() error: %v", g.Name, err) + } + if _, err := g.GetHistoricCandles(context.Background(), optionsTradablePair, asset.Options, kline.OneDay, startTime, time.Now()); !errors.Is(err, common.ErrNotYetImplemented) { + t.Errorf("%s GetHistoricCandles() expecting: %v, but found %v", g.Name, common.ErrNotYetImplemented, err) + } + if _, err := g.GetHistoricCandles(context.Background(), optionsTradablePair, asset.Options, kline.OneDay, startTime, time.Now()); !errors.Is(err, common.ErrNotYetImplemented) { + t.Errorf("%s GetHistoricCandles() expecting: %v, but found %v", g.Name, common.ErrNotYetImplemented, err) + } +} + +func TestGetHistoricCandlesExtended(t *testing.T) { + t.Parallel() + startTime := time.Now().Add(-time.Hour * 5) + _, err := g.GetHistoricCandlesExtended(context.Background(), + spotTradablePair, asset.Spot, kline.OneMin, startTime, time.Now()) + if err != nil { + t.Fatal(err) + } + _, err = g.GetHistoricCandlesExtended(context.Background(), + marginTradablePair, asset.Margin, kline.OneMin, startTime, time.Now()) + if err != nil { + t.Fatal(err) + } + _, err = g.GetHistoricCandlesExtended(context.Background(), + deliveryFuturesTradablePair, asset.DeliveryFutures, kline.OneMin, time.Now().Add(-time.Hour*5), time.Now()) + if err != nil { + t.Error(err) + } + _, err = g.GetHistoricCandlesExtended(context.Background(), futuresTradablePair, asset.Futures, kline.OneMin, startTime, time.Now()) + if err != nil { + t.Error(err) + } + _, err = g.GetHistoricCandlesExtended(context.Background(), + crossMarginTradablePair, asset.CrossMargin, kline.OneMin, startTime, time.Now()) + if err != nil { + t.Error(err) + } + if _, err := g.GetHistoricCandlesExtended(context.Background(), optionsTradablePair, asset.Options, kline.OneDay, startTime, time.Now()); !errors.Is(err, common.ErrNotYetImplemented) { + t.Errorf("%s GetHistoricCandlesExtended() expecting: %v, but found %v", g.Name, common.ErrNotYetImplemented, err) + } +} func TestGetAvailableTransferTrains(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, g) - _, err := g.GetAvailableTransferChains(context.Background(), currency.USDT) if err != nil { t.Error(err) } } + +func TestGetUnderlyingFromCurrencyPair(t *testing.T) { + t.Parallel() + if uly, err := g.GetUnderlyingFromCurrencyPair(currency.Pair{Delimiter: currency.UnderscoreDelimiter, Base: currency.BTC, Quote: currency.NewCode("USDT_LLK")}); err != nil { + t.Error(err) + } else if !uly.Equal(currency.NewPair(currency.BTC, currency.USDT)) { + t.Error("unexpected underlying") + } +} + +const wsTickerPushDataJSON = `{"time": 1606291803, "channel": "spot.tickers", "event": "update", "result": { "currency_pair": "BTC_USDT", "last": "19106.55", "lowest_ask": "19108.71", "highest_bid": "19106.55", "change_percentage": "3.66", "base_volume": "2811.3042155865", "quote_volume": "53441606.52411221454674732293", "high_24h": "19417.74", "low_24h": "18434.21" }}` + +func TestWsTickerPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleData([]byte(wsTickerPushDataJSON)); err != nil { + t.Errorf("%s websocket ticker push data error: %v", g.Name, err) + } +} + +const wsTradePushDataJSON = `{ "time": 1606292218, "channel": "spot.trades", "event": "update", "result": { "id": 309143071, "create_time": 1606292218, "create_time_ms": "1606292218213.4578", "side": "sell", "currency_pair": "GT_USDT", "amount": "16.4700000000", "price": "0.4705000000"}}` + +func TestWsTradePushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleData([]byte(wsTradePushDataJSON)); err != nil { + t.Errorf("%s websocket trade push data error: %v", g.Name, err) + } +} + +const wsCandlestickPushDataJSON = `{"time": 1606292600, "channel": "spot.candlesticks", "event": "update", "result": { "t": "1606292580", "v": "2362.32035", "c": "19128.1", "h": "19128.1", "l": "19128.1", "o": "19128.1","n": "1m_BTC_USDT"}}` + +func TestWsCandlestickPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleData([]byte(wsCandlestickPushDataJSON)); err != nil { + t.Errorf("%s websocket candlestick push data error: %v", g.Name, err) + } +} + +const wsOrderbookTickerJSON = `{"time": 1606293275, "channel": "spot.book_ticker", "event": "update", "result": { "t": 1606293275123, "u": 48733182, "s": "BTC_USDT", "b": "19177.79", "B": "0.0003341504", "a": "19179.38", "A": "0.09" }}` + +func TestWsOrderbookTickerPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleData([]byte(wsOrderbookTickerJSON)); err != nil { + t.Errorf("%s websocket orderbook push data error: %v", g.Name, err) + } +} + +const ( + wsOrderbookUpdatePushDataJSON = `{"time": 1606294781, "channel": "spot.order_book_update", "event": "update", "result": { "t": 1606294781123, "e": "depthUpdate", "E": 1606294781,"s": "BTC_USDT","U": 48776301,"u": 48776306,"b": [["19137.74","0.0001"],["19088.37","0"]],"a": [["19137.75","0.6135"]] }}` + wsOrderbookSnapshotPushDataJSON = `{"time":1606295412,"channel": "spot.order_book", "event": "update", "result": { "t": 1606295412123, "lastUpdateId": 48791820, "s": "BTC_USDT", "bids": [ [ "19079.55", "0.0195" ], [ "19079.07", "0.7341"],["19076.23", "0.00011808" ], [ "19073.9", "0.105" ], [ "19068.83", "0.1009" ] ], "asks": [ [ "19080.24", "0.1638" ], [ "19080.91","0.1366"],["19080.92","0.01"],["19081.29","0.01"],["19083.8","0.097"]]}}` +) + +func TestWsOrderbookSnapshotPushData(t *testing.T) { + t.Parallel() + err := g.wsHandleData([]byte(wsOrderbookSnapshotPushDataJSON)) + if err != nil { + t.Errorf("%s websocket orderbook snapshot push data error: %v", g.Name, err) + } + if err = g.wsHandleData([]byte(wsOrderbookUpdatePushDataJSON)); err != nil { + t.Errorf("%s websocket orderbook update push data error: %v", g.Name, err) + } +} + +const wsSpotOrderPushDataJSON = `{"time": 1605175506, "channel": "spot.orders", "event": "update", "result": [ { "id": "30784435", "user": 123456, "text": "t-abc", "create_time": "1605175506", "create_time_ms": "1605175506123", "update_time": "1605175506", "update_time_ms": "1605175506123", "event": "put", "currency_pair": "BTC_USDT", "type": "limit", "account": "spot", "side": "sell", "amount": "1", "price": "10001", "time_in_force": "gtc", "left": "1", "filled_total": "0", "fee": "0", "fee_currency": "USDT", "point_fee": "0", "gt_fee": "0", "gt_discount": true, "rebated_fee": "0", "rebated_fee_currency": "USDT"} ]}` + +func TestWsPushOrders(t *testing.T) { + t.Parallel() + if err := g.wsHandleData([]byte(wsSpotOrderPushDataJSON)); err != nil { + t.Errorf("%s websocket orders push data error: %v", g.Name, err) + } +} + +const wsUserTradePushDataJSON = `{"time": 1605176741, "channel": "spot.usertrades", "event": "update", "result": [ { "id": 5736713, "user_id": 1000001, "order_id": "30784428", "currency_pair": "BTC_USDT", "create_time": 1605176741, "create_time_ms": "1605176741123.456", "side": "sell", "amount": "1.00000000", "role": "taker", "price": "10000.00000000", "fee": "0.00200000000000", "point_fee": "0", "gt_fee": "0", "text": "apiv4" } ]}` + +func TestWsUserTradesPushDataJSON(t *testing.T) { + t.Parallel() + if err := g.wsHandleData([]byte(wsUserTradePushDataJSON)); err != nil { + t.Errorf("%s websocket users trade push data error: %v", g.Name, err) + } +} + +const wsBalancesPushDataJSON = `{"time": 1605248616, "channel": "spot.balances", "event": "update", "result": [ { "timestamp": "1605248616", "timestamp_ms": "1605248616123", "user": "1000001", "currency": "USDT", "change": "100", "total": "1032951.325075926", "available": "1022943.325075926"} ]}` + +func TestBalancesPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleData([]byte(wsBalancesPushDataJSON)); err != nil { + t.Errorf("%s websocket balances push data error: %v", g.Name, err) + } +} + +const wsMarginBalancePushDataJSON = `{"time": 1605248616, "channel": "spot.funding_balances", "event": "update", "result": [ {"timestamp": "1605248616","timestamp_ms": "1605248616123","user": "1000001","currency": "USDT","change": "100","freeze": "100","lent": "0"} ]}` + +func TestMarginBalancePushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleData([]byte(wsMarginBalancePushDataJSON)); err != nil { + t.Errorf("%s websocket margin balance push data error: %v", g.Name, err) + } +} + +const wsCrossMarginBalancePushDataJSON = `{"time": 1605248616,"channel": "spot.cross_balances","event": "update", "result": [{"timestamp": "1605248616","timestamp_ms": "1605248616123","user": "1000001","currency": "USDT", "change": "100","total": "1032951.325075926","available": "1022943.325075926"}]}` + +func TestCrossMarginBalancePushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleData([]byte(wsCrossMarginBalancePushDataJSON)); err != nil { + t.Errorf("%s websocket cross margin balance push data error: %v", g.Name, err) + } +} + +const wsCrossMarginBalanceLoan = `{ "time":1658289372, "channel":"spot.cross_loan", "event":"update", "result":{ "timestamp":1658289372338, "user":"1000001", "currency":"BTC", "change":"0.01", "total":"4.992341029566", "available":"0.078054772536", "borrowed":"0.01", "interest":"0.00001375" }}` + +func TestCrossMarginBalanceLoan(t *testing.T) { + t.Parallel() + if err := g.wsHandleData([]byte(wsCrossMarginBalanceLoan)); err != nil { + t.Errorf("%s websocket cross margin loan push data error: %v", g.Name, err) + } +} + +const wsFuturesTickerPushDataJSON = `{"time": 1541659086, "channel": "futures.tickers","event": "update", "error": null, "result": [ { "contract": "BTC_USD","last": "118.4","change_percentage": "0.77","funding_rate": "-0.000114","funding_rate_indicative": "0.01875","mark_price": "118.35","index_price": "118.36","total_size": "73648","volume_24h": "745487577","volume_24h_btc": "117", "volume_24h_usd": "419950", "quanto_base_rate": "", "volume_24h_quote": "1665006","volume_24h_settle": "178","volume_24h_base": "5526","low_24h": "99.2","high_24h": "132.5"} ]}` + +func TestFuturesTicker(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesTickerPushDataJSON), asset.Futures); err != nil { + t.Errorf("%s websocket push data error: %v", g.Name, err) + } +} + +const wsFuturesTradesPushDataJSON = `{"channel": "futures.trades","event": "update", "time": 1541503698, "result": [{"size": -108,"id": 27753479,"create_time": 1545136464,"create_time_ms": 1545136464123,"price": "96.4","contract": "BTC_USD"}]}` + +func TestFuturesTrades(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesTradesPushDataJSON), asset.Futures); err != nil { + t.Errorf("%s websocket push data error: %v", g.Name, err) + } +} + +const ( + wsFuturesOrderbookTickerJSON = `{ "time": 1615366379, "channel": "futures.book_ticker", "event": "update", "error": null, "result": { "t": 1615366379123, "u": 2517661076, "s": "BTC_USD", "b": "54696.6", "B": 37000, "a": "54696.7", "A": 47061 }}` +) + +func TestOrderbookData(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesOrderbookTickerJSON), asset.Futures); err != nil { + t.Errorf("%s websocket orderbook ticker push data error: %v", g.Name, err) + } +} + +const wsFuturesOrderPushDataJSON = `{ "channel": "futures.orders", "event": "update", "time": 1541505434, "result": [ { "contract": "BTC_USD", "create_time": 1628736847, "create_time_ms": 1628736847325, "fill_price": 40000.4, "finish_as": "filled", "finish_time": 1628736848, "finish_time_ms": 1628736848321, "iceberg": 0, "id": 4872460, "is_close": false, "is_liq": false, "is_reduce_only": false, "left": 0, "mkfr": -0.00025, "price": 40000.4, "refr": 0, "refu": 0, "size": 1, "status": "finished", "text": "-", "tif": "gtc", "tkfr": 0.0005, "user": "110xxxxx" } ]}` + +func TestFuturesOrderPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesOrderPushDataJSON), asset.Futures); err != nil { + t.Errorf("%s websocket futures order push data error: %v", g.Name, err) + } +} + +const wsFuturesUsertradesPushDataJSON = `{"time": 1543205083, "channel": "futures.usertrades","event": "update", "error": null, "result": [{"id": "3335259","create_time": 1628736848,"create_time_ms": 1628736848321,"contract": "BTC_USD","order_id": "4872460","size": 1,"price": "40000.4","role": "maker","text": "api","fee": 0.0009290592,"point_fee": 0}]}` + +func TestFuturesUserTrades(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesUsertradesPushDataJSON), asset.Futures); err != nil { + t.Errorf("%s websocket futures user trades push data error: %v", g.Name, err) + } +} + +const wsFuturesLiquidationPushDataJSON = `{"channel": "futures.liquidates", "event": "update", "time": 1541505434, "result": [{"entry_price": 209,"fill_price": 215.1,"left": 0,"leverage": 0.0,"liq_price": 213,"margin": 0.007816722941,"mark_price": 213,"order_id": 4093362,"order_price": 215.1,"size": -124,"time": 1541486601,"time_ms": 1541486601123,"contract": "BTC_USD","user": "1040xxxx"} ]}` + +func TestFuturesLiquidationPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesLiquidationPushDataJSON), asset.Futures); err != nil { + t.Errorf("%s websocket futures liquidation push data error: %v", g.Name, err) + } +} + +const wsFuturesAutoDelevergesNotification = `{"channel": "futures.auto_deleverages", "event": "update", "time": 1541505434, "result": [{"entry_price": 209,"fill_price": 215.1,"position_size": 10,"trade_size": 10,"time": 1541486601,"time_ms": 1541486601123,"contract": "BTC_USD","user": "1040"} ]}` + +func TestFuturesAutoDeleverges(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesAutoDelevergesNotification), asset.Futures); err != nil { + t.Errorf("%s websocket futures auto deleverge push data error: %v", g.Name, err) + } +} + +const wsFuturesPositionClosePushDataJSON = ` {"channel": "futures.position_closes", "event": "update", "time": 1541505434, "result": [ { "contract": "BTC_USD", "pnl": -0.000624354791, "side": "long", "text": "web", "time": 1547198562, "time_ms": 1547198562123, "user": "211xxxx" } ]}` + +func TestPositionClosePushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesPositionClosePushDataJSON), asset.Futures); err != nil { + t.Errorf("%s websocket futures position close push data error: %v", g.Name, err) + } +} + +const wsFuturesBalanceNotificationPushDataJSON = `{"channel": "futures.balances", "event": "update", "time": 1541505434, "result": [ { "balance": 9.998739899488, "change": -0.000002074115, "text": "BTC_USD:3914424", "time": 1547199246, "time_ms": 1547199246123, "type": "fee", "user": "211xxx" } ]}` + +func TestFuturesBalanceNotification(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesBalanceNotificationPushDataJSON), asset.Futures); err != nil { + t.Errorf("%s websocket futures balance notification push data error: %v", g.Name, err) + } +} + +const wsFuturesReduceRiskLimitNotificationPushDataJSON = `{"time": 1551858330, "channel": "futures.reduce_risk_limits", "event": "update", "error": null, "result": [ { "cancel_orders": 0, "contract": "ETH_USD", "leverage_max": 10, "liq_price": 136.53, "maintenance_rate": 0.09, "risk_limit": 450, "time": 1551858330, "time_ms": 1551858330123, "user": "20011" } ]}` + +func TestFuturesReduceRiskLimitPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesReduceRiskLimitNotificationPushDataJSON), asset.Futures); err != nil { + t.Errorf("%s websocket futures reduce risk limit notification push data error: %v", g.Name, err) + } +} + +const wsFuturesPositionsNotificationPushDataJSON = `{"time": 1588212926,"channel": "futures.positions", "event": "update", "error": null, "result": [ { "contract": "BTC_USD", "cross_leverage_limit": 0, "entry_price": 40000.36666661111, "history_pnl": -0.000108569505, "history_point": 0, "last_close_pnl": -0.000050123368,"leverage": 0,"leverage_max": 100,"liq_price": 0.1,"maintenance_rate": 0.005,"margin": 49.999890611186,"mode": "single","realised_pnl": -1.25e-8,"realised_point": 0,"risk_limit": 100,"size": 3,"time": 1628736848,"time_ms": 1628736848321,"user": "110xxxxx"}]}` + +func TestFuturesPositionsNotification(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesPositionsNotificationPushDataJSON), asset.Futures); err != nil { + t.Errorf("%s websocket futures positions change notification push data error: %v", g.Name, err) + } +} + +const wsFuturesAutoOrdersPushDataJSON = `{"time": 1596798126,"channel": "futures.autoorders", "event": "update", "error": null, "result": [ { "user": 123456, "trigger": { "strategy_type": 0, "price_type": 0, "price": "10000", "rule": 2, "expiration": 86400 }, "initial": { "contract": "BTC_USDT", "size": 10, "price": "10000", "tif": "gtc", "text": "web", "iceberg": 0, "is_close": false, "is_reduce_only": false }, "id": 9256, "trade_id": 0, "status": "open", "reason": "", "create_time": 1596798126, "name": "price_autoorders", "is_stop_order": false, "stop_trigger": { "rule": 0, "trigger_price": "", "order_price": "" } } ]}` + +func TestFuturesAutoOrderPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleFuturesData([]byte(wsFuturesAutoOrdersPushDataJSON), asset.Futures); err != nil { + t.Errorf("%s websocket futures auto orders push data error: %v", g.Name, err) + } +} + +// ******************************************** Options web-socket unit test funcs ******************** + +const optionsContractTickerPushDataJSON = `{"time": 1630576352, "channel": "options.contract_tickers", "event": "update", "result": { "name": "BTC_USDT-20211231-59800-P", "last_price": "11349.5", "mark_price": "11170.19", "index_price": "", "position_size": 993, "bid1_price": "10611.7", "bid1_size": 100, "ask1_price": "11728.7", "ask1_size": 100, "vega": "34.8731", "theta": "-72.80588", "rho": "-28.53331", "gamma": "0.00003", "delta": "-0.78311", "mark_iv": "0.86695", "bid_iv": "0.65481", "ask_iv": "0.88145", "leverage": "3.5541112718136" }}` + +func TestOptionsContractTickerPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsContractTickerPushDataJSON)); err != nil { + t.Errorf("%s websocket options contract ticker push data failed with error %v", g.Name, err) + } +} + +const optionsUnderlyingTickerPushDataJSON = `{"time": 1630576352, "channel": "options.ul_tickers", "event": "update", "result": { "trade_put": 800, "trade_call": 41700, "index_price": "50695.43", "name": "BTC_USDT" }}` + +func TestOptionsUnderlyingTickerPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsUnderlyingTickerPushDataJSON)); err != nil { + t.Errorf("%s websocket options underlying ticker push data error: %v", g.Name, err) + } +} + +const optionsContractTradesPushDataJSON = `{"time": 1630576356, "channel": "options.trades", "event": "update", "result": [ { "contract": "BTC_USDT-20211231-59800-C", "create_time": 1639144526, "id": 12279, "price": 997.8, "size": -100, "create_time_ms": 1639144526597, "underlying": "BTC_USDT" } ]}` + +func TestOptionsContractTradesPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsContractTradesPushDataJSON)); err != nil { + t.Errorf("%s websocket contract trades push data error: %v", g.Name, err) + } +} + +const optionsUnderlyingTradesPushDataJSON = `{"time": 1630576356, "channel": "options.ul_trades", "event": "update", "result": [{"contract": "BTC_USDT-20211231-59800-C","create_time": 1639144526,"id": 12279,"price": 997.8,"size": -100,"create_time_ms": 1639144526597,"underlying": "BTC_USDT","is_call": true} ]}` + +func TestOptionsUnderlyingTradesPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsUnderlyingTradesPushDataJSON)); err != nil { + t.Errorf("%s websocket underlying trades push data error: %v", g.Name, err) + } +} + +const optionsUnderlyingPricePushDataJSON = `{ "time": 1630576356, "channel": "options.ul_price", "event": "update", "result": { "underlying": "BTC_USDT", "price": 49653.24,"time": 1639143988,"time_ms": 1639143988931 }}` + +func TestOptionsUnderlyingPricePushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsUnderlyingPricePushDataJSON)); err != nil { + t.Errorf("%s websocket underlying price push data error: %v", g.Name, err) + } +} + +const optionsMarkPricePushDataJSON = `{ "time": 1630576356, "channel": "options.mark_price", "event": "update", "result": { "contract": "BTC_USDT-20211231-59800-P", "price": 11021.27, "time": 1639143401, "time_ms": 1639143401676 }}` + +func TestOptionsMarkPricePushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsMarkPricePushDataJSON)); err != nil { + t.Errorf("%s websocket mark price push data error: %v", g.Name, err) + } +} + +const optionsSettlementsPushDataJSON = `{ "time": 1630576356, "channel": "options.settlements", "event": "update", "result": { "contract": "BTC_USDT-20211130-55000-P", "orderbook_id": 2, "position_size": 1, "profit": 0.5, "settle_price": 70000, "strike_price": 65000, "tag": "WEEK", "trade_id": 1, "trade_size": 1, "underlying": "BTC_USDT", "time": 1639051907, "time_ms": 1639051907000 }}` + +func TestSettlementsPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsSettlementsPushDataJSON)); err != nil { + t.Errorf("%s websocket options settlements push data error: %v", g.Name, err) + } +} + +const optionsContractPushDataJSON = `{"time": 1630576356, "channel": "options.contracts", "event": "update", "result": { "contract": "BTC_USDT-20211130-50000-P", "create_time": 1637917026, "expiration_time": 1638230400, "init_margin_high": 0.15, "init_margin_low": 0.1, "is_call": false, "maint_margin_base": 0.075, "maker_fee_rate": 0.0004, "mark_price_round": 0.1, "min_balance_short": 0.5, "min_order_margin": 0.1, "multiplier": 0.0001, "order_price_deviate": 0, "order_price_round": 0.1, "order_size_max": 1, "order_size_min": 10, "orders_limit": 100000, "ref_discount_rate": 0.1, "ref_rebate_rate": 0, "strike_price": 50000, "tag": "WEEK", "taker_fee_rate": 0.0004, "underlying": "BTC_USDT", "time": 1639051907, "time_ms": 1639051907000 }}` + +func TestOptionsContractPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsContractPushDataJSON)); err != nil { + t.Errorf("%s websocket options contracts push data error: %v", g.Name, err) + } +} + +const ( + optionsContractCandlesticksPushDataJSON = `{ "time": 1630650451, "channel": "options.contract_candlesticks", "event": "update", "result": [ { "t": 1639039260, "v": 100, "c": "1041.4", "h": "1041.4", "l": "1041.4", "o": "1041.4", "a": "0", "n": "10s_BTC_USDT-20211231-59800-C" } ]}` + optionsUnderlyingCandlesticksPushDataJSON = `{ "time": 1630650451, "channel": "options.ul_candlesticks", "event": "update", "result": [ { "t": 1639039260, "v": 100, "c": "1041.4", "h": "1041.4", "l": "1041.4", "o": "1041.4", "a": "0", "n": "10s_BTC_USDT" } ]}` +) + +func TestOptionsCandlesticksPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsContractCandlesticksPushDataJSON)); err != nil { + t.Errorf("%s websocket options contracts candlestick push data error: %v", g.Name, err) + } + if err := g.wsHandleOptionsData([]byte(optionsUnderlyingCandlesticksPushDataJSON)); err != nil { + t.Errorf("%s websocket options underlying candlestick push data error: %v", g.Name, err) + } +} + +const ( + optionsOrderbookTickerPushDataJSON = `{ "time": 1630650452, "channel": "options.book_ticker", "event": "update", "result": { "t": 1615366379123, "u": 2517661076, "s": "BTC_USDT-20211130-50000-C", "b": "54696.6", "B": 37000, "a": "54696.7", "A": 47061 }}` + optionsOrderbookUpdatePushDataJSON = `{ "time": 1630650445, "channel": "options.order_book_update", "event": "update", "result": { "t": 1615366381417, "s": "BTC_USDT-20211130-50000-C", "U": 2517661101, "u": 2517661113, "b": [ { "p": "54672.1", "s": 95 }, { "p": "54664.5", "s": 58794 } ], "a": [ { "p": "54743.6", "s": 95 }, { "p": "54742", "s": 95 } ] }}` + optionsOrderbookSnapshotPushDataJSON = `{ "time": 1630650445, "channel": "options.order_book", "event": "all", "result": { "t": 1541500161123, "contract": "BTC_USDT-20211130-50000-C", "id": 93973511, "asks": [ { "p": "97.1", "s": 2245 }, { "p": "97.2", "s": 2245 } ], "bids": [ { "p": "97.2", "s": 2245 }, { "p": "97.1", "s": 2245 } ] }}` + optionsOrderbookSnapshotUpdateEventPushDataJSON = `{"channel": "options.order_book", "event": "update", "time": 1630650445, "result": [ { "p": "49525.6", "s": 7726, "c": "BTC_USDT-20211130-50000-C", "id": 93973511 } ]}` +) + +func TestOptionsOrderbookPushData(t *testing.T) { + t.Parallel() + err := g.wsHandleOptionsData([]byte(optionsOrderbookTickerPushDataJSON)) + if err != nil { + t.Errorf("%s websocket options orderbook ticker push data error: %v", g.Name, err) + } + if err = g.wsHandleOptionsData([]byte(optionsOrderbookSnapshotPushDataJSON)); err != nil { + t.Errorf("%s websocket options orderbook snapshot push data error: %v", g.Name, err) + } + if err = g.wsHandleOptionsData([]byte(optionsOrderbookUpdatePushDataJSON)); err != nil { + t.Errorf("%s websocket options orderbook update push data error: %v", g.Name, err) + } + if err = g.wsHandleOptionsData([]byte(optionsOrderbookSnapshotUpdateEventPushDataJSON)); err != nil { + t.Errorf("%s websocket options orderbook snapshot update event push data error: %v", g.Name, err) + } +} + +const optionsOrderPushDataJSON = `{"time": 1630654851,"channel": "options.orders", "event": "update", "result": [ { "contract": "BTC_USDT-20211130-65000-C", "create_time": 1637897000, "fill_price": 0, "finish_as": "cancelled", "iceberg": 0, "id": 106, "is_close": false, "is_liq": false, "is_reduce_only": false, "left": -10, "mkfr": 0.0004, "price": 15000, "refr": 0, "refu": 0, "size": -10, "status": "finished", "text": "web", "tif": "gtc", "tkfr": 0.0004, "underlying": "BTC_USDT", "user": "9xxx", "time": 1639051907,"time_ms": 1639051907000}]}` + +func TestOptionsOrderPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsOrderPushDataJSON)); err != nil { + t.Errorf("%s websocket options orders push data error: %v", g.Name, err) + } +} + +const optionsUsersTradesPushDataJSON = `{ "time": 1639144214, "channel": "options.usertrades", "event": "update", "result": [{"id": "1","underlying": "BTC_USDT","order": "557940","contract": "BTC_USDT-20211216-44800-C","create_time": 1639144214,"create_time_ms": 1639144214583,"price": "4999","role": "taker","size": -1}]}` + +func TestOptionUserTradesPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsUsersTradesPushDataJSON)); err != nil { + t.Errorf("%s websocket options orders push data error: %v", g.Name, err) + } +} + +const optionsLiquidatesPushDataJSON = `{ "channel": "options.liquidates", "event": "update", "time": 1630654851, "result": [ { "user": "1xxxx", "init_margin": 1190, "maint_margin": 1042.5, "order_margin": 0, "time": 1639051907, "time_ms": 1639051907000 } ]}` + +func TestOptionsLiquidatesPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsLiquidatesPushDataJSON)); err != nil { + t.Errorf("%s websocket options liquidates push data error: %v", g.Name, err) + } +} + +const optionsSettlementPushDataJSON = `{ "channel": "options.user_settlements", "event": "update", "time": 1639051907, "result": [{"contract": "BTC_USDT-20211130-65000-C","realised_pnl": -13.028,"settle_price": 70000,"settle_profit": 5,"size": 10,"strike_price": 65000,"underlying": "BTC_USDT","user": "9xxx","time": 1639051907,"time_ms": 1639051907000}]}` + +func TestOptionsSettlementPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsSettlementPushDataJSON)); err != nil { + t.Errorf("%s websocket options settlement push data error: %v", g.Name, err) + } +} + +const optionsPositionClosePushDataJSON = `{"channel": "options.position_closes", "event": "update", "time": 1630654851, "result": [{"contract": "BTC_USDT-20211130-50000-C","pnl": -0.0056,"settle_size": 0,"side": "long","text": "web","underlying": "BTC_USDT","user": "11xxxxx","time": 1639051907,"time_ms": 1639051907000}]}` + +func TestOptionsPositionClosePushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsPositionClosePushDataJSON)); err != nil { + t.Errorf("%s websocket options position close push data error: %v", g.Name, err) + } +} + +const optionsBalancePushDataJSON = `{ "channel": "options.balances", "event": "update", "time": 1630654851, "result": [ { "balance": 60.79009,"change": -0.5,"text": "BTC_USDT-20211130-55000-P","type": "set","user": "11xxxx","time": 1639051907,"time_ms": 1639051907000}]}` + +func TestOptionsBalancePushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsBalancePushDataJSON)); err != nil { + t.Errorf("%s websocket options balance push data error: %v", g.Name, err) + } +} + +const optionsPositionPushDataJSON = `{"time": 1630654851, "channel": "options.positions", "event": "update", "error": null, "result": [ { "entry_price": 0, "realised_pnl": -13.028, "size": 0, "contract": "BTC_USDT-20211130-65000-C", "user": "9010", "time": 1639051907, "time_ms": 1639051907000 } ]}` + +func TestOptionsPositionPushData(t *testing.T) { + t.Parallel() + if err := g.wsHandleOptionsData([]byte(optionsPositionPushDataJSON)); err != nil { + t.Errorf("%s websocket options position push data error: %v", g.Name, err) + } +} + +const ( + futuresOrderbookPushData = `{"time": 1678468497, "time_ms": 1678468497232, "channel": "futures.order_book", "event": "all", "result": { "t": 1678468497168, "id": 4010394406, "contract": "BTC_USD", "asks": [ { "p": "19909", "s": 3100 }, { "p": "19909.1", "s": 5000 }, { "p": "19910", "s": 3100 }, { "p": "19914.4", "s": 4400 }, { "p": "19916.6", "s": 5000 }, { "p": "19917.2", "s": 8255 }, { "p": "19919.2", "s": 5000 }, { "p": "19920.3", "s": 11967 }, { "p": "19922.2", "s": 5000 }, { "p": "19924.2", "s": 5000 }, { "p": "19927.1", "s": 17129 }, { "p": "19927.2", "s": 5000 }, { "p": "19929", "s": 20864 }, { "p": "19929.3", "s": 5000 }, { "p": "19929.7", "s": 24683 }, { "p": "19930.3", "s": 750 }, { "p": "19931.4", "s": 5000 }, { "p": "19931.5", "s": 1 }, { "p": "19934.2", "s": 5000 }, { "p": "19935.4", "s": 1 } ], "bids": [ { "p": "19901.2", "s": 5000 }, { "p": "19900.3", "s": 3100 }, { "p": "19900.2", "s": 5000 }, { "p": "19899.3", "s": 2983 }, { "p": "19899.2", "s": 6035 }, { "p": "19897.2", "s": 5000 }, { "p": "19895.7", "s": 5984 }, { "p": "19895", "s": 5000 }, { "p": "19892.9", "s": 195 }, { "p": "19892.8", "s": 5000 }, { "p": "19889.4", "s": 5000 }, { "p": "19889", "s": 8800 }, { "p": "19888.5", "s": 11968 }, { "p": "19887.1", "s": 5000 }, { "p": "19886.4", "s": 24683 }, { "p": "19885.7", "s": 1 }, { "p": "19883.8", "s": 5000 }, { "p": "19880.2", "s": 5000 }, { "p": "19878.2", "s": 5000 }, { "p": "19876.8", "s": 1 } ] } }` + futuresOrderbookUpdatePushData = `{"time": 1678469222, "time_ms": 1678469222982, "channel": "futures.order_book_update", "event": "update", "result": { "t": 1678469222617, "s": "BTC_USD", "U": 4010424331, "u": 4010424361, "b": [ { "p": "19860.7", "s": 5984 }, { "p": "19858.6", "s": 5000 }, { "p": "19845.4", "s": 20864 }, { "p": "19859.1", "s": 0 }, { "p": "19862.5", "s": 0 }, { "p": "19358", "s": 0 }, { "p": "19864.5", "s": 5000 }, { "p": "19840.7", "s": 0 }, { "p": "19863.6", "s": 3100 }, { "p": "19839.3", "s": 0 }, { "p": "19851.5", "s": 8800 }, { "p": "19720", "s": 0 }, { "p": "19333", "s": 0 }, { "p": "19852.7", "s": 5000 }, { "p": "19861.5", "s": 0 }, { "p": "19860.6", "s": 3100 }, { "p": "19833.6", "s": 0 }, { "p": "19360", "s": 0 }, { "p": "19863.5", "s": 5000 }, { "p": "19736.9", "s": 0 }, { "p": "19838.5", "s": 0 }, { "p": "19841.3", "s": 0 }, { "p": "19858.1", "s": 3100 }, { "p": "19710.9", "s": 0 }, { "p": "19342", "s": 0 }, { "p": "19852.1", "s": 11967 }, { "p": "19343", "s": 0 }, { "p": "19705", "s": 0 }, { "p": "19836.5", "s": 0 }, { "p": "19862.6", "s": 3100 }, { "p": "19729.6", "s": 0 }, { "p": "19849.9", "s": 5000 } ], "a": [ { "p": "19900.5", "s": 0 }, { "p": "19883.1", "s": 11967 }, { "p": "19910.9", "s": 0 }, { "p": "19897.7", "s": 5000 }, { "p": "19875.9", "s": 5984 }, { "p": "19899.6", "s": 0 }, { "p": "19878", "s": 4400 }, { "p": "19877.6", "s": 0 }, { "p": "19889.5", "s": 5000 }, { "p": "19875.5", "s": 3100 }, { "p": "19875.3", "s": 0 }, { "p": "19878.5", "s": 0 }, { "p": "19895.2", "s": 0 }, { "p": "20284.6", "s": 0 }, { "p": "19880.7", "s": 5000 }, { "p": "19875.4", "s": 0 }, { "p": "19985.8", "s": 0 }, { "p": "19887.1", "s": 5000 }, { "p": "19896", "s": 1 }, { "p": "19869.3", "s": 0 }, { "p": "19900", "s": 0 }, { "p": "19875.6", "s": 5000 }, { "p": "19980.6", "s": 0 }, { "p": "19885.1", "s": 5000 }, { "p": "19877.7", "s": 5000 }, { "p": "20000", "s": 0 }, { "p": "19892.2", "s": 8255 }, { "p": "19886.8", "s": 0 }, { "p": "20257.4", "s": 0 }, { "p": "20280", "s": 0 }, { "p": "20002.5", "s": 0 }, { "p": "20263.1", "s": 0 }, { "p": "19900.2", "s": 0 } ] } }` +) + +func TestFuturesOrderbookPushData(t *testing.T) { + t.Parallel() + err := g.wsHandleFuturesData([]byte(futuresOrderbookPushData), asset.Futures) + if err != nil { + t.Error(err) + } + err = g.wsHandleFuturesData([]byte(futuresOrderbookUpdatePushData), asset.Futures) + if err != nil { + t.Error(err) + } +} + +const futuresCandlesticksPushData = `{"time": 1678469467, "time_ms": 1678469467981, "channel": "futures.candlesticks", "event": "update", "result": [ { "t": 1678469460, "v": 0, "c": "19896", "h": "19896", "l": "19896", "o": "19896", "n": "1m_BTC_USD" } ] }` + +func TestFuturesCandlestickPushData(t *testing.T) { + t.Parallel() + err := g.wsHandleFuturesData([]byte(futuresCandlesticksPushData), asset.Futures) + if err != nil { + t.Error(err) + } +} + +func TestGenerateDefaultSubscriptions(t *testing.T) { + t.Parallel() + if _, err := g.GenerateDefaultSubscriptions(); err != nil { + t.Error(err) + } +} +func TestGenerateDeliveryFuturesDefaultSubscriptions(t *testing.T) { + t.Parallel() + if _, err := g.GenerateDeliveryFuturesDefaultSubscriptions(); err != nil { + t.Error(err) + } +} +func TestGenerateFuturesDefaultSubscriptions(t *testing.T) { + t.Parallel() + if _, err := g.GenerateFuturesDefaultSubscriptions(); err != nil { + t.Error(err) + } +} +func TestGenerateOptionsDefaultSubscriptions(t *testing.T) { + t.Parallel() + if _, err := g.GenerateOptionsDefaultSubscriptions(); err != nil { + t.Error(err) + } +} + +func TestCreateAPIKeysOfSubAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if _, err := g.CreateAPIKeysOfSubAccount(context.Background(), CreateAPIKeySubAccountParams{ + SubAccountUserID: 12345, + Body: &SubAccountKey{ + APIKeyName: "12312mnfsndfsfjsdklfjsdlkfj", + Permissions: []APIV4KeyPerm{ + { + PermissionName: "wallet", + ReadOnly: false, + }, + { + PermissionName: "spot", + ReadOnly: false, + }, + { + PermissionName: "futures", + ReadOnly: false, + }, + { + PermissionName: "delivery", + ReadOnly: false, + }, + { + PermissionName: "earn", + ReadOnly: false, + }, + { + PermissionName: "options", + ReadOnly: false, + }, + }, + }, + }); err != nil { + t.Error(err) + } +} + +func TestListAllAPIKeyOfSubAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + _, err := g.GetAllAPIKeyOfSubAccount(context.Background(), 1234) + if err != nil { + t.Error(err) + } +} + +func TestUpdateAPIKeyOfSubAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + if err := g.UpdateAPIKeyOfSubAccount(context.Background(), apiKey, CreateAPIKeySubAccountParams{ + SubAccountUserID: 12345, + Body: &SubAccountKey{ + APIKeyName: "12312mnfsndfsfjsdklfjsdlkfj", + Permissions: []APIV4KeyPerm{ + { + PermissionName: "wallet", + ReadOnly: false, + }, + { + PermissionName: "spot", + ReadOnly: false, + }, + { + PermissionName: "futures", + ReadOnly: false, + }, + { + PermissionName: "delivery", + ReadOnly: false, + }, + { + PermissionName: "earn", + ReadOnly: false, + }, + { + PermissionName: "options", + ReadOnly: false, + }, + }, + }, + }); err != nil { + t.Error(err) + } +} + +func TestGetAPIKeyOfSubAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + _, err := g.GetAPIKeyOfSubAccount(context.Background(), 1234, "target_api_key") + if err != nil { + t.Error(err) + } +} + +func TestLockSubAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if err := g.LockSubAccount(context.Background(), 1234); err != nil { + t.Error(err) + } +} + +func TestUnlockSubAccount(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) + if err := g.UnlockSubAccount(context.Background(), 1234); err != nil { + t.Error(err) + } +} + +func getFirstTradablePairOfAssets() { + enabledPairs, err := g.GetEnabledPairs(asset.Spot) + if err != nil { + log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.Spot) + } + spotTradablePair = enabledPairs[0] + enabledPairs, err = g.GetEnabledPairs(asset.Margin) + if err != nil { + log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.Margin) + } + marginTradablePair = enabledPairs[0] + enabledPairs, err = g.GetEnabledPairs(asset.CrossMargin) + if err != nil { + log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.CrossMargin) + } + crossMarginTradablePair = enabledPairs[0] + enabledPairs, err = g.GetEnabledPairs(asset.Futures) + if err != nil { + log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.Futures) + } + futuresTradablePair = enabledPairs[len(enabledPairs)-1] + enabledPairs, err = g.GetEnabledPairs(asset.Options) + if err != nil { + log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.Options) + } + optionsTradablePair = enabledPairs[0] + enabledPairs, err = g.GetEnabledPairs(asset.DeliveryFutures) + if err != nil { + log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.DeliveryFutures) + } + deliveryFuturesTradablePair = enabledPairs[0] +} + +func TestSettlement(t *testing.T) { + availablePairs, err := g.GetAvailablePairs(asset.Futures) + if err != nil { + t.Fatal(err) + } + for x := range availablePairs { + t.Run(strconv.Itoa(x), func(t *testing.T) { + _, err = g.getSettlementFromCurrency(availablePairs[x], true) + if err != nil { + t.Fatal(err) + } + }) + } + availablePairs, err = g.GetAvailablePairs(asset.DeliveryFutures) + if err != nil { + t.Fatal(err) + } + for x := range availablePairs { + t.Run(strconv.Itoa(x), func(t *testing.T) { + _, err = g.getSettlementFromCurrency(availablePairs[x], false) + if err != nil { + t.Fatal(err) + } + }) + } + availablePairs, err = g.GetAvailablePairs(asset.Options) + if err != nil { + t.Fatal(err) + } + for x := range availablePairs { + t.Run(strconv.Itoa(x), func(t *testing.T) { + _, err := g.getSettlementFromCurrency(availablePairs[x], false) + if err != nil { + t.Fatal(err) + } + }) + } +} + +func TestParseGateioMilliSecTimeUnmarshal(t *testing.T) { + t.Parallel() + var timeWhenTesting int64 = 1684981731098 + timeWhenTestingString := "1684981731098" + integerJSON := `{"number": 1684981731098}` + float64JSON := `{"number": 1684981731098.234}` + + time := time.UnixMilli(timeWhenTesting) + var in gateioTime + err := json.Unmarshal([]byte(timeWhenTestingString), &in) + if err != nil { + t.Fatal(err) + } + if !in.Time().Equal(time) { + t.Fatalf("found %v, but expected %v", in.Time(), time) + } + inInteger := struct { + Number gateioTime `json:"number"` + }{} + err = json.Unmarshal([]byte(integerJSON), &inInteger) + if err != nil { + t.Fatal(err) + } + if !inInteger.Number.Time().Equal(time) { + t.Fatalf("found %v, but expected %v", inInteger.Number.Time(), time) + } + + inFloat64 := struct { + Number gateioTime `json:"number"` + }{} + err = json.Unmarshal([]byte(float64JSON), &inFloat64) + if err != nil { + t.Fatal(err) + } + if !inFloat64.Number.Time().Equal(time) { + t.Fatalf("found %v, but expected %v", inFloat64.Number.Time(), time) + } +} + +func TestParseGateioTimeUnmarshal(t *testing.T) { + t.Parallel() + var timeWhenTesting int64 = 1684981731 + timeWhenTestingString := "1684981731" + integerJSON := `{"number": 1684981731}` + float64JSON := `{"number": 1684981731.234}` + + time := time.Unix(timeWhenTesting, 0) + var in gateioTime + err := json.Unmarshal([]byte(timeWhenTestingString), &in) + if err != nil { + t.Fatal(err) + } + if !in.Time().Equal(time) { + t.Fatalf("found %v, but expected %v", in.Time(), time) + } + inInteger := struct { + Number gateioTime `json:"number"` + }{} + err = json.Unmarshal([]byte(integerJSON), &inInteger) + if err != nil { + t.Fatal(err) + } + if !inInteger.Number.Time().Equal(time) { + t.Fatalf("found %v, but expected %v", inInteger.Number.Time(), time) + } + + inFloat64 := struct { + Number gateioTime `json:"number"` + }{} + err = json.Unmarshal([]byte(float64JSON), &inFloat64) + if err != nil { + t.Fatal(err) + } + if !inFloat64.Number.Time().Equal(time) { + t.Fatalf("found %v, but expected %v", inFloat64.Number.Time(), time) + } +} + +func TestGateioNumericalValue(t *testing.T) { + t.Parallel() + in := &struct { + Number gateioNumericalValue `json:"number"` + }{} + + numberJSON := `{"number":123442.231}` + err := json.Unmarshal([]byte(numberJSON), in) + if err != nil { + t.Fatal(err) + } else if in.Number != 123442.231 { + t.Fatalf("found %f, but expected %f", in.Number, 123442.231) + } + + numberJSON = `{"number":"123442.231"}` + err = json.Unmarshal([]byte(numberJSON), in) + if err != nil { + t.Fatal(err) + } else if in.Number != 123442.231 { + t.Fatalf("found %f, but expected %s", in.Number, "123442.231") + } + + numberJSON = `{"number":""}` + err = json.Unmarshal([]byte(numberJSON), in) + if err != nil { + t.Fatal(err) + } else if in.Number != 0 { + t.Fatalf("found %f, but expected %d", in.Number, 0) + } + + numberJSON = `{"number":0}` + err = json.Unmarshal([]byte(numberJSON), in) + if err != nil { + t.Fatal(err) + } else if in.Number != 0 { + t.Fatalf("found %f, but expected %d", in.Number, 0) + } +} diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index de91345f..966c20b1 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -1,550 +1,2668 @@ package gateio import ( - "encoding/json" + "strconv" "time" "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) -// TimeInterval Interval represents interval enum. -type TimeInterval int +const ( + // Settles + settleBTC = "btc" + settleUSD = "usd" + settleUSDT = "usdt" -// TimeInterval vars -var ( - TimeIntervalMinute = TimeInterval(60) - TimeIntervalThreeMinutes = TimeInterval(60 * 3) - TimeIntervalFiveMinutes = TimeInterval(60 * 5) - TimeIntervalFifteenMinutes = TimeInterval(60 * 15) - TimeIntervalThirtyMinutes = TimeInterval(60 * 30) - TimeIntervalHour = TimeInterval(60 * 60) - TimeIntervalTwoHours = TimeInterval(2 * 60 * 60) - TimeIntervalFourHours = TimeInterval(4 * 60 * 60) - TimeIntervalSixHours = TimeInterval(6 * 60 * 60) - TimeIntervalDay = TimeInterval(60 * 60 * 24) + // time in force variables + + gtcTIF = "gtc" // good-'til-canceled + iocTIF = "ioc" // immediate-or-cancel + pocTIF = "poc" + focTIF = "foc" // fill-or-kill + + // frequently used order Status + + statusOpen = "open" + statusLoaned = "loaned" + statusFinished = "finished" + + // Loan sides + sideLend = "lend" + sideBorrow = "borrow" ) -// MarketInfoResponse holds the market info data -type MarketInfoResponse struct { - Result string `json:"result"` - Pairs []MarketInfoPairsResponse `json:"pairs"` -} - -// MarketInfoPairsResponse holds the market info response data -type MarketInfoPairsResponse struct { - Symbol string - // DecimalPlaces symbol price accuracy - DecimalPlaces float64 - // MinAmount minimum order amount - MinAmount float64 - // Fee transaction fee - Fee float64 -} - -// BalancesResponse holds the user balances -type BalancesResponse struct { - Result string `json:"result"` - Available interface{} `json:"available"` - Locked interface{} `json:"locked"` -} - -// KlinesRequestParams represents Klines request data. -type KlinesRequestParams struct { - Symbol string // Required field; example LTCBTC,BTCUSDT - HourSize int // How many hours of data - GroupSec string -} - -// KLineResponse holds the kline response data -type KLineResponse struct { - ID float64 - KlineTime time.Time - Open float64 - Time float64 - High float64 - Low float64 - Close float64 - Volume float64 - Amount float64 `db:"amount"` -} - -// TickerResponse holds the ticker response data -type TickerResponse struct { - Period int64 `json:"period"` - BaseVolume float64 `json:"baseVolume,string"` - Change float64 `json:"change,string"` - Close float64 `json:"close,string"` - High float64 `json:"high,string"` - Last float64 `json:"last,string"` - Low float64 `json:"low,string"` - Open float64 `json:"open,string"` - QuoteVolume float64 `json:"quoteVolume,string"` -} - -// OrderbookResponse stores the orderbook data -type OrderbookResponse struct { - Result string `json:"result"` - Elapsed string `json:"elapsed"` - Asks [][]string - Bids [][]string -} - -// OrderbookItem stores an orderbook item -type OrderbookItem struct { - Price float64 - Amount float64 -} - -// Orderbook stores the orderbook data -type Orderbook struct { - Result string - Elapsed string - Bids []OrderbookItem - Asks []OrderbookItem -} - -// SpotNewOrderRequestParams Order params -type SpotNewOrderRequestParams struct { - Amount float64 `json:"amount"` // Order quantity - Price float64 `json:"price"` // Order price - Symbol string `json:"symbol"` // Trading pair; btc_usdt, eth_btc...... - Type string `json:"type"` // Order type (buy or sell), -} - -// SpotNewOrderResponse Order response -type SpotNewOrderResponse struct { - OrderNumber int64 `json:"orderNumber"` // OrderID number - Price float64 `json:"rate,string"` // Order price - LeftAmount float64 `json:"leftAmount,string"` // The remaining amount to fill - FilledAmount float64 `json:"filledAmount,string"` // The filled amount - Filledrate interface{} `json:"filledRate"` // FilledPrice. if we send a market order, the exchange returns float64. - // if we set a limit order, which will remain in the order book, the exchange will return the string -} - -// OpenOrdersResponse the main response from GetOpenOrders -type OpenOrdersResponse struct { - Code int `json:"code"` - Elapsed string `json:"elapsed"` - Message string `json:"message"` - Orders []OpenOrder `json:"orders"` - Result string `json:"result"` -} - -// OpenOrder details each open order -type OpenOrder struct { - Amount float64 `json:"amount,string"` - CurrencyPair string `json:"currencyPair"` - FilledAmount float64 `json:"filledAmount,string"` - FilledRate float64 `json:"filledRate"` - InitialAmount float64 `json:"initialAmount"` - InitialRate float64 `json:"initialRate"` - OrderNumber string `json:"orderNumber"` - Rate float64 `json:"rate"` - Status string `json:"status"` - Timestamp int64 `json:"timestamp"` - Total float64 `json:"total,string"` - Type string `json:"type"` -} - -// TradeHistoryResponse The full response for retrieving all user trade history -type TradeHistoryResponse struct { - Code int `json:"code,omitempty"` - Elapsed string `json:"elapsed,omitempty"` - Message string `json:"message"` - Trades []TradesResponse `json:"trades"` - Result string `json:"result"` -} - -// TradesResponse details trade history -type TradesResponse struct { - ID int64 `json:"tradeID"` - OrderID int64 `json:"orderNumber"` - Pair string `json:"pair"` - Type string `json:"type"` - Side string `json:"side"` - Rate float64 `json:"rate,string"` - Amount float64 `json:"amount,string"` - Total float64 `json:"total"` - Time string `json:"date"` - TimeUnix int64 `json:"time_unix"` -} - // WithdrawalFees the large list of predefined withdrawal fees // Prone to change var WithdrawalFees = map[currency.Code]float64{ - currency.USDT: 10, - currency.USDT_ETH: 10, currency.BTC: 0.001, - currency.BCH: 0.0006, - currency.BTG: 0.002, - currency.LTC: 0.002, - currency.ZEC: 0.001, currency.ETH: 0.003, - currency.ETC: 0.01, - currency.DASH: 0.02, - currency.QTUM: 0.1, - currency.QTUM_ETH: 0.1, - currency.DOGE: 50, - currency.REP: 0.1, - currency.BAT: 10, - currency.SNT: 30, - currency.BTM: 10, - currency.BTM_ETH: 10, - currency.CVC: 5, - currency.REQ: 20, - currency.RDN: 1, - currency.STX: 3, - currency.KNC: 1, - currency.LINK: 8, - currency.FIL: 0.1, - currency.CDT: 20, - currency.AE: 1, - currency.INK: 10, - currency.BOT: 5, - currency.POWR: 5, - currency.WTC: 0.2, - currency.VET: 10, - currency.RCN: 5, - currency.PPT: 0.1, - currency.ARN: 2, - currency.BNT: 0.5, - currency.VERI: 0.005, - currency.MCO: 0.1, - currency.MDA: 0.5, - currency.FUN: 50, - currency.DATA: 10, - currency.RLC: 1, - currency.ZSC: 20, - currency.WINGS: 2, - currency.GVT: 0.2, - currency.KICK: 5, - currency.CTR: 1, - currency.HC: 0.2, - currency.QBT: 5, - currency.QSP: 5, - currency.BCD: 0.02, - currency.MED: 100, - currency.QASH: 1, - currency.DGD: 0.05, - currency.GNT: 10, - currency.MDS: 20, - currency.SBTC: 0.05, - currency.MANA: 50, - currency.GOD: 0.1, - currency.BCX: 30, - currency.SMT: 50, - currency.BTF: 0.1, - currency.IOTA: 0.1, - currency.NAS: 0.5, - currency.NAS_ETH: 0.5, - currency.TSL: 10, - currency.ADA: 1, - currency.LSK: 0.1, - currency.WAVES: 0.1, - currency.BIFI: 0.2, - currency.XTZ: 0.1, - currency.BNTY: 10, - currency.ICX: 0.5, - currency.LEND: 20, - currency.LUN: 0.2, - currency.ELF: 2, - currency.SALT: 0.2, - currency.FUEL: 2, - currency.DRGN: 2, - currency.GTC: 2, - currency.MDT: 2, - currency.QUN: 2, - currency.GNX: 2, - currency.DDD: 10, - currency.OST: 4, - currency.BTO: 10, - currency.TIO: 10, - currency.THETA: 10, - currency.SNET: 10, - currency.OCN: 10, - currency.ZIL: 10, - currency.RUFF: 10, - currency.TNC: 10, - currency.COFI: 10, - currency.ZPT: 0.1, - currency.JNT: 10, - currency.GXS: 1, - currency.MTN: 10, - currency.BLZ: 2, - currency.GEM: 2, - currency.DADI: 2, - currency.ABT: 2, - currency.LEDU: 10, - currency.RFR: 10, - currency.XLM: 1, - currency.MOBI: 1, - currency.ONT: 1, + currency.USDT: 40, + currency.USDC: 2, + currency.BUSD: 5, + currency.ADA: 3.8, + currency.SOL: 0.11, + currency.DOT: .25, + currency.DOGE: 29, + currency.MATIC: 2.2, + currency.STETH: 0.023, + currency.DAI: 24, + currency.SHIB: 420000, + currency.AVAX: 0.083, + currency.TRX: 1, + currency.WBTC: 0.0011, + currency.ETC: 0.051, + currency.OKB: 1.6, + currency.LTC: 0.002, + currency.UNI: 3.2, + currency.LINK: 3.2, + currency.ATOM: 0.19, + currency.XLM: 0.01, + currency.XMR: 0.013, + currency.BCH: 0.014, + currency.ALGO: 6, + currency.ICP: 0.25, + currency.FLOW: 2.6, + currency.VET: 100, + currency.MANA: 26, + currency.SAND: 19, + currency.AXS: 1.3, + currency.HBAR: 1, + currency.XTZ: 1.1, + currency.FRAX: 25, + currency.QNT: 0.24, + currency.THETA: 1.5, + currency.AAVE: 1, + currency.EOS: 1.5, + currency.EGLD: 0.052, + currency.BSV: 0.032, + currency.TUSD: 2, + currency.HNT: 0.21, + currency.MKR: 0.0045, + currency.GRT: 190, + currency.KLAY: 6.4, + currency.BTT: 1, + currency.MIOTA: 0.1, + currency.XEC: 10000, + currency.FTM: 6, + currency.FTM: 6, + currency.SNX: 13, + currency.ZEC: 0.031, currency.NEO: 0, - currency.GAS: 0.02, - currency.DBC: 10, - currency.QLC: 10, - currency.MKR: 0.003, - currency.MKR_OLD: 0.003, - currency.DAI: 2, - currency.LRC: 10, - currency.OAX: 10, - currency.ZRX: 10, - currency.PST: 5, - currency.TNT: 20, - currency.LLT: 10, - currency.DNT: 1, - currency.DPY: 2, - currency.BCDN: 20, - currency.STORJ: 3, - currency.OMG: 0.2, - currency.PAY: 1, - currency.EOS: 0.1, - currency.EON: 20, + currency.BIT: 45, + currency.AR: 0.5, + currency.HT: 1.1, + currency.AMP: 7000, + currency.CHZ: 16, + currency.GT: 0.22, + currency.TENSET: 0.22, + currency.ZIL: 48, + currency.BAT: 12, + currency.BTG: 0.061, + currency.GMT: 5.1, + currency.ENJ: 42, + currency.STX: 5, + currency.CAKE: 0.77, + currency.KSM: 0.032, + currency.WAVES: 0.36, + currency.DASH: 0.04, + currency.LRC: 59, + currency.CRV: 18, + currency.FXS: 7.8, + currency.CVX: 3.8, + currency.CVX: 3.3, + currency.RVN: 1, + currency.CELO: 1.9, + currency.CEL: 25, + currency.QTUM: 0.47, + currency.KAVA: 1.1, + currency.XEM: 25, + currency.ONEINCH: 6.6, + currency.XAUT: 0.0028, + currency.ROSE: 0.1, + currency.GNO: 0.16, + currency.GALA: 440, + currency.NEXO: 34, + currency.COMP: 0.47, + currency.HOT: 2200, + currency.DCR: 0.073, + currency.OP: 1.1, + currency.ENS: 1.9, + currency.SRM: 24, + currency.YFI: 0.0022, + currency.TFUEL: 34, + currency.IOST: 140, + currency.TWT: 2.1, + currency.IOTX: 60, + currency.LPT: 2.2, + currency.ZRX: 72, + currency.SYN: 17, + currency.ONE: 85, + currency.SUSHI: 16, + currency.SAFEMOON: 28000000, + currency.IMX: 22, + currency.JST: 170, + currency.DYDX: 40, + currency.GLM: 94, + currency.LUNA: 2.7, + currency.AUDIO: 85, + currency.ICX: 6.5, + currency.ANKR: 840, + currency.ONT: 1, + currency.NU: 43, + currency.WAXP: 19, + currency.SC: 450, + currency.BAL: 4.3, + currency.ZEN: 0.15, + currency.SGB: 77, + currency.SKL: 830, + currency.EURT: 29, + currency.UMA: 15, + currency.XCH: 0.046, + currency.FEI: 28, + currency.HIVE: 3.8, + currency.SCRT: 1.8, + currency.ELON: 70000000, + currency.CSPR: 62, + currency.SLP: 5700, + currency.MXC: 310, + currency.NFT: 8100000, + currency.BTCST: 0.22, + currency.ASTR: 44, + currency.PLA: 68, + currency.LSK: 0.11, + currency.FX: 16, + currency.YGG: 5.9, + currency.METIS: 0.1, + currency.CKB: 450, + currency.REN: 180, + currency.RLY: 570, + currency.FLUX: 10, + currency.PROM: 3.3, + currency.RACA: 7400, + currency.XYO: 2500, + currency.ACA: 7.3, + currency.SUSD: 54, + currency.RSR: 3900, + currency.NEST: 1000, + currency.ORBS: 580, + currency.WIN: 38000, + currency.ERG: 0.93, + currency.SNT: 1700, + currency.WRX: 7.4, + currency.CHR: 120, + currency.MED: 100, + currency.BNT: 46, + currency.CVC: 160, + currency.SYS: 11, + currency.CELR: 1300, + currency.FLOKI: 3100000, + currency.COTI: 240, + currency.CFX: 0.01, + currency.API3: 13, + currency.PUNDIX: 68, + currency.OGN: 130, + currency.RAY: 5.9, + currency.NMR: 0.29, + currency.POWR: 100, + currency.DENT: 24000, + currency.VTHO: 1000, + currency.MBOX: 4.4, + currency.DKA: 930, + currency.VGX: 70, + currency.REQ: 38, + currency.CTSI: 150, + currency.KEEP: 39, + currency.STRAX: 5, + currency.STEEM: 8, + currency.RAD: 11, + currency.STORJ: 7.4, + currency.MLK: 0.5, + currency.VLX: 48, + currency.BOBA: 49, + currency.C98: 9.8, + currency.INJ: 1.4, + currency.XVS: 0.46, + currency.MTL: 18, + currency.FUN: 4000, + currency.BFC: 320, + currency.OCEAN: 190, + currency.UOS: 15, + currency.RENBTC: 0.0012, + currency.MULTI: 5.5, + currency.RBN: 97, + currency.ILV: 0.043, + currency.ILM: 170, + currency.FLM: 9, + currency.HUSD: 27, + currency.EFI: 27, + currency.MDX: 56, + currency.YFII: 0.011, + currency.ELF: 12, + currency.MASK: 15, + currency.SFUND: 1.4, + currency.ACH: 320, + currency.QKC: 180, + currency.STMX: 3200, + currency.ANT: 12, + currency.TRIBE: 170, + currency.BAND: 1.1, + currency.MOVR: 0.14, + currency.DODO: 150, + currency.RLC: 28, + currency.DOCK: 74, + currency.NKN: 19, + currency.OXT: 210, currency.IQ: 20, - currency.EOSDAC: 20, - currency.TIPS: 100, - currency.XRP: 1, - currency.CNC: 0.1, - currency.TIX: 0.1, - currency.XMR: 0.05, - currency.BTS: 1, - currency.XTC: 10, - currency.BU: 0.1, - currency.DCR: 0.02, - currency.BCN: 10, - currency.XMC: 0.05, - currency.PPS: 0.01, - currency.BOE: 5, - currency.PLY: 10, - currency.MEDX: 100, - currency.TRX: 0.1, - currency.SMT_ETH: 50, - currency.CS: 10, - currency.MAN: 10, - currency.REM: 10, - currency.LYM: 10, - currency.INSTAR: 10, - currency.BFT: 10, - currency.IHT: 10, - currency.SENC: 10, - currency.TOMO: 10, - currency.ELEC: 10, - currency.SHIP: 10, - currency.TFD: 10, - currency.HAV: 10, - currency.HUR: 10, - currency.LST: 10, - currency.LINO: 10, - currency.SWTH: 5, - currency.NKN: 5, - currency.SOUL: 5, - currency.GALA_NEO: 5, - currency.LRN: 5, - currency.ADD: 20, - currency.MEETONE: 5, - currency.DOCK: 20, - currency.GSE: 20, - currency.RATING: 20, - currency.HSC: 100, - currency.HIT: 100, - currency.DX: 100, - currency.BXC: 100, - currency.PAX: 5, - currency.GARD: 100, - currency.FTI: 100, - currency.SOP: 100, - currency.LEMO: 20, + currency.UFO: 9600000, + currency.TRB: 0.18, + currency.REP: 4, + currency.HERO: 1500, + currency.AKT: 5.2, + currency.GHST: 47, + currency.UTK: 180, + currency.KP3R: 0.16, + currency.BAKE: 9.3, + currency.BETA: 180, + currency.AUCTION: 3.1, + currency.PERP: 28, + currency.BOND: 2.9, + currency.RIDE: 10, + currency.XVG: 550, + currency.FET: 23, + currency.DUSK: 34, + currency.SSV: 2.9, + currency.BCN: 2100, + currency.POLS: 42, + currency.TALK: 59, + currency.VRA: 6000, + currency.POND: 1900, + currency.RGT: 2.1, + currency.ATA: 120, + currency.ALCX: 0.71, + currency.AERGO: 210, + currency.MNGO: 100, + currency.OUSD: 32, + currency.TOMO: 3.4, + currency.COCOS: 2.6, + currency.IDEX: 65, + currency.VEGA: 12, + currency.CUSD: 2, + currency.TT: 1, + currency.WNXM: 1.4, + currency.NSBT: 0.3, + currency.CQT: 200, + currency.WOZX: 280, + currency.BEL: 32, + currency.FORTH: 4.6, + currency.ALICE: 8.9, + currency.KISHU: 2000000000, + currency.ALEPH: 96, + currency.UNFI: 3.9, + currency.ORN: 18, + currency.SUPER: 170, + currency.STARL: 5300000, + currency.BADGER: 13, + currency.JASMY: 520, + currency.DG: 320, + currency.RARE: 98, + currency.XPR: 530, + currency.PHA: 200, + currency.MFT: 5700, + currency.SAMO: 410, + currency.SFP: 7.7, + currency.ALPACA: 11, + currency.GAS: 0.69, + currency.TORN: 0.95, + currency.DNT: 920, + currency.ANC: 44, + currency.MLN: 0.18, + currency.KAR: 3.4, + currency.FARM: 0.41, + currency.LTO: 290, + currency.HYDRA: 0.67, + currency.QASH: 540, + currency.AE: 21, + currency.LINA: 3700, + currency.ARPA: 680, + currency.AQT: 20, + currency.XCAD: 3.3, + currency.DIA: 55, + currency.LIT: 26, + currency.AVA: 2.9, + currency.BZZ: 41, + currency.AGLD: 51, + currency.BLZ: 250, + currency.BCD: 11, + currency.CEUR: 2, + currency.NOIA: 390, + currency.FINE: 110, + currency.ERN: 12, + currency.RMRK: 0.57, + currency.MIR: 120, + currency.BTS: 170, + currency.CHESS: 7.3, + currency.HNS: 32, + currency.FIO: 38, + currency.IRIS: 83, + currency.RFR: 8600, + currency.RARI: 7.9, + currency.FIDA: 9.9, + currency.QRDO: 75, + currency.GYEN: 1000, + currency.SPS: 50, + currency.KEY: 5400, + currency.ATM: 1, + currency.SOUL: 7.5, + currency.PRQ: 160, + currency.FRONT: 81, + currency.NCT: 1400, + currency.PSG: 0.33, + currency.BOO: .7, + currency.RSV: 29, + currency.CUDOS: 600, currency.NPXS: 40, - currency.QKC: 20, - currency.IOTX: 20, - currency.RED: 20, - currency.LBA: 20, - currency.KAN: 20, - currency.OPEN: 20, - currency.MITH: 20, - currency.SKM: 20, - currency.XVG: 20, - currency.NANO: 20, - currency.NBAI: 20, + currency.OM: 92, + currency.ADX: 27, + currency.AUTO: .0087, + currency.SAITO: 2000, + currency.COS: 270, + currency.VELO: 99, + currency.FIS: 4.6, + currency.NULS: 8.2, currency.UPP: 20, - currency.ATMI: 20, - currency.TMT: 20, - currency.HT: 1, - currency.BNB: 0.3, - currency.BBK: 20, - currency.EDR: 20, - currency.MET: 0.3, - currency.TCT: 20, - currency.EXC: 10, + currency.XDB: .01, + currency.LUFFY: 140000000000, + currency.TKO: 9.7, + currency.KIN: 410000, + currency.GFI: 22, + currency.MIX: 6100, + currency.TIME: .014, + currency.HOPR: 570, + currency.BEAM: 11, + currency.BTM: 160, + currency.OVR: 29, + currency.CITY: 1, + currency.CATE: 50000000, + currency.DEXE: 13, + currency.ORCA: 5.1, + currency.MDT: 150, + currency.PNK: 1500, + currency.QSP: 180, + currency.DVI: 65, + currency.DF: 610, + currency.INV: .24, + currency.TABOO: 45000, + currency.FSN: 8, + currency.SDN: 6.1, + currency.LON: 33, + currency.MITH: 850, + currency.ATLAS: 630, + currency.LAZIO: 1.1, + currency.MBL: 420, + currency.PNT: 100, + currency.WXT: 280, + currency.NBS: 390, + currency.WHALE: 14, + currency.BOA: 490, + currency.SWFTC: 11000, + currency.JUV: 1, + currency.MAPS: 130, + currency.ADP: 1600, + currency.AST: 60, + currency.EDEN: 190, + currency.WICC: 30, + currency.UFT: 110, + currency.ZKS: 380, + currency.CREAM: 1.5, + currency.MET: 26, + currency.RAI: 9.4, + currency.XAVA: 3.9, + currency.FOR: 1000, + currency.AVT: 18, + currency.SOV: 53, + currency.SOS: 78000000, + currency.LSS: 160, + currency.NFTX: .13, + currency.DEGO: 23, + currency.DERC: 82, + currency.CHAIN: 760, + currency.POLIS: 9.3, + currency.PDEX: 1.3, + currency.SUKU: 200, + currency.ARV: 20000, + currency.REVV: 1300, + currency.GO: 220, + currency.OOE: 83, + currency.EDG: 1300, + currency.STEP: 120, + currency.BORING: 480, + currency.STC: 55, + currency.OCC: 55, + currency.SHFT: 84, + currency.AIR: 79, + currency.URUS: 1.2, + currency.SLIM: 51, + currency.HAI: 100, + currency.ZCN: 120, + currency.ABT: 53, + currency.NWC: 140, + currency.STAKE: 2.7, + currency.OPUL: 60, + currency.RBC: 340, + currency.BAO: 230000, + currency.TCT: 1600, + currency.WTC: .2, + currency.NUM: 730, + currency.DRGN: 1100, + currency.POSI: 99, + currency.TROY: 6100, + currency.ASR: 1, + currency.TBTC: .0011, + currency.GEL: 11, + currency.GRIN: 28, + currency.AFC: 1, + currency.KAN: 20, + currency.OG: 1, + currency.XED: 340, + currency.FEVR: 2900, + currency.HEGIC: 510, + currency.SBR: 810, + currency.HAPI: 2.6, + currency.PING: 33000, + currency.REF: 12, + currency.BUY: 100, + currency.INSUR: 290, + currency.PUSH: 79, } -// WebsocketRequest defines the initial request in JSON -type WebsocketRequest struct { - ID int64 `json:"id"` - Method string `json:"method"` - Params []interface{} `json:"params"` - Channels []stream.ChannelSubscription `json:"-"` // used for tracking associated channel subs on batched requests +// CurrencyInfo represents currency details with permission. +type CurrencyInfo struct { + Currency string `json:"currency"` + Delisted bool `json:"delisted"` + WithdrawDisabled bool `json:"withdraw_disabled"` + WithdrawDelayed bool `json:"withdraw_delayed"` + DepositDisabled bool `json:"deposit_disabled"` + TradeDisabled bool `json:"trade_disabled"` + FixedFeeRate float64 `json:"fixed_rate,omitempty,string"` + Chain string `json:"chain"` } -// WebsocketResponse defines a websocket response from gateio -type WebsocketResponse struct { - Time int64 `json:"time"` - Channel string `json:"channel"` - Error WebsocketError `json:"error"` - Result json.RawMessage `json:"result"` - ID int64 `json:"id"` - Method string `json:"method"` - Params []json.RawMessage `json:"params"` +// CurrencyPairDetail represents a single currency pair detail. +type CurrencyPairDetail struct { + ID string `json:"id"` + Base string `json:"base"` + Quote string `json:"quote"` + TradingFee gateioNumericalValue `json:"fee"` + MinBaseAmount gateioNumericalValue `json:"min_base_amount"` + MinQuoteAmount gateioNumericalValue `json:"min_quote_amount"` + AmountPrecision float64 `json:"amount_precision"` // Amount scale + Precision float64 `json:"precision"` // Price scale + TradeStatus string `json:"trade_status"` + SellStart float64 `json:"sell_start"` + BuyStart float64 `json:"buy_start"` } -// WebsocketError defines a websocket error type -type WebsocketError struct { - Code int64 `json:"code"` - Message string `json:"message"` +// Ticker holds detail ticker information for a currency pair +type Ticker struct { + CurrencyPair string `json:"currency_pair"` + Last gateioNumericalValue `json:"last"` + LowestAsk gateioNumericalValue `json:"lowest_ask"` + HighestBid gateioNumericalValue `json:"highest_bid"` + ChangePercentage string `json:"change_percentage"` + ChangeUtc0 string `json:"change_utc0"` + ChangeUtc8 string `json:"change_utc8"` + BaseVolume gateioNumericalValue `json:"base_volume"` + QuoteVolume gateioNumericalValue `json:"quote_volume"` + High24H gateioNumericalValue `json:"high_24h"` + Low24H gateioNumericalValue `json:"low_24h"` + EtfNetValue string `json:"etf_net_value"` + EtfPreNetValue string `json:"etf_pre_net_value"` + EtfPreTimestamp gateioTime `json:"etf_pre_timestamp"` + EtfLeverage gateioNumericalValue `json:"etf_leverage"` } -// WebsocketTicker defines ticker data -type WebsocketTicker struct { - Period int64 `json:"period"` - Open float64 `json:"open,string"` - Close float64 `json:"close,string"` - High float64 `json:"high,string"` - Low float64 `json:"Low,string"` - Last float64 `json:"last,string"` - Change float64 `json:"change,string"` - QuoteVolume float64 `json:"quoteVolume,string"` - BaseVolume float64 `json:"baseVolume,string"` +// OrderbookData holds orderbook ask and bid datas. +type OrderbookData struct { + ID int64 `json:"id"` + Current gateioTime `json:"current"` // The timestamp of the response data being generated (in milliseconds) + Update gateioTime `json:"update"` // The timestamp of when the orderbook last changed (in milliseconds) + Asks [][2]string `json:"asks"` + Bids [][2]string `json:"bids"` } -// WebsocketTrade defines trade data -type WebsocketTrade struct { - ID int64 `json:"id"` - Time float64 `json:"time"` - Price float64 `json:"price,string"` +// MakeOrderbook parse Orderbook asks/bids Price and Amount and create an Orderbook Instance with asks and bids data in []OrderbookItem. +func (a *OrderbookData) MakeOrderbook() (*Orderbook, error) { + ob := &Orderbook{ + ID: a.ID, + Current: a.Current.Time(), + Update: a.Update.Time(), + } + ob.Asks = make([]OrderbookItem, len(a.Asks)) + ob.Bids = make([]OrderbookItem, len(a.Bids)) + for x := range a.Asks { + price, err := strconv.ParseFloat(a.Asks[x][0], 64) + if err != nil { + return nil, err + } + amount, err := strconv.ParseFloat(a.Asks[x][1], 64) + if err != nil { + return nil, err + } + ob.Asks[x] = OrderbookItem{ + Price: price, + Amount: amount, + } + } + for x := range a.Bids { + price, err := strconv.ParseFloat(a.Bids[x][0], 64) + if err != nil { + return nil, err + } + amount, err := strconv.ParseFloat(a.Bids[x][1], 64) + if err != nil { + return nil, err + } + ob.Bids[x] = OrderbookItem{ + Price: price, + Amount: amount, + } + } + return ob, nil +} + +// OrderbookItem stores an orderbook item +type OrderbookItem struct { + Price float64 `json:"p"` + Amount float64 `json:"s"` +} + +// Orderbook stores the orderbook data +type Orderbook struct { + ID int64 `json:"id"` + Current time.Time `json:"current"` // The timestamp of the response data being generated (in milliseconds) + Update time.Time `json:"update"` // The timestamp of when the orderbook last changed (in milliseconds) + Bids []OrderbookItem `json:"bids"` + Asks []OrderbookItem `json:"asks"` +} + +// Trade represents market trade. +type Trade struct { + ID int64 `json:"id,string"` + TradingTime gateioTime `json:"create_time"` + CreateTimeMs gateioTime `json:"create_time_ms"` + OrderID string `json:"order_id"` + Side string `json:"side"` + Role string `json:"role"` + Amount float64 `json:"amount,string"` + Price float64 `json:"price,string"` + Fee float64 `json:"fee,string"` + FeeCurrency string `json:"fee_currency"` + PointFee string `json:"point_fee"` + GtFee string `json:"gt_fee"` +} + +// Candlestick represents candlestick data point detail. +type Candlestick struct { + Timestamp time.Time + QuoteCcyVolume float64 + ClosePrice float64 + HighestPrice float64 + LowestPrice float64 + OpenPrice float64 + BaseCcyAmount float64 +} + +// CurrencyChain currency chain detail. +type CurrencyChain struct { + Chain string `json:"chain"` + ChineseChainName string `json:"name_cn"` + ChainName string `json:"name_en"` + IsDisabled int64 `json:"is_disabled"` // If it is disabled. 0 means NOT being disabled + IsDepositDisabled int64 `json:"is_deposit_disabled"` // Is deposit disabled. 0 means not + IsWithdrawDisabled int64 `json:"is_withdraw_disabled"` // Is withdrawal disabled. 0 means not +} + +// MarginCurrencyPairInfo represents margin currency pair detailed info. +type MarginCurrencyPairInfo struct { + ID string `json:"id"` + Base string `json:"base"` + Quote string `json:"quote"` + Leverage float64 `json:"leverage"` + MinBaseAmount float64 `json:"min_base_amount,string"` + MinQuoteAmount float64 `json:"min_quote_amount,string"` + MaxQuoteAmount float64 `json:"max_quote_amount,string"` + Status int32 `json:"status"` +} + +// OrderbookOfLendingLoan represents order book of lending loans +type OrderbookOfLendingLoan struct { + Rate float64 `json:"rate,string"` Amount float64 `json:"amount,string"` - Type string `json:"type"` + Days int64 `json:"days"` } -// WebsocketBalance holds a slice of WebsocketBalanceCurrency -type WebsocketBalance struct { - Currency []WebsocketBalanceCurrency +// FuturesContract represents futures contract detailed data. +type FuturesContract struct { + Name string `json:"name"` + Type string `json:"type"` + QuantoMultiplier float64 `json:"quanto_multiplier,string"` + RefDiscountRate float64 `json:"ref_discount_rate,string"` + OrderPriceDeviate string `json:"order_price_deviate"` + MaintenanceRate float64 `json:"maintenance_rate,string"` + MarkType string `json:"mark_type"` + LastPrice float64 `json:"last_price,string"` + MarkPrice float64 `json:"mark_price,string"` + IndexPrice float64 `json:"index_price,string"` + FundingRateIndicative string `json:"funding_rate_indicative"` + MarkPriceRound string `json:"mark_price_round"` + FundingOffset int64 `json:"funding_offset"` + InDelisting bool `json:"in_delisting"` + RiskLimitBase string `json:"risk_limit_base"` + InterestRate string `json:"interest_rate"` + OrderPriceRound string `json:"order_price_round"` + OrderSizeMin int64 `json:"order_size_min"` + RefRebateRate string `json:"ref_rebate_rate"` + FundingInterval int64 `json:"funding_interval"` + RiskLimitStep string `json:"risk_limit_step"` + LeverageMin string `json:"leverage_min"` + LeverageMax string `json:"leverage_max"` + RiskLimitMax string `json:"risk_limit_max"` + MakerFeeRate float64 `json:"maker_fee_rate,string"` + TakerFeeRate float64 `json:"taker_fee_rate,string"` + FundingRate float64 `json:"funding_rate,string"` + OrderSizeMax int64 `json:"order_size_max"` + FundingNextApply gateioTime `json:"funding_next_apply"` + ConfigChangeTime gateioTime `json:"config_change_time"` + ShortUsers int64 `json:"short_users"` + TradeSize int64 `json:"trade_size"` + PositionSize int64 `json:"position_size"` + LongUsers int64 `json:"long_users"` + FundingImpactValue string `json:"funding_impact_value"` + OrdersLimit int64 `json:"orders_limit"` + TradeID int64 `json:"trade_id"` + OrderbookID int64 `json:"orderbook_id"` } -// WebsocketBalanceCurrency contains currency name funds available and frozen -type WebsocketBalanceCurrency struct { - Currency string - Available string `json:"available"` - Locked string `json:"freeze"` +// TradingHistoryItem represents futures trading history item. +type TradingHistoryItem struct { + ID int64 `json:"id"` + CreateTime gateioTime `json:"create_time"` + Contract string `json:"contract"` + Text string `json:"text"` + Size float64 `json:"size"` + Price float64 `json:"price,string"` + // Added for Derived market trade history datas. + Fee float64 `json:"fee,string"` + PointFee float64 `json:"point_fee,string"` + Role string `json:"role"` } -// WebSocketOrderQueryResult data returned from a websocket ordre query holds slice of WebSocketOrderQueryRecords -type WebSocketOrderQueryResult struct { - Error WebsocketError `json:"error"` - Limit int `json:"limit"` - Offset int `json:"offset"` - Total int `json:"total"` - WebSocketOrderQueryRecords []WebSocketOrderQueryRecords `json:"records"` +// FuturesCandlestick represents futures candlestick data +type FuturesCandlestick struct { + Timestamp gateioTime `json:"t"` + Volume float64 `json:"v"` + ClosePrice float64 `json:"c,string"` + HighestPrice float64 `json:"h,string"` + LowestPrice float64 `json:"l,string"` + OpenPrice float64 `json:"o,string"` + + // Added for websocket push data + Name string `json:"n,omitempty"` } -// WebSocketOrderQueryRecords contains order information from a order.query websocket request -type WebSocketOrderQueryRecords struct { - ID int64 `json:"id"` - Market string `json:"market"` - User int64 `json:"user"` - Ctime float64 `json:"ctime"` - Mtime float64 `json:"mtime"` - Price float64 `json:"price,string"` - Amount float64 `json:"amount,string"` - Left float64 `json:"left,string"` - DealFee float64 `json:"dealFee,string"` - OrderType int64 `json:"orderType"` - Type int64 `json:"type"` - FilledAmount float64 `json:"filledAmount,string"` - FilledTotal float64 `json:"filledTotal,string"` +// FuturesPremiumIndexKLineResponse represents premium index K-Line information. +type FuturesPremiumIndexKLineResponse struct { + UnixTimestamp gateioTime `json:"t"` + ClosePrice float64 `json:"c,string"` + HighestPrice float64 `json:"h,string"` + LowestPrice float64 `json:"l,string"` + OpenPrice float64 `json:"o,string"` } -// WebsocketAuthenticationResponse contains the result of a login request -type WebsocketAuthenticationResponse struct { - Error struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` - Result struct { - Status string `json:"status"` - } `json:"result"` +// FuturesTicker represents futures ticker data. +type FuturesTicker struct { + Contract string `json:"contract"` + ChangePercentage string `json:"change_percentage"` + Last float64 `json:"last,string"` + Low24H float64 `json:"low_24h,string"` + High24H float64 `json:"high_24h,string"` + TotalSize float64 `json:"total_size,string"` + Volume24H float64 `json:"volume_24h,string"` + Volume24HBtc float64 `json:"volume_24h_btc,string"` + Volume24HUsd float64 `json:"volume_24h_usd,string"` + Volume24HBase float64 `json:"volume_24h_base,string"` + Volume24HQuote float64 `json:"volume_24h_quote,string"` + Volume24HSettle float64 `json:"volume_24h_settle,string"` + MarkPrice float64 `json:"mark_price,string"` + FundingRate float64 `json:"funding_rate,string"` + FundingRateIndicative string `json:"funding_rate_indicative"` + IndexPrice float64 `json:"index_price,string"` +} + +// FuturesFundingRate represents futures funding rate response. +type FuturesFundingRate struct { + Timestamp gateioTime `json:"t"` + Rate gateioNumericalValue `json:"r"` +} + +// InsuranceBalance represents futures insurance balance item. +type InsuranceBalance struct { + Timestamp gateioTime `json:"t"` + Balance float64 `json:"b"` +} + +// ContractStat represents futures stats +type ContractStat struct { + Time gateioTime `json:"time"` + LongShortTaker float64 `json:"lsr_taker"` + LongShortAccount float64 `json:"lsr_account"` + LongLiqSize float64 `json:"long_liq_size"` + ShortLiquidationSize float64 `json:"short_liq_size"` + OpenInterest float64 `json:"open_interest"` + ShortLiquidationUsd float64 `json:"short_liq_usd"` + MarkPrice float64 `json:"mark_price"` + TopLongShortSize float64 `json:"top_lsr_size"` + ShortLiquidationAmount float64 `json:"short_liq_amount"` + LongLiquidiationAmount float64 `json:"long_liq_amount"` + OpenInterestUsd float64 `json:"open_interest_usd"` + TopLongShortAccount float64 `json:"top_lsr_account"` + LongLiquidationUSD float64 `json:"long_liq_usd"` +} + +// IndexConstituent represents index constituents +type IndexConstituent struct { + Index string `json:"index"` + Constituents []struct { + Exchange string `json:"exchange"` + Symbols []string `json:"symbols"` + } `json:"constituents"` +} + +// LiquidationHistory represents liquidation history for a specifies settle. +type LiquidationHistory struct { + Time gateioTime `json:"time"` + Contract string `json:"contract"` + Size int64 `json:"size"` + Leverage string `json:"leverage"` + Margin string `json:"margin"` + EntryPrice float64 `json:"entry_price,string"` + LiquidationPrice gateioNumericalValue `json:"liq_price"` + MarkPrice float64 `json:"mark_price,string"` + OrderID int64 `json:"order_id"` + OrderPrice float64 `json:"order_price,string"` + FillPrice float64 `json:"fill_price,string"` + Left int64 `json:"left"` +} + +// DeliveryContract represents a delivery contract instance detail. +type DeliveryContract struct { + Name string `json:"name"` + Underlying string `json:"underlying"` + Cycle string `json:"cycle"` + Type string `json:"type"` + QuantoMultiplier string `json:"quanto_multiplier"` + MarkType string `json:"mark_type"` + LastPrice float64 `json:"last_price,string"` + MarkPrice float64 `json:"mark_price,string"` + IndexPrice float64 `json:"index_price,string"` + BasisRate string `json:"basis_rate"` + BasisValue string `json:"basis_value"` + BasisImpactValue string `json:"basis_impact_value"` + SettlePrice float64 `json:"settle_price,string"` + SettlePriceInterval int64 `json:"settle_price_interval"` + SettlePriceDuration int64 `json:"settle_price_duration"` + SettleFeeRate string `json:"settle_fee_rate"` + OrderPriceRound string `json:"order_price_round"` + MarkPriceRound string `json:"mark_price_round"` + LeverageMin string `json:"leverage_min"` + LeverageMax string `json:"leverage_max"` + MaintenanceRate string `json:"maintenance_rate"` + RiskLimitBase string `json:"risk_limit_base"` + RiskLimitStep string `json:"risk_limit_step"` + RiskLimitMax string `json:"risk_limit_max"` + MakerFeeRate string `json:"maker_fee_rate"` + TakerFeeRate string `json:"taker_fee_rate"` + RefDiscountRate string `json:"ref_discount_rate"` + RefRebateRate string `json:"ref_rebate_rate"` + OrderPriceDeviate string `json:"order_price_deviate"` + OrderSizeMin int64 `json:"order_size_min"` + OrderSizeMax int64 `json:"order_size_max"` + OrdersLimit int64 `json:"orders_limit"` + OrderbookID int64 `json:"orderbook_id"` + TradeID int64 `json:"trade_id"` + TradeSize int64 `json:"trade_size"` + PositionSize int64 `json:"position_size"` + ExpireTime gateioTime `json:"expire_time"` + ConfigChangeTime gateioTime `json:"config_change_time"` + InDelisting bool `json:"in_delisting"` +} + +// DeliveryTradingHistory represents futures trading history +type DeliveryTradingHistory struct { + ID int64 `json:"id"` + CreateTime gateioTime `json:"create_time"` + Contract string `json:"contract"` + Size float64 `json:"size"` + Price float64 `json:"price,string"` +} + +// OptionUnderlying represents option underlying and it's index price. +type OptionUnderlying struct { + Name string `json:"name"` + IndexPrice float64 `json:"index_price,string"` + IndexTime gateioTime `json:"index_time"` +} + +// OptionContract represents an option contract detail. +type OptionContract struct { + Name string `json:"name"` + Tag string `json:"tag"` + IsCall bool `json:"is_call"` + StrikePrice float64 `json:"strike_price,string"` + LastPrice float64 `json:"last_price,string"` + MarkPrice float64 `json:"mark_price,string"` + OrderbookID int64 `json:"orderbook_id"` + TradeID int64 `json:"trade_id"` + TradeSize int64 `json:"trade_size"` + PositionSize int64 `json:"position_size"` + Underlying string `json:"underlying"` + UnderlyingPrice float64 `json:"underlying_price,string"` + Multiplier string `json:"multiplier"` + OrderPriceRound string `json:"order_price_round"` + MarkPriceRound string `json:"mark_price_round"` + MakerFeeRate string `json:"maker_fee_rate"` + TakerFeeRate string `json:"taker_fee_rate"` + PriceLimitFeeRate string `json:"price_limit_fee_rate"` + RefDiscountRate string `json:"ref_discount_rate"` + RefRebateRate string `json:"ref_rebate_rate"` + OrderPriceDeviate string `json:"order_price_deviate"` + OrderSizeMin int64 `json:"order_size_min"` + OrderSizeMax int64 `json:"order_size_max"` + OrdersLimit int64 `json:"orders_limit"` + CreateTime gateioTime `json:"create_time"` + ExpirationTime gateioTime `json:"expiration_time"` +} + +// OptionSettlement list settlement history +type OptionSettlement struct { + Timestamp gateioTime `json:"time"` + Profit gateioNumericalValue `json:"profit"` + Fee gateioNumericalValue `json:"fee"` + SettlePrice float64 `json:"settle_price,string"` + Contract string `json:"contract"` + StrikePrice float64 `json:"strike_price,string"` +} + +// SwapCurrencies represents Flash Swap supported currencies +type SwapCurrencies struct { + Currency string `json:"currency"` + MinAmount float64 `json:"min_amount,string"` + MaxAmount float64 `json:"max_amount,string"` + Swappable []string `json:"swappable"` +} + +// MyOptionSettlement represents option private settlement +type MyOptionSettlement struct { + Size float64 `json:"size"` + SettleProfit float64 `json:"settle_profit,string"` + Contract string `json:"contract"` + StrikePrice float64 `json:"strike_price,string"` + Time time.Time `json:"time"` + SettlePrice float64 `json:"settle_price,string"` + Underlying string `json:"underlying"` + RealisedPnl string `json:"realised_pnl"` + Fee float64 `json:"fee,string"` +} + +// OptionsTicker represents tickers of options contracts +type OptionsTicker struct { + Name string `json:"name"` + LastPrice gateioNumericalValue `json:"last_price"` + MarkPrice gateioNumericalValue `json:"mark_price"` + PositionSize float64 `json:"position_size"` + Ask1Size float64 `json:"ask1_size"` + Ask1Price float64 `json:"ask1_price,string"` + Bid1Size float64 `json:"bid1_size"` + Bid1Price float64 `json:"bid1_price,string"` + Vega string `json:"vega"` + Theta string `json:"theta"` + Rho string `json:"rho"` + Gamma string `json:"gamma"` + Delta string `json:"delta"` + MarkImpliedVolatility gateioNumericalValue `json:"mark_iv"` + BidImpliedVolatility gateioNumericalValue `json:"bid_iv"` + AskImpliedVolatility gateioNumericalValue `json:"ask_iv"` + Leverage gateioNumericalValue `json:"leverage"` + + // Added fields for the websocket + IndexPrice gateioNumericalValue `json:"index_price"` +} + +// OptionsUnderlyingTicker represents underlying ticker +type OptionsUnderlyingTicker struct { + TradePut float64 `json:"trade_put"` + TradeCall float64 `json:"trade_call"` + IndexPrice float64 `json:"index_price,string"` +} + +// OptionAccount represents option account. +type OptionAccount struct { + User int64 `json:"user"` + Currency string `json:"currency"` + ShortEnabled bool `json:"short_enabled"` + Total float64 `json:"total,string"` + UnrealisedPnl string `json:"unrealised_pnl"` + InitMargin string `json:"init_margin"` + MaintMargin string `json:"maint_margin"` + OrderMargin string `json:"order_margin"` + Available float64 `json:"available,string"` + Point string `json:"point"` +} + +// AccountBook represents account changing history item +type AccountBook struct { + ChangeTime gateioTime `json:"time"` + AccountChange float64 `json:"change,string"` + Balance float64 `json:"balance,string"` + CustomText string `json:"text"` + ChangingType string `json:"type"` +} + +// UsersPositionForUnderlying represents user's position for specified underlying. +type UsersPositionForUnderlying struct { + User int64 `json:"user"` + Contract string `json:"contract"` + Size int64 `json:"size"` + EntryPrice float64 `json:"entry_price,string"` + RealisedPnl float64 `json:"realised_pnl,string"` + MarkPrice float64 `json:"mark_price,string"` + UnrealisedPnl float64 `json:"unrealised_pnl,string"` + PendingOrders int64 `json:"pending_orders"` + CloseOrder struct { + ID int64 `json:"id"` + Price float64 `json:"price,string"` + IsLiq bool `json:"is_liq"` + } `json:"close_order"` +} + +// ContractClosePosition represents user's liquidation history +type ContractClosePosition struct { + PositionCloseTime gateioTime `json:"time"` + Pnl float64 `json:"pnl,string"` + SettleSize string `json:"settle_size"` + Side string `json:"side"` // Position side, long or short + FuturesContract string `json:"contract"` + CloseOrderText string `json:"text"` +} + +// OptionOrderParam represents option order request body +type OptionOrderParam struct { + OrderSize float64 `json:"size"` // Order size. Specify positive number to make a bid, and negative number to ask + Iceberg float64 `json:"iceberg,omitempty"` // Display size for iceberg order. 0 for non-iceberg. Note that you will have to pay the taker fee for the hidden size + Contract string `json:"contract"` + Text string `json:"text,omitempty"` + TimeInForce string `json:"tif,omitempty"` + Price float64 `json:"price,string,omitempty"` + // Close Set as true to close the position, with size set to 0 + Close bool `json:"close,omitempty"` + ReduceOnly bool `json:"reduce_only,omitempty"` +} + +// OptionOrderResponse represents option order response detail +type OptionOrderResponse struct { + Status string `json:"status"` + Size float64 `json:"size"` + OptionOrderID int64 `json:"id"` + Iceberg int64 `json:"iceberg"` + IsOrderLiquidation bool `json:"is_liq"` + IsOrderPositionClose bool `json:"is_close"` + Contract string `json:"contract"` + Text string `json:"text"` + FillPrice float64 `json:"fill_price,string"` + FinishAs string `json:"finish_as"` // finish_as filled, cancelled, liquidated, ioc, auto_deleveraged, reduce_only, position_closed, reduce_out + Left float64 `json:"left"` + TimeInForce string `json:"tif"` + IsReduceOnly bool `json:"is_reduce_only"` + CreateTime gateioTime `json:"create_time"` + FinishTime gateioTime `json:"finish_time"` + Price float64 `json:"price,string"` + + TakerFee float64 `json:"tkrf,omitempty,string"` + MakerFee float64 `json:"mkrf,omitempty,string"` + ReferenceUserID string `json:"refu"` +} + +// OptionTradingHistory list personal trading history +type OptionTradingHistory struct { + ID int64 `json:"id"` + UnderlyingPrice float64 `json:"underlying_price,string"` + Size float64 `json:"size"` + Contract string `json:"contract"` + TradeRole string `json:"role"` + CreateTime gateioTime `json:"create_time"` + OrderID int64 `json:"order_id"` + Price float64 `json:"price,string"` +} + +// WithdrawalResponse represents withdrawal response +type WithdrawalResponse struct { + ID string `json:"id"` + Timestamp gateioTime `json:"timestamp"` + Currency string `json:"currency"` + WithdrawalAddress string `json:"address"` + TransactionID string `json:"txid"` + Amount float64 `json:"amount,string"` + Memo string `json:"memo"` + Status string `json:"status"` + Chain string `json:"chain"` + Fee float64 `json:"fee,string"` +} + +// WithdrawalRequestParam represents currency withdrawal request param. +type WithdrawalRequestParam struct { + Currency currency.Code `json:"currency"` + Amount float64 `json:"amount,string"` + Chain string `json:"chain,omitempty"` + + // Optional parameters + Address string `json:"address,omitempty"` + Memo string `json:"memo,omitempty"` +} + +// CurrencyDepositAddressInfo represents a crypto deposit address +type CurrencyDepositAddressInfo struct { + Currency string `json:"currency"` + Address string `json:"address"` + MultichainAddresses []MultiChainAddressItem `json:"multichain_addresses"` +} + +// MultiChainAddressItem represents a multi-chain address item +type MultiChainAddressItem struct { + Chain string `json:"chain"` + Address string `json:"address"` + PaymentID string `json:"payment_id"` + PaymentName string `json:"payment_name"` + ObtainFailed int64 `json:"obtain_failed"` +} + +// DepositRecord represents deposit record item +type DepositRecord struct { + ID string `json:"id"` + Timestamp time.Time `json:"timestamp"` + Currency string `json:"currency"` + Address string `json:"address"` + TransactionID string `json:"txid"` + Amount float64 `json:"amount,string"` + Memo string `json:"memo"` + Status string `json:"status"` + Chain string `json:"chain"` + Fee float64 `json:"fee,string"` +} + +// TransferCurrencyParam represents currency transfer. +type TransferCurrencyParam struct { + Currency currency.Code `json:"currency"` + From string `json:"from"` + To string `json:"to"` + Amount float64 `json:"amount,string"` + CurrencyPair currency.Pair `json:"currency_pair"` + Settle string `json:"settle"` +} + +// TransactionIDResponse represents transaction ID +type TransactionIDResponse struct { + TransactionID int64 `json:"tx_id"` +} + +// SubAccountTransferParam represents currency subaccount transfer request param +type SubAccountTransferParam struct { + Currency currency.Code `json:"currency"` + SubAccount string `json:"sub_account"` + Direction string `json:"direction"` + Amount float64 `json:"amount,string"` + SubAccountType string `json:"sub_account_type"` +} + +// SubAccountTransferResponse represents transfer records between main and sub accounts +type SubAccountTransferResponse struct { + MainAccountUserID string `json:"uid"` + Timestamp gateioTime `json:"timest"` + Source string `json:"source"` + Currency string `json:"currency"` + SubAccount string `json:"sub_account"` + TransferDirection string `json:"direction"` + Amount float64 `json:"amount,string"` + SubAccountType string `json:"sub_account_type"` +} + +// WithdrawalStatus represents currency withdrawal status +type WithdrawalStatus struct { + Currency string `json:"currency"` + CurrencyName string `json:"name"` + CurrencyNameChinese string `json:"name_cn"` + Deposit float64 `json:"deposit,string"` + WithdrawPercent string `json:"withdraw_percent"` + FixedWithdrawalFee float64 `json:"withdraw_fix,string"` + WithdrawDayLimit float64 `json:"withdraw_day_limit,string"` + WithdrawDayLimitRemain float64 `json:"withdraw_day_limit_remain,string"` + WithdrawAmountMini float64 `json:"withdraw_amount_mini,string"` + WithdrawEachTimeLimit float64 `json:"withdraw_eachtime_limit,string"` + WithdrawFixOnChains map[string]string `json:"withdraw_fix_on_chains"` + AdditionalProperties string `json:"additionalProperties"` +} + +// FuturesSubAccountBalance represents sub account balance for specific sub account and several currencies +type FuturesSubAccountBalance struct { + UserID int64 `json:"uid,string"` + Available struct { + Total float64 `json:"total,string"` + UnrealisedProfitAndLoss string `json:"unrealised_pnl"` + PositionMargin string `json:"position_margin"` + OrderMargin string `json:"order_margin"` + TotalAvailable float64 `json:"available,string"` + PointAmount float64 `json:"point"` + SettleCurrency string `json:"currency"` + InDualMode bool `json:"in_dual_mode"` + EnableCredit bool `json:"enable_credit"` + PositionInitialMargin string `json:"position_initial_margin"` // applicable to the portfolio margin account model + MaintenanceMarginPosition string `json:"maintenance_margin"` + PerpetualContractBonus string `json:"bonus"` + StatisticalData struct { + TotalDNW float64 `json:"dnw,string"` // total amount of deposit and withdraw + ProfitAndLoss float64 `json:"pnl,string"` // total amount of trading profit and loss + TotalAmountOfFee float64 `json:"fee,string"` + ReferrerRebates float64 `json:"refr,string"` // total amount of referrer rebates + Fund float64 `json:"fund,string"` // total amount of funding costs + PointDNW float64 `json:"point_dnw,string"` + PoointFee float64 `json:"point_fee,string"` + PointRefr float64 `json:"point_refr,string"` + BonusDNW float64 `json:"bonus_dnw,string"` + BonusOffset float64 `json:"bonus_offset,string"` + } `json:"history"` + } `json:"available"` +} + +// SubAccountMarginBalance represents sub account margin balance for specific sub account and several currencies +type SubAccountMarginBalance struct { + UID string `json:"uid"` + Available []struct { + CurrencyPair string `json:"currency_pair"` + Locked bool `json:"locked"` + Risk string `json:"risk"` + Base MarginCurrencyBalance `json:"base"` + Quote MarginCurrencyBalance `json:"quote"` + } `json:"available"` +} + +// MarginCurrencyBalance represents a currency balance detail information. +type MarginCurrencyBalance struct { + Currency string `json:"currency"` + Available float64 `json:"available,string"` + Locked float64 `json:"locked,string"` + BorrowedAmount float64 `json:"borrowed,string"` + UnpairInterest float64 `json:"interest,string"` +} + +// MarginAccountItem margin account item +type MarginAccountItem struct { + Locked bool `json:"locked"` + CurrencyPair string `json:"currency_pair"` + Risk string `json:"risk"` + Base AccountBalanceInformation `json:"base"` + Quote AccountBalanceInformation `json:"quote"` +} + +// AccountBalanceInformation represents currency account balace information. +type AccountBalanceInformation struct { + Available float64 `json:"available,string"` + Borrowed float64 `json:"borrowed,string"` + Interest float64 `json:"interest,string"` + Currency string `json:"currency"` + LockedAmount float64 `json:"locked,string"` +} + +// MarginAccountBalanceChangeInfo represents margin account balance +type MarginAccountBalanceChangeInfo struct { + ID string `json:"id"` + Time gateioTime `json:"time"` + TimeMs gateioTime `json:"time_ms"` + Currency string `json:"currency"` + CurrencyPair string `json:"currency_pair"` + AmountChanged string `json:"change"` + Balance string `json:"balance"` +} + +// MarginFundingAccountItem represents funding account list item. +type MarginFundingAccountItem struct { + Currency string `json:"currency"` + Available float64 `json:"available,string"` + LockedAmount float64 `json:"locked,string"` + Lent string `json:"lent"` // Outstanding loan amount yet to be repaid + TotalLent string `json:"total_lent"` // Amount used for lending. total_lent = lent + locked +} + +// MarginLoanRequestParam represents margin lend or borrow request param +type MarginLoanRequestParam struct { + Side string `json:"side"` + Currency currency.Code `json:"currency"` + Rate float64 `json:"rate,string,omitempty"` + Amount float64 `json:"amount,string"` + Days int64 `json:"days,omitempty"` + AutoRenew bool `json:"auto_renew,omitempty"` + CurrencyPair currency.Pair `json:"currency_pair,omitempty"` + FeeRate float64 `json:"fee_rate,string,omitempty"` + OrigID string `json:"orig_id,omitempty"` + Text string `json:"text,omitempty"` +} + +// MarginLoanResponse represents lending or borrow response. +type MarginLoanResponse struct { + ID string `json:"id"` + OrigID string `json:"orig_id,omitempty"` + Side string `json:"side"` + Currency string `json:"currency"` + Amount float64 `json:"amount,string"` + Rate float64 `json:"rate,string"` + Days int64 `json:"days,omitempty"` + AutoRenew bool `json:"auto_renew,omitempty"` + CurrencyPair string `json:"currency_pair,omitempty"` + FeeRate float64 `json:"fee_rate,string"` + Text string `json:"text,omitempty"` + CreateTime gateioTime `json:"create_time"` + ExpireTime gateioTime `json:"expire_time"` + Status string `json:"status"` + Left float64 `json:"left,string"` + Repaid float64 `json:"repaid,string"` + PaidInterest float64 `json:"paid_interest,string"` + UnpaidInterest float64 `json:"unpaid_interest,string"` +} + +// SubAccountCrossMarginInfo represents subaccount's cross_margin account info +type SubAccountCrossMarginInfo struct { + UID string `json:"uid"` + Available struct { + UserID int64 `json:"user_id"` + Locked bool `json:"locked"` + Total float64 `json:"total,string"` + Borrowed float64 `json:"borrowed,string"` + Interest float64 `json:"interest,string"` // Total unpaid interests in USDT, i.e., the sum of all currencies' interest*price*discount + BorrowedNet string `json:"borrowed_net"` + TotalNetAssets float64 `json:"net,string"` + Leverage float64 `json:"leverage,string"` + Risk string `json:"risk"` + TotalInitialMargin float64 `json:"total_initial_margin,string"` + TotalMarginBalance float64 `json:"total_margin_balance,string"` + TotalMaintenanceMargin float64 `json:"total_maintenance_margin,string"` + TotalInitialMarginRate float64 `json:"total_initial_margin_rate,string"` + TotalMaintenanceMarginRate float64 `json:"total_maintenance_margin_rate,string"` + TotalAvailableMargin float64 `json:"total_available_margin,string"` + CurrencyBalances map[string]CrossMarginBalance `json:"balances"` + } `json:"available"` +} + +// CrossMarginBalance represents cross-margin currency balance detail +type CrossMarginBalance struct { + Available float64 `json:"available,string"` + Freeze float64 `json:"freeze,string"` + Borrowed float64 `json:"borrowed,string"` + Interest float64 `json:"interest,string"` + Total string `json:"total"` + BorrowedNet string `json:"borrowed_net"` + TotalNetAssetInUSDT string `json:"net"` + PositionLeverage string `json:"leverage"` + Risk string `json:"risk"` // Risk rate. When it belows 110%, liquidation will be triggered. Calculation formula: total / (borrowed+interest) +} + +// WalletSavedAddress represents currency saved address +type WalletSavedAddress struct { + Currency string `json:"currency"` + Chain string `json:"chain"` + Address string `json:"address"` + Name string `json:"name"` + Tag string `json:"tag"` + Verified string `json:"verified"` // Whether to pass the verification 0-unverified, 1-verified +} + +// PersonalTradingFee represents personal trading fee for specific currency pair +type PersonalTradingFee struct { + UserID int64 `json:"user_id"` + TakerFee float64 `json:"taker_fee,string"` + MakerFee float64 `json:"maker_fee,string"` + GtDiscount bool `json:"gt_discount"` + GtTakerFee float64 `json:"gt_taker_fee,string"` + GtMakerFee float64 `json:"gt_maker_fee,string"` + LoanFee float64 `json:"loan_fee,string"` + PointType string `json:"point_type"` + FuturesTakerFee float64 `json:"futures_taker_fee,string"` + FuturesMakerFee float64 `json:"futures_maker_fee,string"` +} + +// UsersAllAccountBalance represents user all account balances. +type UsersAllAccountBalance struct { + Details map[string]CurrencyBalanceAmount `json:"details"` + Total CurrencyBalanceAmount `json:"total"` +} + +// CurrencyBalanceAmount represents currency and its amount. +type CurrencyBalanceAmount struct { + Currency string `json:"currency"` + Amount string `json:"amount"` +} + +// SpotTradingFeeRate user trading fee rates +type SpotTradingFeeRate struct { + UserID int64 `json:"user_id"` + TakerFee float64 `json:"taker_fee,string"` + MakerFee float64 `json:"maker_fee,string"` + GtDiscount bool `json:"gt_discount"` + GtTakerFee float64 `json:"gt_taker_fee,string"` + GtMakerFee float64 `json:"gt_maker_fee,string"` + FuturesTakerFee float64 `json:"futures_taker_fee,string"` + FuturesMakerFee float64 `json:"futures_maker_fee,string"` + LoanFee float64 `json:"loan_fee,string"` + PointType string `json:"point_type"` +} + +// SpotAccount represents spot account +type SpotAccount struct { + Currency string `json:"currency"` + Available float64 `json:"available,string"` + Locked float64 `json:"locked,string"` +} + +// CreateOrderRequestData represents a single order creation param. +type CreateOrderRequestData struct { + Text string `json:"text,omitempty"` + CurrencyPair currency.Pair `json:"currency_pair,omitempty"` + Type string `json:"type,omitempty"` + Account string `json:"account,omitempty"` + Side string `json:"side,omitempty"` + Iceberg string `json:"iceberg,omitempty"` + Amount float64 `json:"amount,string,omitempty"` + Price float64 `json:"price,string,omitempty"` + TimeInForce string `json:"time_in_force,omitempty"` + AutoBorrow bool `json:"auto_borrow,omitempty"` +} + +// SpotOrder represents create order response. +type SpotOrder struct { + OrderID string `json:"id,omitempty"` + Text string `json:"text,omitempty"` + Succeeded bool `json:"succeeded"` + ErrorLabel string `json:"label,omitempty"` + Message string `json:"message,omitempty"` + CreateTime gateioTime `json:"create_time,omitempty"` + CreateTimeMs gateioTime `json:"create_time_ms,omitempty"` + UpdateTime gateioTime `json:"update_time,omitempty"` + UpdateTimeMs gateioTime `json:"update_time_ms,omitempty"` + CurrencyPair string `json:"currency_pair,omitempty"` + Status string `json:"status,omitempty"` + Type string `json:"type,omitempty"` + Account string `json:"account,omitempty"` + Side string `json:"side,omitempty"` + Amount float64 `json:"amount,omitempty,string"` + Price float64 `json:"price,omitempty,string"` + TimeInForce string `json:"time_in_force,omitempty"` + Iceberg string `json:"iceberg,omitempty"` + AutoRepay bool `json:"auto_repay"` + AutoBorrow bool `json:"auto_borrow"` + Left gateioNumericalValue `json:"left"` + AverageFillPrice float64 `json:"avg_deal_price,string"` + FeeDeducted float64 `json:"fee,string"` + FeeCurrency string `json:"fee_currency"` + FillPrice float64 `json:"fill_price,string"` // Total filled in quote currency. Deprecated in favor of filled_total + FilledTotal float64 `json:"filled_total,string"` // Total filled in quote currency + PointFee float64 `json:"point_fee,string"` + GtFee string `json:"gt_fee,omitempty"` + GtDiscount bool `json:"gt_discount"` + GtMakerFee float64 `json:"gt_maker_fee,string"` + GtTakerFee float64 `json:"gt_taker_fee,string"` + RebatedFee float64 `json:"rebated_fee,string"` + RebatedFeeCurrency string `json:"rebated_fee_currency"` +} + +// SpotOrdersDetail represents list of orders for specific currency pair +type SpotOrdersDetail struct { + CurrencyPair string `json:"currency_pair"` + Total float64 `json:"total"` + Orders []SpotOrder `json:"orders"` +} + +// ClosePositionRequestParam represents close position when cross currency is disable. +type ClosePositionRequestParam struct { + Text string `json:"text"` + CurrencyPair currency.Pair `json:"currency_pair"` + Amount float64 `json:"amount,string"` + Price float64 `json:"price,string"` +} + +// CancelOrderByIDParam represents cancel order by id request param. +type CancelOrderByIDParam struct { + CurrencyPair currency.Pair `json:"currency_pair"` + ID string `json:"id"` +} + +// CancelOrderByIDResponse represents calcel order response when deleted by id. +type CancelOrderByIDResponse struct { + CurrencyPair string `json:"currency_pair"` + OrderID string `json:"id"` + Succeeded bool `json:"succeeded"` + Label string `json:"label"` + Message string `json:"message"` + Account string `json:"account"` +} + +// SpotPersonalTradeHistory represents personal trading history. +type SpotPersonalTradeHistory struct { + TradeID string `json:"id"` + CreateTime gateioTime `json:"create_time"` + CreateTimeMs gateioTime `json:"create_time_ms"` + CurrencyPair string `json:"currency_pair"` + OrderID string `json:"order_id"` + Side string `json:"side"` + Role string `json:"role"` + Amount float64 `json:"amount,string"` + Price float64 `json:"price,string"` + Fee float64 `json:"fee,string"` + FeeCurrency string `json:"fee_currency"` + PointFee string `json:"point_fee"` + GtFee string `json:"gt_fee"` +} + +// CountdownCancelOrderParam represents countdown cancel order params +type CountdownCancelOrderParam struct { + CurrencyPair currency.Pair `json:"currency_pair"` + Timeout int64 `json:"timeout"` // timeout: Countdown time, in seconds At least 5 seconds, 0 means cancel the countdown +} + +// TriggerTimeResponse represents trigger time as a response for countdown candle order response +type TriggerTimeResponse struct { + TriggerTime gateioTime `json:"trigger_time"` +} + +// PriceTriggeredOrderParam represents price triggered order request. +type PriceTriggeredOrderParam struct { + Trigger TriggerPriceInfo `json:"trigger"` + Put PutOrderData `json:"put"` + Market currency.Pair `json:"market"` +} + +// TriggerPriceInfo represents a trigger price and related information for Price triggered order +type TriggerPriceInfo struct { + Price float64 `json:"price,string"` + Rule string `json:"rule"` + Expiration int64 `json:"expiration"` +} + +// PutOrderData represents order detail for price triggered order request +type PutOrderData struct { + Type string `json:"type"` + Side string `json:"side"` + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + Account string `json:"account"` + TimeInForce string `json:"time_in_force,omitempty"` +} + +// OrderID represents order creation ID response. +type OrderID struct { ID int64 `json:"id"` } -// wsGetBalanceRequest -type wsGetBalanceRequest struct { - ID int64 `json:"id"` - Method string `json:"method"` - Params []string `json:"params"` +// SpotPriceTriggeredOrder represents spot price triggered order response data. +type SpotPriceTriggeredOrder struct { + Trigger TriggerPriceInfo `json:"trigger"` + Put PutOrderData `json:"put"` + AutoOrderID int64 `json:"id"` + UserID int64 `json:"user"` + CreationTime gateioTime `json:"ctime"` + FireTime gateioTime `json:"ftime"` + FiredOrderID int64 `json:"fired_order_id"` + Status string `json:"status"` + Reason string `json:"reason"` + Market string `json:"market"` } -// WsGetBalanceResponse stores WS GetBalance response -type WsGetBalanceResponse struct { - Error WebsocketError `json:"error"` - Result map[string]WsGetBalanceResponseData `json:"result"` - ID int64 `json:"id"` +// ModifyLoanRequestParam represents request parameters for modify loan request +type ModifyLoanRequestParam struct { + Currency currency.Code `json:"currency"` + Side string `json:"side"` + CurrencyPair currency.Pair `json:"currency_pair"` + AutoRenew bool `json:"auto_renew"` + LoanID string `json:"loan_id"` } -// WsGetBalanceResponseData contains currency data -type WsGetBalanceResponseData struct { +// RepayLoanRequestParam represents loan repay request parameters +type RepayLoanRequestParam struct { + CurrencyPair currency.Pair `json:"currency_pair"` + Currency currency.Code `json:"currency"` + Mode string `json:"mode"` + Amount float64 `json:"amount,string"` +} + +// LoanRepaymentRecord represents loan repayment history record item. +type LoanRepaymentRecord struct { + ID string `json:"id"` + CreateTime gateioTime `json:"create_time"` + Principal string `json:"principal"` + Interest string `json:"interest"` +} + +// LoanRecord represents loan repayment specific record +type LoanRecord struct { + ID string `json:"id"` + LoanID string `json:"loan_id"` + CreateTime gateioTime `json:"create_time"` + ExpireTime gateioTime `json:"expire_time"` + Status string `json:"status"` + BorrowUserID string `json:"borrow_user_id"` + Currency string `json:"currency"` + Rate float64 `json:"rate,string"` + Amount float64 `json:"amount,string"` + Days int64 `json:"days"` + AutoRenew bool `json:"auto_renew"` + Repaid float64 `json:"repaid,string"` + PaidInterest float64 `json:"paid_interest,string"` + UnpaidInterest float64 `json:"unpaid_interest,string"` +} + +// OnOffStatus represents on or off status response status +type OnOffStatus struct { + Status string `json:"status"` +} + +// MaxTransferAndLoanAmount represents the maximum amount to transfer, borrow, or lend for specific currency and currency pair +type MaxTransferAndLoanAmount struct { + Currency string `json:"currency"` + CurrencyPair string `json:"currency_pair"` + Amount float64 `json:"amount,string"` +} + +// CrossMarginCurrencies represents a currency supported by cross margin +type CrossMarginCurrencies struct { + Name string `json:"name"` + Rate float64 `json:"rate,string"` + CurrencyPrecision float64 `json:"prec,string"` + Discount string `json:"discount"` + MinBorrowAmount float64 `json:"min_borrow_amount,string"` + UserMaxBorrowAmount float64 `json:"user_max_borrow_amount,string"` + TotalMaxBorrowAmount float64 `json:"total_max_borrow_amount,string"` + Price float64 `json:"price,string"` // Price change between this currency and USDT + Status int64 `json:"status"` +} + +// CrossMarginCurrencyBalance represents the currency detailed balance information for cross margin +type CrossMarginCurrencyBalance struct { Available float64 `json:"available,string"` Freeze float64 `json:"freeze,string"` + Borrowed float64 `json:"borrowed,string"` + Interest float64 `json:"interest,string"` } -type wsBalanceSubscription struct { - Method string `json:"method"` - Parameters []map[string]WsGetBalanceResponseData `json:"params"` - ID int64 `json:"id"` +// CrossMarginAccount represents the account detail for cross margin account balance +type CrossMarginAccount struct { + UserID int64 `json:"user_id"` + Locked bool `json:"locked"` + Balances map[string]CrossMarginCurrencyBalance `json:"balances"` + Total float64 `json:"total,string"` + Borrowed float64 `json:"borrowed,string"` + Interest float64 `json:"interest,string"` + Risk float64 `json:"risk,string"` + TotalInitialMargin string `json:"total_initial_margin"` + TotalMarginBalance float64 `json:"total_margin_balance,string"` + TotalMaintenanceMargin float64 `json:"total_maintenance_margin,string"` + TotalInitialMarginRate float64 `json:"total_initial_margin_rate,string"` + TotalMaintenanceMarginRate float64 `json:"total_maintenance_margin_rate,string"` + TotalAvailableMargin float64 `json:"total_available_margin,string"` + TotalPortfolioMarginAccount float64 `json:"portfolio_margin_total,string"` } -type wsOrderUpdate struct { - ID int64 `json:"id"` - Method string `json:"method"` - Params []interface{} `json:"params"` +// CrossMarginAccountHistoryItem represents a cross margin account change history item +type CrossMarginAccountHistoryItem struct { + ID string `json:"id"` + Time gateioTime `json:"time"` + Currency string `json:"currency"` // Currency changed + Change string `json:"change"` + Balance float64 `json:"balance,string"` + Type string `json:"type"` } -// TradeHistory contains trade history data -type TradeHistory struct { - Elapsed string `json:"elapsed"` - Result bool `json:"result,string"` - Data []TradeHistoryEntry `json:"data"` +// CrossMarginBorrowLoanParams represents a cross margin borrow loan parameters +type CrossMarginBorrowLoanParams struct { + Currency currency.Code `json:"currency"` + Amount float64 `json:"amount,string"` + Text string `json:"text"` } -// TradeHistoryEntry contains an individual trade -type TradeHistoryEntry struct { - Amount float64 `json:"amount,string"` - Date string `json:"date"` - Rate float64 `json:"rate,string"` - Timestamp int64 `json:"timestamp,string"` - Total float64 `json:"total,string"` - TradeID string `json:"tradeID"` - Type string `json:"type"` +// CrossMarginLoanResponse represents a cross margin borrow loan response +type CrossMarginLoanResponse struct { + ID string `json:"id"` + CreateTime gateioTime `json:"create_time"` + UpdateTime gateioTime `json:"update_time"` + Currency string `json:"currency"` + Amount float64 `json:"amount,string"` + Text string `json:"text"` + Status int64 `json:"status"` + Repaid string `json:"repaid"` + RepaidInterest float64 `json:"repaid_interest,string"` + UnpaidInterest float64 `json:"unpaid_interest,string"` } -// wsOrderbook defines a websocket orderbook -type wsOrderbook struct { - Asks [][]string `json:"asks"` - Bids [][]string `json:"bids"` - ID int64 `json:"id"` +// CurrencyAndAmount represents request parameters for repayment +type CurrencyAndAmount struct { + Currency currency.Code `json:"currency"` + Amount float64 `json:"amount,string"` } -// DepositAddr stores the deposit address info -type DepositAddr struct { - Result bool `json:"result,string"` - Code int `json:"code"` - Message string `json:"message"` - Address string `json:"addr"` - Tag string - MultichainAddresses []struct { - Chain string `json:"chain"` - Address string `json:"address"` - PaymentID string `json:"payment_id"` - PaymentName string `json:"payment_name"` - ObtainFailed uint8 `json:"obtain_failed"` - } `json:"multichain_addresses"` +// RepaymentHistoryItem represents an item in a repayment history. +type RepaymentHistoryItem struct { + ID string `json:"id"` + CreateTime gateioTime `json:"create_time"` + LoanID string `json:"loan_id"` + Currency string `json:"currency"` + Principal float32 `json:"principal,string"` + Interest float32 `json:"interest,string"` +} + +// FlashSwapOrderParams represents create flash swap order request parameters. +type FlashSwapOrderParams struct { + PreviewID string `json:"preview_id"` + SellCurrency currency.Code `json:"sell_currency"` + SellAmount float64 `json:"sell_amount,string,omitempty"` + BuyCurrency currency.Code `json:"buy_currency"` + BuyAmount float64 `json:"buy_amount,string,omitempty"` +} + +// FlashSwapOrderResponse represents create flash swap order response +type FlashSwapOrderResponse struct { + ID int64 `json:"id"` + CreateTime gateioTime `json:"create_time"` + UpdateTime gateioTime `json:"update_time"` + UserID int64 `json:"user_id"` + SellCurrency string `json:"sell_currency"` + SellAmount float64 `json:"sell_amount,string"` + BuyCurrency string `json:"buy_currency"` + BuyAmount float64 `json:"buy_amount,string"` + Price float64 `json:"price,string"` + Status int64 `json:"status"` +} + +// InitFlashSwapOrderPreviewResponse represents the order preview for flash order +type InitFlashSwapOrderPreviewResponse struct { + PreviewID string `json:"preview_id"` + SellCurrency string `json:"sell_currency"` + SellAmount float64 `json:"sell_amount,string"` + BuyCurrency string `json:"buy_currency"` + BuyAmount float64 `json:"buy_amount,string"` + Price float64 `json:"price,string"` +} + +// FuturesAccount represents futures account detail +type FuturesAccount struct { + User int64 `json:"user"` + Currency string `json:"currency"` + Total float64 `json:"total,string"` // total = position_margin + order_margin + available + UnrealisedPnl string `json:"unrealised_pnl"` + PositionMargin string `json:"position_margin"` + OrderMargin string `json:"order_margin"` // Order margin of unfinished orders + Available float64 `json:"available,string"` // The available balance for transferring or trading + Point string `json:"point"` + Bonus string `json:"bonus"` + InDualMode bool `json:"in_dual_mode"` // Whether dual mode is enabled + History struct { + DepositAndWithdrawal string `json:"dnw"` // total amount of deposit and withdraw + ProfitAndLoss float64 `json:"pnl,string"` // total amount of trading profit and loss + Fee string `json:"fee"` // total amount of fee + Refr string `json:"refr"` // total amount of referrer rebates + Fund string `json:"fund"` + PointDnw string `json:"point_dnw"` // total amount of point deposit and withdraw + PointFee string `json:"point_fee"` // total amount of point fee + PointRefr string `json:"point_refr"` + BonusDnw string `json:"bonus_dnw"` // total amount of perpetual contract bonus transfer + BonusOffset string `json:"bonus_offset"` // total amount of perpetual contract bonus deduction + } `json:"history"` +} + +// AccountBookItem represents account book item +type AccountBookItem struct { + Time gateioTime `json:"time"` + Change float64 `json:"change,string"` + Balance float64 `json:"balance,string"` + Text string `json:"text"` + Type string `json:"type"` +} + +// Position represents futures position +type Position struct { + User int64 `json:"user"` + Contract string `json:"contract"` + Size int64 `json:"size"` + Leverage float64 `json:"leverage,string"` + RiskLimit float64 `json:"risk_limit,string"` + LeverageMax string `json:"leverage_max"` + MaintenanceRate float64 `json:"maintenance_rate,string"` + Value float64 `json:"value,string"` + Margin float64 `json:"margin,string"` + EntryPrice float64 `json:"entry_price,string"` + LiqPrice float64 `json:"liq_price,string"` + MarkPrice float64 `json:"mark_price,string"` + UnrealisedPnl string `json:"unrealised_pnl"` + RealisedPnl string `json:"realised_pnl"` + HistoryPnl string `json:"history_pnl"` + LastClosePnl string `json:"last_close_pnl"` + RealisedPoint string `json:"realised_point"` + HistoryPoint string `json:"history_point"` + AdlRanking int64 `json:"adl_ranking"` + PendingOrders int64 `json:"pending_orders"` + CloseOrder struct { + ID int64 `json:"id"` + Price float64 `json:"price,string"` + IsLiq bool `json:"is_liq"` + } `json:"close_order"` + Mode string `json:"mode"` + CrossLeverageLimit string `json:"cross_leverage_limit"` +} + +// DualModeResponse represents dual mode enable or disable +type DualModeResponse struct { + User int64 `json:"user"` + Currency string `json:"currency"` + Total string `json:"total"` + UnrealisedPnl float64 `json:"unrealised_pnl,string"` + PositionMargin float64 `json:"position_margin,string"` + OrderMargin string `json:"order_margin"` + Available string `json:"available"` + Point string `json:"point"` + Bonus string `json:"bonus"` + InDualMode bool `json:"in_dual_mode"` + History struct { + DepositAndWithdrawal float64 `json:"dnw,string"` // total amount of deposit and withdraw + ProfitAndLoss float64 `json:"pnl,string"` // total amount of trading profit and loss + Fee float64 `json:"fee,string"` + Refr float64 `json:"refr,string"` + Fund float64 `json:"fund,string"` + PointDnw float64 `json:"point_dnw,string"` + PointFee float64 `json:"point_fee,string"` + PointRefr float64 `json:"point_refr,string"` + BonusDnw float64 `json:"bonus_dnw,string"` + BonusOffset float64 `json:"bonus_offset,string"` + } `json:"history"` +} + +// OrderCreateParams represents future order creation parameters +type OrderCreateParams struct { + Contract currency.Pair `json:"contract"` + Size float64 `json:"size"` + Iceberg int64 `json:"iceberg"` + Price float64 `json:"price,string"` + TimeInForce string `json:"tif"` + Text string `json:"text"` + + // Optional Parameters + ClosePosition bool `json:"close,omitempty"` + ReduceOnly bool `json:"reduce_only,omitempty"` + AutoSize string `json:"auto_size,omitempty"` + Settle string `json:"-"` +} + +// Order represents future order response +type Order struct { + ID int64 `json:"id"` + User int64 `json:"user"` + Contract string `json:"contract"` + CreateTime gateioTime `json:"create_time"` + Size float64 `json:"size"` + Iceberg int64 `json:"iceberg"` + RemainingAmount float64 `json:"left"` // Size left to be traded + OrderPrice float64 `json:"price,string"` + FillPrice float64 `json:"fill_price,string"` // Fill price of the order. total filled in quote currency. + MakerFee string `json:"mkfr"` + TakerFee string `json:"tkfr"` + TimeInForce string `json:"tif"` + ReferenceUserID int64 `json:"refu"` + IsReduceOnly bool `json:"is_reduce_only"` + IsClose bool `json:"is_close"` + IsOrderForLiquidation bool `json:"is_liq"` + Text string `json:"text"` + Status string `json:"status"` + FinishTime gateioTime `json:"finish_time"` + FinishAs string `json:"finish_as"` +} + +// AmendFuturesOrderParam represents amend futures order parameter +type AmendFuturesOrderParam struct { + Size float64 `json:"size,string"` + Price float64 `json:"price,string"` +} + +// PositionCloseHistoryResponse represents a close position history detail +type PositionCloseHistoryResponse struct { + Time gateioTime `json:"time"` + ProfitAndLoss float64 `json:"pnl,string"` + Side string `json:"side"` + Contract string `json:"contract"` + Text string `json:"text"` +} + +// LiquidationHistoryItem liquidation history item +type LiquidationHistoryItem struct { + Time gateioTime `json:"time"` + Contract string `json:"contract"` + Size int64 `json:"size"` + Leverage float64 `json:"leverage,string"` + Margin string `json:"margin"` + EntryPrice float64 `json:"entry_price,string"` + MarkPrice float64 `json:"mark_price,string"` + OrderPrice float64 `json:"order_price,string"` + FillPrice float64 `json:"fill_price,string"` + LiqPrice float64 `json:"liq_price,string"` + OrderID int64 `json:"order_id"` + Left int64 `json:"left"` +} + +// CountdownParams represents query parameters for countdown cancel order +type CountdownParams struct { + Timeout int64 `json:"timeout"` // In Seconds + Contract currency.Pair `json:"contract"` +} + +// FuturesPriceTriggeredOrderParam represents a creates a price triggered order +type FuturesPriceTriggeredOrderParam struct { + Initial FuturesInitial `json:"initial"` + Trigger FuturesTrigger `json:"trigger"` + OrderType string `json:"order_type,omitempty"` +} + +// FuturesInitial represents a price triggered order initial parameters +type FuturesInitial struct { + Contract currency.Pair `json:"contract"` + Size int64 `json:"size"` // Order size. Positive size means to buy, while negative one means to sell. Set to 0 to close the position + Price float64 `json:"price,string"` // Order price. Set to 0 to use market price + Close bool `json:"close,omitempty"` + TimeInForce string `json:"tif,omitempty"` + Text string `json:"text,omitempty"` + ReduceOnly bool `json:"reduce_only,omitempty"` + AutoSize string `json:"auto_size,omitempty"` +} + +// FuturesTrigger represents a price triggered order trigger parameter +type FuturesTrigger struct { + StrategyType int64 `json:"strategy_type,omitempty"` // How the order will be triggered 0: by price, which means the order will be triggered if price condition is satisfied 1: by price gap, which means the order will be triggered if gap of recent two prices of specified price_type are satisfied. Only 0 is supported currently + PriceType int64 `json:"price_type,omitempty"` + Price float64 `json:"price,omitempty,string"` + Rule int64 `json:"rule,omitempty"` + Expiration int64 `json:"expiration,omitempty"` // how long(in seconds) to wait for the condition to be triggered before cancelling the order + OrderType string `json:"order_type,omitempty"` +} + +// PriceTriggeredOrder represents a future triggered price order response +type PriceTriggeredOrder struct { + Initial struct { + Contract string `json:"contract"` + Size float64 `json:"size"` + Price float64 `json:"price,string"` + } `json:"initial"` + Trigger struct { + StrategyType int64 `json:"strategy_type"` + PriceType int64 `json:"price_type"` + Price float64 `json:"price,string"` + Rule int64 `json:"rule"` + Expiration int64 `json:"expiration"` + } `json:"trigger"` + ID int64 `json:"id"` + User int64 `json:"user"` + CreateTime gateioTime `json:"create_time"` + FinishTime gateioTime `json:"finish_time"` + TradeID int64 `json:"trade_id"` + Status string `json:"status"` + FinishAs string `json:"finish_as"` + Reason string `json:"reason"` + OrderType string `json:"order_type"` +} + +// SettlementHistoryItem represents a settlement history item +type SettlementHistoryItem struct { + Time gateioTime `json:"time"` + Contract string `json:"contract"` + Size int64 `json:"size"` + Leverage string `json:"leverage"` + Margin string `json:"margin"` + EntryPrice float64 `json:"entry_price,string"` + SettlePrice float64 `json:"settle_price,string"` + Profit float64 `json:"profit,string"` + Fee float64 `json:"fee,string"` +} + +// SubAccountParams represents subaccount creation parameters +type SubAccountParams struct { + LoginName string `json:"login_name,"` + Remark string `json:"remark,omitempty"` + Email string `json:"email,omitempty"` // The sub-account's password. + Password string `json:"password,omitempty"` // The sub-account's email address. +} + +// SubAccount represents a subaccount response +type SubAccount struct { + Remark string `json:"remark"` // custom text + LoginName string `json:"login_name"` // SubAccount login name + Password string `json:"password"` // The sub-account's password + SubAccountEmail string `json:"email"` // The sub-account's email + UserID int64 `json:"user_id"` + State int64 `json:"state"` + CreateTime gateioTime `json:"create_time"` +} + +// ************************************************************************************************** + +// WsInput represents general structure for websocket requests +type WsInput struct { + Time int64 `json:"time,omitempty"` + ID int64 `json:"id,omitempty"` + Channel string `json:"channel,omitempty"` + Event string `json:"event,omitempty"` + Payload []string `json:"payload,omitempty"` + Auth *WsAuthInput `json:"auth,omitempty"` +} + +// WsAuthInput represents the authentication information +type WsAuthInput struct { + Method string `json:"method,omitempty"` + Key string `json:"KEY,omitempty"` + Sign string `json:"SIGN,omitempty"` +} + +// WsEventResponse represents websocket incoming subscription, unsubscription, and update response +type WsEventResponse struct { + Time int64 `json:"time"` + ID int64 `json:"id"` + Channel string `json:"channel"` + Event string `json:"event"` + Result *struct { + Status string `json:"status"` + } `json:"result"` + Error *struct { + Code int64 `json:"code"` + Message string `json:"message"` + } +} + +// WsResponse represents generalized websocket push data from the server. +type WsResponse struct { + ID int64 `json:"id"` + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result interface{} `json:"result"` +} + +// WsTicker websocket ticker information. +type WsTicker struct { + CurrencyPair string `json:"currency_pair"` + Last float64 `json:"last,string"` + LowestAsk float64 `json:"lowest_ask,string"` + HighestBid float64 `json:"highest_bid,string"` + ChangePercentage float64 `json:"change_percentage,string"` + BaseVolume float64 `json:"base_volume,string"` + QuoteVolume float64 `json:"quote_volume,string"` + High24H float64 `json:"high_24h,string"` + Low24H float64 `json:"low_24h,string"` +} + +// WsTrade represents a websocket push data response for a trade +type WsTrade struct { + ID int64 `json:"id"` + CreateTime int64 `json:"create_time"` + CreateTimeMs float64 `json:"create_time_ms,string"` + Side string `json:"side"` + CurrencyPair string `json:"currency_pair"` + Amount float64 `json:"amount,string"` + Price float64 `json:"price,string"` +} + +// WsCandlesticks represents the candlestick data for spot, margin and cross margin trades pushed through the websocket channel. +type WsCandlesticks struct { + Timestamp int64 `json:"t,string"` + TotalVolume float64 `json:"v,string"` + ClosePrice float64 `json:"c,string"` + HighestPrice float64 `json:"h,string"` + LowestPrice float64 `json:"l,string"` + OpenPrice float64 `json:"o,string"` + NameOfSubscription string `json:"n"` +} + +// WsOrderbookTickerData represents the websocket orderbook best bid or best ask push data +type WsOrderbookTickerData struct { + UpdateTimeMS int64 `json:"t"` + UpdateOrderID int64 `json:"u"` + CurrencyPair string `json:"s"` + BestBidPrice float64 `json:"b,string"` + BestBidAmount float64 `json:"B,string"` + BestAskPrice float64 `json:"a,string"` + BestAskAmount float64 `json:"A,string"` +} + +// WsOrderbookUpdate represents websocket orderbook update push data +type WsOrderbookUpdate struct { + UpdateTimeMs gateioTime `json:"t"` + IgnoreField string `json:"e"` + UpdateTime gateioTime `json:"E"` + CurrencyPair string `json:"s"` + FirstOrderbookUpdatedID int64 `json:"U"` // First update order book id in this event since last update + LastOrderbookUpdatedID int64 `json:"u"` + Bids [][2]string `json:"b"` + Asks [][2]string `json:"a"` +} + +// WsOrderbookSnapshot represents a websocket orderbook snapshot push data +type WsOrderbookSnapshot struct { + UpdateTimeMs gateioTime `json:"t"` + LastUpdateID int64 `json:"lastUpdateId"` + CurrencyPair string `json:"s"` + Bids [][2]string `json:"bids"` + Asks [][2]string `json:"asks"` +} + +// WsSpotOrder represents an order push data through the websocket channel. +type WsSpotOrder struct { + ID string `json:"id,omitempty"` + User int64 `json:"user"` + Text string `json:"text,omitempty"` + Succeeded bool `json:"succeeded,omitempty"` + Label string `json:"label,omitempty"` + Message string `json:"message,omitempty"` + CurrencyPair string `json:"currency_pair,omitempty"` + Type string `json:"type,omitempty"` + Account string `json:"account,omitempty"` + Side string `json:"side,omitempty"` + Amount float64 `json:"amount,omitempty,string"` + Price float64 `json:"price,omitempty,string"` + TimeInForce string `json:"time_in_force,omitempty"` + Iceberg string `json:"iceberg,omitempty"` + Left gateioNumericalValue `json:"left,omitempty"` + FilledTotal float64 `json:"filled_total,omitempty,string"` + Fee float64 `json:"fee,omitempty,string"` + FeeCurrency string `json:"fee_currency,omitempty"` + PointFee string `json:"point_fee,omitempty"` + GtFee string `json:"gt_fee,omitempty"` + GtDiscount bool `json:"gt_discount,omitempty"` + RebatedFee string `json:"rebated_fee,omitempty"` + RebatedFeeCurrency string `json:"rebated_fee_currency,omitempty"` + Event string `json:"event"` + CreateTime gateioTime `json:"create_time,omitempty"` + CreateTimeMs gateioTime `json:"create_time_ms,omitempty"` + UpdateTime gateioTime `json:"update_time,omitempty"` + UpdateTimeMs gateioTime `json:"update_time_ms,omitempty"` +} + +// WsUserPersonalTrade represents a user's personal trade pushed through the websocket connection. +type WsUserPersonalTrade struct { + ID int64 `json:"id"` + UserID int64 `json:"user_id"` + OrderID string `json:"order_id"` + CurrencyPair string `json:"currency_pair"` + CreateTime int64 `json:"create_time"` + CreateTimeMicroS time.Time `json:"create_time_ms"` + Side string `json:"side"` + Amount float64 `json:"amount,string"` + Role string `json:"role"` + Price float64 `json:"price,string"` + Fee float64 `json:"fee,string"` + PointFee float64 `json:"point_fee,string"` + GtFee string `json:"gt_fee"` + Text string `json:"text"` +} + +// WsSpotBalance represents a spot balance. +type WsSpotBalance struct { + Timestamp float64 `json:"timestamp,string"` + TimestampMs float64 `json:"timestamp_ms,string"` + User string `json:"user"` + Currency string `json:"currency"` + Change float64 `json:"change,string"` + Total float64 `json:"total,string"` + Available float64 `json:"available,string"` +} + +// WsMarginBalance represents margin account balance push data +type WsMarginBalance struct { + Timestamp float64 `json:"timestamp,string"` + TimestampMs float64 `json:"timestamp_ms,string"` + User string `json:"user"` + CurrencyPair string `json:"currency_pair"` + Currency string `json:"currency"` + Change float64 `json:"change,string"` + Available float64 `json:"available,string"` + Freeze float64 `json:"freeze,string"` + Borrowed string `json:"borrowed"` + Interest string `json:"interest"` +} + +// WsFundingBalance represents funding balance push data. +type WsFundingBalance struct { + Timestamp int64 `json:"timestamp,string"` + TimestampMs float64 `json:"timestamp_ms,string"` + User string `json:"user"` + Currency string `json:"currency"` + Change string `json:"change"` + Freeze string `json:"freeze"` + Lent string `json:"lent"` +} + +// WsCrossMarginBalance represents a cross margin balance detail +type WsCrossMarginBalance struct { + Timestamp int64 `json:"timestamp,string"` + TimestampMs float64 `json:"timestamp_ms,string"` + User string `json:"user"` + Currency string `json:"currency"` + Change string `json:"change"` + Total float64 `json:"total,string"` + Available float64 `json:"available,string"` +} + +// WsCrossMarginLoan represents a cross margin loan push data +type WsCrossMarginLoan struct { + Timestamp gateioTime `json:"timestamp"` + User string `json:"user"` + Currency string `json:"currency"` + Change string `json:"change"` + Total float64 `json:"total,string"` + Available float64 `json:"available,string"` + Borrowed string `json:"borrowed"` + Interest string `json:"interest"` +} + +// WsFutureTicker represents a futures push data. +type WsFutureTicker struct { + Contract string `json:"contract"` + Last float64 `json:"last,string"` + ChangePercentage string `json:"change_percentage"` + FundingRate string `json:"funding_rate"` + FundingRateIndicative string `json:"funding_rate_indicative"` + MarkPrice float64 `json:"mark_price,string"` + IndexPrice float64 `json:"index_price,string"` + TotalSize float64 `json:"total_size,string"` + Volume24H float64 `json:"volume_24h,string"` + Volume24HBtc float64 `json:"volume_24h_btc,string"` + Volume24HUsd float64 `json:"volume_24h_usd,string"` + QuantoBaseRate string `json:"quanto_base_rate"` + Volume24HQuote float64 `json:"volume_24h_quote,string"` + Volume24HSettle string `json:"volume_24h_settle"` + Volume24HBase float64 `json:"volume_24h_base,string"` + Low24H float64 `json:"low_24h,string"` + High24H float64 `json:"high_24h,string"` +} + +// WsFuturesTrades represents a list of trades push data +type WsFuturesTrades struct { + Size float64 `json:"size"` + ID int64 `json:"id"` + CreateTime gateioTime `json:"create_time"` + CreateTimeMs gateioTime `json:"create_time_ms"` + Price float64 `json:"price,string"` + Contract string `json:"contract"` +} + +// WsFuturesOrderbookTicker represents the orderbook ticker push data +type WsFuturesOrderbookTicker struct { + TimestampMs gateioTime `json:"t"` + UpdateID int64 `json:"u"` + CurrencyPair string `json:"s"` + BestBidPrice float64 `json:"b,string"` + BestBidAmount float64 `json:"B"` + BestAskPrice float64 `json:"a,string"` + BestAskAmount float64 `json:"A"` +} + +// WsFuturesAndOptionsOrderbookUpdate represents futures and options account orderbook update push data +type WsFuturesAndOptionsOrderbookUpdate struct { + TimestampInMs int64 `json:"t"` + ContractName string `json:"s"` + FirstUpdatedID int64 `json:"U"` + LastUpdatedID int64 `json:"u"` + Bids []struct { + Price float64 `json:"p,string"` + Size float64 `json:"s"` + } `json:"b"` + Asks []struct { + Price float64 `json:"p,string"` + Size float64 `json:"s"` + } `json:"a"` +} + +// WsFuturesOrderbookSnapshot represents a futures orderbook snapshot push data +type WsFuturesOrderbookSnapshot struct { + TimestampInMs gateioTime `json:"t"` + Contract string `json:"contract"` + OrderbookID int64 `json:"id"` + Asks []struct { + Price float64 `json:"p,string"` + Size float64 `json:"s"` + } `json:"asks"` + Bids []struct { + Price float64 `json:"p,string"` + Size float64 `json:"s"` + } `json:"bids"` +} + +// WsFuturesOrderbookUpdateEvent represents futures orderbook push data with the event 'update' +type WsFuturesOrderbookUpdateEvent struct { + Price float64 `json:"p,string"` + Amount float64 `json:"s"` + CurrencyPair string `json:"c"` + ID int64 `json:"id"` +} + +// WsFuturesOrder represents futures order +type WsFuturesOrder struct { + Contract string `json:"contract"` + CreateTime gateioTime `json:"create_time"` + CreateTimeMs gateioTime `json:"create_time_ms"` + FillPrice float64 `json:"fill_price"` + FinishAs string `json:"finish_as"` + FinishTime int64 `json:"finish_time"` + FinishTimeMs gateioTime `json:"finish_time_ms"` + Iceberg int64 `json:"iceberg"` + ID int64 `json:"id"` + IsClose bool `json:"is_close"` + IsLiq bool `json:"is_liq"` + IsReduceOnly bool `json:"is_reduce_only"` + Left float64 `json:"left"` + Mkfr float64 `json:"mkfr"` + Price float64 `json:"price"` + Refr int64 `json:"refr"` + Refu int64 `json:"refu"` + Size float64 `json:"size"` + Status string `json:"status"` + Text string `json:"text"` + TimeInForce string `json:"tif"` + Tkfr float64 `json:"tkfr"` + User string `json:"user"` +} + +// WsFuturesUserTrade represents a futures account user trade push data +type WsFuturesUserTrade struct { + ID string `json:"id"` + CreateTime gateioTime `json:"create_time"` + CreateTimeMs gateioTime `json:"create_time_ms"` + Contract string `json:"contract"` + OrderID string `json:"order_id"` + Size float64 `json:"size"` + Price float64 `json:"price,string"` + Role string `json:"role"` + Text string `json:"text"` + Fee float64 `json:"fee"` + PointFee int64 `json:"point_fee"` +} + +// WsFuturesLiquidationNotification represents a liquidation notification push data +type WsFuturesLiquidationNotification struct { + EntryPrice int64 `json:"entry_price"` + FillPrice float64 `json:"fill_price"` + Left float64 `json:"left"` + Leverage float64 `json:"leverage"` + LiqPrice int64 `json:"liq_price"` + Margin float64 `json:"margin"` + MarkPrice int64 `json:"mark_price"` + OrderID int64 `json:"order_id"` + OrderPrice float64 `json:"order_price"` + Size float64 `json:"size"` + Time int64 `json:"time"` + TimeMs gateioTime `json:"time_ms"` + Contract string `json:"contract"` + User string `json:"user"` +} + +// WsFuturesAutoDeleveragesNotification represents futures auto deleverages push data +type WsFuturesAutoDeleveragesNotification struct { + EntryPrice float64 `json:"entry_price"` + FillPrice float64 `json:"fill_price"` + PositionSize int64 `json:"position_size"` + TradeSize int64 `json:"trade_size"` + Time gateioTime `json:"time"` + TimeMs gateioTime `json:"time_ms"` + Contract string `json:"contract"` + User string `json:"user"` +} + +// WsPositionClose represents a close position futures push data +type WsPositionClose struct { + Contract string `json:"contract"` + ProfitAndLoss float64 `json:"pnl,omitempty"` + Side string `json:"side"` + Text string `json:"text"` + Time gateioTime `json:"time"` + TimeMs gateioTime `json:"time_ms"` + User string `json:"user"` + + // Added in options close position push datas + SettleSize float64 `json:"settle_size,omitempty"` + Underlying string `json:"underlying,omitempty"` +} + +// WsBalance represents a options and futures balance push data +type WsBalance struct { + Balance float64 `json:"balance"` + Change float64 `json:"change"` + Text string `json:"text"` + Time gateioTime `json:"time"` + TimeMs gateioTime `json:"time_ms"` + Type string `json:"type"` + User string `json:"user"` +} + +// WsFuturesReduceRiskLimitNotification represents a futures reduced risk limit push data +type WsFuturesReduceRiskLimitNotification struct { + CancelOrders int64 `json:"cancel_orders"` + Contract string `json:"contract"` + LeverageMax int64 `json:"leverage_max"` + LiqPrice float64 `json:"liq_price"` + MaintenanceRate float64 `json:"maintenance_rate"` + RiskLimit int64 `json:"risk_limit"` + Time gateioTime `json:"time"` + TimeMs gateioTime `json:"time_ms"` + User string `json:"user"` +} + +// WsFuturesPosition represents futures notify positions update. +type WsFuturesPosition struct { + Contract string `json:"contract"` + CrossLeverageLimit float64 `json:"cross_leverage_limit"` + EntryPrice float64 `json:"entry_price"` + HistoryPnl float64 `json:"history_pnl"` + HistoryPoint int64 `json:"history_point"` + LastClosePnl float64 `json:"last_close_pnl"` + Leverage float64 `json:"leverage"` + LeverageMax float64 `json:"leverage_max"` + LiqPrice float64 `json:"liq_price"` + MaintenanceRate float64 `json:"maintenance_rate"` + Margin float64 `json:"margin"` + Mode string `json:"mode"` + RealisedPnl float64 `json:"realised_pnl"` + RealisedPoint float64 `json:"realised_point"` + RiskLimit float64 `json:"risk_limit"` + Size float64 `json:"size"` + Time gateioTime `json:"time"` + TimeMs gateioTime `json:"time_ms"` + User string `json:"user"` +} + +// WsFuturesAutoOrder represents an auto order push data. +type WsFuturesAutoOrder struct { + User int64 `json:"user"` + Trigger struct { + StrategyType int64 `json:"strategy_type"` + PriceType int64 `json:"price_type"` + Price string `json:"price"` + Rule int64 `json:"rule"` + Expiration int64 `json:"expiration"` + } `json:"trigger"` + Initial struct { + Contract string `json:"contract"` + Size int64 `json:"size"` + Price float64 `json:"price,string"` + TimeInForce string `json:"tif"` + Text string `json:"text"` + Iceberg int64 `json:"iceberg"` + IsClose bool `json:"is_close"` + IsReduceOnly bool `json:"is_reduce_only"` + } `json:"initial"` + ID int64 `json:"id"` + TradeID int64 `json:"trade_id"` + Status string `json:"status"` + Reason string `json:"reason"` + CreateTime gateioTime `json:"create_time"` + Name string `json:"name"` + IsStopOrder bool `json:"is_stop_order"` + StopTrigger struct { + Rule int64 `json:"rule"` + TriggerPrice string `json:"trigger_price"` + OrderPrice string `json:"order_price"` + } `json:"stop_trigger"` +} + +// WsOptionUnderlyingTicker represents options underlying ticker push data +type WsOptionUnderlyingTicker struct { + TradePut int64 `json:"trade_put"` + TradeCall int64 `json:"trade_call"` + IndexPrice string `json:"index_price"` + Name string `json:"name"` +} + +// WsOptionsTrades represents options trades for websocket push data. +type WsOptionsTrades struct { + ID int64 `json:"id"` + CreateTime gateioTime `json:"create_time"` + Contract string `json:"contract"` + Size float64 `json:"size"` + Price float64 `json:"price"` + + // Added in options websocket push data + CreateTimeMs gateioTime `json:"create_time_ms"` + Underlying string `json:"underlying"` + IsCall bool `json:"is_call"` // added in underlying trades +} + +// WsOptionsUnderlyingPrice represents the underlying price. +type WsOptionsUnderlyingPrice struct { + Underlying string `json:"underlying"` + Price float64 `json:"price"` + UpdateTime gateioTime `json:"time"` + UpdateTimeMs gateioTime `json:"time_ms"` +} + +// WsOptionsMarkPrice represents options mark price push data. +type WsOptionsMarkPrice struct { + Contract string `json:"contract"` + Price float64 `json:"price"` + UpdateTimeMs gateioTime `json:"time_ms"` + UpdateTime gateioTime `json:"time"` +} + +// WsOptionsSettlement represents a options settlement push data. +type WsOptionsSettlement struct { + Contract string `json:"contract"` + OrderbookID int64 `json:"orderbook_id"` + PositionSize float64 `json:"position_size"` + Profit float64 `json:"profit"` + SettlePrice float64 `json:"settle_price"` + StrikePrice float64 `json:"strike_price"` + Tag string `json:"tag"` + TradeID int64 `json:"trade_id"` + TradeSize int64 `json:"trade_size"` + Underlying string `json:"underlying"` + UpdateTime gateioTime `json:"time"` + UpdateTimeMs gateioTime `json:"time_ms"` +} + +// WsOptionsContract represents an option contract push data. +type WsOptionsContract struct { + Contract string `json:"contract"` + CreateTime gateioTime `json:"create_time"` + ExpirationTime int64 `json:"expiration_time"` + InitMarginHigh float64 `json:"init_margin_high"` + InitMarginLow float64 `json:"init_margin_low"` + IsCall bool `json:"is_call"` + MaintMarginBase float64 `json:"maint_margin_base"` + MakerFeeRate float64 `json:"maker_fee_rate"` + MarkPriceRound float64 `json:"mark_price_round"` + MinBalanceShort float64 `json:"min_balance_short"` + MinOrderMargin float64 `json:"min_order_margin"` + Multiplier float64 `json:"multiplier"` + OrderPriceDeviate float64 `json:"order_price_deviate"` + OrderPriceRound float64 `json:"order_price_round"` + OrderSizeMax float64 `json:"order_size_max"` + OrderSizeMin float64 `json:"order_size_min"` + OrdersLimit float64 `json:"orders_limit"` + RefDiscountRate float64 `json:"ref_discount_rate"` + RefRebateRate float64 `json:"ref_rebate_rate"` + StrikePrice float64 `json:"strike_price"` + Tag string `json:"tag"` + TakerFeeRate float64 `json:"taker_fee_rate"` + Underlying string `json:"underlying"` + Time gateioTime `json:"time"` + TimeMs gateioTime `json:"time_ms"` +} + +// WsOptionsContractCandlestick represents an options contract candlestick push data. +type WsOptionsContractCandlestick struct { + Timestamp int64 `json:"t"` + TotalVolume float64 `json:"v"` + ClosePrice float64 `json:"c,string"` + HighestPrice float64 `json:"h,string"` + LowestPrice float64 `json:"l,string"` + OpenPrice float64 `json:"o,string"` + Amount float64 `json:"a,string"` + NameOfSubscription string `json:"n"` // the format of _ +} + +// WsOptionsOrderbookTicker represents options orderbook ticker push data. +type WsOptionsOrderbookTicker struct { + UpdateTimestamp gateioTime `json:"t"` + UpdateID int64 `json:"u"` + ContractName string `json:"s"` + BidPrice float64 `json:"b,string"` + BidSize float64 `json:"B"` + AskPrice float64 `json:"a,string"` + AskSize float64 `json:"A"` +} + +// WsOptionsOrderbookSnapshot represents the options orderbook snapshot push data. +type WsOptionsOrderbookSnapshot struct { + Timestamp gateioTime `json:"t"` + Contract string `json:"contract"` + ID int64 `json:"id"` + Asks []struct { + Price float64 `json:"p,string"` + Size float64 `json:"s"` + } `json:"asks"` + Bids []struct { + Price float64 `json:"p,string"` + Size float64 `json:"s"` + } `json:"bids"` +} + +// WsOptionsOrder represents options order push data. +type WsOptionsOrder struct { + ID int64 `json:"id"` + Contract string `json:"contract"` + CreateTime int64 `json:"create_time"` + FillPrice float64 `json:"fill_price"` + FinishAs string `json:"finish_as"` + Iceberg float64 `json:"iceberg"` + IsClose bool `json:"is_close"` + IsLiq bool `json:"is_liq"` + IsReduceOnly bool `json:"is_reduce_only"` + Left float64 `json:"left"` + Mkfr float64 `json:"mkfr"` + Price float64 `json:"price"` + Refr float64 `json:"refr"` + Refu float64 `json:"refu"` + Size float64 `json:"size"` + Status string `json:"status"` + Text string `json:"text"` + Tif string `json:"tif"` + Tkfr float64 `json:"tkfr"` + Underlying string `json:"underlying"` + User string `json:"user"` + CreationTime gateioTime `json:"time"` + CreationTimeMs gateioTime `json:"time_ms"` +} + +// WsOptionsUserTrade represents user's personal trades of option account. +type WsOptionsUserTrade struct { + ID string `json:"id"` + Underlying string `json:"underlying"` + OrderID string `json:"order"` + Contract string `json:"contract"` + CreateTime gateioTime `json:"create_time"` + CreateTimeMs gateioTime `json:"create_time_ms"` + Price float64 `json:"price,string"` + Role string `json:"role"` + Size float64 `json:"size"` +} + +// WsOptionsLiquidates represents the liquidates push data of option account. +type WsOptionsLiquidates struct { + User string `json:"user"` + InitMargin float64 `json:"init_margin"` + MaintMargin float64 `json:"maint_margin"` + OrderMargin float64 `json:"order_margin"` + Time gateioTime `json:"time"` + TimeMs gateioTime `json:"time_ms"` +} + +// WsOptionsUserSettlement represents user's personal settlements push data of options account. +type WsOptionsUserSettlement struct { + User string `json:"user"` + Contract string `json:"contract"` + RealisedPnl float64 `json:"realised_pnl"` + SettlePrice float64 `json:"settle_price"` + SettleProfit float64 `json:"settle_profit"` + Size float64 `json:"size"` + StrikePrice float64 `json:"strike_price"` + Underlying string `json:"underlying"` + SettleTime gateioTime `json:"time"` + SettleTimeMs gateioTime `json:"time_ms"` +} + +// WsOptionsPosition represents positions push data for options account. +type WsOptionsPosition struct { + EntryPrice float64 `json:"entry_price"` + RealisedPnl float64 `json:"realised_pnl"` + Size float64 `json:"size"` + Contract string `json:"contract"` + User string `json:"user"` + UpdateTime gateioTime `json:"time"` + UpdateTimeMs gateioTime `json:"time_ms"` +} + +// InterSubAccountTransferParams represents parameters to transfer funds between sub-accounts. +type InterSubAccountTransferParams struct { + Currency currency.Code `json:"currency"` // Required + SubAccountType string `json:"sub_account_type"` + SubAccountFromUserID string `json:"sub_account_from"` // Required + SubAccountFromAssetType asset.Item `json:"sub_account_from_type"` // Required + SubAccountToUserID string `json:"sub_account_to"` // Required + SubAccountToAssetType asset.Item `json:"sub_account_to_type"` // Required + Amount float64 `json:"amount,string"` // Required +} + +// CreateAPIKeySubAccountParams represents subaccount new API key creation parameters. +type CreateAPIKeySubAccountParams struct { + SubAccountUserID int64 `json:"user_id"` + Body *SubAccountKey `json:"body"` +} + +// SubAccountKey represents sub-account key detail information +// this is a struct to be used for outbound requests. +type SubAccountKey struct { + APIKeyName string `json:"name,omitempty"` + Permissions []APIV4KeyPerm `json:"perms,omitempty"` +} + +// APIV4KeyPerm represents an API Version 4 Key permission information +type APIV4KeyPerm struct { + PermissionName string `json:"name,omitempty"` + ReadOnly bool `json:"read_only,omitempty"` + IPWhitelist []string `json:"ip_whitelist,omitempty"` +} + +// CreateAPIKeyResponse represents an API key response object +type CreateAPIKeyResponse struct { + UserID string `json:"user_id"` + APIKeyName string `json:"name"` // API key name + Permissions []APIV4KeyPerm `json:"perms"` + IPWhitelist []string `json:"ip_whitelist,omitempty"` + APIKey string `json:"key"` + Secret string `json:"secret"` + State int64 `json:"state"` // State 1 - normal 2 - locked 3 - frozen + CreatedAt gateioTime `json:"created_at"` + UpdatedAt gateioTime `json:"updated_at"` +} + +// PriceAndAmount used in updating an order +type PriceAndAmount struct { + Amount float64 `json:"amount,string,omitempty"` + Price float64 `json:"price,string,omitempty"` } diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index 9b69a9da..7831d7a4 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -2,6 +2,9 @@ package gateio import ( "context" + "crypto/hmac" + "crypto/sha512" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -12,10 +15,11 @@ import ( "github.com/gorilla/websocket" "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" + "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/fill" + "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/stream" @@ -24,90 +28,78 @@ import ( ) const ( - gateioWebsocketEndpoint = "wss://ws.gateio.ws/v3/" + gateioWebsocketEndpoint = "wss://api.gateio.ws/ws/v4/" gateioWebsocketRateLimit = 120 + + spotPingChannel = "spot.ping" + spotPongChannel = "spot.pong" + spotTickerChannel = "spot.tickers" + spotTradesChannel = "spot.trades" + spotCandlesticksChannel = "spot.candlesticks" + spotOrderbookTickerChannel = "spot.book_ticker" // Best bid or ask price + spotOrderbookUpdateChannel = "spot.order_book_update" // Changed order book levels + spotOrderbookChannel = "spot.order_book" // Limited-Level Full Order Book Snapshot + spotOrdersChannel = "spot.orders" + spotUserTradesChannel = "spot.usertrades" + spotBalancesChannel = "spot.balances" + marginBalancesChannel = "spot.margin_balances" + spotFundingBalanceChannel = "spot.funding_balances" + crossMarginBalanceChannel = "spot.cross_balances" + crossMarginLoanChannel = "spot.cross_loan" ) +var defaultSubscriptions = []string{ + spotTickerChannel, + spotCandlesticksChannel, + spotTradesChannel, + spotOrderbookChannel, +} + +var fetchedCurrencyPairSnapshotOrderbook = make(map[string]bool) + // WsConnect initiates a websocket connection func (g *Gateio) WsConnect() error { if !g.Websocket.IsEnabled() || !g.IsEnabled() { return errors.New(stream.WebsocketNotEnabled) } - var dialer websocket.Dialer - err := g.Websocket.Conn.Dial(&dialer, http.Header{}) + err := g.CurrencyPairs.IsAssetEnabled(asset.Spot) if err != nil { return err } - - g.Websocket.Wg.Add(1) - go g.wsReadData() - - if g.IsWebsocketAuthenticationSupported() { - err = g.wsServerSignIn(context.TODO()) - if err != nil { - g.Websocket.DataHandler <- err - g.Websocket.SetCanUseAuthenticatedEndpoints(false) - } else { - var authsubs []stream.ChannelSubscription - authsubs, err = g.GenerateAuthenticatedSubscriptions() - if err != nil { - g.Websocket.DataHandler <- err - g.Websocket.SetCanUseAuthenticatedEndpoints(false) - } else { - err = g.Websocket.SubscribeToChannels(authsubs) - if err != nil { - g.Websocket.DataHandler <- err - g.Websocket.SetCanUseAuthenticatedEndpoints(false) - } - } - } + var dialer websocket.Dialer + err = g.Websocket.Conn.Dial(&dialer, http.Header{}) + if err != nil { + return err } - + pingMessage, err := json.Marshal(WsInput{ + Channel: spotPingChannel, + }) + if err != nil { + return err + } + g.Websocket.Conn.SetupPingHandler(stream.PingHandler{ + Websocket: true, + Delay: time.Second * 15, + Message: pingMessage, + MessageType: websocket.TextMessage, + }) + g.Websocket.Wg.Add(1) + go g.wsReadConnData() return nil } -func (g *Gateio) wsServerSignIn(ctx context.Context) error { - creds, err := g.GetCredentials(ctx) - if err != nil { - return err +func (g *Gateio) generateWsSignature(secret, event, channel string, dtime time.Time) (string, error) { + msg := "channel=" + channel + "&event=" + event + "&time=" + strconv.FormatInt(dtime.Unix(), 10) + mac := hmac.New(sha512.New, []byte(secret)) + if _, err := mac.Write([]byte(msg)); err != nil { + return "", err } - nonce := int(time.Now().Unix() * 1000) - sigTemp, err := g.GenerateSignature(creds.Secret, strconv.Itoa(nonce)) - if err != nil { - return err - } - signature := crypto.Base64Encode(sigTemp) - signinWsRequest := WebsocketRequest{ - ID: g.Websocket.Conn.GenerateMessageID(false), - Method: "server.sign", - Params: []interface{}{creds.Key, signature, nonce}, - } - resp, err := g.Websocket.Conn.SendMessageReturnResponse(signinWsRequest.ID, - signinWsRequest) - if err != nil { - g.Websocket.SetCanUseAuthenticatedEndpoints(false) - return err - } - var response WebsocketAuthenticationResponse - err = json.Unmarshal(resp, &response) - if err != nil { - g.Websocket.SetCanUseAuthenticatedEndpoints(false) - return err - } - if response.Result.Status == "success" { - g.Websocket.SetCanUseAuthenticatedEndpoints(true) - return nil - } - - return fmt.Errorf("%s cannot authenticate websocket connection: %s", - g.Name, - response.Result.Status) + return hex.EncodeToString(mac.Sum(nil)), nil } -// wsReadData receives and passes on websocket messages for processing -func (g *Gateio) wsReadData() { +// wsReadConnData receives and passes on websocket messages for processing +func (g *Gateio) wsReadConnData() { defer g.Websocket.Wg.Done() - for { resp := g.Websocket.Conn.ReadMessage() if resp.Raw == nil { @@ -121,611 +113,788 @@ func (g *Gateio) wsReadData() { } func (g *Gateio) wsHandleData(respRaw []byte) error { - var result WebsocketResponse - err := json.Unmarshal(respRaw, &result) + var result WsResponse + var eventResponse WsEventResponse + err := json.Unmarshal(respRaw, &eventResponse) + if err == nil && + (eventResponse.Result != nil || eventResponse.Error != nil) && + (eventResponse.Event == "subscribe" || eventResponse.Event == "unsubscribe") { + if !g.Websocket.Match.IncomingWithData(eventResponse.ID, respRaw) { + return fmt.Errorf("couldn't match subscription message with ID: %d", eventResponse.ID) + } + return nil + } + err = json.Unmarshal(respRaw, &result) if err != nil { return err } - - if result.ID > 0 { - if g.Websocket.Match.IncomingWithData(result.ID, respRaw) { - return nil - } - } - - if result.Error.Code != 0 { - if strings.Contains(result.Error.Message, "authentication") { - g.Websocket.SetCanUseAuthenticatedEndpoints(false) - return fmt.Errorf("%v - authentication failed: %v", g.Name, err) - } - return fmt.Errorf("%v error %s", g.Name, result.Error.Message) - } - - switch { - case strings.Contains(result.Method, "ticker"): - var wsTicker WebsocketTicker - var c string - err = json.Unmarshal(result.Params[1], &wsTicker) - if err != nil { - return err - } - err = json.Unmarshal(result.Params[0], &c) - if err != nil { - return err - } - - var p currency.Pair - p, err = currency.NewPairFromString(c) - if err != nil { - return err - } - - g.Websocket.DataHandler <- &ticker.Price{ - ExchangeName: g.Name, - Open: wsTicker.Open, - Close: wsTicker.Close, - Volume: wsTicker.BaseVolume, - QuoteVolume: wsTicker.QuoteVolume, - High: wsTicker.High, - Low: wsTicker.Low, - Last: wsTicker.Last, - AssetType: asset.Spot, - Pair: p, - } - - case strings.Contains(result.Method, "trades"): - if !g.IsSaveTradeDataEnabled() { - return nil - } - var tradeData []WebsocketTrade - var c string - err = json.Unmarshal(result.Params[1], &tradeData) - if err != nil { - return err - } - err = json.Unmarshal(result.Params[0], &c) - if err != nil { - return err - } - - var p currency.Pair - p, err = currency.NewPairFromString(c) - if err != nil { - return err - } - var trades []trade.Data - for i := range tradeData { - var tSide order.Side - tSide, err = order.StringToOrderSide(tradeData[i].Type) - if err != nil { - g.Websocket.DataHandler <- order.ClassificationError{ - Exchange: g.Name, - Err: err, - } - } - trades = append(trades, trade.Data{ - Timestamp: convert.TimeFromUnixTimestampDecimal(tradeData[i].Time), - CurrencyPair: p, - AssetType: asset.Spot, - Exchange: g.Name, - Price: tradeData[i].Price, - Amount: tradeData[i].Amount, - Side: tSide, - TID: strconv.FormatInt(tradeData[i].ID, 10), - }) - } - return trade.AddTradesToBuffer(g.Name, trades...) - case strings.Contains(result.Method, "balance.update"): - var balance wsBalanceSubscription - err = json.Unmarshal(respRaw, &balance) - if err != nil { - return err - } - g.Websocket.DataHandler <- balance - case strings.Contains(result.Method, "order.update"): - var orderUpdate wsOrderUpdate - err = json.Unmarshal(respRaw, &orderUpdate) - if err != nil { - return err - } - if len(orderUpdate.Params) < 2 { - return errors.New("unexpected orderUpdate.Params data length") - } - invalidJSON, ok := orderUpdate.Params[1].(map[string]interface{}) - if !ok { - return errors.New("unable to type assert invalidJSON") - } - oStatus := order.UnknownStatus - oType := order.UnknownType - oSide := order.UnknownSide - - orderStatus, ok := orderUpdate.Params[0].(float64) - if !ok { - return errors.New("unable to type assert orderStatus") - } - switch orderStatus { - case 1: - oStatus = order.New - case 2: - oStatus = order.PartiallyFilled - case 3: - oStatus = order.Filled - } - - orderType, ok := invalidJSON["orderType"].(float64) - if !ok { - return errors.New("unable to type assert orderType") - } - switch orderType { - case 1: - oType = order.Limit - case 2: - oType = order.Market - } - - orderSide, ok := invalidJSON["type"].(float64) - if !ok { - return errors.New("unable to type assert orderSide") - } - switch orderSide { - case 1: - oSide = order.Sell - case 2: - oSide = order.Buy - } - - var price, amount, filledTotal, left, fee float64 - price, err = convert.FloatFromString(invalidJSON["price"]) - if err != nil { - return err - } - amount, err = convert.FloatFromString(invalidJSON["amount"]) - if err != nil { - return err - } - filledTotal, err = convert.FloatFromString(invalidJSON["filledTotal"]) - if err != nil { - return err - } - left, err = convert.FloatFromString(invalidJSON["left"]) - if err != nil { - return err - } - fee, err = convert.FloatFromString(invalidJSON["dealFee"]) - if err != nil { - return err - } - - var p currency.Pair - pairStr, ok := invalidJSON["market"].(string) - if !ok { - return errors.New("unable to type assert market") - } - p, err = currency.NewPairFromString(pairStr) - if err != nil { - return err - } - - var a asset.Item - a, err = g.GetPairAssetType(p) - if err != nil { - return err - } - - orderID, ok := invalidJSON["id"].(float64) - if !ok { - return errors.New("unable to type assert order id") - } - - ctime, ok := invalidJSON["ctime"].(float64) - if !ok { - return errors.New("unable to type assert ctime") - } - - mtime, ok := invalidJSON["mtime"].(float64) - if !ok { - return errors.New("unable to type assert mtime") - } - - g.Websocket.DataHandler <- &order.Detail{ - Price: price, - Amount: amount, - ExecutedAmount: filledTotal, - RemainingAmount: left, - Fee: fee, - Exchange: g.Name, - OrderID: strconv.FormatFloat(orderID, 'f', -1, 64), - Type: oType, - Side: oSide, - Status: oStatus, - AssetType: a, - Date: convert.TimeFromUnixTimestampDecimal(ctime), - LastUpdated: convert.TimeFromUnixTimestampDecimal(mtime), - Pair: p, - } - case strings.Contains(result.Method, "depth"): - var IsSnapshot bool - var c string - var data wsOrderbook - - err = json.Unmarshal(result.Params[0], &IsSnapshot) - if err != nil { - return err - } - - err = json.Unmarshal(result.Params[2], &c) - if err != nil { - return err - } - - err = json.Unmarshal(result.Params[1], &data) - if err != nil { - return err - } - - asks := make([]orderbook.Item, len(data.Asks)) - var amount, price float64 - for i := range data.Asks { - amount, err = strconv.ParseFloat(data.Asks[i][1], 64) - if err != nil { - return err - } - price, err = strconv.ParseFloat(data.Asks[i][0], 64) - if err != nil { - return err - } - asks[i] = orderbook.Item{Amount: amount, Price: price} - } - - bids := make([]orderbook.Item, len(data.Bids)) - for i := range data.Bids { - amount, err = strconv.ParseFloat(data.Bids[i][1], 64) - if err != nil { - return err - } - price, err = strconv.ParseFloat(data.Bids[i][0], 64) - if err != nil { - return err - } - bids[i] = orderbook.Item{Amount: amount, Price: price} - } - - var p currency.Pair - p, err = currency.NewPairFromString(c) - if err != nil { - return err - } - - if IsSnapshot { - var newOrderBook orderbook.Base - newOrderBook.Asks = asks - newOrderBook.Bids = bids - newOrderBook.Asset = asset.Spot - newOrderBook.Pair = p - newOrderBook.Exchange = g.Name - newOrderBook.VerifyOrderbook = g.CanVerifyOrderbook - - err = g.Websocket.Orderbook.LoadSnapshot(&newOrderBook) - if err != nil { - return err - } - } else { - err = g.Websocket.Orderbook.Update(&orderbook.Update{ - Asks: asks, - Bids: bids, - Pair: p, - UpdateTime: time.Now(), - Asset: asset.Spot, - }) - if err != nil { - return err - } - } - case strings.Contains(result.Method, "kline"): - var data []interface{} - err = json.Unmarshal(result.Params[0], &data) - if err != nil { - return err - } - open, err := strconv.ParseFloat(data[1].(string), 64) - if err != nil { - return err - } - closePrice, err := strconv.ParseFloat(data[2].(string), 64) - if err != nil { - return err - } - high, err := strconv.ParseFloat(data[3].(string), 64) - if err != nil { - return err - } - low, err := strconv.ParseFloat(data[4].(string), 64) - if err != nil { - return err - } - volume, err := strconv.ParseFloat(data[5].(string), 64) - if err != nil { - return err - } - - p, err := currency.NewPairFromString(data[7].(string)) - if err != nil { - return err - } - - g.Websocket.DataHandler <- stream.KlineData{ - Timestamp: time.Now(), - Pair: p, - AssetType: asset.Spot, - Exchange: g.Name, - OpenPrice: open, - ClosePrice: closePrice, - HighPrice: high, - LowPrice: low, - Volume: volume, - } + switch result.Channel { + case spotTickerChannel: + return g.processTicker(respRaw) + case spotTradesChannel: + return g.processTrades(respRaw) + case spotCandlesticksChannel: + return g.processCandlestick(respRaw) + case spotOrderbookTickerChannel: + return g.processOrderbookTicker(respRaw) + case spotOrderbookUpdateChannel: + return g.processOrderbookUpdate(respRaw) + case spotOrderbookChannel: + return g.processOrderbookSnapshot(respRaw) + case spotOrdersChannel: + return g.processSpotOrders(respRaw) + case spotUserTradesChannel: + return g.processUserPersonalTrades(respRaw) + case spotBalancesChannel: + return g.processSpotBalances(respRaw) + case marginBalancesChannel: + return g.processMarginBalances(respRaw) + case spotFundingBalanceChannel: + return g.processFundingBalances(respRaw) + case crossMarginBalanceChannel: + return g.processCrossMarginBalance(respRaw) + case crossMarginLoanChannel: + return g.processCrossMarginLoans(respRaw) + case spotPongChannel: default: g.Websocket.DataHandler <- stream.UnhandledMessageWarning{ Message: g.Name + stream.UnhandledMessage + string(respRaw), } - return nil + return errors.New(stream.UnhandledMessage) } return nil } -// GenerateAuthenticatedSubscriptions returns authenticated subscriptions -func (g *Gateio) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscription, error) { - if !g.Websocket.CanUseAuthenticatedEndpoints() { - return nil, nil - } - var channels = []string{"balance.subscribe", "order.subscribe"} - var subscriptions []stream.ChannelSubscription - enabledCurrencies, err := g.GetEnabledPairs(asset.Spot) +func (g *Gateio) processTicker(data []byte) error { + var response WsResponse + tickerData := &WsTicker{} + response.Result = tickerData + err := json.Unmarshal(data, &response) if err != nil { - return nil, err + return err } - for i := range channels { - for j := range enabledCurrencies { - subscriptions = append(subscriptions, stream.ChannelSubscription{ - Channel: channels[i], - Currency: enabledCurrencies[j], - Asset: asset.Spot, - }) + currencyPair, err := currency.NewPairFromString(tickerData.CurrencyPair) + if err != nil { + return err + } + tickerPrice := ticker.Price{ + ExchangeName: g.Name, + Volume: tickerData.BaseVolume, + QuoteVolume: tickerData.QuoteVolume, + High: tickerData.High24H, + Low: tickerData.Low24H, + Last: tickerData.Last, + Bid: tickerData.HighestBid, + Ask: tickerData.LowestAsk, + AssetType: asset.Spot, + Pair: currencyPair, + LastUpdated: time.Unix(response.Time, 0), + } + assetPairEnabled := g.listOfAssetsCurrencyPairEnabledFor(currencyPair) + if assetPairEnabled[asset.Spot] { + g.Websocket.DataHandler <- &tickerPrice + } + if assetPairEnabled[asset.Margin] { + marginTicker := tickerPrice + marginTicker.AssetType = asset.Margin + g.Websocket.DataHandler <- &marginTicker + } + if assetPairEnabled[asset.CrossMargin] { + crossMarginTicker := tickerPrice + crossMarginTicker.AssetType = asset.CrossMargin + g.Websocket.DataHandler <- &crossMarginTicker + } + return nil +} + +func (g *Gateio) processTrades(data []byte) error { + var response WsResponse + tradeData := &WsTrade{} + response.Result = tradeData + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + currencyPair, err := currency.NewPairFromString(tradeData.CurrencyPair) + if err != nil { + return err + } + side, err := order.StringToOrderSide(tradeData.Side) + if err != nil { + return err + } + spotTradeData := trade.Data{ + Timestamp: time.UnixMicro(int64(tradeData.CreateTimeMs * 1e3)), // the timestamp data is coming as a floating number. + CurrencyPair: currencyPair, + AssetType: asset.Spot, + Exchange: g.Name, + Price: tradeData.Price, + Amount: tradeData.Amount, + Side: side, + TID: strconv.FormatInt(tradeData.ID, 10), + } + assetPairEnabled := g.listOfAssetsCurrencyPairEnabledFor(currencyPair) + if assetPairEnabled[asset.Spot] { + err = trade.AddTradesToBuffer(g.Name, spotTradeData) + if err != nil { + return err } } - return subscriptions, nil + if assetPairEnabled[asset.Margin] { + marginTradeData := spotTradeData + marginTradeData.AssetType = asset.Margin + err = trade.AddTradesToBuffer(g.Name, marginTradeData) + if err != nil { + return err + } + } + if assetPairEnabled[asset.CrossMargin] { + crossMarginTradeData := spotTradeData + crossMarginTradeData.AssetType = asset.CrossMargin + err = trade.AddTradesToBuffer(g.Name, crossMarginTradeData) + if err != nil { + return err + } + } + return nil +} + +func (g *Gateio) processCandlestick(data []byte) error { + var response WsResponse + candleData := &WsCandlesticks{} + response.Result = candleData + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + icp := strings.Split(candleData.NameOfSubscription, currency.UnderscoreDelimiter) + if len(icp) < 3 { + return errors.New("malformed candlestick websocket push data") + } + currencyPair, err := currency.NewPairFromString(strings.Join(icp[1:], currency.UnderscoreDelimiter)) + if err != nil { + return err + } + spotCandlestick := stream.KlineData{ + Pair: currencyPair, + AssetType: asset.Spot, + Exchange: g.Name, + StartTime: time.Unix(candleData.Timestamp, 0), + Interval: icp[0], + OpenPrice: candleData.OpenPrice, + ClosePrice: candleData.ClosePrice, + HighPrice: candleData.HighestPrice, + LowPrice: candleData.LowestPrice, + Volume: candleData.TotalVolume, + } + assetPairEnabled := g.listOfAssetsCurrencyPairEnabledFor(currencyPair) + if assetPairEnabled[asset.Spot] { + g.Websocket.DataHandler <- spotCandlestick + } + if assetPairEnabled[asset.Margin] { + marginCandlestick := spotCandlestick + marginCandlestick.AssetType = asset.Margin + g.Websocket.DataHandler <- marginCandlestick + } + if assetPairEnabled[asset.CrossMargin] { + crossMarginCandlestick := spotCandlestick + crossMarginCandlestick.AssetType = asset.CrossMargin + g.Websocket.DataHandler <- crossMarginCandlestick + } + return nil +} + +func (g *Gateio) processOrderbookTicker(data []byte) error { + var response WsResponse + tickerData := &WsOrderbookTickerData{} + response.Result = tickerData + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + g.Websocket.DataHandler <- tickerData + return nil +} + +func (g *Gateio) processOrderbookUpdate(data []byte) error { + var response WsResponse + update := new(WsOrderbookUpdate) + response.Result = update + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + pair, err := currency.NewPairFromString(update.CurrencyPair) + if err != nil { + return err + } + assetPairEnabled := g.listOfAssetsCurrencyPairEnabledFor(pair) + if !fetchedCurrencyPairSnapshotOrderbook[update.CurrencyPair] { + var orderbooks *orderbook.Base + orderbooks, err = g.FetchOrderbook(context.Background(), pair, asset.Spot) // currency pair orderbook data for Spot, Margin, and Cross Margin is same + if err != nil { + return err + } + // TODO: handle orderbook update synchronisation + for _, assetType := range []asset.Item{asset.Spot, asset.Margin, asset.CrossMargin} { + if !assetPairEnabled[assetType] { + continue + } + assetOrderbook := *orderbooks + assetOrderbook.Asset = assetType + err = g.Websocket.Orderbook.LoadSnapshot(&assetOrderbook) + if err != nil { + return err + } + } + fetchedCurrencyPairSnapshotOrderbook[update.CurrencyPair] = true + } + updates := orderbook.Update{ + UpdateTime: update.UpdateTimeMs.Time(), + Pair: pair, + } + updates.Bids = make([]orderbook.Item, len(update.Bids)) + updates.Asks = make([]orderbook.Item, len(update.Asks)) + var price float64 + var amount float64 + for x := range updates.Asks { + price, err = strconv.ParseFloat(update.Asks[x][0], 64) + if err != nil { + return err + } + amount, err = strconv.ParseFloat(update.Asks[x][1], 64) + if err != nil { + return err + } + updates.Asks[x] = orderbook.Item{ + Amount: amount, + Price: price, + } + } + for x := range updates.Bids { + price, err = strconv.ParseFloat(update.Bids[x][0], 64) + if err != nil { + return err + } + amount, err = strconv.ParseFloat(update.Bids[x][1], 64) + if err != nil { + return err + } + updates.Bids[x] = orderbook.Item{ + Amount: amount, + Price: price, + } + } + if len(updates.Asks) == 0 && len(updates.Bids) == 0 { + return nil + } + if assetPairEnabled[asset.Spot] { + updates.Asset = asset.Spot + err = g.Websocket.Orderbook.Update(&updates) + if err != nil { + return err + } + } + if assetPairEnabled[asset.Margin] { + marginUpdates := updates + marginUpdates.Asset = asset.Margin + err = g.Websocket.Orderbook.Update(&marginUpdates) + if err != nil { + return err + } + } + if assetPairEnabled[asset.CrossMargin] { + crossMarginUpdate := updates + crossMarginUpdate.Asset = asset.CrossMargin + err = g.Websocket.Orderbook.Update(&crossMarginUpdate) + if err != nil { + return err + } + } + return nil +} + +func (g *Gateio) processOrderbookSnapshot(data []byte) error { + var response WsResponse + snapshot := &WsOrderbookSnapshot{} + response.Result = snapshot + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + pair, err := currency.NewPairFromString(snapshot.CurrencyPair) + if err != nil { + return err + } + assetPairEnabled := g.listOfAssetsCurrencyPairEnabledFor(pair) + bases := orderbook.Base{ + Exchange: g.Name, + Pair: pair, + Asset: asset.Spot, + LastUpdated: snapshot.UpdateTimeMs.Time(), + LastUpdateID: snapshot.LastUpdateID, + VerifyOrderbook: g.CanVerifyOrderbook, + } + bases.Bids = make([]orderbook.Item, len(snapshot.Bids)) + bases.Asks = make([]orderbook.Item, len(snapshot.Asks)) + var price float64 + var amount float64 + for x := range bases.Asks { + price, err = strconv.ParseFloat(snapshot.Asks[x][0], 64) + if err != nil { + return err + } + amount, err = strconv.ParseFloat(snapshot.Asks[x][1], 64) + if err != nil { + return err + } + bases.Asks[x] = orderbook.Item{ + Amount: amount, + Price: price, + } + } + for x := range bases.Bids { + price, err = strconv.ParseFloat(snapshot.Bids[x][0], 64) + if err != nil { + return err + } + amount, err = strconv.ParseFloat(snapshot.Bids[x][1], 64) + if err != nil { + return err + } + bases.Bids[x] = orderbook.Item{ + Amount: amount, + Price: price, + } + } + if assetPairEnabled[asset.Spot] { + err = g.Websocket.Orderbook.LoadSnapshot(&bases) + if err != nil { + return err + } + } + if assetPairEnabled[asset.Margin] { + marginBases := bases + marginBases.Asset = asset.Margin + err = g.Websocket.Orderbook.LoadSnapshot(&marginBases) + if err != nil { + return err + } + } + if assetPairEnabled[asset.CrossMargin] { + crossMarginBases := bases + crossMarginBases.Asset = asset.CrossMargin + err = g.Websocket.Orderbook.LoadSnapshot(&crossMarginBases) + if err != nil { + return err + } + } + return nil +} + +func (g *Gateio) processSpotOrders(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsSpotOrder `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + details := make([]order.Detail, len(resp.Result)) + for x := range resp.Result { + pair, err := currency.NewPairFromString(resp.Result[x].CurrencyPair) + if err != nil { + return err + } + side, err := order.StringToOrderSide(resp.Result[x].Side) + if err != nil { + return err + } + orderType, err := order.StringToOrderType(resp.Result[x].Type) + if err != nil { + return err + } + a, err := asset.New(resp.Result[x].Account) + if err != nil { + return err + } + details[x] = order.Detail{ + Amount: resp.Result[x].Amount, + Exchange: g.Name, + OrderID: resp.Result[x].ID, + Side: side, + Type: orderType, + Pair: pair, + Cost: resp.Result[x].Fee, + AssetType: a, + Price: resp.Result[x].Price, + ExecutedAmount: resp.Result[x].Amount - resp.Result[x].Left.Float64(), + Date: resp.Result[x].CreateTimeMs.Time(), + LastUpdated: resp.Result[x].UpdateTimeMs.Time(), + } + } + g.Websocket.DataHandler <- details + return nil +} + +func (g *Gateio) processUserPersonalTrades(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsUserPersonalTrade `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + fills := make([]fill.Data, len(resp.Result)) + for x := range fills { + currencyPair, err := currency.NewPairFromString(resp.Result[x].CurrencyPair) + if err != nil { + return err + } + side, err := order.StringToOrderSide(resp.Result[x].Side) + if err != nil { + return err + } + fills[x] = fill.Data{ + Timestamp: resp.Result[x].CreateTimeMicroS, + Exchange: g.Name, + CurrencyPair: currencyPair, + Side: side, + OrderID: resp.Result[x].OrderID, + TradeID: strconv.FormatInt(resp.Result[x].ID, 10), + Price: resp.Result[x].Price, + Amount: resp.Result[x].Amount, + } + } + return g.Websocket.Fills.Update(fills...) +} + +func (g *Gateio) processSpotBalances(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsSpotBalance `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + accountChanges := make([]account.Change, len(resp.Result)) + for x := range resp.Result { + code := currency.NewCode(resp.Result[x].Currency) + accountChanges[x] = account.Change{ + Exchange: g.Name, + Currency: code, + Asset: asset.Spot, + Amount: resp.Result[x].Available, + } + } + g.Websocket.DataHandler <- accountChanges + return nil +} + +func (g *Gateio) processMarginBalances(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsMarginBalance `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + accountChange := make([]account.Change, len(resp.Result)) + for x := range resp.Result { + code := currency.NewCode(resp.Result[x].Currency) + accountChange[x] = account.Change{ + Exchange: g.Name, + Currency: code, + Asset: asset.Margin, + Amount: resp.Result[x].Available, + } + } + g.Websocket.DataHandler <- accountChange + return nil +} + +func (g *Gateio) processFundingBalances(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFundingBalance `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + g.Websocket.DataHandler <- resp + return nil +} + +func (g *Gateio) processCrossMarginBalance(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsCrossMarginBalance `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + accountChanges := make([]account.Change, len(resp.Result)) + for x := range resp.Result { + code := currency.NewCode(resp.Result[x].Currency) + accountChanges[x] = account.Change{ + Exchange: g.Name, + Currency: code, + Asset: asset.Margin, + Amount: resp.Result[x].Available, + Account: resp.Result[x].User, + } + } + g.Websocket.DataHandler <- accountChanges + return nil +} + +func (g *Gateio) processCrossMarginLoans(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result WsCrossMarginLoan `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + g.Websocket.DataHandler <- resp + return nil } // GenerateDefaultSubscriptions returns default subscriptions func (g *Gateio) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) { - var channels = []string{"ticker.subscribe", - "trades.subscribe", - "depth.subscribe", - "kline.subscribe"} + channelsToSubscribe := defaultSubscriptions + if g.Websocket.CanUseAuthenticatedEndpoints() { + channelsToSubscribe = append(channelsToSubscribe, []string{ + crossMarginBalanceChannel, + marginBalancesChannel, + spotBalancesChannel}...) + } var subscriptions []stream.ChannelSubscription - enabledCurrencies, err := g.GetEnabledPairs(asset.Spot) + var pairs []currency.Pair + var crossMarginPairs, spotPairs currency.Pairs + marginPairs, err := g.GetEnabledPairs(asset.Margin) if err != nil { return nil, err } - for i := range channels { - for j := range enabledCurrencies { + crossMarginPairs, err = g.GetEnabledPairs(asset.CrossMargin) + if err != nil { + return nil, err + } + spotPairs, err = g.GetEnabledPairs(asset.Spot) + if err != nil { + return nil, err + } + for i := range channelsToSubscribe { + switch channelsToSubscribe[i] { + case marginBalancesChannel: + pairs = marginPairs + case crossMarginBalanceChannel: + pairs = crossMarginPairs + default: + pairs = spotPairs + } + for j := range pairs { params := make(map[string]interface{}) - if strings.EqualFold(channels[i], "depth.subscribe") { - params["limit"] = 30 - params["interval"] = "0.1" - } else if strings.EqualFold(channels[i], "kline.subscribe") { - params["interval"] = 1800 + switch channelsToSubscribe[i] { + case spotOrderbookChannel: + params["level"] = 100 + params["interval"] = kline.HundredMilliseconds + case spotCandlesticksChannel: + params["interval"] = kline.FiveMin + case spotOrderbookUpdateChannel: + params["interval"] = kline.ThousandMilliseconds } - - fpair, err := g.FormatExchangeCurrency(enabledCurrencies[j], - asset.Spot) + if spotTradesChannel == channelsToSubscribe[i] { + if !g.IsSaveTradeDataEnabled() { + continue + } + } + fpair, err := g.FormatExchangeCurrency(pairs[j], asset.Spot) if err != nil { return nil, err } subscriptions = append(subscriptions, stream.ChannelSubscription{ - Channel: channels[i], + Channel: channelsToSubscribe[i], Currency: fpair.Upper(), Params: params, - Asset: asset.Spot, }) } } return subscriptions, nil } -// Subscribe sends a websocket message to receive data from the channel -func (g *Gateio) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error { - payloads, err := g.generatePayload(channelsToSubscribe) +// handleSubscription sends a websocket message to receive data from the channel +func (g *Gateio) handleSubscription(event string, channelsToSubscribe []stream.ChannelSubscription) error { + payloads, err := g.generatePayload(event, channelsToSubscribe) if err != nil { return err } - var errs error for k := range payloads { - resp, err := g.Websocket.Conn.SendMessageReturnResponse(payloads[k].ID, payloads[k]) + result, err := g.Websocket.Conn.SendMessageReturnResponse(payloads[k].ID, payloads[k]) if err != nil { errs = common.AppendError(errs, err) continue } - var response WebsocketAuthenticationResponse - err = json.Unmarshal(resp, &response) - if err != nil { + var resp WsEventResponse + if err = json.Unmarshal(result, &resp); err != nil { errs = common.AppendError(errs, err) - continue + } else { + if resp.Error != nil && resp.Error.Code != 0 { + errs = common.AppendError(errs, fmt.Errorf("error while %s to channel %s error code: %d message: %s", payloads[k].Event, payloads[k].Channel, resp.Error.Code, resp.Error.Message)) + continue + } + if payloads[k].Event == "subscribe" { + g.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[k]) + } else { + g.Websocket.RemoveSuccessfulUnsubscriptions(channelsToSubscribe[k]) + } } - if response.Result.Status != "success" { - errs = common.AppendError(errs, fmt.Errorf("%v could not subscribe to %v", - g.Name, - payloads[k].Method)) - continue - } - g.Websocket.AddSuccessfulSubscriptions(payloads[k].Channels...) } return errs } -func (g *Gateio) generatePayload(channelsToSubscribe []stream.ChannelSubscription) ([]WebsocketRequest, error) { +func (g *Gateio) generatePayload(event string, channelsToSubscribe []stream.ChannelSubscription) ([]WsInput, error) { if len(channelsToSubscribe) == 0 { return nil, errors.New("cannot generate payload, no channels supplied") } - - var payloads []WebsocketRequest -channels: + var creds *account.Credentials + var err error + if g.Websocket.CanUseAuthenticatedEndpoints() { + creds, err = g.GetCredentials(context.TODO()) + if err != nil { + return nil, err + } + } + var intervalString string + payloads := make([]WsInput, len(channelsToSubscribe)) for i := range channelsToSubscribe { - // Ensures params are in order - params := []interface{}{channelsToSubscribe[i].Currency} - if strings.EqualFold(channelsToSubscribe[i].Channel, "depth.subscribe") { - params = append(params, - channelsToSubscribe[i].Params["limit"], - channelsToSubscribe[i].Params["interval"]) - } else if strings.EqualFold(channelsToSubscribe[i].Channel, "kline.subscribe") { - params = append(params, channelsToSubscribe[i].Params["interval"]) - } - - for j := range payloads { - if payloads[j].Method == channelsToSubscribe[i].Channel { - switch { - case strings.EqualFold(channelsToSubscribe[i].Channel, "depth.subscribe"): - if len(payloads[j].Params) == 3 { - // If more than one currency pair we need to send as - // matrix - _, ok := payloads[j].Params[0].(currency.Pair) - if ok { - var bucket = payloads[j].Params - payloads[j].Params = nil - payloads[j].Params = append(payloads[j].Params, bucket) - } - } - - payloads[j].Params = append(payloads[j].Params, params) - case strings.EqualFold(channelsToSubscribe[i].Channel, "kline.subscribe"): - // Can only subscribe one market at the same time, market - // list is not supported currently. For multiple - // subscriptions, only the last one takes effect. - default: - payloads[j].Params = append(payloads[j].Params, params...) - } - payloads[j].Channels = append(payloads[j].Channels, channelsToSubscribe[i]) - continue channels + var auth *WsAuthInput + timestamp := time.Now() + channelsToSubscribe[i].Currency.Delimiter = currency.UnderscoreDelimiter + params := []string{channelsToSubscribe[i].Currency.String()} + switch channelsToSubscribe[i].Channel { + case spotOrderbookChannel: + interval, okay := channelsToSubscribe[i].Params["interval"].(kline.Interval) + if !okay { + return nil, errors.New("invalid interval parameter") } + level, okay := channelsToSubscribe[i].Params["level"].(int) + if !okay { + return nil, errors.New("invalid spot order level") + } + intervalString, err = g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params = append(params, + strconv.Itoa(level), + intervalString, + ) + case spotCandlesticksChannel: + interval, ok := channelsToSubscribe[i].Params["interval"].(kline.Interval) + if !ok { + return nil, errors.New("missing spot candlesticks interval") + } + intervalString, err = g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params = append( + []string{intervalString}, + params...) + } + switch channelsToSubscribe[i].Channel { + case spotUserTradesChannel, + spotBalancesChannel, + marginBalancesChannel, + spotFundingBalanceChannel, + crossMarginBalanceChannel, + crossMarginLoanChannel: + if !g.Websocket.CanUseAuthenticatedEndpoints() { + continue + } + value, ok := channelsToSubscribe[i].Params["user"].(string) + if ok { + params = append( + []string{value}, + params...) + } + var sigTemp string + sigTemp, err = g.generateWsSignature(creds.Secret, event, channelsToSubscribe[i].Channel, timestamp) + if err != nil { + return nil, err + } + auth = &WsAuthInput{ + Method: "api_key", + Key: creds.Key, + Sign: sigTemp, + } + case spotOrderbookUpdateChannel: + interval, ok := channelsToSubscribe[i].Params["interval"].(kline.Interval) + if !ok { + return nil, errors.New("missing spot orderbook interval") + } + intervalString, err = g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params = append(params, intervalString) + } + payloads[i] = WsInput{ + ID: g.Websocket.Conn.GenerateMessageID(false), + Event: event, + Channel: channelsToSubscribe[i].Channel, + Payload: params, + Auth: auth, + Time: timestamp.Unix(), } - - payloads = append(payloads, WebsocketRequest{ - ID: g.Websocket.Conn.GenerateMessageID(false), - Method: channelsToSubscribe[i].Channel, - Params: params, - Channels: []stream.ChannelSubscription{channelsToSubscribe[i]}, - }) } return payloads, nil } +// Subscribe sends a websocket message to stop receiving data from the channel +func (g *Gateio) Subscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { + return g.handleSubscription("subscribe", channelsToUnsubscribe) +} + // Unsubscribe sends a websocket message to stop receiving data from the channel func (g *Gateio) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { - // NOTE: This function does not take in parameters, it cannot unsubscribe a - // single item but a full channel. i.e. if you subscribe to ticker BTC_USDT - // & LTC_USDT this function will unsubscribe both. This function will be - // kept unlinked to the websocket subsystem and a full connection flush will - // occur when currency items are disabled. - channelsThusFar := make([]string, 0, len(channelsToUnsubscribe)) - for i := range channelsToUnsubscribe { - if common.StringDataCompare(channelsThusFar, - channelsToUnsubscribe[i].Channel) { + return g.handleSubscription("unsubscribe", channelsToUnsubscribe) +} + +func (g *Gateio) listOfAssetsCurrencyPairEnabledFor(cp currency.Pair) map[asset.Item]bool { + assetTypes := g.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.Options: false, asset.Futures: false, asset.CrossMargin: false, asset.Margin: false, asset.DeliveryFutures: false} + for i := range assetTypes { + pairs, err := g.GetEnabledPairs(assetTypes[i]) + if err != nil { continue } - - channelsThusFar = append(channelsThusFar, - channelsToUnsubscribe[i].Channel) - - unsubscribeText := strings.Replace(channelsToUnsubscribe[i].Channel, - "subscribe", - "unsubscribe", - 1) - - unsubscribe := WebsocketRequest{ - ID: g.Websocket.Conn.GenerateMessageID(false), - Method: unsubscribeText, - Params: []interface{}{channelsToUnsubscribe[i].Currency.String()}, - } - - resp, err := g.Websocket.Conn.SendMessageReturnResponse(unsubscribe.ID, - unsubscribe) - if err != nil { - return err - } - var response WebsocketAuthenticationResponse - err = json.Unmarshal(resp, &response) - if err != nil { - return err - } - if response.Result.Status != "success" { - return fmt.Errorf("%v could not subscribe to %v", - g.Name, - channelsToUnsubscribe[i].Channel) - } + assetPairEnabled[assetTypes[i]] = pairs.Contains(cp, true) } - return nil -} - -func (g *Gateio) wsGetBalance(currencies []string) (*WsGetBalanceResponse, error) { - if !g.Websocket.CanUseAuthenticatedEndpoints() { - return nil, fmt.Errorf("%v not authorised to get balance", g.Name) - } - balanceWsRequest := wsGetBalanceRequest{ - ID: g.Websocket.Conn.GenerateMessageID(false), - Method: "balance.query", - Params: currencies, - } - resp, err := g.Websocket.Conn.SendMessageReturnResponse(balanceWsRequest.ID, balanceWsRequest) - if err != nil { - return nil, err - } - var balance WsGetBalanceResponse - err = json.Unmarshal(resp, &balance) - if err != nil { - return &balance, err - } - - if balance.Error.Message != "" { - return nil, fmt.Errorf("%s websocket error: %s", - g.Name, - balance.Error.Message) - } - - return &balance, nil -} - -func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) (*WebSocketOrderQueryResult, error) { - if !g.Websocket.CanUseAuthenticatedEndpoints() { - return nil, fmt.Errorf("%v not authorised to get order info", g.Name) - } - ord := WebsocketRequest{ - ID: g.Websocket.Conn.GenerateMessageID(false), - Method: "order.query", - Params: []interface{}{ - market, - offset, - limit, - }, - } - - resp, err := g.Websocket.Conn.SendMessageReturnResponse(ord.ID, ord) - if err != nil { - return nil, err - } - - var orderQuery WebSocketOrderQueryResult - err = json.Unmarshal(resp, &orderQuery) - if err != nil { - return &orderQuery, err - } - - if orderQuery.Error.Message != "" { - return nil, fmt.Errorf("%s websocket error: %s", - g.Name, - orderQuery.Error.Message) - } - - return &orderQuery, nil + return assetPairEnabled } diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 367daa72..f37d20ba 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "sort" "strconv" "strings" @@ -11,7 +12,6 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -49,7 +49,6 @@ func (g *Gateio) GetDefaultConfig(ctx context.Context) (*config.Exchange, error) return nil, err } } - return exchCfg, nil } @@ -61,9 +60,9 @@ func (g *Gateio) SetDefaults() { g.API.CredentialsValidator.RequiresKey = true g.API.CredentialsValidator.RequiresSecret = true - requestFmt := ¤cy.PairFormat{Delimiter: currency.UnderscoreDelimiter} + requestFmt := ¤cy.PairFormat{Delimiter: currency.UnderscoreDelimiter, Uppercase: true} configFmt := ¤cy.PairFormat{Delimiter: currency.UnderscoreDelimiter, Uppercase: true} - err := g.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot) + err := g.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot, asset.Futures, asset.Margin, asset.CrossMargin, asset.DeliveryFutures, asset.Options) if err != nil { log.Errorln(log.ExchangeSys, err) } @@ -115,31 +114,61 @@ func (g *Gateio) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( + kline.IntervalCapacity{Interval: kline.HundredMilliseconds}, + kline.IntervalCapacity{Interval: kline.ThousandMilliseconds}, + kline.IntervalCapacity{Interval: kline.TenSecond}, + kline.IntervalCapacity{Interval: kline.ThirtySecond}, 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}, + kline.IntervalCapacity{Interval: kline.OneMonth}, + kline.IntervalCapacity{Interval: kline.ThreeMonth}, + kline.IntervalCapacity{Interval: kline.SixMonth}, ), - GlobalResultLimit: 1001, + GlobalResultLimit: 1000, }, }, } g.Requester, err = request.New(g.Name, - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), + request.WithLimiter(SetRateLimit()), + ) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + err = g.DisableAssetWebsocketSupport(asset.Margin) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + err = g.DisableAssetWebsocketSupport(asset.CrossMargin) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + err = g.DisableAssetWebsocketSupport(asset.Futures) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + err = g.DisableAssetWebsocketSupport(asset.DeliveryFutures) + if err != nil { + log.Errorln(log.ExchangeSys, err) + } + err = g.DisableAssetWebsocketSupport(asset.Options) if err != nil { log.Errorln(log.ExchangeSys, err) } g.API.Endpoints = g.NewEndpoints() err = g.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{ exchange.RestSpot: gateioTradeURL, - exchange.RestSpotSupplementary: gateioMarketURL, + exchange.RestFutures: gateioFuturesLiveTradingAlternative, + exchange.RestSpotSupplementary: gateioFuturesTestnetTrading, exchange.WebsocketSpot: gateioWebsocketEndpoint, }) if err != nil { @@ -177,6 +206,7 @@ func (g *Gateio) Setup(exch *config.Exchange) error { RunningURL: wsRunningURL, Connector: g.WsConnect, Subscriber: g.Subscribe, + Unsubscriber: g.Unsubscribe, GenerateSubscriptions: g.GenerateDefaultSubscriptions, ConnectionMonitorDelay: exch.ConnectionMonitorDelay, Features: &g.Features.Supports.WebsocketCapabilities, @@ -184,8 +214,8 @@ func (g *Gateio) Setup(exch *config.Exchange) error { if err != nil { return err } - return g.Websocket.SetupNewConnection(stream.ConnectionSetup{ + URL: gateioWebsocketEndpoint, RateLimit: gateioWebsocketRateLimit, ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, ResponseMaxLimit: exch.WebsocketResponseMaxLimit, @@ -210,92 +240,435 @@ func (g *Gateio) Run(ctx context.Context) { if g.Verbose { g.PrintEnabledPairs() } - if !g.GetEnabledFeatures().AutoPairUpdates { return } - err := g.UpdateTradablePairs(ctx, false) if err != nil { log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", g.Name, err) } } -// FetchTradablePairs returns a list of the exchanges tradable pairs -func (g *Gateio) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) { +// UpdateTicker updates and returns the ticker for a currency pair +func (g *Gateio) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) { if !g.SupportsAsset(a) { - return nil, asset.ErrNotSupported + return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) } - symbols, err := g.GetSymbols(ctx) + fPair, err := g.FormatExchangeCurrency(p, a) if err != nil { return nil, err } - return currency.NewPairsFromStrings(symbols) + if fPair.IsEmpty() || fPair.Quote.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + fPair = fPair.Upper() + var tickerData *ticker.Price + switch a { + case asset.Margin, asset.Spot, asset.CrossMargin: + var available bool + available, err = g.checkInstrumentAvailabilityInSpot(fPair) + if err != nil { + return nil, err + } + if a != asset.Spot && !available { + return nil, fmt.Errorf("%v instrument %v does not have ticker data", a, fPair) + } + var tickerNew *Ticker + tickerNew, err = g.GetTicker(ctx, fPair.String(), "") + if err != nil { + return nil, err + } + tickerData = &ticker.Price{ + Pair: fPair, + Low: tickerNew.Low24H.Float64(), + High: tickerNew.High24H.Float64(), + Bid: tickerNew.HighestBid.Float64(), + Ask: tickerNew.LowestAsk.Float64(), + Last: tickerNew.Last.Float64(), + ExchangeName: g.Name, + AssetType: a, + } + case asset.Futures: + var settle string + settle, err = g.getSettlementFromCurrency(fPair, true) + if err != nil { + return nil, err + } + var tickers []FuturesTicker + tickers, err = g.GetFuturesTickers(ctx, settle, fPair) + if err != nil { + return nil, err + } + var tick *FuturesTicker + for x := range tickers { + if tickers[x].Contract == fPair.String() { + tick = &tickers[x] + break + } + } + if tick == nil { + return nil, errNoTickerData + } + tickerData = &ticker.Price{ + Pair: fPair, + Low: tick.Low24H, + High: tick.High24H, + Last: tick.Last, + Volume: tick.Volume24HBase, + QuoteVolume: tick.Volume24HQuote, + ExchangeName: g.Name, + AssetType: a, + } + case asset.Options: + var underlying currency.Pair + var tickers []OptionsTicker + underlying, err = g.GetUnderlyingFromCurrencyPair(fPair) + if err != nil { + return nil, err + } + tickers, err = g.GetOptionsTickers(ctx, underlying.String()) + if err != nil { + return nil, err + } + for x := range tickers { + if tickers[x].Name != fPair.String() { + continue + } + var cp currency.Pair + cp, err = currency.NewPairFromString(strings.ReplaceAll(tickers[x].Name, currency.DashDelimiter, currency.UnderscoreDelimiter)) + if err != nil { + return nil, err + } + cp.Quote = currency.NewCode(strings.ReplaceAll(cp.Quote.String(), currency.UnderscoreDelimiter, currency.DashDelimiter)) + if err != nil { + return nil, err + } + tickerData = &ticker.Price{ + Pair: cp, + Last: tickers[x].LastPrice.Float64(), + Bid: tickers[x].Bid1Price, + Ask: tickers[x].Ask1Price, + AskSize: tickers[x].Ask1Size, + BidSize: tickers[x].Bid1Size, + ExchangeName: g.Name, + AssetType: a, + } + err = ticker.ProcessTicker(tickerData) + if err != nil { + return nil, err + } + } + return ticker.GetTicker(g.Name, fPair, a) + case asset.DeliveryFutures: + var settle string + settle, err = g.getSettlementFromCurrency(fPair, false) + if err != nil { + return nil, err + } + var tickers []FuturesTicker + tickers, err = g.GetDeliveryFutureTickers(ctx, settle, fPair) + if err != nil { + return nil, err + } + for x := range tickers { + if tickers[x].Contract == fPair.Upper().String() { + tickerData = &ticker.Price{ + Pair: fPair, + Last: tickers[x].Last, + High: tickers[x].High24H, + Low: tickers[x].Low24H, + Volume: tickers[x].Volume24H, + QuoteVolume: tickers[x].Volume24HQuote, + ExchangeName: g.Name, + AssetType: a, + } + break + } + } + } + err = ticker.ProcessTicker(tickerData) + if err != nil { + return nil, err + } + return ticker.GetTicker(g.Name, fPair, a) +} + +// FetchTicker retrieves a list of tickers. +func (g *Gateio) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) { + fPair, err := g.FormatExchangeCurrency(p, assetType) + if err != nil { + return nil, err + } + tickerNew, err := ticker.GetTicker(g.Name, fPair, assetType) + if err != nil { + return g.UpdateTicker(ctx, fPair, assetType) + } + return tickerNew, nil +} + +// FetchTradablePairs returns a list of the exchanges tradable pairs +func (g *Gateio) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) { + if !g.SupportsAsset(a) { + return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) + } + switch a { + case asset.Spot: + tradables, err := g.ListSpotCurrencyPairs(ctx) + if err != nil { + return nil, err + } + pairs := make([]currency.Pair, 0, len(tradables)) + for x := range tradables { + if tradables[x].TradeStatus == "untradable" { + continue + } + p := strings.ToUpper(tradables[x].ID) + if !g.IsValidPairString(p) { + continue + } + cp, err := currency.NewPairFromString(p) + if err != nil { + return nil, err + } + pairs = append(pairs, cp) + } + return pairs, nil + case asset.Margin, asset.CrossMargin: + tradables, err := g.GetMarginSupportedCurrencyPairs(ctx) + if err != nil { + return nil, err + } + pairs := make([]currency.Pair, 0, len(tradables)) + for x := range tradables { + if tradables[x].Status == 0 { + continue + } + p := strings.ToUpper(tradables[x].Base + currency.UnderscoreDelimiter + tradables[x].Quote) + if !g.IsValidPairString(p) { + continue + } + cp, err := currency.NewPairFromString(p) + if err != nil { + return nil, err + } + pairs = append(pairs, cp) + } + return pairs, nil + case asset.Futures: + btcContracts, err := g.GetAllFutureContracts(ctx, settleBTC) + if err != nil { + return nil, err + } + usdtContracts, err := g.GetAllFutureContracts(ctx, settleUSDT) + if err != nil { + return nil, err + } + btcContracts = append(btcContracts, usdtContracts...) + pairs := make([]currency.Pair, 0, len(btcContracts)) + for x := range btcContracts { + if btcContracts[x].InDelisting { + continue + } + p := strings.ToUpper(btcContracts[x].Name) + if !g.IsValidPairString(p) { + continue + } + cp, err := currency.NewPairFromString(p) + if err != nil { + return nil, err + } + pairs = append(pairs, cp) + } + return pairs, nil + case asset.DeliveryFutures: + btcContracts, err := g.GetAllDeliveryContracts(ctx, settleBTC) + if err != nil { + return nil, err + } + usdtContracts, err := g.GetAllDeliveryContracts(ctx, settleUSDT) + if err != nil { + return nil, err + } + btcContracts = append(btcContracts, usdtContracts...) + pairs := make([]currency.Pair, 0, len(btcContracts)) + for x := range btcContracts { + if btcContracts[x].InDelisting { + continue + } + p := strings.ToUpper(btcContracts[x].Name) + if !g.IsValidPairString(p) { + continue + } + cp, err := currency.NewPairFromString(p) + if err != nil { + return nil, err + } + pairs = append(pairs, cp) + } + return pairs, nil + case asset.Options: + underlyings, err := g.GetAllOptionsUnderlyings(ctx) + if err != nil { + return nil, err + } + var pairs []currency.Pair + for x := range underlyings { + contracts, err := g.GetAllContractOfUnderlyingWithinExpiryDate(ctx, underlyings[x].Name, time.Time{}) + if err != nil { + return nil, err + } + for c := range contracts { + if !g.IsValidPairString(contracts[c].Name) { + continue + } + cp, err := currency.NewPairFromString(strings.ReplaceAll(contracts[c].Name, currency.DashDelimiter, currency.UnderscoreDelimiter)) + if err != nil { + return nil, err + } + cp.Quote = currency.NewCode(strings.ReplaceAll(cp.Quote.String(), currency.UnderscoreDelimiter, currency.DashDelimiter)) + if err != nil { + return nil, err + } + pairs = append(pairs, cp) + } + } + return pairs, nil + default: + return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) + } } // UpdateTradablePairs updates the exchanges available pairs and stores // them in the exchanges config func (g *Gateio) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error { - pairs, err := g.FetchTradablePairs(ctx, asset.Spot) - if err != nil { - return err + assets := g.GetAssetTypes(false) + for x := range assets { + pairs, err := g.FetchTradablePairs(ctx, assets[x]) + if err != nil { + return err + } + if len(pairs) == 0 { + return errors.New("no tradable pairs found") + } + err = g.UpdatePairs(pairs, assets[x], false, forceUpdate) + if err != nil { + return err + } } - return g.UpdatePairs(pairs, asset.Spot, false, forceUpdate) + return nil } // UpdateTickers updates the ticker for all currency pairs of a given asset type func (g *Gateio) UpdateTickers(ctx context.Context, a asset.Item) error { - result, err := g.GetTickers(ctx) - if err != nil { - return err + if !g.SupportsAsset(a) { + return fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) } - pairs, err := g.GetEnabledPairs(a) - if err != nil { - return err - } - for p := range pairs { - for k := range result { - if !strings.EqualFold(k, pairs[p].String()) { - continue + var err error + switch a { + case asset.Spot, asset.Margin, asset.CrossMargin: + var tickers []Ticker + tickers, err = g.GetTickers(ctx, currency.EMPTYPAIR.String(), "") + if err != nil { + return err + } + for x := range tickers { + var currencyPair currency.Pair + currencyPair, err = currency.NewPairFromString(tickers[x].CurrencyPair) + if err != nil { + return err } - err = ticker.ProcessTicker(&ticker.Price{ - Last: result[k].Last, - High: result[k].High, - Low: result[k].Low, - Volume: result[k].BaseVolume, - QuoteVolume: result[k].QuoteVolume, - Open: result[k].Open, - Close: result[k].Close, - Pair: pairs[p], + Last: tickers[x].Last.Float64(), + High: tickers[x].High24H.Float64(), + Low: tickers[x].Low24H.Float64(), + Bid: tickers[x].HighestBid.Float64(), + Ask: tickers[x].LowestAsk.Float64(), + QuoteVolume: tickers[x].QuoteVolume.Float64(), + Volume: tickers[x].BaseVolume.Float64(), ExchangeName: g.Name, - AssetType: a}) + Pair: currencyPair, + AssetType: a, + }) if err != nil { return err } } + case asset.Futures, asset.DeliveryFutures: + var tickers []FuturesTicker + var ticks []FuturesTicker + for _, settle := range []string{settleBTC, settleUSDT, settleUSD} { + if a == asset.Futures { + ticks, err = g.GetFuturesTickers(ctx, settle, currency.EMPTYPAIR) + } else { + if settle == settleUSD { + continue + } + ticks, err = g.GetDeliveryFutureTickers(ctx, settle, currency.EMPTYPAIR) + } + if err != nil { + return err + } + tickers = append(tickers, ticks...) + } + for x := range tickers { + currencyPair, err := currency.NewPairFromString(tickers[x].Contract) + if err != nil { + return err + } + err = ticker.ProcessTicker(&ticker.Price{ + Last: tickers[x].Last, + High: tickers[x].High24H, + Low: tickers[x].Low24H, + Volume: tickers[x].Volume24H, + QuoteVolume: tickers[x].Volume24HQuote, + ExchangeName: g.Name, + Pair: currencyPair, + AssetType: a, + }) + if err != nil { + return err + } + } + case asset.Options: + pairs, err := g.GetEnabledPairs(a) + if err != nil { + return err + } + for i := range pairs { + underlying, err := g.GetUnderlyingFromCurrencyPair(pairs[i]) + if err != nil { + return err + } + tickers, err := g.GetOptionsTickers(ctx, underlying.String()) + if err != nil { + return err + } + for x := range tickers { + currencyPair, err := currency.NewPairFromString(tickers[x].Name) + if err != nil { + return err + } + err = ticker.ProcessTicker(&ticker.Price{ + Last: tickers[x].LastPrice.Float64(), + Ask: tickers[x].Ask1Price, + AskSize: tickers[x].Ask1Size, + Bid: tickers[x].Bid1Price, + BidSize: tickers[x].Bid1Size, + Pair: currencyPair, + ExchangeName: g.Name, + AssetType: a, + }) + if err != nil { + return err + } + } + } + default: + return fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) } - return nil } -// UpdateTicker updates and returns the ticker for a currency pair -func (g *Gateio) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) { - if err := g.UpdateTickers(ctx, a); err != nil { - return nil, err - } - return ticker.GetTicker(g.Name, p, a) -} - -// FetchTicker returns the ticker for a currency pair -func (g *Gateio) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) { - tickerNew, err := ticker.GetTicker(g.Name, p, assetType) - if err != nil { - return g.UpdateTicker(ctx, p, assetType) - } - return tickerNew, nil -} - // FetchOrderbook returns orderbook base on the currency pair func (g *Gateio) FetchOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { ob, err := orderbook.Get(g.Name, p, assetType) @@ -306,23 +679,53 @@ func (g *Gateio) FetchOrderbook(ctx context.Context, p currency.Pair, assetType } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (g *Gateio) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { +func (g *Gateio) UpdateOrderbook(ctx context.Context, p currency.Pair, a asset.Item) (*orderbook.Base, error) { + p, err := g.FormatExchangeCurrency(p, a) + if err != nil { + return nil, err + } + var orderbookNew *Orderbook + switch a { + case asset.Spot, asset.Margin, asset.CrossMargin: + var available bool + available, err = g.checkInstrumentAvailabilityInSpot(p) + if err != nil { + return nil, err + } + if a != asset.Spot && !available { + return nil, fmt.Errorf("%v instrument %v does not have orderbook data", a, p) + } + orderbookNew, err = g.GetOrderbook(ctx, p.String(), "", 0, true) + case asset.Futures: + var settle string + settle, err = g.getSettlementFromCurrency(p, true) + if err != nil { + return nil, err + } + orderbookNew, err = g.GetFuturesOrderbook(ctx, settle, p.String(), "", 0, true) + case asset.DeliveryFutures: + var settle string + settle, err = g.getSettlementFromCurrency(p.Upper(), false) + if err != nil { + return nil, err + } + orderbookNew, err = g.GetDeliveryOrderbook(ctx, settle, "", p, 0, true) + case asset.Options: + orderbookNew, err = g.GetOptionsOrderbook(ctx, p, "", 0, true) + default: + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + } + if err != nil { + return nil, err + } book := &orderbook.Base{ Exchange: g.Name, - Pair: p, - Asset: assetType, + Asset: a, VerifyOrderbook: g.CanVerifyOrderbook, + Pair: p.Upper(), + LastUpdateID: orderbookNew.ID, + LastUpdated: orderbookNew.Update, } - curr, err := g.FormatExchangeCurrency(p, assetType) - if err != nil { - return book, err - } - - orderbookNew, err := g.GetOrderbook(ctx, curr.String()) - if err != nil { - return book, err - } - book.Bids = make(orderbook.Items, len(orderbookNew.Bids)) for x := range orderbookNew.Bids { book.Bids[x] = orderbook.Item{ @@ -330,7 +733,6 @@ func (g *Gateio) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType Price: orderbookNew.Bids[x].Price, } } - book.Asks = make(orderbook.Items, len(orderbookNew.Asks)) for x := range orderbookNew.Asks { book.Asks[x] = orderbook.Item{ @@ -342,103 +744,113 @@ func (g *Gateio) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType if err != nil { return book, err } - return orderbook.Get(g.Name, p, assetType) + return orderbook.Get(g.Name, book.Pair, a) } // UpdateAccountInfo retrieves balances for all enabled currencies for the -// ZB exchange -func (g *Gateio) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { +func (g *Gateio) UpdateAccountInfo(ctx context.Context, a asset.Item) (account.Holdings, error) { var info account.Holdings - var balances []account.Balance - - if g.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - resp, err := g.wsGetBalance([]string{}) + info.Exchange = g.Name + var err error + switch a { + case asset.Spot: + var balances []SpotAccount + balances, err = g.GetSpotAccounts(ctx, currency.EMPTYCODE) + currencies := make([]account.Balance, len(balances)) if err != nil { return info, err } - var currData []account.Balance - for k := range resp.Result { - currData = append(currData, account.Balance{ - Currency: currency.NewCode(k), - Total: resp.Result[k].Available + resp.Result[k].Freeze, - Hold: resp.Result[k].Freeze, - Free: resp.Result[k].Available, + for x := range balances { + currencies[x] = account.Balance{ + Currency: currency.NewCode(balances[x].Currency), + Total: balances[x].Available - balances[x].Locked, + Hold: balances[x].Locked, + Free: balances[x].Available, + } + } + info.Accounts = append(info.Accounts, account.SubAccount{ + AssetType: a, + Currencies: currencies, + }) + case asset.Margin, asset.CrossMargin: + var balances []MarginAccountItem + balances, err = g.GetMarginAccountList(ctx, currency.EMPTYPAIR) + if err != nil { + return info, err + } + var currencies []account.Balance + for x := range balances { + currencies = append(currencies, account.Balance{ + Currency: currency.NewCode(balances[x].Base.Currency), + Total: balances[x].Base.Available + balances[x].Base.LockedAmount, + Hold: balances[x].Base.LockedAmount, + Free: balances[x].Base.Available, + }, account.Balance{ + Currency: currency.NewCode(balances[x].Quote.Currency), + Total: balances[x].Quote.Available + balances[x].Quote.LockedAmount, + Hold: balances[x].Quote.LockedAmount, + Free: balances[x].Quote.Available, }) } info.Accounts = append(info.Accounts, account.SubAccount{ - Currencies: currData, - AssetType: assetType, + AssetType: a, + Currencies: currencies, }) - } else { - balance, err := g.GetBalances(ctx) + case asset.Futures, asset.DeliveryFutures: + currencies := make([]account.Balance, 3) + settles := []currency.Code{currency.BTC, currency.USDT, currency.USD} + for x := range settles { + var balance *FuturesAccount + if a == asset.Futures { + if settles[x].Equal(currency.USD) { + continue + } + balance, err = g.QueryFuturesAccount(ctx, settles[x].String()) + } else { + balance, err = g.GetDeliveryFuturesAccounts(ctx, settles[x].String()) + } + if err != nil { + return info, err + } + currencies[x] = account.Balance{ + Currency: currency.NewCode(balance.Currency), + Total: balance.Total, + Hold: balance.Total - balance.Available, + Free: balance.Available, + } + } + info.Accounts = append(info.Accounts, account.SubAccount{ + AssetType: a, + Currencies: currencies, + }) + case asset.Options: + var balance *OptionAccount + balance, err = g.GetOptionAccounts(ctx) if err != nil { return info, err } - - switch l := balance.Locked.(type) { - case map[string]interface{}: - for x := range l { - var lockedF float64 - lockedF, err = strconv.ParseFloat(l[x].(string), 64) - if err != nil { - return info, err - } - - balances = append(balances, account.Balance{ - Currency: currency.NewCode(x), - Hold: lockedF, - }) - } - default: - break - } - - switch v := balance.Available.(type) { - case map[string]interface{}: - for x := range v { - var availAmount float64 - availAmount, err = strconv.ParseFloat(v[x].(string), 64) - if err != nil { - return info, err - } - - var updated bool - for i := range balances { - if !balances[i].Currency.Equal(currency.NewCode(x)) { - continue - } - balances[i].Total = balances[i].Hold + availAmount - balances[i].Free = availAmount - balances[i].AvailableWithoutBorrow = availAmount - updated = true - break - } - if !updated { - balances = append(balances, account.Balance{ - Currency: currency.NewCode(x), - Total: availAmount, - }) - } - } - default: - break - } - info.Accounts = append(info.Accounts, account.SubAccount{ - AssetType: assetType, - Currencies: balances, + AssetType: a, + Currencies: []account.Balance{ + { + Currency: currency.NewCode(balance.Currency), + Total: balance.Total, + Hold: balance.Total - balance.Available, + Free: balance.Available, + }, + }, }) + default: + return info, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) } - - info.Exchange = g.Name creds, err := g.GetCredentials(ctx) if err != nil { - return account.Holdings{}, err + return info, err } - if err := account.Process(&info, creds); err != nil { - return account.Holdings{}, err + err = account.Process(&info, creds) + if err != nil { + return info, err } - return info, nil } @@ -462,46 +874,132 @@ func (g *Gateio) GetFundingHistory(_ context.Context) ([]exchange.FundHistory, e } // GetWithdrawalsHistory returns previous withdrawals data -func (g *Gateio) GetWithdrawalsHistory(_ context.Context, _ currency.Code, _ asset.Item) (resp []exchange.WithdrawalHistory, err error) { - return nil, common.ErrNotYetImplemented +func (g *Gateio) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) { + records, err := g.GetWithdrawalRecords(ctx, c, time.Time{}, time.Time{}, 0, 0) + if err != nil { + return nil, err + } + withdrawalHistories := make([]exchange.WithdrawalHistory, len(records)) + for x := range records { + withdrawalHistories[x] = exchange.WithdrawalHistory{ + Status: records[x].Status, + TransferID: records[x].ID, + Currency: records[x].Currency, + Amount: records[x].Amount, + CryptoTxID: records[x].TransactionID, + CryptoToAddress: records[x].WithdrawalAddress, + Timestamp: records[x].Timestamp.Time(), + } + } + return withdrawalHistories, nil } // GetRecentTrades returns the most recent trades for a currency and asset -func (g *Gateio) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) { - var err error - p, err = g.FormatExchangeCurrency(p, assetType) +func (g *Gateio) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.Item) ([]trade.Data, error) { + p, err := g.FormatExchangeCurrency(p, a) if err != nil { return nil, err } - var tradeData TradeHistory - tradeData, err = g.GetTrades(ctx, p.String()) - if err != nil { - return nil, err - } - resp := make([]trade.Data, len(tradeData.Data)) - for i := range tradeData.Data { - var side order.Side - side, err = order.StringToOrderSide(tradeData.Data[i].Type) + var resp []trade.Data + switch a { + case asset.Spot, asset.Margin, asset.CrossMargin: + var tradeData []Trade + if p.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + tradeData, err = g.GetMarketTrades(ctx, p, 0, "", false, time.Time{}, time.Time{}, 0) if err != nil { return nil, err } - resp[i] = trade.Data{ - Exchange: g.Name, - TID: tradeData.Data[i].TradeID, - CurrencyPair: p, - AssetType: assetType, - Side: side, - Price: tradeData.Data[i].Rate, - Amount: tradeData.Data[i].Amount, - Timestamp: time.Unix(tradeData.Data[i].Timestamp, 0), + resp = make([]trade.Data, len(tradeData)) + for i := range tradeData { + var side order.Side + side, err = order.StringToOrderSide(tradeData[i].Side) + if err != nil { + return nil, err + } + resp[i] = trade.Data{ + Exchange: g.Name, + TID: tradeData[i].OrderID, + CurrencyPair: p, + AssetType: a, + Side: side, + Price: tradeData[i].Price, + Amount: tradeData[i].Amount, + Timestamp: tradeData[i].CreateTimeMs.Time(), + } } + case asset.Futures: + var settle string + settle, err = g.getSettlementFromCurrency(p, true) + if err != nil { + return nil, err + } + var futuresTrades []TradingHistoryItem + futuresTrades, err = g.GetFuturesTradingHistory(ctx, settle, p, 0, 0, "", time.Time{}, time.Time{}) + if err != nil { + return nil, err + } + resp = make([]trade.Data, len(futuresTrades)) + for i := range futuresTrades { + resp[i] = trade.Data{ + TID: strconv.FormatInt(futuresTrades[i].ID, 10), + Exchange: g.Name, + CurrencyPair: p, + AssetType: a, + Price: futuresTrades[i].Price, + Amount: futuresTrades[i].Size, + Timestamp: futuresTrades[i].CreateTime.Time(), + } + } + case asset.DeliveryFutures: + var settle string + settle, err = g.getSettlementFromCurrency(p, false) + if err != nil { + return nil, err + } + var deliveryTrades []DeliveryTradingHistory + deliveryTrades, err = g.GetDeliveryTradingHistory(ctx, settle, "", p.Upper(), 0, time.Time{}, time.Time{}) + if err != nil { + return nil, err + } + resp = make([]trade.Data, len(deliveryTrades)) + for i := range deliveryTrades { + resp[i] = trade.Data{ + TID: strconv.FormatInt(deliveryTrades[i].ID, 10), + Exchange: g.Name, + CurrencyPair: p, + AssetType: a, + Price: deliveryTrades[i].Price, + Amount: deliveryTrades[i].Size, + Timestamp: deliveryTrades[i].CreateTime.Time(), + } + } + case asset.Options: + var trades []TradingHistoryItem + trades, err = g.GetOptionsTradeHistory(ctx, p.Upper(), "", 0, 0, time.Time{}, time.Time{}) + if err != nil { + return nil, err + } + resp = make([]trade.Data, len(trades)) + for i := range trades { + resp[i] = trade.Data{ + TID: strconv.FormatInt(trades[i].ID, 10), + Exchange: g.Name, + CurrencyPair: p, + AssetType: a, + Price: trades[i].Price, + Amount: trades[i].Size, + Timestamp: trades[i].CreateTime.Time(), + } + } + default: + return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) } - err = g.AddTradesToBuffer(resp...) if err != nil { return nil, err } - sort.Sort(trade.ByDate(resp)) return resp, nil } @@ -514,45 +1012,171 @@ func (g *Gateio) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.I // SubmitOrder submits a new order // TODO: support multiple order types (IOC) func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + err := s.Validate() + if err != nil { return nil, err } - var orderTypeFormat string - if s.Side == order.Buy { + switch s.Side { + case order.Buy: orderTypeFormat = order.Buy.Lower() - } else { + case order.Sell: orderTypeFormat = order.Sell.Lower() + case order.Bid: + orderTypeFormat = order.Bid.Lower() + case order.Ask: + orderTypeFormat = order.Ask.Lower() + default: + return nil, errInvalidOrderSide } - - fPair, err := g.FormatExchangeCurrency(s.Pair, s.AssetType) + s.Pair, err = g.FormatExchangeCurrency(s.Pair, s.AssetType) if err != nil { return nil, err } - - var spotNewOrderRequestParams = SpotNewOrderRequestParams{ - Amount: s.Amount, - Price: s.Price, - Symbol: fPair.String(), - Type: orderTypeFormat, + s.Pair = s.Pair.Upper() + switch s.AssetType { + case asset.Spot, asset.Margin, asset.CrossMargin: + if s.Type != order.Limit { + return nil, errOnlyLimitOrderType + } + sOrder, err := g.PlaceSpotOrder(ctx, &CreateOrderRequestData{ + Side: orderTypeFormat, + Type: s.Type.Lower(), + Account: g.assetTypeToString(s.AssetType), + Amount: s.Amount, + Price: s.Price, + CurrencyPair: s.Pair, + Text: s.ClientOrderID, + }) + if err != nil { + return nil, err + } + response, err := s.DeriveSubmitResponse(sOrder.OrderID) + if err != nil { + return nil, err + } + side, err := order.StringToOrderSide(sOrder.Side) + if err != nil { + return nil, err + } + response.Side = side + status, err := order.StringToOrderStatus(sOrder.Status) + if err != nil { + return nil, err + } + response.Status = status + response.Fee = sOrder.FeeDeducted + response.FeeAsset = currency.NewCode(sOrder.FeeCurrency) + response.Pair = s.Pair + response.Date = sOrder.CreateTime.Time() + response.ClientOrderID = sOrder.Text + response.Date = sOrder.CreateTimeMs.Time() + response.LastUpdated = sOrder.UpdateTimeMs.Time() + return response, nil + case asset.Futures: + settle, err := g.getSettlementFromCurrency(s.Pair, true) + if err != nil { + return nil, err + } + if orderTypeFormat == "bid" && s.Price < 0 { + s.Price = -s.Price + } else if orderTypeFormat == "ask" && s.Price > 0 { + s.Price = -s.Price + } + fOrder, err := g.PlaceFuturesOrder(ctx, &OrderCreateParams{ + Contract: s.Pair, + Size: s.Amount, + Price: s.Price, + Settle: settle, + ReduceOnly: s.ReduceOnly, + TimeInForce: "gtc", + Text: s.ClientOrderID, + }) + if err != nil { + return nil, err + } + response, err := s.DeriveSubmitResponse(strconv.FormatInt(fOrder.ID, 10)) + if err != nil { + return nil, err + } + status, err := order.StringToOrderStatus(fOrder.Status) + if err != nil { + return nil, err + } + response.Status = status + response.Pair = s.Pair + response.Date = fOrder.CreateTime.Time() + response.ClientOrderID = fOrder.Text + response.ReduceOnly = fOrder.IsReduceOnly + response.Amount = fOrder.RemainingAmount + return response, nil + case asset.DeliveryFutures: + settle, err := g.getSettlementFromCurrency(s.Pair, false) + if err != nil { + return nil, err + } + if orderTypeFormat == "bid" && s.Price < 0 { + s.Price = -s.Price + } else if orderTypeFormat == "ask" && s.Price > 0 { + s.Price = -s.Price + } + newOrder, err := g.PlaceDeliveryOrder(ctx, &OrderCreateParams{ + Contract: s.Pair, + Size: s.Amount, + Price: s.Price, + Settle: settle, + ReduceOnly: s.ReduceOnly, + TimeInForce: "gtc", + Text: s.ClientOrderID, + }) + if err != nil { + return nil, err + } + response, err := s.DeriveSubmitResponse(strconv.FormatInt(newOrder.ID, 10)) + if err != nil { + return nil, err + } + status, err := order.StringToOrderStatus(newOrder.Status) + if err != nil { + return nil, err + } + response.Status = status + response.Pair = s.Pair + response.Date = newOrder.CreateTime.Time() + response.ClientOrderID = newOrder.Text + response.Amount = newOrder.Size + response.Price = newOrder.OrderPrice + return response, nil + case asset.Options: + optionOrder, err := g.PlaceOptionOrder(ctx, OptionOrderParam{ + Contract: s.Pair.String(), + OrderSize: s.Amount, + Price: s.Price, + ReduceOnly: s.ReduceOnly, + Text: s.ClientOrderID, + }) + if err != nil { + return nil, err + } + response, err := s.DeriveSubmitResponse(strconv.FormatInt(optionOrder.OptionOrderID, 10)) + if err != nil { + return nil, err + } + status, err := order.StringToOrderStatus(optionOrder.Status) + if err != nil { + return nil, err + } + response.Status = status + response.Pair = s.Pair + response.Date = optionOrder.CreateTime.Time() + response.ClientOrderID = optionOrder.Text + return response, nil + default: + return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, s.AssetType) } - - response, err := g.SpotNewOrder(ctx, spotNewOrderRequestParams) - if err != nil { - return nil, err - } - subResp, err := s.DeriveSubmitResponse(strconv.FormatInt(response.OrderNumber, 10)) - if err != nil { - return nil, err - } - if response.LeftAmount == 0 { - subResp.Status = order.Filled - } - return subResp, nil } -// ModifyOrder will allow of changing orderbook placement and limit to -// market conversion +// ModifyOrder will allow of changing orderbook placement and limit to market conversion func (g *Gateio) ModifyOrder(_ context.Context, _ *order.Modify) (*order.ModifyResponse, error) { return nil, common.ErrFunctionNotSupported } @@ -562,113 +1186,335 @@ func (g *Gateio) CancelOrder(ctx context.Context, o *order.Cancel) error { if err := o.Validate(o.StandardCancel()); err != nil { return err } - - orderIDInt, err := strconv.ParseInt(o.OrderID, 10, 64) + fPair, err := g.FormatExchangeCurrency(o.Pair, o.AssetType) if err != nil { return err } - - fpair, err := g.FormatExchangeCurrency(o.Pair, o.AssetType) - if err != nil { - return err + switch o.AssetType { + case asset.Spot, asset.Margin, asset.CrossMargin: + _, err = g.CancelSingleSpotOrder(ctx, o.OrderID, fPair.String(), o.AssetType == asset.CrossMargin) + case asset.Futures, asset.DeliveryFutures: + var settle string + settle, err = g.getSettlementFromCurrency(fPair, true) + if err != nil { + return err + } + if o.AssetType == asset.Futures { + _, err = g.CancelSingleFuturesOrder(ctx, settle, o.OrderID) + } else { + _, err = g.CancelSingleDeliveryOrder(ctx, settle, o.OrderID) + } + if err != nil { + return err + } + case asset.Options: + _, err = g.CancelOptionSingleOrder(ctx, o.OrderID) + default: + return fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, o.AssetType) } - - _, err = g.CancelExistingOrder(ctx, orderIDInt, fpair.String()) return err } // CancelBatchOrders cancels an orders by their corresponding ID numbers -func (g *Gateio) CancelBatchOrders(_ context.Context, _ []order.Cancel) (order.CancelBatchResponse, error) { - return order.CancelBatchResponse{}, common.ErrNotYetImplemented +func (g *Gateio) CancelBatchOrders(ctx context.Context, o []order.Cancel) (order.CancelBatchResponse, error) { + var response order.CancelBatchResponse + response.Status = map[string]string{} + if len(o) == 0 { + return response, errors.New("no cancel order passed") + } + var err error + var cancelSpotOrdersParam []CancelOrderByIDParam + a := o[0].AssetType + for x := range o { + o[x].Pair, err = g.FormatExchangeCurrency(o[x].Pair, a) + if err != nil { + return response, err + } + o[x].Pair = o[x].Pair.Upper() + if a != o[x].AssetType { + return response, errors.New("cannot cancel orders of different asset types") + } + if a == asset.Spot || a == asset.Margin || a == asset.CrossMargin { + cancelSpotOrdersParam = append(cancelSpotOrdersParam, CancelOrderByIDParam{ + ID: o[x].OrderID, + CurrencyPair: o[x].Pair, + }) + continue + } + err = o[x].Validate(o[x].StandardCancel()) + if err != nil { + return response, err + } + } + switch a { + case asset.Spot, asset.Margin, asset.CrossMargin: + loop := int(math.Ceil(float64(len(cancelSpotOrdersParam)) / 10)) + for count := 0; count < loop; count++ { + var input []CancelOrderByIDParam + if (count + 1) == loop { + input = cancelSpotOrdersParam[count*10:] + } else { + input = cancelSpotOrdersParam[count*10 : (count*10)+10] + } + var cancel []CancelOrderByIDResponse + cancel, err = g.CancelBatchOrdersWithIDList(ctx, input) + if err != nil { + return response, err + } + for x := range cancel { + response.Status[cancel[x].OrderID] = func() string { + if cancel[x].Succeeded { + return order.Cancelled.String() + } + return "" + }() + } + } + case asset.Futures: + for a := range o { + cancel, err := g.CancelMultipleFuturesOpenOrders(ctx, o[a].Pair, o[a].Side.Lower(), o[a].Pair.Quote.String()) + if err != nil { + return response, err + } + for x := range cancel { + response.Status[strconv.FormatInt(cancel[x].ID, 10)] = cancel[x].Status + } + } + case asset.DeliveryFutures: + for a := range o { + settle, err := g.getSettlementFromCurrency(o[a].Pair, false) + if err != nil { + return response, err + } + cancel, err := g.CancelMultipleDeliveryOrders(ctx, o[a].Pair, o[a].Side.Lower(), settle) + if err != nil { + return response, err + } + for x := range cancel { + response.Status[strconv.FormatInt(cancel[x].ID, 10)] = cancel[x].Status + } + } + case asset.Options: + for a := range o { + cancel, err := g.CancelMultipleOptionOpenOrders(ctx, o[a].Pair, o[a].Pair.String(), o[a].Side.Lower()) + if err != nil { + return response, err + } + for x := range cancel { + response.Status[strconv.FormatInt(cancel[x].OptionOrderID, 10)] = cancel[x].Status + } + } + default: + return response, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) + } + return response, nil } // CancelAllOrders cancels all orders associated with a currency pair -func (g *Gateio) CancelAllOrders(ctx context.Context, _ *order.Cancel) (order.CancelAllResponse, error) { - cancelAllOrdersResponse := order.CancelAllResponse{ - Status: make(map[string]string), - } - openOrders, err := g.GetOpenOrders(ctx, "") +func (g *Gateio) CancelAllOrders(ctx context.Context, o *order.Cancel) (order.CancelAllResponse, error) { + err := o.Validate() if err != nil { - return cancelAllOrdersResponse, err + return order.CancelAllResponse{}, err } - - uniqueSymbols := make(map[string]int) - for i := range openOrders.Orders { - uniqueSymbols[openOrders.Orders[i].CurrencyPair]++ - } - - for unique := range uniqueSymbols { - err = g.CancelAllExistingOrders(ctx, -1, unique) - if err != nil { - cancelAllOrdersResponse.Status[unique] = err.Error() + var cancelAllOrdersResponse order.CancelAllResponse + cancelAllOrdersResponse.Status = map[string]string{} + switch o.AssetType { + case asset.Spot, asset.Margin, asset.CrossMargin: + if o.Pair.IsEmpty() { + return order.CancelAllResponse{}, currency.ErrCurrencyPairEmpty } + var cancel []SpotPriceTriggeredOrder + cancel, err = g.CancelMultipleSpotOpenOrders(ctx, o.Pair, o.AssetType) + if err != nil { + return cancelAllOrdersResponse, err + } + for x := range cancel { + cancelAllOrdersResponse.Status[strconv.FormatInt(cancel[x].AutoOrderID, 10)] = cancel[x].Status + } + case asset.Futures: + if o.Pair.IsEmpty() { + return cancelAllOrdersResponse, currency.ErrCurrencyPairEmpty + } + var settle string + settle, err = g.getSettlementFromCurrency(o.Pair, true) + if err != nil { + return cancelAllOrdersResponse, err + } + var cancel []Order + cancel, err = g.CancelMultipleFuturesOpenOrders(ctx, o.Pair, o.Side.Lower(), settle) + if err != nil { + return cancelAllOrdersResponse, err + } + for f := range cancel { + cancelAllOrdersResponse.Status[strconv.FormatInt(cancel[f].ID, 10)] = cancel[f].Status + } + case asset.DeliveryFutures: + if o.Pair.IsEmpty() { + return cancelAllOrdersResponse, currency.ErrCurrencyPairEmpty + } + var settle string + settle, err = g.getSettlementFromCurrency(o.Pair, false) + if err != nil { + return cancelAllOrdersResponse, err + } + var cancel []Order + cancel, err = g.CancelMultipleDeliveryOrders(ctx, o.Pair, o.Side.Lower(), settle) + if err != nil { + return cancelAllOrdersResponse, err + } + for f := range cancel { + cancelAllOrdersResponse.Status[strconv.FormatInt(cancel[f].ID, 10)] = cancel[f].Status + } + case asset.Options: + var underlying currency.Pair + if !o.Pair.IsEmpty() { + underlying, err = g.GetUnderlyingFromCurrencyPair(o.Pair) + if err != nil { + return cancelAllOrdersResponse, err + } + } + cancel, err := g.CancelMultipleOptionOpenOrders(ctx, o.Pair, underlying.String(), o.Side.Lower()) + if err != nil { + return cancelAllOrdersResponse, err + } + for x := range cancel { + cancelAllOrdersResponse.Status[strconv.FormatInt(cancel[x].OptionOrderID, 10)] = cancel[x].Status + } + default: + return cancelAllOrdersResponse, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, o.AssetType) } return cancelAllOrdersResponse, nil } // GetOrderInfo returns order information based on order ID -func (g *Gateio) GetOrderInfo(ctx context.Context, orderID string, _ currency.Pair, assetType asset.Item) (order.Detail, error) { +func (g *Gateio) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, a asset.Item) (order.Detail, error) { var orderDetail order.Detail - orders, err := g.GetOpenOrders(ctx, "") - if err != nil { - return orderDetail, errors.New("failed to get open orders") - } - - if assetType == asset.Empty { - assetType = asset.Spot - } - - format, err := g.GetPairFormat(assetType, false) + pair, err := g.FormatExchangeCurrency(pair, a) if err != nil { return orderDetail, err } - - for x := range orders.Orders { - if orders.Orders[x].OrderNumber != orderID { - continue - } - orderDetail.Exchange = g.Name - orderDetail.OrderID = orders.Orders[x].OrderNumber - orderDetail.RemainingAmount = orders.Orders[x].InitialAmount - orders.Orders[x].FilledAmount - orderDetail.ExecutedAmount = orders.Orders[x].FilledAmount - orderDetail.Amount = orders.Orders[x].InitialAmount - orderDetail.Date = time.Unix(orders.Orders[x].Timestamp, 0) - if orderDetail.Status, err = order.StringToOrderStatus(orders.Orders[x].Status); err != nil { - log.Errorf(log.ExchangeSys, "%s %v", g.Name, err) - } - orderDetail.Price = orders.Orders[x].Rate - orderDetail.Pair, err = currency.NewPairDelimiter(orders.Orders[x].CurrencyPair, - format.Delimiter) + switch a { + case asset.Spot, asset.Margin, asset.CrossMargin: + var spotOrder *SpotOrder + spotOrder, err = g.GetSpotOrder(ctx, orderID, pair, a) if err != nil { return orderDetail, err } - if strings.EqualFold(orders.Orders[x].Type, order.Ask.String()) { - orderDetail.Side = order.Ask - } else if strings.EqualFold(orders.Orders[x].Type, order.Bid.String()) { - orderDetail.Side = order.Buy + var side order.Side + side, err = order.StringToOrderSide(spotOrder.Side) + if err != nil { + return orderDetail, err } - return orderDetail, nil + var orderType order.Type + orderType, err = order.StringToOrderType(spotOrder.Type) + if err != nil { + return orderDetail, err + } + var orderStatus order.Status + orderStatus, err = order.StringToOrderStatus(spotOrder.Status) + if err != nil { + return orderDetail, err + } + return order.Detail{ + Amount: spotOrder.Amount, + Exchange: g.Name, + OrderID: spotOrder.OrderID, + Side: side, + Type: orderType, + Pair: pair, + Cost: spotOrder.FeeDeducted, + AssetType: a, + Status: orderStatus, + Price: spotOrder.Price, + ExecutedAmount: spotOrder.Amount - spotOrder.Left.Float64(), + Date: spotOrder.CreateTimeMs.Time(), + LastUpdated: spotOrder.UpdateTimeMs.Time(), + }, nil + case asset.Futures, asset.DeliveryFutures: + var settle string + if a == asset.Futures { + settle, err = g.getSettlementFromCurrency(pair, true) + } else { + settle, err = g.getSettlementFromCurrency(pair, false) + } + if err != nil { + return orderDetail, err + } + var fOrder *Order + var err error + if asset.Futures == a { + fOrder, err = g.GetSingleFuturesOrder(ctx, settle, orderID) + } else { + fOrder, err = g.GetSingleDeliveryOrder(ctx, settle, orderID) + } + if err != nil { + return orderDetail, err + } + orderStatus, err := order.StringToOrderStatus(fOrder.Status) + if err != nil { + return orderDetail, err + } + pair, err = currency.NewPairFromString(fOrder.Contract) + if err != nil { + return orderDetail, err + } + return order.Detail{ + Amount: fOrder.Size, + ExecutedAmount: fOrder.Size - fOrder.RemainingAmount, + Exchange: g.Name, + OrderID: orderID, + Status: orderStatus, + Price: fOrder.OrderPrice, + Date: fOrder.CreateTime.Time(), + LastUpdated: fOrder.FinishTime.Time(), + Pair: pair, + AssetType: a, + }, nil + case asset.Options: + optionOrder, err := g.GetSingleOptionOrder(ctx, orderID) + if err != nil { + return orderDetail, err + } + orderStatus, err := order.StringToOrderStatus(optionOrder.Status) + if err != nil { + return orderDetail, err + } + pair, err = currency.NewPairFromString(optionOrder.Contract) + if err != nil { + return orderDetail, err + } + return order.Detail{ + Amount: optionOrder.Size, + ExecutedAmount: optionOrder.Size - optionOrder.Left, + Exchange: g.Name, + OrderID: orderID, + Status: orderStatus, + Price: optionOrder.Price, + Date: optionOrder.CreateTime.Time(), + LastUpdated: optionOrder.FinishTime.Time(), + Pair: pair, + AssetType: a, + }, nil + default: + return orderDetail, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) } - return orderDetail, fmt.Errorf("no order found with id %v", orderID) } // GetDepositAddress returns a deposit address for a specified currency func (g *Gateio) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, chain string) (*deposit.Address, error) { - addr, err := g.GetCryptoDepositAddress(ctx, cryptocurrency.String()) + addr, err := g.GenerateCurrencyDepositAddress(ctx, cryptocurrency) if err != nil { return nil, err } - - if addr.Address == gateioGenerateAddress { - return nil, - errors.New("new deposit address is being generated, please retry again shortly") - } - if chain != "" { for x := range addr.MultichainAddresses { - if strings.EqualFold(addr.MultichainAddresses[x].Chain, chain) { + if addr.MultichainAddresses[x].ObtainFailed == 1 { + continue + } + if addr.MultichainAddresses[x].Chain == chain { return &deposit.Address{ + Chain: addr.MultichainAddresses[x].Chain, Address: addr.MultichainAddresses[x].Address, Tag: addr.MultichainAddresses[x].PaymentName, }, nil @@ -678,7 +1524,7 @@ func (g *Gateio) GetDepositAddress(ctx context.Context, cryptocurrency currency. } return &deposit.Address{ Address: addr.Address, - Tag: addr.Tag, + Chain: chain, }, nil } @@ -688,17 +1534,24 @@ func (g *Gateio) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawReques if err := withdrawRequest.Validate(); err != nil { return nil, err } - return g.WithdrawCrypto(ctx, - withdrawRequest.Currency.String(), - withdrawRequest.Crypto.Address, - withdrawRequest.Crypto.AddressTag, - withdrawRequest.Crypto.Chain, - withdrawRequest.Amount, - ) + response, err := g.WithdrawCurrency(ctx, + WithdrawalRequestParam{ + Amount: withdrawRequest.Amount, + Currency: withdrawRequest.Currency, + Address: withdrawRequest.Crypto.Address, + Chain: withdrawRequest.Crypto.Chain, + }) + if err != nil { + return nil, err + } + return &withdraw.ExchangeResponse{ + Name: response.Chain, + ID: response.TransactionID, + Status: response.Status, + }, nil } -// WithdrawFiatFunds returns a withdrawal ID when a -// withdrawal is submitted +// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted func (g *Gateio) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } @@ -723,109 +1576,148 @@ func (g *Gateio) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuild // GetActiveOrders retrieves any orders that are active/open func (g *Gateio) GetActiveOrders(ctx context.Context, req *order.GetOrdersRequest) (order.FilteredOrders, error) { - err := req.Validate() + if err := req.Validate(); err != nil { + return nil, err + } + var orders []order.Detail + format, err := g.GetPairFormat(req.AssetType, false) if err != nil { return nil, err } - - var orders []order.Detail - var currPair string - if len(req.Pairs) == 1 { - var fPair currency.Pair - fPair, err = g.FormatExchangeCurrency(req.Pairs[0], asset.Spot) + switch req.AssetType { + case asset.Spot, asset.Margin, asset.CrossMargin: + var spotOrders []SpotOrdersDetail + spotOrders, err = g.GateioSpotOpenOrders(ctx, 0, 0, req.AssetType == asset.CrossMargin) if err != nil { return nil, err } - currPair = fPair.String() - } - if g.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - for i := 0; ; i += 100 { - var resp *WebSocketOrderQueryResult - resp, err = g.wsGetOrderInfo(req.Type.String(), i, 100) - if err != nil { - return orders, err - } - - for j := range resp.WebSocketOrderQueryRecords { - orderSide := order.Buy - if resp.WebSocketOrderQueryRecords[j].Type == 1 { - orderSide = order.Sell - } - orderType := order.Market - if resp.WebSocketOrderQueryRecords[j].OrderType == 1 { - orderType = order.Limit - } - var p currency.Pair - p, err = currency.NewPairFromString(resp.WebSocketOrderQueryRecords[j].Market) - if err != nil { - return nil, err - } - orders = append(orders, order.Detail{ - Exchange: g.Name, - AccountID: strconv.FormatInt(resp.WebSocketOrderQueryRecords[j].User, 10), - OrderID: strconv.FormatInt(resp.WebSocketOrderQueryRecords[j].ID, 10), - Pair: p, - Side: orderSide, - Type: orderType, - Date: convert.TimeFromUnixTimestampDecimal(resp.WebSocketOrderQueryRecords[j].Ctime), - Price: resp.WebSocketOrderQueryRecords[j].Price, - Amount: resp.WebSocketOrderQueryRecords[j].Amount, - ExecutedAmount: resp.WebSocketOrderQueryRecords[j].FilledAmount, - RemainingAmount: resp.WebSocketOrderQueryRecords[j].Left, - Fee: resp.WebSocketOrderQueryRecords[j].DealFee, - }) - } - if len(resp.WebSocketOrderQueryRecords) < 100 { - break - } - } - } else { - var resp OpenOrdersResponse - resp, err = g.GetOpenOrders(ctx, currPair) - if err != nil { - return nil, err - } - - var format currency.PairFormat - format, err = g.GetPairFormat(asset.Spot, false) - if err != nil { - return nil, err - } - - for i := range resp.Orders { - if resp.Orders[i].Status != "open" { - continue - } + for x := range spotOrders { var symbol currency.Pair - symbol, err = currency.NewPairDelimiter(resp.Orders[i].CurrencyPair, - format.Delimiter) + symbol, err = currency.NewPairDelimiter(spotOrders[x].CurrencyPair, format.Delimiter) if err != nil { return nil, err } - var side order.Side - side, err = order.StringToOrderSide(resp.Orders[i].Type) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", g.Name, err) + for y := range spotOrders[x].Orders { + if spotOrders[x].Orders[y].Status != "open" { + continue + } + var side order.Side + side, err = order.StringToOrderSide(spotOrders[x].Orders[x].Side) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", g.Name, err) + } + var oType order.Type + oType, err = order.StringToOrderType(spotOrders[x].Orders[y].Type) + if err != nil { + return nil, err + } + var status order.Status + status, err = order.StringToOrderStatus(spotOrders[x].Orders[y].Status) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", g.Name, err) + } + orders = append(orders, order.Detail{ + Side: side, + Type: oType, + Status: status, + Pair: symbol, + OrderID: spotOrders[x].Orders[y].OrderID, + Amount: spotOrders[x].Orders[y].Amount, + ExecutedAmount: spotOrders[x].Orders[y].Amount - spotOrders[x].Orders[y].Left.Float64(), + RemainingAmount: spotOrders[x].Orders[y].Left.Float64(), + Price: spotOrders[x].Orders[y].Price, + AverageExecutedPrice: spotOrders[x].Orders[y].AverageFillPrice, + Date: spotOrders[x].Orders[y].CreateTimeMs.Time(), + LastUpdated: spotOrders[x].Orders[y].UpdateTimeMs.Time(), + Exchange: g.Name, + AssetType: req.AssetType, + ClientOrderID: spotOrders[x].Orders[y].Text, + FeeAsset: currency.NewCode(spotOrders[x].Orders[y].FeeCurrency), + }) } + } + case asset.Futures, asset.DeliveryFutures: + if len(req.Pairs) == 0 { + return nil, currency.ErrCurrencyPairsEmpty + } + for z := range req.Pairs { + var settle string + if req.AssetType == asset.Futures { + settle, err = g.getSettlementFromCurrency(req.Pairs[z], true) + } else { + settle, err = g.getSettlementFromCurrency(req.Pairs[z], false) + } + if err != nil { + return nil, err + } + var futuresOrders []Order + if req.AssetType == asset.Futures { + futuresOrders, err = g.GetFuturesOrders(ctx, req.Pairs[z], "open", "", settle, 0, 0, 0) + } else { + futuresOrders, err = g.GetDeliveryOrders(ctx, req.Pairs[z], "open", settle, "", 0, 0, 0) + } + if err != nil { + return nil, err + } + for x := range futuresOrders { + if futuresOrders[x].Status != "open" { + continue + } + var status order.Status + status, err = order.StringToOrderStatus(futuresOrders[x].Status) + if err != nil { + log.Errorf(log.ExchangeSys, "%s %v", g.Name, err) + } + orders = append(orders, order.Detail{ + Status: status, + Amount: futuresOrders[x].Size, + Pair: req.Pairs[x], + OrderID: strconv.FormatInt(futuresOrders[x].ID, 10), + Price: futuresOrders[x].OrderPrice, + ExecutedAmount: futuresOrders[x].Size - futuresOrders[x].RemainingAmount, + RemainingAmount: futuresOrders[x].RemainingAmount, + LastUpdated: futuresOrders[x].FinishTime.Time(), + Date: futuresOrders[x].CreateTime.Time(), + ClientOrderID: futuresOrders[x].Text, + Exchange: g.Name, + AssetType: req.AssetType, + }) + } + } + case asset.Options: + var optionsOrders []OptionOrderResponse + optionsOrders, err = g.GetOptionFuturesOrders(ctx, currency.EMPTYPAIR, "", "open", 0, 0, req.StartTime, req.EndTime) + if err != nil { + return nil, err + } + for x := range optionsOrders { + var currencyPair currency.Pair var status order.Status - status, err = order.StringToOrderStatus(resp.Orders[i].Status) + currencyPair, err = currency.NewPairFromString(optionsOrders[x].Contract) if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", g.Name, err) + return nil, err + } + status, err = order.StringToOrderStatus(optionsOrders[x].Status) + if err != nil { + return nil, err } - orderDate := time.Unix(resp.Orders[i].Timestamp, 0) orders = append(orders, order.Detail{ - OrderID: resp.Orders[i].OrderNumber, - Amount: resp.Orders[i].Amount, - ExecutedAmount: resp.Orders[i].Amount - resp.Orders[i].FilledAmount, - RemainingAmount: resp.Orders[i].FilledAmount, - Price: resp.Orders[i].Rate, - Date: orderDate, - Side: side, - Exchange: g.Name, - Pair: symbol, Status: status, + Amount: optionsOrders[x].Size, + Pair: currencyPair, + OrderID: strconv.FormatInt(optionsOrders[x].OptionOrderID, 10), + Price: optionsOrders[x].Price, + ExecutedAmount: optionsOrders[x].Size - optionsOrders[x].Left, + RemainingAmount: optionsOrders[x].Left, + LastUpdated: optionsOrders[x].FinishTime.Time(), + Date: optionsOrders[x].CreateTime.Time(), + Exchange: g.Name, + AssetType: req.AssetType, + ClientOrderID: optionsOrders[x].Text, }) } + default: + return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, req.AssetType) } return req.Filter(g.Name, orders), nil } @@ -837,55 +1729,257 @@ func (g *Gateio) GetOrderHistory(ctx context.Context, req *order.GetOrdersReques if err != nil { return nil, err } - - var trades []TradesResponse - for i := range req.Pairs { - var resp TradeHistoryResponse - resp, err = g.GetTradeHistory(ctx, req.Pairs[i].String()) - if err != nil { - return nil, err - } - trades = append(trades, resp.Trades...) - } - - format, err := g.GetPairFormat(asset.Spot, false) + var orders []order.Detail + format, err := g.GetPairFormat(req.AssetType, true) if err != nil { return nil, err } - - orders := make([]order.Detail, len(trades)) - for i := range trades { - var pair currency.Pair - pair, err = currency.NewPairDelimiter(trades[i].Pair, format.Delimiter) - if err != nil { - return nil, err + switch req.AssetType { + case asset.Spot, asset.Margin, asset.CrossMargin: + for x := range req.Pairs { + fPair := req.Pairs[x].Format(format) + var spotOrders []SpotPersonalTradeHistory + spotOrders, err = g.GateIOGetPersonalTradingHistory(ctx, fPair, req.OrderID, 0, 0, req.AssetType == asset.CrossMargin, req.StartTime, req.EndTime) + if err != nil { + return nil, err + } + for o := range spotOrders { + var side order.Side + side, err = order.StringToOrderSide(spotOrders[o].Side) + if err != nil { + return nil, err + } + detail := order.Detail{ + OrderID: spotOrders[o].OrderID, + Amount: spotOrders[o].Amount, + ExecutedAmount: spotOrders[o].Amount, + Price: spotOrders[o].Price, + Date: spotOrders[o].CreateTime.Time(), + Side: side, + Exchange: g.Name, + Pair: fPair, + AssetType: req.AssetType, + Fee: spotOrders[o].Fee, + FeeAsset: currency.NewCode(spotOrders[o].FeeCurrency), + } + detail.InferCostsAndTimes() + orders = append(orders, detail) + } } - var side order.Side - side, err = order.StringToOrderSide(trades[i].Type) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", g.Name, err) + case asset.Futures, asset.DeliveryFutures: + for x := range req.Pairs { + fPair := req.Pairs[x].Format(format) + var settle string + if req.AssetType == asset.Futures { + settle, err = g.getSettlementFromCurrency(fPair, true) + } else { + settle, err = g.getSettlementFromCurrency(fPair, false) + } + if err != nil { + return nil, err + } + if req.AssetType == asset.Futures && settle == settleUSD { + settle = settleBTC + } + var futuresOrder []TradingHistoryItem + if req.AssetType == asset.Futures { + futuresOrder, err = g.GetMyPersonalTradingHistory(ctx, settle, "", req.OrderID, fPair, 0, 0, 0) + } else { + futuresOrder, err = g.GetDeliveryPersonalTradingHistory(ctx, settle, req.OrderID, fPair, 0, 0, 0, "") + } + if err != nil { + return nil, err + } + for o := range futuresOrder { + detail := order.Detail{ + OrderID: strconv.FormatInt(futuresOrder[o].ID, 10), + Amount: futuresOrder[o].Size, + Price: futuresOrder[o].Price, + Date: futuresOrder[o].CreateTime.Time(), + Exchange: g.Name, + Pair: fPair, + AssetType: req.AssetType, + } + detail.InferCostsAndTimes() + orders = append(orders, detail) + } } - orderDate := time.Unix(trades[i].TimeUnix, 0) - detail := order.Detail{ - OrderID: strconv.FormatInt(trades[i].OrderID, 10), - Amount: trades[i].Amount, - ExecutedAmount: trades[i].Amount, - Price: trades[i].Rate, - AverageExecutedPrice: trades[i].Rate, - Date: orderDate, - Side: side, - Exchange: g.Name, - Pair: pair, + case asset.Options: + for x := range req.Pairs { + fPair := req.Pairs[x].Format(format) + var optionOrders []OptionTradingHistory + optionOrders, err = g.GetOptionsPersonalTradingHistory(ctx, fPair.String(), fPair.Upper(), 0, 0, req.StartTime, req.EndTime) + if err != nil { + return nil, err + } + for o := range optionOrders { + detail := order.Detail{ + OrderID: strconv.FormatInt(optionOrders[o].OrderID, 10), + Amount: optionOrders[o].Size, + Price: optionOrders[o].Price, + Date: optionOrders[o].CreateTime.Time(), + Exchange: g.Name, + Pair: fPair, + AssetType: req.AssetType, + } + detail.InferCostsAndTimes() + orders = append(orders, detail) + } } - detail.InferCostsAndTimes() - orders[i] = detail + default: + return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, req.AssetType) } return req.Filter(g.Name, orders), nil } -// AuthenticateWebsocket sends an authentication message to the websocket -func (g *Gateio) AuthenticateWebsocket(ctx context.Context) error { - return g.wsServerSignIn(ctx) +// GetHistoricCandles returns candles between a time period for a set time interval +func (g *Gateio) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { + req, err := g.GetKlineRequest(pair, a, interval, start, end, false) + if err != nil { + return nil, err + } + var listCandlesticks []kline.Candle + switch a { + case asset.Spot, asset.Margin, asset.CrossMargin: + var candles []Candlestick + candles, err = g.GetCandlesticks(ctx, req.RequestFormatted, 0, start, end, interval) + if err != nil { + return nil, err + } + listCandlesticks = make([]kline.Candle, len(candles)) + for x := range candles { + listCandlesticks[x] = kline.Candle{ + Time: candles[x].Timestamp, + Open: candles[x].OpenPrice, + High: candles[x].HighestPrice, + Low: candles[x].LowestPrice, + Close: candles[x].ClosePrice, + Volume: candles[x].QuoteCcyVolume, + } + } + case asset.Futures, asset.DeliveryFutures: + var settlement string + if req.Asset == asset.Futures { + settlement, err = g.getSettlementFromCurrency(req.RequestFormatted, true) + } else { + settlement, err = g.getSettlementFromCurrency(req.RequestFormatted, false) + } + if err != nil { + return nil, err + } + if req.Asset == asset.Futures && settlement == settleUSD { + settlement = settleBTC + } + var candles []FuturesCandlestick + if a == asset.Futures { + candles, err = g.GetFuturesCandlesticks(ctx, settlement, req.RequestFormatted.String(), start, end, 0, interval) + } else { + candles, err = g.GetDeliveryFuturesCandlesticks(ctx, settlement, req.RequestFormatted.Upper(), start, end, 0, interval) + } + if err != nil { + return nil, err + } + listCandlesticks = make([]kline.Candle, len(candles)) + for x := range candles { + listCandlesticks[x] = kline.Candle{ + Time: candles[x].Timestamp.Time(), + Open: candles[x].OpenPrice, + High: candles[x].HighestPrice, + Low: candles[x].LowestPrice, + Close: candles[x].ClosePrice, + Volume: candles[x].Volume, + } + } + case asset.Options: + // TODO: add support for options when endpoint is returning data + return nil, common.ErrNotYetImplemented + default: + return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) + } + return req.ProcessResponse(listCandlesticks) +} + +// GetHistoricCandlesExtended returns candles between a time period for a set time interval +func (g *Gateio) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { + req, err := g.GetKlineExtendedRequest(pair, a, interval, start, end) + if err != nil { + return nil, err + } + candlestickItems := make([]kline.Candle, 0, req.Size()) + for b := range req.RangeHolder.Ranges { + switch a { + case asset.Spot, asset.Margin, asset.CrossMargin: + var candles []Candlestick + candles, err = g.GetCandlesticks(ctx, req.RequestFormatted, 0, req.RangeHolder.Ranges[b].Start.Time, req.RangeHolder.Ranges[b].End.Time, interval) + if err != nil { + return nil, err + } + for x := range candles { + candlestickItems = append(candlestickItems, kline.Candle{ + Time: candles[x].Timestamp, + Open: candles[x].OpenPrice, + High: candles[x].HighestPrice, + Low: candles[x].LowestPrice, + Close: candles[x].ClosePrice, + Volume: candles[x].QuoteCcyVolume, + }) + } + case asset.Futures, asset.DeliveryFutures: + var settle string + if req.Asset == asset.Futures { + settle, err = g.getSettlementFromCurrency(req.RequestFormatted, true) + } else { + settle, err = g.getSettlementFromCurrency(req.RequestFormatted, false) + } + if err != nil { + return nil, err + } + if req.Asset == asset.Futures && settle == settleUSD { + settle = settleBTC + } + var candles []FuturesCandlestick + if a == asset.Futures { + candles, err = g.GetFuturesCandlesticks(ctx, settle, req.RequestFormatted.String(), req.RangeHolder.Ranges[b].Start.Time, req.RangeHolder.Ranges[b].End.Time, 0, interval) + } else { + candles, err = g.GetDeliveryFuturesCandlesticks(ctx, settle, req.RequestFormatted.Upper(), req.RangeHolder.Ranges[b].Start.Time, req.RangeHolder.Ranges[b].End.Time, 0, interval) + } + if err != nil { + return nil, err + } + for x := range candles { + candlestickItems = append(candlestickItems, kline.Candle{ + Time: candles[x].Timestamp.Time(), + Open: candles[x].OpenPrice, + High: candles[x].HighestPrice, + Low: candles[x].LowestPrice, + Close: candles[x].ClosePrice, + Volume: candles[x].Volume, + }) + } + case asset.Options: + // TODO: add support for options when endpoint is returning data + return nil, common.ErrNotYetImplemented + default: + return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) + } + } + return req.ProcessResponse(candlestickItems) +} + +// GetAvailableTransferChains returns the available transfer blockchains for the specific +// cryptocurrency +func (g *Gateio) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) { + chains, err := g.ListCurrencyChain(ctx, cryptocurrency.Upper()) + if err != nil { + return nil, err + } + availableChains := make([]string, 0, len(chains)) + for x := range chains { + if chains[x].IsDisabled == 0 { + availableChains = append(availableChains, chains[x].Chain) + } + } + return availableChains, nil } // ValidateAPICredentials validates current credentials used for wrapper @@ -895,45 +1989,12 @@ func (g *Gateio) ValidateAPICredentials(ctx context.Context, assetType asset.Ite return g.CheckTransientError(err) } -// FormatExchangeKlineInterval returns Interval to exchange formatted string -func (g *Gateio) FormatExchangeKlineInterval(in kline.Interval) string { - return strconv.FormatFloat(in.Duration().Seconds(), 'f', 0, 64) -} - -// GetHistoricCandles returns candles between a time period for a set time interval -func (g *Gateio) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := g.GetKlineRequest(pair, a, interval, start, end, true) +// checkInstrumentAvailabilityInSpot checks whether the instrument is available in the spot exchange +// if so we can use the instrument to retrieve orderbook and ticker information using the spot endpoints. +func (g *Gateio) checkInstrumentAvailabilityInSpot(instrument currency.Pair) (bool, error) { + availables, err := g.CurrencyPairs.GetPairs(asset.Spot, false) if err != nil { - return nil, err + return false, err } - - timeSeries, err := g.GetSpotKline(ctx, KlinesRequestParams{ - Symbol: req.RequestFormatted.String(), - GroupSec: g.FormatExchangeKlineInterval(req.ExchangeInterval), - HourSize: int(time.Since(req.Start).Hours()), - }) - if err != nil { - return nil, err - } - return req.ProcessResponse(timeSeries) -} - -// GetHistoricCandlesExtended returns candles between a time period for a set time interval -func (g *Gateio) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair, _ asset.Item, _ kline.Interval, _, _ time.Time) (*kline.Item, error) { - return nil, common.ErrNotYetImplemented -} - -// GetAvailableTransferChains returns the available transfer blockchains for the specific -// cryptocurrency -func (g *Gateio) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) { - chains, err := g.GetCryptoDepositAddress(ctx, cryptocurrency.String()) - if err != nil { - return nil, err - } - - availableChains := make([]string, len(chains.MultichainAddresses)) - for x := range chains.MultichainAddresses { - availableChains[x] = chains.MultichainAddresses[x].Chain - } - return availableChains, nil + return availables.Contains(instrument, true), nil } diff --git a/exchanges/gateio/gateio_ws_delivery_futures.go b/exchanges/gateio/gateio_ws_delivery_futures.go new file mode 100644 index 00000000..e596b3cc --- /dev/null +++ b/exchanges/gateio/gateio_ws_delivery_futures.go @@ -0,0 +1,333 @@ +package gateio + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common" + "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/stream" + "github.com/thrasher-corp/gocryptotrader/log" +) + +const ( + // delivery real trading urls + deliveryRealUSDTTradingURL = "wss://fx-ws.gateio.ws/v4/ws/delivery/usdt" + deliveryRealBTCTradingURL = "wss://fx-ws.gateio.ws/v4/ws/delivery/btc" + + // delivery testnet urls + deliveryTestNetBTCTradingURL = "wss://fx-ws-testnet.gateio.ws/v4/ws/delivery/btc" + deliveryTestNetUSDTTradingURL = "wss://fx-ws-testnet.gateio.ws/v4/ws/delivery/usdt" +) + +var defaultDeliveryFuturesSubscriptions = []string{ + futuresTickersChannel, + futuresTradesChannel, + futuresOrderbookChannel, + futuresCandlesticksChannel, +} + +// responseDeliveryFuturesStream a channel thought which the data coming from the two websocket connection will go through. +var responseDeliveryFuturesStream = make(chan stream.Response) + +var fetchedFuturesCurrencyPairSnapshotOrderbook = make(map[string]bool) + +// WsDeliveryFuturesConnect initiates a websocket connection for delivery futures account +func (g *Gateio) WsDeliveryFuturesConnect() error { + if !g.Websocket.IsEnabled() || !g.IsEnabled() { + return errors.New(stream.WebsocketNotEnabled) + } + err := g.CurrencyPairs.IsAssetEnabled(asset.DeliveryFutures) + if err != nil { + return err + } + var dialer websocket.Dialer + err = g.Websocket.SetWebsocketURL(deliveryRealUSDTTradingURL, false, true) + if err != nil { + return err + } + err = g.Websocket.Conn.Dial(&dialer, http.Header{}) + if err != nil { + return err + } + err = g.Websocket.SetupNewConnection(stream.ConnectionSetup{ + URL: deliveryRealBTCTradingURL, + RateLimit: gateioWebsocketRateLimit, + ResponseCheckTimeout: g.Config.WebsocketResponseCheckTimeout, + ResponseMaxLimit: g.Config.WebsocketResponseMaxLimit, + Authenticated: true, + }) + if err != nil { + return err + } + err = g.Websocket.AuthConn.Dial(&dialer, http.Header{}) + if err != nil { + return err + } + g.Websocket.Wg.Add(3) + go g.wsReadDeliveryFuturesData() + go g.wsFunnelDeliveryFuturesConnectionData(g.Websocket.Conn) + go g.wsFunnelDeliveryFuturesConnectionData(g.Websocket.AuthConn) + if g.Verbose { + log.Debugf(log.ExchangeSys, "successful connection to %v\n", + g.Websocket.GetWebsocketURL()) + } + pingMessage, err := json.Marshal(WsInput{ + ID: g.Websocket.Conn.GenerateMessageID(false), + Time: time.Now().Unix(), + Channel: futuresPingChannel, + }) + if err != nil { + return err + } + g.Websocket.Conn.SetupPingHandler(stream.PingHandler{ + Websocket: true, + Delay: time.Second * 5, + MessageType: websocket.PingMessage, + Message: pingMessage, + }) + return nil +} + +// wsReadDeliveryFuturesData read coming messages thought the websocket connection and pass the data to wsHandleFuturesData for further process. +func (g *Gateio) wsReadDeliveryFuturesData() { + defer g.Websocket.Wg.Done() + for { + select { + case <-g.Websocket.ShutdownC: + select { + case resp := <-responseDeliveryFuturesStream: + err := g.wsHandleFuturesData(resp.Raw, asset.DeliveryFutures) + if err != nil { + select { + case g.Websocket.DataHandler <- err: + default: + log.Errorf(log.WebsocketMgr, "%s websocket handle data error: %v", g.Name, err) + } + } + default: + } + return + case resp := <-responseDeliveryFuturesStream: + err := g.wsHandleFuturesData(resp.Raw, asset.DeliveryFutures) + if err != nil { + g.Websocket.DataHandler <- err + } + } + } +} + +// wsFunnelDeliveryFuturesConnectionData receives data from multiple connection and pass the data +// to wsRead through a channel responseStream +func (g *Gateio) wsFunnelDeliveryFuturesConnectionData(ws stream.Connection) { + defer g.Websocket.Wg.Done() + for { + resp := ws.ReadMessage() + if resp.Raw == nil { + return + } + responseDeliveryFuturesStream <- stream.Response{Raw: resp.Raw} + } +} + +// GenerateDeliveryFuturesDefaultSubscriptions returns delivery futures default subscriptions params. +func (g *Gateio) GenerateDeliveryFuturesDefaultSubscriptions() ([]stream.ChannelSubscription, error) { + _, err := g.GetCredentials(context.Background()) + if err != nil { + g.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + channelsToSubscribe := defaultDeliveryFuturesSubscriptions + if g.Websocket.CanUseAuthenticatedEndpoints() { + channelsToSubscribe = append( + channelsToSubscribe, + futuresOrdersChannel, + futuresUserTradesChannel, + futuresBalancesChannel, + ) + } + pairs, err := g.GetAvailablePairs(asset.DeliveryFutures) + if err != nil { + return nil, err + } + var subscriptions []stream.ChannelSubscription + for i := range channelsToSubscribe { + for j := range pairs { + params := make(map[string]interface{}) + switch channelsToSubscribe[i] { + case futuresOrderbookChannel: + params["limit"] = 20 + params["interval"] = "0" + case futuresCandlesticksChannel: + params["interval"] = kline.FiveMin + } + fpair, err := g.FormatExchangeCurrency(pairs[j], asset.DeliveryFutures) + if err != nil { + return nil, err + } + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channelsToSubscribe[i], + Currency: fpair.Upper(), + Params: params, + }) + } + } + return subscriptions, nil +} + +// DeliveryFuturesSubscribe sends a websocket message to stop receiving data from the channel +func (g *Gateio) DeliveryFuturesSubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { + return g.handleDeliveryFuturesSubscription("subscribe", channelsToUnsubscribe) +} + +// DeliveryFuturesUnsubscribe sends a websocket message to stop receiving data from the channel +func (g *Gateio) DeliveryFuturesUnsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { + return g.handleDeliveryFuturesSubscription("unsubscribe", channelsToUnsubscribe) +} + +// handleDeliveryFuturesSubscription sends a websocket message to receive data from the channel +func (g *Gateio) handleDeliveryFuturesSubscription(event string, channelsToSubscribe []stream.ChannelSubscription) error { + payloads, err := g.generateDeliveryFuturesPayload(event, channelsToSubscribe) + if err != nil { + return err + } + var errs error + var respByte []byte + // con represents the websocket connection. 0 - for usdt settle and 1 - for btc settle connections. + for con, val := range payloads { + for k := range val { + if con == 0 { + respByte, err = g.Websocket.Conn.SendMessageReturnResponse(val[k].ID, val[k]) + } else { + respByte, err = g.Websocket.AuthConn.SendMessageReturnResponse(val[k].ID, val[k]) + } + if err != nil { + errs = common.AppendError(errs, err) + continue + } + var resp WsEventResponse + if err = json.Unmarshal(respByte, &resp); err != nil { + errs = common.AppendError(errs, err) + } else { + if resp.Error != nil && resp.Error.Code != 0 { + errs = common.AppendError(errs, fmt.Errorf("error while %s to channel %s error code: %d message: %s", val[k].Event, val[k].Channel, resp.Error.Code, resp.Error.Message)) + continue + } + g.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[k]) + } + } + } + return errs +} + +func (g *Gateio) generateDeliveryFuturesPayload(event string, channelsToSubscribe []stream.ChannelSubscription) ([2][]WsInput, error) { + if len(channelsToSubscribe) == 0 { + return [2][]WsInput{}, errors.New("cannot generate payload, no channels supplied") + } + var creds *account.Credentials + var err error + if g.Websocket.CanUseAuthenticatedEndpoints() { + creds, err = g.GetCredentials(context.TODO()) + if err != nil { + g.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + } + payloads := [2][]WsInput{} + for i := range channelsToSubscribe { + var auth *WsAuthInput + timestamp := time.Now() + var params []string + params = []string{channelsToSubscribe[i].Currency.String()} + if g.Websocket.CanUseAuthenticatedEndpoints() { + switch channelsToSubscribe[i].Channel { + case futuresOrdersChannel, futuresUserTradesChannel, + futuresLiquidatesChannel, futuresAutoDeleveragesChannel, + futuresAutoPositionCloseChannel, futuresBalancesChannel, + futuresReduceRiskLimitsChannel, futuresPositionsChannel, + futuresAutoOrdersChannel: + value, ok := channelsToSubscribe[i].Params["user"].(string) + if ok { + params = append( + []string{value}, + params...) + } + var sigTemp string + sigTemp, err = g.generateWsSignature(creds.Secret, event, channelsToSubscribe[i].Channel, timestamp) + if err != nil { + return [2][]WsInput{}, err + } + auth = &WsAuthInput{ + Method: "api_key", + Key: creds.Key, + Sign: sigTemp, + } + } + } + frequency, okay := channelsToSubscribe[i].Params["frequency"].(kline.Interval) + if okay { + var frequencyString string + frequencyString, err = g.GetIntervalString(frequency) + if err != nil { + return payloads, err + } + params = append(params, frequencyString) + } + levelString, okay := channelsToSubscribe[i].Params["level"].(string) + if okay { + params = append(params, levelString) + } + limit, okay := channelsToSubscribe[i].Params["limit"].(int) + if okay { + params = append(params, strconv.Itoa(limit)) + } + accuracy, okay := channelsToSubscribe[i].Params["accuracy"].(string) + if okay { + params = append(params, accuracy) + } + switch channelsToSubscribe[i].Channel { + case futuresCandlesticksChannel: + interval, okay := channelsToSubscribe[i].Params["interval"].(kline.Interval) + if okay { + var intervalString string + intervalString, err = g.GetIntervalString(interval) + if err != nil { + return payloads, err + } + params = append([]string{intervalString}, params...) + } + case futuresOrderbookChannel: + intervalString, okay := channelsToSubscribe[i].Params["interval"].(string) + if okay { + params = append(params, intervalString) + } + } + if strings.HasPrefix(channelsToSubscribe[i].Currency.Quote.Upper().String(), "USDT") { + payloads[0] = append(payloads[0], WsInput{ + ID: g.Websocket.Conn.GenerateMessageID(false), + Event: event, + Channel: channelsToSubscribe[i].Channel, + Payload: params, + Auth: auth, + Time: timestamp.Unix(), + }) + } else { + payloads[1] = append(payloads[1], WsInput{ + ID: g.Websocket.Conn.GenerateMessageID(false), + Event: event, + Channel: channelsToSubscribe[i].Channel, + Payload: params, + Auth: auth, + Time: timestamp.Unix(), + }) + } + } + return payloads, nil +} diff --git a/exchanges/gateio/gateio_ws_futures.go b/exchanges/gateio/gateio_ws_futures.go new file mode 100644 index 00000000..15e45f78 --- /dev/null +++ b/exchanges/gateio/gateio_ws_futures.go @@ -0,0 +1,870 @@ +package gateio + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/fill" + "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/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/trade" + "github.com/thrasher-corp/gocryptotrader/log" +) + +const ( + futuresWebsocketBtcURL = "wss://fx-ws.gateio.ws/v4/ws/btc" + futuresWebsocketUsdtURL = "wss://fx-ws.gateio.ws/v4/ws/usdt" + + futuresPingChannel = "futures.ping" + futuresTickersChannel = "futures.tickers" + futuresTradesChannel = "futures.trades" + futuresOrderbookChannel = "futures.order_book" + futuresOrderbookTickerChannel = "futures.book_ticker" + futuresOrderbookUpdateChannel = "futures.order_book_update" + futuresCandlesticksChannel = "futures.candlesticks" + futuresOrdersChannel = "futures.orders" + + // authenticated channels + futuresUserTradesChannel = "futures.usertrades" + futuresLiquidatesChannel = "futures.liquidates" + futuresAutoDeleveragesChannel = "futures.auto_deleverages" + futuresAutoPositionCloseChannel = "futures.position_closes" + futuresBalancesChannel = "futures.balances" + futuresReduceRiskLimitsChannel = "futures.reduce_risk_limits" + futuresPositionsChannel = "futures.positions" + futuresAutoOrdersChannel = "futures.autoorders" +) + +var defaultFuturesSubscriptions = []string{ + futuresTickersChannel, + futuresTradesChannel, + futuresOrderbookChannel, + futuresOrderbookUpdateChannel, + futuresCandlesticksChannel, +} + +// responseFuturesStream a channel thought which the data coming from the two websocket connection will go through. +var responseFuturesStream = make(chan stream.Response) + +// WsFuturesConnect initiates a websocket connection for futures account +func (g *Gateio) WsFuturesConnect() error { + if !g.Websocket.IsEnabled() || !g.IsEnabled() { + return errors.New(stream.WebsocketNotEnabled) + } + err := g.CurrencyPairs.IsAssetEnabled(asset.Futures) + if err != nil { + return err + } + var dialer websocket.Dialer + err = g.Websocket.SetWebsocketURL(futuresWebsocketUsdtURL, false, true) + if err != nil { + return err + } + err = g.Websocket.Conn.Dial(&dialer, http.Header{}) + if err != nil { + return err + } + + err = g.Websocket.SetupNewConnection(stream.ConnectionSetup{ + URL: futuresWebsocketBtcURL, + RateLimit: gateioWebsocketRateLimit, + ResponseCheckTimeout: g.Config.WebsocketResponseCheckTimeout, + ResponseMaxLimit: g.Config.WebsocketResponseMaxLimit, + Authenticated: true, + }) + if err != nil { + return err + } + err = g.Websocket.AuthConn.Dial(&dialer, http.Header{}) + if err != nil { + return err + } + g.Websocket.Wg.Add(3) + go g.wsReadFuturesData() + go g.wsFunnelFuturesConnectionData(g.Websocket.Conn) + go g.wsFunnelFuturesConnectionData(g.Websocket.AuthConn) + if g.Verbose { + log.Debugf(log.ExchangeSys, "Successful connection to %v\n", + g.Websocket.GetWebsocketURL()) + } + pingMessage, err := json.Marshal(WsInput{ + ID: g.Websocket.Conn.GenerateMessageID(false), + Time: func() int64 { + return time.Now().Unix() + }(), + Channel: futuresPingChannel, + }) + if err != nil { + return err + } + g.Websocket.Conn.SetupPingHandler(stream.PingHandler{ + Websocket: true, + MessageType: websocket.PingMessage, + Delay: time.Second * 15, + Message: pingMessage, + }) + return nil +} + +// GenerateFuturesDefaultSubscriptions returns default subscriptions information. +func (g *Gateio) GenerateFuturesDefaultSubscriptions() ([]stream.ChannelSubscription, error) { + channelsToSubscribe := defaultFuturesSubscriptions + if g.Websocket.CanUseAuthenticatedEndpoints() { + channelsToSubscribe = append(channelsToSubscribe, + futuresOrdersChannel, + futuresUserTradesChannel, + futuresBalancesChannel, + ) + } + pairs, err := g.GetEnabledPairs(asset.Futures) + if err != nil { + return nil, err + } + subscriptions := make([]stream.ChannelSubscription, len(channelsToSubscribe)*len(pairs)) + count := 0 + for i := range channelsToSubscribe { + for j := range pairs { + params := make(map[string]interface{}) + switch channelsToSubscribe[i] { + case futuresOrderbookChannel: + params["limit"] = 100 + params["interval"] = "0" + case futuresCandlesticksChannel: + params["interval"] = kline.FiveMin + case futuresOrderbookUpdateChannel: + params["frequency"] = kline.ThousandMilliseconds + params["level"] = "100" + } + fpair, err := g.FormatExchangeCurrency(pairs[j], asset.Futures) + if err != nil { + return nil, err + } + subscriptions[count] = stream.ChannelSubscription{ + Channel: channelsToSubscribe[i], + Currency: fpair.Upper(), + Params: params, + } + count++ + } + } + return subscriptions, nil +} + +// FuturesSubscribe sends a websocket message to stop receiving data from the channel +func (g *Gateio) FuturesSubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { + return g.handleFuturesSubscription("subscribe", channelsToUnsubscribe) +} + +// FuturesUnsubscribe sends a websocket message to stop receiving data from the channel +func (g *Gateio) FuturesUnsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { + return g.handleFuturesSubscription("unsubscribe", channelsToUnsubscribe) +} + +// wsReadFuturesData read coming messages thought the websocket connection and pass the data to wsHandleData for further process. +func (g *Gateio) wsReadFuturesData() { + defer g.Websocket.Wg.Done() + for { + select { + case <-g.Websocket.ShutdownC: + select { + case resp := <-responseFuturesStream: + err := g.wsHandleFuturesData(resp.Raw, asset.Futures) + if err != nil { + select { + case g.Websocket.DataHandler <- err: + default: + log.Errorf(log.WebsocketMgr, "%s websocket handle data error: %v", g.Name, err) + } + } + default: + } + return + case resp := <-responseFuturesStream: + err := g.wsHandleFuturesData(resp.Raw, asset.Futures) + if err != nil { + g.Websocket.DataHandler <- err + } + } + } +} + +// wsFunnelFuturesConnectionData receives data from multiple connection and pass the data +// to wsRead through a channel responseStream +func (g *Gateio) wsFunnelFuturesConnectionData(ws stream.Connection) { + defer g.Websocket.Wg.Done() + for { + resp := ws.ReadMessage() + if resp.Raw == nil { + return + } + responseFuturesStream <- stream.Response{Raw: resp.Raw} + } +} + +func (g *Gateio) wsHandleFuturesData(respRaw []byte, assetType asset.Item) error { + var result WsResponse + var eventResponse WsEventResponse + err := json.Unmarshal(respRaw, &eventResponse) + if err == nil && + (eventResponse.Result != nil || eventResponse.Error != nil) && + (eventResponse.Event == "subscribe" || eventResponse.Event == "unsubscribe") { + if !g.Websocket.Match.IncomingWithData(eventResponse.ID, respRaw) { + return fmt.Errorf("couldn't match subscription message with ID: %d", eventResponse.ID) + } + return nil + } + err = json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + switch result.Channel { + // Futures push datas. + case futuresTickersChannel: + return g.processFuturesTickers(respRaw, assetType) + case futuresTradesChannel: + return g.processFuturesTrades(respRaw, assetType) + case futuresOrderbookChannel: + return g.processFuturesOrderbookSnapshot(result.Event, respRaw, assetType) + case futuresOrderbookTickerChannel: + return g.processFuturesOrderbookTicker(respRaw) + case futuresOrderbookUpdateChannel: + return g.processFuturesAndOptionsOrderbookUpdate(respRaw, assetType) + case futuresCandlesticksChannel: + return g.processFuturesCandlesticks(respRaw, assetType) + case futuresOrdersChannel: + return g.processFuturesOrdersPushData(respRaw, assetType) + case futuresUserTradesChannel: + return g.procesFuturesUserTrades(respRaw, assetType) + case futuresLiquidatesChannel: + return g.processFuturesLiquidatesNotification(respRaw) + case futuresAutoDeleveragesChannel: + return g.processFuturesAutoDeleveragesNotification(respRaw) + case futuresAutoPositionCloseChannel: + return g.processPositionCloseData(respRaw) + case futuresBalancesChannel: + return g.processBalancePushData(respRaw, assetType) + case futuresReduceRiskLimitsChannel: + return g.processFuturesReduceRiskLimitNotification(respRaw) + case futuresPositionsChannel: + return g.processFuturesPositionsNotification(respRaw) + case futuresAutoOrdersChannel: + return g.processFuturesAutoOrderPushData(respRaw) + default: + g.Websocket.DataHandler <- stream.UnhandledMessageWarning{ + Message: g.Name + stream.UnhandledMessage + string(respRaw), + } + return errors.New(stream.UnhandledMessage) + } +} + +// handleFuturesSubscription sends a websocket message to receive data from the channel +func (g *Gateio) handleFuturesSubscription(event string, channelsToSubscribe []stream.ChannelSubscription) error { + payloads, err := g.generateFuturesPayload(event, channelsToSubscribe) + if err != nil { + return err + } + var errs error + var respByte []byte + // con represents the websocket connection. 0 - for usdt settle and 1 - for btc settle connections. + for con, val := range payloads { + for k := range val { + if con == 0 { + respByte, err = g.Websocket.Conn.SendMessageReturnResponse(val[k].ID, val[k]) + } else { + respByte, err = g.Websocket.AuthConn.SendMessageReturnResponse(val[k].ID, val[k]) + } + if err != nil { + errs = common.AppendError(errs, err) + continue + } + var resp WsEventResponse + if err = json.Unmarshal(respByte, &resp); err != nil { + errs = common.AppendError(errs, err) + } else { + if resp.Error != nil && resp.Error.Code != 0 { + errs = common.AppendError(errs, fmt.Errorf("error while %s to channel %s error code: %d message: %s", val[k].Event, val[k].Channel, resp.Error.Code, resp.Error.Message)) + continue + } + g.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[k]) + } + } + } + if errs != nil { + return errs + } + return nil +} + +func (g *Gateio) generateFuturesPayload(event string, channelsToSubscribe []stream.ChannelSubscription) ([2][]WsInput, error) { + if len(channelsToSubscribe) == 0 { + return [2][]WsInput{}, errors.New("cannot generate payload, no channels supplied") + } + var creds *account.Credentials + var err error + if g.Websocket.CanUseAuthenticatedEndpoints() { + creds, err = g.GetCredentials(context.TODO()) + if err != nil { + g.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + } + payloads := [2][]WsInput{} + for i := range channelsToSubscribe { + var auth *WsAuthInput + timestamp := time.Now() + var params []string + params = []string{channelsToSubscribe[i].Currency.String()} + if g.Websocket.CanUseAuthenticatedEndpoints() { + switch channelsToSubscribe[i].Channel { + case futuresOrdersChannel, futuresUserTradesChannel, + futuresLiquidatesChannel, futuresAutoDeleveragesChannel, + futuresAutoPositionCloseChannel, futuresBalancesChannel, + futuresReduceRiskLimitsChannel, futuresPositionsChannel, + futuresAutoOrdersChannel: + value, ok := channelsToSubscribe[i].Params["user"].(string) + if ok { + params = append( + []string{value}, + params...) + } + var sigTemp string + sigTemp, err = g.generateWsSignature(creds.Secret, event, channelsToSubscribe[i].Channel, timestamp) + if err != nil { + return [2][]WsInput{}, err + } + auth = &WsAuthInput{ + Method: "api_key", + Key: creds.Key, + Sign: sigTemp, + } + } + } + frequency, okay := channelsToSubscribe[i].Params["frequency"].(kline.Interval) + if okay { + var frequencyString string + frequencyString, err = g.GetIntervalString(frequency) + if err != nil { + return payloads, err + } + params = append(params, frequencyString) + } + levelString, okay := channelsToSubscribe[i].Params["level"].(string) + if okay { + params = append(params, levelString) + } + limit, okay := channelsToSubscribe[i].Params["limit"].(int) + if okay { + params = append(params, strconv.Itoa(limit)) + } + accuracy, okay := channelsToSubscribe[i].Params["accuracy"].(string) + if okay { + params = append(params, accuracy) + } + switch channelsToSubscribe[i].Channel { + case futuresCandlesticksChannel: + interval, okay := channelsToSubscribe[i].Params["interval"].(kline.Interval) + if okay { + var intervalString string + intervalString, err = g.GetIntervalString(interval) + if err != nil { + return payloads, err + } + params = append([]string{intervalString}, params...) + } + case futuresOrderbookChannel: + intervalString, okay := channelsToSubscribe[i].Params["interval"].(string) + if okay { + params = append(params, intervalString) + } + } + if strings.HasPrefix(channelsToSubscribe[i].Currency.Quote.Upper().String(), "USDT") { + payloads[0] = append(payloads[0], WsInput{ + ID: g.Websocket.Conn.GenerateMessageID(false), + Event: event, + Channel: channelsToSubscribe[i].Channel, + Payload: params, + Auth: auth, + Time: timestamp.Unix(), + }) + } else { + payloads[1] = append(payloads[1], WsInput{ + ID: g.Websocket.Conn.GenerateMessageID(false), + Event: event, + Channel: channelsToSubscribe[i].Channel, + Payload: params, + Auth: auth, + Time: timestamp.Unix(), + }) + } + } + return payloads, nil +} + +func (g *Gateio) processFuturesTickers(data []byte, assetType asset.Item) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFutureTicker `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + tickerPriceDatas := make([]ticker.Price, len(resp.Result)) + for x := range resp.Result { + currencyPair, err := currency.NewPairFromString(resp.Result[x].Contract) + if err != nil { + return err + } + tickerPriceDatas[x] = ticker.Price{ + ExchangeName: g.Name, + Volume: resp.Result[x].Volume24HBase, + QuoteVolume: resp.Result[x].Volume24HQuote, + High: resp.Result[x].High24H, + Low: resp.Result[x].Low24H, + Last: resp.Result[x].Last, + AssetType: assetType, + Pair: currencyPair, + LastUpdated: time.Unix(resp.Time, 0), + } + } + g.Websocket.DataHandler <- tickerPriceDatas + return nil +} + +func (g *Gateio) processFuturesTrades(data []byte, assetType asset.Item) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFuturesTrades `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + trades := make([]trade.Data, len(resp.Result)) + for x := range resp.Result { + currencyPair, err := currency.NewPairFromString(resp.Result[x].Contract) + if err != nil { + return err + } + trades[x] = trade.Data{ + Timestamp: resp.Result[x].CreateTimeMs.Time(), + CurrencyPair: currencyPair, + AssetType: assetType, + Exchange: g.Name, + Price: resp.Result[x].Price, + Amount: resp.Result[x].Size, + TID: strconv.FormatInt(resp.Result[x].ID, 10), + } + } + return trade.AddTradesToBuffer(g.Name, trades...) +} + +func (g *Gateio) processFuturesCandlesticks(data []byte, assetType asset.Item) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []FuturesCandlestick `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + klineDatas := make([]stream.KlineData, len(resp.Result)) + for x := range resp.Result { + icp := strings.Split(resp.Result[x].Name, currency.UnderscoreDelimiter) + if len(icp) < 3 { + return errors.New("malformed futures candlestick websocket push data") + } + currencyPair, err := currency.NewPairFromString(strings.Join(icp[1:], currency.UnderscoreDelimiter)) + if err != nil { + return err + } + klineDatas[x] = stream.KlineData{ + Pair: currencyPair, + AssetType: assetType, + Exchange: g.Name, + StartTime: resp.Result[x].Timestamp.Time(), + Interval: icp[0], + OpenPrice: resp.Result[x].OpenPrice, + ClosePrice: resp.Result[x].ClosePrice, + HighPrice: resp.Result[x].HighestPrice, + LowPrice: resp.Result[x].LowestPrice, + Volume: resp.Result[x].Volume, + } + } + g.Websocket.DataHandler <- klineDatas + return nil +} + +func (g *Gateio) processFuturesOrderbookTicker(data []byte) error { + var response WsResponse + orderbookTicker := &WsFuturesOrderbookTicker{} + response.Result = orderbookTicker + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + g.Websocket.DataHandler <- response + return nil +} + +func (g *Gateio) processFuturesAndOptionsOrderbookUpdate(data []byte, assetType asset.Item) error { + var response WsResponse + update := &WsFuturesAndOptionsOrderbookUpdate{} + response.Result = update + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + pair, err := currency.NewPairFromString(update.ContractName) + if err != nil { + return err + } + if (assetType == asset.Options && !fetchedOptionsCurrencyPairSnapshotOrderbook[update.ContractName]) || + (assetType != asset.Options && !fetchedFuturesCurrencyPairSnapshotOrderbook[update.ContractName]) { + orderbooks, err := g.FetchOrderbook(context.Background(), pair, assetType) + if err != nil { + return err + } + if orderbooks.LastUpdateID < update.FirstUpdatedID || orderbooks.LastUpdateID > update.LastUpdatedID { + return nil + } + err = g.Websocket.Orderbook.LoadSnapshot(orderbooks) + if err != nil { + return err + } + if assetType == asset.Options { + fetchedOptionsCurrencyPairSnapshotOrderbook[update.ContractName] = true + } else { + fetchedFuturesCurrencyPairSnapshotOrderbook[update.ContractName] = true + } + } + updates := orderbook.Update{ + UpdateTime: time.UnixMilli(update.TimestampInMs), + Pair: pair, + Asset: assetType, + } + updates.Bids = make([]orderbook.Item, len(update.Bids)) + updates.Asks = make([]orderbook.Item, len(update.Asks)) + for x := range updates.Asks { + updates.Asks[x] = orderbook.Item{ + Amount: update.Asks[x].Size, + Price: update.Asks[x].Price, + } + } + for x := range updates.Bids { + updates.Bids[x] = orderbook.Item{ + Amount: update.Bids[x].Size, + Price: update.Bids[x].Price, + } + } + if len(updates.Asks) == 0 && len(updates.Bids) == 0 { + return errors.New("malformed orderbook data") + } + return g.Websocket.Orderbook.Update(&updates) +} + +func (g *Gateio) processFuturesOrderbookSnapshot(event string, data []byte, assetType asset.Item) error { + if event == "all" { + var response WsResponse + snapshot := &WsFuturesOrderbookSnapshot{} + response.Result = snapshot + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + pair, err := currency.NewPairFromString(snapshot.Contract) + if err != nil { + return err + } + base := orderbook.Base{ + Asset: assetType, + Exchange: g.Name, + Pair: pair, + LastUpdated: snapshot.TimestampInMs.Time(), + VerifyOrderbook: g.CanVerifyOrderbook, + } + base.Bids = make([]orderbook.Item, len(snapshot.Bids)) + base.Asks = make([]orderbook.Item, len(snapshot.Asks)) + for x := range base.Asks { + base.Asks[x] = orderbook.Item{ + Amount: snapshot.Asks[x].Size, + Price: snapshot.Asks[x].Price, + } + } + for x := range base.Bids { + base.Bids[x] = orderbook.Item{ + Amount: snapshot.Bids[x].Size, + Price: snapshot.Bids[x].Price, + } + } + return g.Websocket.Orderbook.LoadSnapshot(&base) + } + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFuturesOrderbookUpdateEvent `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + dataMap := map[string][2][]orderbook.Item{} + for x := range resp.Result { + ab, ok := dataMap[resp.Result[x].CurrencyPair] + if !ok { + ab = [2][]orderbook.Item{} + } + if resp.Result[x].Amount > 0 { + ab[1] = append(ab[1], orderbook.Item{ + Price: resp.Result[x].Price, + Amount: resp.Result[x].Amount, + }) + } else { + ab[0] = append(ab[0], orderbook.Item{ + Price: resp.Result[x].Price, + Amount: -resp.Result[x].Amount, + }) + } + if !ok { + dataMap[resp.Result[x].CurrencyPair] = ab + } + } + if len(dataMap) == 0 { + return errors.New("missing orderbook ask and bid data") + } + for key, ab := range dataMap { + currencyPair, err := currency.NewPairFromString(key) + if err != nil { + return err + } + err = g.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ + Asks: ab[0], + Bids: ab[1], + Asset: assetType, + Exchange: g.Name, + Pair: currencyPair, + LastUpdated: time.Unix(resp.Time, 0), + VerifyOrderbook: g.CanVerifyOrderbook, + }) + if err != nil { + return err + } + } + return nil +} + +func (g *Gateio) processFuturesOrdersPushData(data []byte, assetType asset.Item) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFuturesOrder `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + orderDetails := make([]order.Detail, len(resp.Result)) + for x := range resp.Result { + currencyPair, err := currency.NewPairFromString(resp.Result[x].Contract) + if err != nil { + return err + } + status, err := order.StringToOrderStatus(func() string { + if resp.Result[x].Status == "finished" { + return "cancelled" + } + return resp.Result[x].Status + }()) + if err != nil { + return err + } + orderDetails[x] = order.Detail{ + Amount: resp.Result[x].Size, + Exchange: g.Name, + OrderID: strconv.FormatInt(resp.Result[x].ID, 10), + Status: status, + Pair: currencyPair, + LastUpdated: resp.Result[x].FinishTimeMs.Time(), + Date: resp.Result[x].CreateTimeMs.Time(), + ExecutedAmount: resp.Result[x].Size - resp.Result[x].Left, + Price: resp.Result[x].Price, + AssetType: assetType, + AccountID: resp.Result[x].User, + CloseTime: resp.Result[x].FinishTimeMs.Time(), + } + } + g.Websocket.DataHandler <- orderDetails + return nil +} + +func (g *Gateio) procesFuturesUserTrades(data []byte, assetType asset.Item) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFuturesUserTrade `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + fills := make([]fill.Data, len(resp.Result)) + for x := range resp.Result { + currencyPair, err := currency.NewPairFromString(resp.Result[x].Contract) + if err != nil { + return err + } + fills[x] = fill.Data{ + Timestamp: resp.Result[x].CreateTimeMs.Time(), + Exchange: g.Name, + CurrencyPair: currencyPair, + OrderID: resp.Result[x].OrderID, + TradeID: resp.Result[x].ID, + Price: resp.Result[x].Price, + Amount: resp.Result[x].Size, + AssetType: assetType, + } + } + return g.Websocket.Fills.Update(fills...) +} + +func (g *Gateio) processFuturesLiquidatesNotification(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFuturesLiquidationNotification `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + g.Websocket.DataHandler <- &resp + return nil +} + +func (g *Gateio) processFuturesAutoDeleveragesNotification(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFuturesAutoDeleveragesNotification `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + g.Websocket.DataHandler <- &resp + return nil +} + +func (g *Gateio) processPositionCloseData(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsPositionClose `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + g.Websocket.DataHandler <- &resp + return nil +} + +func (g *Gateio) processBalancePushData(data []byte, assetType asset.Item) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsBalance `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + accountChange := make([]account.Change, len(resp.Result)) + for x := range resp.Result { + info := strings.Split(resp.Result[x].Text, currency.UnderscoreDelimiter) + if len(info) != 2 { + return errors.New("malformed text") + } + code := currency.NewCode(info[0]) + accountChange[x] = account.Change{ + Exchange: g.Name, + Currency: code, + Asset: assetType, + Amount: resp.Result[x].Balance, + Account: resp.Result[x].User, + } + } + g.Websocket.DataHandler <- accountChange + return nil +} + +func (g *Gateio) processFuturesReduceRiskLimitNotification(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFuturesReduceRiskLimitNotification `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + g.Websocket.DataHandler <- &resp + return nil +} + +func (g *Gateio) processFuturesPositionsNotification(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFuturesPosition `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + g.Websocket.DataHandler <- &resp + return nil +} + +func (g *Gateio) processFuturesAutoOrderPushData(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFuturesAutoOrder `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + g.Websocket.DataHandler <- &resp + return nil +} diff --git a/exchanges/gateio/gateio_ws_option.go b/exchanges/gateio/gateio_ws_option.go new file mode 100644 index 00000000..527046a8 --- /dev/null +++ b/exchanges/gateio/gateio_ws_option.go @@ -0,0 +1,780 @@ +package gateio + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/fill" + "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/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/trade" + "github.com/thrasher-corp/gocryptotrader/log" +) + +const ( + optionsWebsocketURL = "wss://op-ws.gateio.live/v4/ws" + optionsWebsocketTestnetURL = "wss://op-ws-testnet.gateio.live/v4/ws" + + // channels + optionsPingChannel = "options.ping" + optionsContractTickersChannel = "options.contract_tickers" + optionsUnderlyingTickersChannel = "options.ul_tickers" + optionsTradesChannel = "options.trades" + optionsUnderlyingTradesChannel = "options.ul_trades" + optionsUnderlyingPriceChannel = "options.ul_price" + optionsMarkPriceChannel = "options.mark_price" + optionsSettlementChannel = "options.settlements" + optionsContractsChannel = "options.contracts" + optionsContractCandlesticksChannel = "options.contract_candlesticks" + optionsUnderlyingCandlesticksChannel = "options.ul_candlesticks" + optionsOrderbookChannel = "options.order_book" + optionsOrderbookTickerChannel = "options.book_ticker" + optionsOrderbookUpdateChannel = "options.order_book_update" + optionsOrdersChannel = "options.orders" + optionsUserTradesChannel = "options.usertrades" + optionsLiquidatesChannel = "options.liquidates" + optionsUserSettlementChannel = "options.user_settlements" + optionsPositionCloseChannel = "options.position_closes" + optionsBalancesChannel = "options.balances" + optionsPositionsChannel = "options.positions" +) + +var defaultOptionsSubscriptions = []string{ + optionsContractTickersChannel, + optionsUnderlyingTickersChannel, + optionsTradesChannel, + optionsUnderlyingTradesChannel, + optionsContractCandlesticksChannel, + optionsUnderlyingCandlesticksChannel, + optionsOrderbookChannel, + optionsOrderbookUpdateChannel, +} + +var fetchedOptionsCurrencyPairSnapshotOrderbook = make(map[string]bool) + +// WsOptionsConnect initiates a websocket connection to options websocket endpoints. +func (g *Gateio) WsOptionsConnect() error { + if !g.Websocket.IsEnabled() || !g.IsEnabled() { + return errors.New(stream.WebsocketNotEnabled) + } + err := g.CurrencyPairs.IsAssetEnabled(asset.Options) + if err != nil { + return err + } + var dialer websocket.Dialer + err = g.Websocket.SetWebsocketURL(optionsWebsocketURL, false, true) + if err != nil { + return err + } + err = g.Websocket.Conn.Dial(&dialer, http.Header{}) + if err != nil { + return err + } + pingMessage, err := json.Marshal(WsInput{ + ID: g.Websocket.Conn.GenerateMessageID(false), + Time: time.Now().Unix(), + Channel: optionsPingChannel, + }) + if err != nil { + return err + } + g.Websocket.Wg.Add(1) + go g.wsReadOptionsConnData() + g.Websocket.Conn.SetupPingHandler(stream.PingHandler{ + Websocket: true, + Delay: time.Second * 5, + MessageType: websocket.PingMessage, + Message: pingMessage, + }) + return nil +} + +// GenerateOptionsDefaultSubscriptions generates list of channel subscriptions for options asset type. +func (g *Gateio) GenerateOptionsDefaultSubscriptions() ([]stream.ChannelSubscription, error) { + channelsToSubscribe := defaultOptionsSubscriptions + var userID int64 + if g.Websocket.CanUseAuthenticatedEndpoints() { + var err error + _, err = g.GetCredentials(context.TODO()) + if err != nil { + g.Websocket.SetCanUseAuthenticatedEndpoints(false) + goto getEnabledPairs + } + response, err := g.GetSubAccountBalances(context.Background(), "") + if err != nil { + return nil, err + } + if len(response) != 0 { + channelsToSubscribe = append(channelsToSubscribe, + optionsUserTradesChannel, + optionsBalancesChannel, + ) + userID = response[0].UserID + } else if g.Verbose { + log.Errorf(log.ExchangeSys, "no subaccount found for authenticated options channel subscriptions") + } + } +getEnabledPairs: + var subscriptions []stream.ChannelSubscription + pairs, err := g.GetEnabledPairs(asset.Options) + if err != nil { + return nil, err + } + for i := range channelsToSubscribe { + for j := range pairs { + params := make(map[string]interface{}) + switch channelsToSubscribe[i] { + case optionsOrderbookChannel: + params["accuracy"] = "0" + params["level"] = "20" + case optionsContractCandlesticksChannel, optionsUnderlyingCandlesticksChannel: + params["interval"] = kline.FiveMin + case optionsOrderbookUpdateChannel: + params["interval"] = kline.ThousandMilliseconds + params["level"] = "20" + case optionsOrdersChannel, + optionsUserTradesChannel, + optionsLiquidatesChannel, + optionsUserSettlementChannel, + optionsPositionCloseChannel, + optionsBalancesChannel, + optionsPositionsChannel: + if userID == 0 { + continue + } + params["user_id"] = userID + } + fpair, err := g.FormatExchangeCurrency(pairs[j], asset.Options) + if err != nil { + return nil, err + } + subscriptions = append(subscriptions, stream.ChannelSubscription{ + Channel: channelsToSubscribe[i], + Currency: fpair.Upper(), + Params: params, + }) + } + } + return subscriptions, nil +} + +func (g *Gateio) generateOptionsPayload(event string, channelsToSubscribe []stream.ChannelSubscription) ([]WsInput, error) { + if len(channelsToSubscribe) == 0 { + return nil, errors.New("cannot generate payload, no channels supplied") + } + var err error + var intervalString string + payloads := make([]WsInput, len(channelsToSubscribe)) + for i := range channelsToSubscribe { + var auth *WsAuthInput + timestamp := time.Now() + var params []string + switch channelsToSubscribe[i].Channel { + case optionsUnderlyingTickersChannel, + optionsUnderlyingTradesChannel, + optionsUnderlyingPriceChannel, + optionsUnderlyingCandlesticksChannel: + var uly currency.Pair + uly, err = g.GetUnderlyingFromCurrencyPair(channelsToSubscribe[i].Currency) + if err != nil { + return nil, err + } + params = append(params, uly.String()) + case optionsBalancesChannel: + // options.balance channel does not require underlying or contract + default: + channelsToSubscribe[i].Currency.Delimiter = currency.UnderscoreDelimiter + params = append(params, channelsToSubscribe[i].Currency.String()) + } + switch channelsToSubscribe[i].Channel { + case optionsOrderbookChannel: + accuracy, ok := channelsToSubscribe[i].Params["accuracy"].(string) + if !ok { + return nil, fmt.Errorf("%w, invalid options orderbook accuracy", orderbook.ErrOrderbookInvalid) + } + level, ok := channelsToSubscribe[i].Params["level"].(string) + if !ok { + return nil, fmt.Errorf("%w, invalid options orderbook level", orderbook.ErrOrderbookInvalid) + } + params = append( + params, + level, + accuracy, + ) + case optionsUserTradesChannel, + optionsBalancesChannel, + optionsOrdersChannel, + optionsLiquidatesChannel, + optionsUserSettlementChannel, + optionsPositionCloseChannel, + optionsPositionsChannel: + userID, ok := channelsToSubscribe[i].Params["user_id"].(int64) + if !ok { + continue + } + params = append([]string{strconv.FormatInt(userID, 10)}, params...) + var creds *account.Credentials + creds, err = g.GetCredentials(context.Background()) + if err != nil { + return nil, err + } + var sigTemp string + sigTemp, err = g.generateWsSignature(creds.Secret, event, channelsToSubscribe[i].Channel, timestamp) + if err != nil { + return nil, err + } + auth = &WsAuthInput{ + Method: "api_key", + Key: creds.Key, + Sign: sigTemp, + } + case optionsOrderbookUpdateChannel: + interval, ok := channelsToSubscribe[i].Params["interval"].(kline.Interval) + if !ok { + return nil, fmt.Errorf("%w, missing options orderbook interval", orderbook.ErrOrderbookInvalid) + } + intervalString, err = g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params = append(params, + intervalString) + if value, ok := channelsToSubscribe[i].Params["level"].(int); ok { + params = append(params, strconv.Itoa(value)) + } + case optionsContractCandlesticksChannel, + optionsUnderlyingCandlesticksChannel: + interval, ok := channelsToSubscribe[i].Params["interval"].(kline.Interval) + if !ok { + return nil, errors.New("missing options underlying candlesticks interval") + } + intervalString, err = g.GetIntervalString(interval) + if err != nil { + return nil, err + } + params = append( + []string{intervalString}, + params...) + } + payloads[i] = WsInput{ + ID: g.Websocket.Conn.GenerateMessageID(false), + Event: event, + Channel: channelsToSubscribe[i].Channel, + Payload: params, + Auth: auth, + Time: timestamp.Unix(), + } + } + return payloads, nil +} + +// wsReadOptionsConnData receives and passes on websocket messages for processing +func (g *Gateio) wsReadOptionsConnData() { + defer g.Websocket.Wg.Done() + for { + resp := g.Websocket.Conn.ReadMessage() + if resp.Raw == nil { + return + } + err := g.wsHandleOptionsData(resp.Raw) + if err != nil { + g.Websocket.DataHandler <- err + } + } +} + +// OptionsSubscribe sends a websocket message to stop receiving data for asset type options +func (g *Gateio) OptionsSubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { + return g.handleOptionsSubscription("subscribe", channelsToUnsubscribe) +} + +// OptionsUnsubscribe sends a websocket message to stop receiving data for asset type options +func (g *Gateio) OptionsUnsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error { + return g.handleOptionsSubscription("unsubscribe", channelsToUnsubscribe) +} + +// handleOptionsSubscription sends a websocket message to receive data from the channel +func (g *Gateio) handleOptionsSubscription(event string, channelsToSubscribe []stream.ChannelSubscription) error { + payloads, err := g.generateOptionsPayload(event, channelsToSubscribe) + if err != nil { + return err + } + var errs error + for k := range payloads { + result, err := g.Websocket.Conn.SendMessageReturnResponse(payloads[k].ID, payloads[k]) + if err != nil { + errs = common.AppendError(errs, err) + continue + } + var resp WsEventResponse + if err = json.Unmarshal(result, &resp); err != nil { + errs = common.AppendError(errs, err) + } else { + if resp.Error != nil && resp.Error.Code != 0 { + errs = common.AppendError(errs, fmt.Errorf("error while %s to channel %s asset type: options error code: %d message: %s", payloads[k].Event, payloads[k].Channel, resp.Error.Code, resp.Error.Message)) + continue + } + if payloads[k].Event == "subscribe" { + g.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[k]) + } else { + g.Websocket.RemoveSuccessfulUnsubscriptions(channelsToSubscribe[k]) + } + } + } + return errs +} + +func (g *Gateio) wsHandleOptionsData(respRaw []byte) error { + var result WsResponse + var eventResponse WsEventResponse + err := json.Unmarshal(respRaw, &eventResponse) + if err == nil && + (eventResponse.Result != nil || eventResponse.Error != nil) && + (eventResponse.Event == "subscribe" || eventResponse.Event == "unsubscribe") { + if !g.Websocket.Match.IncomingWithData(eventResponse.ID, respRaw) { + return fmt.Errorf("couldn't match subscription message with ID: %d", eventResponse.ID) + } + return nil + } + err = json.Unmarshal(respRaw, &result) + if err != nil { + return err + } + switch result.Channel { + case optionsContractTickersChannel: + return g.processOptionsContractTickers(respRaw) + case optionsUnderlyingTickersChannel: + return g.processOptionsUnderlyingTicker(respRaw) + case optionsTradesChannel, + optionsUnderlyingTradesChannel: + return g.processOptionsTradesPushData(respRaw) + case optionsUnderlyingPriceChannel: + return g.processOptionsUnderlyingPricePushData(respRaw) + case optionsMarkPriceChannel: + return g.processOptionsMarkPrice(respRaw) + case optionsSettlementChannel: + return g.processOptionsSettlementPushData(respRaw) + case optionsContractsChannel: + return g.processOptionsContractPushData(respRaw) + case optionsContractCandlesticksChannel, + optionsUnderlyingCandlesticksChannel: + return g.processOptionsCandlestickPushData(respRaw) + case optionsOrderbookChannel: + return g.processOptionsOrderbookSnapshotPushData(result.Event, respRaw) + case optionsOrderbookTickerChannel: + return g.processOrderbookTickerPushData(respRaw) + case optionsOrderbookUpdateChannel: + return g.processFuturesAndOptionsOrderbookUpdate(respRaw, asset.Options) + case optionsOrdersChannel: + return g.processOptionsOrderPushData(respRaw) + case optionsUserTradesChannel: + return g.processOptionsUserTradesPushData(respRaw) + case optionsLiquidatesChannel: + return g.processOptionsLiquidatesPushData(respRaw) + case optionsUserSettlementChannel: + return g.processOptionsUsersPersonalSettlementsPushData(respRaw) + case optionsPositionCloseChannel: + return g.processPositionCloseData(respRaw) + case optionsBalancesChannel: + return g.processBalancePushData(respRaw, asset.Options) + case optionsPositionsChannel: + return g.processOptionsPositionPushData(respRaw) + default: + g.Websocket.DataHandler <- stream.UnhandledMessageWarning{ + Message: g.Name + stream.UnhandledMessage + string(respRaw), + } + return errors.New(stream.UnhandledMessage) + } +} + +func (g *Gateio) processOptionsContractTickers(data []byte) error { + var response WsResponse + tickerData := OptionsTicker{} + response.Result = &tickerData + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + currencyPair, err := currency.NewPairFromString(tickerData.Name) + if err != nil { + return err + } + g.Websocket.DataHandler <- &ticker.Price{ + Pair: currencyPair, + Last: tickerData.LastPrice.Float64(), + Bid: tickerData.Bid1Price, + Ask: tickerData.Ask1Price, + AskSize: tickerData.Ask1Size, + BidSize: tickerData.Bid1Size, + ExchangeName: g.Name, + AssetType: asset.Options, + } + return nil +} + +func (g *Gateio) processOptionsUnderlyingTicker(data []byte) error { + var response WsResponse + response.Result = &WsOptionUnderlyingTicker{} + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + g.Websocket.DataHandler <- &response + return nil +} + +func (g *Gateio) processOptionsTradesPushData(data []byte) error { + saveTradeData := g.IsSaveTradeDataEnabled() + if !saveTradeData && + !g.IsTradeFeedEnabled() { + return nil + } + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsOptionsTrades `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + trades := make([]trade.Data, len(resp.Result)) + for x := range resp.Result { + currencyPair, err := currency.NewPairFromString(resp.Result[x].Contract) + if err != nil { + return err + } + trades[x] = trade.Data{ + Timestamp: resp.Result[x].CreateTimeMs.Time(), + CurrencyPair: currencyPair, + AssetType: asset.Options, + Exchange: g.Name, + Price: resp.Result[x].Price, + Amount: resp.Result[x].Size, + TID: strconv.FormatInt(resp.Result[x].ID, 10), + } + } + return g.Websocket.Trade.Update(saveTradeData, trades...) +} + +func (g *Gateio) processOptionsUnderlyingPricePushData(data []byte) error { + var response WsResponse + priceD := WsOptionsUnderlyingPrice{} + response.Result = &priceD + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + g.Websocket.DataHandler <- &response + return nil +} + +func (g *Gateio) processOptionsMarkPrice(data []byte) error { + var response WsResponse + markPrice := WsOptionsMarkPrice{} + response.Result = &markPrice + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + g.Websocket.DataHandler <- &response + return nil +} + +func (g *Gateio) processOptionsSettlementPushData(data []byte) error { + var response WsResponse + settlementData := WsOptionsSettlement{} + response.Result = &settlementData + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + g.Websocket.DataHandler <- &response + return nil +} + +func (g *Gateio) processOptionsContractPushData(data []byte) error { + var response WsResponse + contractData := WsOptionsContract{} + response.Result = &contractData + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + g.Websocket.DataHandler <- &response + return nil +} + +func (g *Gateio) processOptionsCandlestickPushData(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsOptionsContractCandlestick `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + klineDatas := make([]stream.KlineData, len(resp.Result)) + for x := range resp.Result { + icp := strings.Split(resp.Result[x].NameOfSubscription, currency.UnderscoreDelimiter) + if len(icp) < 3 { + return errors.New("malformed options candlestick websocket push data") + } + currencyPair, err := currency.NewPairFromString(strings.Join(icp[1:], currency.UnderscoreDelimiter)) + if err != nil { + return err + } + klineDatas[x] = stream.KlineData{ + Pair: currencyPair, + AssetType: asset.Options, + Exchange: g.Name, + StartTime: time.Unix(resp.Result[x].Timestamp, 0), + Interval: icp[0], + OpenPrice: resp.Result[x].OpenPrice, + ClosePrice: resp.Result[x].ClosePrice, + HighPrice: resp.Result[x].HighestPrice, + LowPrice: resp.Result[x].LowestPrice, + Volume: resp.Result[x].Amount, + } + } + g.Websocket.DataHandler <- klineDatas + return nil +} + +func (g *Gateio) processOrderbookTickerPushData(data []byte) error { + var response WsResponse + orderbookTicker := WsOptionsOrderbookTicker{} + response.Result = &orderbookTicker + err := json.Unmarshal(data, &orderbookTicker) + if err != nil { + return err + } + g.Websocket.DataHandler <- &response + return nil +} + +func (g *Gateio) processOptionsOrderbookSnapshotPushData(event string, data []byte) error { + if event == "all" { + var response WsResponse + snapshot := WsOptionsOrderbookSnapshot{} + response.Result = &snapshot + err := json.Unmarshal(data, &response) + if err != nil { + return err + } + pair, err := currency.NewPairFromString(snapshot.Contract) + if err != nil { + return err + } + base := orderbook.Base{ + Asset: asset.Options, + Exchange: g.Name, + Pair: pair, + LastUpdated: snapshot.Timestamp.Time(), + VerifyOrderbook: g.CanVerifyOrderbook, + } + base.Asks = make([]orderbook.Item, len(snapshot.Asks)) + base.Bids = make([]orderbook.Item, len(snapshot.Bids)) + for x := range base.Asks { + base.Asks[x] = orderbook.Item{ + Amount: snapshot.Asks[x].Size, + Price: snapshot.Asks[x].Price, + } + } + for x := range base.Bids { + base.Bids[x] = orderbook.Item{ + Amount: snapshot.Bids[x].Size, + Price: snapshot.Bids[x].Price, + } + } + return g.Websocket.Orderbook.LoadSnapshot(&base) + } + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsFuturesOrderbookUpdateEvent `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + dataMap := map[string][2][]orderbook.Item{} + for x := range resp.Result { + ab, ok := dataMap[resp.Result[x].CurrencyPair] + if !ok { + ab = [2][]orderbook.Item{} + } + if resp.Result[x].Amount > 0 { + ab[1] = append(ab[1], orderbook.Item{ + Price: resp.Result[x].Price, + Amount: resp.Result[x].Amount, + }) + } else { + ab[0] = append(ab[0], orderbook.Item{ + Price: resp.Result[x].Price, + Amount: -resp.Result[x].Amount, + }) + } + if !ok { + dataMap[resp.Result[x].CurrencyPair] = ab + } + } + if len(dataMap) == 0 { + return errors.New("missing orderbook ask and bid data") + } + for key, ab := range dataMap { + currencyPair, err := currency.NewPairFromString(key) + if err != nil { + return err + } + err = g.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ + Asks: ab[0], + Bids: ab[1], + Asset: asset.Options, + Exchange: g.Name, + Pair: currencyPair, + LastUpdated: time.Unix(resp.Time, 0), + VerifyOrderbook: g.CanVerifyOrderbook, + }) + if err != nil { + return err + } + } + return nil +} + +func (g *Gateio) processOptionsOrderPushData(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsOptionsOrder `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + orderDetails := make([]order.Detail, len(resp.Result)) + for x := range resp.Result { + currencyPair, err := currency.NewPairFromString(resp.Result[x].Contract) + if err != nil { + return err + } + status, err := order.StringToOrderStatus(func() string { + if resp.Result[x].Status == "finished" { + return "cancelled" + } + return resp.Result[x].Status + }()) + if err != nil { + return err + } + orderDetails[x] = order.Detail{ + Amount: resp.Result[x].Size, + Exchange: g.Name, + OrderID: strconv.FormatInt(resp.Result[x].ID, 10), + Status: status, + Pair: currencyPair, + Date: resp.Result[x].CreationTimeMs.Time(), + ExecutedAmount: resp.Result[x].Size - resp.Result[x].Left, + Price: resp.Result[x].Price, + AssetType: asset.Options, + AccountID: resp.Result[x].User, + } + } + g.Websocket.DataHandler <- orderDetails + return nil +} + +func (g *Gateio) processOptionsUserTradesPushData(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsOptionsUserTrade `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + fills := make([]fill.Data, len(resp.Result)) + for x := range resp.Result { + currencyPair, err := currency.NewPairFromString(resp.Result[x].Contract) + if err != nil { + return err + } + fills[x] = fill.Data{ + Timestamp: resp.Result[x].CreateTimeMs.Time(), + Exchange: g.Name, + CurrencyPair: currencyPair, + OrderID: resp.Result[x].OrderID, + TradeID: resp.Result[x].ID, + Price: resp.Result[x].Price, + Amount: resp.Result[x].Size, + } + } + return g.Websocket.Fills.Update(fills...) +} + +func (g *Gateio) processOptionsLiquidatesPushData(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsOptionsLiquidates `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + g.Websocket.DataHandler <- &resp + return nil +} + +func (g *Gateio) processOptionsUsersPersonalSettlementsPushData(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsOptionsUserSettlement `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + g.Websocket.DataHandler <- &resp + return nil +} + +func (g *Gateio) processOptionsPositionPushData(data []byte) error { + resp := struct { + Time int64 `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []WsOptionsPosition `json:"result"` + }{} + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + g.Websocket.DataHandler <- &resp + return nil +} diff --git a/exchanges/gateio/ratelimiter.go b/exchanges/gateio/ratelimiter.go new file mode 100644 index 00000000..0d024095 --- /dev/null +++ b/exchanges/gateio/ratelimiter.go @@ -0,0 +1,118 @@ +package gateio + +import ( + "context" + "fmt" + "time" + + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "golang.org/x/time/rate" +) + +// GateIO endpoints limits. +const ( + spotDefaultEPL request.EndpointLimit = iota + spotPrivateEPL + spotPlaceOrdersEPL + spotCancelOrdersEPL + perpetualSwapDefaultEPL + perpetualSwapPlaceOrdersEPL + perpetualSwapPrivateEPL + perpetualSwapCancelOrdersEPL + walletEPL + withdrawalEPL + + // Request rates per interval + + spotPublicRate = 900 + spotPrivateRate = 900 + spotPlaceOrdersRate = 10 + spotCancelOrdersRate = 500 + perpetualSwapPublicRate = 300 + perpetualSwapPlaceOrdersRate = 100 + perpetualSwapPrivateRate = 400 + perpetualSwapCancelOrdersRate = 400 + walletRate = 200 + withdrawalRate = 1 + + // interval + oneSecondInterval = time.Second + threeSecondsInterval = time.Second * 3 +) + +// RateLimitter represents a rate limiter structure for gateIO endpoints. +type RateLimitter struct { + SpotDefault *rate.Limiter + SpotPrivate *rate.Limiter + SpotPlaceOrders *rate.Limiter + SpotCancelOrders *rate.Limiter + PerpetualSwapDefault *rate.Limiter + PerpetualSwapPlaceOrders *rate.Limiter + PerpetualSwapPrivate *rate.Limiter + PerpetualSwapCancelOrders *rate.Limiter + Wallet *rate.Limiter + Withdrawal *rate.Limiter +} + +// Limit executes rate limiting functionality +// implements the request.Limiter interface +func (r *RateLimitter) Limit(ctx context.Context, epl request.EndpointLimit) error { + var limiter *rate.Limiter + var tokens int + switch epl { + case spotDefaultEPL: + limiter, tokens = r.SpotDefault, 1 + case spotPrivateEPL: + return r.SpotPrivate.Wait(ctx) + case spotPlaceOrdersEPL: + return r.SpotPlaceOrders.Wait(ctx) + case spotCancelOrdersEPL: + return r.SpotCancelOrders.Wait(ctx) + case perpetualSwapDefaultEPL: + limiter, tokens = r.PerpetualSwapDefault, 1 + case perpetualSwapPlaceOrdersEPL: + return r.PerpetualSwapPlaceOrders.Wait(ctx) + case perpetualSwapPrivateEPL: + return r.PerpetualSwapPrivate.Wait(ctx) + case perpetualSwapCancelOrdersEPL: + return r.PerpetualSwapCancelOrders.Wait(ctx) + case walletEPL: + return r.Wallet.Wait(ctx) + case withdrawalEPL: + return r.Withdrawal.Wait(ctx) + default: + } + var finalDelay time.Duration + var reserves = make([]*rate.Reservation, tokens) + for i := 0; i < tokens; i++ { + reserves[i] = limiter.Reserve() + finalDelay = reserves[i].Delay() + } + if dl, ok := ctx.Deadline(); ok && dl.Before(time.Now().Add(finalDelay)) { + for x := range reserves { + reserves[x].Cancel() + } + return fmt.Errorf("rate limit delay of %s will exceed deadline: %w", + finalDelay, + context.DeadlineExceeded) + } + + time.Sleep(finalDelay) + return nil +} + +// SetRateLimit returns the rate limiter for the exchange +func SetRateLimit() *RateLimitter { + return &RateLimitter{ + SpotDefault: request.NewRateLimit(oneSecondInterval, spotPublicRate), + SpotPrivate: request.NewRateLimit(oneSecondInterval, spotPrivateRate), + SpotPlaceOrders: request.NewRateLimit(oneSecondInterval, spotPlaceOrdersRate), + SpotCancelOrders: request.NewRateLimit(oneSecondInterval, spotCancelOrdersRate), + PerpetualSwapDefault: request.NewRateLimit(oneSecondInterval, perpetualSwapPublicRate), + PerpetualSwapPlaceOrders: request.NewRateLimit(oneSecondInterval, perpetualSwapPlaceOrdersRate), + PerpetualSwapPrivate: request.NewRateLimit(oneSecondInterval, perpetualSwapPrivateRate), + PerpetualSwapCancelOrders: request.NewRateLimit(oneSecondInterval, perpetualSwapCancelOrdersRate), + Wallet: request.NewRateLimit(oneSecondInterval, walletRate), + Withdrawal: request.NewRateLimit(threeSecondsInterval, withdrawalRate), + } +} diff --git a/exchanges/kline/kline.go b/exchanges/kline/kline.go index 0e49f878..60db39cc 100644 --- a/exchanges/kline/kline.go +++ b/exchanges/kline/kline.go @@ -251,6 +251,12 @@ func (k *Item) FormatDates() { // durationToWord returns english version of interval func durationToWord(in Interval) string { switch in { + case HundredMilliseconds: + return "hundredmillisec" + case ThousandMilliseconds: + return "thousandmillisec" + case TenSecond: + return "tensec" case FifteenSecond: return "fifteensecond" case OneMin: @@ -291,6 +297,10 @@ func durationToWord(in Interval) string { return "twoweek" case OneMonth: return "onemonth" + case ThreeMonth: + return "threemonth" + case SixMonth: + return "sixmonth" case OneYear: return "oneyear" default: diff --git a/exchanges/kline/kline_test.go b/exchanges/kline/kline_test.go index 076df458..bc3ceef2 100644 --- a/exchanges/kline/kline_test.go +++ b/exchanges/kline/kline_test.go @@ -153,6 +153,18 @@ func TestDurationToWord(t *testing.T) { name string interval Interval }{ + { + "hundredmillisec", + HundredMilliseconds, + }, + { + "thousandmillisec", + ThousandMilliseconds, + }, + { + "tensec", + TenSecond, + }, { "FifteenSecond", FifteenSecond, @@ -233,6 +245,14 @@ func TestDurationToWord(t *testing.T) { "OneMonth", OneMonth, }, + { + "ThreeMonth", + ThreeMonth, + }, + { + "SixMonth", + SixMonth, + }, { "OneYear", OneYear, diff --git a/exchanges/kline/kline_types.go b/exchanges/kline/kline_types.go index 143f01b3..995df6bf 100644 --- a/exchanges/kline/kline_types.go +++ b/exchanges/kline/kline_types.go @@ -11,32 +11,36 @@ import ( // Consts here define basic time intervals const ( - FifteenSecond = Interval(15 * time.Second) - OneMin = Interval(time.Minute) - ThreeMin = 3 * OneMin - FiveMin = 5 * OneMin - TenMin = 10 * OneMin - FifteenMin = 15 * OneMin - ThirtyMin = 30 * OneMin - OneHour = Interval(time.Hour) - TwoHour = 2 * OneHour - ThreeHour = 3 * OneHour - FourHour = 4 * OneHour - SixHour = 6 * OneHour - EightHour = 8 * OneHour - TwelveHour = 12 * OneHour - OneDay = 24 * OneHour - TwoDay = 2 * OneDay - ThreeDay = 3 * OneDay - FiveDay = 5 * OneDay - SevenDay = 7 * OneDay - FifteenDay = 15 * OneDay - OneWeek = 7 * OneDay - TwoWeek = 2 * OneWeek - OneMonth = 30 * OneDay - ThreeMonth = 3 * OneMonth - SixMonth = 6 * OneMonth - OneYear = 365 * OneDay + HundredMilliseconds = Interval(100 * time.Millisecond) + ThousandMilliseconds = 10 * HundredMilliseconds + TenSecond = Interval(10 * time.Second) + FifteenSecond = Interval(15 * time.Second) + ThirtySecond = 2 * FifteenSecond + OneMin = Interval(time.Minute) + ThreeMin = 3 * OneMin + FiveMin = 5 * OneMin + TenMin = 10 * OneMin + FifteenMin = 15 * OneMin + ThirtyMin = 30 * OneMin + OneHour = Interval(time.Hour) + TwoHour = 2 * OneHour + ThreeHour = 3 * OneHour + FourHour = 4 * OneHour + SixHour = 6 * OneHour + EightHour = 8 * OneHour + TwelveHour = 12 * OneHour + OneDay = 24 * OneHour + TwoDay = 2 * OneDay + ThreeDay = 3 * OneDay + SevenDay = 7 * OneDay + FifteenDay = 15 * OneDay + OneWeek = 7 * OneDay + TwoWeek = 2 * OneWeek + OneMonth = 30 * OneDay + ThreeMonth = 90 * OneDay + SixMonth = 2 * ThreeMonth + OneYear = 365 * OneDay + FiveDay = 5 * OneDay ) var ( @@ -84,6 +88,9 @@ var ( // SupportedIntervals is a list of all supported intervals SupportedIntervals = []Interval{ + HundredMilliseconds, + ThousandMilliseconds, + TenSecond, FifteenSecond, OneMin, ThreeMin, @@ -100,6 +107,7 @@ var ( TwelveHour, OneDay, ThreeDay, + FiveDay, SevenDay, FifteenDay, OneWeek, @@ -108,6 +116,8 @@ var ( ThreeMonth, SixMonth, OneYear, + ThreeMonth, + SixMonth, } ) diff --git a/exchanges/request/request.go b/exchanges/request/request.go index 186cfa0d..cf010c0f 100644 --- a/exchanges/request/request.go +++ b/exchanges/request/request.go @@ -236,7 +236,7 @@ func (r *Requester) doRequest(ctx context.Context, endpoint EndpointLimit, newRe } if resp.StatusCode < http.StatusOK || - resp.StatusCode > http.StatusAccepted { + resp.StatusCode > http.StatusNoContent { return fmt.Errorf("%s unsuccessful HTTP status code: %d raw response: %s", r.name, resp.StatusCode, diff --git a/testdata/configtest.json b/testdata/configtest.json index 93b271b2..0fdf5344 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -1488,82 +1488,107 @@ ] }, { - "name": "GateIO", - "enabled": true, - "verbose": false, - "httpTimeout": 15000000000, - "websocketResponseCheckTimeout": 30000000, - "websocketResponseMaxLimit": 7000000000, - "websocketTrafficTimeout": 30000000000, - "websocketOrderbookBufferLimit": 5, - "baseCurrencies": "USD", - "currencyPairs": { - "requestFormat": { - "uppercase": false, - "delimiter": "_" - }, - "configFormat": { - "uppercase": true, - "delimiter": "_" - }, - "useGlobalFormat": true, - "assetTypes": [ - "spot" - ], - "pairs": { - "spot": { - "enabled": "BTC_USDT", - "available": "USDT_CNYX,BTC_CNYX,ETH_CNYX,EOS_CNYX,BCH_CNYX,XRP_CNYX,DOGE_CNYX,TIPS_CNYX,BTC_USDC,BTC_PAX,BTC_USDT,BCH_USDT,ETH_USDT,ETC_USDT,QTUM_USDT,LTC_USDT,DASH_USDT,ZEC_USDT,BTM_USDT,EOS_USDT,REQ_USDT,SNT_USDT,OMG_USDT,PAY_USDT,CVC_USDT,ZRX_USDT,TNT_USDT,XMR_USDT,XRP_USDT,DOGE_USDT,BAT_USDT,PST_USDT,BTG_USDT,DPY_USDT,LRC_USDT,STORJ_USDT,RDN_USDT,STX_USDT,KNC_USDT,LINK_USDT,CDT_USDT,AE_USDT,AE_ETH,AE_BTC,CDT_ETH,RDN_ETH,STX_ETH,KNC_ETH,LINK_ETH,REQ_ETH,RCN_ETH,TRX_ETH,ARN_ETH,BNT_ETH,VET_ETH,MCO_ETH,FUN_ETH,DATA_ETH,RLC_ETH,RLC_USDT,ZSC_ETH,WINGS_ETH,MDA_ETH,RCN_USDT,TRX_USDT,VET_USDT,MCO_USDT,FUN_USDT,DATA_USDT,ZSC_USDT,MDA_USDT,XTZ_USDT,XTZ_BTC,XTZ_ETH,GNT_USDT,GNT_ETH,GEM_USDT,GEM_ETH,RFR_USDT,RFR_ETH,DADI_USDT,DADI_ETH,ABT_USDT,ABT_ETH,LEDU_BTC,LEDU_ETH,OST_USDT,OST_ETH,XLM_USDT,XLM_ETH,XLM_BTC,MOBI_USDT,MOBI_ETH,MOBI_BTC,OCN_USDT,OCN_ETH,OCN_BTC,ZPT_USDT,ZPT_ETH,ZPT_BTC,COFI_USDT,COFI_ETH,JNT_USDT,JNT_ETH,JNT_BTC,BLZ_USDT,BLZ_ETH,GXS_USDT,GXS_BTC,MTN_USDT,MTN_ETH,RUFF_USDT,RUFF_ETH,RUFF_BTC,TNC_USDT,TNC_ETH,TNC_BTC,ZIL_USDT,ZIL_ETH,BTO_USDT,BTO_ETH,THETA_USDT,THETA_ETH,DDD_USDT,DDD_ETH,DDD_BTC,MKR_USDT,MKR_ETH,DAI_USDT,SMT_USDT,SMT_ETH,MDT_USDT,MDT_ETH,MDT_BTC,MANA_USDT,MANA_ETH,LUN_USDT,LUN_ETH,SALT_USDT,SALT_ETH,FUEL_USDT,FUEL_ETH,ELF_USDT,ELF_ETH,DRGN_USDT,DRGN_ETH,GTC_USDT,GTC_ETH,GTC_BTC,QLC_USDT,QLC_BTC,QLC_ETH,DBC_USDT,DBC_BTC,DBC_ETH,BNTY_USDT,BNTY_ETH,LEND_USDT,LEND_ETH,ICX_USDT,ICX_ETH,BTF_USDT,BTF_BTC,ADA_USDT,ADA_BTC,LSK_USDT,LSK_BTC,WAVES_USDT,WAVES_BTC,BIFI_USDT,BIFI_BTC,MDS_ETH,MDS_USDT,DGD_USDT,DGD_ETH,QASH_USDT,QASH_ETH,QASH_BTC,POWR_USDT,POWR_ETH,POWR_BTC,FIL_USDT,BCD_USDT,BCD_BTC,SBTC_USDT,SBTC_BTC,GOD_USDT,GOD_BTC,BCX_USDT,BCX_BTC,QSP_USDT,QSP_ETH,INK_BTC,INK_USDT,INK_ETH,INK_QTUM,QBT_QTUM,QBT_ETH,QBT_USDT,TSL_QTUM,TSL_USDT,GNX_USDT,GNX_ETH,NEO_USDT,GAS_USDT,NEO_BTC,GAS_BTC,IOTA_USDT,IOTA_BTC,NAS_USDT,NAS_ETH,NAS_BTC,ETH_BTC,ETC_BTC,ETC_ETH,ZEC_BTC,DASH_BTC,LTC_BTC,BCH_BTC,BTG_BTC,QTUM_BTC,QTUM_ETH,XRP_BTC,DOGE_BTC,XMR_BTC,ZRX_BTC,ZRX_ETH,DNT_ETH,DPY_ETH,OAX_BTC,OAX_USDT,OAX_ETH,REP_ETH,LRC_ETH,LRC_BTC,PST_ETH,BCDN_ETH,BCDN_USDT,TNT_ETH,SNT_ETH,SNT_BTC,BTM_ETH,BTM_BTC,SNET_ETH,SNET_USDT,LLT_SNET,OMG_ETH,OMG_BTC,PAY_ETH,PAY_BTC,BAT_ETH,BAT_BTC,CVC_ETH,STORJ_ETH,STORJ_BTC,EOS_ETH,EOS_BTC,BTS_USDT,BTS_BTC,TIPS_ETH,GT_BTC,GT_USDT,ATOM_BTC,ATOM_USDT,XEM_ETH,XEM_USDT,XEM_BTC,BU_USDT,BU_ETH,BU_BTC,BCHSV_USDT,BCHSV_CNYX,BCHSV_BTC,DCR_USDT,DCR_BTC,BCN_USDT,BCN_BTC,XMC_USDT,XMC_BTC,ATP_USDT,ATP_ETH,NAX_ETH,NBOT_ETH,NBOT_USDT,MED_USDT,MED_ETH,GRIN_USDT,GRIN_ETH,GRIN_BTC,BEAM_USDT,BEAM_ETH,BEAM_BTC,VTHO_ETH,BTT_USDT,BTT_ETH,BTT_TRX,TFUEL_ETH,TFUEL_USDT,CELR_ETH,CELR_USDT,CS_ETH,CS_USDT,MAN_ETH,MAN_USDT,REM_ETH,REM_USDT,LYM_ETH,LYM_BTC,LYM_USDT,ONG_ETH,ONG_USDT,ONT_ETH,ONT_USDT,BFT_ETH,BFT_USDT,IHT_ETH,IHT_USDT,SENC_ETH,SENC_USDT,TOMO_ETH,TOMO_USDT,ELEC_ETH,ELEC_USDT,HAV_ETH,HAV_USDT,SWTH_ETH,SWTH_USDT,NKN_ETH,NKN_USDT,SOUL_ETH,SOUL_USDT,LRN_ETH,LRN_USDT,EOSDAC_ETH,EOSDAC_USDT,DOCK_USDT,DOCK_ETH,GSE_USDT,GSE_ETH,RATING_USDT,RATING_ETH,HSC_USDT,HSC_ETH,HIT_USDT,HIT_ETH,DX_USDT,DX_ETH,CNNS_ETH,CNNS_USDT,DREP_ETH,DREP_USDT,MBL_USDT,MBL_ETH,GMAT_USDT,GMAT_ETH,MIX_USDT,MIX_ETH,LAMB_USDT,LAMB_ETH,LEO_USDT,LEO_BTC,WICC_USDT,WICC_ETH,SERO_USDT,SERO_ETH,VIDY_USDT,VIDY_ETH,KGC_USDT,FTM_USDT,FTM_ETH,COS_USDT,CRO_USDT,ALY_USDT,WIN_USDT,MTV_USDT,ONE_USDT,ARPA_USDT,ARPA_ETH,DILI_USDT,ALGO_USDT,PI_USDT,CKB_USDT,CKB_BTC,CKB_ETH,BKC_USDT,BXC_USDT,BXC_ETH,PAX_USDT,PAX_CNYX,USDC_CNYX,USDC_USDT,TUSD_CNYX,TUSD_USDT,HC_USDT,HC_BTC,HC_ETH,GARD_USDT,GARD_ETH,FTI_USDT,FTI_ETH,SOP_ETH,SOP_USDT,LEMO_USDT,LEMO_ETH,QKC_USDT,QKC_ETH,QKC_BTC,IOTX_USDT,IOTX_ETH,RED_USDT,RED_ETH,LBA_USDT,LBA_ETH,OPEN_USDT,OPEN_ETH,MITH_USDT,MITH_ETH,SKM_USDT,SKM_ETH,XVG_USDT,XVG_BTC,NANO_USDT,NANO_BTC,HT_USDT,BNB_USDT,MET_ETH,MET_USDT,TCT_ETH,TCT_USDT,MXC_USDT,MXC_BTC,MXC_ETH" - } - } - }, - "api": { - "authenticatedSupport": false, - "authenticatedWebsocketApiSupport": false, - "endpoints": { - "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" - }, - "credentials": { - "key": "Key", - "secret": "Secret" - }, - "credentialsValidator": { - "requiresKey": true, - "requiresSecret": true - } - }, - "features": { - "supports": { - "restAPI": true, - "restCapabilities": { - "tickerBatching": true, - "autoPairUpdates": true + "name": "GateIO", + "enabled": true, + "verbose": false, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketTrafficTimeout": 30000000000, + "websocketOrderbookBufferLimit": 5, + "baseCurrencies": "USD", + "currencyPairs": { + "requestFormat": { + "uppercase": true, + "delimiter": "_" }, - "websocketAPI": true, - "websocketCapabilities": {} + "configFormat": { + "uppercase": true, + "delimiter": "_" + }, + "useGlobalFormat": true, + "assetTypes": [ + "spot", + "option", + "futures", + "cross_margin", + "margin", + "delivery" + ], + "pairs": { + "spot": { + "enabled": "BTC_USDT,IHT_ETH,AME_ETH,CEUR_ETH,ALEPH_USDT,OMG_TRY,BTC_TRY,OGN_USDT,ALA_USDT", + "available": "IHT_ETH,AME_ETH,CEUR_ETH,ALEPH_USDT,OMG_TRY,BTC_TRY,OGN_USDT,ALA_USDT,HC_USDT,BTC_USDT,QNT_USDT,QTUM_ETH,MAHA_ETH,XCN_ETH,POOL_USDT,KGC_USDT,MCO2_USDT,HARD_USDT,GHNY_USDT,FTT_ETH,K21_ETH,FINE_USDT,REP_USDT,SBR_USDT,SKM_ETH,QLC_ETH,GAS_BTC,ALICE3L_USDT,BAO_USDT,FALCONS_USDT,ANT_USDT,VIDYX_USDT,DXCT_ETH,SMTY_ETH,HERO_USDT,SHARE_USDT,FIN_USDT,MTV_USDT,MOO_USDT,SMTY_USDT,ORAO_USDT,AE_ETH,SUSD_USDT,MAN_USDT,UNDEAD_USDT,MC_USDT,VET_USDT,WAXP_ETH,MDA_ETH,LYXE_USDT,SPS_USDT,STX_ETH,WSIENNA_USDT,NAOS_BTC,NFTX_USDT,OPUL_USDT,ICP3L_USDT,SFI_ETH,CTT_USDT,BSV3L_USDT,DFI_USDT,DIS_ETH,FET_USDT,ARG_USDT,VELO_USDT,NSBT_BTC,GSE_ETH,HNS_BTC,DOGEDASH_ETH,BACON_USDT,DUSK_USDT,MAPE_USDT,EGLD_ETH,TDROP_USDT,C983L_USDT,FAN_ETH,CZZ_USDT,FIU_USDT,SWRV_USDT,ONT_ETH,KINE_ETH,IMX_ETH,SPAY_ETH,CFG_BTC,RACA3S_USDT,UNO_ETH,DMLG_USDT,SAKE_ETH,ASM_USDT,CUSD_ETH,SUSD_ETH,ONC_USDT,DAI_USDT,VEGA_ETH,PYM_USDT,LTC_TRY,LOKA_USDT,NIF_USDT,BNC_USDT,PERL_ETH,MATIC3S_USDT,STMX_USDT,SKL_USDT,WLKN_USDT,XYO_ETH,AMPL3S_USDT,WEX_USDT,ULU_ETH,LIKE_ETH,INSUR_ETH,CAKE_ETH,SXP_ETH,COTI_USDT,ORT_USDT,RACA3L_USDT,GASDAO_USDT,AVA_USDT,OPA_USDT,ATS_USDT,VEGA_USDT,KILT_USDT,HIT_ETH,BRISE_USDT,SAUBER_USDT,SPS_ETH,FSN_USDT,EOS_ETH,KYL_USDT,REVV_ETH,SVT_ETH,XRP_USDT,DYDX3S_USDT,MANA3S_USDT,ICP_ETH,ALICE3S_USDT,PCX_USDT,LEMO_ETH,MKR_ETH,WOO3S_USDT,CART_ETH,MATIC_USDT,UNI_USD,MOBI_BTC,ICP3S_USDT,BEAM_BTC,CRO3S_USDT,FTT_USDT,IQ_ETH,TAP_USDT,MLT_USDT,RBN_USDT,AMPL3L_USDT,KINT_ETH,HECH_USDT,GAFI_ETH,WOO3L_USDT,TAI_USDT,HERA_USDT,AST_USDT,DHV_ETH,XAVA_USDT,LSS_USDT,SNX3S_USDT,PBR_USDT,XEND_ETH,SHR_ETH,PRQ_USDT,MATIC3L_USDT,WIT_ETH,LPOOL_USDT,PSP_USDT,BXC_USDT,CBK_USDT,REVO_BTC,MANA3L_USDT,ALPINE_USDT,DEGO_USDT,SIN_USDT,OCT_USDT,KZEN_USDT,L3P_USDT,FX_ETH,ONC_ETH,AXS_USD,BORA_USDT,XTZ_ETH,NEO3L_USDT,FROG_USDT,CHAMP_USDT,XNFT_USDT,BCH3S_USDT,FORT_USDT,XLM_TRY,TRX_TRY,CRPT_USDT,ROUTE_USDT,GLM_USDT,SLRS_ETH,TIMECHRONO_USDT,VRA_USDT,ONS_USDT,ZEC3L_USDT,KFT_ETH,TFD_ETH,FRA_USDT,RDN_ETH,BLANK_USDT,IOST3L_USDT,DDD_USDT,DOGE_USD,UNQ_USDT,API33S_USDT,AKRO_ETH,GITCOIN_USDT,THG_USDT,BDX_USDT,LTO_ETH,FLY_USDT,CREDIT_USDT,RENA_USDT,ZRX_ETH,CRP_ETH,NBOT_USDT,HT3L_USDT,DORA_ETH,LLT_SNET,ASD_USDT,XMR_USDT,SSV_BTC,FTM_USDT,XELS_USDT,MTL_ETH,ADX_ETH,API33L_USDT,PIG_USDT,RUNE_ETH,QRDO_BTC,THN_USDT,BCUG_USDT,EGG_ETH,GGM_USDT,HOTCROSS_USDT,SKYRIM_USDT,BTG_USDT,POT_USDT,CS_USDT,XVS_USDT,A5T_USDT,GOD_BTC,WAVES_USDT,LSK_BTC,BTT_TRY,YIN_USDT,PEOPLE_USDT,SPELL_ETH,POLC_USDT,BZZ3L_USDT,UNO_USDT,HDV_USDT,CELL_USDT,DAR_ETH,MIR_ETH,FODL_USDT,SRM_ETH,PROS_USDT,ORN_ETH,WAG_USDT,RBC_ETH,VENT_USDT,WND_USDT,AAA_ETH,BSCS_ETH,ZEC3S_USDT,DOS_USDT,HT3S_USDT,LAND_USDT,BCD_BTC,RING_USDT,FIRO_USDT,AUDIO_USDT,KUMA_USDT,SOLO_BTC,CRBN_USDT,MM_ETH,SAKE_USDT,XMARK_USDT,SLP_USDT,F2C_USDT,LUNA_USDT,ONIT_USDT,FTM3L_USDT,POPK_USDT,RFUEL_USDT,NEO3S_USDT,MIR_USDT,ETC_BTC,STETH_ETH,MANA_TRY,ALPACA_ETH,WAXL_USDT,EGS_USDT,DAR_USDT,KSM_USDT,XMARK_ETH,QTUM_USDT,C983S_USDT,INDI_ETH,DOGE3S_USDT,RVN_USDT,NOS_USDT,ALU_ETH,ALD_ETH,LUNC_USDT,ARES_ETH,BZZ3S_USDT,TNC_ETH,ONE_USDT,SENC_ETH,FTM3S_USDT,FLUX_USDT,STORJ_ETH,MTN_ETH,MNW_USDT,BLES_ETH,STG_ETH,LIME_ETH,WAGYU_USDT,XRP_TRY,XOR_ETH,ANGLE_USDT,DOGA_USDT,JFI_USDT,USDG_USDT,GRND_USDT,BOND_ETH,DMTR_USDT,YIN_ETH,ENJ_USDT,GOLDMINER_USDT,WIT_USDT,DOGE3L_USDT,FORM_USDT,LYXE_ETH,MLK_USDT,VR_USDT,DMS_USDT,LRC_TRY,ONX_USDT,ASK_USDT,ISP_ETH,TXT_USDT,IOEN_ETH,NIIFI_USDT,VRX_USDT,DOME_USDT,CTSI_USDT,ORBS_USDT,ZLW_ETH,FIL_USDT,FTI_ETH,CTK_USDT,ASR_USDT,GBPT_BTC,CBK_BTC,MBOX_ETH,RAM_USDT,IRIS_USDT,AME_USDT,KUB_USDT,ENV_USDT,RING_ETH,COTI3S_USDT,JULD_ETH,POLK_ETH,ACH3S_USDT,HYVE_ETH,MIX_ETH,RFT_USDT,ORAO_ETH,IHT_USDT,POLYPAD_USDT,CTRC_USDT,SFUND_USDT,MXC_BTC,DDD_BTC,CHESS_ETH,SHIB_USDT,SN_USDT,NFT_USDT,ASTRO_ETH,SOLO_USDT,TSHP_USDT,AMP_USDT,BTCST_ETH,VLXPAD_USDT,GAN_USDT,O3_USDT,WBTC_TRY,TULIP_USDT,GS_ETH,DX_ETH,NYZO_ETH,TT_USDT,SHILL_USDT,RATING_ETH,DUST_USDT,PSB_USDT,BFT1_USDT,GALA_ETH,XDC_USDT,LON3L_USDT,HE_USDT,ICE_ETH,LINK_ETH,SKU_USDT,QLC_USDT,DOMI_USDT,IDEA_USDT,METO_USDT,LIFE_ETH,ACH3L_USDT,POWR_ETH,VET_ETH,ALGO_USDT,BLIN_USDT,BAO_ETH,RBLS_USDT,TORN_ETH,VRT_USDT,BLANKV2_ETH,AUCTION_ETH,OLE_USDT,NWC_BTC,DOT5S_USDT,M RCH_ETH,SUNNY_ETH,GST_USDT,ENJ_TRY,KIBA_USDT,KLAP_USDT,SNTR_ETH,CELR_ETH,CHESS_USDT,XLM3L_USDT,LIQ_USDT,TRU_ETH,CHZ_USD,EPK_USDT,MED_ETH,BSCPAD_ETH,ZCN_USDT,AIOZ_ETH,FOR_ETH,CVC3L_USDT,MNY_USDT,SALT_USDT,CSTR_USDT,MPL_USDT,PLY_ETH,FIS_USDT,CHO_USDT,BICO_ETH,STOX_ETH,HIGH_USDT,SDAO_BTC,STEP_USD,CRV_BTC,SCRT_ETH,ROSE_USDT,SKILL_ETH,FRAX_USDT,BAGS_USDT,WIKEN_BTC,WOO_USDT,BBANK_ETH,SNX3L_USDT,XRD_ETH,VTHO_USDT,OKB3L_USDT,SAFEMOON_USDT,RAD_ETH,IOI_USDT,LAMB_USDT,CHZ_USDT,FAR_ETH,OKB3S_USDT,ELU_USDT,JGN_ETH,EOS3S_USDT,DBC_USDT,ATOM_USDT,ACH_ETH,LBLOCK_USDT,WZRD_USDT,OST_ETH,MEAN_USDT,IDEX_USDT,HOT_TRY,EWT_ETH,EMON_USDT,FXS_USDT,PSY_ETH,SIDUS_USDT,ATA_USDT,CVC3S_USDT,LOOKS_ETH,ALPA_ETH,CGG_ETH,CIR_ETH,PRT_ETH,LON3S_USDT,INJ_USDT,FIRE_ETH,MAHA_USDT,IOST3S_USDT,NU_ETH,LEO_BTC,VOXEL_USDT,CRV_USDT,EQX_USDT,WHALE_USDT,INJ_ETH,GRAP_USDT,AVAX3S_USDT,TIFI_USDT,C98_USDT,ERN_ETH,SUSHI_ETH,VET3S_USDT,KPAD_USDT,CRPT_ETH,CRO_USDT,AZY_USDT,LEMD_USDT,ETH2_ETH,BASE_ETH,TT_ETH,PERL_USDT,BANK_ETH,LST_ETH,PYR_ETH,RATIO_USDT,UMB_USDT,M ETALDR_USDT,SWINGBY_ETH,WICC_ETH,NUM_USDT,SHOE_USDT,BORING_ETH,SDN_USDT,GXS_BTC,ALICE_ETH,BRKL_USDT,GF_ETH,ELEC_USDT,SFG_USDT,COFIX_USDT,TIPS_ETH,FIL_BTC,CWAR_USDT,WILD_USDT,RENBTC_USDT,BNX_USDT,TRU_USDT,SWEAT_USDT,IOST_BTC,NVIR_USDT,1EARTH_USDT,ADAPAD_USDT,PPS_USDT,CUBE_USDT,DLC_USDT,DAFI_ETH,UNISTAKE_ETH,NFTL_USDT,ATOM_TRY,SHIB3S_USDT,BNB_USD,CNAME_USDT,GTH_ETH,ZCX_USDT,DYDX3L_USDT,ASTRO_USDT,GLQ_USDT,PROPS_USDT,AART_USDT,BTRST_ETH,KFT_USDT,AERGO_USDT,RUFF_ETH,EOS3L_USDT,API3_USDT,MINA_BTC,ETHA_ETH,AXIS_ETH,LOON_USDT,AVAX3L_USDT,VET3L_USDT,AE_USDT,SHX_USDT,LYM_USDT,DCR_BTC,LBK_USDT,QTC_USDT,LAVA_USDT,XCN_USDT,BRT_USDT,RSV_USDT,KIF_USDT,PSL_USDT,AZERO_USDT,LUNA_ETH,MILO_USDT,OGN_ETH,TOTM_USDT,BYN_ETH,MINA_USDT,PUNDIX_ETH,SRT_USDT,DG_ETH,IHC_USDT,SYS_ETH,TITA_USDT,COTI3L_USDT,DAG_USDT,DOT5L_USDT,TRADE_USDT,SHPING_USDT,NU_USDT,BLANK_ETH,PCNT_ETH,SCCP_USDT,POLS_USDT,NPT_USDT,MTA_USDT,YIELD_USDT,ZCN_ETH,DVP_ETH,KART_USDT,SYLO_USDT,MCRT_USDT,SPFC_USDT,BASE_USDT,ICX_USDT,PET_USDT,GZONE_USDT,RED_ETH,SBTC_USDT,BATH_ ETH,SOL_USD,NAFT_USDT,GMX_USDT,VADER_USDT,GTC_USDT,CVP_ETH,XRPBEAR_USDT,TIME_USDT,SXP_USDT,CITY_USDT,QASH_USDT,FAST_USDT,BCD_USDT,KNIGHT_USDT,BOO_ETH,ZODI_USDT,REI_USDT,PBX_ETH,SRM_USDT,LDO_ETH,ZEC_USDT,UFT_USDT,DAG_BTC,RIDE_USDT,ERN_USDT,T_USDT,CEEK_USDT,STI_USDT,IMX3S_USDT,ELA_USDT,MNGO_ETH,EHASH_ETH,BADGER_ETH,SUPE_USDT,AR3L_USDT,AUDIO_ETH,DOCK_ETH,QSP_USDT,FLM_USDT,AAVE3S_USDT,BOND_USDT,HT_USD,TARA_USDT,TRX_USDT,SPO_USDT,DSLA_USDT,LTC_BTC,DOGE_USDT,SLIM_ETH,ALN_ETH,CFX3S_USDT,FXS_ETH,RARE_ETH,VLXPAD_ETH,ETH_USD,SDN_BTC,QUICK_USDT,UTK_USDT,XPNET_USDT,TRB_USDT,LAZIO_USDT,FTM_TRY,ALPHA_ETH,CVC_ETH,WSG_USDT,UNI_ETH,DASH3L_USDT,BTL_USDT,CPOOL_USDT,MCG_USDT,SFP_ETH,REALM_USDT,RUFF_BTC,MOB_ETH,IBFK_USDT,ALPHA3S_USDT,BLOK_USDT,WIKEN_USDT,OMG3S_USDT,UTK_ETH,BCH5S_USDT,MED_USDT,REN_USD,MAN_ETH,SLND_ETH,CGG_USDT,CRE_USDT,SOURCE_USDT,ABT_USDT,DPET_USDT,WOM_USDT,FOREX_ETH,SNFT1_USDT,RIF_USDT,BENQI_USDT,XCV_ETH,GTC_BTC,ADA_TRY,LAT_USDT,ITGR_USDT,DLTA_USDT,SMT_USDT,APYS_USDT,MFT_ETH,ABT_ETH,STOX_USDT,ZRX_BTC,GMAT_USDT,R OOM_ETH,STORJ_BTC,RAZOR_USDT,RAGE_USDT,DOCK_USDT,RDN_USDT,MTR_USDT,NKN_USDT,SWASH_USDT,FX_USDT,POR_USDT,DENT_ETH,DERI_USDT,DFND_USDT,BLES_USDT,SLND_USDT,WNXM_ETH,CRTS_USDT,BTC3S_USDT,BKC_USDT,STEPG_ETH,THETA3L_USDT,NBS_BTC,AVAX_ETH,NANO_BTC,DEFILAND_ETH,LOOKS_USDT,BCX_BTC,BCH_USD,ETH3L_USDT,QLC_BTC,BCUG_ETH,RDF_USDT,DOGEDASH_USDT,ARSW_USDT,NEAR_ETH,QTCON_USDT,BABI_USDT,MBX_USDT,PNL_USDT,ODDZ_ETH,ATOM_BTC,XRP_BTC,BTCBULL_USDT,HMT_USDT,PORTO_USDT,STND_USDT,ETHW_ETH,LPT_USDT,LTC3L_USDT,TOKAU_USDT,QI_ETH,TVK_USDT,CWS_USDT,SWOP_USDT,WBTC_USDT,INSTAR_ETH,ICX_ETH,GALA5L_USDT,XTZ_BTC,AGS_USDT,TARA_BTC,DYDX_ETH,CATGIRL_USDT,SASHIMI_ETH,EPX_ETH,GCOIN_USDT,GDAO_USDT,MARS_ETH,OMG_USD,PMON_USDT,MNGO_USDT,TVK_ETH,SLG_USDT,MSOL_USDT,POWR_USDT,UOS_USDT,USDD_USDT,SLICE_USDT,ARRR_ETH,NSBT_USDT,STR_ETH,BEAM3L_USDT,BEL_USDT,MM_USDT,AXS_ETH,WEST_ETH,FTT3L_USDT,OMI_USDT,TIPS_USDT,SLC_ETH,SQUID_USDT,FEI_USDT,GEM_USDT,UMEE_USDT,DOGE_TRY,FCD_USDT,PVU_USDT,XED_ETH,LRN_ETH,NRFB_USDT,LION_USDT,BLACK_USDT,DOGE5S_USDT,CUDOS_USDT,PCNT_USDT ,OVR_USDT,ETC3S_USDT,CHR_ETH,MER_USDT,BOBA_USDT,FUEL_USDT,BAC_USDT,ONE3S_USDT,CONV_ETH,CDT_BTC,CELL_ETH,ASM_ETH,OPIUM_USDT,JST3L_USDT,BONDLY_USDT,RAZE_USDT,LIME_BTC,NFTX_ETH,PNK_ETH,LDO_USDT,DKS_USDT,ORO_USDT,LITH_USDT,ALPHR_ETH,INK_BTC,RLY_USDT,NEAR3S_USDT,XLM3S_USDT,AR_USDT,AKT_USDT,HCT_USDT,REEF_ETH,BZZ_USDT,SRM3L_USDT,AQDC_USDT,OPIUM_ETH,BAT_TRY,EWT_USDT,ALCX_ETH,CORN_USDT,HYDRA_USDT,RUNE_USD,STEP_USDT,CKB_BTC,MATTER_USDT,STSOL_ETH,CEEK_ETH,FXF_ETH,LIKE_USDT,HIT_USDT,LEO_USDT,COMP_USDT,BAL_USDT,LMR_USDT,AQT_USDT,BUY_ETH,LINK3S_USDT,ROOK_ETH,IMX_USDT,EFI_USDT,TAUR_USDT,OKT_ETH,GALO_USDT,MOOV_USDT,RUNE_USDT,TCP_USDT,ITEM_USDT,SCLP_USDT,RBC_USDT,SPI_USDT,ETC_USDT,RENBTC_BTC,CHICKS_USDT,KNOT_USDT,XEC3L_USDT,XCV_USDT,ETC_ETH,AAVE_TRY,APT_USDT,GNX_ETH,KISHU_USDT,AE_BTC,LIEN_USDT,CREAM_USDT,ATOM3S_USDT,OP_ETH,FORTH_ETH,PYR_USDT,KTN_ETH,TKO_ETH,METAG_USDT,ACE_USDT,CIR_USDT,BEAM_ETH,TCP_ETH,SRM_USD,CEL_USD,TRIBE3S_USDT,MESA_ETH,EVA_USDT,BBANK_USDT,BLANKV2_USDT,FORM_ETH,BAL3S_USDT,VISR_ETH,REVO_ETH,ALTB_USDT,KNC_US DT,GAS_USDT,SAFEMARS_USDT,TIP_USDT,VADER_ETH,NWC_USDT,VALUE_USDT,ATA_ETH,SSX_USDT,JOE_USDT,BAS_ETH,FITFI3S_USDT,BIT_USDT,QNT_ETH,RFOX_ETH,MSU_USDT,MSOL_ETH,CRV3L_USDT,OXT_USDT,SHFT_USDT,VERA_ETH,LYM_ETH,BP_USDT,KBOX_USDT,DOGNFT_ETH,PERP_USDT,VELO_ETH,SAO_USDT,DUCK2_USDT,DEFILAND_USDT,DUCK2_ETH,GLMR3L_USDT,SERO_ETH,MTS_USDT,STX_USDT,KEX_ETH,ZIG_USDT,CARDS_USDT,ANML_USDT,GALA_USDT,RAY3S_USDT,KAVA3L_USDT,GARD_USDT,GRT3L_USDT,BFC_USDT,NIFT_USDT,ORION_USDT,CTX_USDT,ASW_USDT,CERE_USDT,COMBO_ETH,MKR_USDT,MASK_USDT,MGA_USDT,AVAX_USDT,SKL3L_USDT,FRR_USDT,MV_USDT,BMI_ETH,SFIL_USDT,TEER_USDT,KLV_USDT,DMS_ETH,LBL_USDT,MKR3L_USDT,LEDU_BTC,XLM_BTC,MIST_ETH,OIN_USDT,CAKE_USDT,RNDR_USDT,STEPG_USDT,YCT_USDT,OPS_ETH,SHR_USDT,OXY_ETH" + }, + "option": { + "enabled": "BTC_USDT-20230217-28000-P,BTC_USDT-20221028-34000-P,BTC_USDT-20221028-40000-C", + "available": "BTC_USDT-20221028-26000-C,BTC_USDT-20221028-34000-P,BTC_USDT-20221028-40000-C,BTC_USDT-20221028-28000-P,BTC_USDT-20221028-34000-C,BTC_USDT-20221028-28000-C,BTC_USDT-20221028-36000-P,BTC_USDT-20221028-50000-P,BTC_USDT-20221028-36000-C,BTC_USDT-20221028-50000-C,BTC_USDT-20221028-21000-P,BTC_USDT-20221028-38000-P,BTC_USDT-20221028-21000-C,BTC_USDT-20221028-38000-C,BTC_USDT-20221028-23000-P,BTC_USDT-20221028-17000-P,BTC_USDT-20221028-23000-C,BTC_USDT-20221028-17000-C,BTC_USDT-20221028-25000-P,BTC_USDT-20221028-19000-P,BTC_USDT-20221028-25000-C,BTC_USDT-20221028-10000-P,BTC_USDT-20221028-19000-C,BTC_USDT-20221028-27000-P,BTC_USDT-20221028-10000-C,BTC_USDT-20221028-27000-C,BTC_USDT-20221028-12000-P,BTC_USDT-20221028-12000-C,BTC_USDT-20221028-20000-P,BTC_USDT-20221028-5000-P,BTC_USDT-20221028-14000-P,BTC_USDT-20221028-20000-C,BTC_USDT-20221028-45000-P,BTC_USDT-20221028-5000-C,BTC_USDT-20221028-14000-C,BTC_USDT-20221028-22000-P,BTC_USDT-20221028-45000-C,BTC_USDT-20221028-16000-P,BTC_USDT-20221028-22000-C,BTC_USDT-20221028-30000-P,BTC_USDT-20221028-16000-C,BTC_USDT-20221028-24000-P,BTC_USDT-20221028-30000-C,BTC_USDT-20221028-18000-P,BTC_USDT-20221028-24000-C,BTC_USDT-20221028-32000-P,BTC_USDT-20221028-18000-C,BTC_USDT-20221028-26000-P,BTC_USDT-20221028-32000-C,BTC_USDT-20221028-40000-P" + }, + "futures": { + "enabled": "ETH_USD,BTC_USD,KNC_USDT,OOKI_USDT,BIT_USDT,ZEC_USDT,SC_USDT,RVN_USDT,ICX_USDT", + "available": "ETH_USD,BTC_USD,KNC_USDT,OOKI_USDT,BIT_USDT,ZEC_USDT,SC_USDT,RVN_USDT,ICX_USDT,DUSK_USDT,BEL_USDT,REEF_USDT,ALCX_USDT,ASTR_USDT,INJ_USDT,CAKE_USDT,LAZIO_USDT,ONE_USDT,CEL_USDT,ETH_USDT,KLAY_USDT,COTI_USDT,MKISHU_USDT,MANA_USDT,MOVR_USDT,OMG_USDT,UNI_USDT,LTC_USDT,AAVE_USDT,DENT_USDT,QRDO_USDT,BNB_USDT,ALPHA_USDT,RAY_USDT,APE_USDT,CERE_USDT,STMX_USDT,XCN_USDT,OGN_USDT,OKB_USDT,DOT_USDT,TLM_USDT,BTM_USDT,ADA_USDT,ANKR_USDT,ANT_USDT,TRX_USDT,MTL_USDT,YFII_USDT,SUN_USDT,SAND_USDT,MBABYDOGE_USDT,WIN_USDT,LUNC_USDT,SRM_USDT,STG_USDT,BAT_USDT,AXS_USDT,SOL_USDT,MAKITA_USDT,BNT_USDT,BLZ_USDT,PSG_USDT,IOTA_USDT,BONK_USDT,RSR_USDT,PYR_USDT,FITFI_USDT,MKR_USDT,PERP_USDT,COMP_USDT,LINK_USDT,CHR_USDT,CFX_USDT,GARI_USDT,DGB_USDT,MBOX_USDT,WEMIX_USDT,DYDX_USDT,LUNA_USDT,HT_USDT,TRB_USDT,CTK_USDT,ACA_USDT,TFUEL_USDT,OCEAN_USDT,XLM_USDT,HOT_USDT,FTM_USDT,LPT_USDT,SOS_USDT,ALGO_USDT,SHIB_USDT,BSV_USDT,PORTO_USDT,SFP_USDT,SANTOS_USDT,BADGER_USDT,DAR_USDT,DEFI_USDT,XEM_USDT,ALICE_USDT,ICP_USDT,RARE_USDT,LRC_USDT,BAKE_USDT,FLUX_USDT,CRO_USDT,CVC_USDT,MINA_USDT,LIT_USDT,AUDIO_USDT,ZIL_USDT,XMR_USDT,FRONT_USDT,CTSI_USDT,AGLD_USDT,YGG_USDT,OP_USDT,ZRX_USDT,GT_USDT,XCH_USDT,VET_USDT,MOB_USDT,BICO_USDT,SLP_USDT,ACH_USDT,AR_USDT,CLV_USDT,IMX_USDT,SPELL_USDT,UNFI_USDT,SUSHI_USDT,FTT_USDT,HIGH_USDT,HNT_USDT,ALT_USDT,YFI_USDT,NEAR_USDT,NKN_USDT,XVS_USDT,BAND_USDT,LOKA_USDT,BCH_USDT,TOMO_USDT,WAVES_USDT,FIDA_USDT,DIA_USDT,ANC_USDT,CELO_USDT,CRV_USDT,FLM_USDT,GLMR_USDT,FIL_USDT,PEOPLE_USDT,WAXP_USDT,IOTX_USDT,ATOM_USDT,RLC_USDT,HBAR_USDT,REN_USDT,GMT_USDT,KAVA_USDT,KDA_USDT,GALA_USDT,STORJ_USDT,PUNDIX_USDT,BAL_USDT,XAUG_USDT,GRIN_USDT,SXP_USDT,AKRO_USDT,NEXO_USDT,CKB_USDT,API3_USDT,NEST_USDT,ETHW_USDT,TONCOIN_USDT,THETA_USDT,CREAM_USDT,BTC_USDT,GST_USDT,BEAM_USDT,HFT_USDT,KSM_USDT,RAD_USDT,QTUM_USDT,WOO_USDT,ATA_USDT,AVAX_USDT,EOS_USDT,SNX_USDT,AUCTION_USDT,XRP_USDT,GITCOIN_USDT,MATIC_USDT,ONT_USDT,LINA_USDT,DASH_USDT,MASK_USDT,ETC_USDT,JST_USDT,BSW_USDT,CONV_USDT,SKL_USDT,GAL_USDT,DODO_USDT,GRT_USDT,TRU_USDT,STX_USDT,CVX_USDT,JASMY_USDT,HIVE_USDT,EXCH_USDT,ROSE_USDT,SUPER_USDT,SCRT_USDT,USTC_USDT,ENJ_USDT,BTS_USDT,LOOKS_USDT,QNT_USDT,HOOK_USDT,FLOW_USDT,RUNE_USDT,APT_USDT,CHZ_USDT,DOGE_USDT,1INCH_USDT,PRIV_USDT,CSPR_USDT,C98_USDT,RACA_USDT,CELR_USDT,XEC_USDT,ENS_USDT,POND_USDT,NYM_USDT,PROM_USDT,IOST_USDT,ZEN_USDT,LDO_USDT,RNDR_USDT,REQ_USDT,DEGO_USDT,VRA_USDT,QUICK_USDT,VGX_USDT,XTZ_USDT,EGLD_USDT,POLS_USDT,ARPA_USDT,NFT_USDT" + }, + "cross_margin": { + "enabled": "BTC_USDT,ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT", + "available": "ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT,TARA_USDT,TRX_USDT,OXY_USDT,LON_USDT,DOGE_USDT,ISP_USDT,TWT_USDT,BAO_USDT,QUACK_USDT,ANT_USDT,VGX_USDT,ARPA_USDT,QUICK_USDT,UTK_USDT,HERO_USDT,WSG_USDT,BICO_USDT,MTV_USDT,VET_USDT,GARI_USDT,BCH_USDT,KLAY_USDT,WING_USDT,BLOK_USDT,SPS_USDT,WIKEN_USDT,WSIENNA_USDT,PUNDIX_USDT,FIC_USDT,ASTR_USDT,FET_USDT,VELO_USDT,BENQI_USDT,CWEB_USDT,RIF_USDT,UNI_USDT,ONG_USDT,ERG_USDT,ALPHA_USDT,CELO_USDT,XVG_USDT,GMAT_USDT,BTS_USDT,DOCK_USDT,GMT_USDT,DIA_USDT,CSPR_USDT,NKN_USDT,STAKE_USDT,SWASH_USDT,XEC_USDT,SWRV_USDT,QRDO_USDT,BLES_USDT,EOS_USDT,GRT_USDT,ASM_USDT,FIL6_USDT,GNO_USDT,EGLD_USDT,XYM_USDT,LOOKS_USDT,LOKA_USDT,BNC_USDT,BAS_USDT,SKL_USDT,STMX_USDT,CVC_USDT,DDOS_USDT,COTI_USDT,AVA_USDT,HMT_USDT,DF_USDT,LPT_USDT,XRP_USDT,TVK_USDT,FEVR_USDT,MBL_USDT,KIN_USDT,SPELL_USDT,MATIC_USDT,FTT_USDT,NMR_USDT,PMON_USDT,BNB_USDT,USDD_USDT,LSS_USDT,MDX_USDT,PRQ_USDT,ALPINE_USDT,DEGO_USDT,OMI_USDT,TIPS_USDT,OCT_USDT,FEI_USDT,UMEE_USDT,CRP_USDT,LION_USDT,YFI_USDT,DASH_USDT,REQ_USDT,SDAO_USDT,PNT_USDT,INSUR_USDT,OOKI_USDT,SUN_USDT,CRPT_USDT,BAC_USDT,DATA_USDT,LRN_USDT,JGN_USDT,KIMCHI_USDT,SUKU_USDT,VRA_USDT,AAVE_USDT,FTI_USDT,LDO_USDT,FRA_USDT,BLANK_USDT,NEAR_USDT,ZKS_USDT,MTRG_USDT,RLY_USDT,TCT_USDT,FLY_USDT,JST_USDT,YFII_USDT,AR_USDT,POLY_USDT,JULD_USDT,SOL_USDT,BZZ_USDT,AXS_USDT,ASD_USDT,XMR_USDT,FTM_USDT,HIT_USDT,LEO_USDT,LIT_USDT,PIG_USDT,COMP_USDT,ELON_USDT,IMX_USDT,EFI_USDT,XVS_USDT,WAVES_USDT,PEOPLE_USDT,SOS_USDT,RUNE_USDT,POLC_USDT,SCLP_USDT,BABYDOGE_USDT,KONO_USDT,SPI_USDT,ETC_USDT,MDA_USDT,MTL_USDT,BCHA_USDT,KISHU_USDT,SUNNY_USDT,PYR_USDT,XTZ_USDT,TRIBE_USDT,AUDIO_USDT,FIRO_USDT,MANA_USDT,OKB_USDT,DOG_USDT,SLP_USDT,KNC_USDT,GAS_USDT,LUNA_USDT,SAFEMARS_USDT,MIR_USDT,DAR_USDT,EGS_USDT,KSM_USDT,ATP_USDT,BIT_USDT,STORJ_USDT,XEM_USDT,QTUM_USDT,AGLD_USDT,RVN_USDT,OXT_USDT,SHFT_USDT,IOTX_USDT,LUNC_USDT,NEXO_USDT,AKITA_USDT,PERP_USDT,ONE_USDT,ETH_USDT,FLUX_USDT,FLOKI_USDT,STX_USDT,ANML_USDT,XPRT_USDT,GALA_USDT,GXS_USDT,TORN_USDT,KAI_USDT,1INCH_USDT,CHR_USDT,GAL_USDT,GLMR_USDT,CTX_USDT,CERE_USDT,CART_USDT,STRAX_USDT,MASK_USDT,MKR_USDT,AVAX_USDT,ENJ_USDT,YAM_USDT,ALPACA_USDT,DODO_USDT,MFT_USDT,CAKE_USDT,RNDR_USDT,CTSI_USDT,GRIN_USDT,MXC_USDT,ONT_USDT,ANKR_USDT,SLIM_USDT,FIL_USDT,CTK_USDT,ASR_USDT,FEG_USDT,SERO_USDT,RSS3_USDT,IRIS_USDT,XCH_USDT,ZRX_USDT,BAND_USDT,BADGER_USDT,DAO_USDT,EPS_USDT,THETA_USDT,BAKE_USDT,SHIB_USDT,MBOX_USDT,NBS_USDT,SNT_USDT,DREP_USDT,NFT_USDT,AUCTION_USDT,BOSON_USDT,O3_USDT,NULS_USDT,OMG_USDT,PEARL_USDT,HAPI_USDT,STG_USDT,IDV_USDT,HORD_USDT,ZIL_USDT,SUPER_USDT,DENT_USDT,REN_USDT,RAI_USDT,ZEN_USDT,ALGO_USDT,BLZ_USDT,BOR_USDT,SC_USDT,HEGIC_USDT,MOB_USDT,DORA_USDT,FOR_USDT,FLOW_USDT,RARI_USDT,DYDX_USDT,ATLAS_USDT,GST_USDT,REEF_USDT,HT_USDT,XYO_USDT,CHESS_USDT,BAT_USDT,NYM_USDT,RAMP_USDT,USDC_USDT,ICP_USDT,EPK_USDT,EXRD_USDT,DOT_USDT,COOK_USDT,CKB_USDT,YGG_USDT,CRU_USDT,ANC_USDT,FIS_USDT,ALCX_USDT,HIGH_USDT,BEAM_USDT,BSW_USDT,STAR_USDT,ROSE_USDT,CNNS_USDT,BZRX_USDT,WOO_USDT,SAFEMOON_USDT,VTHO_USDT,OM_USDT,LAMB_USDT,CHZ_USDT,AIOZ_USDT,EDEN_USDT,POND_USDT,ATOM_USDT,UNFI_USDT,FORTH_USDT,MLN_USDT,NEO_USDT,MOVR_USDT,RLC_USDT,FXS_USDT,ENS_USDT,ATA_USDT,XPR_USDT,NEST_USDT,XLM_USDT,AUTO_USDT,SNX_USDT,OCN_USDT,RSR_USDT,MITH_USDT,KAR_USDT,INJ_USDT,PLA_USDT,CYS_USDT,WAXP_USDT,VOXEL_USDT,CRV_USDT,FITFI_USDT,WHALE_USDT,WRX_USDT,TIDAL_USDT,C98_USDT,HNT_USDT,TONCOIN_USDT,DOGGY_USDT,SYS_USDT,NPXS_USDT,CRO_USDT,LEMD_USDT,RAY_USDT,PERL_USDT,CQT_USDT,CFX_USDT,TOMO_USDT,ACA_USDT,SDN_USDT,OKT_USDT,WILD_USDT,BNX_USDT,TRU_USDT,RACA_USDT,SWEAT_USDT,ACH_USDT,AKRO_USDT,BTM_USDT,TKO_USDT,GT_USDT,OCEAN_USDT,WNCG_USDT,BSV_USDT,GHST_USDT,CELR_USDT,LINA_USDT,SAND_USDT,APE_USDT,WICC_USDT,FIDA_USDT,ADA_USDT,PROPS_USDT,METIS_USDT,KAVA_USDT,AERGO_USDT,CONV_USDT,TFUEL_USDT,FRONT_USDT,API3_USDT,FARM_USDT,AE_USDT,LRC_USDT,IOTA_USDT,RFOX_USDT,PHA_USDT,XCN_USDT,NAS_USDT,KEEP_USDT,VIDY_USDT,HOT_USDT,MINA_USDT,ETHW_USDT,ALICE_USDT,HAI_USDT,BTC_USDT,LTC_USDT,LTO_USDT,DC_USDT,NU_USDT,IOST_USDT,RAD_USDT,POLS_USDT,OP_USDT,WXT_USDT,STR_USDT,YIELD_USDT,GM_USDT,SPA_USDT,BTCST_USDT,WEMIX_USDT,CLV_USDT,ICX_USDT,PET_USDT,STARL_USDT,HBAR_USDT,REDTOKEN_USDT,BTT_USDT,LINK_USDT,TLM_USDT,ARES_USDT,GTC_USDT,SUSHI_USDT,KEY_USDT,ALN_USDT,KDA_USDT,DVI_USDT,SXP_USDT,MAPS_USDT,BCD_USDT,SRM_USDT,WIN_USDT,ZEC_USDT,JASMY_USDT" + }, + "margin": { + "enabled": "BTC_USDT,ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT", + "available": "BTC_USDT,ERN_USDT,T_USDT,CEEK_USDT,OGN_USDT,QNT_USDT,WOZX_USDT,ZEE_USDT,FUN_USDT,FLM_USDT,BOND_USDT,TARA_USDT,TRX_USDT,OXY_USDT,LON_USDT,DOGE_USDT,ISP_USDT,TWT_USDT,BAO_USDT,QUACK_USDT,ANT_USDT,VGX_USDT,ARPA_USDT,QUICK_USDT,UTK_USDT,HERO_USDT,WSG_USDT,BICO_USDT,MTV_USDT,VET_USDT,GARI_USDT,BCH_USDT,KLAY_USDT,WING_USDT,BLOK_USDT,SPS_USDT,WIKEN_USDT,WSIENNA_USDT,PUNDIX_USDT,FIC_USDT,ASTR_USDT,FET_USDT,VELO_USDT,BENQI_USDT,CWEB_USDT,RIF_USDT,UNI_USDT,ONG_USDT,ERG_USDT,ALPHA_USDT,CELO_USDT,XVG_USDT,GMAT_USDT,BTS_USDT,DOCK_USDT,GMT_USDT,DIA_USDT,CSPR_USDT,NKN_USDT,STAKE_USDT,SWASH_USDT,XEC_USDT,SWRV_USDT,QRDO_USDT,BLES_USDT,EOS_USDT,GRT_USDT,ASM_USDT,FIL6_USDT,GNO_USDT,EGLD_USDT,XYM_USDT,LOOKS_USDT,LOKA_USDT,BNC_USDT,BAS_USDT,SKL_USDT,STMX_USDT,CVC_USDT,DDOS_USDT,COTI_USDT,AVA_USDT,HMT_USDT,DF_USDT,LPT_USDT,XRP_USDT,TVK_USDT,FEVR_USDT,MBL_USDT,KIN_USDT,SPELL_USDT,MATIC_USDT,FTT_USDT,NMR_USDT,PMON_USDT,BNB_USDT,USDD_USDT,LSS_USDT,MDX_USDT,PRQ_USDT,ALPINE_USDT,DEGO_USDT,OMI_USDT,TIPS_USDT,OCT_USDT,FEI_USDT,UMEE_USDT,CRP_USDT,LION_USDT,YFI_USDT,DASH_USDT,REQ_USDT,SDAO_USDT,PNT_USDT,INSUR_USDT,OOKI_USDT,SUN_USDT,CRPT_USDT,BAC_USDT,DATA_USDT,LRN_USDT,JGN_USDT,KIMCHI_USDT,SUKU_USDT,VRA_USDT,AAVE_USDT,FTI_USDT,LDO_USDT,FRA_USDT,BLANK_USDT,NEAR_USDT,ZKS_USDT,MTRG_USDT,RLY_USDT,TCT_USDT,FLY_USDT,JST_USDT,YFII_USDT,AR_USDT,POLY_USDT,JULD_USDT,SOL_USDT,BZZ_USDT,AXS_USDT,ASD_USDT,XMR_USDT,FTM_USDT,HIT_USDT,LEO_USDT,LIT_USDT,PIG_USDT,COMP_USDT,ELON_USDT,IMX_USDT,EFI_USDT,XVS_USDT,WAVES_USDT,PEOPLE_USDT,SOS_USDT,RUNE_USDT,POLC_USDT,SCLP_USDT,BABYDOGE_USDT,KONO_USDT,SPI_USDT,ETC_USDT,MDA_USDT,MTL_USDT,BCHA_USDT,KISHU_USDT,SUNNY_USDT,PYR_USDT,XTZ_USDT,TRIBE_USDT,AUDIO_USDT,FIRO_USDT,MANA_USDT,OKB_USDT,DOG_USDT,SLP_USDT,KNC_USDT,GAS_USDT,LUNA_USDT,SAFEMARS_USDT,MIR_USDT,DAR_USDT,EGS_USDT,KSM_USDT,ATP_USDT,BIT_USDT,STORJ_USDT,XEM_USDT,QTUM_USDT,AGLD_USDT,RVN_USDT,OXT_USDT,SHFT_USDT,IOTX_USDT,LUNC_USDT,NEXO_USDT,AKITA_USDT,PERP_USDT,ONE_USDT,ETH_USDT,FLUX_USDT,FLOKI_USDT,STX_USDT,ANML_USDT,XPRT_USDT,GALA_USDT,GXS_USDT,TORN_USDT,KAI_USDT,1INCH_USDT,CHR_USDT,GAL_USDT,GLMR_USDT,CTX_USDT,CERE_USDT,CART_USDT,STRAX_USDT,MASK_USDT,MKR_USDT,AVAX_USDT,ENJ_USDT,YAM_USDT,ALPACA_USDT,DODO_USDT,MFT_USDT,CAKE_USDT,RNDR_USDT,CTSI_USDT,GRIN_USDT,MXC_USDT,ONT_USDT,ANKR_USDT,SLIM_USDT,FIL_USDT,CTK_USDT,ASR_USDT,FEG_USDT,SERO_USDT,RSS3_USDT,IRIS_USDT,XCH_USDT,ZRX_USDT,BAND_USDT,BADGER_USDT,DAO_USDT,EPS_USDT,THETA_USDT,BAKE_USDT,SHIB_USDT,MBOX_USDT,NBS_USDT,SNT_USDT,DREP_USDT,NFT_USDT,AUCTION_USDT,BOSON_USDT,O3_USDT,NULS_USDT,OMG_USDT,PEARL_USDT,HAPI_USDT,STG_USDT,IDV_USDT,HORD_USDT,ZIL_USDT,SUPER_USDT,DENT_USDT,REN_USDT,RAI_USDT,ZEN_USDT,ALGO_USDT,BLZ_USDT,BOR_USDT,SC_USDT,HEGIC_USDT,MOB_USDT,DORA_USDT,FOR_USDT,FLOW_USDT,RARI_USDT,DYDX_USDT,ATLAS_USDT,GST_USDT,REEF_USDT,HT_USDT,XYO_USDT,CHESS_USDT,BAT_USDT,NYM_USDT,RAMP_USDT,USDC_USDT,ICP_USDT,EPK_USDT,EXRD_USDT,DOT_USDT,COOK_USDT,CKB_USDT,YGG_USDT,CRU_USDT,ANC_USDT,FIS_USDT,ALCX_USDT,HIGH_USDT,BEAM_USDT,BSW_USDT,STAR_USDT,ROSE_USDT,CNNS_USDT,BZRX_USDT,WOO_USDT,SAFEMOON_USDT,VTHO_USDT,OM_USDT,LAMB_USDT,CHZ_USDT,AIOZ_USDT,EDEN_USDT,POND_USDT,ATOM_USDT,UNFI_USDT,FORTH_USDT,MLN_USDT,NEO_USDT,MOVR_USDT,RLC_USDT,FXS_USDT,ENS_USDT,ATA_USDT,XPR_USDT,NEST_USDT,XLM_USDT,AUTO_USDT,SNX_USDT,OCN_USDT,RSR_USDT,MITH_USDT,KAR_USDT,INJ_USDT,PLA_USDT,CYS_USDT,WAXP_USDT,VOXEL_USDT,CRV_USDT,FITFI_USDT,WHALE_USDT,WRX_USDT,TIDAL_USDT,C98_USDT,HNT_USDT,TONCOIN_USDT,DOGGY_USDT,SYS_USDT,NPXS_USDT,CRO_USDT,LEMD_USDT,RAY_USDT,PERL_USDT,CQT_USDT,CFX_USDT,TOMO_USDT,ACA_USDT,SDN_USDT,OKT_USDT,WILD_USDT,BNX_USDT,TRU_USDT,RACA_USDT,SWEAT_USDT,ACH_USDT,AKRO_USDT,BTM_USDT,TKO_USDT,GT_USDT,OCEAN_USDT,WNCG_USDT,BSV_USDT,GHST_USDT,CELR_USDT,LINA_USDT,SAND_USDT,APE_USDT,WICC_USDT,FIDA_USDT,ADA_USDT,PROPS_USDT,METIS_USDT,KAVA_USDT,AERGO_USDT,CONV_USDT,TFUEL_USDT,FRONT_USDT,API3_USDT,FARM_USDT,AE_USDT,LRC_USDT,IOTA_USDT,RFOX_USDT,PHA_USDT,XCN_USDT,NAS_USDT,KEEP_USDT,VIDY_USDT,HOT_USDT,MINA_USDT,ETHW_USDT,ALICE_USDT,HAI_USDT,LTC_USDT,LTO_USDT,DC_USDT,NU_USDT,IOST_USDT,RAD_USDT,POLS_USDT,OP_USDT,WXT_USDT,STR_USDT,YIELD_USDT,GM_USDT,SPA_USDT,BTCST_USDT,WEMIX_USDT,CLV_USDT,ICX_USDT,PET_USDT,STARL_USDT,HBAR_USDT,REDTOKEN_USDT,BTT_USDT,LINK_USDT,TLM_USDT,ARES_USDT,GTC_USDT,SUSHI_USDT,KEY_USDT,ALN_USDT,KDA_USDT,DVI_USDT,SXP_USDT,MAPS_USDT,BCD_USDT,SRM_USDT,WIN_USDT,ZEC_USDT,JASMY_USDT" + }, + "delivery": { + "enabled": "BTC_USD_20230331,BTC_USD_20221230,BTC_USDT_20221021,BTC_USDT_20221014", + "available": "BTC_USD_20221021,BTC_USD_20221014,BTC_USD_20230331,BTC_USD_20221230,BTC_USDT_20221021,BTC_USDT_20221014,BTC_USDT_20230331,BTC_USDT_20221230" + } + } }, - "enabled": { - "autoPairUpdates": true, - "websocketAPI": false - } - }, - "bankAccounts": [ - { - "enabled": false, - "bankName": "", - "bankAddress": "", - "bankPostalCode": "", - "bankPostalCity": "", - "bankCountry": "", - "accountName": "", - "accountNumber": "", - "swiftCode": "", - "iban": "", - "supportedCurrencies": "" - } - ] + "api": { + "authenticatedSupport": false, + "authenticatedWebsocketApiSupport": false, + "endpoints": { + "url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "websocketURL": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + }, + "credentials": { + "key": "Key", + "secret": "Secret" + }, + "credentialsValidator": { + "requiresKey": true, + "requiresSecret": true + } + }, + "features": { + "supports": { + "restAPI": true, + "restCapabilities": { + "tickerBatching": true, + "autoPairUpdates": true + }, + "websocketAPI": true, + "websocketCapabilities": {} + }, + "enabled": { + "autoPairUpdates": true, + "websocketAPI": true + } + }, + "bankAccounts": [ + { + "enabled": false, + "bankName": "", + "bankAddress": "", + "bankPostalCode": "", + "bankPostalCity": "", + "bankCountry": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] }, { "name": "Gemini",