Files
gocryptotrader/exchanges/huobi/huobi.go
soxipy fb4e2d1452 localbitcoins fixes (#177)
* General LocalBitcoin fixes

* Added override variables to config for exchange packages to allow different API URL's
2018-08-27 14:19:29 +10:00

791 lines
20 KiB
Go

package huobi
import (
"bytes"
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"log"
"net/url"
"strconv"
"strings"
"time"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/request"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
)
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"
huobiMarketTradeHistory = "market/history/trade"
huobiSymbols = "common/symbols"
huobiCurrencies = "common/currencys"
huobiTimestamp = "common/timestamp"
huobiAccounts = "account/accounts"
huobiAccountBalance = "account/accounts/%s/balance"
huobiOrderPlace = "order/orders/place"
huobiOrderCancel = "order/orders/%s/submitcancel"
huobiOrderCancelBatch = "order/orders/batchcancel"
huobiGetOrder = "order/orders/%s"
huobiGetOrderMatch = "order/orders/%s/matchresults"
huobiGetOrders = "order/orders"
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
}
// SetDefaults sets default values for the exchange
func (h *HUOBI) SetDefaults() {
h.Name = "Huobi"
h.Enabled = false
h.Fee = 0
h.Verbose = false
h.Websocket = false
h.RESTPollingDelay = 10
h.RequestCurrencyPairFormat.Delimiter = ""
h.RequestCurrencyPairFormat.Uppercase = false
h.ConfigCurrencyPairFormat.Delimiter = "-"
h.ConfigCurrencyPairFormat.Uppercase = true
h.AssetTypes = []string{ticker.Spot}
h.SupportsAutoPairUpdating = true
h.SupportsRESTTickerBatching = false
h.Requester = request.New(h.Name,
request.NewRateLimit(time.Second*10, huobiAuthRate),
request.NewRateLimit(time.Second*10, huobiUnauthRate),
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
h.APIUrlDefault = huobiAPIURL
h.APIUrl = h.APIUrlDefault
}
// Setup sets user configuration
func (h *HUOBI) Setup(exch config.ExchangeConfig) {
if !exch.Enabled {
h.SetEnabled(false)
} else {
h.Enabled = true
h.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
h.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
h.APIAuthPEMKey = exch.APIAuthPEMKey
h.SetHTTPClientTimeout(exch.HTTPTimeout)
h.SetHTTPClientUserAgent(exch.HTTPUserAgent)
h.RESTPollingDelay = exch.RESTPollingDelay
h.Verbose = exch.Verbose
h.Websocket = exch.Websocket
h.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
h.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
h.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
err := h.SetCurrencyPairFormat()
if err != nil {
log.Fatal(err)
}
err = h.SetAssetTypes()
if err != nil {
log.Fatal(err)
}
err = h.SetAutoPairDefaults()
if err != nil {
log.Fatal(err)
}
err = h.SetAPIURL(exch)
if err != nil {
log.Fatal(err)
}
}
}
// GetFee returns Huobi fee
func (h *HUOBI) GetFee() float64 {
return h.Fee
}
// 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
url := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketHistoryKline)
err := h.SendHTTPRequest(common.EncodeURLValues(url, vals), &result)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
return result.Data, err
}
// 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
url := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketDetailMerged)
err := h.SendHTTPRequest(common.EncodeURLValues(url, 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
url := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketDepth)
err := h.SendHTTPRequest(common.EncodeURLValues(url, 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
url := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketTrade)
err := h.SendHTTPRequest(common.EncodeURLValues(url, 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
url := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketTradeHistory)
err := h.SendHTTPRequest(common.EncodeURLValues(url, 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
url := fmt.Sprintf("%s/%s", h.APIUrl, huobiMarketDetail)
err := h.SendHTTPRequest(common.EncodeURLValues(url, 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
url := fmt.Sprintf("%s/v%s/%s", h.APIUrl, huobiAPIVersion, huobiSymbols)
err := h.SendHTTPRequest(url, &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
url := fmt.Sprintf("%s/v%s/%s", h.APIUrl, huobiAPIVersion, huobiCurrencies)
err := h.SendHTTPRequest(url, &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
url := fmt.Sprintf("%s/v%s/%s", h.APIUrl, huobiAPIVersion, huobiTimestamp)
err := h.SendHTTPRequest(url, &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("GET", huobiAccounts, url.Values{}, &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)
err := h.SendAuthenticatedHTTPRequest("GET", endpoint, url.Values{}, &result)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
return result.AccountBalanceData.AccountBalanceDetails, err
}
// SpotNewOrder submits an order to Huobi
func (h *HUOBI) SpotNewOrder(arg SpotNewOrderRequestParams) (int64, error) {
vals := url.Values{}
vals.Set("account-id", fmt.Sprintf("%d", arg.AccountID))
vals.Set("amount", strconv.FormatFloat(arg.Amount, 'f', -1, 64))
// Only set price if order type is not equal to buy-market or sell-market
if arg.Type != SpotNewOrderRequestTypeBuyMarket && arg.Type != SpotNewOrderRequestTypeSellMarket {
vals.Set("price", strconv.FormatFloat(arg.Price, 'f', -1, 64))
}
if arg.Source != "" {
vals.Set("source", arg.Source)
}
vals.Set("symbol", arg.Symbol)
vals.Set("type", string(arg.Type))
type response struct {
Response
OrderID int64 `json:"data,string"`
}
var result response
err := h.SendAuthenticatedHTTPRequest("POST", huobiOrderPlace, vals, &result)
if result.ErrorMessage != "" {
return 0, errors.New(result.ErrorMessage)
}
return result.OrderID, err
}
// CancelOrder cancels an order on Huobi
func (h *HUOBI) CancelOrder(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("POST", endpoint, url.Values{}, &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(orderIDs []int64) ([]CancelOrderBatch, error) {
type response struct {
Response
Data []CancelOrderBatch `json:"data"`
}
var result response
err := h.SendAuthenticatedHTTPRequest("POST", huobiOrderCancelBatch, url.Values{}, &result)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
return result.Data, 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
endpoint := fmt.Sprintf(huobiGetOrder, strconv.FormatInt(orderID, 10))
err := h.SendAuthenticatedHTTPRequest("GET", endpoint, url.Values{}, &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("GET", endpoint, url.Values{}, &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("GET", huobiGetOrders, vals, &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("GET", huobiGetOrdersMatch, vals, &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) {
vals := url.Values{}
vals.Set("symbol", symbol)
vals.Set("currency", currency)
vals.Set("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("POST", path, vals, &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) {
vals := url.Values{}
vals.Set("symbol", symbol)
vals.Set("currency", currency)
vals.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
type response struct {
Response
MarginOrderID int64 `json:"data"`
}
var result response
err := h.SendAuthenticatedHTTPRequest("POST", huobiMarginOrders, vals, &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) {
vals := url.Values{}
vals.Set("order-id", strconv.FormatInt(orderID, 10))
vals.Set("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("POST", endpoint, vals, &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("GET", huobiMarginLoanOrders, vals, &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("GET", huobiMarginAccountBalance, vals, &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(address, currency, addrTag string, amount, fee float64) (int64, error) {
type response struct {
Response
WithdrawID int64 `json:"data"`
}
vals := url.Values{}
vals.Set("address", address)
vals.Set("currency", currency)
vals.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
if fee != 0 {
vals.Set("fee", strconv.FormatFloat(fee, 'f', -1, 64))
}
if currency == "XRP" {
vals.Set("addr-tag", addrTag)
}
var result response
err := h.SendAuthenticatedHTTPRequest("POST", huobiWithdrawCreate, vals, &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("POST", endpoint, vals, &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("GET", path, nil, nil, result, false, h.Verbose)
}
// SendAuthenticatedHTTPRequest sends authenticated requests to the HUOBI API
func (h *HUOBI) SendAuthenticatedHTTPRequest(method, endpoint string, values url.Values, result interface{}) error {
if !h.AuthenticatedAPISupport {
return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, h.Name)
}
values.Set("AccessKeyId", h.APIKey)
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)
headers["Content-Type"] = "application/x-www-form-urlencoded"
hmac := common.GetHMAC(common.HashSHA256, []byte(payload), []byte(h.APISecret))
signature := common.Base64Encode(hmac)
values.Set("Signature", signature)
pemKey := strings.NewReader(h.APIAuthPEMKey)
pemBytes, err := ioutil.ReadAll(pemKey)
if err != nil {
return fmt.Errorf("Huobi unable to ioutil.ReadAll PEM key: %s", err)
}
block, _ := pem.Decode(pemBytes)
if block == nil {
return fmt.Errorf("Huobi block is nil")
}
x509Encoded := block.Bytes
privKey, err := x509.ParseECPrivateKey(x509Encoded)
if err != nil {
return fmt.Errorf("Huobi unable to ParseECPrivKey: %s", err)
}
r, s, err := ecdsa.Sign(rand.Reader, privKey, common.GetSHA256([]byte(signature)))
if err != nil {
return fmt.Errorf("Huobi unable to sign: %s", err)
}
privSig := r.Bytes()
privSig = append(privSig, s.Bytes()...)
values.Set("PrivateSignature", common.Base64Encode(privSig))
url := fmt.Sprintf("%s%s", h.APIUrl, endpoint)
url = common.EncodeURLValues(url, values)
return h.SendPayload(method, url, headers, bytes.NewBufferString(""), result, true, h.Verbose)
}