package huobi import ( "bytes" "crypto/ecdsa" "crypto/rand" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "io/ioutil" "net/http" "net/url" "strconv" "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" ) const ( huobiAPIURL = "https://api.huobi.pro" huobiAPIVersion = "1" huobiMarketHistoryKline = "market/history/kline" huobiMarketDetail = "market/detail" huobiMarketDetailMerged = "market/detail/merged" huobiMarketDepth = "market/depth" huobiMarketTrade = "market/trade" huobiMarketTickers = "market/tickers" huobiMarketTradeHistory = "market/history/trade" huobiSymbols = "common/symbols" huobiCurrencies = "common/currencys" huobiTimestamp = "common/timestamp" huobiAccounts = "account/accounts" huobiAccountBalance = "account/accounts/%s/balance" huobiAggregatedBalance = "subuser/aggregate-balance" huobiOrderPlace = "order/orders/place" huobiOrderCancel = "order/orders/%s/submitcancel" huobiOrderCancelBatch = "order/orders/batchcancel" huobiBatchCancelOpenOrders = "order/orders/batchCancelOpenOrders" huobiGetOrder = "order/orders/getClientOrder" huobiGetOrderMatch = "order/orders/%s/matchresults" huobiGetOrders = "order/orders" huobiGetOpenOrders = "order/openOrders" huobiGetOrdersMatch = "orders/matchresults" huobiMarginTransferIn = "dw/transfer-in/margin" huobiMarginTransferOut = "dw/transfer-out/margin" huobiMarginOrders = "margin/orders" huobiMarginRepay = "margin/orders/%s/repay" huobiMarginLoanOrders = "margin/loan-orders" huobiMarginAccountBalance = "margin/accounts/balance" huobiWithdrawCreate = "dw/withdraw/api/create" huobiWithdrawCancel = "dw/withdraw-virtual/%s/cancel" huobiAuthRate = 100 huobiUnauthRate = 100 ) // HUOBI is the overarching type across this package type HUOBI struct { exchange.Base AccountID string WebsocketConn *wshandler.WebsocketConnection AuthenticatedWebsocketConn *wshandler.WebsocketConnection } // GetSpotKline returns kline data // KlinesRequestParams contains symbol, period and size func (h *HUOBI) GetSpotKline(arg KlinesRequestParams) ([]KlineItem, error) { vals := url.Values{} vals.Set("symbol", arg.Symbol) vals.Set("period", string(arg.Period)) if arg.Size != 0 { vals.Set("size", strconv.Itoa(arg.Size)) } type response struct { Response Data []KlineItem `json:"data"` } var result response urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketHistoryKline) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Data, err } // GetTickers returns the ticker for the specified symbol func (h *HUOBI) GetTickers() (Tickers, error) { var result Tickers urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketTickers) return result, h.SendHTTPRequest(urlPath, &result) } // GetMarketDetailMerged returns the ticker for the specified symbol func (h *HUOBI) GetMarketDetailMerged(symbol string) (DetailMerged, error) { vals := url.Values{} vals.Set("symbol", symbol) type response struct { Response Tick DetailMerged `json:"tick"` } var result response urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketDetailMerged) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { return result.Tick, errors.New(result.ErrorMessage) } return result.Tick, err } // GetDepth returns the depth for the specified symbol func (h *HUOBI) GetDepth(obd OrderBookDataRequestParams) (Orderbook, error) { vals := url.Values{} vals.Set("symbol", obd.Symbol) if obd.Type != OrderBookDataRequestParamsTypeNone { vals.Set("type", string(obd.Type)) } type response struct { Response Depth Orderbook `json:"tick"` } var result response urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketDepth) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { return result.Depth, errors.New(result.ErrorMessage) } return result.Depth, err } // GetTrades returns the trades for the specified symbol func (h *HUOBI) GetTrades(symbol string) ([]Trade, error) { vals := url.Values{} vals.Set("symbol", symbol) type response struct { Response Tick struct { Data []Trade `json:"data"` } `json:"tick"` } var result response urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketTrade) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Tick.Data, err } // GetLatestSpotPrice returns latest spot price of symbol // // symbol: string of currency pair func (h *HUOBI) GetLatestSpotPrice(symbol string) (float64, error) { list, err := h.GetTradeHistory(symbol, "1") if err != nil { return 0, err } if len(list) == 0 { return 0, errors.New("the length of the list is 0") } return list[0].Trades[0].Price, nil } // GetTradeHistory returns the trades for the specified symbol func (h *HUOBI) GetTradeHistory(symbol, size string) ([]TradeHistory, error) { vals := url.Values{} vals.Set("symbol", symbol) if size != "" { vals.Set("size", size) } type response struct { Response TradeHistory []TradeHistory `json:"data"` } var result response urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketTradeHistory) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.TradeHistory, err } // GetMarketDetail returns the ticker for the specified symbol func (h *HUOBI) GetMarketDetail(symbol string) (Detail, error) { vals := url.Values{} vals.Set("symbol", symbol) type response struct { Response Tick Detail `json:"tick"` } var result response urlPath := fmt.Sprintf("%s/%s", h.API.Endpoints.URL, huobiMarketDetail) err := h.SendHTTPRequest(common.EncodeURLValues(urlPath, vals), &result) if result.ErrorMessage != "" { return result.Tick, errors.New(result.ErrorMessage) } return result.Tick, err } // GetSymbols returns an array of symbols supported by Huobi func (h *HUOBI) GetSymbols() ([]Symbol, error) { type response struct { Response Symbols []Symbol `json:"data"` } var result response urlPath := fmt.Sprintf("%s/v%s/%s", h.API.Endpoints.URL, huobiAPIVersion, huobiSymbols) err := h.SendHTTPRequest(urlPath, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Symbols, err } // GetCurrencies returns a list of currencies supported by Huobi func (h *HUOBI) GetCurrencies() ([]string, error) { type response struct { Response Currencies []string `json:"data"` } var result response urlPath := fmt.Sprintf("%s/v%s/%s", h.API.Endpoints.URL, huobiAPIVersion, huobiCurrencies) err := h.SendHTTPRequest(urlPath, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Currencies, err } // GetTimestamp returns the Huobi server time func (h *HUOBI) GetTimestamp() (int64, error) { type response struct { Response Timestamp int64 `json:"data"` } var result response urlPath := fmt.Sprintf("%s/v%s/%s", h.API.Endpoints.URL, huobiAPIVersion, huobiTimestamp) err := h.SendHTTPRequest(urlPath, &result) if result.ErrorMessage != "" { return 0, errors.New(result.ErrorMessage) } return result.Timestamp, err } // GetAccounts returns the Huobi user accounts func (h *HUOBI) GetAccounts() ([]Account, error) { type response struct { Response AccountData []Account `json:"data"` } var result response err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiAccounts, url.Values{}, nil, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.AccountData, err } // GetAccountBalance returns the users Huobi account balance func (h *HUOBI) GetAccountBalance(accountID string) ([]AccountBalanceDetail, error) { type response struct { Response AccountBalanceData AccountBalance `json:"data"` } var result response endpoint := fmt.Sprintf(huobiAccountBalance, accountID) v := url.Values{} v.Set("account-id", accountID) err := h.SendAuthenticatedHTTPRequest(http.MethodGet, endpoint, v, nil, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.AccountBalanceData.AccountBalanceDetails, err } // GetAggregatedBalance returns the balances of all the sub-account aggregated. func (h *HUOBI) GetAggregatedBalance() ([]AggregatedBalance, error) { type response struct { Response AggregatedBalances []AggregatedBalance `json:"data"` } var result response err := h.SendAuthenticatedHTTPRequest( http.MethodGet, huobiAggregatedBalance, nil, nil, &result, ) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.AggregatedBalances, err } // SpotNewOrder submits an order to Huobi func (h *HUOBI) SpotNewOrder(arg SpotNewOrderRequestParams) (int64, error) { data := struct { AccountID int `json:"account-id,string"` Amount string `json:"amount"` Price string `json:"price"` Source string `json:"source"` Symbol string `json:"symbol"` Type string `json:"type"` }{ AccountID: arg.AccountID, Amount: strconv.FormatFloat(arg.Amount, 'f', -1, 64), Symbol: arg.Symbol, Type: string(arg.Type), } // Only set price if order type is not equal to buy-market or sell-market if arg.Type != SpotNewOrderRequestTypeBuyMarket && arg.Type != SpotNewOrderRequestTypeSellMarket { data.Price = strconv.FormatFloat(arg.Price, 'f', -1, 64) } if arg.Source != "" { data.Source = arg.Source } type response struct { Response OrderID int64 `json:"data,string"` } var result response err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiOrderPlace, nil, data, &result) if result.ErrorMessage != "" { return 0, errors.New(result.ErrorMessage) } return result.OrderID, err } // CancelExistingOrder cancels an order on Huobi func (h *HUOBI) CancelExistingOrder(orderID int64) (int64, error) { type response struct { Response OrderID int64 `json:"data,string"` } var result response endpoint := fmt.Sprintf(huobiOrderCancel, strconv.FormatInt(orderID, 10)) err := h.SendAuthenticatedHTTPRequest(http.MethodPost, endpoint, url.Values{}, nil, &result) if result.ErrorMessage != "" { return 0, errors.New(result.ErrorMessage) } return result.OrderID, err } // CancelOrderBatch cancels a batch of orders -- to-do func (h *HUOBI) CancelOrderBatch(_ []int64) ([]CancelOrderBatch, error) { type response struct { Response Data []CancelOrderBatch `json:"data"` } var result response err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiOrderCancelBatch, url.Values{}, nil, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Data, err } // CancelOpenOrdersBatch cancels a batch of orders -- to-do func (h *HUOBI) CancelOpenOrdersBatch(accountID, symbol string) (CancelOpenOrdersBatch, error) { params := url.Values{} params.Set("account-id", accountID) var result CancelOpenOrdersBatch data := struct { AccountID string `json:"account-id"` Symbol string `json:"symbol"` }{ AccountID: accountID, Symbol: symbol, } err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiBatchCancelOpenOrders, url.Values{}, data, &result) if result.Data.FailedCount > 0 { return result, fmt.Errorf("there were %v failed order cancellations", result.Data.FailedCount) } return result, err } // GetOrder returns order information for the specified order func (h *HUOBI) GetOrder(orderID int64) (OrderInfo, error) { type response struct { Response Order OrderInfo `json:"data"` } var result response urlVal := url.Values{} urlVal.Set("clientOrderId", strconv.FormatInt(orderID, 10)) err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOrder, urlVal, nil, &result) if result.ErrorMessage != "" { return result.Order, errors.New(result.ErrorMessage) } return result.Order, err } // GetOrderMatchResults returns matched order info for the specified order func (h *HUOBI) GetOrderMatchResults(orderID int64) ([]OrderMatchInfo, error) { type response struct { Response Orders []OrderMatchInfo `json:"data"` } var result response endpoint := fmt.Sprintf(huobiGetOrderMatch, strconv.FormatInt(orderID, 10)) err := h.SendAuthenticatedHTTPRequest(http.MethodGet, endpoint, url.Values{}, nil, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Orders, err } // GetOrders returns a list of orders func (h *HUOBI) GetOrders(symbol, types, start, end, states, from, direct, size string) ([]OrderInfo, error) { type response struct { Response Orders []OrderInfo `json:"data"` } vals := url.Values{} vals.Set("symbol", symbol) vals.Set("states", states) if types != "" { vals.Set("types", types) } if start != "" { vals.Set("start-date", start) } if end != "" { vals.Set("end-date", end) } if from != "" { vals.Set("from", from) } if direct != "" { vals.Set("direct", direct) } if size != "" { vals.Set("size", size) } var result response err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOrders, vals, nil, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Orders, err } // GetOpenOrders returns a list of orders func (h *HUOBI) GetOpenOrders(accountID, symbol, side string, size int64) ([]OrderInfo, error) { type response struct { Response Orders []OrderInfo `json:"data"` } vals := url.Values{} vals.Set("symbol", symbol) vals.Set("accountID", accountID) if len(side) > 0 { vals.Set("side", side) } vals.Set("size", strconv.FormatInt(size, 10)) var result response err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOpenOrders, vals, nil, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Orders, err } // GetOrdersMatch returns a list of matched orders func (h *HUOBI) GetOrdersMatch(symbol, types, start, end, from, direct, size string) ([]OrderMatchInfo, error) { type response struct { Response Orders []OrderMatchInfo `json:"data"` } vals := url.Values{} vals.Set("symbol", symbol) if types != "" { vals.Set("types", types) } if start != "" { vals.Set("start-date", start) } if end != "" { vals.Set("end-date", end) } if from != "" { vals.Set("from", from) } if direct != "" { vals.Set("direct", direct) } if size != "" { vals.Set("size", size) } var result response err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOrdersMatch, vals, nil, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Orders, err } // MarginTransfer transfers assets into or out of the margin account func (h *HUOBI) MarginTransfer(symbol, currency string, amount float64, in bool) (int64, error) { data := struct { Symbol string `json:"symbol"` Currency string `json:"currency"` Amount string `json:"amount"` }{ Symbol: symbol, Currency: currency, Amount: strconv.FormatFloat(amount, 'f', -1, 64), } path := huobiMarginTransferIn if !in { path = huobiMarginTransferOut } type response struct { Response TransferID int64 `json:"data"` } var result response err := h.SendAuthenticatedHTTPRequest(http.MethodPost, path, nil, data, &result) if result.ErrorMessage != "" { return 0, errors.New(result.ErrorMessage) } return result.TransferID, err } // MarginOrder submits a margin order application func (h *HUOBI) MarginOrder(symbol, currency string, amount float64) (int64, error) { data := struct { Symbol string `json:"symbol"` Currency string `json:"currency"` Amount string `json:"amount"` }{ Symbol: symbol, Currency: currency, Amount: strconv.FormatFloat(amount, 'f', -1, 64), } type response struct { Response MarginOrderID int64 `json:"data"` } var result response err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiMarginOrders, nil, data, &result) if result.ErrorMessage != "" { return 0, errors.New(result.ErrorMessage) } return result.MarginOrderID, err } // MarginRepayment repays a margin amount for a margin ID func (h *HUOBI) MarginRepayment(orderID int64, amount float64) (int64, error) { data := struct { Amount string `json:"amount"` }{ Amount: strconv.FormatFloat(amount, 'f', -1, 64), } type response struct { Response MarginOrderID int64 `json:"data"` } var result response endpoint := fmt.Sprintf(huobiMarginRepay, strconv.FormatInt(orderID, 10)) err := h.SendAuthenticatedHTTPRequest(http.MethodPost, endpoint, nil, data, &result) if result.ErrorMessage != "" { return 0, errors.New(result.ErrorMessage) } return result.MarginOrderID, err } // GetMarginLoanOrders returns the margin loan orders func (h *HUOBI) GetMarginLoanOrders(symbol, currency, start, end, states, from, direct, size string) ([]MarginOrder, error) { vals := url.Values{} vals.Set("symbol", symbol) vals.Set("currency", currency) if start != "" { vals.Set("start-date", start) } if end != "" { vals.Set("end-date", end) } if states != "" { vals.Set("states", states) } if from != "" { vals.Set("from", from) } if direct != "" { vals.Set("direct", direct) } if size != "" { vals.Set("size", size) } type response struct { Response MarginLoanOrders []MarginOrder `json:"data"` } var result response err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiMarginLoanOrders, vals, nil, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.MarginLoanOrders, err } // GetMarginAccountBalance returns the margin account balances func (h *HUOBI) GetMarginAccountBalance(symbol string) ([]MarginAccountBalance, error) { type response struct { Response Balances []MarginAccountBalance `json:"data"` } vals := url.Values{} if symbol != "" { vals.Set("symbol", symbol) } var result response err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiMarginAccountBalance, vals, nil, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Balances, err } // Withdraw withdraws the desired amount and currency func (h *HUOBI) Withdraw(c currency.Code, address, addrTag string, amount, fee float64) (int64, error) { type response struct { Response WithdrawID int64 `json:"data"` } data := struct { Address string `json:"address"` Amount string `json:"amount"` Currency string `json:"currency"` Fee string `json:"fee,omitempty"` AddrTag string `json:"addr-tag,omitempty"` }{ Address: address, Currency: c.Lower().String(), Amount: strconv.FormatFloat(amount, 'f', -1, 64), } if fee > 0 { data.Fee = strconv.FormatFloat(fee, 'f', -1, 64) } if c == currency.XRP { data.AddrTag = addrTag } var result response err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiWithdrawCreate, nil, data, &result) if result.ErrorMessage != "" { return 0, errors.New(result.ErrorMessage) } return result.WithdrawID, err } // CancelWithdraw cancels a withdraw request func (h *HUOBI) CancelWithdraw(withdrawID int64) (int64, error) { type response struct { Response WithdrawID int64 `json:"data"` } vals := url.Values{} vals.Set("withdraw-id", strconv.FormatInt(withdrawID, 10)) var result response endpoint := fmt.Sprintf(huobiWithdrawCancel, strconv.FormatInt(withdrawID, 10)) err := h.SendAuthenticatedHTTPRequest(http.MethodPost, endpoint, vals, nil, &result) if result.ErrorMessage != "" { return 0, errors.New(result.ErrorMessage) } return result.WithdrawID, err } // SendHTTPRequest sends an unauthenticated HTTP request func (h *HUOBI) SendHTTPRequest(path string, result interface{}) error { return h.SendPayload(http.MethodGet, path, nil, nil, result, false, false, h.Verbose, h.HTTPDebugging, h.HTTPRecording) } // SendAuthenticatedHTTPRequest sends authenticated requests to the HUOBI API func (h *HUOBI) SendAuthenticatedHTTPRequest(method, endpoint string, values url.Values, data, result interface{}) error { if !h.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, h.Name) } if values == nil { values = url.Values{} } values.Set("AccessKeyId", h.API.Credentials.Key) values.Set("SignatureMethod", "HmacSHA256") values.Set("SignatureVersion", "2") values.Set("Timestamp", time.Now().UTC().Format("2006-01-02T15:04:05")) endpoint = fmt.Sprintf("/v%s/%s", huobiAPIVersion, endpoint) payload := fmt.Sprintf("%s\napi.huobi.pro\n%s\n%s", method, endpoint, values.Encode()) headers := make(map[string]string) if method == http.MethodGet { headers["Content-Type"] = "application/x-www-form-urlencoded" } else { headers["Content-Type"] = "application/json" } hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(payload), []byte(h.API.Credentials.Secret)) signature := crypto.Base64Encode(hmac) values.Set("Signature", signature) if h.API.Credentials.PEMKey != "" && h.API.PEMKeySupport { pemKey := strings.NewReader(h.API.Credentials.PEMKey) pemBytes, err := ioutil.ReadAll(pemKey) if err != nil { return fmt.Errorf("%s unable to ioutil.ReadAll PEM key: %s", h.Name, err) } block, _ := pem.Decode(pemBytes) if block == nil { return fmt.Errorf("%s PEM block is nil", h.Name) } x509Encoded := block.Bytes privKey, err := x509.ParseECPrivateKey(x509Encoded) if err != nil { return fmt.Errorf("%s unable to ParseECPrivKey: %s", h.Name, err) } r, s, err := ecdsa.Sign(rand.Reader, privKey, crypto.GetSHA256([]byte(signature))) if err != nil { return fmt.Errorf("%s unable to sign: %s", h.Name, err) } privSig := r.Bytes() privSig = append(privSig, s.Bytes()...) values.Set("PrivateSignature", crypto.Base64Encode(privSig)) } urlPath := h.API.Endpoints.URL + common.EncodeURLValues(endpoint, values) var body []byte if data != nil { encoded, err := json.Marshal(data) if err != nil { return fmt.Errorf("%s unable to marshal data: %s", h.Name, err) } body = encoded } return h.SendPayload(method, urlPath, headers, bytes.NewReader(body), result, true, false, h.Verbose, h.HTTPDebugging, h.HTTPRecording) } // GetFee returns an estimate of fee based on type of transaction func (h *HUOBI) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { var fee float64 if feeBuilder.FeeType == exchange.OfflineTradeFee || feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { fee = calculateTradingFee(feeBuilder.Pair, feeBuilder.PurchasePrice, feeBuilder.Amount) } if fee < 0 { fee = 0 } return fee, nil } func calculateTradingFee(c currency.Pair, price, amount float64) float64 { if c.IsCryptoFiatPair() { return 0.001 * price * amount } return 0.002 * price * amount }