From 71333b29c7126c7e6eebf2e139380eb51e24c0c5 Mon Sep 17 00:00:00 2001 From: Bret Palsson Date: Sat, 6 Jan 2018 00:45:56 -0800 Subject: [PATCH] Add Exchange: HitBTC --- CONTRIBUTORS | 3 +- README.md | 1 + config/config_test.go | 4 +- config_example.json | 21 ++ exchange.go | 3 + exchanges/hitbtc/hitbtc.go | 507 +++++++++++++++++++++++++++ exchanges/hitbtc/hitbtc_test.go | 69 ++++ exchanges/hitbtc/hitbtc_types.go | 233 ++++++++++++ exchanges/hitbtc/hitbtc_websocket.go | 182 ++++++++++ exchanges/hitbtc/hitbtc_wrapper.go | 108 ++++++ testdata/configtest.json | 21 ++ 11 files changed, 1149 insertions(+), 3 deletions(-) create mode 100644 exchanges/hitbtc/hitbtc.go create mode 100644 exchanges/hitbtc/hitbtc_test.go create mode 100644 exchanges/hitbtc/hitbtc_types.go create mode 100644 exchanges/hitbtc/hitbtc_websocket.go create mode 100644 exchanges/hitbtc/hitbtc_wrapper.go diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 4752f566..080abaf2 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -10,4 +10,5 @@ Manuel Kreutz - 140am libsora.so - if1live Tong - tongxiaofeng Jamie Cheng - starit -Jake - snipesjr \ No newline at end of file +Jake - snipesjr +Bret Palsson - bretep \ No newline at end of file diff --git a/README.md b/README.md index 8f41af94..c54baa4b 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | COINUT | Yes | No | NA | | GDAX(Coinbase) | Yes | Yes | No| | Gemini | Yes | NA | NA | +| HitBTC | Yes | Yes | NA | | Huobi.Pro | Yes | No |No | | ItBit | Yes | NA | NA | | Kraken | Yes | NA | NA | diff --git a/config/config_test.go b/config/config_test.go index 07691305..e380ff59 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -89,7 +89,7 @@ func TestGetEnabledExchanges(t *testing.T) { } exchanges := cfg.GetEnabledExchanges() - if len(exchanges) != 23 { + if len(exchanges) != 24 { t.Error( "Test failed. TestGetEnabledExchanges. Enabled exchanges value mismatch", ) @@ -141,7 +141,7 @@ func TestGetDisabledExchanges(t *testing.T) { } func TestCountEnabledExchanges(t *testing.T) { - defaultEnabledExchanges := 23 + defaultEnabledExchanges := 24 GetConfigEnabledExchanges := GetConfig() err := GetConfigEnabledExchanges.LoadConfig(ConfigTestFile) if err != nil { diff --git a/config_example.json b/config_example.json index 87e663db..8ac0a5cb 100644 --- a/config_example.json +++ b/config_example.json @@ -298,6 +298,27 @@ "Uppercase": true } }, + { + "Name": "HitBTC", + "Enabled": true, + "Verbose": false, + "Websocket": false, + "UseSandbox": false, + "RESTPollingDelay": 10, + "AuthenticatedAPISupport": false, + "APIKey": "Key", + "APISecret": "Secret", + "AvailablePairs": "BTCUSD,ETHBTC,ETHUSD", + "EnabledPairs": "BTCUSD", + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } + }, { "Name": "Huobi", "Enabled": true, diff --git a/exchange.go b/exchange.go index 0944133a..08a6124c 100644 --- a/exchange.go +++ b/exchange.go @@ -17,6 +17,7 @@ import ( "github.com/thrasher-/gocryptotrader/exchanges/coinut" "github.com/thrasher-/gocryptotrader/exchanges/gdax" "github.com/thrasher-/gocryptotrader/exchanges/gemini" + "github.com/thrasher-/gocryptotrader/exchanges/hitbtc" "github.com/thrasher-/gocryptotrader/exchanges/huobi" "github.com/thrasher-/gocryptotrader/exchanges/itbit" "github.com/thrasher-/gocryptotrader/exchanges/kraken" @@ -150,6 +151,8 @@ func LoadExchange(name string) error { exch = new(gdax.GDAX) case "gemini": exch = new(gemini.Gemini) + case "hitbtc": + exch = new(hitbtc.HitBTC) case "huobi": exch = new(huobi.HUOBI) case "itbit": diff --git a/exchanges/hitbtc/hitbtc.go b/exchanges/hitbtc/hitbtc.go new file mode 100644 index 00000000..0e84c8e5 --- /dev/null +++ b/exchanges/hitbtc/hitbtc.go @@ -0,0 +1,507 @@ +package hitbtc + +import ( + "bytes" + "errors" + "fmt" + "log" + "net/url" + "strconv" + + "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/config" + "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" +) + +const ( + // API + APIURL = "https://api.hitbtc.com" + + // Public + APIv2Trades = "api/2/public/trades" + APIv2Currency = "api/2/public/currency" + APIv2Symbol = "api/2/public/symbol" + APIv2Ticker = "api/2/public/ticker" + APIv2OrderBook = "api/2/public/orderbook" + APIv2Candles = "api/2/public/candles" + + // Authenticated + APIv2Balance = "api/2/trading/balance" + APIv2CryptoAddress = "api/2/account/crypto/address" + APIv2CryptoWithdraw = "api/2/account/crypto/withdraw" + APIv2TradeHistory = "api/2/history/trades" + APIv2FeeInfo = "api/2/trading/fee" + Orders = "order" + OrderBuy = "buy" + OrderSell = "sell" + OrderCancel = "cancelOrder" + OrderMove = "moveOrder" + TradableBalances = "returnTradableBalances" + TransferBalance = "transferBalance" +) + +// HitBTC is the overarching type across the hitbtc package +type HitBTC struct { + exchange.Base +} + +// SetDefaults sets default settings for hitbtc +func (p *HitBTC) SetDefaults() { + p.Name = "HitBTC" + p.Enabled = false + p.Fee = 0 + p.Verbose = false + p.Websocket = false + p.RESTPollingDelay = 10 + p.RequestCurrencyPairFormat.Delimiter = "" + p.RequestCurrencyPairFormat.Uppercase = true + p.ConfigCurrencyPairFormat.Delimiter = "" + p.ConfigCurrencyPairFormat.Uppercase = true + p.AssetTypes = []string{ticker.Spot} +} + +// Setup sets user exchange configuration settings +func (p *HitBTC) Setup(exch config.ExchangeConfig) { + if !exch.Enabled { + p.SetEnabled(false) + } else { + p.Enabled = true + p.AuthenticatedAPISupport = exch.AuthenticatedAPISupport + p.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) + p.RESTPollingDelay = exch.RESTPollingDelay // Max 60000ms + p.Verbose = exch.Verbose + p.Websocket = exch.Websocket + p.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") + availiableSymbols, err := p.GetSymbols("") + + if err != nil { + log.Println(err) + p.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") + } else { + p.AvailablePairs = availiableSymbols + } + p.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err = p.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = p.SetAssetTypes() + if err != nil { + log.Fatal(err) + } + } +} + +// GetFee returns the fee for hitbtc +func (p *HitBTC) GetFee() float64 { + return p.Fee +} + +// Public Market Data +// https://api.hitbtc.com/?python#market-data + +// GetCurrencies +// Return the actual list of available currencies, tokens, ICO etc. +func (p *HitBTC) GetCurrencies(currency string) (map[string]Currencies, error) { + type Response struct { + Data []Currencies + } + resp := Response{} + path := fmt.Sprintf("%s/%s/%s", APIURL, APIv2Currency, currency) + err := common.SendHTTPGetRequest(path, true, p.Verbose, &resp.Data) + ret := make(map[string]Currencies) + for _, id := range resp.Data { + ret[id.Id] = id + } + + return ret, err +} + +// GetSymbols +// Return the actual list of currency symbols (currency pairs) traded on HitBTC exchange. +// The first listed currency of a symbol is called the base currency, and the second currency +// is called the quote currency. The currency pair indicates how much of the quote currency +// is needed to purchase one unit of the base currency. +func (p *HitBTC) GetSymbols(symbol string) ([]string, error) { + + resp := []Symbol{} + path := fmt.Sprintf("%s/%s/%s", APIURL, APIv2Symbol, symbol) + err := common.SendHTTPGetRequest(path, true, p.Verbose, &resp) + ret := make([]string, 0, len(resp)) + for _, x := range resp { + ret = append(ret, x.Id) + } + + return ret, err +} + +// GetTicker +// Return ticker information +func (p *HitBTC) GetTicker(symbol string) map[string]Ticker { + + resp1 := []Ticker{} + resp2 := Ticker{} + ret := make(map[string]Ticker) + path := fmt.Sprintf("%s/%s/%s", APIURL, APIv2Ticker, symbol) + + if symbol == "" { + common.SendHTTPGetRequest(path, true, p.Verbose, &resp1) + + for _, item := range resp1 { + if item.Symbol != "" { + ret[item.Symbol] = item + } + } + } else { + common.SendHTTPGetRequest(path, true, p.Verbose, &resp2) + ret[resp2.Symbol] = resp2 + } + + return ret +} + +// GetTrades returns trades from hitbtc +func (p *HitBTC) GetTrades(currencyPair, from, till, limit, offset, by, sort string) ([]TradeHistory, error) { + // start Number or Datetime + // end Number or Datetime + // limit Number + // offset Number + // by Filtration definition. Accepted values: id, timestamp. Default timestamp + // sort Default DESC + vals := url.Values{} + + if from != "" { + vals.Set("from", from) + } + + if till != "" { + vals.Set("till", till) + } + + if limit != "" { + vals.Set("limit", limit) + } + + if offset != "" { + vals.Set("offset", offset) + } + + if by != "" { + vals.Set("by", by) + } + + if sort != "" { + vals.Set("sort", sort) + } + + resp := []TradeHistory{} + path := fmt.Sprintf("%s/%s/%s?%s", APIURL, APIv2Trades, currencyPair, vals.Encode()) + + return resp, common.SendHTTPGetRequest(path, true, p.Verbose, &resp) +} + +// GetOrderbook +// An order book is an electronic list of buy and sell orders for a specific +// symbol, organized by price level. +func (p *HitBTC) GetOrderbook(currencyPair string, limit int) (Orderbook, error) { + // limit Limit of orderbook levels, default 100. Set 0 to view full orderbook levels + vals := url.Values{} + + if limit != 0 { + vals.Set("limit", strconv.Itoa(limit)) + } + + resp := OrderbookResponse{} + path := fmt.Sprintf("%s/%s/%s?%s", APIURL, APIv2OrderBook, currencyPair, vals.Encode()) + + err := common.SendHTTPGetRequest(path, true, p.Verbose, &resp) + if err != nil { + return Orderbook{}, err + } + + ob := Orderbook{} + for _, x := range resp.Asks { + ob.Asks = append(ob.Asks, x) + } + + for _, x := range resp.Bids { + ob.Bids = append(ob.Bids, x) + } + return ob, nil +} + +// GetCandles +// A candles used for OHLC a specific symbol. +// Note: Result contain candles only with non zero volume. +func (p *HitBTC) GetCandles(currencyPair, limit, period string) ([]ChartData, error) { + // limit Limit of candles, default 100. + // period One of: M1 (one minute), M3, M5, M15, M30, H1, H4, D1, D7, 1M (one month). Default is M30 (30 minutes). + vals := url.Values{} + + if limit != "" { + vals.Set("limit", limit) + } + + if period != "" { + vals.Set("period", period) + } + + resp := []ChartData{} + path := fmt.Sprintf("%s/%s/%s?%s", APIURL, APIv2Candles, currencyPair, vals.Encode()) + + err := common.SendHTTPGetRequest(path, true, p.Verbose, &resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +// Authenticated Market Data +// https://api.hitbtc.com/?python#market-data + +// GetBalances +func (p *HitBTC) GetBalances() (map[string]Balance, error) { + + result := []Balance{} + err := p.SendAuthenticatedHTTPRequest("GET", APIv2Balance, url.Values{}, &result) + ret := make(map[string]Balance) + + if err != nil { + return ret, err + } + + for _, item := range result { + ret[item.Currency] = item + } + + return ret, nil +} + +func (p *HitBTC) GetDepositAddresses(currency string) (DepositCryptoAddresses, error) { + resp := DepositCryptoAddresses{} + err := p.SendAuthenticatedHTTPRequest("GET", APIv2CryptoAddress+"/"+currency, url.Values{}, &resp) + + return resp, err +} + +func (p *HitBTC) GenerateNewAddress(currency string) (DepositCryptoAddresses, error) { + + resp := DepositCryptoAddresses{} + err := p.SendAuthenticatedHTTPRequest("POST", APIv2CryptoAddress+"/"+currency, url.Values{}, &resp) + + return resp, err +} + +// Get Active orders +func (p *HitBTC) GetActiveOrders(currency string) ([]Order, error) { + + resp := []Order{} + err := p.SendAuthenticatedHTTPRequest("GET", Orders+"?symbol="+currency, url.Values{}, &resp) + + return resp, err +} + +func (p *HitBTC) GetAuthenticatedTradeHistory(currency, start, end string) (interface{}, error) { + values := url.Values{} + + if start != "" { + values.Set("start", start) + } + + if end != "" { + values.Set("end", end) + } + + if currency != "" && currency != "all" { + values.Set("currencyPair", currency) + result := AuthenticatedTradeHistoryResponse{} + err := p.SendAuthenticatedHTTPRequest("POST", APIv2TradeHistory, values, &result.Data) + + if err != nil { + return result, err + } + + return result, nil + } else { + values.Set("currencyPair", "all") + result := AuthenticatedTradeHistoryAll{} + err := p.SendAuthenticatedHTTPRequest("POST", APIv2TradeHistory, values, &result.Data) + + if err != nil { + return result, err + } + + return result, nil + } +} + +func (p *HitBTC) PlaceOrder(currency string, rate, amount float64, immediate, fillOrKill, buy bool) (OrderResponse, error) { + result := OrderResponse{} + values := url.Values{} + + var orderType string + if buy { + orderType = OrderBuy + } else { + orderType = OrderSell + } + + values.Set("currencyPair", currency) + values.Set("rate", strconv.FormatFloat(rate, 'f', -1, 64)) + values.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) + + if immediate { + values.Set("immediateOrCancel", "1") + } + + if fillOrKill { + values.Set("fillOrKill", "1") + } + + err := p.SendAuthenticatedHTTPRequest("POST", orderType, values, &result) + + if err != nil { + return result, err + } + + return result, nil +} + +func (p *HitBTC) CancelOrder(orderID int64) (bool, error) { + result := GenericResponse{} + values := url.Values{} + values.Set("orderNumber", strconv.FormatInt(orderID, 10)) + + err := p.SendAuthenticatedHTTPRequest("POST", OrderCancel, values, &result) + + if err != nil { + return false, err + } + + if result.Success != 1 { + return false, errors.New(result.Error) + } + + return true, nil +} + +func (p *HitBTC) MoveOrder(orderID int64, rate, amount float64) (MoveOrderResponse, error) { + result := MoveOrderResponse{} + values := url.Values{} + values.Set("orderNumber", strconv.FormatInt(orderID, 10)) + values.Set("rate", strconv.FormatFloat(rate, 'f', -1, 64)) + + if amount != 0 { + values.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) + } + + err := p.SendAuthenticatedHTTPRequest("POST", OrderMove, values, &result) + + if err != nil { + return result, err + } + + if result.Success != 1 { + return result, errors.New(result.Error) + } + + return result, nil +} + +func (p *HitBTC) Withdraw(currency, address string, amount float64) (bool, error) { + result := Withdraw{} + values := url.Values{} + + values.Set("currency", currency) + values.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) + values.Set("address", address) + + err := p.SendAuthenticatedHTTPRequest("POST", APIv2CryptoWithdraw, values, &result) + + if err != nil { + return false, err + } + + if result.Error != "" { + return false, errors.New(result.Error) + } + + return true, nil +} + +func (p *HitBTC) GetFeeInfo(currencyPair string) (Fee, error) { + result := Fee{} + err := p.SendAuthenticatedHTTPRequest("GET", APIv2FeeInfo+"/"+currencyPair, url.Values{}, &result) + + return result, err +} + +func (p *HitBTC) GetTradableBalances() (map[string]map[string]float64, error) { + type Response struct { + Data map[string]map[string]interface{} + } + result := Response{} + + err := p.SendAuthenticatedHTTPRequest("POST", TradableBalances, url.Values{}, &result.Data) + + if err != nil { + return nil, err + } + + balances := make(map[string]map[string]float64) + + for x, y := range result.Data { + balances[x] = make(map[string]float64) + for z, w := range y { + balances[x][z], _ = strconv.ParseFloat(w.(string), 64) + } + } + + return balances, nil +} + +func (p *HitBTC) TransferBalance(currency, from, to string, amount float64) (bool, error) { + values := url.Values{} + result := GenericResponse{} + + values.Set("currency", currency) + values.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) + values.Set("fromAccount", from) + values.Set("toAccount", to) + + err := p.SendAuthenticatedHTTPRequest("POST", TransferBalance, values, &result) + + if err != nil { + return false, err + } + + if result.Error != "" && result.Success != 1 { + return false, errors.New(result.Error) + } + + return true, nil +} + +func (p *HitBTC) SendAuthenticatedHTTPRequest(method, endpoint string, values url.Values, result interface{}) error { + if !p.AuthenticatedAPISupport { + return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, p.Name) + } + headers := make(map[string]string) + headers["Authorization"] = "Basic " + common.Base64Encode([]byte(p.APIKey+":"+p.APISecret)) + + path := fmt.Sprintf("%s/%s", APIURL, endpoint) + resp, err := common.SendHTTPRequest(method, path, headers, bytes.NewBufferString(values.Encode())) + + if err != nil { + return err + } + + err = common.JSONDecode([]byte(resp), &result) + + if err != nil { + return errors.New("Unable to JSON Unmarshal response.") + } + return nil +} diff --git a/exchanges/hitbtc/hitbtc_test.go b/exchanges/hitbtc/hitbtc_test.go new file mode 100644 index 00000000..faabff04 --- /dev/null +++ b/exchanges/hitbtc/hitbtc_test.go @@ -0,0 +1,69 @@ +package hitbtc + +import ( + "testing" + + "github.com/thrasher-/gocryptotrader/config" +) + +var p HitBTC + +// Please supply your own APIKEYS here for due diligence testing + +const ( + apiKey = "" + apiSecret = "" +) + +func TestSetDefaults(t *testing.T) { + p.SetDefaults() +} + +func TestSetup(t *testing.T) { + cfg := config.GetConfig() + cfg.LoadConfig("../../testdata/configtest.json") + hitbtcConfig, err := cfg.GetExchangeConfig("HitBTC") + if err != nil { + t.Error("Test Failed - HitBTC Setup() init error") + } + + hitbtcConfig.AuthenticatedAPISupport = true + hitbtcConfig.APIKey = apiKey + hitbtcConfig.APISecret = apiSecret + + p.Setup(hitbtcConfig) +} + +func TestGetFee(t *testing.T) { + if p.GetFee() != 0 { + t.Error("Test faild - HitBTC GetFee() error") + } +} + +func TestGetOrderbook(t *testing.T) { + _, err := p.GetOrderbook("BTCUSD", 50) + if err != nil { + t.Error("Test faild - HitBTC GetOrderbook() error", err) + } +} + +func TestGetTrades(t *testing.T) { + _, err := p.GetTrades("BTCUSD", "", "", "", "", "", "") + if err != nil { + t.Error("Test faild - HitBTC GetTradeHistory() error", err) + } +} + +func TestGetChartCandles(t *testing.T) { + _, err := p.GetCandles("BTCUSD", "", "") + if err != nil { + t.Error("Test faild - HitBTC GetChartData() error", err) + } +} + +func TestGetCurrencies(t *testing.T) { + _, err := p.GetCurrencies("") + if err != nil { + t.Error("Test faild - HitBTC GetCurrencies() error", err) + } +} diff --git a/exchanges/hitbtc/hitbtc_types.go b/exchanges/hitbtc/hitbtc_types.go new file mode 100644 index 00000000..eecb1e0c --- /dev/null +++ b/exchanges/hitbtc/hitbtc_types.go @@ -0,0 +1,233 @@ +package hitbtc + +import "time" + +type Ticker struct { + Last float64 `json:"last,string"` // Last trade price + Ask float64 `json:"ask,string"` // Best ask price + Bid float64 `json:"bid,string"` // Best bid price + Timestamp time.Time `json:"timestamp,string"` // Last update or refresh ticker timestamp + Volume float64 `json:"volume,string"` // Total trading amount within 24 hours in base currency + VolumeQuote float64 `json:"volumeQuote,string"` // Total trading amount within 24 hours in quote currency + Symbol string `json:"symbol"` + High float64 `json:"high,string"` // Highest trade price within 24 hours + Low float64 `json:"low,string"` // Lowest trade price within 24 hours + Open float64 `json:"open,string"` // Last trade price 24 hours ago +} + +type Symbol struct { + Id string `json:"id"` // Symbol identifier. In the future, the description will simply use the symbol + BaseCurrency string `json:"baseCurrency"` + QuoteCurrency string `json:"quoteCurrency"` + QuantityIncrement float64 `json:"quantityIncrement,string"` + TickSize float64 `json:"tickSize,string"` + TakeLiquidityRate float64 `json:"takeLiquidityRate,string"` // Default fee rate + ProvideLiquidityRate float64 `json:"provideLiquidityRate,string"` // Default fee rate for market making trades + FeeCurrency string `json:"feeCurrency"` // Default fee rate for market making trades +} + +type OrderbookResponse struct { + Asks []OrderbookItem `json:"ask"` // Ask side array of levels + Bids []OrderbookItem `json:"bid"` // Bid side array of levels +} + +type OrderbookItem struct { + Price float64 `json:"price,string"` // Price level + Amount float64 `json:"size,string"` // Total volume of orders with the specified price +} + +type Orderbook struct { + Asks []OrderbookItem `json:"asks"` + Bids []OrderbookItem `json:"bids"` +} + +type TradeHistory struct { + Id int64 `json:"id"` // Trade id + Timestamp string `json:"timestamp"` // Trade timestamp + Side string `json:"side"` // Trade side sell or buy + Price float64 `json:"price,string"` // Trade price + Quantity float64 `json:"quantity,string"` // Trade quantity +} + +type ChartData struct { + Timestamp time.Time `json:"timestamp,string"` + Max float64 `json:"max,string"` // Max price + Min float64 `json:"min,string"` // Min price + Open float64 `json:"open,string"` // Open price + Close float64 `json:"close,string"` // Close price + Volume float64 `json:"volume,string"` // Volume in base currency + VolumeQuote float64 `json:"volumeQuote,string"` // Volume in quote currency +} + +type Currencies struct { + Id string `json:"id"` // Currency identifier. + FullName string `json:"fullName"` // Currency full name + Crypto bool `json:"crypto,boolean"` // Is currency belongs to blockchain (false for ICO and fiat, like EUR) + PayinEnabled bool `json:"payinEnabled"` // Is allowed for deposit (false for ICO) + PayinPaymentId bool `json:"payinPaymentId"` // Is required to provide additional information other than the address for deposit + PayinConfirmations int64 `json:"payinConfirmations"` // Blocks confirmations count for deposit + PayoutEnabled bool `json:"payoutEnabled"` // Is allowed for withdraw (false for ICO) + PayoutIsPaymentId bool `json:"payoutIsPaymentId"` // Is allowed to provide additional information for withdraw + TransferEnabled bool `json:"transferEnabled"` // Is allowed to transfer between trading and account (may be disabled on maintain) +} + +type LoanOrder struct { + Rate float64 `json:"rate,string"` + Amount float64 `json:"amount,string"` + RangeMin int `json:"rangeMin"` + RangeMax int `json:"rangeMax"` +} + +type LoanOrders struct { + Offers []LoanOrder `json:"offers"` + Demands []LoanOrder `json:"demands"` +} + +type Balance struct { + Currency string `json:"currency"` + Available float64 `json:"available,string"` // Amount available for trading or transfer to main account + Reserved float64 `json:"reserved,string"` // Amount reserved for active orders or incomplete transfers to main account + +} + +type DepositCryptoAddresses struct { + Address string `json:"address"` // Address for deposit + PaymentId string `json:"paymentId"` // Optional additional parameter. Required for deposit if persist +} + +type Order struct { + Id int64 `json:"id,string"` // Unique identifier for Order as assigned by exchange + ClientOrderId string `json:"clientOrderId"` // Unique identifier for Order as assigned by trader. Uniqueness must be + // guaranteed within a single trading day, including all active orders. + Symbol string `json:"symbol"` // Trading symbol + Side string `json:"side"` // sell buy + Status string `json:"status"` // new, suspended, partiallyFilled, filled, canceled, expired + Type string `json:"type"` // Enum: limit, market, stopLimit, stopMarket + TimeInForce string `json:"timeInForce"` // Time in force is a special instruction used when placing a trade to + // indicate how long an order will remain active before it is executed or expires + // GTC - Good till cancel. GTC order won't close until it is filled. + // IOC - An immediate or cancel order is an order to buy or sell that must be executed immediately, and any portion + // of the order that cannot be immediately filled is cancelled. + // FOK - Fill or kill is a type of time-in-force designation used in securities trading that instructs a brokerage + // to execute a transaction immediately and completely or not at all. + // Day - keeps the order active until the end of the trading day in UTC. + // GTD - Good till date specified in expireTime. + Quantity float64 `json:"quantity,string"` // Order quantity + Price float64 `json:"price,string"` // Order price + CumQuantity float64 `json:"cumQuantity,string"` // Cumulative executed quantity + CreatedAt time.Time `json:"createdAt,string"` + UpdatedAt time.Time `json:"updatedAt,string"` + StopPrice float64 `json:"stopPrice,string"` + ExpireTime time.Time `json:"expireTime,string"` +} + +type OpenOrdersResponseAll struct { + Data map[string][]Order +} + +type OpenOrdersResponse struct { + Data []Order +} + +type AuthentictedTradeHistory struct { + GlobalTradeID int64 `json:"globalTradeID"` + TradeID int64 `json:"tradeID,string"` + Date string `json:"date"` + Rate float64 `json:"rate,string"` + Amount float64 `json:"amount,string"` + Total float64 `json:"total,string"` + Fee float64 `json:"fee,string"` + OrderNumber int64 `json:"orderNumber,string"` + Type string `json:"type"` + Category string `json:"category"` +} + +type AuthenticatedTradeHistoryAll struct { + Data map[string][]AuthentictedTradeHistory +} + +type AuthenticatedTradeHistoryResponse struct { + Data []AuthentictedTradeHistory +} + +type ResultingTrades struct { + Amount float64 `json:"amount,string"` + Date string `json:"date"` + Rate float64 `json:"rate,string"` + Total float64 `json:"total,string"` + TradeID int64 `json:"tradeID,string"` + Type string `json:"type"` +} + +type OrderResponse struct { + OrderNumber int64 `json:"orderNumber,string"` + Trades []ResultingTrades `json:"resultingTrades"` +} + +type GenericResponse struct { + Success int `json:"success"` + Error string `json:"error"` +} + +type MoveOrderResponse struct { + Success int `json:"success"` + Error string `json:"error"` + OrderNumber int64 `json:"orderNumber,string"` + Trades map[string][]ResultingTrades `json:"resultingTrades"` +} + +type Withdraw struct { + Response string `json:"response"` + Error string `json:"error"` +} + +type Fee struct { + TakeLiquidityRate float64 `json:"takeLiquidityRate,string"` // Taker + ProvideLiquidityRate float64 `json:"provideLiquidityRate,string"` // Maker +} + +type Margin struct { + TotalValue float64 `json:"totalValue,string"` + ProfitLoss float64 `json:"pl,string"` + LendingFees float64 `json:"lendingFees,string"` + NetValue float64 `json:"netValue,string"` + BorrowedValue float64 `json:"totalBorrowedValue,string"` + CurrentMargin float64 `json:"currentMargin,string"` +} + +type MarginPosition struct { + Amount float64 `json:"amount,string"` + Total float64 `json:"total,string"` + BasePrice float64 `json:"basePrice,string"` + LiquidiationPrice float64 `json:"liquidiationPrice"` + ProfitLoss float64 `json:"pl,string"` + LendingFees float64 `json:"lendingFees,string"` + Type string `json:"type"` +} + +type LoanOffer struct { + ID int64 `json:"id"` + Rate float64 `json:"rate,string"` + Amount float64 `json:"amount,string"` + Duration int `json:"duration"` + AutoRenew bool `json:"autoRenew,int"` + Date string `json:"date"` +} + +type ActiveLoans struct { + Provided []LoanOffer `json:"provided"` + Used []LoanOffer `json:"used"` +} + +type LendingHistory struct { + ID int64 `json:"id"` + Currency string `json:"currency"` + Rate float64 `json:"rate,string"` + Amount float64 `json:"amount,string"` + Duration float64 `json:"duration,string"` + Interest float64 `json:"interest,string"` + Fee float64 `json:"fee,string"` + Earned float64 `json:"earned,string"` + Open string `json:"open"` + Close string `json:"close"` +} diff --git a/exchanges/hitbtc/hitbtc_websocket.go b/exchanges/hitbtc/hitbtc_websocket.go new file mode 100644 index 00000000..5552f8dd --- /dev/null +++ b/exchanges/hitbtc/hitbtc_websocket.go @@ -0,0 +1,182 @@ +package hitbtc + +import ( + "log" + "strconv" + + "github.com/beatgammit/turnpike" +) + +const ( + HITBTC_WEBSOCKET_ADDRESS = "wss://api.hitbtc.com" + HITBTC_WEBSOCKET_REALM = "realm1" + HITBTC_WEBSOCKET_TICKER = "ticker" + HITBTC_WEBSOCKET_TROLLBOX = "trollbox" +) + +type HitBTCWebsocketTicker struct { + CurrencyPair string + Last float64 + LowestAsk float64 + HighestBid float64 + PercentChange float64 + BaseVolume float64 + QuoteVolume float64 + IsFrozen bool + High float64 + Low float64 +} + +func HitBTCOnTicker(args []interface{}, kwargs map[string]interface{}) { + ticker := HitBTCWebsocketTicker{} + ticker.CurrencyPair = args[0].(string) + ticker.Last, _ = strconv.ParseFloat(args[1].(string), 64) + ticker.LowestAsk, _ = strconv.ParseFloat(args[2].(string), 64) + ticker.HighestBid, _ = strconv.ParseFloat(args[3].(string), 64) + ticker.PercentChange, _ = strconv.ParseFloat(args[4].(string), 64) + ticker.BaseVolume, _ = strconv.ParseFloat(args[5].(string), 64) + ticker.QuoteVolume, _ = strconv.ParseFloat(args[6].(string), 64) + + if args[7].(float64) != 0 { + ticker.IsFrozen = true + } else { + ticker.IsFrozen = false + } + + ticker.High, _ = strconv.ParseFloat(args[8].(string), 64) + ticker.Low, _ = strconv.ParseFloat(args[9].(string), 64) +} + +type HitBTCWebsocketTrollboxMessage struct { + MessageNumber float64 + Username string + Message string + Reputation float64 +} + +func HitBTCOnTrollbox(args []interface{}, kwargs map[string]interface{}) { + message := HitBTCWebsocketTrollboxMessage{} + message.MessageNumber, _ = args[1].(float64) + message.Username = args[2].(string) + message.Message = args[3].(string) + if len(args) == 5 { + message.Reputation = args[4].(float64) + } +} + +func HitBTCOnDepthOrTrade(args []interface{}, kwargs map[string]interface{}) { + for x := range args { + data := args[x].(map[string]interface{}) + msgData := data["data"].(map[string]interface{}) + msgType := data["type"].(string) + + switch msgType { + case "orderBookModify": + { + type HitBTCWebsocketOrderbookModify struct { + Type string + Rate float64 + Amount float64 + } + + orderModify := HitBTCWebsocketOrderbookModify{} + orderModify.Type = msgData["type"].(string) + + rateStr := msgData["rate"].(string) + orderModify.Rate, _ = strconv.ParseFloat(rateStr, 64) + + amountStr := msgData["amount"].(string) + orderModify.Amount, _ = strconv.ParseFloat(amountStr, 64) + } + case "orderBookRemove": + { + type HitBTCWebsocketOrderbookRemove struct { + Type string + Rate float64 + } + + orderRemoval := HitBTCWebsocketOrderbookRemove{} + orderRemoval.Type = msgData["type"].(string) + + rateStr := msgData["rate"].(string) + orderRemoval.Rate, _ = strconv.ParseFloat(rateStr, 64) + } + case "newTrade": + { + type HitBTCWebsocketNewTrade struct { + Type string + TradeID int64 + Rate float64 + Amount float64 + Date string + Total float64 + } + + trade := HitBTCWebsocketNewTrade{} + trade.Type = msgData["type"].(string) + + tradeIDstr := msgData["tradeID"].(string) + trade.TradeID, _ = strconv.ParseInt(tradeIDstr, 10, 64) + + rateStr := msgData["rate"].(string) + trade.Rate, _ = strconv.ParseFloat(rateStr, 64) + + amountStr := msgData["amount"].(string) + trade.Amount, _ = strconv.ParseFloat(amountStr, 64) + + totalStr := msgData["total"].(string) + trade.Rate, _ = strconv.ParseFloat(totalStr, 64) + + trade.Date = msgData["date"].(string) + } + } + } +} + +func (p *HitBTC) WebsocketClient() { + for p.Enabled && p.Websocket { + c, err := turnpike.NewWebsocketClient(turnpike.JSON, HITBTC_WEBSOCKET_ADDRESS, nil) + if err != nil { + log.Printf("%s Unable to connect to Websocket. Error: %s\n", p.GetName(), err) + continue + } + + if p.Verbose { + log.Printf("%s Connected to Websocket.\n", p.GetName()) + } + + _, err = c.JoinRealm(HITBTC_WEBSOCKET_REALM, nil) + if err != nil { + log.Printf("%s Unable to join realm. Error: %s\n", p.GetName(), err) + continue + } + + if p.Verbose { + log.Printf("%s Joined Websocket realm.\n", p.GetName()) + } + + c.ReceiveDone = make(chan bool) + + if err := c.Subscribe(HITBTC_WEBSOCKET_TICKER, HitBTCOnTicker); err != nil { + log.Printf("%s Error subscribing to ticker channel: %s\n", p.GetName(), err) + } + + if err := c.Subscribe(HITBTC_WEBSOCKET_TROLLBOX, HitBTCOnTrollbox); err != nil { + log.Printf("%s Error subscribing to trollbox channel: %s\n", p.GetName(), err) + } + + for x := range p.EnabledPairs { + currency := p.EnabledPairs[x] + if err := c.Subscribe(currency, HitBTCOnDepthOrTrade); err != nil { + log.Printf("%s Error subscribing to %s channel: %s\n", p.GetName(), currency, err) + } + } + + if p.Verbose { + log.Printf("%s Subscribed to websocket channels.\n", p.GetName()) + } + + <-c.ReceiveDone + log.Printf("%s Websocket client disconnected.\n", p.GetName()) + } +} diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go new file mode 100644 index 00000000..526baabc --- /dev/null +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -0,0 +1,108 @@ +package hitbtc + +import ( + "log" + + "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" +) + +// Start starts the HitBTC go routine +func (p *HitBTC) Start() { + go p.Run() +} + +// Run implements the HitBTC wrapper +func (p *HitBTC) Run() { + if p.Verbose { + log.Printf("%s Websocket: %s (url: %s).\n", p.GetName(), common.IsEnabled(p.Websocket), HITBTC_WEBSOCKET_ADDRESS) + log.Printf("%s polling delay: %ds.\n", p.GetName(), p.RESTPollingDelay) + log.Printf("%s %d currencies enabled: %s.\n", p.GetName(), len(p.EnabledPairs), p.EnabledPairs) + } + + if p.Websocket { + go p.WebsocketClient() + } +} + +// UpdateTicker updates and returns the ticker for a currency pair +func (p *HitBTC) UpdateTicker(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) { + tick := p.GetTicker("") + + for _, x := range p.GetEnabledCurrencies() { + var tp ticker.Price + curr := exchange.FormatExchangeCurrency(p.GetName(), x).String() + tp.Pair = x + tp.Ask = tick[curr].Ask + tp.Bid = tick[curr].Bid + tp.High = tick[curr].High + tp.Last = tick[curr].Last + tp.Low = tick[curr].Low + tp.Volume = tick[curr].Volume + ticker.ProcessTicker(p.GetName(), x, tp, assetType) + } + return ticker.GetTicker(p.Name, currencyPair, assetType) +} + +// GetTickerPrice returns the ticker for a currency pair +func (p *HitBTC) GetTickerPrice(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(p.GetName(), currencyPair, assetType) + if err != nil { + return p.UpdateTicker(currencyPair, assetType) + } + return tickerNew, nil +} + +// GetOrderbookEx returns orderbook base on the currency pair +func (p *HitBTC) GetOrderbookEx(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(p.GetName(), currencyPair, assetType) + if err == nil { + return p.UpdateOrderbook(currencyPair, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (p *HitBTC) UpdateOrderbook(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + orderbookNew, err := p.GetOrderbook(exchange.FormatExchangeCurrency(p.GetName(), currencyPair).String(), 1000) + if err != nil { + return orderBook, err + } + + for x := range orderbookNew.Bids { + data := orderbookNew.Bids[x] + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data.Amount, Price: data.Price}) + } + + for x := range orderbookNew.Asks { + data := orderbookNew.Asks[x] + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data.Amount, Price: data.Price}) + } + + orderbook.ProcessOrderbook(p.GetName(), currencyPair, orderBook, assetType) + return orderbook.GetOrderbook(p.Name, currencyPair, assetType) +} + +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// HitBTC exchange +func (p *HitBTC) GetExchangeAccountInfo() (exchange.AccountInfo, error) { + var response exchange.AccountInfo + response.ExchangeName = p.GetName() + accountBalance, err := p.GetBalances() + if err != nil { + return response, err + } + + for _, item := range accountBalance { + var exchangeCurrency exchange.AccountCurrencyInfo + exchangeCurrency.CurrencyName = item.Currency + exchangeCurrency.TotalValue = item.Available + exchangeCurrency.Hold = item.Reserved + response.Currencies = append(response.Currencies, exchangeCurrency) + } + return response, nil +} diff --git a/testdata/configtest.json b/testdata/configtest.json index b754dcbb..d1577acb 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -321,6 +321,27 @@ "Uppercase": true } }, + { + "Name": "HitBTC", + "Enabled": true, + "Verbose": false, + "Websocket": false, + "UseSandbox": false, + "RESTPollingDelay": 10, + "AuthenticatedAPISupport": false, + "APIKey": "Key", + "APISecret": "Secret", + "AvailablePairs": "BTCUSD,ETHBTC,ETHUSD", + "EnabledPairs": "BTCUSD", + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } + }, { "Name": "Huobi", "Enabled": true,