diff --git a/exchanges/huobi/cfutures_types.go b/exchanges/huobi/cfutures_types.go index d2d84d0c1f8..031b1708b6a 100644 --- a/exchanges/huobi/cfutures_types.go +++ b/exchanges/huobi/cfutures_types.go @@ -1,5 +1,7 @@ package huobi +import "github.com/thrasher-corp/gocryptotrader/types" + // WsSwapReqKline stores req kline data for swap websocket type WsSwapReqKline struct { Rep string `json:"rep"` @@ -19,22 +21,22 @@ type WsSwapReqKline struct { // WsSwapReqTradeDetail stores requested trade detail data for swap websocket type WsSwapReqTradeDetail struct { - Rep string `json:"rep"` - ID int64 `json:"id"` - Timestamp int64 `json:"ts"` + Rep string `json:"rep"` + ID int64 `json:"id"` + Timestamp types.Time `json:"ts"` Data []struct { - ID int64 `json:"id"` - Price float64 `json:"price"` - Amount float64 `json:"amount"` - Direction string `json:"direction"` - Timestamp int64 `json:"ts"` + ID int64 `json:"id"` + Price float64 `json:"price"` + Amount float64 `json:"amount"` + Direction string `json:"direction"` + Timestamp types.Time `json:"ts"` } `json:"data"` } // SwapWsSubPremiumKline stores subscribed premium kline data for futures websocket type SwapWsSubPremiumKline struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { ID int64 `json:"id"` Volume float64 `json:"vol"` @@ -49,10 +51,10 @@ type SwapWsSubPremiumKline struct { // SwapWsReqPremiumKline stores requested premium kline data for futures websocket type SwapWsReqPremiumKline struct { - Rep string `json:"rep"` - ID string `json:"id"` - WsID int64 `json:"wsid"` - Timestamp int64 `json:"ts"` + Rep string `json:"rep"` + ID string `json:"id"` + WsID int64 `json:"wsid"` + Timestamp types.Time `json:"ts"` Data []struct { Volume float64 `json:"vol"` Count float64 `json:"count"` @@ -67,8 +69,8 @@ type SwapWsReqPremiumKline struct { // SwapWsSubEstimatedFunding stores estimated funding rate data for swap websocket type SwapWsSubEstimatedFunding struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { ID int64 `json:"id"` Volume float64 `json:"vol,string"` @@ -83,10 +85,10 @@ type SwapWsSubEstimatedFunding struct { // SwapWsReqEstimatedFunding stores requested estimated funding data for swap websocket type SwapWsReqEstimatedFunding struct { - Rep string `json:"rep"` - ID string `json:"id"` - WsID int64 `json:"wsid"` - Timestamp int64 `json:"ts"` + Rep string `json:"rep"` + ID string `json:"id"` + WsID int64 `json:"wsid"` + Timestamp types.Time `json:"ts"` Data []struct { Volume float64 `json:"vol,string"` Count float64 `json:"count,string"` @@ -101,8 +103,8 @@ type SwapWsReqEstimatedFunding struct { // SwapWsSubBasisData stores subscribed basis data for swap websocket type SwapWsSubBasisData struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick []struct { ID int64 `json:"id"` ContractPrice float64 `json:"contract_price,string"` @@ -114,10 +116,10 @@ type SwapWsSubBasisData struct { // SwapWsReqBasisData stores requested basis data for swap websocket type SwapWsReqBasisData struct { - Rep string `json:"rep"` - ID string `json:"id"` - WsID int64 `json:"wsid"` - Timestamp int64 `json:"ts"` + Rep string `json:"rep"` + ID string `json:"id"` + WsID int64 `json:"wsid"` + Timestamp types.Time `json:"ts"` Data []struct { ID int64 `json:"id"` ContractPrice float64 `json:"contract_price"` @@ -129,33 +131,33 @@ type SwapWsReqBasisData struct { // SwapWsSubOrderData stores subscribed order data for swap websocket type SwapWsSubOrderData struct { - Operation string `json:"op"` - Topic string `json:"topic"` - UID string `json:"uid"` - Timestamp int64 `json:"ts"` - Symbol string `json:"symbol"` - ContractCode string `json:"contract_code"` - Volume float64 `json:"volume"` - Price float64 `json:"price"` - OrderPriceType string `json:"order_price_type"` - Direction string `json:"direction"` - Offset string `json:"offset"` - Status int64 `json:"status"` - LeverateRate float64 `json:"lever_rate"` - OrderID int64 `json:"order_id"` - OrderIDString string `json:"order_id_str"` - ClientOrderID int64 `json:"client_order_id"` - OrderSource string `json:"order_source"` - OrderType int64 `json:"order_type"` - CreatedAt int64 `json:"created_at"` - CanceledAt int64 `json:"canceled_at"` - TradeVolume float64 `json:"trade_volume"` - TradeTurnover float64 `json:"trade_turnover"` - Fee float64 `json:"fee"` - FeeAsset string `json:"fee_asset"` - TradeAvgPrice float64 `json:"trade_avg_price"` - MarginFrozen float64 `json:"margin_frozen"` - Profit float64 `json:"profit"` + Operation string `json:"op"` + Topic string `json:"topic"` + UID string `json:"uid"` + Timestamp types.Time `json:"ts"` + Symbol string `json:"symbol"` + ContractCode string `json:"contract_code"` + Volume float64 `json:"volume"` + Price float64 `json:"price"` + OrderPriceType string `json:"order_price_type"` + Direction string `json:"direction"` + Offset string `json:"offset"` + Status int64 `json:"status"` + LeverateRate float64 `json:"lever_rate"` + OrderID int64 `json:"order_id"` + OrderIDString string `json:"order_id_str"` + ClientOrderID int64 `json:"client_order_id"` + OrderSource string `json:"order_source"` + OrderType int64 `json:"order_type"` + CreatedAt int64 `json:"created_at"` + CanceledAt int64 `json:"canceled_at"` + TradeVolume float64 `json:"trade_volume"` + TradeTurnover float64 `json:"trade_turnover"` + Fee float64 `json:"fee"` + FeeAsset string `json:"fee_asset"` + TradeAvgPrice float64 `json:"trade_avg_price"` + MarginFrozen float64 `json:"margin_frozen"` + Profit float64 `json:"profit"` Trade []struct { ID string `json:"id"` TradeID int64 `json:"trade_id"` @@ -172,19 +174,19 @@ type SwapWsSubOrderData struct { // SwapWsSubMatchOrderData stores subscribed match order data for swap websocket type SwapWsSubMatchOrderData struct { - Operation string `json:"op"` - Topic string `json:"topic"` - UID string `json:"uid"` - Timestamp int64 `json:"ts"` - Symbol string `json:"symbol"` - ContractCode string `json:"contract_code"` - Status int64 `json:"status"` - OrderID int64 `json:"order_id"` - OrderIDString string `json:"order_id_str"` - ClientOrderID int64 `json:"client_order_id"` - OrderType string `json:"order_type"` - TradeVolume int64 `json:"trade_volume"` - Volume float64 `json:"volume"` + Operation string `json:"op"` + Topic string `json:"topic"` + UID string `json:"uid"` + Timestamp types.Time `json:"ts"` + Symbol string `json:"symbol"` + ContractCode string `json:"contract_code"` + Status int64 `json:"status"` + OrderID int64 `json:"order_id"` + OrderIDString string `json:"order_id_str"` + ClientOrderID int64 `json:"client_order_id"` + OrderType string `json:"order_type"` + TradeVolume int64 `json:"trade_volume"` + Volume float64 `json:"volume"` Trade []struct { ID string `json:"id"` TradeID int64 `json:"trade_id"` @@ -198,11 +200,11 @@ type SwapWsSubMatchOrderData struct { // SwapWsSubEquityData stores subscribed account data for swap account equity updates through websocket type SwapWsSubEquityData struct { - Operation string `json:"op"` - Topic string `json:"topic"` - Timestamp int64 `json:"ts"` - UID string `json:"uid"` - Event string `json:"event"` + Operation string `json:"op"` + Topic string `json:"topic"` + Timestamp types.Time `json:"ts"` + UID string `json:"uid"` + Event string `json:"event"` Data []struct { Symbol string `json:"symbol"` MarginBalance float64 `json:"margin_balance"` @@ -222,11 +224,11 @@ type SwapWsSubEquityData struct { // SwapWsSubPositionUpdates stores subscribed position updates data for swap websocket type SwapWsSubPositionUpdates struct { - Operation string `json:"op"` - Topic string `json:"topic"` - UID string `json:"uid"` - Timestamp int64 `json:"ts"` - Event string `json:"event"` + Operation string `json:"op"` + Topic string `json:"topic"` + UID string `json:"uid"` + Timestamp types.Time `json:"ts"` + Event string `json:"event"` Data []struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` @@ -247,9 +249,9 @@ type SwapWsSubPositionUpdates struct { // SwapWsSubLiquidationOrders stores subscribed liquidation orders data for swap futures type SwapWsSubLiquidationOrders struct { - Operation string `json:"op"` - Topic string `json:"topic"` - Timestamp int64 `json:"ts"` + Operation string `json:"op"` + Topic string `json:"topic"` + Timestamp types.Time `json:"ts"` OrdersData []struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` @@ -263,9 +265,9 @@ type SwapWsSubLiquidationOrders struct { // SwapWsSubFundingData stores funding rate data for swap websocket type SwapWsSubFundingData struct { - Operation string `json:"op"` - Topic string `json:"topic"` - Timestamp int64 `json:"ts"` + Operation string `json:"op"` + Topic string `json:"topic"` + Timestamp types.Time `json:"ts"` FundingData []struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` @@ -279,10 +281,10 @@ type SwapWsSubFundingData struct { // SwapWsSubContractInfo stores funding rate data for swap websocket type SwapWsSubContractInfo struct { - Operation string `json:"op"` - Topic string `json:"topic"` - Timestamp int64 `json:"ts"` - Event string `json:"event"` + Operation string `json:"op"` + Topic string `json:"topic"` + Timestamp types.Time `json:"ts"` + Event string `json:"event"` ContractData []struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` @@ -330,9 +332,9 @@ type SwapWsSubTriggerOrderUpdates struct { // SwapIndexPriceData gets price of a perpetual swap type SwapIndexPriceData struct { Data []struct { - ContractCode string `json:"contract_code"` - IndexPrice float64 `json:"index_price"` - IndexTimestamp int64 `json:"index_ts"` + ContractCode string `json:"contract_code"` + IndexPrice float64 `json:"index_price"` + IndexTimestamp types.Time `json:"index_ts"` } `json:"data"` } @@ -364,7 +366,7 @@ type SwapMarketDepthData struct { Channel string `json:"ch"` ID int64 `json:"id"` MRID int64 `json:"mrid"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Version int64 `json:"version"` } `json:"tick"` } @@ -372,14 +374,14 @@ type SwapMarketDepthData struct { // SwapKlineData stores kline data for perpetual swaps type SwapKlineData struct { Data []struct { - Volume float64 `json:"vol"` - Close float64 `json:"close"` - Count float64 `json:"count"` - High float64 `json:"high"` - IDTimestamp int64 `json:"id"` - Low float64 `json:"low"` - Open float64 `json:"open"` - Amount float64 `json:"amount"` + Volume float64 `json:"vol"` + Close float64 `json:"close"` + Count float64 `json:"count"` + High float64 `json:"high"` + IDTimestamp types.Time `json:"id"` + Low float64 `json:"low"` + Open float64 `json:"open"` + Amount float64 `json:"amount"` } `json:"data"` } @@ -387,17 +389,17 @@ type SwapKlineData struct { type MarketOverviewData struct { Channel string `json:"ch"` Tick struct { - Vol float64 `json:"vol,string"` - Ask []float64 `json:"ask"` - Bid []float64 `json:"bid"` - Close float64 `json:"close,string"` - Count float64 `json:"count"` - High float64 `json:"high,string"` - ID int64 `json:"id"` - Low float64 `json:"low,string"` - Open float64 `json:"open,string"` - Timestamp int64 `json:"ts"` - Amount float64 `json:"amount,string"` + Vol float64 `json:"vol,string"` + Ask []float64 `json:"ask"` + Bid []float64 `json:"bid"` + Close float64 `json:"close,string"` + Count float64 `json:"count"` + High float64 `json:"high,string"` + ID int64 `json:"id"` + Low float64 `json:"low,string"` + Open float64 `json:"open,string"` + Timestamp types.Time `json:"ts"` + Amount float64 `json:"amount,string"` } `json:"tick"` } @@ -406,11 +408,11 @@ type LastTradeData struct { Ch string `json:"ch"` Tick struct { Data []struct { - Amount float64 `json:"amount,string"` - Direction string `json:"direction"` - ID int64 `json:"id"` - Price float64 `json:"price,string"` - Timestamp int64 `json:"ts"` + Amount float64 `json:"amount,string"` + Direction string `json:"direction"` + ID int64 `json:"id"` + Price float64 `json:"price,string"` + Timestamp types.Time `json:"ts"` } `json:"data"` } `json:"tick"` } @@ -418,17 +420,17 @@ type LastTradeData struct { // BatchTradesData stores batch trades for a given swap contract type BatchTradesData struct { ID int64 `json:"id"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Data []CoinMarginedFuturesTrade `json:"data"` } // CoinMarginedFuturesTrade holds coinmarginedfutures trade data type CoinMarginedFuturesTrade struct { - Amount float64 `json:"amount"` - Direction string `json:"direction"` - ID int64 `json:"id"` - Price float64 `json:"price"` - Timestamp int64 `json:"ts"` + Amount float64 `json:"amount"` + Direction string `json:"direction"` + ID int64 `json:"id"` + Price float64 `json:"price"` + Timestamp types.Time `json:"ts"` } // InsuranceAndClawbackData stores insurance fund's and clawback rate's data @@ -447,8 +449,8 @@ type HistoricalInsuranceFundBalance struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` Tick []struct { - InsuranceFund float64 `json:"insurance_fund"` - Timestamp int64 `json:"ts"` + InsuranceFund float64 `json:"insurance_fund"` + Timestamp types.Time `json:"ts"` } `json:"tick"` TotalPage int64 `json:"total_page"` TotalSize int64 `json:"total_size"` @@ -479,9 +481,9 @@ type OpenInterestData struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` Tick []struct { - Volume float64 `json:"volume"` - AmountType float64 `json:"amountType"` - Timestamp int64 `json:"ts"` + Volume float64 `json:"volume"` + AmountType float64 `json:"amountType"` + Timestamp types.Time `json:"ts"` } `json:"tick"` } `json:"data"` } @@ -507,10 +509,10 @@ type TraderSentimentIndexAccountData struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` List []struct { - BuyRatio float64 `json:"buy_ratio"` - SellRatio float64 `json:"sell_ratio"` - LockedRatio float64 `json:"locked_ratio"` - Timestamp int64 `json:"ts"` + BuyRatio float64 `json:"buy_ratio"` + SellRatio float64 `json:"sell_ratio"` + LockedRatio float64 `json:"locked_ratio"` + Timestamp types.Time `json:"ts"` } `json:"list"` } `json:"data"` } @@ -521,9 +523,9 @@ type TraderSentimentIndexPositionData struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` List []struct { - BuyRatio float64 `json:"buy_ratio"` - SellRatio float64 `json:"sell_ratio"` - Timestamp int64 `json:"ts"` + BuyRatio float64 `json:"buy_ratio"` + SellRatio float64 `json:"sell_ratio"` + Timestamp types.Time `json:"ts"` } `json:"list"` } `json:"data"` } @@ -594,7 +596,7 @@ type PremiumIndexKlineData struct { Open float64 `json:"open,string"` Amount float64 `json:"amount,string"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // EstimatedFundingRateData stores estimated funding rate data @@ -610,7 +612,7 @@ type EstimatedFundingRateData struct { Open float64 `json:"open"` Amount float64 `json:"amount"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // BasisData stores basis data for swaps @@ -623,7 +625,7 @@ type BasisData struct { ID int64 `json:"id"` IndexPrice string `json:"index_price"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // SwapAccountInformation stores swap account information @@ -668,7 +670,7 @@ type SwapPositionInfo struct { // SwapAssetsAndPositionsData stores positions and assets data for swaps type SwapAssetsAndPositionsData struct { - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Data []struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` @@ -705,7 +707,7 @@ type SwapAssetsAndPositionsData struct { // SubAccountsAssetData stores asset data for all subaccounts type SubAccountsAssetData struct { - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Data []struct { SubUID int64 `json:"sub_uid"` List []struct { @@ -720,7 +722,7 @@ type SubAccountsAssetData struct { // SingleSubAccountAssetsInfo stores asset info for a single subaccount type SingleSubAccountAssetsInfo struct { - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Data []struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` @@ -741,7 +743,7 @@ type SingleSubAccountAssetsInfo struct { // SingleSubAccountPositionsInfo stores single subaccount's positions data type SingleSubAccountPositionsInfo struct { - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Data []struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` @@ -766,19 +768,19 @@ type AvailableLeverageData struct { ContractCode string `json:"contract_code"` AvailableLeverage string `json:"available_level_rate"` } `json:"data"` - Timestamp int64 `json:"timestamp"` + Timestamp types.Time `json:"timestamp"` } // FinancialRecordData stores an accounts financial records type FinancialRecordData struct { Data struct { FinancialRecord []struct { - ID int64 `json:"id"` - Timestamp int64 `json:"ts"` - Symbol string `json:"symbol"` - ContractCode string `json:"contract_code"` - OrderType int64 `json:"type"` - Amount float64 `json:"amount"` + ID int64 `json:"id"` + Timestamp types.Time `json:"ts"` + Symbol string `json:"symbol"` + ContractCode string `json:"contract_code"` + OrderType int64 `json:"type"` + Amount float64 `json:"amount"` } `json:"financial_record"` TotalPage int64 `json:"total_page"` CurrentPage int64 `json:"current_page"` @@ -797,7 +799,7 @@ type SwapOrderLimitInfo struct { CloseLimit float64 `json:"close_limit"` } `json:"list"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // SwapTradingFeeData stores trading fee data for swaps @@ -811,7 +813,7 @@ type SwapTradingFeeData struct { CloseMakerFee float64 `json:"close_maker_fee,string"` CloseTakerFee float64 `json:"close_taker_fee,string"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // TransferLimitData stores transfer limits @@ -830,7 +832,7 @@ type TransferLimitData struct { NetTransferInMaxDaily float64 `json:"net_transfer_in_max_daily"` NetTransferOutMaxDaily float64 `json:"net_transfer_out_max_daily"` } `json:"data"` - Timestamp int64 `json:"timestamp"` + Timestamp types.Time `json:"timestamp"` } // PositionLimitData stores position limit data @@ -841,7 +843,7 @@ type PositionLimitData struct { BuyLimit float64 `json:"buy_limit"` SellLimit float64 `json:"sell_limit"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // InternalAccountTransferData stores transfer data between subaccounts and main account @@ -854,16 +856,16 @@ type InternalAccountTransferData struct { // InternalAccountTransferRecords stores data for transfer records within the account type InternalAccountTransferRecords struct { - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Data struct { TransferRecord []struct { - ID int64 `json:"id"` - Timestamp int64 `json:"ts"` - Symbol string `json:"symbol"` - SubUID int64 `json:"sub_uid"` - SubAccountName string `json:"sub_account_name"` - TransferType int64 `json:"transfer_type"` - Amount float64 `json:"amount"` + ID int64 `json:"id"` + Timestamp types.Time `json:"ts"` + Symbol string `json:"symbol"` + SubUID int64 `json:"sub_uid"` + SubAccountName string `json:"sub_account_name"` + TransferType int64 `json:"transfer_type"` + Amount float64 `json:"amount"` } `json:"transfer_record"` TotalPage int64 `json:"total_page"` CurrentPage int64 `json:"current_page"` @@ -878,7 +880,7 @@ type SwapOrderData struct { OrderIDString string `json:"order_id_string"` ClientOrderID int64 `json:"client_order_id"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // BatchOrderData stores data for batch orders @@ -895,7 +897,7 @@ type BatchOrderData struct { OrderIDString string `json:"order_id_str"` } `json:"success"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // BatchOrderRequestType stores batch order request data @@ -921,8 +923,8 @@ type CancelOrdersData struct { ErrCode int64 `json:"err_code"` ErrMsg string `json:"err_msg"` } `json:"errors"` - Successes string `json:"successes"` - Timestamp int64 `json:"ts"` + Successes string `json:"successes"` + Timestamp types.Time `json:"ts"` } // LightningCloseOrderData stores order data from a lightning close order @@ -932,7 +934,7 @@ type LightningCloseOrderData struct { OrderIDString string `json:"order_id_str"` ClientOrderID int64 `json:"client_order_id"` } - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // SwapOrderInfo stores info for swap orders @@ -962,7 +964,7 @@ type SwapOrderInfo struct { FeeAsset float64 `json:"fee_asset"` LiquidationType int64 `json:"liquidation_type"` } - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // OrderDetailData acquires order details @@ -1006,7 +1008,7 @@ type OrderDetailData struct { TotalSize int64 `json:"total_size"` CurrentPage int64 `json:"current_page"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // SwapOpenOrdersData stores open orders data for swaps @@ -1039,7 +1041,7 @@ type SwapOpenOrdersData struct { CurrentPage int64 `json:"current_page"` TotalSize int64 `json:"total_size"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // SwapOrderHistory gets order history for swaps @@ -1073,7 +1075,7 @@ type SwapOrderHistory struct { CurrentPage int64 `json:"current_page"` TotalSize int64 `json:"total_size"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // AccountTradeHistoryData stores account trade history for swaps @@ -1102,7 +1104,7 @@ type AccountTradeHistoryData struct { FeeAsset string `json:"fee_asset"` } `json:"trades"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // TriggerOrderData stores trigger order data @@ -1123,7 +1125,7 @@ type CancelTriggerOrdersData struct { } `json:"errors"` Successes string `json:"successes"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // TriggerOpenOrdersData stores trigger open orders data @@ -1151,7 +1153,7 @@ type TriggerOpenOrdersData struct { CurrentPage int64 `json:"current_page"` TotalSize int64 `json:"total_size"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // TriggerOrderHistory stores trigger order history data for swaps @@ -1186,7 +1188,7 @@ type TriggerOrderHistory struct { CurrentPage int64 `json:"current_page"` TotalSize int64 `json:"total_size"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // TransferMarginBetweenAccountsData stores margin transfer data between spot and swap accounts diff --git a/exchanges/huobi/futures_types.go b/exchanges/huobi/futures_types.go index 692f5645cb6..0d1cac72cdf 100644 --- a/exchanges/huobi/futures_types.go +++ b/exchanges/huobi/futures_types.go @@ -1,18 +1,20 @@ package huobi +import "github.com/thrasher-corp/gocryptotrader/types" + // FContractInfoData gets contract info data for futures type FContractInfoData struct { Data []struct { - Symbol string `json:"symbol"` - ContractCode string `json:"contract_code"` - ContractType string `json:"contract_type"` - ContractSize float64 `json:"contract_size"` - PriceTick float64 `json:"price_tick"` - DeliveryDate string `json:"delivery_date"` - DeliveryTime int64 `json:"delivery_time,string"` - CreateDate string `json:"create_date"` - ContractStatus int64 `json:"contract_status"` - SettlementTime int64 `json:"settlement_time,string"` + Symbol string `json:"symbol"` + ContractCode string `json:"contract_code"` + ContractType string `json:"contract_type"` + ContractSize float64 `json:"contract_size"` + PriceTick float64 `json:"price_tick"` + DeliveryDate string `json:"delivery_date"` + DeliveryTime types.Time `json:"delivery_time"` + CreateDate string `json:"create_date"` + ContractStatus int64 `json:"contract_status"` + SettlementTime types.Time `json:"settlement_time"` } } @@ -22,7 +24,7 @@ type FContractIndexPriceInfo struct { Symbol string `json:"symbol"` IndexPrice float64 `json:"index_price"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FContractPriceLimits gets limits for futures contracts @@ -34,13 +36,13 @@ type FContractPriceLimits struct { ContractCode string `json:"contract_code"` ContractType string `json:"contract_type"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FContractOIData stores open interest data for futures contracts type FContractOIData struct { Data []UContractOpenInterest `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // UContractOpenInterest stores open interest data for futures contracts @@ -63,19 +65,19 @@ type FEstimatedDeliveryPriceInfo struct { Data struct { DeliveryPrice float64 `json:"delivery_price"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FMarketDepth gets orderbook data for futures type FMarketDepth struct { - Ch string `json:"ch"` - Timestamp int64 `json:"ts"` + Ch string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { MRID int64 `json:"mrid"` ID int64 `json:"id"` Bids [][2]float64 `json:"bids"` Asks [][2]float64 `json:"asks"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Version int64 `json:"version"` Ch string `json:"ch"` } `json:"tick"` @@ -97,16 +99,16 @@ type obItem struct { type FKlineData struct { Ch string `json:"ch"` Data []struct { - Volume float64 `json:"vol"` - Close float64 `json:"close"` - Count float64 `json:"count"` - High float64 `json:"high"` - IDTimestamp int64 `json:"id"` - Low float64 `json:"low"` - Open float64 `json:"open"` - Amount float64 `json:"amount"` + Volume float64 `json:"vol"` + Close float64 `json:"close"` + Count float64 `json:"count"` + High float64 `json:"high"` + IDTimestamp types.Time `json:"id"` + Low float64 `json:"low"` + Open float64 `json:"open"` + Amount float64 `json:"amount"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FMarketOverviewData stores overview data for futures @@ -116,16 +118,16 @@ type FMarketOverviewData struct { Vol float64 `json:"vol,string"` Ask [2]float64 Bid [2]float64 - Close float64 `json:"close,string"` - Count float64 `json:"count"` - High float64 `json:"high,string"` - ID int64 `jso:"id"` - Low float64 `json:"low,string"` - Open float64 `json:"open,string"` - Timestamp int64 `json:"ts"` - Amount float64 `json:"amount,string"` + Close float64 `json:"close,string"` + Count float64 `json:"count"` + High float64 `json:"high,string"` + ID int64 `jso:"id"` + Low float64 `json:"low,string"` + Open float64 `json:"open,string"` + Timestamp types.Time `json:"ts"` + Amount float64 `json:"amount,string"` } `json:"tick"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FLastTradeData stores last trade's data for a contract @@ -133,41 +135,41 @@ type FLastTradeData struct { Ch string `json:"ch"` Tick struct { Data []struct { - Amount float64 `json:"amount,string"` - Direction string `json:"direction"` - ID int64 `json:"id"` - Price float64 `json:"price,string"` - Timestamp int64 `json:"ts"` + Amount float64 `json:"amount,string"` + Direction string `json:"direction"` + ID int64 `json:"id"` + Price float64 `json:"price,string"` + Timestamp types.Time `json:"ts"` } `json:"data"` - ID int64 `json:"id"` - Timestamp int64 `json:"ts"` + ID int64 `json:"id"` + Timestamp types.Time `json:"ts"` } `json:"tick"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FBatchTradesForContractData stores batch of trades data for a contract type FBatchTradesForContractData struct { - Ch string `json:"ch"` - Timestamp int64 `json:"ts"` + Ch string `json:"ch"` + Timestamp types.Time `json:"ts"` Data []struct { ID int64 `json:"id"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Data []FuturesTrade `json:"data"` } `json:"data"` } // FuturesTrade is futures trade data type FuturesTrade struct { - Amount float64 `json:"amount"` - Direction string `json:"direction"` - ID int64 `json:"id"` - Price float64 `json:"price"` - Timestamp int64 `json:"ts"` + Amount float64 `json:"amount"` + Direction string `json:"direction"` + ID int64 `json:"id"` + Price float64 `json:"price"` + Timestamp types.Time `json:"ts"` } // FClawbackRateAndInsuranceData stores clawback rate and insurance data for futures type FClawbackRateAndInsuranceData struct { - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Data []struct { Symbol string `json:"symbol"` InsuranceFund float64 `json:"insurance_fund"` @@ -177,12 +179,12 @@ type FClawbackRateAndInsuranceData struct { // FHistoricalInsuranceRecordsData stores historical records of insurance fund balances for futures type FHistoricalInsuranceRecordsData struct { - Timestamp int64 `json:"timestamp"` + Timestamp types.Time `json:"timestamp"` Data struct { Symbol string `json:"symbol"` Tick []struct { - InsuranceFund float64 `json:"insurance_fund"` - Timestamp int64 `json:"ts"` + InsuranceFund float64 `json:"insurance_fund"` + Timestamp types.Time `json:"ts"` } `json:"tick"` } `json:"data"` } @@ -201,7 +203,7 @@ type FTieredAdjustmentFactorInfo struct { } `json:"ladders"` } `json:"list"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FOIData gets oi data on futures @@ -210,12 +212,12 @@ type FOIData struct { Symbol string `json:"symbol"` ContractType string `json:"contract_type"` Tick []struct { - Volume float64 `json:"volume,string"` - AmountType int64 `json:"amount_type"` - Timestamp int64 `json:"ts"` + Volume float64 `json:"volume,string"` + AmountType int64 `json:"amount_type"` + Timestamp types.Time `json:"ts"` } `json:"tick"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FInfoSystemStatusData stores system status info for futures @@ -228,21 +230,21 @@ type FInfoSystemStatusData struct { TransferIn int64 `json:"transfer_in"` TransferOut int64 `json:"transfer_out"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FTopAccountsLongShortRatio stores long/short ratio for top futures accounts type FTopAccountsLongShortRatio struct { Data struct { List []struct { - BuyRatio float64 `json:"buy_ratio"` - SellRatio float64 `json:"sell_ratio"` - LockedRatio float64 `json:"locked_ratio"` - Timestamp int64 `json:"ts"` + BuyRatio float64 `json:"buy_ratio"` + SellRatio float64 `json:"sell_ratio"` + LockedRatio float64 `json:"locked_ratio"` + Timestamp types.Time `json:"ts"` } `json:"list"` Symbol string `json:"symbol"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FTopPositionsLongShortRatio stores long short ratio for top futures positions @@ -250,12 +252,12 @@ type FTopPositionsLongShortRatio struct { Data struct { Symbol string `json:"symbol"` List []struct { - BuyRatio float64 `json:"buy_ratio"` - SellRatio float64 `json:"sell_ratio"` - Timestamp int64 `json:"timestamp"` + BuyRatio float64 `json:"buy_ratio"` + SellRatio float64 `json:"sell_ratio"` + Timestamp types.Time `json:"timestamp"` } `json:"list"` } `json:"data"` - Timestamp int64 `json:"timestamp"` + Timestamp types.Time `json:"timestamp"` } // FLiquidationOrdersInfo stores data of futures liquidation orders @@ -274,7 +276,7 @@ type FLiquidationOrdersInfo struct { CurrentPage int64 `json:"current_page"` TotalSize int64 `json:"total_size"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FIndexKlineData stores index kline data for futures @@ -290,7 +292,7 @@ type FIndexKlineData struct { Open float64 `json:"open"` Amount float64 `json:"amount"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FBasisData stores basis data for futures @@ -303,7 +305,7 @@ type FBasisData struct { ID int64 `json:"id"` IndexPrice float64 `json:"index_price,string"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FUserAccountData stores user account data info for futures @@ -323,7 +325,7 @@ type FUserAccountData struct { AdjustFactor float64 `json:"adjust_factor"` MarginStatic float64 `json:"margin_static"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FUsersPositionsInfo stores positions data for futures @@ -345,12 +347,12 @@ type FUsersPositionsInfo struct { Direction string `json:"direction"` LastPrice float64 `json:"last_price"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FSubAccountAssetsInfo gets subaccounts asset data type FSubAccountAssetsInfo struct { - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Data []struct { SubUID int64 `json:"sub_uid"` List []struct { @@ -379,7 +381,7 @@ type FSingleSubAccountAssetsInfo struct { LeverageRate float64 `json:"lever_rate"` MarginStatic float64 `json:"margin_static"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FSingleSubAccountPositionsInfo stores futures positions' info for a single subaccount @@ -401,24 +403,24 @@ type FSingleSubAccountPositionsInfo struct { Direction string `json:"direction"` LastPrice float64 `json:"last_price"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FFinancialRecords stores financial records data for futures type FFinancialRecords struct { Data struct { FinancialRecord []struct { - ID int64 `json:"id"` - Timestamp int64 `json:"ts"` - Symbol string `json:"symbol"` - RecordType int64 `json:"type"` - Amount float64 `json:"amount"` + ID int64 `json:"id"` + Timestamp types.Time `json:"ts"` + Symbol string `json:"symbol"` + RecordType int64 `json:"type"` + Amount float64 `json:"amount"` } `json:"financial_record"` TotalPage int64 `json:"total_page"` CurrentPage int64 `json:"current_page"` TotalSize int64 `json:"total_size"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FSettlementRecords stores user's futures settlement records @@ -452,7 +454,7 @@ type FSettlementRecords struct { TotalPage int64 `json:"total_page"` TotalSize int64 `json:"total_size"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FContractInfoOnOrderLimit stores contract info on futures order limit @@ -468,7 +470,7 @@ type FContractInfoOnOrderLimit struct { } `json:"types"` } `json:"list"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FContractTradingFeeData stores contract trading fee data @@ -482,7 +484,7 @@ type FContractTradingFeeData struct { DeliveryFee float64 `json:"delivery_fee,string"` FeeAsset string `json:"fee_asset"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FTransferLimitData stores transfer limit data for futures @@ -498,7 +500,7 @@ type FTransferLimitData struct { NetTransferInMaxDaily float64 `json:"net_transfer_in_max_daily"` NetTransferOutMaxDaily float64 `json:"net_transfer_out_max_daily"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FPositionLimitData stores information on futures positions limit @@ -511,7 +513,7 @@ type FPositionLimitData struct { SellLimit float64 `json:"sell_limit"` } `json:"list"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FAssetsAndPositionsData stores assets and positions data for futures @@ -531,8 +533,8 @@ type FAssetsAndPositionsData struct { // FAccountTransferData stores internal transfer data for futures type FAccountTransferData struct { - Status string `json:"status"` - Timestamp int64 `json:"ts"` + Status string `json:"status"` + Timestamp types.Time `json:"ts"` Data struct { OrderID string `json:"order_id"` } `json:"data"` @@ -540,16 +542,16 @@ type FAccountTransferData struct { // FTransferRecords gets transfer records data type FTransferRecords struct { - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Data struct { TransferRecord []struct { - ID int64 `json:"id"` - Timestamp int64 `json:"ts"` - Symbol string `json:"symbol"` - SubUID int64 `json:"sub_uid"` - SubAccountName string `json:"sub_account_name"` - TransferType int64 `json:"transfer_type"` - Amount float64 `json:"amount"` + ID int64 `json:"id"` + Timestamp types.Time `json:"ts"` + Symbol string `json:"symbol"` + SubUID int64 `json:"sub_uid"` + SubAccountName string `json:"sub_account_name"` + TransferType int64 `json:"transfer_type"` + Amount float64 `json:"amount"` } `json:"transfer_record"` TotalPage int64 `json:"total_page"` CurrentPage int64 `json:"current_page"` @@ -563,7 +565,7 @@ type FAvailableLeverageData struct { Symbol string `json:"symbol"` AvailableLeverageRate string `json:"available_level_rate"` } `json:"data"` - Timestamp int64 `json:"timestamp"` + Timestamp types.Time `json:"timestamp"` } // FOrderData stores order data for futures @@ -573,7 +575,7 @@ type FOrderData struct { OrderIDStr string `json:"order_id_str"` ClientOrderID int64 `json:"client_order_id"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } type fBatchOrderData struct { @@ -604,7 +606,7 @@ type FCancelOrderData struct { } `json:"errors"` Successes string `json:"successes"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FOrderInfo stores order info @@ -636,7 +638,7 @@ type FOrderInfo struct { Volume float64 `json:"volume"` LiquidationType int64 `json:"liquidation_type"` } `json:"data"` - Timestamp int64 `json:"timestamp"` + Timestamp types.Time `json:"timestamp"` } // FOrderDetailsData stores order details for futures orders @@ -682,7 +684,7 @@ type FOrderDetailsData struct { TotalSize int64 `json:"total_size"` CurrentPage int64 `json:"current_page"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FOpenOrdersData stores open orders data for futures @@ -716,7 +718,7 @@ type FOpenOrdersData struct { CurrentPage int64 `json:"current_page"` TotalSize int64 `json:"total_size"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FOrderHistoryData stores order history data @@ -751,7 +753,7 @@ type FOrderHistoryData struct { CurrentPage int64 `json:"current_page"` TotalSize int64 `json:"total_size"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FTradeHistoryData stores trade history data for futures @@ -781,7 +783,7 @@ type FTradeHistoryData struct { FeeAsset string `json:"fee_asset"` } `json:"trades"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FTriggerOrderData stores trigger order data @@ -790,7 +792,7 @@ type FTriggerOrderData struct { OrderID int64 `json:"order_id"` OrderIDStr string `json:"order_id_str"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FTriggerOpenOrders stores trigger open orders data @@ -819,7 +821,7 @@ type FTriggerOpenOrders struct { CurrentPage int64 `json:"current_page"` TotalSize int64 `json:"total_size"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // FTriggerOrderHistoryData stores trigger order history for futures @@ -855,5 +857,5 @@ type FTriggerOrderHistoryData struct { CurrentPage int64 `json:"current_page"` TotalSize int64 `json:"total_size"` } `json:"data"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index 15dec4ba245..e65a1841b29 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -3,12 +3,15 @@ package huobi import ( "context" "errors" + "fmt" "log" "os" "strconv" + "sync" "testing" "time" + "github.com/buger/jsonparser" "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -23,13 +26,16 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange" testsubs "github.com/thrasher-corp/gocryptotrader/internal/testing/subscriptions" + mockws "github.com/thrasher-corp/gocryptotrader/internal/testing/websocket" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" + "github.com/thrasher-corp/gocryptotrader/types" ) // Please supply you own test keys here for due diligence testing. @@ -37,13 +43,16 @@ const ( apiKey = "" apiSecret = "" canManipulateRealOrders = false - testSymbol = "btcusdt" ) var ( - h = &HUOBI{} - wsSetupRan bool - futuresTestPair = currency.NewPair(currency.BTC, currency.NewCode("CW")) // represents this week - NQ (next quarter) is erroring out. + h = &HUOBI{} + wsSetupRan bool + btcFutureDatedPair currency.Pair + btccwPair = currency.NewPair(currency.BTC, currency.NewCode("CW")) + btcusdPair = currency.NewPairWithDelimiter("BTC", "USD", "-") + btcusdtPair = currency.NewPairWithDelimiter("BTC", "USDT", "-") + ethusdPair = currency.NewPairWithDelimiter("ETH", "USD", "-") ) func TestMain(m *testing.M) { @@ -66,177 +75,108 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal("Huobi setup error", err) } - - err = h.UpdateTradablePairs(context.Background(), true) - if err != nil { - log.Fatal("Huobi setup error", err) - } os.Exit(m.Run()) } -func setupWsTests(t *testing.T) { - t.Helper() - if wsSetupRan { - return - } - if !h.Websocket.IsEnabled() && !h.API.AuthenticatedWebsocketSupport || !sharedtestvalues.AreAPICredentialsSet(h) { - t.Skip(stream.ErrWebsocketNotEnabled.Error()) - } - comms = make(chan WsMessage, sharedtestvalues.WebsocketChannelOverrideCapacity) - go h.wsReadData() - var dialer websocket.Dialer - err := h.wsAuthenticatedDial(&dialer) - if err != nil { - t.Fatal(err) - } - err = h.wsLogin(context.Background()) - if err != nil { - t.Fatal(err) - } - - wsSetupRan = true -} - func TestGetCurrenciesIncludingChains(t *testing.T) { t.Parallel() r, err := h.GetCurrenciesIncludingChains(context.Background(), currency.EMPTYCODE) - if err != nil { - t.Error(err) - } - if len(r) == 1 { - t.Error("expected 1 result") - } + require.NoError(t, err) + assert.Greater(t, len(r), 1, "should get more than one currency back") r, err = h.GetCurrenciesIncludingChains(context.Background(), currency.USDT) - if err != nil { - t.Error(err) - } - if len(r) < 1 { - t.Error("expected >= 1 results") - } + require.NoError(t, err) + assert.Equal(t, 1, len(r), "Should only get one currency back") } func TestFGetContractInfo(t *testing.T) { t.Parallel() _, err := h.FGetContractInfo(context.Background(), "", "", currency.EMPTYPAIR) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFIndexPriceInfo(t *testing.T) { t.Parallel() _, err := h.FIndexPriceInfo(context.Background(), currency.BTC) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFContractPriceLimitations(t *testing.T) { t.Parallel() _, err := h.FContractPriceLimitations(context.Background(), "BTC", "this_week", currency.EMPTYPAIR) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFContractOpenInterest(t *testing.T) { t.Parallel() - _, err := h.FContractOpenInterest(context.Background(), - "BTC", "this_week", currency.EMPTYPAIR) - if err != nil { - t.Error(err) - } + _, err := h.FContractOpenInterest(context.Background(), "BTC", "this_week", currency.EMPTYPAIR) + require.NoError(t, err) } func TestFGetEstimatedDeliveryPrice(t *testing.T) { t.Parallel() _, err := h.FGetEstimatedDeliveryPrice(context.Background(), currency.BTC) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetMarketDepth(t *testing.T) { t.Parallel() - _, err := h.FGetMarketDepth(context.Background(), futuresTestPair, "step5") - if err != nil { - t.Error(err) - } + _, err := h.FGetMarketDepth(context.Background(), btccwPair, "step5") + require.NoError(t, err) } func TestFGetKlineData(t *testing.T) { t.Parallel() - _, err := h.FGetKlineData(context.Background(), futuresTestPair, "5min", 5, time.Now().Add(-time.Minute*5), time.Now()) - if err != nil { - t.Error(err) - } + _, err := h.FGetKlineData(context.Background(), btccwPair, "5min", 5, time.Now().Add(-time.Minute*5), time.Now()) + require.NoError(t, err) } func TestFGetMarketOverviewData(t *testing.T) { t.Parallel() - _, err := h.FGetMarketOverviewData(context.Background(), futuresTestPair) - if err != nil { - t.Error(err) - } + _, err := h.FGetMarketOverviewData(context.Background(), btccwPair) + require.NoError(t, err) } func TestFLastTradeData(t *testing.T) { t.Parallel() - _, err := h.FLastTradeData(context.Background(), futuresTestPair) - if err != nil { - t.Error(err) - } + _, err := h.FLastTradeData(context.Background(), btccwPair) + require.NoError(t, err) } func TestFRequestPublicBatchTrades(t *testing.T) { t.Parallel() - _, err := h.FRequestPublicBatchTrades(context.Background(), futuresTestPair, 50) - if err != nil { - t.Error(err) - } + _, err := h.FRequestPublicBatchTrades(context.Background(), btccwPair, 50) + require.NoError(t, err) } func TestFQueryTieredAdjustmentFactor(t *testing.T) { t.Parallel() _, err := h.FQueryTieredAdjustmentFactor(context.Background(), currency.BTC) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFQueryHisOpenInterest(t *testing.T) { t.Parallel() - _, err := h.FQueryHisOpenInterest(context.Background(), - "BTC", "this_week", "60min", "cont", 3) - if err != nil { - t.Error(err) - } + _, err := h.FQueryHisOpenInterest(context.Background(), "BTC", "this_week", "60min", "cont", 3) + require.NoError(t, err) } func TestFQuerySystemStatus(t *testing.T) { t.Parallel() - _, err := h.FQuerySystemStatus(context.Background(), currency.BTC) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFQueryTopAccountsRatio(t *testing.T) { t.Parallel() _, err := h.FQueryTopAccountsRatio(context.Background(), "BTC", "5min") - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFQueryTopPositionsRatio(t *testing.T) { t.Parallel() _, err := h.FQueryTopPositionsRatio(context.Background(), "BTC", "5min") - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFLiquidationOrders(t *testing.T) { @@ -248,187 +188,126 @@ func TestFLiquidationOrders(t *testing.T) { func TestFIndexKline(t *testing.T) { t.Parallel() - _, err := h.FIndexKline(context.Background(), futuresTestPair, "5min", 5) - if err != nil { - t.Error(err) - } + _, err := h.FIndexKline(context.Background(), btccwPair, "5min", 5) + require.NoError(t, err) } func TestFGetBasisData(t *testing.T) { t.Parallel() - _, err := h.FGetBasisData(context.Background(), futuresTestPair, "5min", "open", 3) - if err != nil { - t.Error(err) - } + _, err := h.FGetBasisData(context.Background(), btccwPair, "5min", "open", 3) + require.NoError(t, err) } func TestFGetAccountInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetAccountInfo(context.Background(), currency.EMPTYCODE) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetPositionsInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetPositionsInfo(context.Background(), currency.EMPTYCODE) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetAllSubAccountAssets(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetAllSubAccountAssets(context.Background(), currency.EMPTYCODE) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetSingleSubAccountInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetSingleSubAccountInfo(context.Background(), "", "154263566") - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetSingleSubPositions(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetSingleSubPositions(context.Background(), "", "154263566") - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetFinancialRecords(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetFinancialRecords(context.Background(), "BTC", "closeLong", 2, 0, 0) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetSettlementRecords(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetSettlementRecords(context.Background(), currency.BTC, 0, 0, time.Now().Add(-48*time.Hour), time.Now()) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFContractTradingFee(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FContractTradingFee(context.Background(), currency.EMPTYCODE) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetTransferLimits(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetTransferLimits(context.Background(), currency.EMPTYCODE) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetPositionLimits(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetPositionLimits(context.Background(), currency.EMPTYCODE) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetAssetsAndPositions(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetAssetsAndPositions(context.Background(), currency.HT) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFTransfer(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - _, err := h.FTransfer(context.Background(), - "154263566", "HT", "sub_to_master", 5) - if err != nil { - t.Error(err) - } + _, err := h.FTransfer(context.Background(), "154263566", "HT", "sub_to_master", 5) + require.NoError(t, err) } func TestFGetTransferRecords(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - _, err := h.FGetTransferRecords(context.Background(), - "HT", "master_to_sub", 90, 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.FGetTransferRecords(context.Background(), "HT", "master_to_sub", 90, 0, 0) + require.NoError(t, err) } func TestFGetAvailableLeverage(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetAvailableLeverage(context.Background(), currency.BTC) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - tradablePairs, err := h.CurrencyPairs.GetPairs(asset.Futures, false) - if err != nil { - t.Fatal(err) - } - if len(tradablePairs) == 0 { - t.Fatal("no tradable pairs") - } - _, err = h.FOrder(context.Background(), - currency.EMPTYPAIR, tradablePairs[0].Base.Upper().String(), - "quarter", "123", "BUY", "open", "limit", 1, 1, 1) - if err != nil { - t.Error(err) - } + _, err := h.FOrder(context.Background(), currency.EMPTYPAIR, "BTC", "quarter", "123", "BUY", "open", "limit", 1, 1, 1) + require.NoError(t, err) } func TestFPlaceBatchOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - var req []fBatchOrderData order1 := fBatchOrderData{ Symbol: "btc", @@ -454,165 +333,104 @@ func TestFPlaceBatchOrder(t *testing.T) { } req = append(req, order1, order2) _, err := h.FPlaceBatchOrder(context.Background(), req) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFCancelOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - _, err := h.FCancelOrder(context.Background(), currency.BTC, "123", "") - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFCancelAllOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - tradablePairs, err := h.CurrencyPairs.GetPairs(asset.Futures, false) - if err != nil { - t.Fatal(err) - } - if len(tradablePairs) == 0 { - t.Fatal("no tradable pairs") - } - _, err = h.FCancelAllOrders(context.Background(), tradablePairs[0], "", "") - if err != nil { - t.Error(err) - } + updatePairsOnce(t, h) + _, err := h.FCancelAllOrders(context.Background(), btcFutureDatedPair, "", "") + require.NoError(t, err) } func TestFFlashCloseOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - _, err := h.FFlashCloseOrder(context.Background(), currency.EMPTYPAIR, "BTC", "quarter", "BUY", "lightning", "", 1) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetOrderInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetOrderInfo(context.Background(), "BTC", "", "123") - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFOrderDetails(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - _, err := h.FOrderDetails(context.Background(), - "BTC", "123", "quotation", time.Now().Add(-1*time.Hour), 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.FOrderDetails(context.Background(), "BTC", "123", "quotation", time.Now().Add(-1*time.Hour), 0, 0) + require.NoError(t, err) } func TestFGetOpenOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.FGetOpenOrders(context.Background(), currency.BTC, 1, 2) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFGetOrderHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - tradablePairs, err := h.CurrencyPairs.GetPairs(asset.Futures, false) - if err != nil { - t.Fatal(err) - } - if len(tradablePairs) == 0 { - t.Fatal("no tradable pairs") - } - _, err = h.FGetOrderHistory(context.Background(), - currency.EMPTYPAIR, tradablePairs[0].Base.Upper().String(), + _, err := h.FGetOrderHistory(context.Background(), + currency.EMPTYPAIR, "BTC", "all", "all", "limit", []order.Status{}, 5, 0, 0) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFTradeHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - _, err := h.FTradeHistory(context.Background(), - currency.EMPTYPAIR, "BTC", "all", 10, 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.FTradeHistory(context.Background(), currency.EMPTYPAIR, "BTC", "all", 10, 0, 0) + require.NoError(t, err) } func TestFPlaceTriggerOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - _, err := h.FPlaceTriggerOrder(context.Background(), - currency.EMPTYPAIR, "EOS", "quarter", "greaterOrEqual", - "limit", "buy", "close", 1.1, 1.05, 5, 2) - if err != nil { - t.Error(err) - } + _, err := h.FPlaceTriggerOrder(context.Background(), currency.EMPTYPAIR, "EOS", "quarter", "greaterOrEqual", "limit", "buy", "close", 1.1, 1.05, 5, 2) + require.NoError(t, err) } func TestFCancelTriggerOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - _, err := h.FCancelTriggerOrder(context.Background(), "ETH", "123") - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestFCancelAllTriggerOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - _, err := h.FCancelAllTriggerOrders(context.Background(), - currency.EMPTYPAIR, "BTC", "this_week") - if err != nil { - t.Error(err) - } + _, err := h.FCancelAllTriggerOrders(context.Background(), currency.EMPTYPAIR, "BTC", "this_week") + require.NoError(t, err) } func TestFQueryTriggerOpenOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - _, err := h.FQueryTriggerOpenOrders(context.Background(), - currency.EMPTYPAIR, "BTC", 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.FQueryTriggerOpenOrders(context.Background(), currency.EMPTYPAIR, "BTC", 0, 0) + require.NoError(t, err) } func TestFQueryTriggerOrderHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - _, err := h.FQueryTriggerOrderHistory(context.Background(), - currency.EMPTYPAIR, "EOS", "all", "all", 10, 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.FQueryTriggerOrderHistory(context.Background(), currency.EMPTYPAIR, "EOS", "all", "all", 10, 0, 0) + require.NoError(t, err) } func TestFetchTradablePairs(t *testing.T) { @@ -624,106 +442,49 @@ func TestFetchTradablePairs(t *testing.T) { func TestUpdateTickerSpot(t *testing.T) { t.Parallel() _, err := h.UpdateTicker(context.Background(), currency.NewPairWithDelimiter("INV", "ALID", "-"), asset.Spot) - if err == nil { - t.Error("expected invalid pair") - } + assert.ErrorContains(t, err, "invalid symbol") _, err = h.UpdateTicker(context.Background(), currency.NewPairWithDelimiter("BTC", "USDT", "_"), asset.Spot) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestUpdateTickerCMF(t *testing.T) { t.Parallel() _, err := h.UpdateTicker(context.Background(), currency.NewPairWithDelimiter("INV", "ALID", "_"), asset.CoinMarginedFutures) - if err == nil { - t.Error("expected invalid contract code") - } + assert.ErrorContains(t, err, "symbol data error") _, err = h.UpdateTicker(context.Background(), currency.NewPairWithDelimiter("BTC", "USD", "_"), asset.CoinMarginedFutures) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestUpdateTickerFutures(t *testing.T) { t.Parallel() - tradablePairs, err := h.CurrencyPairs.GetPairs(asset.Futures, false) - if err != nil { - t.Fatal(err) - } - if len(tradablePairs) == 0 { - t.Fatal("no tradable pairs") - } - _, err = h.UpdateTicker(context.Background(), tradablePairs[0], asset.Futures) - if err != nil { - t.Error(err) - } + _, err := h.UpdateTicker(context.Background(), btccwPair, asset.Futures) + require.NoError(t, err) } func TestUpdateOrderbookSpot(t *testing.T) { t.Parallel() - sp, err := currency.NewPairFromString("BTC_USDT") - if err != nil { - t.Error(err) - } - _, err = h.UpdateOrderbook(context.Background(), sp, asset.Spot) - if err != nil { - t.Error(err) - } + _, err := h.UpdateOrderbook(context.Background(), btcusdtPair, asset.Spot) + require.NoError(t, err) } func TestUpdateOrderbookCMF(t *testing.T) { t.Parallel() - cp1, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.UpdateOrderbook(context.Background(), cp1, asset.CoinMarginedFutures) - if err != nil { - t.Error(err) - } + _, err := h.UpdateOrderbook(context.Background(), btcusdPair, asset.CoinMarginedFutures) + require.NoError(t, err) } func TestUpdateOrderbookFuture(t *testing.T) { t.Parallel() - tradablePairs, err := h.CurrencyPairs.GetPairs(asset.Futures, false) - if err != nil { - t.Fatal(err) - } - if len(tradablePairs) == 0 { - t.Fatal("no tradable pairs") - } - _, err = h.UpdateOrderbook(context.Background(), tradablePairs[0], asset.Futures) - if err != nil { - t.Error(err) - } - tradablePairs, err = h.CurrencyPairs.GetPairs(asset.CoinMarginedFutures, false) - if err != nil { - t.Fatal(err) - } - if len(tradablePairs) == 0 { - t.Fatal("no tradable pairs") - } - _, err = h.UpdateOrderbook(context.Background(), tradablePairs[0], asset.CoinMarginedFutures) - if err != nil { - t.Error(err) - } -} - -func TestUpdateAccountInfo(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - _, err := h.UpdateAccountInfo(context.Background(), asset.Spot) - if err != nil { - t.Error(err) - } + _, err := h.UpdateOrderbook(context.Background(), btccwPair, asset.Futures) + require.NoError(t, err) + _, err = h.UpdateOrderbook(context.Background(), btcusdPair, asset.CoinMarginedFutures) + require.NoError(t, err) } func TestGetOrderHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - + updatePairsOnce(t, h) getOrdersRequest := order.MultiOrderRequest{ Type: order.AnyType, Pairs: []currency.Pair{currency.NewPair(currency.BTC, currency.USDT)}, @@ -731,496 +492,251 @@ func TestGetOrderHistory(t *testing.T) { Side: order.AnySide, } _, err := h.GetOrderHistory(context.Background(), &getOrdersRequest) - if err != nil { - t.Error(err) - } + require.NoError(t, err) cp1, err := currency.NewPairFromString("ADA-USD") - if err != nil { - t.Error(err) - } + require.NoError(t, err) getOrdersRequest.Pairs = []currency.Pair{cp1} getOrdersRequest.AssetType = asset.CoinMarginedFutures _, err = h.GetOrderHistory(context.Background(), &getOrdersRequest) - if err != nil { - t.Error(err) - } - tradablePairs, err := h.CurrencyPairs.GetPairs(asset.Futures, false) - if err != nil { - t.Error(err) - } - if len(tradablePairs) == 0 { - t.Fatal("no tradable pairs") - } - getOrdersRequest.Pairs = []currency.Pair{tradablePairs[0]} + require.NoError(t, err) + getOrdersRequest.Pairs = []currency.Pair{btcFutureDatedPair} getOrdersRequest.AssetType = asset.Futures _, err = h.GetOrderHistory(context.Background(), &getOrdersRequest) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestCancelAllOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - _, err := h.CancelAllOrders(context.Background(), - &order.Cancel{AssetType: asset.Futures}) - if err != nil { - t.Error(err) - } + _, err := h.CancelAllOrders(context.Background(), &order.Cancel{AssetType: asset.Futures}) + require.NoError(t, err) } func TestQuerySwapIndexPriceInfo(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.QuerySwapIndexPriceInfo(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.QuerySwapIndexPriceInfo(context.Background(), btcusdPair) + require.NoError(t, err) } func TestSwapOpenInterestInformation(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.SwapOpenInterestInformation(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.SwapOpenInterestInformation(context.Background(), btcusdPair) + require.NoError(t, err) } func TestGetSwapMarketDepth(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapMarketDepth(context.Background(), cp, "step0") - if err != nil { - t.Error(err) - } + _, err := h.GetSwapMarketDepth(context.Background(), btcusdPair, "step0") + require.NoError(t, err) } func TestGetSwapKlineData(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapKlineData(context.Background(), - cp, "5min", 5, time.Now().Add(-time.Hour), time.Now()) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapKlineData(context.Background(), btcusdPair, "5min", 5, time.Now().Add(-time.Hour), time.Now()) + require.NoError(t, err) } func TestGetSwapMarketOverview(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapMarketOverview(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapMarketOverview(context.Background(), btcusdPair) + require.NoError(t, err) } func TestGetLastTrade(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetLastTrade(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetLastTrade(context.Background(), btcusdPair) + require.NoError(t, err) } func TestGetBatchTrades(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetBatchTrades(context.Background(), cp, 5) - if err != nil { - t.Error(err) - } + _, err := h.GetBatchTrades(context.Background(), btcusdPair, 5) + require.NoError(t, err) } func TestGetTieredAjustmentFactorInfo(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetTieredAjustmentFactorInfo(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetTieredAjustmentFactorInfo(context.Background(), btcusdPair) + require.NoError(t, err) } func TestGetOpenInterestInfo(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetOpenInterestInfo(context.Background(), - cp, "5min", "cryptocurrency", 50) - if err != nil { - t.Error(err) - } + updatePairsOnce(t, h) + _, err := h.GetOpenInterestInfo(context.Background(), btcusdPair, "5min", "cryptocurrency", 50) + require.NoError(t, err) } func TestGetTraderSentimentIndexAccount(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetTraderSentimentIndexAccount(context.Background(), cp, "5min") - if err != nil { - t.Error(err) - } + _, err := h.GetTraderSentimentIndexAccount(context.Background(), btcusdPair, "5min") + require.NoError(t, err) } func TestGetTraderSentimentIndexPosition(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetTraderSentimentIndexPosition(context.Background(), cp, "5min") - if err != nil { - t.Error(err) - } + _, err := h.GetTraderSentimentIndexPosition(context.Background(), btcusdPair, "5min") + require.NoError(t, err) } func TestGetLiquidationOrders(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - - if _, err = h.GetLiquidationOrders(context.Background(), cp, "closed", 0, 0, "", 0); err != nil { - t.Error(err) - } + _, err := h.GetLiquidationOrders(context.Background(), btcusdPair, "closed", 0, 0, "", 0) + require.NoError(t, err) } func TestGetHistoricalFundingRates(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetHistoricalFundingRatesForPair(context.Background(), cp, 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.GetHistoricalFundingRatesForPair(context.Background(), btcusdPair, 0, 0) + require.NoError(t, err) } func TestGetPremiumIndexKlineData(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetPremiumIndexKlineData(context.Background(), cp, "5min", 15) - if err != nil { - t.Error(err) - } + _, err := h.GetPremiumIndexKlineData(context.Background(), btcusdPair, "5min", 15) + require.NoError(t, err) } func TestGetEstimatedFundingRates(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetPremiumIndexKlineData(context.Background(), cp, "5min", 15) - if err != nil { - t.Error(err) - } + _, err := h.GetPremiumIndexKlineData(context.Background(), btcusdPair, "5min", 15) + require.NoError(t, err) } func TestGetBasisData(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetBasisData(context.Background(), cp, "5min", "close", 5) - if err != nil { - t.Error(err) - } + _, err := h.GetBasisData(context.Background(), btcusdPair, "5min", "close", 5) + require.NoError(t, err) } func TestGetSystemStatusInfo(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSystemStatusInfo(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetSystemStatusInfo(context.Background(), btcusdPair) + require.NoError(t, err) } func TestGetSwapPriceLimits(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapPriceLimits(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapPriceLimits(context.Background(), btcusdPair) + require.NoError(t, err) } func TestGetMarginRates(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("BTC-USDT") - if err != nil { - t.Error(err) - } - _, err = h.GetMarginRates(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetMarginRates(context.Background(), btcusdtPair) + require.NoError(t, err) } func TestGetSwapAccountInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapAccountInfo(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapAccountInfo(context.Background(), ethusdPair) + require.NoError(t, err) } func TestGetSwapPositionsInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapPositionsInfo(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapPositionsInfo(context.Background(), ethusdPair) + require.NoError(t, err) } func TestGetSwapAssetsAndPositions(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapAssetsAndPositions(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapAssetsAndPositions(context.Background(), ethusdPair) + require.NoError(t, err) } func TestGetSwapAllSubAccAssets(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapAllSubAccAssets(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapAllSubAccAssets(context.Background(), ethusdPair) + require.NoError(t, err) } func TestGetSubAccPositionInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSubAccPositionInfo(context.Background(), cp, 0) - if err != nil { - t.Error(err) - } + _, err := h.GetSubAccPositionInfo(context.Background(), ethusdPair, 0) + require.NoError(t, err) } func TestGetAccountFinancialRecords(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetAccountFinancialRecords(context.Background(), cp, "3,4", 15, 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.GetAccountFinancialRecords(context.Background(), ethusdPair, "3,4", 15, 0, 0) + require.NoError(t, err) } -func TestGetSwapSettlementRecords(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapSettlementRecords(context.Background(), - cp, time.Time{}, time.Time{}, 0, 0) - if err != nil { - t.Error(err) - } +func TestGetSwapSettlementRecords(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, h) + _, err := h.GetSwapSettlementRecords(context.Background(), ethusdPair, time.Time{}, time.Time{}, 0, 0) + require.NoError(t, err) } func TestGetAvailableLeverage(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetAvailableLeverage(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetAvailableLeverage(context.Background(), ethusdPair) + require.NoError(t, err) } func TestGetSwapOrderLimitInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapOrderLimitInfo(context.Background(), cp, "limit") - if err != nil { - t.Error(err) - } + _, err := h.GetSwapOrderLimitInfo(context.Background(), ethusdPair, "limit") + require.NoError(t, err) } func TestGetSwapTradingFeeInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapTradingFeeInfo(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapTradingFeeInfo(context.Background(), ethusdPair) + require.NoError(t, err) } func TestGetSwapTransferLimitInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapTransferLimitInfo(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapTransferLimitInfo(context.Background(), ethusdPair) + require.NoError(t, err) } func TestGetSwapPositionLimitInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapPositionLimitInfo(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapPositionLimitInfo(context.Background(), ethusdPair) + require.NoError(t, err) } func TestAccountTransferData(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.AccountTransferData(context.Background(), - cp, "123", "master_to_sub", 15) - if err != nil { - t.Error(err) - } + _, err := h.AccountTransferData(context.Background(), ethusdPair, "123", "master_to_sub", 15) + require.NoError(t, err) } func TestAccountTransferRecords(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.AccountTransferRecords(context.Background(), - cp, "master_to_sub", 12, 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.AccountTransferRecords(context.Background(), ethusdPair, "master_to_sub", 12, 0, 0) + require.NoError(t, err) } func TestPlaceSwapOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.PlaceSwapOrders(context.Background(), - cp, "", "buy", "open", "limit", 0.01, 1, 1) - if err != nil { - t.Error(err) - } + _, err := h.PlaceSwapOrders(context.Background(), ethusdPair, "", "buy", "open", "limit", 0.01, 1, 1) + require.NoError(t, err) } func TestPlaceSwapBatchOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - var req BatchOrderRequestType order1 := batchOrderData{ ContractCode: "ETH-USD", @@ -1245,555 +761,304 @@ func TestPlaceSwapBatchOrders(t *testing.T) { req.Data = append(req.Data, order1, order2) _, err := h.PlaceSwapBatchOrders(context.Background(), req) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestCancelSwapOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.CancelSwapOrder(context.Background(), "test123", "", cp) - if err != nil { - t.Error(err) - } + _, err := h.CancelSwapOrder(context.Background(), "test123", "", ethusdPair) + require.NoError(t, err) } func TestCancelAllSwapOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.CancelAllSwapOrders(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.CancelAllSwapOrders(context.Background(), ethusdPair) + require.NoError(t, err) } func TestPlaceLightningCloseOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.PlaceLightningCloseOrder(context.Background(), - cp, "buy", "lightning", 5, 1) - if err != nil { - t.Error(err) - } + _, err := h.PlaceLightningCloseOrder(context.Background(), ethusdPair, "buy", "lightning", 5, 1) + require.NoError(t, err) } func TestGetSwapOrderInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapOrderInfo(context.Background(), cp, "123", "") - if err != nil { - t.Error(err) - } + _, err := h.GetSwapOrderInfo(context.Background(), ethusdPair, "123", "") + require.NoError(t, err) } func TestGetSwapOrderDetails(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapOrderDetails(context.Background(), - cp, "123", "10", "cancelledOrder", 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapOrderDetails(context.Background(), ethusdPair, "123", "10", "cancelledOrder", 0, 0) + require.NoError(t, err) } func TestGetSwapOpenOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapOpenOrders(context.Background(), cp, 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapOpenOrders(context.Background(), ethusdPair, 0, 0) + require.NoError(t, err) } func TestGetSwapOrderHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapOrderHistory(context.Background(), - cp, "all", "all", - []order.Status{order.PartiallyCancelled, order.Active}, 25, 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapOrderHistory(context.Background(), ethusdPair, "all", "all", []order.Status{order.PartiallyCancelled, order.Active}, 25, 0, 0) + require.NoError(t, err) } func TestGetSwapTradeHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapTradeHistory(context.Background(), - cp, "liquidateShort", 10, 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapTradeHistory(context.Background(), ethusdPair, "liquidateShort", 10, 0, 0) + require.NoError(t, err) } func TestPlaceSwapTriggerOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.PlaceSwapTriggerOrder(context.Background(), - cp, "greaterOrEqual", "buy", "open", "optimal_5", 5, 3, 1, 1) - if err != nil { - t.Error(err) - } + _, err := h.PlaceSwapTriggerOrder(context.Background(), ethusdPair, "greaterOrEqual", "buy", "open", "optimal_5", 5, 3, 1, 1) + require.NoError(t, err) } func TestCancelSwapTriggerOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.CancelSwapTriggerOrder(context.Background(), cp, "test123") - if err != nil { - t.Error(err) - } + _, err := h.CancelSwapTriggerOrder(context.Background(), ethusdPair, "test123") + require.NoError(t, err) } func TestCancelAllSwapTriggerOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.CancelAllSwapTriggerOrders(context.Background(), cp) - if err != nil { - t.Error(err) - } + _, err := h.CancelAllSwapTriggerOrders(context.Background(), ethusdPair) + require.NoError(t, err) } func TestGetSwapTriggerOrderHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString("ETH-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetSwapTriggerOrderHistory(context.Background(), - cp, "open", "all", 15, 0, 0) - if err != nil { - t.Error(err) - } + _, err := h.GetSwapTriggerOrderHistory(context.Background(), ethusdPair, "open", "all", 15, 0, 0) + require.NoError(t, err) } func TestGetSwapMarkets(t *testing.T) { t.Parallel() _, err := h.GetSwapMarkets(context.Background(), currency.EMPTYPAIR) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestGetSpotKline(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString(testSymbol) - if err != nil { - t.Error(err) - } - _, err = h.GetSpotKline(context.Background(), KlinesRequestParams{ - Symbol: cp, - Period: "1min", - }) - if err != nil { - t.Errorf("Huobi TestGetSpotKline: %s", err) - } + _, err := h.GetSpotKline(context.Background(), KlinesRequestParams{Symbol: btcusdtPair, Period: "1min"}) + require.NoError(t, err) } func TestGetHistoricCandles(t *testing.T) { t.Parallel() - pair, err := currency.NewPairFromString("BTC-USDT") - if err != nil { - t.Error(err) - } + + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(h), "Setup Instance must not error") + updatePairsOnce(t, h) endTime := time.Now().Add(-time.Hour).Truncate(time.Hour) - _, err = h.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneMin, endTime.Add(-time.Hour), endTime) - if err != nil { - t.Error(err) - } + _, err := h.GetHistoricCandles(context.Background(), btcusdtPair, asset.Spot, kline.OneMin, endTime.Add(-time.Hour), endTime) + require.NoError(t, err) - _, err = h.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, endTime.AddDate(0, 0, -7), endTime) - if err != nil { - t.Error(err) - } + _, err = h.GetHistoricCandles(context.Background(), btcusdtPair, asset.Spot, kline.OneDay, endTime.AddDate(0, 0, -7), endTime) + require.NoError(t, err) - pairs, err := h.CurrencyPairs.GetPairs(asset.Futures, false) - if err != nil { - t.Error(err) - } - err = h.CurrencyPairs.EnablePair(asset.Futures, pairs[0]) - if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) { - t.Error(err) - } - _, err = h.GetHistoricCandles(context.Background(), pairs[0], asset.Futures, kline.OneDay, endTime.AddDate(0, 0, -7), endTime) - if err != nil { - t.Error(err) - } + _, err = h.GetHistoricCandles(context.Background(), btcFutureDatedPair, asset.Futures, kline.OneDay, endTime.AddDate(0, 0, -7), endTime) + require.NoError(t, err) - pairs, err = h.CurrencyPairs.GetPairs(asset.CoinMarginedFutures, false) - if err != nil { - t.Error(err) - } - err = h.CurrencyPairs.EnablePair(asset.CoinMarginedFutures, pairs[0]) - if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) { - t.Error(err) - } - _, err = h.GetHistoricCandles(context.Background(), pairs[0], asset.CoinMarginedFutures, kline.OneDay, endTime.AddDate(0, 0, -7), endTime) - if err != nil { - t.Error(err) - } + _, err = h.GetHistoricCandles(context.Background(), btcusdPair, asset.CoinMarginedFutures, kline.OneDay, endTime.AddDate(0, 0, -7), endTime) + require.NoError(t, err) } func TestGetHistoricCandlesExtended(t *testing.T) { t.Parallel() - pair, err := currency.NewPairFromString("BTC-USDT") - if err != nil { - t.Error(err) - } + + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(h), "Setup Instance must not error") + updatePairsOnce(t, h) + endTime := time.Now().Add(-time.Hour).Truncate(time.Hour) - _, err = h.GetHistoricCandlesExtended(context.Background(), pair, asset.Spot, kline.OneMin, endTime.Add(-time.Hour), endTime) - if !errors.Is(err, common.ErrFunctionNotSupported) { - t.Error(err) - } + _, err := h.GetHistoricCandlesExtended(context.Background(), btcusdtPair, asset.Spot, kline.OneMin, endTime.Add(-time.Hour), endTime) + require.ErrorIs(t, err, common.ErrFunctionNotSupported) - pairs, err := h.CurrencyPairs.GetPairs(asset.Futures, false) - if err != nil { - t.Error(err) - } - err = h.CurrencyPairs.EnablePair(asset.Futures, pairs[0]) - if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) { - t.Error(err) - } - _, err = h.GetHistoricCandlesExtended(context.Background(), pairs[0], asset.Futures, kline.OneDay, endTime.AddDate(0, 0, -7), endTime) - if err != nil { - t.Error(err) - } + _, err = h.GetHistoricCandlesExtended(context.Background(), btcFutureDatedPair, asset.Futures, kline.OneDay, endTime.AddDate(0, 0, -7), endTime) + require.NoError(t, err) // demonstrate that adjusting time doesn't wreck non-day intervals - _, err = h.GetHistoricCandlesExtended(context.Background(), pairs[0], asset.Futures, kline.OneHour, endTime.AddDate(0, 0, -1), endTime) - if err != nil { - t.Error(err) - } + _, err = h.GetHistoricCandlesExtended(context.Background(), btcFutureDatedPair, asset.Futures, kline.OneHour, endTime.AddDate(0, 0, -1), endTime) + require.NoError(t, err) - pairs, err = h.CurrencyPairs.GetPairs(asset.CoinMarginedFutures, false) - if err != nil { - t.Error(err) - } - err = h.CurrencyPairs.EnablePair(asset.CoinMarginedFutures, pairs[0]) - if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) { - t.Error(err) - } - _, err = h.GetHistoricCandlesExtended(context.Background(), pairs[0], asset.CoinMarginedFutures, kline.OneDay, endTime.AddDate(0, 0, -7), time.Now()) - if err != nil { - t.Error(err) - } + _, err = h.GetHistoricCandlesExtended(context.Background(), btcusdPair, asset.CoinMarginedFutures, kline.OneDay, endTime.AddDate(0, 0, -7), time.Now()) + require.NoError(t, err) - _, err = h.GetHistoricCandlesExtended(context.Background(), pairs[0], asset.CoinMarginedFutures, kline.OneHour, endTime.AddDate(0, 0, -1), time.Now()) - if err != nil { - t.Error(err) - } + _, err = h.GetHistoricCandlesExtended(context.Background(), btcusdPair, asset.CoinMarginedFutures, kline.OneHour, endTime.AddDate(0, 0, -1), time.Now()) + require.NoError(t, err) } func TestGetMarketDetailMerged(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString(testSymbol) - if err != nil { - t.Error(err) - } - _, err = h.GetMarketDetailMerged(context.Background(), cp) - if err != nil { - t.Errorf("Huobi TestGetMarketDetailMerged: %s", err) - } + _, err := h.GetMarketDetailMerged(context.Background(), btcusdtPair) + require.NoError(t, err) } -func TestGetDepth(t *testing.T) { - t.Parallel() - cp, err := currency.NewPairFromString(testSymbol) - if err != nil { - t.Error(err) - } - _, err = h.GetDepth(context.Background(), +func TestGetDepth(t *testing.T) { + t.Parallel() + _, err := h.GetDepth(context.Background(), &OrderBookDataRequestParams{ - Symbol: cp, + Symbol: btcusdtPair, Type: OrderBookDataRequestParamsTypeStep1, }) - if err != nil { - t.Errorf("Huobi TestGetDepth: %s", err) - } + require.NoError(t, err) } func TestGetTrades(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString(testSymbol) - if err != nil { - t.Error(err) - } - _, err = h.GetTrades(context.Background(), cp) - if err != nil { - t.Errorf("Huobi TestGetTrades: %s", err) - } + _, err := h.GetTrades(context.Background(), btcusdtPair) + require.NoError(t, err) } func TestGetLatestSpotPrice(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString(testSymbol) - if err != nil { - t.Error(err) - } - _, err = h.GetLatestSpotPrice(context.Background(), cp) - if err != nil { - t.Errorf("Huobi GetLatestSpotPrice: %s", err) - } + _, err := h.GetLatestSpotPrice(context.Background(), btcusdtPair) + require.NoError(t, err) } func TestGetTradeHistory(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString(testSymbol) - if err != nil { - t.Error(err) - } - _, err = h.GetTradeHistory(context.Background(), cp, 50) - if err != nil { - t.Errorf("Huobi TestGetTradeHistory: %s", err) - } + _, err := h.GetTradeHistory(context.Background(), btcusdtPair, 50) + require.NoError(t, err) } func TestGetMarketDetail(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString(testSymbol) - if err != nil { - t.Error(err) - } - _, err = h.GetMarketDetail(context.Background(), cp) - if err != nil { - t.Errorf("Huobi TestGetTradeHistory: %s", err) - } + _, err := h.GetMarketDetail(context.Background(), btcusdtPair) + require.NoError(t, err) } func TestGetSymbols(t *testing.T) { t.Parallel() _, err := h.GetSymbols(context.Background()) - if err != nil { - t.Errorf("Huobi TestGetSymbols: %s", err) - } + require.NoError(t, err) } func TestGetCurrencies(t *testing.T) { t.Parallel() _, err := h.GetCurrencies(context.Background()) - if err != nil { - t.Errorf("Huobi TestGetCurrencies: %s", err) - } + require.NoError(t, err) } func TestGet24HrMarketSummary(t *testing.T) { t.Parallel() cp, err := currency.NewPairFromString("ethusdt") - if err != nil { - t.Error(err) - } + require.NoError(t, err) _, err = h.Get24HrMarketSummary(context.Background(), cp) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestGetTicker(t *testing.T) { t.Parallel() _, err := h.GetTickers(context.Background()) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestGetTimestamp(t *testing.T) { t.Parallel() st, err := h.GetCurrentServerTime(context.Background()) - if err != nil { - t.Errorf("Huobi TestGetTimestamp: %s", err) - } - - if st.IsZero() { - t.Error("expected a time") - } + require.NoError(t, err) + assert.NotEmpty(t, st, "GetCurrentServerTime should return a time") } func TestWrapperGetServerTime(t *testing.T) { t.Parallel() st, err := h.GetServerTime(context.Background(), asset.Spot) - if !errors.Is(err, nil) { - t.Errorf("received: '%v' but expected: '%v'", err, nil) - } - - if st.IsZero() { - t.Error("expected a time") - } + require.NoError(t, err) + assert.NotEmpty(t, st, "GetServerTime should return a time") } func TestGetAccounts(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - _, err := h.GetAccounts(context.Background()) - if err != nil { - t.Errorf("Huobi GetAccounts: %s", err) - } + require.NoError(t, err) } func TestGetAccountBalance(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - result, err := h.GetAccounts(context.Background()) - if err != nil { - t.Errorf("Huobi GetAccounts: %s", err) - } + require.NoError(t, err, "GetAccounts must not error") userID := strconv.FormatInt(result[0].ID, 10) _, err = h.GetAccountBalance(context.Background(), userID) - if err != nil { - t.Errorf("Huobi GetAccountBalance: %s", err) - } + require.NoError(t, err, "GetAccountBalance must not error") } func TestGetAggregatedBalance(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.GetAggregatedBalance(context.Background()) - if err != nil { - t.Errorf("Huobi GetAggregatedBalance: %s", err) - } + require.NoError(t, err) } func TestSpotNewOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - - cp, err := currency.NewPairFromString(testSymbol) - if err != nil { - t.Error(err) - } arg := SpotNewOrderRequestParams{ - Symbol: cp, + Symbol: btcusdtPair, AccountID: 1997024, Amount: 0.01, Price: 10.1, Type: SpotNewOrderRequestTypeBuyLimit, } - _, err = h.SpotNewOrder(context.Background(), &arg) - if err != nil { - t.Errorf("Huobi SpotNewOrder: %s", err) - } + _, err := h.SpotNewOrder(context.Background(), &arg) + require.NoError(t, err) } func TestCancelExistingOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - _, err := h.CancelExistingOrder(context.Background(), 1337) - if err == nil { - t.Error("Huobi TestCancelExistingOrder Expected error") - } + assert.Error(t, err) } func TestGetOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - _, err := h.GetOrder(context.Background(), 1337) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestGetMarginLoanOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString(testSymbol) - if err != nil { - t.Error(err) - } - _, err = h.GetMarginLoanOrders(context.Background(), - cp, "", "", "", "", "", "", "") - if err != nil { - t.Errorf("Huobi TestGetMarginLoanOrders: %s", err) - } + _, err := h.GetMarginLoanOrders(context.Background(), btcusdtPair, "", "", "", "", "", "", "") + require.NoError(t, err) } func TestGetMarginAccountBalance(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - cp, err := currency.NewPairFromString(testSymbol) - if err != nil { - t.Error(err) - } - _, err = h.GetMarginAccountBalance(context.Background(), cp) - if err != nil { - t.Errorf("Huobi TestGetMarginAccountBalance: %s", err) - } + _, err := h.GetMarginAccountBalance(context.Background(), btcusdtPair) + require.NoError(t, err) } func TestCancelWithdraw(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - _, err := h.CancelWithdraw(context.Background(), 1337) - if err == nil { - t.Error("Huobi TestCancelWithdraw Expected error") - } + require.Error(t, err) } func setFeeBuilder() *exchange.FeeBuilder { @@ -1813,17 +1078,11 @@ func setFeeBuilder() *exchange.FeeBuilder { func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = setFeeBuilder() _, err := h.GetFeeByType(context.Background(), feeBuilder) - if err != nil { - t.Error(err) - } + require.NoError(t, err) if !sharedtestvalues.AreAPICredentialsSet(h) { - if feeBuilder.FeeType != exchange.OfflineTradeFee { - t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) - } + assert.Equal(t, exchange.OfflineTradeFee, feeBuilder.FeeType) } else { - if feeBuilder.FeeType != exchange.CryptocurrencyTradeFee { - t.Errorf("Expected %v, received %v", exchange.CryptocurrencyTradeFee, feeBuilder.FeeType) - } + assert.Equal(t, exchange.CryptocurrencyTradeFee, feeBuilder.FeeType) } } @@ -1831,70 +1090,66 @@ func TestGetFee(t *testing.T) { t.Parallel() var feeBuilder = setFeeBuilder() // CryptocurrencyTradeFee Basic - if _, err := h.GetFee(feeBuilder); err != nil { - t.Error(err) - } + _, err := h.GetFee(feeBuilder) + require.NoError(t, err) // CryptocurrencyTradeFee High quantity feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if _, err := h.GetFee(feeBuilder); err != nil { - t.Error(err) - } + _, err = h.GetFee(feeBuilder) + require.NoError(t, err) + // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if _, err := h.GetFee(feeBuilder); err != nil { - t.Error(err) - } + _, err = h.GetFee(feeBuilder) + require.NoError(t, err) + // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if _, err := h.GetFee(feeBuilder); err != nil { - t.Error(err) - } + _, err = h.GetFee(feeBuilder) + require.NoError(t, err) + // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if _, err := h.GetFee(feeBuilder); err != nil { - t.Error(err) - } + _, err = h.GetFee(feeBuilder) + require.NoError(t, err) + // CryptocurrencyWithdrawalFee Invalid currency feeBuilder = setFeeBuilder() feeBuilder.Pair.Base = currency.NewCode("hello") feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if _, err := h.GetFee(feeBuilder); err != nil { - t.Error(err) - } + _, err = h.GetFee(feeBuilder) + require.NoError(t, err) + // CryptocurrencyDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyDepositFee - if _, err := h.GetFee(feeBuilder); err != nil { - t.Error(err) - } + _, err = h.GetFee(feeBuilder) + require.NoError(t, err) + // InternationalBankDepositFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankDepositFee - if _, err := h.GetFee(feeBuilder); err != nil { - t.Error(err) - } + _, err = h.GetFee(feeBuilder) + require.NoError(t, err) + // InternationalBankWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee feeBuilder.FiatCurrency = currency.USD - if _, err := h.GetFee(feeBuilder); err != nil { - t.Error(err) - } + _, err = h.GetFee(feeBuilder) + require.NoError(t, err) } func TestFormatWithdrawPermissions(t *testing.T) { t.Parallel() expectedResult := exchange.AutoWithdrawCryptoWithSetupText + " & " + exchange.NoFiatWithdrawalsText withdrawPermissions := h.FormatWithdrawPermissions() - if withdrawPermissions != expectedResult { - t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) - } + assert.Equal(t, expectedResult, withdrawPermissions) } func TestGetActiveOrders(t *testing.T) { @@ -1907,10 +1162,10 @@ func TestGetActiveOrders(t *testing.T) { } _, err := h.GetActiveOrders(context.Background(), &getOrdersRequest) - if sharedtestvalues.AreAPICredentialsSet(h) && err == nil { - t.Errorf("Could not get open orders: %s", err) - } else if !sharedtestvalues.AreAPICredentialsSet(h) && err == nil { - t.Error("Expecting an error when no keys are set") + if sharedtestvalues.AreAPICredentialsSet(h) { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, exchange.ErrCredentialsAreEmpty) } } @@ -1919,11 +1174,8 @@ func TestGetActiveOrders(t *testing.T) { func TestSubmitOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - accounts, err := h.GetAccounts(context.Background()) - if err != nil { - t.Errorf("Failed to get accounts. Err: %s", err) - } + require.NoError(t, err, "GetAccounts must not error") var orderSubmission = &order.Submit{ Exchange: h.Name, @@ -1939,38 +1191,28 @@ func TestSubmitOrder(t *testing.T) { AssetType: asset.Spot, } response, err := h.SubmitOrder(context.Background(), orderSubmission) - if sharedtestvalues.AreAPICredentialsSet(h) && (err != nil || response.Status != order.New) { - t.Errorf("Order failed to be placed: %v", err) - } + require.NoError(t, err) + assert.Equal(t, order.New, response.Status, "response status should be correct") } func TestCancelExchangeOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, h, canManipulateRealOrders) - - currencyPair := currency.NewPair(currency.LTC, currency.BTC) + sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) var orderCancellation = &order.Cancel{ OrderID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - Pair: currencyPair, + Pair: btcusdtPair, AssetType: asset.Spot, } err := h.CancelOrder(context.Background(), orderCancellation) - - if !sharedtestvalues.AreAPICredentialsSet(h) && err == nil { - t.Error("Expecting an error when no keys are set") - } - if sharedtestvalues.AreAPICredentialsSet(h) && err != nil { - t.Errorf("Could not cancel orders: %v", err) - } + require.NoError(t, err) } func TestCancelAllExchangeOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - currencyPair := currency.NewPair(currency.LTC, currency.BTC) var orderCancellation = order.Cancel{ OrderID: "1", @@ -1981,489 +1223,305 @@ func TestCancelAllExchangeOrders(t *testing.T) { } _, err := h.CancelAllOrders(context.Background(), &orderCancellation) - if err != nil { - t.Error(err) - } -} - -func TestGetAccountInfo(t *testing.T) { - t.Parallel() - if !sharedtestvalues.AreAPICredentialsSet(h) { - _, err := h.UpdateAccountInfo(context.Background(), - asset.CoinMarginedFutures) - if err == nil { - t.Error("GetAccountInfo() Expected error") - } - _, err = h.UpdateAccountInfo(context.Background(), asset.Futures) - if err == nil { - t.Error("GetAccountInfo() Expected error") - } - } else { - _, err := h.UpdateAccountInfo(context.Background(), - asset.CoinMarginedFutures) - if err != nil { - // Spot and Futures have separate api keys. Please ensure that the correct keys are provided - t.Error(err) - } - _, err = h.UpdateAccountInfo(context.Background(), asset.Futures) - if err != nil { - // Spot and Futures have separate api keys. Please ensure that the correct keys are provided - t.Error(err) - } - } + require.NoError(t, err) } -func TestGetSpotAccountInfo(t *testing.T) { +func TestUpdateAccountInfo(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - - _, err := h.UpdateAccountInfo(context.Background(), asset.Spot) - if err != nil { - // Spot and Futures have separate api keys. Please ensure that the correct keys are provided - t.Error(err) + sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) + for _, a := range []asset.Item{asset.Spot, asset.CoinMarginedFutures, asset.Futures} { + _, err := h.UpdateAccountInfo(context.Background(), a) + require.NoErrorf(t, err, "UpdateAccountInfo must not error for asset %s; Spot and Futures have separate api keys. Please ensure that the correct keys are provided", a) } } func TestModifyOrder(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, h, canManipulateRealOrders) - - _, err := h.ModifyOrder(context.Background(), - &order.Modify{AssetType: asset.Spot}) - if err == nil { - t.Error("ModifyOrder() Expected error") - } -} - -func TestWithdraw(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, h, canManipulateRealOrders) - - withdrawCryptoRequest := withdraw.Request{ - Exchange: h.Name, - Amount: -1, - Currency: currency.BTC, - Description: "WITHDRAW IT ALL", - Crypto: withdraw.CryptoRequest{ - Address: core.BitcoinDonationAddress, - }, - } - - _, err := h.WithdrawCryptocurrencyFunds(context.Background(), - &withdrawCryptoRequest) - if !sharedtestvalues.AreAPICredentialsSet(h) && err == nil { - t.Error("Expecting an error when no keys are set") - } - if sharedtestvalues.AreAPICredentialsSet(h) && err != nil { - t.Errorf("Withdraw failed to be placed: %v", err) - } -} - -func TestWithdrawFiat(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCannotManipulateOrders(t, h, canManipulateRealOrders) - - var withdrawFiatRequest = withdraw.Request{} - _, err := h.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, h, canManipulateRealOrders) - - var withdrawFiatRequest = withdraw.Request{} - _, err := h.WithdrawFiatFundsToInternationalBank(context.Background(), - &withdrawFiatRequest) - if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) - } -} - -func TestQueryDepositAddress(t *testing.T) { - t.Parallel() - - _, err := h.QueryDepositAddress(context.Background(), currency.USDT) - if !sharedtestvalues.AreAPICredentialsSet(h) && err == nil { - t.Error("Expecting an error when no keys are set") - } - if sharedtestvalues.AreAPICredentialsSet(h) && err != nil { - t.Error(err) - } -} - -func TestGetDepositAddress(t *testing.T) { - t.Parallel() - - _, err := h.GetDepositAddress(context.Background(), currency.USDT, "", "uSdTeRc20") - if !sharedtestvalues.AreAPICredentialsSet(h) && err == nil { - t.Error("Expecting an error when no keys are set") - } - if sharedtestvalues.AreAPICredentialsSet(h) && err != nil { - t.Error(err) - } -} - -func TestQueryWithdrawQuota(t *testing.T) { - t.Parallel() - - _, err := h.QueryWithdrawQuotas(context.Background(), - currency.BTC.Lower().String()) - if !sharedtestvalues.AreAPICredentialsSet(h) && err == nil { - t.Error("Expecting an error when no keys are set") - } - if sharedtestvalues.AreAPICredentialsSet(h) && err != nil { - t.Error(err) - } -} - -// TestWsGetAccountsList connects to WS, logs in, gets account list -func TestWsGetAccountsList(t *testing.T) { - setupWsTests(t) - if _, err := h.wsGetAccountsList(context.Background()); err != nil { - t.Error(err) - } -} - -// TestWsGetOrderList connects to WS, logs in, gets order list -func TestWsGetOrderList(t *testing.T) { - setupWsTests(t) - p, err := currency.NewPairFromString("ethbtc") - if err != nil { - t.Error(err) - } - _, err = h.wsGetOrdersList(context.Background(), 1, p) - if err != nil { - t.Error(err) - } -} - -// TestWsGetOrderDetails connects to WS, logs in, gets order details -func TestWsGetOrderDetails(t *testing.T) { - setupWsTests(t) - orderID := "123" - _, err := h.wsGetOrderDetails(context.Background(), orderID) - if err != nil { - t.Error(err) - } + t.Parallel() + sharedtestvalues.SkipTestIfCannotManipulateOrders(t, h, canManipulateRealOrders) + _, err := h.ModifyOrder(context.Background(), &order.Modify{AssetType: asset.Spot}) + require.Error(t, err, "ModifyOrder must error without any order details") } -func TestWsSubResponse(t *testing.T) { - pressXToJSON := []byte(`{ - "op": "sub", - "cid": "123", - "err-code": 0, - "ts": 1489474081631, - "topic": "accounts" -}`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) +func TestWithdraw(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCannotManipulateOrders(t, h, canManipulateRealOrders) + + withdrawCryptoRequest := withdraw.Request{ + Exchange: h.Name, + Amount: -1, + Currency: currency.BTC, + Description: "WITHDRAW IT ALL", + Crypto: withdraw.CryptoRequest{ + Address: core.BitcoinDonationAddress, + }, } + + _, err := h.WithdrawCryptocurrencyFunds(context.Background(), &withdrawCryptoRequest) + require.ErrorContains(t, err, withdraw.ErrStrAmountMustBeGreaterThanZero) } -func TestWsKline(t *testing.T) { - pressXToJSON := []byte(`{ - "ch": "market.btcusdt.kline.1min", - "ts": 1489474082831, - "tick": { - "id": 1489464480, - "amount": 0.0, - "count": 0, - "open": 7962.62, - "close": 7962.62, - "low": 7962.62, - "high": 7962.62, - "vol": 0.0 - } -}`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } +func TestWithdrawFiat(t *testing.T) { + t.Parallel() + _, err := h.WithdrawFiatFunds(context.Background(), &withdraw.Request{}) + assert.ErrorIs(t, err, common.ErrFunctionNotSupported) } -func TestWsUnsubscribe(t *testing.T) { - pressXToJSON := []byte(`{ - "id": "id4", - "status": "ok", - "unsubbed": "market.btcusdt.trade.detail", - "ts": 1494326028889 -}`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } +func TestWithdrawInternationalBank(t *testing.T) { + t.Parallel() + _, err := h.WithdrawFiatFundsToInternationalBank(context.Background(), &withdraw.Request{}) + assert.ErrorIs(t, err, common.ErrFunctionNotSupported) } -func TestWsKlineArray(t *testing.T) { - pressXToJSON := []byte(`{ - "status": "ok", - "rep": "market.btcusdt.kline.1min", - "data": [ - { - "amount": 1.6206, - "count": 3, - "id": 1494465840, - "open": 9887.00, - "close": 9885.00, - "low": 9885.00, - "high": 9887.00, - "vol": 16021.632026 - }, - { - "amount": 2.2124, - "count": 6, - "id": 1494465900, - "open": 9885.00, - "close": 9880.00, - "low": 9880.00, - "high": 9885.00, - "vol": 21859.023500 - } - ] -}`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) +func TestQueryDepositAddress(t *testing.T) { + t.Parallel() + _, err := h.QueryDepositAddress(context.Background(), currency.USDT) + if sharedtestvalues.AreAPICredentialsSet(h) { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, exchange.ErrCredentialsAreEmpty) } } -func TestWsMarketDepth(t *testing.T) { - pressXToJSON := []byte(`{ - "ch": "market.htusdt.depth.step0", - "ts": 1572362902027, - "tick": { - "bids": [ - [3.7721, 344.86], - [3.7709, 46.66] - ], - "asks": [ - [3.7745, 15.44], - [3.7746, 70.52] - ], - "version": 100434317651, - "ts": 1572362902012 - } -}`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) +func TestGetDepositAddress(t *testing.T) { + t.Parallel() + _, err := h.GetDepositAddress(context.Background(), currency.USDT, "", "uSdTeRc20") + if sharedtestvalues.AreAPICredentialsSet(h) { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, exchange.ErrCredentialsAreEmpty) } } -func TestWsBestBidOffer(t *testing.T) { - pressXToJSON := []byte(`{ - "ch": "market.btcusdt.bbo", - "ts": 1489474082831, - "tick": { - "symbol": "btcusdt", - "quoteTime": "1489474082811", - "bid": "10008.31", - "bidSize": "0.01", - "ask": "10009.54", - "askSize": "0.3" - } - }`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) +func TestQueryWithdrawQuota(t *testing.T) { + t.Parallel() + _, err := h.QueryWithdrawQuotas(context.Background(), currency.BTC.Lower().String()) + if sharedtestvalues.AreAPICredentialsSet(h) { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, exchange.ErrCredentialsAreEmpty) } } +func TestWsCandles(t *testing.T) { + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(h), "Setup Instance must not error") + err := h.Websocket.AddSubscriptions(h.Websocket.Conn, &subscription.Subscription{Key: "market.btcusdt.kline.1min", Asset: asset.Spot, Pairs: currency.Pairs{btcusdtPair}, Channel: subscription.CandlesChannel}) + require.NoError(t, err, "AddSubscriptions must not error") + testexch.FixtureToDataHandler(t, "testdata/wsCandles.json", h.wsHandleData) + close(h.Websocket.DataHandler) + require.Len(t, h.Websocket.DataHandler, 1, "Must see correct number of records") + cAny := <-h.Websocket.DataHandler + c, ok := cAny.(stream.KlineData) + require.True(t, ok, "Must get the correct type from DataHandler") + exp := stream.KlineData{ + Timestamp: time.UnixMilli(1489474082831), + Pair: btcusdtPair, + AssetType: asset.Spot, + Exchange: h.Name, + OpenPrice: 7962.62, + ClosePrice: 8014.56, + HighPrice: 14962.77, + LowPrice: 5110.14, + Volume: 4.4, + Interval: "0s", + } + assert.Equal(t, exp, c) +} + +func TestWsOrderbook(t *testing.T) { + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(h), "Setup Instance must not error") + err := h.Websocket.AddSubscriptions(h.Websocket.Conn, &subscription.Subscription{Key: "market.btcusdt.depth.step0", Asset: asset.Spot, Pairs: currency.Pairs{btcusdtPair}, Channel: subscription.OrderbookChannel}) + require.NoError(t, err, "AddSubscriptions must not error") + testexch.FixtureToDataHandler(t, "testdata/wsOrderbook.json", h.wsHandleData) + close(h.Websocket.DataHandler) + require.Len(t, h.Websocket.DataHandler, 1, "Must see correct number of records") + dAny := <-h.Websocket.DataHandler + d, ok := dAny.(*orderbook.Depth) + require.True(t, ok, "Must get the correct type from DataHandler") + require.NotNil(t, d) + l, err := d.GetAskLength() + require.NoError(t, err, "GetAskLength must not error") + assert.Equal(t, 2, l, "Ask length should be correct") + liq, _, err := d.TotalAskAmounts() + require.NoError(t, err, "TotalAskAmount must not error") + assert.Equal(t, 0.502591, liq, "Ask Liquidity should be correct") + l, err = d.GetBidLength() + require.NoError(t, err, "GetBidLength must not error") + assert.Equal(t, 2, l, "Bid length should be correct") + liq, _, err = d.TotalBidAmounts() + require.NoError(t, err, "TotalBidAmount must not error") + assert.Equal(t, 0.56281, liq, "Bid Liquidity should be correct") +} + +// TestWsTradeDetail checks we can send a trade detail through +// We can't currently easily see the result with the current DB instance, so we just check it doesn't error func TestWsTradeDetail(t *testing.T) { - pressXToJSON := []byte(`{ - "ch": "market.btcusdt.trade.detail", - "ts": 1489474082831, - "tick": { - "id": 14650745135, - "ts": 1533265950234, - "data": [ - { - "amount": 0.0099, - "ts": 1533265950234, - "id": 146507451359183894799, - "tradeId": 102043495674, - "price": 401.74, - "direction": "buy" - } - ] - } - }`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(h), "Setup Instance must not error") + err := h.Websocket.AddSubscriptions(h.Websocket.Conn, &subscription.Subscription{Key: "market.btcusdt.trade.detail", Asset: asset.Spot, Pairs: currency.Pairs{btcusdtPair}, Channel: subscription.AllTradesChannel}) + require.NoError(t, err, "AddSubscriptions must not error") + h.SetSaveTradeDataStatus(true) + testexch.FixtureToDataHandler(t, "testdata/wsAllTrades.json", h.wsHandleData) + close(h.Websocket.DataHandler) + require.Empty(t, h.Websocket.DataHandler, "Must not see any errors going to datahandler") } func TestWsTicker(t *testing.T) { - pressXToJSON := []byte(`{ - "rep": "market.btcusdt.detail", - "id": "id11", - "data":{ - "amount": 12224.2922, - "open": 9790.52, - "close": 10195.00, - "high": 10300.00, - "ts": 1494496390000, - "id": 1494496390, - "count": 15195, - "low": 9657.00, - "vol": 121906001.754751 - } -}`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(h), "Setup Instance must not error") + err := h.Websocket.AddSubscriptions(h.Websocket.Conn, &subscription.Subscription{Key: "market.btcusdt.detail", Asset: asset.Spot, Pairs: currency.Pairs{btcusdtPair}, Channel: subscription.TickerChannel}) + require.NoError(t, err, "AddSubscriptions must not error") + testexch.FixtureToDataHandler(t, "testdata/wsTicker.json", h.wsHandleData) + close(h.Websocket.DataHandler) + require.Len(t, h.Websocket.DataHandler, 1, "Must see correct number of records") + tickAny := <-h.Websocket.DataHandler + tick, ok := tickAny.(*ticker.Price) + require.True(t, ok, "Must get the correct type from DataHandler") + require.NotNil(t, tick) + exp := &ticker.Price{ + High: 52924.14, + Low: 51000, + Bid: 0, + Volume: 13991.028076056185, + QuoteVolume: 7.27676440200527e+08, + Open: 51823.62, + Close: 52379.99, + Pair: btcusdtPair, + ExchangeName: h.Name, + AssetType: asset.Spot, + LastUpdated: time.UnixMilli(1630998026649), + } + assert.Equal(t, exp, tick) } func TestWsAccountUpdate(t *testing.T) { - pressXToJSON := []byte(`{ - "op": "notify", - "ts": 1522856623232, - "topic": "accounts", - "data": { - "event": "order.place", - "list": [ - { - "account-id": 419013, - "currency": "usdt", - "type": "trade", - "balance": "500009195917.4362872650" - } - ] - } - }`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(h), "Setup Instance must not error") + err := h.Websocket.AddSubscriptions(h.Websocket.Conn, &subscription.Subscription{Key: "accounts.update#2", Asset: asset.Spot, Pairs: currency.Pairs{btcusdtPair}, Channel: subscription.MyAccountChannel}) + require.NoError(t, err, "AddSubscriptions must not error") + h.SetSaveTradeDataStatus(true) + testexch.FixtureToDataHandler(t, "testdata/wsMyAccount.json", h.wsHandleData) + close(h.Websocket.DataHandler) + require.Len(t, h.Websocket.DataHandler, 3, "Must see correct number of records") + exp := []WsAccountUpdate{ + {Currency: "btc", AccountID: 123456, Balance: 23.111, ChangeType: "transfer", AccountType: "trade", ChangeTime: types.Time(time.UnixMilli(1568601800000)), SeqNum: 1}, + {Currency: "btc", AccountID: 33385, Available: 2028.69, ChangeType: "order.match", AccountType: "trade", ChangeTime: types.Time(time.UnixMilli(1574393385167)), SeqNum: 2}, + {Currency: "usdt", AccountID: 14884859, Available: 20.29388158, Balance: 20.29388158, AccountType: "trade", SeqNum: 3}, + } + for _, e := range exp { + uAny := <-h.Websocket.DataHandler + u, ok := uAny.(WsAccountUpdate) + require.True(t, ok, "Must get the correct type from DataHandler") + require.NotNil(t, u) + assert.Equal(t, e, u) } } func TestWsOrderUpdate(t *testing.T) { - pressXToJSON := []byte(`{ - "op": "notify", - "topic": "orders.htusdt", - "ts": 1522856623232, - "data": { - "seq-id": 94984, - "order-id": 2039498445, - "symbol": "btcusdt", - "account-id": 100077, - "order-amount": "5000.000000000000000000", - "order-price": "1.662100000000000000", - "created-at": 1522858623622, - "order-type": "buy-limit", - "order-source": "api", - "order-state": "filled", - "role": "taker", - "price": "1.662100000000000000", - "filled-amount": "5000.000000000000000000", - "unfilled-amount": "0.000000000000000000", - "filled-cash-amount": "8301.357280000000000000", - "filled-fees": "8.000000000000000000" - } -}`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } -} - -func TestWsSubsbOp(t *testing.T) { - pressXToJSON := []byte(`{ - "op": "unsub", - "topic": "accounts", - "cid": "123" - }`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } - pressXToJSON = []byte(`{ - "op": "sub", - "cid": "123", - "err-code": 0, - "ts": 1489474081631, - "topic": "accounts" - }`) - err = h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) - } -} - -func TestWsMarketByPrice(t *testing.T) { - pressXToJSON := []byte(`{ - "ch": "market.btcusdt.mbp.150", - "ts": 1573199608679, - "tick": { - "seqNum": 100020146795, - "prevSeqNum": 100020146794, - "bids": [], - "asks": [ - [645.140000000000000000, 26.755973959140651643] - ] - } - }`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(h), "Setup Instance must not error") + err := h.Websocket.AddSubscriptions(h.Websocket.Conn, &subscription.Subscription{Key: "orders#*", Asset: asset.Spot, Pairs: currency.Pairs{btcusdtPair}, Channel: subscription.MyOrdersChannel}) + require.NoError(t, err, "AddSubscriptions must not error") + h.SetSaveTradeDataStatus(true) + errs := testexch.FixtureToDataHandlerWithErrors(t, "testdata/wsMyOrders.json", h.wsHandleData) + close(h.Websocket.DataHandler) + require.Equal(t, 1, len(errs), "Must receive the correct number of errors back") + require.ErrorContains(t, errs[0].Err, "error with order `test1`: invalid.client.order.id (NT) (2002)") + require.Len(t, h.Websocket.DataHandler, 4, "Must see correct number of records") + exp := []*order.Detail{ + { + Exchange: h.Name, + Pair: btcusdtPair, + Side: order.Buy, + Status: order.Rejected, + ClientOrderID: "test1", + AssetType: asset.Spot, + LastUpdated: time.UnixMicro(1583853365586000), + }, + { + Exchange: h.Name, + Pair: btcusdtPair, + Side: order.Buy, + Status: order.Cancelled, + ClientOrderID: "test2", + AssetType: asset.Spot, + LastUpdated: time.UnixMicro(1583853365586000), + }, + { + Exchange: h.Name, + Pair: btcusdtPair, + Side: order.Sell, + Status: order.New, + ClientOrderID: "test3", + AssetType: asset.Spot, + Price: 77, + Amount: 2, + Type: order.Limit, + OrderID: "27163533", + LastUpdated: time.UnixMicro(1583853365586000), + }, + { + Exchange: h.Name, + Pair: btcusdtPair, + Side: order.Buy, + Status: order.New, + AssetType: asset.Spot, + Price: 70000, + Amount: 0.000157, + Type: order.Limit, + OrderID: "1199329381585359", + LastUpdated: time.UnixMicro(1731039387696000), + }, } - pressXToJSON = []byte(`{ - "id": "id2", - "rep": "market.btcusdt.mbp.150", - "status": "ok", - "data": { - "seqNum": 100020142010, - "bids": [ - [618.37, 71.594], - [423.33, 77.726], - [223.18, 47.997], - [219.34, 24.82], - [210.34, 94.463] - ], - "asks": [ - [650.59, 14.909733438479636], - [650.63, 97.996], - [650.77, 97.465], - [651.23, 83.973], - [651.42, 34.465] - ] - } - }`) - err = h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) + for _, e := range exp { + m := <-h.Websocket.DataHandler + require.IsType(t, &order.Detail{}, m, "Must get the correct type from DataHandler") + d, _ := m.(*order.Detail) + require.NotNil(t, d) + assert.Equal(t, e, d, "Order Detail should match") } } -func TestWsOrdersUpdate(t *testing.T) { - pressXToJSON := []byte(`{ - "op": "notify", - "ts": 1522856623232, - "topic": "orders.btcusdt.update", - "data": { - "unfilled-amount": "0.000000000000000000", - "filled-amount": "5000.000000000000000000", - "price": "1.662100000000000000", - "order-id": 2039498445, - "symbol": "btcusdt", - "match-id": 94984, - "filled-cash-amount": "8301.357280000000000000", - "role": "taker|maker", - "order-state": "filled", - "client-order-id": "a0001", - "order-type": "buy-limit" - } - }`) - err := h.wsHandleData(pressXToJSON) - if err != nil { - t.Error(err) +func TestWsMyTrades(t *testing.T) { + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(h), "Setup Instance must not error") + err := h.Websocket.AddSubscriptions(h.Websocket.Conn, &subscription.Subscription{Key: "trade.clearing#btcusdt#1", Asset: asset.Spot, Pairs: currency.Pairs{btcusdtPair}, Channel: subscription.MyTradesChannel}) + require.NoError(t, err, "AddSubscriptions must not error") + h.SetSaveTradeDataStatus(true) + testexch.FixtureToDataHandler(t, "testdata/wsMyTrades.json", h.wsHandleData) + close(h.Websocket.DataHandler) + require.Len(t, h.Websocket.DataHandler, 1, "Must see correct number of records") + m := <-h.Websocket.DataHandler + exp := &order.Detail{ + Exchange: h.Name, + Pair: btcusdtPair, + Side: order.Buy, + Status: order.PartiallyFilled, + ClientOrderID: "a001", + OrderID: "99998888", + AssetType: asset.Spot, + Date: time.UnixMicro(1583853365586000), + LastUpdated: time.UnixMicro(1583853365996000), + Price: 10000, + Amount: 1, + Trades: []order.TradeHistory{ + { + Price: 9999.99, + Amount: 0.96, + Fee: 19.88, + Exchange: h.Name, + TID: "919219323232", + Side: order.Buy, + IsMaker: false, + Timestamp: time.UnixMicro(1583853365996000), + }, + }, } + require.IsType(t, &order.Detail{}, m, "Must get the correct type from DataHandler") + d, _ := m.(*order.Detail) + require.NotNil(t, d) + assert.Equal(t, exp, d, "Order Detail should match") } func TestStringToOrderStatus(t *testing.T) { @@ -2523,104 +1581,36 @@ func TestStringToOrderType(t *testing.T) { } func Test_FormatExchangeKlineInterval(t *testing.T) { - testCases := []struct { - name string + for _, tt := range []struct { interval kline.Interval output string }{ - { - "OneMin", - kline.OneMin, - "1min", - }, - { - "FourHour", - kline.FourHour, - "4hour", - }, - { - "OneDay", - kline.OneDay, - "1day", - }, - { - "OneWeek", - kline.OneWeek, - "1week", - }, - { - "OneMonth", - kline.OneMonth, - "1mon", - }, - { - "OneYear", - kline.OneYear, - "1year", - }, - { - "AllOthers", - kline.TwoWeek, - "", - }, - } - - for x := range testCases { - test := testCases[x] - - t.Run(test.name, func(t *testing.T) { - ret := h.FormatExchangeKlineInterval(test.interval) - - if ret != test.output { - t.Errorf("unexpected result return expected: %v received: %v", test.output, ret) - } - }) + {kline.OneMin, "1min"}, + {kline.FourHour, "4hour"}, + {kline.OneDay, "1day"}, + {kline.OneWeek, "1week"}, + {kline.OneMonth, "1mon"}, + {kline.OneYear, "1year"}, + {kline.TwoWeek, ""}, + } { + assert.Equalf(t, tt.output, h.FormatExchangeKlineInterval(tt.interval), "FormatExchangeKlineInterval should return correctly for %s", tt.output) } } func TestGetRecentTrades(t *testing.T) { t.Parallel() - currencyPair, err := currency.NewPairFromString("BTC-USDT") - if err != nil { - t.Error(err) - } - _, err = h.GetRecentTrades(context.Background(), currencyPair, asset.Spot) - if err != nil { - t.Error(err) - } - fPairs, err := h.CurrencyPairs.GetPairs(asset.Futures, false) - if err != nil { - t.Error(err) - } - currencyPair, err = fPairs.GetRandomPair() - if err != nil { - t.Error(err) - } - _, err = h.GetRecentTrades(context.Background(), currencyPair, asset.Futures) - if err != nil { - t.Error(err) - } - currencyPair, err = currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - _, err = h.GetRecentTrades(context.Background(), currencyPair, asset.CoinMarginedFutures) - if err != nil { - t.Error(err) - } + _, err := h.GetRecentTrades(context.Background(), btcusdtPair, asset.Spot) + require.NoError(t, err) + _, err = h.GetRecentTrades(context.Background(), btccwPair, asset.Futures) + require.NoError(t, err) + _, err = h.GetRecentTrades(context.Background(), btcusdPair, asset.CoinMarginedFutures) + require.NoError(t, err) } func TestGetHistoricTrades(t *testing.T) { t.Parallel() - currencyPair, err := currency.NewPairFromString(testSymbol) - if err != nil { - t.Error(err) - } - _, err = h.GetHistoricTrades(context.Background(), - currencyPair, asset.Spot, time.Now().Add(-time.Minute*15), time.Now()) - if err != nil && err != common.ErrFunctionNotSupported { - t.Error(err) - } + _, err := h.GetHistoricTrades(context.Background(), btcusdtPair, asset.Spot, time.Now().Add(-time.Minute*15), time.Now()) + require.ErrorIs(t, err, common.ErrFunctionNotSupported) } func TestGetAvailableTransferChains(t *testing.T) { @@ -2631,27 +1621,23 @@ func TestGetAvailableTransferChains(t *testing.T) { } func TestFormatFuturesPair(t *testing.T) { - r, err := h.formatFuturesPair(futuresTestPair, false) + t.Parallel() + updatePairsOnce(t, h) + + r, err := h.formatFuturesPair(btccwPair, false) require.NoError(t, err) assert.Equal(t, "BTC_CW", r) - p, err := h.FetchTradablePairs(context.Background(), asset.Futures) - require.NoError(t, err, "FetchTradablePairs must not error") - require.NotEmpty(t, p, "FetchTradablePairs must return pairs") - - // test getting a tradable pair in the format of BTC210827 but make it lower case to test correct formatting - r, err = h.formatFuturesPair(p[0].Lower(), false) + // pair in the format of BTC210827 but make it lower case to test correct formatting + r, err = h.formatFuturesPair(btcFutureDatedPair.Lower(), false) require.NoError(t, err) assert.Len(t, r, 9, "Should be an 9 character string") + assert.Equal(t, "BTC2", r[0:4], "Should start with btc and a date this millennium") - // Test for upper case 'BTC' not lower case 'btc', disregarded numerals - // as they not deterministic from this endpoint. - assert.Equal(t, "BTC", r[0:3]) - - r, err = h.formatFuturesPair(futuresTestPair, true) + r, err = h.formatFuturesPair(btccwPair, true) require.NoError(t, err) assert.Len(t, r, 9, "Should be an 9 character string") - assert.Equal(t, "BTC", r[0:3]) + assert.Equal(t, "BTC2", r[0:4], "Should start with btc and a date this millennium") r, err = h.formatFuturesPair(currency.NewPair(currency.BTC, currency.USDT), false) require.NoError(t, err) @@ -2661,26 +1647,20 @@ func TestFormatFuturesPair(t *testing.T) { func TestSearchForExistedWithdrawsAndDeposits(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.SearchForExistedWithdrawsAndDeposits(context.Background(), currency.BTC, "deposit", "", 0, 100) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestCancelOrderBatch(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) _, err := h.CancelOrderBatch(context.Background(), []string{"1234"}, nil) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestCancelBatchOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h, canManipulateRealOrders) - _, err := h.CancelBatchOrders(context.Background(), []order.Cancel{ { OrderID: "1234", @@ -2688,100 +1668,74 @@ func TestCancelBatchOrders(t *testing.T) { Pair: currency.NewPair(currency.BTC, currency.USDT), }, }) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestGetWithdrawalsHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, h) - _, err := h.GetWithdrawalsHistory(context.Background(), currency.BTC, asset.Spot) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestGetFuturesContractDetails(t *testing.T) { t.Parallel() _, err := h.GetFuturesContractDetails(context.Background(), asset.Spot) - if !errors.Is(err, futures.ErrNotFuturesAsset) { - t.Error(err) - } + require.ErrorIs(t, err, futures.ErrNotFuturesAsset) _, err = h.GetFuturesContractDetails(context.Background(), asset.USDTMarginedFutures) - if !errors.Is(err, asset.ErrNotSupported) { - t.Error(err) - } + require.ErrorIs(t, err, asset.ErrNotSupported) _, err = h.GetFuturesContractDetails(context.Background(), asset.CoinMarginedFutures) - if !errors.Is(err, nil) { - t.Error(err) - } + require.NoError(t, err) _, err = h.GetFuturesContractDetails(context.Background(), asset.Futures) - if !errors.Is(err, nil) { - t.Error(err) - } + require.NoError(t, err) } func TestGetLatestFundingRates(t *testing.T) { t.Parallel() + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(h), "Test Instance Setup must not fail") + updatePairsOnce(t, h) + _, err := h.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ Asset: asset.USDTMarginedFutures, Pair: currency.NewPair(currency.BTC, currency.USD), IncludePredictedRate: true, }) - if !errors.Is(err, asset.ErrNotSupported) { - t.Error(err) - } + require.ErrorIs(t, err, asset.ErrNotSupported) _, err = h.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ Asset: asset.CoinMarginedFutures, Pair: currency.NewPair(currency.BTC, currency.USD), IncludePredictedRate: true, }) - if err != nil { - t.Error(err) - } + require.NoError(t, err) err = h.CurrencyPairs.EnablePair(asset.CoinMarginedFutures, currency.NewPair(currency.BTC, currency.USD)) - if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) { - t.Fatal(err) - } + require.ErrorIs(t, err, currency.ErrPairAlreadyEnabled) + _, err = h.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ Asset: asset.CoinMarginedFutures, IncludePredictedRate: true, }) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestIsPerpetualFutureCurrency(t *testing.T) { t.Parallel() is, err := h.IsPerpetualFutureCurrency(asset.Binary, currency.NewPair(currency.BTC, currency.USDT)) - if err != nil { - t.Error(err) - } - if is { - t.Error("expected false") - } + require.NoError(t, err) + assert.False(t, is) is, err = h.IsPerpetualFutureCurrency(asset.CoinMarginedFutures, currency.NewPair(currency.BTC, currency.USDT)) - if err != nil { - t.Error(err) - } - if !is { - t.Error("expected true") - } + require.NoError(t, err) + assert.True(t, is) } func TestGetSwapFundingRates(t *testing.T) { t.Parallel() _, err := h.GetSwapFundingRates(context.Background()) - if err != nil { - t.Error(err) - } + require.NoError(t, err) } func TestGetBatchCoinMarginSwapContracts(t *testing.T) { @@ -2807,6 +1761,7 @@ func TestGetBatchFuturesContracts(t *testing.T) { func TestUpdateTickers(t *testing.T) { t.Parallel() + updatePairsOnce(t, h) for _, a := range h.GetAssetTypes(false) { err := h.UpdateTickers(context.Background(), a) require.NoErrorf(t, err, "asset %s", a) @@ -2859,29 +1814,33 @@ func TestPairFromContractExpiryCode(t *testing.T) { func TestGetOpenInterest(t *testing.T) { t.Parallel() + updatePairsOnce(t, h) + _, err := h.GetOpenInterest(context.Background(), key.PairAsset{ Base: currency.ETH.Item, Quote: currency.USDT.Item, Asset: asset.USDTMarginedFutures, }) assert.ErrorIs(t, err, asset.ErrNotSupported) + resp, err := h.GetOpenInterest(context.Background(), key.PairAsset{ Base: currency.BTC.Item, Quote: currency.USD.Item, Asset: asset.CoinMarginedFutures, }) - assert.NoError(t, err) + require.NoError(t, err) assert.NotEmpty(t, resp) resp, err = h.GetOpenInterest(context.Background(), key.PairAsset{ - Base: futuresTestPair.Base.Item, - Quote: futuresTestPair.Quote.Item, + Base: btccwPair.Base.Item, + Quote: btccwPair.Quote.Item, Asset: asset.Futures, }) - assert.NoError(t, err) + require.NoError(t, err) assert.NotEmpty(t, resp) + resp, err = h.GetOpenInterest(context.Background()) - assert.NoError(t, err) + require.NoError(t, err) assert.NotEmpty(t, resp) } @@ -2911,7 +1870,7 @@ func TestContractOpenInterestUSDT(t *testing.T) { func TestGetCurrencyTradeURL(t *testing.T) { t.Parallel() - testexch.UpdatePairsOnce(t, h) + updatePairsOnce(t, h) for _, a := range h.GetAssetTypes(false) { pairs, err := h.CurrencyPairs.GetPairs(a, false) require.NoError(t, err, "cannot get pairs for %s", a) @@ -2929,13 +1888,18 @@ func TestGetCurrencyTradeURL(t *testing.T) { func TestGenerateSubscriptions(t *testing.T) { t.Parallel() - h := new(HUOBI) + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes require.NoError(t, testexch.Setup(h), "Test instance Setup must not error") + + h.Websocket.SetCanUseAuthenticatedEndpoints(true) subs, err := h.generateSubscriptions() require.NoError(t, err, "generateSubscriptions must not error") exp := subscription.List{} for _, s := range h.Features.Subscriptions { - if s.Authenticated && !h.Websocket.CanUseAuthenticatedEndpoints() { + if s.Asset == asset.Empty { + s := s.Clone() //nolint:govet // Intentional lexical scope shadow + s.QualifiedChannel = channelName(s) + exp = append(exp, s) continue } for _, a := range h.GetAssetTypes(true) { @@ -2947,6 +1911,12 @@ func TestGenerateSubscriptions(t *testing.T) { pairs = common.SortStrings(pairs).Format(currency.PairFormat{Uppercase: false, Delimiter: ""}) s := s.Clone() //nolint:govet // Intentional lexical scope shadow s.Asset = a + if isWildcardChannel(s) { + s.Pairs = pairs + s.QualifiedChannel = channelName(s) + exp = append(exp, s) + continue + } for i, p := range pairs { s := s.Clone() //nolint:govet // Intentional lexical scope shadow s.QualifiedChannel = channelName(s, p) @@ -2964,10 +1934,28 @@ func TestGenerateSubscriptions(t *testing.T) { testsubs.EqualLists(t, exp, subs) } +func wsFixture(tb testing.TB, msg []byte, w *websocket.Conn) error { + tb.Helper() + action, _ := jsonparser.GetString(msg, "action") + ch, _ := jsonparser.GetString(msg, "ch") + if action == "req" && ch == "auth" { + return w.WriteMessage(websocket.TextMessage, []byte(`{"action":"req","code":200,"ch":"auth","data":{}}`)) + } + if action == "sub" { + return w.WriteMessage(websocket.TextMessage, []byte(`{"action":"sub","code":200,"ch":"`+ch+`"}`)) + } + id, _ := jsonparser.GetString(msg, "id") + sub, _ := jsonparser.GetString(msg, "sub") + if id != "" && sub != "" { + return w.WriteMessage(websocket.TextMessage, []byte(`{"id":"`+id+`","status":"ok","subbed":"`+sub+`"}`)) + } + return fmt.Errorf("%w: %s", errors.New("Unhandled mock websocket message"), msg) +} + // TestSubscribe exercises live public subscriptions func TestSubscribe(t *testing.T) { t.Parallel() - h := new(HUOBI) + h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes require.NoError(t, testexch.Setup(h), "Test instance Setup must not error") subs, err := h.Features.Subscriptions.ExpandTemplates(h) require.NoError(t, err, "ExpandTemplates must not error") @@ -2975,17 +1963,52 @@ func TestSubscribe(t *testing.T) { err = h.Subscribe(subs) require.NoError(t, err, "Subscribe must not error") got := h.Websocket.GetSubscriptions() - require.Equal(t, 4, len(got), "Must get correct number of subscriptions") + require.Equal(t, 8, len(got), "Must get correct number of subscriptions") + for _, s := range got { + assert.Equal(t, subscription.SubscribedState, s.State()) + } +} + +// TestAuthSubscribe exercises mock subscriptions including private +func TestAuthSubscribe(t *testing.T) { + t.Parallel() + subCfg := h.Features.Subscriptions + h := testexch.MockWsInstance[HUOBI](t, mockws.CurryWsMockUpgrader(t, wsFixture)) + h.Websocket.SetCanUseAuthenticatedEndpoints(true) + subs, err := subCfg.ExpandTemplates(h) + require.NoError(t, err, "ExpandTemplates must not error") + err = h.Subscribe(subs) + require.NoError(t, err, "Subscribe must not error") + got := h.Websocket.GetSubscriptions() + require.Equal(t, 11, len(got), "Must get correct number of subscriptions") for _, s := range got { assert.Equal(t, subscription.SubscribedState, s.State()) } } func TestChannelName(t *testing.T) { - p := currency.NewPair(currency.BTC, currency.USD) - assert.Equal(t, "market.BTCUSD.kline", channelName(&subscription.Subscription{Channel: subscription.CandlesChannel}, p)) - assert.Panics(t, func() { channelName(&subscription.Subscription{Channel: wsOrderbookChannel}, p) }) - assert.Panics(t, func() { channelName(&subscription.Subscription{Channel: subscription.MyAccountChannel}, p) }, "Should panic on V2 endpoints until implemented") + assert.Equal(t, "market.BTC-USD.kline", channelName(&subscription.Subscription{Channel: subscription.CandlesChannel}, btcusdPair)) + assert.Equal(t, "trade.clearing#*#1", channelName(&subscription.Subscription{Channel: subscription.MyTradesChannel}, btcusdPair)) + assert.Panics(t, func() { channelName(&subscription.Subscription{Channel: wsOrderbookChannel}, btcusdPair) }) +} + +func TestIsWildcardChannel(t *testing.T) { + assert.False(t, isWildcardChannel(&subscription.Subscription{Channel: subscription.CandlesChannel})) + assert.True(t, isWildcardChannel(&subscription.Subscription{Channel: subscription.MyOrdersChannel})) + assert.Panics(t, func() { channelName(&subscription.Subscription{Channel: wsOrderbookChannel}) }) +} + +func TestGetErrResp(t *testing.T) { + err := getErrResp([]byte(`{"status":"error","err-code":"bad-request","err-msg":"invalid topic promiscuous.drop🐻s.nearby"}`)) + assert.ErrorContains(t, err, "invalid topic promiscuous.drop🐻s.nearby (bad-request)", "V1 errors should return correctly") + err = getErrResp([]byte(`{"status":"ok","subbed":"market.btcusdt.trade.detail"}`)) + assert.NoError(t, err, "V1 success should not error") + + err = getErrResp([]byte(`{"action":"sub","code":2001,"ch":"naughty.drop🐻s.locally","message":"invalid.ch"}`)) + assert.ErrorContains(t, err, "invalid.ch (2001)", "V2 errors should return correctly") + + err = getErrResp([]byte(`{"action":"sub","code":200,"ch":"orders#btcusdt","data":{}}`)) + assert.NoError(t, err, "V2 success should not error") } func TestBootstrap(t *testing.T) { @@ -3003,3 +2026,35 @@ func TestBootstrap(t *testing.T) { require.NoError(t, err) require.NotNil(t, h.futureContractCodes) } + +var updatePairsMutex sync.Mutex +var futureContractCodesCache map[string]currency.Code + +// updatePairsOnce updates the pairs once, and ensures a future dated contract is enabled +func updatePairsOnce(tb testing.TB, h *HUOBI) { + tb.Helper() + + updatePairsMutex.Lock() + defer updatePairsMutex.Unlock() + + testexch.UpdatePairsOnce(tb, h) + + h.futureContractCodesMutex.Lock() + if len(h.futureContractCodes) == 0 { + // Restored pairs from cache, so haven't populated futureContract Codes + require.NotEmpty(tb, futureContractCodesCache, "futureContractCodesCache must not be empty") + h.futureContractCodes = futureContractCodesCache + } else { + futureContractCodesCache = h.futureContractCodes + } + h.futureContractCodesMutex.Unlock() + + if btcFutureDatedPair.Equal(currency.EMPTYPAIR) { + p, err := h.pairFromContractExpiryCode(btccwPair) + require.NoError(tb, err, "pairFromContractCode must not error") + btcFutureDatedPair = p + } + + err := h.CurrencyPairs.EnablePair(asset.Futures, btcFutureDatedPair) // Must enable every time we refresh the CurrencyPairs from cache + require.NoError(tb, common.ExcludeError(err, currency.ErrPairAlreadyEnabled)) +} diff --git a/exchanges/huobi/huobi_types.go b/exchanges/huobi/huobi_types.go index 948e23d3e24..80a04aaa1c0 100644 --- a/exchanges/huobi/huobi_types.go +++ b/exchanges/huobi/huobi_types.go @@ -12,7 +12,7 @@ type errorCapture struct { ErrMsgType1 string `json:"err-msg"` CodeType2 interface{} `json:"err_code"` ErrMsgType2 string `json:"err_msg"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` } // MarketSummary24Hr stores past 24hr market summary data of a given symbol @@ -61,8 +61,8 @@ type CurrenciesChainData struct { // WsKlineData stores kline data for futures and swap websocket type WsKlineData struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { ID int64 `json:"id"` MRID int64 `json:"mrid"` @@ -78,14 +78,14 @@ type WsKlineData struct { // WsMarketDepth stores market depth data for futures and swap websocket type WsMarketDepth struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { MRID int64 `json:"mrid"` ID int64 `json:"id"` Bids [][2]float64 `json:"bids"` Asks [][2]float64 `json:"asks"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Version int64 `json:"version"` Channel string `json:"ch"` } `json:"tick"` @@ -93,14 +93,14 @@ type WsMarketDepth struct { // WsIncrementalMarketDepth stores incremental market depth data for swap and futures websocket type WsIncrementalMarketDepth struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { MRID int64 `json:"mrid"` ID int64 `json:"id"` Bids [][2]float64 `json:"bids"` Asks [][2]float64 `json:"asks"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Version int64 `json:"version"` Channel string `json:"ch"` Event string `json:"event"` @@ -109,8 +109,8 @@ type WsIncrementalMarketDepth struct { // WsMarketDetail stores market detail data for futures and swap websocket type WsMarketDetail struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { ID int64 `json:"id"` MRID int64 `json:"mrid"` @@ -126,32 +126,32 @@ type WsMarketDetail struct { // WsMarketBBOData stores BBO data for futures and swap websocket type WsMarketBBOData struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { Channel string `json:"ch"` MRID int64 `json:"mrid"` ID int64 `json:"id"` Bid [2]float64 `json:"bid"` Ask [2]float64 `json:"ask"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Version int64 `json:":version"` } `json:"tick"` } // WsSubTradeDetail stores trade detail data for futures websocket type WsSubTradeDetail struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { - ID int64 `json:"id"` - Timestamp int64 `json:"ts"` + ID int64 `json:"id"` + Timestamp types.Time `json:"ts"` Data []struct { - Amount float64 `json:"amount"` - Timestamp int64 `json:"ts"` - ID int64 `json:"id"` - Price float64 `json:"price"` - Direction string `json:"direction"` + Amount float64 `json:"amount"` + Timestamp types.Time `json:"ts"` + ID int64 `json:"id"` + Price float64 `json:"price"` + Direction string `json:"direction"` } `json:"data"` } `json:"tick"` } @@ -179,22 +179,22 @@ type FWsRequestKline struct { // FWsReqTradeDetail stores requested trade detail data for futures websocket type FWsReqTradeDetail struct { - Rep string `json:"rep"` - ID string `json:"id"` - Timestamp int64 `json:"ts"` + Rep string `json:"rep"` + ID string `json:"id"` + Timestamp types.Time `json:"ts"` Data []struct { - ID int64 `json:"id"` - Price float64 `json:"price"` - Amount float64 `json:"amount"` - Direction string `json:"direction"` - Timestamp int64 `json:"ts"` + ID int64 `json:"id"` + Price float64 `json:"price"` + Amount float64 `json:"amount"` + Direction string `json:"direction"` + Timestamp types.Time `json:"ts"` } `json:"data"` } // FWsSubKlineIndex stores subscribed kline index data for futures websocket type FWsSubKlineIndex struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { ID string `json:"id"` Open float64 `json:"open,string"` @@ -209,10 +209,10 @@ type FWsSubKlineIndex struct { // FWsReqKlineIndex stores requested kline index data for futures websocket type FWsReqKlineIndex struct { - ID string `json:"id"` - Rep string `json:"rep"` - WsID int64 `json:"wsid"` - Timestamp int64 `json:"ts"` + ID string `json:"id"` + Rep string `json:"rep"` + WsID int64 `json:"wsid"` + Timestamp types.Time `json:"ts"` Data []struct { ID int64 `json:"id"` Open float64 `json:"open"` @@ -227,8 +227,8 @@ type FWsReqKlineIndex struct { // FWsSubBasisData stores subscribed basis data for futures websocket type FWsSubBasisData struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { ID int64 `json:"id"` IndexPrice float64 `json:"index_price,string"` @@ -240,10 +240,10 @@ type FWsSubBasisData struct { // FWsReqBasisData stores requested basis data for futures websocket type FWsReqBasisData struct { - ID string `json:"id"` - Rep string `json:"rep"` - Timestamp int64 `json:"ts"` - WsID int64 `json:"wsid"` + ID string `json:"id"` + Rep string `json:"rep"` + Timestamp types.Time `json:"ts"` + WsID int64 `json:"wsid"` Tick struct { ID int64 `json:"id"` IndexPrice float64 `json:"index_price,string"` @@ -255,34 +255,34 @@ type FWsReqBasisData struct { // FWsSubOrderData stores subscribed order data for futures websocket type FWsSubOrderData struct { - Operation string `json:"op"` - Topic string `json:"topic"` - UID string `json:"uid"` - Timestamp int64 `json:"ts"` - Symbol string `json:"symbol"` - ContractType string `json:"contract_type"` - ContractCode string `json:"contract_code"` - Volume float64 `json:"volume"` - Price float64 `json:"price"` - OrderPriceType string `json:"order_price_type"` - Direction string `json:"direction"` - Offset string `json:"offset"` - Status int64 `json:"status"` - LeverageRate int64 `json:"lever_rate"` - OrderID int64 `json:"order_id"` - OrderIDString string `json:"order_id_string"` - ClientOrderID int64 `json:"client_order_id"` - OrderSource string `json:"order_source"` - OrderType int64 `json:"order_type"` - CreatedAt int64 `json:"created_at"` - TradeVolume float64 `json:"trade_volume"` - TradeTurnover float64 `json:"trade_turnover"` - Fee float64 `json:"fee"` - TradeAvgPrice float64 `json:"trade_avg_price"` - MarginFrozen float64 `json:"margin_frozen"` - Profit float64 `json:"profit"` - FeeAsset string `json:"fee_asset"` - CancelledAt int64 `json:"canceled_at"` + Operation string `json:"op"` + Topic string `json:"topic"` + UID string `json:"uid"` + Timestamp types.Time `json:"ts"` + Symbol string `json:"symbol"` + ContractType string `json:"contract_type"` + ContractCode string `json:"contract_code"` + Volume float64 `json:"volume"` + Price float64 `json:"price"` + OrderPriceType string `json:"order_price_type"` + Direction string `json:"direction"` + Offset string `json:"offset"` + Status int64 `json:"status"` + LeverageRate int64 `json:"lever_rate"` + OrderID int64 `json:"order_id"` + OrderIDString string `json:"order_id_string"` + ClientOrderID int64 `json:"client_order_id"` + OrderSource string `json:"order_source"` + OrderType int64 `json:"order_type"` + CreatedAt int64 `json:"created_at"` + TradeVolume float64 `json:"trade_volume"` + TradeTurnover float64 `json:"trade_turnover"` + Fee float64 `json:"fee"` + TradeAvgPrice float64 `json:"trade_avg_price"` + MarginFrozen float64 `json:"margin_frozen"` + Profit float64 `json:"profit"` + FeeAsset string `json:"fee_asset"` + CancelledAt int64 `json:"canceled_at"` Trade []struct { ID string `json:"id"` TradeID int64 `json:"trade_id"` @@ -298,20 +298,20 @@ type FWsSubOrderData struct { // FWsSubMatchOrderData stores subscribed match order data for futures websocket type FWsSubMatchOrderData struct { - Operation string `json:"op"` - Topic string `json:"topic"` - UID string `json:"uid"` - Timestamp int64 `json:"ts"` - Symbol string `json:"symbol"` - ContractType string `json:"contract_type"` - ContractCode string `json:"contract_code"` - Status int64 `json:"status"` - OrderID int64 `json:"order_id"` - OrderIDString string `json:"order_id_string"` - OrderType string `json:"order_type"` - Volume float64 `json:"volume"` - TradeVolume float64 `json:"trade_volume"` - ClientOrderID int64 `json:"client_order_id"` + Operation string `json:"op"` + Topic string `json:"topic"` + UID string `json:"uid"` + Timestamp types.Time `json:"ts"` + Symbol string `json:"symbol"` + ContractType string `json:"contract_type"` + ContractCode string `json:"contract_code"` + Status int64 `json:"status"` + OrderID int64 `json:"order_id"` + OrderIDString string `json:"order_id_string"` + OrderType string `json:"order_type"` + Volume float64 `json:"volume"` + TradeVolume float64 `json:"trade_volume"` + ClientOrderID int64 `json:"client_order_id"` Trade []struct { ID string `json:"id"` TradeID int64 `json:"trade_id"` @@ -325,11 +325,11 @@ type FWsSubMatchOrderData struct { // FWsSubEquityUpdates stores account equity updates data for futures websocket type FWsSubEquityUpdates struct { - Operation string `json:"op"` - Topic string `json:"topic"` - UID string `json:"uid"` - Timestamp int64 `json:"ts"` - Event string `json:"event"` + Operation string `json:"op"` + Topic string `json:"topic"` + UID string `json:"uid"` + Timestamp types.Time `json:"ts"` + Event string `json:"event"` Data []struct { Symbol string `json:"symbol"` MarginBalance float64 `json:"margin_balance"` @@ -349,11 +349,11 @@ type FWsSubEquityUpdates struct { // FWsSubPositionUpdates stores subscribed position updates data for futures websocket type FWsSubPositionUpdates struct { - Operation string `json:"op"` - Topic string `json:"topic"` - UID string `json:"uid"` - Timestamp int64 `json:"ts"` - Event string `json:"event"` + Operation string `json:"op"` + Topic string `json:"topic"` + UID string `json:"uid"` + Timestamp types.Time `json:"ts"` + Event string `json:"event"` PositionsData []struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` @@ -375,9 +375,9 @@ type FWsSubPositionUpdates struct { // FWsSubLiquidationOrders stores subscribed liquidation orders data for futures websocket type FWsSubLiquidationOrders struct { - Operation string `json:"op"` - Topic string `json:"topic"` - Timestamp int64 `json:"ts"` + Operation string `json:"op"` + Topic string `json:"topic"` + Timestamp types.Time `json:"ts"` OrdersData []struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` @@ -391,10 +391,10 @@ type FWsSubLiquidationOrders struct { // FWsSubContractInfo stores contract info data for futures websocket type FWsSubContractInfo struct { - Operation string `json:"op"` - Topic string `json:"topic"` - Timestamp int64 `json:"ts"` - Event string `json:"event"` + Operation string `json:"op"` + Topic string `json:"topic"` + Timestamp types.Time `json:"ts"` + Event string `json:"event"` ContractData []struct { Symbol string `json:"symbol"` ContractCode string `json:"contract_code"` @@ -445,11 +445,11 @@ type FWsSubTriggerOrderUpdates struct { // Response stores the Huobi response information type Response struct { - Status string `json:"status"` - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` - ErrorCode string `json:"err-code"` - ErrorMessage string `json:"err-msg"` + Status string `json:"status"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` + ErrorCode string `json:"err-code"` + ErrorMessage string `json:"err-msg"` } // MarginRatesData stores margin rates data @@ -487,14 +487,14 @@ type SwapMarketsData struct { // KlineItem stores a kline item type KlineItem struct { - IDTimestamp int64 `json:"id"` - Open float64 `json:"open"` - Close float64 `json:"close"` - Low float64 `json:"low"` - High float64 `json:"high"` - Amount float64 `json:"amount"` - Volume float64 `json:"vol"` - Count int `json:"count"` + IDTimestamp types.Time `json:"id"` + Open float64 `json:"open"` + Close float64 `json:"close"` + Low float64 `json:"low"` + High float64 `json:"high"` + Amount float64 `json:"amount"` + Volume float64 `json:"vol"` + Count int `json:"count"` } // CancelOpenOrdersBatch stores open order batch response data @@ -524,7 +524,7 @@ type Tickers struct { // FuturesBatchTicker holds ticker data type FuturesBatchTicker struct { ID float64 `json:"id"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Ask [2]float64 `json:"ask"` Bid [2]float64 `json:"bid"` BusinessType string `json:"business_type"` @@ -587,31 +587,31 @@ type Orderbook struct { // Trade stores the trade data type Trade struct { - TradeID float64 `json:"trade-id"` - Price float64 `json:"price"` - Amount float64 `json:"amount"` - Direction string `json:"direction"` - Timestamp int64 `json:"ts"` + TradeID float64 `json:"trade-id"` + Price float64 `json:"price"` + Amount float64 `json:"amount"` + Direction string `json:"direction"` + Timestamp types.Time `json:"ts"` } // TradeHistory stores the trade history data type TradeHistory struct { - ID int64 `json:"id"` - Timestamp int64 `json:"ts"` - Trades []Trade `json:"data"` + ID int64 `json:"id"` + Timestamp types.Time `json:"ts"` + Trades []Trade `json:"data"` } // Detail stores the ticker detail data type Detail struct { - Amount float64 `json:"amount"` - Open float64 `json:"open"` - Close float64 `json:"close"` - High float64 `json:"high"` - Timestamp int64 `json:"timestamp"` - ID int64 `json:"id"` - Count int `json:"count"` - Low float64 `json:"low"` - Volume float64 `json:"vol"` + Amount float64 `json:"amount"` + Open float64 `json:"open"` + Close float64 `json:"close"` + High float64 `json:"high"` + Timestamp types.Time `json:"timestamp"` + ID int64 `json:"id"` + Count int `json:"count"` + Low float64 `json:"low"` + Volume float64 `json:"vol"` } // Symbol stores the symbol data @@ -807,30 +807,11 @@ type KlinesRequestParams struct { Size int // Size; [1-2000] } -// WsRequest defines a request data structure -type WsRequest struct { - Topic string `json:"req,omitempty"` - Subscribe string `json:"sub,omitempty"` - Unsubscribe string `json:"unsub,omitempty"` - ClientID int64 `json:"cid,string,omitempty"` -} - -// WsResponse defines a response from the websocket connection when there -// is an error -type WsResponse struct { - Op string `json:"op"` - TS int64 `json:"ts"` - Status string `json:"status"` - // ErrorCode returns either an integer or a string - ErrorCode interface{} `json:"err-code"` - ErrorMessage string `json:"err-msg"` - Ping int64 `json:"ping"` - Channel string `json:"ch"` - Rep string `json:"rep"` - Topic string `json:"topic"` - Subscribed string `json:"subbed"` - UnSubscribed string `json:"unsubbed"` - ClientID int64 `json:"cid,string"` +// wsSubReq is a request to subscribe to or unubscribe from a topic for public channels (private channels use generic wsReq) +type wsSubReq struct { + ID string `json:"id,omitempty"` + Sub string `json:"sub,omitempty"` + Unsub string `json:"unsub,omitempty"` } // WsHeartBeat defines a heartbeat request @@ -840,20 +821,20 @@ type WsHeartBeat struct { // WsDepth defines market depth websocket response type WsDepth struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { Bids [][]interface{} `json:"bids"` Asks [][]interface{} `json:"asks"` - Timestamp int64 `json:"ts"` + Timestamp types.Time `json:"ts"` Version int64 `json:"version"` } `json:"tick"` } // WsKline defines market kline websocket response type WsKline struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { ID int64 `json:"id"` Open float64 `json:"open"` @@ -868,222 +849,133 @@ type WsKline struct { // WsTick stores websocket ticker data type WsTick struct { - Channel string `json:"ch"` - Rep string `json:"rep"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Rep string `json:"rep"` + Timestamp types.Time `json:"ts"` Tick struct { - Amount float64 `json:"amount"` - Close float64 `json:"close"` - Count float64 `json:"count"` - High float64 `json:"high"` - ID float64 `json:"id"` - Low float64 `json:"low"` - Open float64 `json:"open"` - Timestamp float64 `json:"ts"` - Volume float64 `json:"vol"` + Amount float64 `json:"amount"` + Close float64 `json:"close"` + Count float64 `json:"count"` + High float64 `json:"high"` + ID float64 `json:"id"` + Low float64 `json:"low"` + Open float64 `json:"open"` + Timestamp types.Time `json:"ts"` + Volume float64 `json:"vol"` } `json:"tick"` } // WsTrade defines market trade websocket response type WsTrade struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` + Channel string `json:"ch"` + Timestamp types.Time `json:"ts"` Tick struct { - ID int64 `json:"id"` - Timestamp int64 `json:"ts"` + ID int64 `json:"id"` + Timestamp types.Time `json:"ts"` Data []struct { - Amount float64 `json:"amount"` - Timestamp int64 `json:"ts"` - TradeID float64 `json:"tradeId"` - Price float64 `json:"price"` - Direction string `json:"direction"` + Amount float64 `json:"amount"` + Timestamp types.Time `json:"ts"` + TradeID float64 `json:"tradeId"` + Price float64 `json:"price"` + Direction string `json:"direction"` } `json:"data"` } } -// WsAuthenticationRequest data for login -type WsAuthenticationRequest struct { - Op string `json:"op"` - AccessKeyID string `json:"AccessKeyId"` - SignatureMethod string `json:"SignatureMethod"` - SignatureVersion string `json:"SignatureVersion"` - Timestamp string `json:"Timestamp"` - Signature string `json:"Signature"` - ClientID int64 `json:"cid,string,omitempty"` -} - -// WsMessage defines read data from the websocket connection -type WsMessage struct { - Raw []byte - URL string -} - -// WsAuthenticatedSubscriptionRequest request for subscription on authenticated connection -type WsAuthenticatedSubscriptionRequest struct { - Op string `json:"op"` - AccessKeyID string `json:"AccessKeyId"` - SignatureMethod string `json:"SignatureMethod"` - SignatureVersion string `json:"SignatureVersion"` - Timestamp string `json:"Timestamp"` - Signature string `json:"Signature"` - Topic string `json:"topic"` - ClientID int64 `json:"cid,string,omitempty"` -} - -// WsAuthenticatedAccountsListRequest request for account list authenticated connection -type WsAuthenticatedAccountsListRequest struct { - Op string `json:"op"` - AccessKeyID string `json:"AccessKeyId"` - SignatureMethod string `json:"SignatureMethod"` - SignatureVersion string `json:"SignatureVersion"` - Timestamp string `json:"Timestamp"` - Signature string `json:"Signature"` - Topic string `json:"topic"` - Symbol string `json:"symbol"` - ClientID int64 `json:"cid,string,omitempty"` -} - -// WsAuthenticatedOrderDetailsRequest request for order details authenticated connection -type WsAuthenticatedOrderDetailsRequest struct { - Op string `json:"op"` - AccessKeyID string `json:"AccessKeyId"` - SignatureMethod string `json:"SignatureMethod"` - SignatureVersion string `json:"SignatureVersion"` - Timestamp string `json:"Timestamp"` - Signature string `json:"Signature"` - Topic string `json:"topic"` - OrderID string `json:"order-id"` - ClientID int64 `json:"cid,string,omitempty"` -} - -// WsAuthenticatedOrdersListRequest request for orderslist authenticated connection -type WsAuthenticatedOrdersListRequest struct { - Op string `json:"op"` - AccessKeyID string `json:"AccessKeyId"` - SignatureMethod string `json:"SignatureMethod"` - SignatureVersion string `json:"SignatureVersion"` - Timestamp string `json:"Timestamp"` - Signature string `json:"Signature"` - Topic string `json:"topic"` - States string `json:"states"` - AccountID int64 `json:"account-id"` - Symbol string `json:"symbol"` - ClientID int64 `json:"cid,string,omitempty"` -} - -// WsAuthenticatedAccountsResponse response from Accounts authenticated subscription -type WsAuthenticatedAccountsResponse struct { - WsResponse - Data WsAuthenticatedAccountsResponseData `json:"data"` -} - -// WsAuthenticatedAccountsResponseData account data -type WsAuthenticatedAccountsResponseData struct { - Event string `json:"event"` - List []WsAuthenticatedAccountsResponseDataList `json:"list"` -} - -// WsAuthenticatedAccountsResponseDataList detailed account data -type WsAuthenticatedAccountsResponseDataList struct { - AccountID int64 `json:"account-id"` - Currency string `json:"currency"` - Type string `json:"type"` - Balance float64 `json:"balance,string"` -} - -// WsAuthenticatedOrdersUpdateResponse response from OrdersUpdate authenticated subscription -type WsAuthenticatedOrdersUpdateResponse struct { - WsResponse - Data WsAuthenticatedOrdersUpdateResponseData `json:"data"` -} - -// WsAuthenticatedOrdersUpdateResponseData order update data -type WsAuthenticatedOrdersUpdateResponseData struct { - UnfilledAmount float64 `json:"unfilled-amount,string"` - FilledAmount float64 `json:"filled-amount,string"` - Price float64 `json:"price,string"` - OrderID int64 `json:"order-id"` - Symbol string `json:"symbol"` - MatchID int64 `json:"match-id"` - FilledCashAmount float64 `json:"filled-cash-amount,string"` - Role string `json:"role"` - OrderState string `json:"order-state"` - OrderType string `json:"order-type"` -} - -// WsAuthenticatedOrdersResponse response from Orders authenticated subscription -type WsAuthenticatedOrdersResponse struct { - WsResponse - Data []WsAuthenticatedOrdersResponseData `json:"data"` -} - -// WsOldOrderUpdate response from Orders authenticated subscription -type WsOldOrderUpdate struct { - WsResponse - Data WsAuthenticatedOrdersResponseData `json:"data"` -} - -// WsAuthenticatedOrdersResponseData order data -type WsAuthenticatedOrdersResponseData struct { - SeqID int64 `json:"seq-id"` - OrderID int64 `json:"order-id"` - Symbol string `json:"symbol"` - AccountID int64 `json:"account-id"` - OrderAmount float64 `json:"order-amount,string"` - OrderPrice float64 `json:"order-price,string"` - CreatedAt int64 `json:"created-at"` - OrderType string `json:"order-type"` - OrderSource string `json:"order-source"` - OrderState string `json:"order-state"` - Role string `json:"role"` - Price float64 `json:"price,string"` - FilledAmount float64 `json:"filled-amount,string"` - UnfilledAmount float64 `json:"unfilled-amount,string"` - FilledCashAmount float64 `json:"filled-cash-amount,string"` - FilledFees float64 `json:"filled-fees,string"` -} - -// WsAuthenticatedAccountsListResponse response from AccountsList authenticated endpoint -type WsAuthenticatedAccountsListResponse struct { - WsResponse - Data []WsAuthenticatedAccountsListResponseData `json:"data"` -} - -// WsAuthenticatedAccountsListResponseData account data -type WsAuthenticatedAccountsListResponseData struct { - ID int64 `json:"id"` - Type string `json:"type"` - State string `json:"state"` - List []WsAuthenticatedAccountsListResponseDataList `json:"list"` -} - -// WsAuthenticatedAccountsListResponseDataList detailed account data -type WsAuthenticatedAccountsListResponseDataList struct { - Currency string `json:"currency"` - Type string `json:"type"` - Balance float64 `json:"balance,string"` -} - -// WsAuthenticatedOrdersListResponse response from OrdersList authenticated endpoint -type WsAuthenticatedOrdersListResponse struct { - WsResponse - Data []OrderInfo `json:"data"` -} - -// WsAuthenticatedOrderDetailResponse response from OrderDetail authenticated endpoint -type WsAuthenticatedOrderDetailResponse struct { - WsResponse - Data OrderInfo `json:"data"` -} - -// WsPong sent for pong messages -type WsPong struct { - Pong int64 `json:"pong"` -} - -type authenticationPing struct { - OP string `json:"op"` - TS int64 `json:"ts"` +// wsReq contains authentication login fields +type wsReq struct { + Action string `json:"action"` + Channel string `json:"ch"` + Params any `json:"params"` +} + +// wsAuthReq contains authentication login fields +type wsAuthReq struct { + AuthType string `json:"authType"` + AccessKey string `json:"accessKey"` + SignatureMethod string `json:"signatureMethod"` + SignatureVersion string `json:"signatureVersion"` + Timestamp string `json:"timestamp"` + Signature string `json:"signature"` +} + +type wsAccountUpdateMsg struct { + Data WsAccountUpdate `json:"data"` +} + +// WsAccountUpdate contains account updates to balances +type WsAccountUpdate struct { + Currency string `json:"currency"` + AccountID int64 `json:"accountId"` + Balance float64 `json:"balance,string"` + Available float64 `json:"available,string"` + ChangeType string `json:"changeType"` + AccountType string `json:"accountType"` + ChangeTime types.Time `json:"changeTime"` + SeqNum int64 `json:"seqNum"` +} + +type wsOrderUpdateMsg struct { + Data WsOrderUpdate `json:"data"` +} + +// WsOrderUpdate contains updates to orders +type WsOrderUpdate struct { + EventType string `json:"eventType"` + Symbol string `json:"symbol"` + AccountID int64 `json:"accountId"` + OrderID int64 `json:"orderId"` + TradeID int64 `json:"tradeId"` + ClientOrderID string `json:"clientOrderId"` + Source string `json:"orderSource"` + Price float64 `json:"orderPrice,string"` + Size float64 `json:"orderSize,string"` + Value float64 `json:"orderValue,string"` + OrderType string `json:"type"` + TradePrice float64 `json:"tradePrice,string"` + TradeVolume float64 `json:"tradeVolume,string"` + RemainingAmount float64 `json:"remainAmt,string"` + ExecutedAmount float64 `json:"execAmt,string"` + IsTaker bool `json:"aggressor"` + Side order.Side `json:"orderSide"` + OrderStatus string `json:"orderStatus"` + LastActTime types.Time `json:"lastActTime"` + CreateTime types.Time `json:"orderCreateTime"` + TradeTime types.Time `json:"tradeTime"` + ErrCode int64 `json:"errCode"` + ErrMessage string `json:"errMessage"` +} + +type wsTradeUpdateMsg struct { + Data WsTradeUpdate `json:"data"` +} + +// WsTradeUpdate contains trade updates to orders +type WsTradeUpdate struct { + EventType string `json:"eventType"` + Symbol string `json:"symbol"` + OrderID int64 `json:"orderId"` + TradePrice float64 `json:"tradePrice,string"` + TradeVolume float64 `json:"tradeVolume,string"` + Side order.Side `json:"orderSide"` + OrderType string `json:"orderType"` + IsTaker bool `json:"aggressor"` + TradeID int64 `json:"tradeId"` + TradeTime types.Time `json:"tradeTime"` + TransactFee float64 `json:"transactFee,string"` + FeeCurrency string `json:"feeCurrency"` + FeeDeduct string `json:"feeDeduct"` + FeeDeductType string `json:"feeDeductType"` + AccountID int64 `json:"accountId"` + Source string `json:"orderSource"` + OrderPrice float64 `json:"orderPrice,string"` + OrderSize float64 `json:"orderSize,string"` + Value float64 `json:"orderValue,string"` + ClientOrderID string `json:"clientOrderId"` + StopPrice string `json:"stopPrice"` + Operator string `json:"operator"` + OrderCreateTime types.Time `json:"orderCreateTime"` + OrderStatus string `json:"orderStatus"` } // OrderVars stores side, status and type for any order/trade @@ -1277,6 +1169,6 @@ type WithdrawalData struct { State string `json:"state"` ErrorCode string `json:"error-code"` ErrorMessage string `json:"error-message"` - CreatedAt int64 `json:"created-at"` - UpdatedAt int64 `json:"updated-at"` + CreatedAt types.Time `json:"created-at"` + UpdatedAt types.Time `json:"updated-at"` } diff --git a/exchanges/huobi/huobi_websocket.go b/exchanges/huobi/huobi_websocket.go index 0ff45913cce..a8a05103843 100644 --- a/exchanges/huobi/huobi_websocket.go +++ b/exchanges/huobi/huobi_websocket.go @@ -12,6 +12,7 @@ import ( "text/template" "time" + "github.com/buger/jsonparser" "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/crypto" @@ -30,34 +31,31 @@ import ( ) const ( - baseWSURL = "wss://api.huobi.pro" - futuresWSURL = "wss://api.hbdm.com/" + wsSpotHost = "api.huobi.pro" + wsSpotURL = "wss://" + wsSpotHost + wsPublicPath = "/ws" + wsPrivatePath = "/ws/v2" - wsMarketURL = baseWSURL + "/ws" wsCandlesChannel = "market.%s.kline" wsOrderbookChannel = "market.%s.depth" wsTradesChannel = "market.%s.trade.detail" wsMarketDetailChannel = "market.%s.detail" - wsMyOrdersChannel = "orders.%s" - wsMyTradesChannel = "orders.%s.update" - - wsAccountsOrdersEndPoint = "/ws/v1" - wsAccountsList = "accounts.list" - wsOrdersList = "orders.list" - wsOrdersDetail = "orders.detail" - wsAccountsOrdersURL = baseWSURL + wsAccountsOrdersEndPoint - wsAccountListEndpoint = wsAccountsOrdersEndPoint + "/" + wsAccountsList - wsOrdersListEndpoint = wsAccountsOrdersEndPoint + "/" + wsOrdersList - wsOrdersDetailEndpoint = wsAccountsOrdersEndPoint + "/" + wsOrdersDetail + wsMyOrdersChannel = "orders#*" + wsMyTradesChannel = "trade.clearing#*#1" // 0=Only trade events, 1=Trade and Cancellation events + wsMyAccountChannel = "accounts.update#2" // 0=Only balance, 1=Balance or Available, 2=Balance and Available when either change + wsAuthChannel = "auth" wsDateTimeFormatting = "2006-01-02T15:04:05" + signatureMethod = "HmacSHA256" + signatureVersion = "2.1" + wsRequestOp = "req" + wsSubOp = "sub" + wsUnsubOp = "unsub" +) - signatureMethod = "HmacSHA256" - signatureVersion = "2" - requestOp = "req" - authOp = "auth" - - loginDelay = 50 * time.Millisecond +var ( + errInvalidChannel = errors.New("invalid channel format") + errParsingMsg = errors.New("error parsing message") ) var defaultSubscriptions = subscription.List{ @@ -75,427 +73,233 @@ var subscriptionNames = map[string]string{ subscription.CandlesChannel: wsCandlesChannel, subscription.OrderbookChannel: wsOrderbookChannel, subscription.AllTradesChannel: wsTradesChannel, - /* TODO: Pending upcoming V2 support, these are dropped from the translation table so that the sub conf will be correct and not need upgrading, but will error on usage - subscription.MyTradesChannel: wsMyOrdersChannel, - subscription.MyOrdersChannel: wsMyTradesChannel, + subscription.MyTradesChannel: wsMyTradesChannel, + subscription.MyOrdersChannel: wsMyOrdersChannel, subscription.MyAccountChannel: wsMyAccountChannel, - */ } -// Instantiates a communications channel between websocket connections -var comms = make(chan WsMessage) - // WsConnect initiates a new websocket connection func (h *HUOBI) WsConnect() error { if !h.Websocket.IsEnabled() || !h.IsEnabled() { return stream.ErrWebsocketNotEnabled } - var dialer websocket.Dialer - err := h.wsDial(&dialer) - if err != nil { + if err := h.Websocket.Conn.Dial(&websocket.Dialer{}, http.Header{}); err != nil { return err } + ctx := context.Background() - if h.Websocket.CanUseAuthenticatedEndpoints() { - err = h.wsAuthenticatedDial(&dialer) - if err != nil { - log.Errorf(log.ExchangeSys, - "%v - authenticated dial failed: %v\n", - h.Name, - err) - } - err = h.wsLogin(context.TODO()) - if err != nil { - log.Errorf(log.ExchangeSys, - "%v - authentication failed: %v\n", - h.Name, - err) - h.Websocket.SetCanUseAuthenticatedEndpoints(false) - } - } - - h.Websocket.Wg.Add(1) - go h.wsReadData() - return nil -} - -func (h *HUOBI) wsDial(dialer *websocket.Dialer) error { - err := h.Websocket.Conn.Dial(dialer, http.Header{}) - if err != nil { - return err - } h.Websocket.Wg.Add(1) - go h.wsFunnelConnectionData(h.Websocket.Conn, wsMarketURL) - return nil -} + go h.wsReadMsgs(h.Websocket.Conn) -func (h *HUOBI) wsAuthenticatedDial(dialer *websocket.Dialer) error { - if !h.IsWebsocketAuthenticationSupported() { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", - h.Name) - } - err := h.Websocket.AuthConn.Dial(dialer, http.Header{}) - if err != nil { - return err + h.Websocket.SetCanUseAuthenticatedEndpoints(false) + if h.IsWebsocketAuthenticationSupported() { + if err := h.wsAuthConnect(ctx); err != nil { + return fmt.Errorf("error authenticating websocket: %w", err) + } + h.Websocket.Wg.Add(1) + go h.wsReadMsgs(h.Websocket.AuthConn) } - h.Websocket.Wg.Add(1) - go h.wsFunnelConnectionData(h.Websocket.AuthConn, wsAccountsOrdersURL) return nil } -// wsFunnelConnectionData manages data from multiple endpoints and passes it to -// a channel -func (h *HUOBI) wsFunnelConnectionData(ws stream.Connection, url string) { +// wsReadMsgs reads and processes messages from a websocket connection +func (h *HUOBI) wsReadMsgs(s stream.Connection) { defer h.Websocket.Wg.Done() for { - resp := ws.ReadMessage() - if resp.Raw == nil { + msg := s.ReadMessage() + if msg.Raw == nil { return } - comms <- WsMessage{Raw: resp.Raw, URL: url} + + if err := h.wsHandleData(msg.Raw); err != nil { + h.Websocket.DataHandler <- err + } } } - -// wsReadData receives and passes on websocket messages for processing -func (h *HUOBI) wsReadData() { - defer h.Websocket.Wg.Done() - for { - select { - case <-h.Websocket.ShutdownC: - select { - case resp := <-comms: - err := h.wsHandleData(resp.Raw) - if err != nil { - select { - case h.Websocket.DataHandler <- err: - default: - log.Errorf(log.WebsocketMgr, - "%s websocket handle data error: %v", - h.Name, - err) - } - } - default: - } - return - case resp := <-comms: - err := h.wsHandleData(resp.Raw) - if err != nil { - h.Websocket.DataHandler <- err +func (h *HUOBI) wsHandleData(respRaw []byte) error { + for _, op := range []string{wsSubOp, wsUnsubOp} { + key := op + "bed" // subbed, unsubbed + if ch, err := jsonparser.GetString(respRaw, key); err == nil { + if !h.Websocket.Match.IncomingWithData(op+":"+ch, respRaw) { + return fmt.Errorf("%w: %s:%s", stream.ErrNoMessageListener, op, ch) } + return nil } } -} -func stringToOrderStatus(status string) (order.Status, error) { - switch status { - case "submitted": - return order.New, nil - case "canceled": - return order.Cancelled, nil - case "partial-filled": - return order.PartiallyFilled, nil - case "partial-canceled": - return order.PartiallyCancelled, nil - default: - return order.UnknownStatus, - errors.New(status + " not recognised as order status") + if id, err := jsonparser.GetString(respRaw, "id"); err == nil { + if h.Websocket.Match.IncomingWithData(id, respRaw) { + return nil + } } -} -func stringToOrderSide(side string) (order.Side, error) { - switch { - case strings.Contains(side, "buy"): - return order.Buy, nil - case strings.Contains(side, "sell"): - return order.Sell, nil + if pingValue, err := jsonparser.GetInt(respRaw, "ping"); err == nil { + return h.wsHandleV1ping(int(pingValue)) } - return order.UnknownSide, - errors.New(side + " not recognised as order side") -} - -func stringToOrderType(oType string) (order.Type, error) { - switch { - case strings.Contains(oType, "limit"): - return order.Limit, nil - case strings.Contains(oType, "market"): - return order.Market, nil + if action, err := jsonparser.GetString(respRaw, "action"); err == nil { + switch action { + case "ping": + return h.wsHandleV2ping(respRaw) + case wsSubOp, wsUnsubOp: + return h.wsHandleV2subResp(action, respRaw) + } } - return order.UnknownType, - errors.New(oType + " not recognised as order type") -} - -func (h *HUOBI) wsHandleData(respRaw []byte) error { - var init WsResponse - err := json.Unmarshal(respRaw, &init) - if err != nil { + if err := getErrResp(respRaw); err != nil { return err } - if init.Subscribed != "" || - init.UnSubscribed != "" || - init.Op == "sub" || - init.Op == "unsub" { - // TODO handle subs - return nil - } - if init.Ping != 0 { - h.sendPingResponse(init.Ping) - return nil - } - if init.Op == "ping" { - authPing := authenticationPing{ - OP: "pong", - TS: init.TS, - } - err := h.Websocket.AuthConn.SendJSONMessage(context.TODO(), request.Unset, authPing) - if err != nil { - log.Errorln(log.ExchangeSys, err) + if ch, err := jsonparser.GetString(respRaw, "ch"); err == nil { + s := h.Websocket.GetSubscription(ch) + if s == nil { + return fmt.Errorf("%w: `%s`", subscription.ErrNotFound, ch) } - return nil + return h.wsHandleChannelMsgs(s, respRaw) } - if init.ErrorMessage != "" { - if init.ErrorMessage == "api-signature-not-valid" { - h.Websocket.SetCanUseAuthenticatedEndpoints(false) - return errors.New(h.Name + - " - invalid credentials. Authenticated requests disabled") - } - - codes, _ := init.ErrorCode.(string) - return errors.New(h.Name + " Code:" + codes + " Message:" + init.ErrorMessage) + h.Websocket.DataHandler <- stream.UnhandledMessageWarning{ + Message: h.Name + stream.UnhandledMessage + string(respRaw), } - if init.ClientID > 0 { - if h.Websocket.Match.IncomingWithData(init.ClientID, respRaw) { - return nil - } - } - - switch { - case strings.EqualFold(init.Op, authOp): - h.Websocket.SetCanUseAuthenticatedEndpoints(true) - // Auth captured - return nil - case strings.EqualFold(init.Topic, "accounts"): - var response WsAuthenticatedAccountsResponse - err := json.Unmarshal(respRaw, &response) - if err != nil { - return err - } - h.Websocket.DataHandler <- response - - case strings.Contains(init.Topic, "orders") && - strings.Contains(init.Topic, "update"): - var response WsAuthenticatedOrdersUpdateResponse - err := json.Unmarshal(respRaw, &response) - if err != nil { - return err - } - data := strings.Split(response.Topic, ".") - if len(data) < 2 { - return errors.New(h.Name + - " - currency could not be extracted from response") - } - orderID := strconv.FormatInt(response.Data.OrderID, 10) - var oSide order.Side - oSide, err = stringToOrderSide(response.Data.OrderType) - if err != nil { - h.Websocket.DataHandler <- order.ClassificationError{ - Exchange: h.Name, - OrderID: orderID, - Err: err, - } - } - var oType order.Type - oType, err = stringToOrderType(response.Data.OrderType) - if err != nil { - h.Websocket.DataHandler <- order.ClassificationError{ - Exchange: h.Name, - OrderID: orderID, - Err: err, - } - } - var oStatus order.Status - oStatus, err = stringToOrderStatus(response.Data.OrderState) - if err != nil { - h.Websocket.DataHandler <- order.ClassificationError{ - Exchange: h.Name, - OrderID: orderID, - Err: err, - } - } - var p currency.Pair - var a asset.Item - p, a, err = h.GetRequestFormattedPairAndAssetType(data[1]) - if err != nil { - return err - } - h.Websocket.DataHandler <- &order.Detail{ - Price: response.Data.Price, - Amount: response.Data.UnfilledAmount + response.Data.FilledAmount, - ExecutedAmount: response.Data.FilledAmount, - RemainingAmount: response.Data.UnfilledAmount, - Exchange: h.Name, - OrderID: orderID, - Type: oType, - Side: oSide, - Status: oStatus, - AssetType: a, - LastUpdated: time.Unix(response.TS*1000, 0), - Pair: p, - } - - case strings.Contains(init.Topic, "orders"): - var response WsOldOrderUpdate - err := json.Unmarshal(respRaw, &response) - if err != nil { - return err - } - h.Websocket.DataHandler <- response - case strings.Contains(init.Channel, "depth"): - var depth WsDepth - err := json.Unmarshal(respRaw, &depth) - if err != nil { - return err - } + return nil +} - data := strings.Split(depth.Channel, ".") - err = h.WsProcessOrderbook(&depth, data[1]) - if err != nil { - return err - } - case strings.Contains(init.Channel, "kline"): - var kline WsKline - err := json.Unmarshal(respRaw, &kline) - if err != nil { - return err - } - data := strings.Split(kline.Channel, ".") - var p currency.Pair - var a asset.Item - p, a, err = h.GetRequestFormattedPairAndAssetType(data[1]) - if err != nil { - return err - } - h.Websocket.DataHandler <- stream.KlineData{ - Timestamp: time.UnixMilli(kline.Timestamp), - Exchange: h.Name, - AssetType: a, - Pair: p, - OpenPrice: kline.Tick.Open, - ClosePrice: kline.Tick.Close, - HighPrice: kline.Tick.High, - LowPrice: kline.Tick.Low, - Volume: kline.Tick.Volume, - Interval: data[3], - } - case strings.Contains(init.Channel, "trade.detail"): - if !h.IsSaveTradeDataEnabled() { - return nil - } - var t WsTrade - err := json.Unmarshal(respRaw, &t) - if err != nil { - return err - } - data := strings.Split(t.Channel, ".") - var p currency.Pair - var a asset.Item - p, a, err = h.GetRequestFormattedPairAndAssetType(data[1]) - if err != nil { - return err - } - var trades []trade.Data - for i := range t.Tick.Data { - side := order.Buy - if t.Tick.Data[i].Direction != "buy" { - side = order.Sell - } - trades = append(trades, trade.Data{ - Exchange: h.Name, - AssetType: a, - CurrencyPair: p, - Timestamp: time.UnixMilli(t.Tick.Data[i].Timestamp), - Amount: t.Tick.Data[i].Amount, - Price: t.Tick.Data[i].Price, - Side: side, - TID: strconv.FormatFloat(t.Tick.Data[i].TradeID, 'f', -1, 64), - }) - } - return trade.AddTradesToBuffer(h.Name, trades...) - case strings.Contains(init.Channel, "detail"), - strings.Contains(init.Rep, "detail"): - var wsTicker WsTick - err := json.Unmarshal(respRaw, &wsTicker) - if err != nil { - return err - } - var data []string - if wsTicker.Channel != "" { - data = strings.Split(wsTicker.Channel, ".") - } - if wsTicker.Rep != "" { - data = strings.Split(wsTicker.Rep, ".") - } +// wsHandleV1ping handles v1 style pings, currently only used with public connections +func (h *HUOBI) wsHandleV1ping(pingValue int) error { + if err := h.Websocket.Conn.SendJSONMessage(context.Background(), request.Unset, json.RawMessage(`{"pong":`+strconv.Itoa(pingValue)+`}`)); err != nil { + return fmt.Errorf("error sending pong response: %w", err) + } + return nil +} - var p currency.Pair - var a asset.Item - p, a, err = h.GetRequestFormattedPairAndAssetType(data[1]) - if err != nil { - return err - } +// wsHandleV2ping handles v2 style pings, currently only used with private connections +func (h *HUOBI) wsHandleV2ping(respRaw []byte) error { + ts, err := jsonparser.GetInt(respRaw, "data", "ts") + if err != nil { + return fmt.Errorf("error getting ts from auth ping: %w", err) + } + if err := h.Websocket.AuthConn.SendJSONMessage(context.Background(), request.Unset, json.RawMessage(`{"action":"pong","data":{"ts":`+strconv.Itoa(int(ts))+`}}`)); err != nil { + return fmt.Errorf("error sending auth pong response: %w", err) + } + return nil +} - h.Websocket.DataHandler <- &ticker.Price{ - ExchangeName: h.Name, - Open: wsTicker.Tick.Open, - Close: wsTicker.Tick.Close, - Volume: wsTicker.Tick.Amount, - QuoteVolume: wsTicker.Tick.Volume, - High: wsTicker.Tick.High, - Low: wsTicker.Tick.Low, - LastUpdated: time.UnixMilli(wsTicker.Timestamp), - AssetType: a, - Pair: p, - } - default: - h.Websocket.DataHandler <- stream.UnhandledMessageWarning{ - Message: h.Name + stream.UnhandledMessage + string(respRaw), +func (h *HUOBI) wsHandleV2subResp(action string, respRaw []byte) error { + if ch, err := jsonparser.GetString(respRaw, "ch"); err == nil { + if !h.Websocket.Match.IncomingWithData(action+":"+ch, respRaw) { + return fmt.Errorf("%w: %s:%s", stream.ErrNoMessageListener, action, ch) } - return nil } return nil } -func (h *HUOBI) sendPingResponse(pong int64) { - err := h.Websocket.Conn.SendJSONMessage(context.TODO(), request.Unset, WsPong{Pong: pong}) - if err != nil { - log.Errorln(log.ExchangeSys, err) - } +func (h *HUOBI) wsHandleChannelMsgs(s *subscription.Subscription, respRaw []byte) error { + switch s.Channel { + case subscription.TickerChannel: + return h.wsHandleTickerMsg(s, respRaw) + case subscription.OrderbookChannel: + return h.wsHandleOrderbookMsg(s, respRaw) + case subscription.CandlesChannel: + return h.wsHandleCandleMsg(s, respRaw) + case subscription.AllTradesChannel: + return h.wsHandleAllTradesMsg(s, respRaw) + case subscription.MyAccountChannel: + return h.wsHandleMyAccountMsg(respRaw) + case subscription.MyOrdersChannel: + return h.wsHandleMyOrdersMsg(s, respRaw) + case subscription.MyTradesChannel: + return h.wsHandleMyTradesMsg(s, respRaw) + } + return fmt.Errorf("%w: %s", common.ErrNotYetImplemented, s.Channel) } -// WsProcessOrderbook processes new orderbook data -func (h *HUOBI) WsProcessOrderbook(update *WsDepth, symbol string) error { - pairs, err := h.GetEnabledPairs(asset.Spot) - if err != nil { +func (h *HUOBI) wsHandleCandleMsg(s *subscription.Subscription, respRaw []byte) error { + if len(s.Pairs) != 1 { + return subscription.ErrNotSinglePair + } + var c WsKline + if err := json.Unmarshal(respRaw, &c); err != nil { return err } + h.Websocket.DataHandler <- stream.KlineData{ + Timestamp: c.Timestamp.Time(), + Exchange: h.Name, + AssetType: s.Asset, + Pair: s.Pairs[0], + OpenPrice: c.Tick.Open, + ClosePrice: c.Tick.Close, + HighPrice: c.Tick.High, + LowPrice: c.Tick.Low, + Volume: c.Tick.Volume, + Interval: s.Interval.String(), + } + return nil +} - format, err := h.GetPairFormat(asset.Spot, true) - if err != nil { +func (h *HUOBI) wsHandleAllTradesMsg(s *subscription.Subscription, respRaw []byte) error { + if !h.IsSaveTradeDataEnabled() { + return nil + } + if len(s.Pairs) != 1 { + return subscription.ErrNotSinglePair + } + var t WsTrade + if err := json.Unmarshal(respRaw, &t); err != nil { return err } + trades := make([]trade.Data, 0, len(t.Tick.Data)) + for i := range t.Tick.Data { + side := order.Buy + if t.Tick.Data[i].Direction != "buy" { + side = order.Sell + } + trades = append(trades, trade.Data{ + Exchange: h.Name, + AssetType: s.Asset, + CurrencyPair: s.Pairs[0], + Timestamp: t.Tick.Data[i].Timestamp.Time(), + Amount: t.Tick.Data[i].Amount, + Price: t.Tick.Data[i].Price, + Side: side, + TID: strconv.FormatFloat(t.Tick.Data[i].TradeID, 'f', -1, 64), + }) + } + return trade.AddTradesToBuffer(h.Name, trades...) +} - p, err := currency.NewPairFromFormattedPairs(symbol, - pairs, - format) - if err != nil { +func (h *HUOBI) wsHandleTickerMsg(s *subscription.Subscription, respRaw []byte) error { + if len(s.Pairs) != 1 { + return subscription.ErrNotSinglePair + } + var wsTicker WsTick + if err := json.Unmarshal(respRaw, &wsTicker); err != nil { return err } + h.Websocket.DataHandler <- &ticker.Price{ + ExchangeName: h.Name, + Open: wsTicker.Tick.Open, + Close: wsTicker.Tick.Close, + Volume: wsTicker.Tick.Amount, + QuoteVolume: wsTicker.Tick.Volume, + High: wsTicker.Tick.High, + Low: wsTicker.Tick.Low, + LastUpdated: wsTicker.Timestamp.Time(), + AssetType: s.Asset, + Pair: s.Pairs[0], + } + return nil +} +func (h *HUOBI) wsHandleOrderbookMsg(s *subscription.Subscription, respRaw []byte) error { + if len(s.Pairs) != 1 { + return subscription.ErrNotSinglePair + } + var update WsDepth + if err := json.Unmarshal(respRaw, &update); err != nil { + return err + } bids := make(orderbook.Tranches, len(update.Tick.Bids)) for i := range update.Tick.Bids { price, ok := update.Tick.Bids[i][0].(float64) @@ -531,15 +335,156 @@ func (h *HUOBI) WsProcessOrderbook(update *WsDepth, symbol string) error { var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids - newOrderBook.Pair = p + newOrderBook.Pair = s.Pairs[0] newOrderBook.Asset = asset.Spot newOrderBook.Exchange = h.Name newOrderBook.VerifyOrderbook = h.CanVerifyOrderbook - newOrderBook.LastUpdated = time.UnixMilli(update.Timestamp) + newOrderBook.LastUpdated = update.Timestamp.Time() return h.Websocket.Orderbook.LoadSnapshot(&newOrderBook) } +func (h *HUOBI) wsHandleMyOrdersMsg(s *subscription.Subscription, respRaw []byte) error { + var msg wsOrderUpdateMsg + if err := json.Unmarshal(respRaw, &msg); err != nil { + return err + } + o := msg.Data + p, err := h.CurrencyPairs.Match(o.Symbol, s.Asset) + if err != nil { + return err + } + d := &order.Detail{ + ClientOrderID: o.ClientOrderID, + Price: o.Price, + Amount: o.Size, + ExecutedAmount: o.ExecutedAmount, + RemainingAmount: o.RemainingAmount, + Exchange: h.Name, + Side: o.Side, + AssetType: s.Asset, + Pair: p, + } + if o.OrderID != 0 { + d.OrderID = strconv.FormatInt(o.OrderID, 10) + } + switch o.EventType { + case "trigger", "deletion", "cancellation": + d.LastUpdated = o.LastActTime.Time() + case "creation": + d.LastUpdated = o.CreateTime.Time() + case "trade": + d.LastUpdated = o.TradeTime.Time() + } + if d.Status, err = order.StringToOrderStatus(o.OrderStatus); err != nil { + return &order.ClassificationError{ + Exchange: h.Name, + OrderID: d.OrderID, + Err: err, + } + } + if o.Side == order.UnknownSide { + d.Side, err = stringToOrderSide(o.OrderType) + if err != nil { + return &order.ClassificationError{ + Exchange: h.Name, + OrderID: d.OrderID, + Err: err, + } + } + } + if o.OrderType != "" { + d.Type, err = stringToOrderType(o.OrderType) + if err != nil { + return &order.ClassificationError{ + Exchange: h.Name, + OrderID: d.OrderID, + Err: err, + } + } + } + h.Websocket.DataHandler <- d + if o.ErrCode != 0 { + return fmt.Errorf("error with order `%s`: %s (%v)", o.ClientOrderID, o.ErrMessage, o.ErrCode) + } + return nil +} + +func (h *HUOBI) wsHandleMyTradesMsg(s *subscription.Subscription, respRaw []byte) error { + var msg wsTradeUpdateMsg + if err := json.Unmarshal(respRaw, &msg); err != nil { + return err + } + t := msg.Data + p, err := h.CurrencyPairs.Match(t.Symbol, s.Asset) + if err != nil { + return err + } + d := &order.Detail{ + ClientOrderID: t.ClientOrderID, + Price: t.OrderPrice, + Amount: t.OrderSize, + Exchange: h.Name, + Side: t.Side, + AssetType: s.Asset, + Pair: p, + Date: t.OrderCreateTime.Time(), + LastUpdated: t.TradeTime.Time(), + OrderID: strconv.FormatInt(t.OrderID, 10), + } + if d.Status, err = order.StringToOrderStatus(t.OrderStatus); err != nil { + return &order.ClassificationError{ + Exchange: h.Name, + OrderID: d.OrderID, + Err: err, + } + } + if t.Side == order.UnknownSide { + d.Side, err = stringToOrderSide(t.OrderType) + if err != nil { + return &order.ClassificationError{ + Exchange: h.Name, + OrderID: d.OrderID, + Err: err, + } + } + } + if t.OrderType != "" { + d.Type, err = stringToOrderType(t.OrderType) + if err != nil { + return &order.ClassificationError{ + Exchange: h.Name, + OrderID: d.OrderID, + Err: err, + } + } + } + d.Trades = []order.TradeHistory{ + { + Price: t.TradePrice, + Amount: t.TradeVolume, + Fee: t.TransactFee, + Exchange: h.Name, + TID: strconv.FormatInt(t.TradeID, 10), + Type: d.Type, + Side: d.Side, + IsMaker: !t.IsTaker, + Timestamp: t.TradeTime.Time(), + }, + } + h.Websocket.DataHandler <- d + return nil +} + +func (h *HUOBI) wsHandleMyAccountMsg(respRaw []byte) error { + u := &wsAccountUpdateMsg{} + if err := json.Unmarshal(respRaw, u); err != nil { + return err + } + h.Websocket.DataHandler <- u.Data + return nil +} + // generateSubscriptions returns a list of subscriptions from the configured subscriptions feature func (h *HUOBI) generateSubscriptions() (subscription.List, error) { return h.Features.Subscriptions.ExpandTemplates(h) @@ -548,284 +493,232 @@ func (h *HUOBI) generateSubscriptions() (subscription.List, error) { // GetSubscriptionTemplate returns a subscription channel template func (h *HUOBI) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) { return template.New("master.tmpl").Funcs(template.FuncMap{ - "channelName": channelName, - "interval": h.FormatExchangeKlineInterval, + "channelName": channelName, + "isWildcardChannel": isWildcardChannel, + "interval": h.FormatExchangeKlineInterval, }).Parse(subTplText) } // Subscribe sends a websocket message to receive data from the channel func (h *HUOBI) Subscribe(subs subscription.List) error { - ctx := context.Background() - var errs error - var creds *account.Credentials - if len(subs.Private()) > 0 { - if creds, errs = h.GetCredentials(ctx); errs != nil { - return errs - } - } - for _, s := range subs { - var err error - if s.Authenticated { - if err = h.wsAuthenticatedSubscribe(creds, "sub", wsAccountsOrdersEndPoint+"/"+s.QualifiedChannel, s.QualifiedChannel); err == nil { - err = h.Websocket.AddSuccessfulSubscriptions(h.Websocket.Conn, s) - } - } else { - if err = h.Websocket.Conn.SendJSONMessage(ctx, request.Unset, WsRequest{Subscribe: s.QualifiedChannel}); err == nil { - err = h.Websocket.AddSuccessfulSubscriptions(h.Websocket.AuthConn, s) - } - } - errs = common.AppendError(errs, err) - } - return nil + subs, errs := subs.ExpandTemplates(h) + return common.AppendError(errs, h.ParallelChanOp(subs, func(l subscription.List) error { return h.manageSubs(wsSubOp, l) }, 1)) } // Unsubscribe sends a websocket message to stop receiving data from the channel func (h *HUOBI) Unsubscribe(subs subscription.List) error { - ctx := context.Background() - var errs error - var creds *account.Credentials - if len(subs.Private()) > 0 { - if creds, errs = h.GetCredentials(ctx); errs != nil { - return errs - } + subs, errs := subs.ExpandTemplates(h) + return common.AppendError(errs, h.ParallelChanOp(subs, func(l subscription.List) error { return h.manageSubs(wsUnsubOp, l) }, 1)) +} + +func (h *HUOBI) manageSubs(op string, subs subscription.List) error { + if len(subs) != 1 { + return subscription.ErrBatchingNotSupported } - for _, s := range subs { - var err error - if s.Authenticated { - err = h.wsAuthenticatedSubscribe(creds, "unsub", wsAccountsOrdersEndPoint+"/"+s.QualifiedChannel, s.QualifiedChannel) + s := subs[0] + var c stream.Connection + var req any + if s.Authenticated { + c = h.Websocket.AuthConn + req = wsReq{Action: op, Channel: s.QualifiedChannel} + } else { + c = h.Websocket.Conn + if op == wsSubOp { + // Set the id to the channel so that V1 errors can make it back to us + req = wsSubReq{ID: wsSubOp + ":" + s.QualifiedChannel, Sub: s.QualifiedChannel} } else { - err = h.Websocket.Conn.SendJSONMessage(ctx, request.Unset, WsRequest{Unsubscribe: s.QualifiedChannel}) + req = wsSubReq{Unsub: s.QualifiedChannel} + } + } + if op == wsSubOp { + s.SetKey(s.QualifiedChannel) + if err := h.Websocket.AddSubscriptions(c, s); err != nil { + return fmt.Errorf("%w: %s; error: %w", stream.ErrSubscriptionFailure, s, err) + } + } + ctx := context.Background() + respRaw, err := c.SendMessageReturnResponse(ctx, request.Unset, wsSubOp+":"+s.QualifiedChannel, req) + if err == nil { + err = getErrResp(respRaw) + } + if err != nil { + if op == wsSubOp { + _ = h.Websocket.RemoveSubscriptions(c, s) } - if err == nil { - err = h.Websocket.RemoveSubscriptions(h.Websocket.Conn, s) + return fmt.Errorf("%s: %w", s, err) + } + if op == wsSubOp { + err = s.SetState(subscription.SubscribedState) + if h.Verbose { + log.Debugf(log.ExchangeSys, "%s Subscribed to %s", h.Name, s) } - errs = common.AppendError(errs, err) + } else { + err = h.Websocket.RemoveSubscriptions(c, s) } - return errs + return err } -func (h *HUOBI) wsGenerateSignature(creds *account.Credentials, timestamp, endpoint string) ([]byte, error) { +func (h *HUOBI) wsGenerateSignature(creds *account.Credentials, timestamp string) ([]byte, error) { values := url.Values{} - values.Set("AccessKeyId", creds.Key) - values.Set("SignatureMethod", signatureMethod) - values.Set("SignatureVersion", signatureVersion) - values.Set("Timestamp", timestamp) - host := "api.huobi.pro" - payload := fmt.Sprintf("%s\n%s\n%s\n%s", - http.MethodGet, host, endpoint, values.Encode()) + values.Set("accessKey", creds.Key) + values.Set("signatureMethod", signatureMethod) + values.Set("signatureVersion", signatureVersion) + values.Set("timestamp", timestamp) + payload := http.MethodGet + "\n" + wsSpotHost + "\n" + wsPrivatePath + "\n" + values.Encode() return crypto.GetHMAC(crypto.HashSHA256, []byte(payload), []byte(creds.Secret)) } -func (h *HUOBI) wsLogin(ctx context.Context) error { - if !h.IsWebsocketAuthenticationSupported() { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", h.Name) +func (h *HUOBI) wsAuthConnect(ctx context.Context) error { + if err := h.Websocket.AuthConn.Dial(&websocket.Dialer{}, http.Header{}); err != nil { + return fmt.Errorf("authenticated dial failed: %w", err) } - creds, err := h.GetCredentials(ctx) - if err != nil { - return err + if err := h.wsLogin(ctx); err != nil { + return fmt.Errorf("authentication failed: %w", err) } - h.Websocket.SetCanUseAuthenticatedEndpoints(true) - timestamp := time.Now().UTC().Format(wsDateTimeFormatting) - req := WsAuthenticationRequest{ - Op: authOp, - AccessKeyID: creds.Key, - SignatureMethod: signatureMethod, - SignatureVersion: signatureVersion, - Timestamp: timestamp, - } - hmac, err := h.wsGenerateSignature(creds, timestamp, wsAccountsOrdersEndPoint) - if err != nil { - return err - } - req.Signature = crypto.Base64Encode(hmac) - err = h.Websocket.AuthConn.SendJSONMessage(context.TODO(), request.Unset, req) - if err != nil { - h.Websocket.SetCanUseAuthenticatedEndpoints(false) - return err - } - - time.Sleep(loginDelay) return nil } -func (h *HUOBI) wsAuthenticatedSubscribe(creds *account.Credentials, operation, endpoint, topic string) error { - timestamp := time.Now().UTC().Format(wsDateTimeFormatting) - req := WsAuthenticatedSubscriptionRequest{ - Op: operation, - AccessKeyID: creds.Key, - SignatureMethod: signatureMethod, - SignatureVersion: signatureVersion, - Timestamp: timestamp, - Topic: topic, - } - hmac, err := h.wsGenerateSignature(creds, timestamp, endpoint) - if err != nil { - return err - } - req.Signature = crypto.Base64Encode(hmac) - return h.Websocket.AuthConn.SendJSONMessage(context.TODO(), request.Unset, req) -} - -func (h *HUOBI) wsGetAccountsList(ctx context.Context) (*WsAuthenticatedAccountsListResponse, error) { - if !h.Websocket.CanUseAuthenticatedEndpoints() { - return nil, fmt.Errorf("%v not authenticated cannot get accounts list", h.Name) - } +func (h *HUOBI) wsLogin(ctx context.Context) error { creds, err := h.GetCredentials(ctx) if err != nil { - return nil, err + return err } - timestamp := time.Now().UTC().Format(wsDateTimeFormatting) - req := WsAuthenticatedAccountsListRequest{ - Op: requestOp, - AccessKeyID: creds.Key, - SignatureMethod: signatureMethod, - SignatureVersion: signatureVersion, - Timestamp: timestamp, - Topic: wsAccountsList, - } - hmac, err := h.wsGenerateSignature(creds, timestamp, wsAccountListEndpoint) + c := h.Websocket.AuthConn + ts := time.Now().UTC().Format(wsDateTimeFormatting) + hmac, err := h.wsGenerateSignature(creds, ts) if err != nil { - return nil, err + return err } - req.Signature = crypto.Base64Encode(hmac) - req.ClientID = h.Websocket.AuthConn.GenerateMessageID(true) - resp, err := h.Websocket.AuthConn.SendMessageReturnResponse(context.TODO(), request.Unset, req.ClientID, req) + req := wsReq{ + Action: wsRequestOp, + Channel: wsAuthChannel, + Params: wsAuthReq{ + AuthType: "api", + AccessKey: creds.Key, + SignatureMethod: signatureMethod, + SignatureVersion: signatureVersion, + Signature: crypto.Base64Encode(hmac), + Timestamp: ts, + }, + } + err = c.SendJSONMessage(context.Background(), request.Unset, req) if err != nil { - return nil, err + return err } - var response WsAuthenticatedAccountsListResponse - err = json.Unmarshal(resp, &response) - if err != nil { - return nil, err + resp := c.ReadMessage() + if resp.Raw == nil { + return &websocket.CloseError{Code: websocket.CloseAbnormalClosure} } - code, _ := response.ErrorCode.(int) - if code != 0 { - return nil, errors.New(response.ErrorMessage) - } - return &response, nil + return getErrResp(resp.Raw) } -func (h *HUOBI) wsGetOrdersList(ctx context.Context, accountID int64, pair currency.Pair) (*WsAuthenticatedOrdersResponse, error) { - if !h.Websocket.CanUseAuthenticatedEndpoints() { - return nil, fmt.Errorf("%v not authenticated cannot get orders list", h.Name) - } - - creds, err := h.GetCredentials(ctx) - if err != nil { - return nil, err - } - - fPair, err := h.FormatExchangeCurrency(pair, asset.Spot) - if err != nil { - return nil, err - } - - timestamp := time.Now().UTC().Format(wsDateTimeFormatting) - req := WsAuthenticatedOrdersListRequest{ - Op: requestOp, - AccessKeyID: creds.Key, - SignatureMethod: signatureMethod, - SignatureVersion: signatureVersion, - Timestamp: timestamp, - Topic: wsOrdersList, - AccountID: accountID, - Symbol: fPair.String(), - States: "submitted,partial-filled", +func stringToOrderStatus(status string) (order.Status, error) { + switch status { + case "rejected": + return order.Rejected, nil + case "submitted": + return order.New, nil + case "partial-filled": + return order.PartiallyFilled, nil + case "filled": + return order.Filled, nil + case "partial-canceled": + return order.PartiallyCancelled, nil + case "canceled": + return order.Cancelled, nil + default: + return order.UnknownStatus, errors.New(status + " not recognised as order status") } +} - hmac, err := h.wsGenerateSignature(creds, timestamp, wsOrdersListEndpoint) - if err != nil { - return nil, err +func stringToOrderSide(side string) (order.Side, error) { + switch { + case strings.Contains(side, "buy"): + return order.Buy, nil + case strings.Contains(side, "sell"): + return order.Sell, nil } - req.Signature = crypto.Base64Encode(hmac) - req.ClientID = h.Websocket.AuthConn.GenerateMessageID(true) - resp, err := h.Websocket.AuthConn.SendMessageReturnResponse(context.TODO(), request.Unset, req.ClientID, req) - if err != nil { - return nil, err - } + return order.UnknownSide, errors.New(side + " not recognised as order side") +} - var response WsAuthenticatedOrdersResponse - err = json.Unmarshal(resp, &response) - if err != nil { - return nil, err +func stringToOrderType(oType string) (order.Type, error) { + switch { + case strings.Contains(oType, "limit"): + return order.Limit, nil + case strings.Contains(oType, "market"): + return order.Market, nil } - code, _ := response.ErrorCode.(int) - if code != 0 { - return nil, errors.New(response.ErrorMessage) - } - return &response, nil + return order.UnknownType, + errors.New(oType + " not recognised as order type") } -func (h *HUOBI) wsGetOrderDetails(ctx context.Context, orderID string) (*WsAuthenticatedOrderDetailResponse, error) { - if !h.Websocket.CanUseAuthenticatedEndpoints() { - return nil, fmt.Errorf("%v not authenticated cannot get order details", h.Name) - } - creds, err := h.GetCredentials(ctx) - if err != nil { - return nil, err - } - timestamp := time.Now().UTC().Format(wsDateTimeFormatting) - req := WsAuthenticatedOrderDetailsRequest{ - Op: requestOp, - AccessKeyID: creds.Key, - SignatureMethod: signatureMethod, - SignatureVersion: signatureVersion, - Timestamp: timestamp, - Topic: wsOrdersDetail, - OrderID: orderID, - } - hmac, err := h.wsGenerateSignature(creds, timestamp, wsOrdersDetailEndpoint) - if err != nil { - return nil, err - } - req.Signature = crypto.Base64Encode(hmac) - req.ClientID = h.Websocket.AuthConn.GenerateMessageID(true) - resp, err := h.Websocket.AuthConn.SendMessageReturnResponse(context.TODO(), request.Unset, req.ClientID, req) - if err != nil { - return nil, err - } - var response WsAuthenticatedOrderDetailResponse - err = json.Unmarshal(resp, &response) - if err != nil { - return nil, err +/* +getErrResp looks for any of the following to determine an error: +- An err-code (V1) +- A code field that isn't 200 (V2) +Error message is retreieved from the field err-message or message. +Errors are returned in the format of () +*/ +func getErrResp(msg []byte) error { + var errCode string + errMsg, _ := jsonparser.GetString(msg, "err-msg") + errCode, err := jsonparser.GetString(msg, "err-code") + switch err { + case nil: // Nothing to do + case jsonparser.KeyPathNotFoundError: // Look for a V2 error + errCodeInt, err := jsonparser.GetInt(msg, "code") + if errCodeInt == 200 || errors.Is(err, jsonparser.KeyPathNotFoundError) { + return nil + } + if err != nil { + return fmt.Errorf("%w: %w", errParsingMsg, err) + } + errCode = strconv.Itoa(int(errCodeInt)) + errMsg, _ = jsonparser.GetString(msg, "message") } - - code, _ := response.ErrorCode.(int) - if code != 0 { - return nil, errors.New(response.ErrorMessage) + if errCode != "" { + return fmt.Errorf("%s (%v)", errMsg, errCode) } - return &response, nil + return nil } // channelName converts global channel Names used in config of channel input into exchange channel names // returns the name unchanged if no match is found -func channelName(s *subscription.Subscription, p currency.Pair) string { +func channelName(s *subscription.Subscription, p ...currency.Pair) string { if n, ok := subscriptionNames[s.Channel]; ok { - return fmt.Sprintf(n, p) - } - if s.Authenticated { - panic(fmt.Errorf("%w: Private endpoints not currently supported", common.ErrNotYetImplemented)) + if strings.Contains(n, "%s") { + return fmt.Sprintf(n, p[0]) + } + return n } panic(subscription.ErrUseConstChannelName) } +func isWildcardChannel(s *subscription.Subscription) bool { + return s.Channel == subscription.MyTradesChannel || s.Channel == subscription.MyOrdersChannel +} + const subTplText = ` {{- if $.S.Asset }} {{ range $asset, $pairs := $.AssetPairs }} - {{- range $p := $pairs }} - {{- channelName $.S $p -}} - {{- if eq $.S.Channel "candles" -}} . {{- interval $.S.Interval }}{{ end }} - {{- if eq $.S.Channel "orderbook" -}} .step {{- $.S.Levels }}{{ end }} - {{ $.PairSeparator }} + {{- if isWildcardChannel $.S }} + {{- channelName $.S -}} + {{- else }} + {{- range $p := $pairs }} + {{- channelName $.S $p -}} + {{- if eq $.S.Channel "candles" -}} . {{- interval $.S.Interval }}{{ end }} + {{- if eq $.S.Channel "orderbook" -}} .step {{- $.S.Levels }}{{ end }} + {{ $.PairSeparator }} + {{- end }} {{- end }} {{ $.AssetSeparator }} {{- end }} {{- else -}} - {{ channelName $.S nil }} + {{ channelName $.S }} {{- end }} ` diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 76c461cb196..d62389028ef 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -80,6 +80,12 @@ func (h *HUOBI) SetDefaults() { log.Errorln(log.ExchangeSys, err) } + for _, a := range []asset.Item{asset.Futures, asset.CoinMarginedFutures} { + if err = h.DisableAssetWebsocketSupport(a); err != nil { + log.Errorf(log.ExchangeSys, "%s error disabling `%s` asset type websocket support: %s", h.Name, a, err) + } + } + h.Features = exchange.Features{ Supports: exchange.FeaturesSupported{ REST: true, @@ -176,7 +182,7 @@ func (h *HUOBI) SetDefaults() { exchange.RestSpot: huobiAPIURL, exchange.RestFutures: huobiFuturesURL, exchange.RestCoinMargined: huobiFuturesURL, - exchange.WebsocketSpot: wsMarketURL, + exchange.WebsocketSpot: wsSpotURL + wsPublicPath, }) if err != nil { log.Errorln(log.ExchangeSys, err) @@ -220,7 +226,7 @@ func (h *HUOBI) Setup(exch *config.Exchange) error { err = h.Websocket.Setup(&stream.WebsocketSetup{ ExchangeConfig: exch, - DefaultURL: wsMarketURL, + DefaultURL: wsSpotURL + wsPublicPath, RunningURL: wsRunningURL, Connector: h.WsConnect, Subscriber: h.Subscribe, @@ -245,7 +251,7 @@ func (h *HUOBI) Setup(exch *config.Exchange) error { RateLimit: request.NewWeightedRateLimitByDuration(20 * time.Millisecond), ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, ResponseMaxLimit: exch.WebsocketResponseMaxLimit, - URL: wsAccountsOrdersURL, + URL: wsSpotURL + wsPrivatePath, Authenticated: true, }) } @@ -301,7 +307,8 @@ func (h *HUOBI) FetchTradablePairs(ctx context.Context, a asset.Item) (currency. } pairs = make([]currency.Pair, 0, len(symbols.Data)) expiryCodeDates := map[string]currency.Code{} - for _, c := range symbols.Data { + for i := range symbols.Data { + c := symbols.Data[i] if c.ContractStatus != 1 { continue } @@ -397,7 +404,7 @@ func (h *HUOBI) UpdateTickers(ctx context.Context, a asset.Item) error { } continue } - tt := time.UnixMilli(ticks[i].Timestamp) + tt := ticks[i].Timestamp.Time() err = ticker.ProcessTicker(&ticker.Price{ High: ticks[i].High.Float64(), Low: ticks[i].Low.Float64(), @@ -465,7 +472,7 @@ func (h *HUOBI) UpdateTickers(ctx context.Context, a asset.Item) error { Pair: cp, ExchangeName: h.Name, AssetType: a, - LastUpdated: time.UnixMilli(ticks[i].Timestamp), + LastUpdated: ticks[i].Timestamp.Time(), }) if err != nil { errs = common.AppendError(errs, err) @@ -694,72 +701,50 @@ func (h *HUOBI) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (ac info.Exchange = h.Name switch assetType { case asset.Spot: - if h.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - resp, err := h.wsGetAccountsList(ctx) - if err != nil { - return info, err - } - var currencyDetails []account.Balance - for i := range resp.Data { - if len(resp.Data[i].List) == 0 { - continue - } - currData := account.Balance{ - Currency: currency.NewCode(resp.Data[i].List[0].Currency), - Total: resp.Data[i].List[0].Balance, - } - if len(resp.Data[i].List) > 1 && resp.Data[i].List[1].Type == "frozen" { - currData.Hold = resp.Data[i].List[1].Balance - } - currencyDetails = append(currencyDetails, currData) + accounts, err := h.GetAccountID(ctx) + if err != nil { + return info, err + } + for i := range accounts { + if accounts[i].Type != "spot" { + continue } - acc.Currencies = currencyDetails - } else { - accounts, err := h.GetAccountID(ctx) + acc.ID = strconv.FormatInt(accounts[i].ID, 10) + balances, err := h.GetAccountBalance(ctx, acc.ID) if err != nil { return info, err } - for i := range accounts { - if accounts[i].Type != "spot" { - continue - } - acc.ID = strconv.FormatInt(accounts[i].ID, 10) - balances, err := h.GetAccountBalance(ctx, acc.ID) - if err != nil { - return info, err - } - var currencyDetails []account.Balance - balance: - for j := range balances { - frozen := balances[j].Type == "frozen" - for i := range currencyDetails { - if currencyDetails[i].Currency.String() == balances[j].Currency { - if frozen { - currencyDetails[i].Hold = balances[j].Balance - } else { - currencyDetails[i].Total = balances[j].Balance - } - continue balance + var currencyDetails []account.Balance + balance: + for j := range balances { + frozen := balances[j].Type == "frozen" + for i := range currencyDetails { + if currencyDetails[i].Currency.String() == balances[j].Currency { + if frozen { + currencyDetails[i].Hold = balances[j].Balance + } else { + currencyDetails[i].Total = balances[j].Balance } + continue balance } + } - if frozen { - currencyDetails = append(currencyDetails, - account.Balance{ - Currency: currency.NewCode(balances[j].Currency), - Hold: balances[j].Balance, - }) - } else { - currencyDetails = append(currencyDetails, - account.Balance{ - Currency: currency.NewCode(balances[j].Currency), - Total: balances[j].Balance, - }) - } + if frozen { + currencyDetails = append(currencyDetails, + account.Balance{ + Currency: currency.NewCode(balances[j].Currency), + Hold: balances[j].Balance, + }) + } else { + currencyDetails = append(currencyDetails, + account.Balance{ + Currency: currency.NewCode(balances[j].Currency), + Total: balances[j].Balance, + }) } - acc.Currencies = currencyDetails } + acc.Currencies = currencyDetails } case asset.CoinMarginedFutures: @@ -898,7 +883,7 @@ func (h *HUOBI) GetWithdrawalsHistory(ctx context.Context, c currency.Code, a as resp[i] = exchange.WithdrawalHistory{ Status: withdrawals.Data[i].State, TransferID: withdrawals.Data[i].TransactionHash, - Timestamp: time.UnixMilli(withdrawals.Data[i].CreatedAt), + Timestamp: withdrawals.Data[i].CreatedAt.Time(), Currency: withdrawals.Data[i].Currency.String(), Amount: withdrawals.Data[i].Amount, Fee: withdrawals.Data[i].Fee, @@ -942,7 +927,7 @@ func (h *HUOBI) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.It Side: side, Price: sTrades[i].Trades[j].Price, Amount: sTrades[i].Trades[j].Amount, - Timestamp: time.UnixMilli(sTrades[i].Timestamp), + Timestamp: sTrades[i].Timestamp.Time(), }) } } @@ -969,7 +954,7 @@ func (h *HUOBI) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.It Side: side, Price: fTrades.Data[i].Data[j].Price, Amount: fTrades.Data[i].Data[j].Amount, - Timestamp: time.UnixMilli(fTrades.Data[i].Data[j].Timestamp), + Timestamp: fTrades.Data[i].Data[j].Timestamp.Time(), }) } } @@ -995,7 +980,7 @@ func (h *HUOBI) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.It Side: side, Price: cTrades.Data[i].Price, Amount: cTrades.Data[i].Amount, - Timestamp: time.UnixMilli(cTrades.Data[i].Timestamp), + Timestamp: cTrades.Data[i].Timestamp.Time(), }) } } @@ -1316,24 +1301,15 @@ func (h *HUOBI) GetOrderInfo(ctx context.Context, orderID string, pair currency. var orderDetail order.Detail switch assetType { case asset.Spot: - var respData *OrderInfo - if h.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - resp, err := h.wsGetOrderDetails(ctx, orderID) - if err != nil { - return nil, err - } - respData = &resp.Data - } else { - oID, err := strconv.ParseInt(orderID, 10, 64) - if err != nil { - return nil, err - } - resp, err := h.GetOrder(ctx, oID) - if err != nil { - return nil, err - } - respData = &resp + oID, err := strconv.ParseInt(orderID, 10, 64) + if err != nil { + return nil, err + } + resp, err := h.GetOrder(ctx, oID) + if err != nil { + return nil, err } + respData := &resp if respData.ID == 0 { return nil, fmt.Errorf("%s - order not found for orderid %s", h.Name, orderID) } @@ -1543,87 +1519,34 @@ func (h *HUOBI) GetActiveOrders(ctx context.Context, req *order.MultiOrderReques if req.Side == order.Sell { side = req.Side.Lower() } - if h.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - for i := range req.Pairs { - resp, err := h.wsGetOrdersList(ctx, -1, req.Pairs[i]) - if err != nil { - return orders, err - } - for j := range resp.Data { - sideData := strings.Split(resp.Data[j].OrderState, "-") - side = sideData[0] - var orderID = strconv.FormatInt(resp.Data[j].OrderID, 10) - orderSide, err := order.StringToOrderSide(side) - if err != nil { - h.Websocket.DataHandler <- order.ClassificationError{ - Exchange: h.Name, - OrderID: orderID, - Err: err, - } - } - orderType, err := order.StringToOrderType(sideData[1]) - if err != nil { - h.Websocket.DataHandler <- order.ClassificationError{ - Exchange: h.Name, - OrderID: orderID, - Err: err, - } - } - orderStatus, err := order.StringToOrderStatus(resp.Data[j].OrderState) - if err != nil { - h.Websocket.DataHandler <- order.ClassificationError{ - Exchange: h.Name, - OrderID: orderID, - Err: err, - } - } - orders = append(orders, order.Detail{ - Exchange: h.Name, - AccountID: strconv.FormatInt(resp.Data[j].AccountID, 10), - OrderID: orderID, - Pair: req.Pairs[i], - Type: orderType, - Side: orderSide, - Date: time.UnixMilli(resp.Data[j].CreatedAt), - Status: orderStatus, - Price: resp.Data[j].Price, - Amount: resp.Data[j].OrderAmount, - ExecutedAmount: resp.Data[j].FilledAmount, - RemainingAmount: resp.Data[j].UnfilledAmount, - Fee: resp.Data[j].FilledFees, - }) - } - } - } else { - creds, err := h.GetCredentials(ctx) + creds, err := h.GetCredentials(ctx) + if err != nil { + return nil, err + } + for i := range req.Pairs { + resp, err := h.GetOpenOrders(ctx, + req.Pairs[i], + creds.ClientID, + side, + 500) if err != nil { return nil, err } - for i := range req.Pairs { - resp, err := h.GetOpenOrders(ctx, - req.Pairs[i], - creds.ClientID, - side, - 500) - if err != nil { - return nil, err - } - for x := range resp { - orderDetail := order.Detail{ - OrderID: strconv.FormatInt(resp[x].ID, 10), - Price: resp[x].Price, - Amount: resp[x].Amount, - ExecutedAmount: resp[x].FilledAmount, - RemainingAmount: resp[x].Amount - resp[x].FilledAmount, - Pair: req.Pairs[i], - Exchange: h.Name, - Date: time.UnixMilli(resp[x].CreatedAt), - AccountID: strconv.FormatInt(resp[x].AccountID, 10), - Fee: resp[x].FilledFees, - } - setOrderSideStatusAndType(resp[x].State, resp[x].Type, &orderDetail) - orders = append(orders, orderDetail) + for x := range resp { + orderDetail := order.Detail{ + OrderID: strconv.FormatInt(resp[x].ID, 10), + Price: resp[x].Price, + Amount: resp[x].Amount, + ExecutedAmount: resp[x].FilledAmount, + RemainingAmount: resp[x].Amount - resp[x].FilledAmount, + Pair: req.Pairs[i], + Exchange: h.Name, + Date: time.UnixMilli(resp[x].CreatedAt), + AccountID: strconv.FormatInt(resp[x].AccountID, 10), + Fee: resp[x].FilledFees, } + setOrderSideStatusAndType(resp[x].State, resp[x].Type, &orderDetail) + orders = append(orders, orderDetail) } } case asset.CoinMarginedFutures: @@ -1950,7 +1873,7 @@ func (h *HUOBI) GetHistoricCandles(ctx context.Context, pair currency.Pair, a as } for x := range candles { - timestamp := time.Unix(candles[x].IDTimestamp, 0) + timestamp := candles[x].IDTimestamp.Time() if timestamp.Before(req.Start) || timestamp.After(req.End) { continue } @@ -1971,7 +1894,7 @@ func (h *HUOBI) GetHistoricCandles(ctx context.Context, pair currency.Pair, a as return nil, err } for x := range candles.Data { - timestamp := time.Unix(candles.Data[x].IDTimestamp, 0) + timestamp := candles.Data[x].IDTimestamp.Time() if timestamp.Before(req.Start) || timestamp.After(req.End) { continue } @@ -1992,7 +1915,7 @@ func (h *HUOBI) GetHistoricCandles(ctx context.Context, pair currency.Pair, a as return nil, err } for x := range candles.Data { - timestamp := time.Unix(candles.Data[x].IDTimestamp, 0) + timestamp := candles.Data[x].IDTimestamp.Time() if timestamp.Before(req.Start) || timestamp.After(req.End) { continue } @@ -2032,7 +1955,7 @@ func (h *HUOBI) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pa } for x := range candles.Data { // align response data - timestamp := time.Unix(candles.Data[x].IDTimestamp, 0).UTC() + timestamp := candles.Data[x].IDTimestamp.Time() if timestamp.Before(req.Start) || timestamp.After(req.End) { continue } @@ -2057,7 +1980,7 @@ func (h *HUOBI) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pa } for x := range candles.Data { // align response data - timestamp := time.Unix(candles.Data[x].IDTimestamp, 0) + timestamp := candles.Data[x].IDTimestamp.Time() if timestamp.Before(req.Start) || timestamp.After(req.End) { continue } @@ -2212,10 +2135,10 @@ func (h *HUOBI) GetFuturesContractDetails(ctx context.Context, item asset.Item) if err != nil { return nil, err } - if result.Data[x].DeliveryTime > 0 { - e = time.UnixMilli(result.Data[x].DeliveryTime) + if result.Data[x].DeliveryTime.Time().IsZero() { + e = result.Data[x].DeliveryTime.Time() } else { - e = time.UnixMilli(result.Data[x].SettlementTime) + e = result.Data[x].SettlementTime.Time() } contractLength := e.Sub(s) var ct futures.ContractType diff --git a/exchanges/huobi/testdata/wsAllTrades.json b/exchanges/huobi/testdata/wsAllTrades.json new file mode 100644 index 00000000000..916c41ce0a9 --- /dev/null +++ b/exchanges/huobi/testdata/wsAllTrades.json @@ -0,0 +1 @@ +{"ch":"market.btcusdt.trade.detail","ts":1630994963175,"tick":{"id":137005445109,"ts":1630994963173,"data":[{"id":137005445109359290000000000,"ts":1630994963173,"tradeId":102523573486,"amount":0.006754,"price":52648.62,"direction":"buy"}]}} diff --git a/exchanges/huobi/testdata/wsCandles.json b/exchanges/huobi/testdata/wsCandles.json new file mode 100644 index 00000000000..a51783aa675 --- /dev/null +++ b/exchanges/huobi/testdata/wsCandles.json @@ -0,0 +1 @@ +{"ch":"market.btcusdt.kline.1min","ts":1489474082831,"tick":{"id":1489464480,"amount":1821.49,"count":4,"open":7962.62,"close":8014.56,"low":5110.14,"high":14962.77,"vol":4.4}} diff --git a/exchanges/huobi/testdata/wsMyAccount.json b/exchanges/huobi/testdata/wsMyAccount.json new file mode 100644 index 00000000000..bc85fa36fb4 --- /dev/null +++ b/exchanges/huobi/testdata/wsMyAccount.json @@ -0,0 +1,3 @@ +{"action":"push","ch":"accounts.update#2","data":{"currency":"btc","accountId":123456,"balance":"23.111","changeType":"transfer","accountType":"trade","seqNum":1,"changeTime":1568601800000}} +{"action":"push","ch":"accounts.update#2","data":{"currency":"btc","accountId":33385,"available":"2028.69","changeType":"order.match","accountType":"trade","seqNum":2,"changeTime":1574393385167}} +{"action":"push","ch":"accounts.update#2","data":{"currency":"usdt","accountId":14884859,"balance":"20.29388158","available":"20.29388158","changeType":null,"accountType":"trade","changeTime":null,"seqNum":3}} diff --git a/exchanges/huobi/testdata/wsMyOrders.json b/exchanges/huobi/testdata/wsMyOrders.json new file mode 100644 index 00000000000..689d7bf44f6 --- /dev/null +++ b/exchanges/huobi/testdata/wsMyOrders.json @@ -0,0 +1,4 @@ +{"action":"push","ch":"orders#*","data":{"orderSide":"buy","lastActTime":1583853365586,"clientOrderId":"test1","orderStatus":"rejected","symbol":"btcusdt","eventType":"trigger","errCode":2002,"errMessage":"invalid.client.order.id (NT)"}} +{"action":"push","ch":"orders#*","data":{"orderSide":"buy","lastActTime":1583853365586,"clientOrderId":"test2","orderStatus":"canceled","symbol":"btcusdt","eventType":"deletion"}} +{"action":"push","ch":"orders#*","data":{"orderSize":"2.000000000000000000","orderCreateTime":1583853365586,"accountld":992701,"orderPrice":"77.000000000000000000","type":"sell-limit","orderId":27163533,"clientOrderId":"test3","orderSource":"spot-api","orderStatus":"submitted","symbol":"btcusdt","eventType":"creation"}} +{"action":"push","ch":"orders#*","data":{"orderSource":"spot-web","accountId":16820007,"orderPrice":"70000","orderSize":"0.000157","orderCreateTime":1731039387696,"symbol":"btcusdt","eventType":"creation","type":"buy-limit","orderId":1199329381585359,"clientOrderId":"","orderStatus":"submitted"}} diff --git a/exchanges/huobi/testdata/wsMyTrades.json b/exchanges/huobi/testdata/wsMyTrades.json new file mode 100644 index 00000000000..4bb712679ba --- /dev/null +++ b/exchanges/huobi/testdata/wsMyTrades.json @@ -0,0 +1 @@ +{"ch":"trade.clearing#btcusdt#1","data":{"eventType":"trade","symbol":"btcusdt","orderId":99998888,"tradePrice":"9999.99","tradeVolume":"0.96","orderSide":"buy","aggressor":true,"tradeId":919219323232,"tradeTime":1583853365996,"transactFee":"19.88","feeDeduct ":"0","feeDeductType":"","feeCurrency":"btc","accountId":9912791,"source":"spot-api","orderPrice":"10000","orderSize":"1","clientOrderId":"a001","orderCreateTime":1583853365586,"orderStatus":"partial-filled"}} diff --git a/exchanges/huobi/testdata/wsOrderbook.json b/exchanges/huobi/testdata/wsOrderbook.json new file mode 100644 index 00000000000..1385e890b0e --- /dev/null +++ b/exchanges/huobi/testdata/wsOrderbook.json @@ -0,0 +1 @@ +{"ch":"market.btcusdt.depth.step0","ts":1630983549503,"tick":{"bids":[[52690.69,0.36281],[52690.68,0.2]],"asks":[[52690.7,0.372591],[52691.26,0.13]],"version":136998124622,"ts":1630983549500}} diff --git a/exchanges/huobi/testdata/wsTicker.json b/exchanges/huobi/testdata/wsTicker.json new file mode 100644 index 00000000000..01f566010b6 --- /dev/null +++ b/exchanges/huobi/testdata/wsTicker.json @@ -0,0 +1 @@ +{"ch":"market.btcusdt.detail","ts":1630998026649,"tick":{"id":273956868110,"low":51000,"high":52924.14,"open":51823.62,"close":52379.99,"vol":727676440.200527,"amount":13991.028076056185,"version":273956868110,"count":471348}} diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 75193c1ccab..5f5d28fe7af 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -184,8 +184,7 @@ type ModifyResponse struct { } // Detail contains all properties of an order -// Each exchange has their own requirements, so not all fields -// are required to be populated +// Each exchange has their own requirements, so not all fields are required to be populated type Detail struct { ImmediateOrCancel bool HiddenOrder bool diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index ad09ba5dfc8..08c82a6001c 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -1167,15 +1167,15 @@ func StringToOrderStatus(status string) (Status, error) { switch status { case AnyStatus.String(): return AnyStatus, nil - case New.String(), "PLACED", "ACCEPTED": + case New.String(), "PLACED", "ACCEPTED", "SUBMITTED": return New, nil case Active.String(), "STATUS_ACTIVE", "LIVE": return Active, nil - case PartiallyFilled.String(), "PARTIALLY MATCHED", "PARTIALLY FILLED": + case PartiallyFilled.String(), "PARTIAL-FILLED", "PARTIALLY MATCHED", "PARTIALLY FILLED": return PartiallyFilled, nil case Filled.String(), "FULLY MATCHED", "FULLY FILLED", "ORDER_FULLY_TRANSACTED", "EFFECTIVE": return Filled, nil - case PartiallyCancelled.String(), "PARTIALLY CANCELLED", "ORDER_PARTIALLY_TRANSACTED": + case PartiallyCancelled.String(), "PARTIAL-CANCELED", "PARTIALLY CANCELLED", "ORDER_PARTIALLY_TRANSACTED": return PartiallyCancelled, nil case PartiallyFilledCancelled.String(), "PARTIALLYFILLEDCANCELED": return PartiallyFilledCancelled, nil diff --git a/internal/testing/exchange/exchange.go b/internal/testing/exchange/exchange.go index 3612edb2acf..2788993c695 100644 --- a/internal/testing/exchange/exchange.go +++ b/internal/testing/exchange/exchange.go @@ -118,23 +118,45 @@ func MockWsInstance[T any, PT interface { return e } -// FixtureToDataHandler squirts the contents of a file to a reader function (probably e.wsHandleData) +// FixtureError contains an error and the message that caused it +type FixtureError struct { + Err error + Msg []byte +} + +// FixtureToDataHandler squirts the contents of a file to a reader function (probably e.wsHandleData) and asserts no errors are returned func FixtureToDataHandler(tb testing.TB, fixturePath string, reader func([]byte) error) { tb.Helper() + for _, e := range FixtureToDataHandlerWithErrors(tb, fixturePath, reader) { + assert.NoErrorf(tb, e.Err, "Should not error handling message:\n%s", e.Msg) + } +} + +// FixtureToDataHandlerWithErrors squirts the contents of a file to a reader function (probably e.wsHandleData) and returns handler errors +// Any errors setting up the fixture will fail tests +func FixtureToDataHandlerWithErrors(tb testing.TB, fixturePath string, reader func([]byte) error) []FixtureError { + tb.Helper() + fixture, err := os.Open(fixturePath) - assert.NoError(tb, err, "Opening fixture '%s' should not error", fixturePath) + require.NoError(tb, err, "Opening fixture '%s' should not error", fixturePath) defer func() { assert.NoError(tb, fixture.Close(), "Closing the fixture file should not error") }() + errs := []FixtureError{} s := bufio.NewScanner(fixture) for s.Scan() { msg := s.Bytes() - err := reader(msg) - assert.NoErrorf(tb, err, "Fixture message should not error:\n%s", msg) + if err := reader(msg); err != nil { + errs = append(errs, FixtureError{ + Err: err, + Msg: msg, + }) + } } assert.NoError(tb, s.Err(), "Fixture Scanner should not error") + return errs } var setupWsMutex sync.Mutex @@ -178,6 +200,7 @@ var updatePairsOnce = make(map[string]*currency.PairsManager) // UpdatePairsOnce ensures pairs are only updated once in parallel tests // A clone of the cache of the updated pairs is used to populate duplicate requests +// Any pairs enabled after this is called will be lost on the next call func UpdatePairsOnce(tb testing.TB, e exchange.IBotExchange) { tb.Helper() diff --git a/testdata/configtest.json b/testdata/configtest.json index 51de85a84c9..caa19e825ef 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -1844,7 +1844,7 @@ }, "spot": { "assetEnabled": true, - "enabled": "BTC-USDT", + "enabled": "BTC-USDT,ETH-BTC", "available": "PROPY-ETH,IOTA-BTC,UGAS-ETH,PAI-USDT,BSV-HUSD,MTX-ETH,BCH-BTC,LTC-HT,SOC-USDT,WXT-BTC,SALT-BTC,RCN-ETH,PNT-ETH,TT-USDT,AIDOC-ETH,BIX-BTC,OCN-USDT,QTUM-ETH,KCASH-ETH,SNT-USDT,LUN-BTC,QASH-BTC,ITC-BTC,NAS-BTC,XMR-BTC,TNT-ETH,UC-ETH,FAIR-BTC,PC-ETH,YEE-BTC,PAY-ETH,XMX-BTC,CRE-USDT,BAT-ETH,BHT-USDT,CKB-HT,LAMB-HT,AE-USDT,QUN-ETH,LYM-BTC,BCH-HT,BHT-BTC,RUFF-ETH,CNN-BTC,FOR-USDT,GTC-ETH,TRX-ETH,ELA-USDT,ACT-ETH,SMT-ETH,BUT-ETH,BCH-USDT,ICX-BTC,MEET-BTC,NCC-BTC,APPC-BTC,GVE-ETH,TNB-BTC,STEEM-ETH,18C-ETH,LBA-BTC,EKO-BTC,REQ-BTC,SOC-BTC,BOX-ETH,ELF-BTC,ZRX-ETH,LET-USDT,HT-BTC,TUSD-HUSD,EGCC-BTC,WTC-BTC,ATP-USDT,DOCK-USDT,PAI-BTC,ONT-ETH,IRIS-BTC,BTT-ETH,SC-BTC,XZC-BTC,LBA-USDT,HT-USDT,VET-ETH,KMD-ETH,SHE-ETH,PORTAL-BTC,ONE-BTC,BIX-USDT,RCCC-BTC,SKM-USDT,XTZ-ETH,SWFTC-BTC,RSR-BTC,LINK-ETH,DATX-BTC,HPT-HT,GET-ETH,BLZ-ETH,CTXC-USDT,CNNS-USDT,PVT-HT,ITC-USDT,LTC-BTC,NCASH-BTC,HOT-ETH,ADA-USDT,ADX-BTC,NODE-USDT,TRIO-BTC,GXC-ETH,SNT-BTC,FOR-BTC,DBC-BTC,UUU-USDT,CVCOIN-ETH,RSR-USDT,CRO-USDT,OCN-BTC,NEW-USDT,EGT-USDT,MANA-BTC,CMT-USDT,WXT-HT,XRP-BTC,MT-ETH,PAX-HUSD,LSK-ETH,IOTA-USDT,SRN-ETH,ZIL-ETH,ELF-USDT,LXT-ETH,LAMB-BTC,CRE-HT,CKB-BTC,XVG-BTC,BSV-BTC,BFT-BTC,WPR-ETH,HT-HUSD,POWR-BTC,MANA-ETH,ENG-ETH,ZJLT-ETH,SNC-ETH,ATOM-ETH,WICC-USDT,KAN-ETH,DGD-BTC,VSYS-HT,BCD-BTC,BTM-ETH,DOGE-USDT,MEX-BTC,BTG-BTC,DAC-ETH,DAT-BTC,GRS-ETH,ADX-ETH,EM-HT,GXC-USDT,CVC-BTC,OMG-ETH,SSP-ETH,OGO-HT,CMT-ETH,POLY-ETH,XZC-USDT,THETA-USDT,XEM-USDT,LOL-USDT,BCH-HUSD,GSC-BTC,DOGE-ETH,MDS-BTC,BTS-ETH,CTXC-BTC,MCO-BTC,BCX-BTC,ZLA-ETH,EKT-USDT,MAN-BTC,BLZ-BTC,ATOM-USDT,LOL-BTC,HPT-USDT,EM-BTC,EOS-USDT,WAN-BTC,GNT-BTC,CRO-BTC,MANA-USDT,SEELE-USDT,FSN-BTC,VIDY-HT,USDC-HUSD,LTC-HUSD,XRP-USDT,VSYS-BTC,STORJ-BTC,LOOM-ETH,SKM-BTC,LINK-USDT,TT-HT,QSP-ETH,ETN-BTC,FSN-HT,NODE-BTC,HC-USDT,PHX-BTC,XLM-BTC,RCCC-ETH,LTC-USDT,UUU-BTC,SEELE-ETH,PVT-BTC,HC-ETH,REN-ETH,KAN-USDT,EOS-ETH,BSV-USDT,BTS-USDT,KMD-BTC,OGO-USDT,THETA-ETH,MUSK-BTC,CNNS-HT,ETC-BTC,COVA-BTC,BTT-TRX,XMR-USDT,MTN-ETH,QUN-BTC,NAS-USDT,ELA-ETH,HIT-ETH,BTT-USDT,EKT-ETH,TOS-BTC,GAS-ETH,DCR-USDT,ONT-BTC,NEW-HT,NEXO-BTC,ETH-USDT,WXT-USDT,FOR-HT,ADA-BTC,EVX-ETH,VET-BTC,ZEC-USDT,NANO-ETH,IOST-HT,BCV-ETH,REN-USDT,NULS-ETH,ACT-USDT,LET-ETH,BTM-USDT,MEET-ETH,AKRO-HT,ARDR-BTC,DCR-ETH,NANO-USDT,BTC-HUSD,ALGO-BTC,IIC-ETH,BHD-BTC,KNC-ETH,ATP-BTC,ZRX-BTC,ABT-BTC,18C-BTC,XMR-ETH,WAXP-BTC,CVNT-BTC,MX-USDT,OST-ETH,NKN-BTC,TOPC-BTC,GNX-BTC,FTT-USDT,ONE-HT,DGB-ETH,NULS-USDT,DASH-BTC,UIP-BTC,KCASH-HT,WICC-ETH,EKO-ETH,EGT-HT,IRIS-USDT,STK-ETH,MXC-BTC,NAS-ETH,OMG-USDT,SMT-BTC,BUT-BTC,HIT-USDT,BAT-BTC,IRIS-ETH,NKN-HT,PC-BTC,TOP-USDT,GTC-BTC,LSK-BTC,ITC-ETH,DTA-BTC,HOT-BTC,BTT-BTC,FAIR-ETH,DOCK-ETH,QTUM-BTC,ZEN-BTC,ZIL-BTC,RCN-BTC,FTI-BTC,BHD-USDT,VIDY-USDT,LUN-ETH,DBC-ETH,TOPC-ETH,IIC-BTC,STEEM-USDT,IOTA-ETH,KCASH-BTC,RUFF-BTC,APPC-ETH,MT-BTC,SOC-ETH,GT-HT,PROPY-BTC,AIDOC-BTC,ACT-BTC,LYM-ETH,CHAT-BTC,SWFTC-ETH,ETH-BTC,UIP-USDT,UGAS-BTC,XRP-HUSD,ALGO-USDT,TNT-BTC,ONT-USDT,YEE-ETH,AKRO-BTC,TRX-USDT,OCN-ETH,SRN-BTC,DASH-USDT,XMX-ETH,NANO-BTC,QASH-ETH,EOS-HT,GT-BTC,XTZ-USDT,ARPA-USDT,SALT-ETH,BKBT-ETH,MTX-BTC,SMT-USDT,GXC-BTC,VIDY-BTC,FTT-HT,LAMB-ETH,TRX-BTC,TRIO-ETH,BFT-ETH,LINK-BTC,AE-ETH,NULS-BTC,BHD-HT,AST-ETH,NEO-USDT,EDU-BTC,CVCOIN-BTC,GVE-BTC,GET-BTC,ZRX-USDT,ELF-ETH,DATX-ETH,ADA-ETH,TOP-HT,NCASH-ETH,QTUM-USDT,ETC-HT,ZIL-USDT,TNB-ETH,BIX-ETH,SHE-BTC,PNT-BTC,BTC-USDT,PORTAL-ETH,WAVES-USDT,XZC-ETH,HT-ETH,POLY-BTC,MCO-ETH,MUSK-ETH,PAI-ETH,LXT-USDT,UTK-BTC,RTE-BTC,NCC-ETH,HB10-USDT,BOX-BTC,RDN-ETH,ARPA-BTC,LBA-ETH,CNN-ETH,AAC-ETH,XTZ-BTC,IDT-BTC,AKRO-USDT,IOST-BTC,GT-USDT,WAN-ETH,ETN-ETH,PVT-USDT,NEO-BTC,WAVES-ETH,ONE-USDT,ZEC-BTC,SKM-HT,IOST-ETH,NPXS-ETH,CVC-ETH,CMT-BTC,COVA-ETH,ARDR-ETH,RDN-BTC,DCR-BTC,REN-BTC,YCC-ETH,MX-HT,NEXO-ETH,XLM-ETH,YCC-BTC,ENG-BTC,CNNS-BTC,ZLA-BTC,QSP-BTC,MAN-ETH,UUU-ETH,ETH-HUSD,RTE-ETH,ATP-HT,BTM-BTC,DAC-BTC,TOS-ETH,LAMB-USDT,DASH-HT,NPXS-BTC,NEW-BTC,FTT-BTC,EOS-HUSD,GRS-BTC,POWR-ETH,VET-USDT,AAC-BTC,MX-BTC,MTN-BTC,XVG-ETH,GNX-ETH,SSP-BTC,WAVES-BTC,EGT-BTC,CTXC-ETH,IDT-ETH,STK-BTC,WICC-BTC,UTK-ETH,CRO-HT,LXT-BTC,GSC-ETH,OMG-BTC,XRP-HT,DGB-BTC,IOST-USDT,CVNT-ETH,GAS-BTC,HIT-BTC,CKB-USDT,ARPA-HT,RUFF-USDT,HC-BTC,WTC-ETH,MDS-USDT,ABT-ETH,ALGO-ETH,BIFI-BTC,KNC-BTC,TT-BTC,LET-BTC,NKN-USDT,PAY-BTC,DTA-USDT,AE-BTC,UC-BTC,VSYS-USDT,USDT-HUSD,EOS-BTC,STEEM-BTC,DOGE-BTC,NODE-HT,MDS-ETH,CRE-BTC,GNT-USDT,UIP-ETH,AST-BTC,XEM-BTC,ZEN-ETH,EDU-ETH,MEX-ETH,EKT-BTC,CVC-USDT,WAXP-ETH,REQ-ETH,OST-BTC,STORJ-USDT,SBTC-BTC,DGD-ETH,SC-ETH,WTC-USDT,THETA-BTC,DTA-ETH,BCV-BTC,SNC-BTC,RSR-HT,KAN-BTC,ELA-BTC,ATOM-BTC,BKBT-BTC,FSN-USDT,EM-USDT,WPR-BTC,TOP-BTC,BTS-BTC,EGCC-ETH,MTL-BTC,GNT-ETH,SEELE-BTC,EVX-BTC,FTI-ETH,BAT-USDT,MT-HT,LOL-HT,ICX-ETH,LOOM-BTC,ZJLT-BTC,XLM-USDT,OGO-BTC,DOCK-BTC,CHAT-ETH,DAT-ETH,ETC-USDT,HPT-BTC,BHT-HT" } }