From a33ddcfa0a1b90b833a39058359e71d304a55374 Mon Sep 17 00:00:00 2001 From: Adam <31364354+MadCozBadd@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:53:35 +1100 Subject: [PATCH] Engine: BTC Markets V3 Updates (#385) * Broken WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP * Errors Fixed * PR Changes * PR Changes * PR Changes * PR Changes * PR Changes * PR Changes * PR Changes * PR Changes * PR Changes * Offline Fees Fixed * MarketCandles fixed and constants added * t.log deleted * Broken WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP WIP * Errors Fixed * PR Changes * PR Changes * PR Changes * MarketCandles fixed and constants added * t.log deleted * Added BSB and Order Status * WIP * Websocket and BatchPlaceCancelOrder and other minor nits fixed * Linter Issues Fixed * Function Name Change * Replacing b.GetMarkets with b.GetEnabledPairs * Pagination param changes and PlaceCancelBatch changes * Merge Conflicts WIP * Linter Issue Fixed * optional params fixed --- cmd/exchange_wrapper_issues/types.go | 2 +- exchanges/bitfinex/bitfinex.go | 2 +- exchanges/bitfinex/bitfinex_test.go | 4 +- exchanges/bithumb/bithumb_test.go | 2 +- exchanges/bithumb/bithumb_wrapper.go | 5 +- exchanges/bitstamp/bitstamp_test.go | 4 +- exchanges/btcmarkets/btcmarkets.go | 983 ++++++++++++------- exchanges/btcmarkets/btcmarkets_test.go | 779 +++++++-------- exchanges/btcmarkets/btcmarkets_types.go | 378 +++++-- exchanges/btcmarkets/btcmarkets_websocket.go | 109 +- exchanges/btcmarkets/btcmarkets_wrapper.go | 531 +++++----- exchanges/exchange_types.go | 3 +- exchanges/order/order_types.go | 37 +- 13 files changed, 1710 insertions(+), 1129 deletions(-) diff --git a/cmd/exchange_wrapper_issues/types.go b/cmd/exchange_wrapper_issues/types.go index 60427c52..08b7e467 100644 --- a/cmd/exchange_wrapper_issues/types.go +++ b/cmd/exchange_wrapper_issues/types.go @@ -73,7 +73,7 @@ type EndpointResponse struct { // Bank contains all required data for a wrapper withdrawal request type Bank struct { BankAccountName string `json:"bankAccountName"` - BankAccountNumber float64 `json:"bankAccountNumber"` + BankAccountNumber string `json:"bankAccountNumber"` BankAddress string `json:"bankAddress"` BankCity string `json:"bankCity"` BankCountry string `json:"bankCountry"` diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index 5e185410..f85e4c9d 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -548,7 +548,7 @@ func (b *Bitfinex) WithdrawFIAT(withdrawalType, walletType string, withdrawReque req["walletselected"] = walletType req["amount"] = strconv.FormatFloat(withdrawRequest.Amount, 'f', -1, 64) req["account_name"] = withdrawRequest.BankAccountName - req["account_number"] = strconv.FormatFloat(withdrawRequest.BankAccountNumber, 'f', -1, 64) + req["account_number"] = withdrawRequest.BankAccountNumber req["bank_name"] = withdrawRequest.BankName req["bank_address"] = withdrawRequest.BankAddress req["bank_city"] = withdrawRequest.BankCity diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index 32208cd9..04f44be4 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -917,7 +917,7 @@ func TestWithdrawFiat(t *testing.T) { Description: "WITHDRAW IT ALL", }, BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, + BankAccountNumber: "12345", BankAddress: "123 Fake St", BankCity: "Tarry Town", BankCountry: "Hyrule", @@ -952,7 +952,7 @@ func TestWithdrawInternationalBank(t *testing.T) { Description: "WITHDRAW IT ALL", }, BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, + BankAccountNumber: "12345", BankAddress: "123 Fake St", BankCity: "Tarry Town", BankCountry: "Hyrule", diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index a4127426..8f82f248 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -497,7 +497,7 @@ func TestWithdrawFiat(t *testing.T) { Description: "WITHDRAW IT ALL", }, BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, + BankAccountNumber: "12345", BankCode: 123, BankAddress: "123 Fake St", BankCity: "Tarry Town", diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index c4bf45a8..91f8f983 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "math" - "strconv" "sync" "time" @@ -419,9 +418,7 @@ func (b *Bithumb) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawReques return "", errors.New("only KRW is supported") } bankDetails := fmt.Sprintf("%v_%v", withdrawRequest.BankCode, withdrawRequest.BankName) - bankAccountNumber := strconv.FormatFloat(withdrawRequest.BankAccountNumber, 'f', -1, 64) - withdrawAmountInt := int64(withdrawRequest.Amount) - resp, err := b.RequestKRWWithdraw(bankDetails, bankAccountNumber, withdrawAmountInt) + resp, err := b.RequestKRWWithdraw(bankDetails, withdrawRequest.BankAccountNumber, int64(withdrawRequest.Amount)) if err != nil { return "", err } diff --git a/exchanges/bitstamp/bitstamp_test.go b/exchanges/bitstamp/bitstamp_test.go index c541dd8a..77d8b575 100644 --- a/exchanges/bitstamp/bitstamp_test.go +++ b/exchanges/bitstamp/bitstamp_test.go @@ -489,7 +489,7 @@ func TestWithdrawFiat(t *testing.T) { Description: "WITHDRAW IT ALL", }, BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, + BankAccountNumber: "12345", BankAddress: "123 Fake St", BankCity: "Tarry Town", BankCountry: "AU", @@ -527,7 +527,7 @@ func TestWithdrawInternationalBank(t *testing.T) { Description: "WITHDRAW IT ALL", }, BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, + BankAccountNumber: "12345", BankAddress: "123 Fake St", BankCity: "Tarry Town", BankCountry: "AU", diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index 11882328..a477f483 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -5,44 +5,75 @@ import ( "encoding/json" "errors" "fmt" + "io" "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" - log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( - btcMarketsAPIURL = "https://api.btcmarkets.net" - btcMarketsAPIVersion = "0" - btcMarketsAccountBalance = "/account/balance" - btcMarketsTradingFee = "/account/%s/%s/tradingfee" - btcMarketsOrderCreate = "/order/create" - btcMarketsOrderCancel = "/order/cancel" - btcMarketsOrderHistory = "/order/history" - btcMarketsOrderOpen = "/order/open" - btcMarketsOrderTradeHistory = "/order/trade/history" - btcMarketsOrderDetail = "/order/detail" - btcMarketsWithdrawCrypto = "/fundtransfer/withdrawCrypto" - btcMarketsWithdrawAud = "/fundtransfer/withdrawEFT" + btcMarketsAPIURL = "https://api.btcmarkets.net" + btcMarketsAPIVersion = "/v3" - // Status Values - orderStatusNew = "New" - orderStatusPlaced = "Placed" - orderStatusFailed = "Failed" - orderStatusError = "Error" - orderStatusCancelled = "Cancelled" - orderStatusPartiallyCancelled = "Partially Cancelled" - orderStatusFullyMatched = "Fully Matched" - orderStatusPartiallyMatched = "Partially Matched" + // UnAuthenticated EPs + btcMarketsAllMarkets = "/markets/" + btcMarketsGetTicker = "/ticker/" + btcMarketsGetTrades = "/trades?" + btcMarketOrderBooks = "/orderbook?" + btcMarketsCandles = "/candles?" + btcMarketsTickers = "/tickers?" + btcMarketsMultipleOrderbooks = "/orderbooks?" + btcMarketsGetTime = "/time" + btcMarketsWithdrawalFees = "/withdrawal-fees" + btcMarketsUnauthPath = btcMarketsAPIURL + btcMarketsAPIVersion + btcMarketsAllMarkets - btcmarketsAuthLimit = 10 - btcmarketsUnauthLimit = 25 + // Authenticated EPs + btcMarketsAccountBalance = "/accounts/me/balances" + btcMarketsTradingFees = "/accounts/me/trading-fees" + btcMarketsTransactions = "/accounts/me/transactions" + btcMarketsOrders = "/orders" + btcMarketsTradeHistory = "/trades" + btcMarketsWithdrawals = "/withdrawals" + btcMarketsDeposits = "/deposits" + btcMarketsTransfers = "/transfers" + btcMarketsAddresses = "/addresses" + btcMarketsAssets = "/assets" + btcMarketsReports = "/reports" + btcMarketsBatchOrders = "/batchorders" + + btcmarketsAuthLimit = 3 + btcmarketsUnauthLimit = 50 + + orderFailed = "Failed" + orderPartiallyCancelled = "Partially Cancelled" + orderCancelled = "Cancelled" + orderFullyMatched = "FullyMatched" + orderPartiallyMatched = "Partially Matched" + orderPlaced = "Placed" + orderAccepted = "Accepted" + + ask = "ask" + limit = "Limit" + market = "Market" + stopLimit = "Stop Limit" + stop = "Stop" + takeProfit = "Take Profit" + + subscribe = "subscribe" + fundChange = "fundChange" + orderChange = "orderChange" + heartbeat = "heartbeat" + tick = "tick" + wsOB = "orderbook" + trade = "trade" ) // BTCMarkets is the overarching type across the BTCMarkets package @@ -53,316 +84,579 @@ type BTCMarkets struct { // GetMarkets returns the BTCMarkets instruments func (b *BTCMarkets) GetMarkets() ([]Market, error) { - type marketsResp struct { - Response - Markets []Market `json:"markets"` - } - - var resp marketsResp - path := fmt.Sprintf("%s/v2/market/active", b.API.Endpoints.URL) - - err := b.SendHTTPRequest(path, &resp) - if err != nil { - return nil, err - } - - if !resp.Success { - return nil, fmt.Errorf("%s unable to get markets: %s", b.Name, resp.ErrorMessage) - } - - return resp.Markets, nil + var resp []Market + return resp, b.SendHTTPRequest(btcMarketsUnauthPath, &resp) } // GetTicker returns a ticker // symbol - example "btc" or "ltc" -func (b *BTCMarkets) GetTicker(firstPair, secondPair string) (Ticker, error) { - tick := Ticker{} - path := fmt.Sprintf("%s/market/%s/%s/tick", - b.API.Endpoints.URL, - strings.ToUpper(firstPair), - strings.ToUpper(secondPair)) - - return tick, b.SendHTTPRequest(path, &tick) -} - -// GetOrderbook returns current orderbook -// symbol - example "btc" or "ltc" -func (b *BTCMarkets) GetOrderbook(firstPair, secondPair string) (Orderbook, error) { - orderbook := Orderbook{} - path := fmt.Sprintf("%s/market/%s/%s/orderbook", - b.API.Endpoints.URL, - strings.ToUpper(firstPair), - strings.ToUpper(secondPair)) - - return orderbook, b.SendHTTPRequest(path, &orderbook) +func (b *BTCMarkets) GetTicker(marketID string) (Ticker, error) { + var tick Ticker + return tick, b.SendHTTPRequest(btcMarketsUnauthPath+marketID+btcMarketsGetTicker, &tick) } // GetTrades returns executed trades on the exchange -// symbol - example "btc" or "ltc" -// values - optional paramater "since" example values.Set(since, "59868345231") -func (b *BTCMarkets) GetTrades(firstPair, secondPair string, values url.Values) ([]Trade, error) { +func (b *BTCMarkets) GetTrades(marketID string, before, after, limit int64) ([]Trade, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } var trades []Trade - path := common.EncodeURLValues(fmt.Sprintf("%s/market/%s/%s/trades", - b.API.Endpoints.URL, strings.ToUpper(firstPair), - strings.ToUpper(secondPair)), values) - - return trades, b.SendHTTPRequest(path, &trades) + params := url.Values{} + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return trades, b.SendHTTPRequest(btcMarketsUnauthPath+marketID+btcMarketsGetTrades+params.Encode(), + &trades) } -// NewOrder requests a new order and returns an ID -// currency - example "AUD" -// instrument - example "BTC" -// price - example 13000000000 (i.e x 100000000) -// amount - example 100000000 (i.e x 100000000) -// orderside - example "Bid" or "Ask" -// orderType - example "limit" -// clientReq - example "abc-cdf-1000" -func (b *BTCMarkets) NewOrder(instrument, currency string, price, amount float64, orderSide, orderType, clientReq string) (int64, error) { - newPrice := int64(price * float64(common.SatoshisPerBTC)) - newVolume := int64(amount * float64(common.SatoshisPerBTC)) - - order := OrderToGo{ - Currency: strings.ToUpper(currency), - Instrument: strings.ToUpper(instrument), - Price: newPrice, - Volume: newVolume, - OrderSide: orderSide, - OrderType: orderType, - ClientRequestID: clientReq, +// GetOrderbook returns current orderbook +func (b *BTCMarkets) GetOrderbook(marketID string, level int64) (Orderbook, error) { + var orderbook Orderbook + var temp tempOrderbook + params := url.Values{} + if level != 0 { + params.Set("level", strconv.FormatInt(level, 10)) } - - resp := Response{} - - err := b.SendAuthenticatedRequest(http.MethodPost, btcMarketsOrderCreate, order, &resp) + err := b.SendHTTPRequest(btcMarketsUnauthPath+marketID+btcMarketOrderBooks+params.Encode(), + &temp) if err != nil { - return 0, err + return orderbook, err } - if !resp.Success { - return 0, fmt.Errorf("%s Unable to place order. Error message: %s", b.Name, resp.ErrorMessage) - } - return int64(resp.ID), nil -} - -// CancelExistingOrder cancels an order by its ID -// orderID - id for order example "1337" -func (b *BTCMarkets) CancelExistingOrder(orderID []int64) ([]ResponseDetails, error) { - resp := Response{} - type CancelOrder struct { - OrderIDs []int64 `json:"orderIds"` - } - orders := CancelOrder{} - orders.OrderIDs = append(orders.OrderIDs, orderID...) - - err := b.SendAuthenticatedRequest(http.MethodPost, btcMarketsOrderCancel, orders, &resp) - if err != nil { - return resp.Responses, err - } - - if !resp.Success { - return resp.Responses, fmt.Errorf("%s Unable to cancel order. Error message: %s", b.Name, resp.ErrorMessage) - } - - return resp.Responses, nil -} - -// GetOrders returns current order information on the exchange -// currency - example "AUD" -// instrument - example "BTC" -// limit - example "10" -// since - since a time example "33434568724" -// historic - if false just normal Orders open -func (b *BTCMarkets) GetOrders(currency, instrument string, limit, since int64, historic bool) ([]Order, error) { - req := make(map[string]interface{}) - - if currency != "" { - req["currency"] = strings.ToUpper(currency) - } - if instrument != "" { - req["instrument"] = strings.ToUpper(instrument) - } - if limit != 0 { - req["limit"] = limit - } - if since != 0 { - req["since"] = since - } - - path := btcMarketsOrderOpen - if historic { - path = btcMarketsOrderHistory - } - - resp := Response{} - - err := b.SendAuthenticatedRequest(http.MethodPost, path, req, &resp) - if err != nil { - return nil, err - } - - if !resp.Success { - return nil, errors.New(resp.ErrorMessage) - } - - for i := range resp.Orders { - resp.Orders[i].Price /= common.SatoshisPerBTC - resp.Orders[i].OpenVolume /= common.SatoshisPerBTC - resp.Orders[i].Volume /= common.SatoshisPerBTC - - for j := range resp.Orders[i].Trades { - resp.Orders[i].Trades[j].Fee /= common.SatoshisPerBTC - resp.Orders[i].Trades[j].Price /= common.SatoshisPerBTC - resp.Orders[i].Trades[j].Volume /= common.SatoshisPerBTC + orderbook.MarketID = temp.MarketID + orderbook.SnapshotID = temp.SnapshotID + for x := range temp.Asks { + price, err := strconv.ParseFloat(temp.Asks[x][0], 64) + if err != nil { + return orderbook, err } + amount, err := strconv.ParseFloat(temp.Asks[x][1], 64) + if err != nil { + return orderbook, err + } + orderbook.Asks = append(orderbook.Asks, OBData{ + Price: price, + Volume: amount, + }) } - - return resp.Orders, nil + for a := range temp.Bids { + price, err := strconv.ParseFloat(temp.Bids[a][0], 64) + if err != nil { + return orderbook, err + } + amount, err := strconv.ParseFloat(temp.Bids[a][1], 64) + if err != nil { + return orderbook, err + } + orderbook.Bids = append(orderbook.Bids, OBData{ + Price: price, + Volume: amount, + }) + } + return orderbook, nil } -// GetOpenOrders returns all open orders -func (b *BTCMarkets) GetOpenOrders() ([]Order, error) { - type marketsResp struct { - Response +// GetMarketCandles gets candles for specified currency pair +func (b *BTCMarkets) GetMarketCandles(marketID, timeWindow, from, to string, before, after, limit int64) ([]MarketCandle, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") } - req := make(map[string]interface{}) - var resp marketsResp - path := fmt.Sprintf("/v2/order/open") - - err := b.SendAuthenticatedRequest(http.MethodGet, path, req, &resp) + var marketCandles []MarketCandle + var temp [][]string + params := url.Values{} + if timeWindow != "" { + params.Set("timeWindow", timeWindow) + } + if from != "" { + params.Set("from", from) + } + if to != "" { + params.Set("to", to) + } + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + err := b.SendHTTPRequest(btcMarketsUnauthPath+marketID+btcMarketsCandles+params.Encode(), + &temp) if err != nil { - return nil, err + return marketCandles, err } - - if !resp.Success { - return nil, errors.New(resp.ErrorMessage) - } - - for i := range resp.Orders { - resp.Orders[i].Price /= common.SatoshisPerBTC - resp.Orders[i].OpenVolume /= common.SatoshisPerBTC - resp.Orders[i].Volume /= common.SatoshisPerBTC - - for j := range resp.Orders[i].Trades { - resp.Orders[i].Trades[j].Fee /= common.SatoshisPerBTC - resp.Orders[i].Trades[j].Price /= common.SatoshisPerBTC - resp.Orders[i].Trades[j].Volume /= common.SatoshisPerBTC + var tempData MarketCandle + var tempTime time.Time + for x := range temp { + tempTime, err = time.Parse(time.RFC3339, temp[x][0]) + if err != nil { + return marketCandles, err } + tempData.Time = tempTime + tempData.Open, err = strconv.ParseFloat(temp[x][1], 64) + if err != nil { + return marketCandles, err + } + tempData.High, err = strconv.ParseFloat(temp[x][2], 64) + if err != nil { + return marketCandles, err + } + tempData.Low, err = strconv.ParseFloat(temp[x][3], 64) + if err != nil { + return marketCandles, err + } + tempData.Close, err = strconv.ParseFloat(temp[x][4], 64) + if err != nil { + return marketCandles, err + } + tempData.Volume, err = strconv.ParseFloat(temp[x][5], 64) + if err != nil { + return marketCandles, err + } + marketCandles = append(marketCandles, tempData) } - - return resp.Orders, nil + return marketCandles, nil } -// GetOrderDetail returns order information an a specific order -// orderID - example "1337" -func (b *BTCMarkets) GetOrderDetail(orderID []int64) ([]Order, error) { - type OrderDetail struct { - OrderIDs []int64 `json:"orderIds"` +// GetTickers gets multiple tickers +func (b *BTCMarkets) GetTickers(marketIDs []string) ([]Ticker, error) { + var tickers []Ticker + params := url.Values{} + for x := range marketIDs { + params.Add("marketId", marketIDs[x]) } - orders := OrderDetail{} - orders.OrderIDs = append(orders.OrderIDs, orderID...) + return tickers, b.SendHTTPRequest(btcMarketsUnauthPath+btcMarketsTickers+params.Encode(), + &tickers) +} - resp := Response{} - - err := b.SendAuthenticatedRequest(http.MethodPost, btcMarketsOrderDetail, orders, &resp) +// GetMultipleOrderbooks gets orderbooks +func (b *BTCMarkets) GetMultipleOrderbooks(marketIDs []string) ([]Orderbook, error) { + var orderbooks []Orderbook + var temp []tempOrderbook + var tempOB Orderbook + params := url.Values{} + for x := range marketIDs { + params.Add("marketId", marketIDs[x]) + } + err := b.SendHTTPRequest(btcMarketsUnauthPath+btcMarketsMultipleOrderbooks+params.Encode(), + &temp) if err != nil { - return nil, err + return orderbooks, err } - - if !resp.Success { - return nil, errors.New(resp.ErrorMessage) - } - - for i := range resp.Orders { - resp.Orders[i].Price /= common.SatoshisPerBTC - resp.Orders[i].OpenVolume /= common.SatoshisPerBTC - resp.Orders[i].Volume /= common.SatoshisPerBTC - - for x := range resp.Orders[i].Trades { - resp.Orders[i].Trades[x].Fee /= common.SatoshisPerBTC - resp.Orders[i].Trades[x].Price /= common.SatoshisPerBTC - resp.Orders[i].Trades[x].Volume /= common.SatoshisPerBTC + for i := range temp { + var price, volume float64 + tempOB.MarketID = temp[i].MarketID + tempOB.SnapshotID = temp[i].SnapshotID + for a := range temp[i].Asks { + volume, err = strconv.ParseFloat(temp[i].Asks[a][1], 64) + if err != nil { + return orderbooks, err + } + price, err = strconv.ParseFloat(temp[i].Asks[a][0], 64) + if err != nil { + return orderbooks, err + } + tempOB.Asks = append(tempOB.Asks, OBData{Price: price, Volume: volume}) } + for y := range temp[i].Bids { + volume, err = strconv.ParseFloat(temp[i].Bids[y][1], 64) + if err != nil { + return orderbooks, err + } + price, err = strconv.ParseFloat(temp[i].Bids[y][0], 64) + if err != nil { + return orderbooks, err + } + tempOB.Bids = append(tempOB.Bids, OBData{Price: price, Volume: volume}) + } + orderbooks = append(orderbooks, tempOB) } - return resp.Orders, nil + return orderbooks, nil +} + +// GetServerTime gets time from btcmarkets +func (b *BTCMarkets) GetServerTime() (time.Time, error) { + var resp TimeResp + return resp.Time, b.SendHTTPRequest(btcMarketsAPIURL+btcMarketsAPIVersion+btcMarketsGetTime, + &resp) } // GetAccountBalance returns the full account balance -func (b *BTCMarkets) GetAccountBalance() ([]AccountBalance, error) { - var balance []AccountBalance - - err := b.SendAuthenticatedRequest(http.MethodGet, btcMarketsAccountBalance, nil, &balance) - if err != nil { - return nil, err - } - - // All values are returned in Satoshis, even for fiat currencies. - for i := range balance { - balance[i].Balance /= common.SatoshisPerBTC - balance[i].PendingFunds /= common.SatoshisPerBTC - } - return balance, nil +func (b *BTCMarkets) GetAccountBalance() ([]AccountData, error) { + var resp []AccountData + return resp, + b.SendAuthenticatedRequest(http.MethodGet, + btcMarketsAccountBalance, + nil, + &resp) } -// GetTradingFee returns the account's trading fee for a currency pair -func (b *BTCMarkets) GetTradingFee(base, quote currency.Code) (TradingFee, error) { - var tradingFee TradingFee - path := fmt.Sprintf(btcMarketsTradingFee, base, quote) - return tradingFee, b.SendAuthenticatedRequest(http.MethodGet, path, nil, &tradingFee) -} - -// WithdrawCrypto withdraws cryptocurrency into a designated address -func (b *BTCMarkets) WithdrawCrypto(amount float64, currency, address string) (string, error) { - newAmount := int64(amount * float64(common.SatoshisPerBTC)) - - req := WithdrawRequestCrypto{ - Amount: newAmount, - Currency: strings.ToUpper(currency), - Address: address, - } - - resp := Response{} - err := b.SendAuthenticatedRequest(http.MethodPost, - btcMarketsWithdrawCrypto, - req, +// GetTradingFees returns trading fees for all pairs based on trading activity +func (b *BTCMarkets) GetTradingFees() (TradingFeeResponse, error) { + var resp TradingFeeResponse + return resp, b.SendAuthenticatedRequest(http.MethodGet, + btcMarketsTradingFees, + nil, &resp) - if err != nil { - return "", err - } - - if !resp.Success { - return "", errors.New(resp.ErrorMessage) - } - - return resp.Status, nil } -// WithdrawAUD withdraws AUD into a designated bank address -// Does not return a TxID! -func (b *BTCMarkets) WithdrawAUD(accountName, accountNumber, bankName, bsbNumber string, amount float64) (string, error) { - newAmount := int64(amount * float64(common.SatoshisPerBTC)) - - req := WithdrawRequestAUD{ - AccountName: accountName, - AccountNumber: accountNumber, - BankName: bankName, - BSBNumber: bsbNumber, - Amount: newAmount, - Currency: "AUD", +// GetTradeHistory returns trade history +func (b *BTCMarkets) GetTradeHistory(marketID, orderID string, before, after, limit int64) ([]TradeHistoryData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") } - - resp := Response{} - err := b.SendAuthenticatedRequest(http.MethodPost, btcMarketsWithdrawAud, - req, + var resp []TradeHistoryData + params := url.Values{} + if marketID != "" { + params.Set("marketId", marketID) + } + if orderID != "" { + params.Set("orderId", orderID) + } + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsTradeHistory, params), + nil, &resp) - if err != nil { - return "", err - } +} - if !resp.Success { - return "", errors.New(resp.ErrorMessage) - } +// GetTradeByID returns the singular trade of the ID given +func (b *BTCMarkets) GetTradeByID(id string) (TradeHistoryData, error) { + var resp TradeHistoryData + return resp, b.SendAuthenticatedRequest(http.MethodGet, + btcMarketsTradeHistory+"/"+id, + nil, + &resp) +} - return resp.Status, nil +// NewOrder requests a new order and returns an ID +func (b *BTCMarkets) NewOrder(marketID string, price, amount float64, orderType, side string, triggerPrice, + targetAmount float64, timeInForce string, postOnly bool, selfTrade, clientOrderID string) (OrderData, error) { + var resp OrderData + req := make(map[string]interface{}) + req["marketId"] = marketID + req["price"] = strconv.FormatFloat(price, 'f', -1, 64) + req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) + req["type"] = orderType + req["side"] = side + if orderType == stopLimit || orderType == takeProfit || orderType == stop { + req["triggerPrice"] = strconv.FormatFloat(triggerPrice, 'f', -1, 64) + } + if targetAmount > 0 { + req["targetAmount"] = strconv.FormatFloat(targetAmount, 'f', -1, 64) + } + if timeInForce != "" { + req["timeInForce"] = timeInForce + } + req["postOnly"] = postOnly + if selfTrade != "" { + req["selfTrade"] = selfTrade + } + if clientOrderID != "" { + req["clientOrderID"] = clientOrderID + } + return resp, b.SendAuthenticatedRequest(http.MethodPost, btcMarketsOrders, req, &resp) +} + +// GetOrders returns current order information on the exchange +func (b *BTCMarkets) GetOrders(marketID string, before, after, limit int64, status string) ([]OrderData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } + var resp []OrderData + params := url.Values{} + if marketID != "" { + params.Set("marketId", marketID) + } + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + if status != "" { + params.Set("status", status) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsOrders, params), nil, &resp) +} + +// CancelAllOpenOrdersByPairs cancels all open orders unless pairs are specified +func (b *BTCMarkets) CancelAllOpenOrdersByPairs(marketIDs []string) ([]CancelOrderResp, error) { + var resp []CancelOrderResp + req := make(map[string]interface{}) + if len(marketIDs) > 0 { + var strTemp strings.Builder + for x := range marketIDs { + strTemp.WriteString("marketId=" + marketIDs[x] + "&") + } + req["marketId"] = strTemp.String()[:strTemp.Len()-1] + } + return resp, b.SendAuthenticatedRequest(http.MethodDelete, btcMarketsOrders, req, &resp) +} + +// FetchOrder finds order based on the provided id +func (b *BTCMarkets) FetchOrder(id string) (OrderData, error) { + var resp OrderData + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsOrders+"/"+id, + nil, &resp) +} + +// RemoveOrder removes a given order +func (b *BTCMarkets) RemoveOrder(id string) (CancelOrderResp, error) { + var resp CancelOrderResp + return resp, b.SendAuthenticatedRequest(http.MethodDelete, btcMarketsOrders+"/"+id, + nil, &resp) +} + +// ListWithdrawals lists the withdrawal history +func (b *BTCMarkets) ListWithdrawals(before, after, limit int64) ([]TransferData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } + var resp []TransferData + params := url.Values{} + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsWithdrawals, params), + nil, + &resp) +} + +// GetWithdrawal gets withdrawawl info for a given id +func (b *BTCMarkets) GetWithdrawal(id string) (TransferData, error) { + var resp TransferData + if id == "" { + return resp, errors.New("id cannot be an empty string") + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsWithdrawals+"/"+id, + nil, &resp) +} + +// ListDeposits lists the deposit history +func (b *BTCMarkets) ListDeposits(before, after, limit int64) ([]TransferData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } + var resp []TransferData + params := url.Values{} + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsDeposits, params), + nil, + &resp) +} + +// GetDeposit gets deposit info for a given ID +func (b *BTCMarkets) GetDeposit(id string) (TransferData, error) { + var resp TransferData + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsDeposits+"/"+id, + nil, &resp) +} + +// ListTransfers lists the past asset transfers +func (b *BTCMarkets) ListTransfers(before, after, limit int64) ([]TransferData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } + var resp []TransferData + params := url.Values{} + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsTransfers, params), + nil, + &resp) +} + +// GetTransfer gets asset transfer info for a given ID +func (b *BTCMarkets) GetTransfer(id string) (TransferData, error) { + var resp TransferData + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsTransfers+"/"+id, + nil, &resp) +} + +// FetchDepositAddress gets deposit address for the given asset +func (b *BTCMarkets) FetchDepositAddress(assetName string, before, after, limit int64) (DepositAddress, error) { + var resp DepositAddress + if (before > 0) && (after >= 0) { + return resp, errors.New("BTCMarkets only supports either before or after, not both") + } + params := url.Values{} + params.Set("assetName", assetName) + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsAddresses, params), + nil, + &resp) +} + +// GetWithdrawalFees gets withdrawal fees for all assets +func (b *BTCMarkets) GetWithdrawalFees() ([]WithdrawalFeeData, error) { + var resp []WithdrawalFeeData + return resp, b.SendHTTPRequest(btcMarketsAPIURL+btcMarketsAPIVersion+btcMarketsWithdrawalFees, + &resp) +} + +// ListAssets lists all available assets +func (b *BTCMarkets) ListAssets() ([]AssetData, error) { + var resp []AssetData + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsAssets, nil, &resp) +} + +// GetTransactions gets trading fees +func (b *BTCMarkets) GetTransactions(assetName string, before, after, limit int64) ([]TransactionData, error) { + if (before > 0) && (after >= 0) { + return nil, errors.New("BTCMarkets only supports either before or after, not both") + } + var resp []TransactionData + params := url.Values{} + if assetName != "" { + params.Set("assetName", assetName) + } + if before > 0 { + params.Set("before", strconv.FormatInt(before, 10)) + } + if after >= 0 { + params.Set("after", strconv.FormatInt(after, 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + return resp, b.SendAuthenticatedRequest(http.MethodGet, + common.EncodeURLValues(btcMarketsTransactions, params), + nil, + &resp) +} + +// CreateNewReport creates a new report +func (b *BTCMarkets) CreateNewReport(reportType, format string) (CreateReportResp, error) { + var resp CreateReportResp + req := make(map[string]interface{}) + req["type"] = reportType + req["format"] = format + return resp, b.SendAuthenticatedRequest(http.MethodPost, btcMarketsReports, req, &resp) +} + +// GetReport finds details bout a past report +func (b *BTCMarkets) GetReport(reportID string) (ReportData, error) { + var resp ReportData + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsReports+"/"+reportID, + nil, &resp) +} + +// RequestWithdraw requests withdrawals +func (b *BTCMarkets) RequestWithdraw(assetName string, amount float64, + toAddress, accountName, accountNumber, bsbNumber, bankName string) (TransferData, error) { + var resp TransferData + req := make(map[string]interface{}) + req["assetName"] = assetName + req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) + if assetName != "AUD" { + req["toAddress"] = toAddress + } else { + if accountName != "" { + req["accountName"] = accountName + } + if accountNumber != "" { + req["accountNumber"] = accountNumber + } + if bsbNumber != "" { + req["bsbNumber"] = bsbNumber + } + if bankName != "" { + req["bankName"] = bankName + } + } + return resp, b.SendAuthenticatedRequest(http.MethodPost, btcMarketsWithdrawals, req, &resp) +} + +// BatchPlaceCancelOrders places and cancels batch orders +func (b *BTCMarkets) BatchPlaceCancelOrders(cancelOrders []CancelBatch, placeOrders []PlaceBatch) (BatchPlaceCancelResponse, error) { + var resp BatchPlaceCancelResponse + var orderRequests []interface{} + if len(cancelOrders)+len(placeOrders) > 4 { + return resp, errors.New("BTCMarkets can only handle 4 orders at a time") + } + for x := range cancelOrders { + orderRequests = append(orderRequests, CancelOrderMethod{CancelOrder: cancelOrders[x]}) + } + for y := range placeOrders { + if placeOrders[y].ClientOrderID == "" { + return resp, errors.New("placeorders must have clientorderids filled") + } + orderRequests = append(orderRequests, PlaceOrderMethod{PlaceOrder: placeOrders[y]}) + } + return resp, b.SendAuthenticatedRequest(http.MethodPost, btcMarketsBatchOrders, orderRequests, &resp) +} + +// GetBatchTrades gets batch trades +func (b *BTCMarkets) GetBatchTrades(ids []string) (BatchTradeResponse, error) { + var resp BatchTradeResponse + if len(ids) > 50 { + return resp, errors.New("batchtrades can only handle 50 ids at a time") + } + marketIDs := strings.Join(ids, ",") + return resp, b.SendAuthenticatedRequest(http.MethodGet, btcMarketsBatchOrders+"/"+marketIDs, + nil, &resp) +} + +// CancelBatchOrders cancels given ids +func (b *BTCMarkets) CancelBatchOrders(ids []string) (BatchCancelResponse, error) { + var resp BatchCancelResponse + marketIDs := strings.Join(ids, ",") + return resp, b.SendAuthenticatedRequest(http.MethodDelete, btcMarketsBatchOrders+"/"+marketIDs, + nil, &resp) } // SendHTTPRequest sends an unauthenticated HTTP request @@ -380,52 +674,47 @@ func (b *BTCMarkets) SendHTTPRequest(path string, result interface{}) error { } // SendAuthenticatedRequest sends an authenticated HTTP request -func (b *BTCMarkets) SendAuthenticatedRequest(reqType, path string, data, result interface{}) (err error) { +func (b *BTCMarkets) SendAuthenticatedRequest(method, path string, data, result interface{}) (err error) { if !b.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } - n := b.Requester.GetNonce(true).String()[0:13] + strTime := strconv.FormatInt(time.Now().UTC().UnixNano()/1000000, 10) - var req string - payload := []byte("") - - if data != nil { + var body io.Reader + var payload, hmac []byte + switch data.(type) { + case map[string]interface{}, []interface{}: payload, err = json.Marshal(data) if err != nil { return err } - req = path + "\n" + n + "\n" + string(payload) - } else { - req = path + "\n" + n + "\n" - } - - hmac := crypto.GetHMAC(crypto.HashSHA512, - []byte(req), []byte(b.API.Credentials.Secret)) - - if b.Verbose { - log.Debugf(log.ExchangeSys, "Sending %s request to URL %s with params %s\n", - reqType, - b.API.Endpoints.URL+path, - req) + body = bytes.NewBuffer(payload) + strMsg := method + btcMarketsAPIVersion + path + strTime + string(payload) + hmac = crypto.GetHMAC(crypto.HashSHA512, + []byte(strMsg), []byte(b.API.Credentials.Secret)) + default: + strArray := strings.Split(path, "?") + hmac = crypto.GetHMAC(crypto.HashSHA512, + []byte(method+btcMarketsAPIVersion+strArray[0]+strTime), + []byte(b.API.Credentials.Secret)) } headers := make(map[string]string) headers["Accept"] = "application/json" headers["Accept-Charset"] = "UTF-8" headers["Content-Type"] = "application/json" - headers["apikey"] = b.API.Credentials.Key - headers["timestamp"] = n - headers["signature"] = crypto.Base64Encode(hmac) - - return b.SendPayload(reqType, - b.API.Endpoints.URL+path, + headers["BM-AUTH-APIKEY"] = b.API.Credentials.Key + headers["BM-AUTH-TIMESTAMP"] = strTime + headers["BM-AUTH-SIGNATURE"] = crypto.Base64Encode(hmac) + return b.SendPayload(method, + btcMarketsAPIURL+btcMarketsAPIVersion+path, headers, - bytes.NewBuffer(payload), + body, result, true, - true, + false, b.Verbose, b.HTTPDebugging, b.HTTPRecording) @@ -437,22 +726,33 @@ func (b *BTCMarkets) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { switch feeBuilder.FeeType { case exchange.CryptocurrencyTradeFee: - tradingFee, err := b.GetTradingFee(feeBuilder.Pair.Base, - feeBuilder.Pair.Quote) + temp, err := b.GetTradingFees() if err != nil { - return 0, err + return fee, err + } + for x := range temp.FeeByMarkets { + if currency.NewPairFromString(temp.FeeByMarkets[x].MarketID) == feeBuilder.Pair { + fee = temp.FeeByMarkets[x].MakerFeeRate + if !feeBuilder.IsMaker { + fee = temp.FeeByMarkets[x].TakerFeeRate + } + } } - - fee = calculateTradingFee(tradingFee, - feeBuilder.PurchasePrice, - feeBuilder.Amount) - case exchange.CryptocurrencyWithdrawalFee: - fee = getCryptocurrencyWithdrawalFee(feeBuilder.Pair.Base) + temp, err := b.GetWithdrawalFees() + if err != nil { + return fee, err + } + for x := range temp { + if currency.NewCode(temp[x].AssetName) == feeBuilder.Pair.Base { + fee = temp[x].Fee * feeBuilder.PurchasePrice * feeBuilder.Amount + } + } case exchange.InternationalBankWithdrawalFee: - fee = getInternationalBankWithdrawalFee(feeBuilder.FiatCurrency) + return 0, errors.New("international bank withdrawals are not supported") + case exchange.OfflineTradeFee: - fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount) + fee = getOfflineTradeFee(feeBuilder) } if fee < 0 { fee = 0 @@ -461,24 +761,11 @@ func (b *BTCMarkets) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { } // getOfflineTradeFee calculates the worst case-scenario trading fee -func getOfflineTradeFee(price, amount float64) float64 { - return 0.0085 * price * amount -} - -func calculateTradingFee(tradingFee TradingFee, purchasePrice, amount float64) (fee float64) { - fee = tradingFee.TradingFeeRate / 100000000 - return fee * amount * purchasePrice -} - -func getCryptocurrencyWithdrawalFee(c currency.Code) float64 { - return WithdrawalFees[c] -} - -func getInternationalBankWithdrawalFee(c currency.Code) float64 { - var fee float64 - - if c == currency.AUD { - fee = 0 +func getOfflineTradeFee(feeBuilder *exchange.FeeBuilder) float64 { + switch { + case feeBuilder.Pair.IsCryptoPair(): + return 0.002 * feeBuilder.PurchasePrice * feeBuilder.Amount + default: + return 0.0085 * feeBuilder.PurchasePrice * feeBuilder.Amount } - return fee } diff --git a/exchanges/btcmarkets/btcmarkets_test.go b/exchanges/btcmarkets/btcmarkets_test.go index 4fca1c10..f7e669cb 100644 --- a/exchanges/btcmarkets/btcmarkets_test.go +++ b/exchanges/btcmarkets/btcmarkets_test.go @@ -1,13 +1,13 @@ package btcmarkets import ( - "net/url" + "log" + "os" "testing" - "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" - exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) @@ -18,21 +18,23 @@ const ( apiKey = "" apiSecret = "" canManipulateRealOrders = false + BTCAUD = "BTC-AUD" + LTCAUD = "LTC-AUD" + ETHAUD = "ETH-AUD" + fakePair = "Fake-USDT" + bid = "bid" ) -func TestSetDefaults(t *testing.T) { +func TestMain(m *testing.M) { b.SetDefaults() -} - -func TestSetup(t *testing.T) { cfg := config.GetConfig() err := cfg.LoadConfig("../../testdata/configtest.json", true) if err != nil { - t.Fatal("BTC Markets load config error", err) + log.Fatal(err) } bConfig, err := cfg.GetExchangeConfig("BTC Markets") if err != nil { - t.Error("BTC Markets Setup() init error") + log.Fatal(err) } bConfig.API.Credentials.Key = apiKey bConfig.API.Credentials.Secret = apiSecret @@ -40,29 +42,27 @@ func TestSetup(t *testing.T) { err = b.Setup(bConfig) if err != nil { - t.Fatal("BTC Markets setup error", err) + log.Fatal(err) } + + os.Exit(m.Run()) +} + +func areTestAPIKeysSet() bool { + return b.AllowAuthenticatedRequest() } func TestGetMarkets(t *testing.T) { t.Parallel() _, err := b.GetMarkets() if err != nil { - t.Error("GetMarkets() error", err) + t.Error("GetTicker() error", err) } } func TestGetTicker(t *testing.T) { t.Parallel() - _, err := b.GetTicker("BTC", "AUD") - if err != nil { - t.Error("GetTicker() error", err) - } -} - -func TestGetOrderbook(t *testing.T) { - t.Parallel() - _, err := b.GetOrderbook("BTC", "AUD") + _, err := b.GetTicker(BTCAUD) if err != nil { t.Error("GetOrderbook() error", err) } @@ -70,437 +70,412 @@ func TestGetOrderbook(t *testing.T) { func TestGetTrades(t *testing.T) { t.Parallel() - _, err := b.GetTrades("BTC", "AUD", nil) - if err != nil { - t.Error("GetTrades() error", err) - } - - val := url.Values{} - val.Set("since", "0") - _, err = b.GetTrades("BTC", "AUD", val) + _, err := b.GetTrades(BTCAUD, 0, 0, 5) if err != nil { t.Error("GetTrades() error", err) } } -func TestNewOrder(t *testing.T) { +func TestGetOrderbook(t *testing.T) { t.Parallel() - _, err := b.NewOrder("AUD", "BTC", 0, 0, "Bid", - order.Limit.Lower(), "testTest") - if err == nil { - t.Error("NewOrder() Expected error") + _, err := b.GetOrderbook(BTCAUD, 2) + if err != nil { + t.Error("GetTrades() error", err) } } -func TestCancelExistingOrder(t *testing.T) { +func TestGetMarketCandles(t *testing.T) { t.Parallel() - _, err := b.CancelExistingOrder([]int64{1337}) - if err == nil { - t.Error("CancelExistingOrder() Expected error") + _, err := b.GetMarketCandles(BTCAUD, "", "", "", 0, 0, 5) + if err != nil { + t.Error(err) } } -func TestGetOrders(t *testing.T) { +func TestGetTickers(t *testing.T) { t.Parallel() - _, err := b.GetOrders("AUD", "BTC", 10, 0, false) - if err == nil { - t.Error("GetOrders() Expected error") - } - _, err = b.GetOrders("AUD", "BTC", 10, 0, true) - if err == nil { - t.Error("GetOrders() Expected error") + temp := []string{BTCAUD, LTCAUD, ETHAUD} + _, err := b.GetTickers(temp) + if err != nil { + t.Error(err) } } -func TestGetOrderDetail(t *testing.T) { +func TestGetMultipleOrderbooks(t *testing.T) { t.Parallel() - _, err := b.GetOrderDetail([]int64{1337}) - if err == nil { - t.Error("GetOrderDetail() Expected error") + temp := []string{BTCAUD, LTCAUD, ETHAUD} + _, err := b.GetMultipleOrderbooks(temp) + if err != nil { + t.Error(err) + } +} + +func TestGetServerTime(t *testing.T) { + t.Parallel() + _, err := b.GetServerTime() + if err != nil { + t.Error(err) } } func TestGetAccountBalance(t *testing.T) { t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } _, err := b.GetAccountBalance() - if err == nil { - t.Error("GetAccountBalance() Expected error") + if err != nil { + t.Error(err) } } -func TestWithdrawCrypto(t *testing.T) { +func TestGetTradingFees(t *testing.T) { t.Parallel() - _, err := b.WithdrawCrypto(0, "BTC", "LOLOLOL") - if err == nil { - t.Error("WithdrawCrypto() Expected error") + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetTradingFees() + if err != nil { + t.Error(err) } } -func TestWithdrawAUD(t *testing.T) { +func TestGetTradeHistory(t *testing.T) { t.Parallel() - _, err := b.WithdrawAUD("BLA", "1337", "blawest", "1336", 10000000) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetTradeHistory(ETHAUD, "", -1, -1, -1) + if err != nil { + t.Error(err) + } + _, err = b.GetTradeHistory(BTCAUD, "", -1, -1, 1) + if err != nil { + t.Error(err) + } + _, err = b.GetTradeHistory(fakePair, "", -1, -1, -1) if err == nil { - t.Error("WithdrawAUD() Expected error") + t.Error("expected an error due to invalid trading pair") + } +} + +func TestGetTradeByID(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetTradeByID("4712043732") + if err != nil { + t.Error(err) + } +} + +func TestNewOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + _, err := b.NewOrder(BTCAUD, 100, 1, limit, bid, 0, 0, "", true, "", "") + if err != nil { + t.Error(err) + } + _, err = b.NewOrder(BTCAUD, 100, 1, "invalid", bid, 0, 0, "", true, "", "") + if err == nil { + t.Error("expected an error due to invalid ordertype") + } + _, err = b.NewOrder(BTCAUD, 100, 1, limit, "invalid", 0, 0, "", true, "", "") + if err == nil { + t.Error("expected an error due to invalid orderside") + } +} + +func TestGetOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetOrders("", -1, -1, 2, "") + if err != nil { + t.Error(err) + } + _, err = b.GetOrders(LTCAUD, -1, -1, -1, "open") + if err != nil { + t.Error(err) + } +} + +func TestCancelOpenOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + temp := []string{BTCAUD, LTCAUD} + _, err := b.CancelAllOpenOrdersByPairs(temp) + if err != nil { + t.Error(err) + } + temp = []string{BTCAUD, fakePair} + _, err = b.CancelAllOpenOrdersByPairs(temp) + if err == nil { + t.Error("expected an error due to invalid marketID") + } +} + +func TestFetchOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.FetchOrder("4477045999") + if err != nil { + t.Error(err) + } + _, err = b.FetchOrder("696969") + if err == nil { + t.Error(err) + } +} + +func TestRemoveOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + _, err := b.RemoveOrder("") + if err != nil { + t.Error(err) + } +} + +func TestListWithdrawals(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.ListWithdrawals(-1, -1, -1) + if err != nil { + t.Error(err) + } +} + +func TestGetWithdrawal(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetWithdrawal("4477381751") + if err != nil { + t.Error(err) + } +} + +func TestListDeposits(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.ListDeposits(-1, -1, -1) + if err != nil { + t.Error(err) + } +} + +func TestGetDeposit(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetDeposit("4476769607") + if err != nil { + t.Error(err) + } +} + +func TestListTransfers(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.ListTransfers(-1, -1, -1) + if err != nil { + t.Error(err) + } +} + +func TestGetTransfer(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetTransfer("4476769607") + if err != nil { + t.Error(err) + } + _, err = b.GetTransfer("6969696") + if err == nil { + t.Error("expected an error due to invalid transferID") + } +} + +func TestFetchDepositAddress(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.FetchDepositAddress("LTC", -1, -1, -1) + if err != nil { + t.Error(err) + } + _, err = b.FetchDepositAddress(fakePair, -1, -1, -1) + if err != nil { + t.Error("expected an error due to invalid assetID") + } +} + +func TestGetWithdrawalFees(t *testing.T) { + t.Parallel() + _, err := b.GetWithdrawalFees() + if err != nil { + t.Error(err) + } +} + +func TestListAssets(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.ListAssets() + if err != nil { + t.Error(err) + } +} + +func TestGetTransactions(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetTransactions("", -1, -1, -1) + if err != nil { + t.Error(err) + } +} + +func TestCreateNewReport(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.CreateNewReport("TransactionReport", "json") + if err != nil { + t.Error(err) + } +} + +func TestGetReport(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := b.GetReport("1kv38epne5v7lek9f18m60idg6") + if err != nil { + t.Error(err) + } +} + +func TestRequestWithdaw(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + _, err := b.RequestWithdraw("BTC", 1, "sdjflajdslfjld", "", "", "", "") + if err == nil { + t.Error("expected an error due to invalid toAddress") + } +} + +func TestBatchPlaceCancelOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + var temp []PlaceBatch + o := PlaceBatch{ + MarketID: BTCAUD, + Amount: 11000, + Price: 1, + OrderType: order.Limit.String(), + Side: bid, + } + _, err := b.BatchPlaceCancelOrders(nil, append(temp, o)) + if err != nil { + t.Error(err) + } +} + +func TestGetBatchTrades(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + temp := []string{"4477045999", "4477381751", "4476769607"} + _, err := b.GetBatchTrades(temp) + if err != nil { + t.Error(err) + } +} + +func TestCancelBatchOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + temp := []string{"4477045999", "4477381751", "4477381751"} + _, err := b.CancelBatchOrders(temp) + if err != nil { + t.Error(err) } } func TestGetAccountInfo(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } _, err := b.GetAccountInfo() - if err == nil { - t.Error("GetAccountInfo() Expected error") - } -} - -func TestGetFundingHistory(t *testing.T) { - _, err := b.GetFundingHistory() - if err == nil { - t.Error("GetAccountInfo() Expected error") - } -} - -func TestCancelOrder(t *testing.T) { - _, err := b.CancelExistingOrder([]int64{1337}) - - if err == nil { - t.Error("CancelgOrder() Expected error") - } -} - -func TestGetOrderInfo(t *testing.T) { - _, err := b.GetOrderInfo("1337") - if err == nil { - t.Error("GetOrderInfo() Expected error") - } -} - -func setFeeBuilder() *exchange.FeeBuilder { - return &exchange.FeeBuilder{ - Amount: 1, - FeeType: exchange.CryptocurrencyTradeFee, - Pair: currency.NewPair(currency.BTC, currency.LTC), - PurchasePrice: 1, - } -} - -// TestGetFeeByTypeOfflineTradeFee logic test -func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { - var feeBuilder = setFeeBuilder() - b.GetFeeByType(feeBuilder) - if apiKey == "" || apiSecret == "" { - if feeBuilder.FeeType != exchange.OfflineTradeFee { - t.Errorf("Expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) - } - } else { - if feeBuilder.FeeType != exchange.CryptocurrencyTradeFee { - t.Errorf("Expected %v, received %v", exchange.CryptocurrencyTradeFee, feeBuilder.FeeType) - } - } -} - -func TestGetFee(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var feeBuilder = setFeeBuilder() - - if apiKey != "" || apiSecret != "" { - // CryptocurrencyTradeFee Fiat - feeBuilder = setFeeBuilder() - feeBuilder.Pair.Quote = currency.USD - if resp, err := b.GetFee(feeBuilder); resp != float64(0.00849999) || err != nil { - t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.00849999), resp) - } - - // CryptocurrencyTradeFee Basic - feeBuilder = setFeeBuilder() - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0022) || err != nil { - t.Error(err) - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0022), resp) - } - - // CryptocurrencyTradeFee High quantity - feeBuilder = setFeeBuilder() - feeBuilder.Amount = 1000 - feeBuilder.PurchasePrice = 1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(2200) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(22000), resp) - t.Error(err) - } - - // CryptocurrencyTradeFee IsMaker - feeBuilder = setFeeBuilder() - feeBuilder.IsMaker = true - if resp, err := b.GetFee(feeBuilder); resp != float64(0.0022) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.0022), resp) - t.Error(err) - } - - // CryptocurrencyTradeFee Negative purchase price - feeBuilder = setFeeBuilder() - feeBuilder.PurchasePrice = -1000 - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) - t.Error(err) - } - } - - // CryptocurrencyWithdrawalFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0.001) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) + if err != nil { t.Error(err) } - - // CyptocurrencyDepositFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.CyptocurrencyDepositFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) - t.Error(err) - } - - // InternationalBankDepositFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.InternationalBankDepositFee - feeBuilder.FiatCurrency = currency.AUD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) - t.Error(err) - } - - // InternationalBankWithdrawalFee Basic - feeBuilder = setFeeBuilder() - feeBuilder.FeeType = exchange.InternationalBankWithdrawalFee - feeBuilder.FiatCurrency = currency.AUD - if resp, err := b.GetFee(feeBuilder); resp != float64(0) || err != nil { - t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp) - t.Error(err) - } -} - -func TestFormatWithdrawPermissions(t *testing.T) { - b.SetDefaults() - expectedResult := exchange.AutoWithdrawCryptoText + " & " + exchange.AutoWithdrawFiatText - - withdrawPermissions := b.FormatWithdrawPermissions() - - if withdrawPermissions != expectedResult { - t.Errorf("Expected: %s, Received: %s", expectedResult, withdrawPermissions) - } -} - -func TestGetActiveOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - } - - _, err := b.GetActiveOrders(&getOrdersRequest) - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not get open orders: %s", err) - } else if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } } func TestGetOrderHistory(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - var getOrdersRequest = order.GetOrdersRequest{ - OrderType: order.AnyType, - Currencies: []currency.Pair{currency.NewPair(currency.LTC, - currency.BTC)}, + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") } - - _, err := b.GetOrderHistory(&getOrdersRequest) - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not get order history: %s", err) - } else if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") + var input order.GetOrdersRequest + input.OrderSide = order.Buy + _, err := b.GetOrderHistory(&input) + if err != nil { + t.Error(err) } } -// Any tests below this line have the ability to impact your orders on the exchange. Enable canManipulateRealOrders to run them -// ---------------------------------------------------------------------------------------------------------------------------- -func areTestAPIKeysSet() bool { - return b.ValidateAPICredentials() -} - -func TestSubmitOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - var orderSubmission = &order.Submit{ - Pair: currency.Pair{ - Delimiter: "-", - Base: currency.BTC, - Quote: currency.LTC, - }, - OrderSide: order.Buy, - OrderType: order.Limit, - Price: 1, - Amount: 1, - ClientID: "meowOrder", - } - response, err := b.SubmitOrder(orderSubmission) - if areTestAPIKeysSet() && (err != nil || !response.IsOrderPlaced) { - t.Errorf("Order failed to be placed: %v", err) - } else if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") +func TestUpdateOrderbook(t *testing.T) { + t.Parallel() + cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.AUD.String(), "-") + _, err := b.UpdateOrderbook(cp, asset.Spot) + if err != nil { + t.Error(err) } } -func TestCancelExchangeOrder(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &order.Cancel{ - OrderID: "1", - WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - AccountID: "1", - CurrencyPair: currencyPair, - } - - err := b.CancelOrder(orderCancellation) - if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not cancel orders: %v", err) - } -} - -func TestCancelAllExchangeOrders(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - currencyPair := currency.NewPair(currency.LTC, currency.BTC) - - var orderCancellation = &order.Cancel{ - OrderID: "1", - WalletAddress: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - AccountID: "1", - CurrencyPair: currencyPair, - } - - resp, err := b.CancelAllOrders(orderCancellation) - - if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } - if areTestAPIKeysSet() && err != nil { - t.Errorf("Could not cancel orders: %v", err) - } - - if len(resp.Status) > 0 { - t.Errorf("%v orders failed to cancel", len(resp.Status)) - } -} - -func TestModifyOrder(t *testing.T) { - _, err := b.ModifyOrder(&order.Modify{}) - if err == nil { - t.Error("ModifyOrder() Expected error") - } -} - -func TestWithdraw(t *testing.T) { - b.SetDefaults() - TestSetup(t) - withdrawCryptoRequest := exchange.CryptoWithdrawRequest{ - GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ - Amount: -1, - Currency: currency.BTC, - Description: "WITHDRAW IT ALL", - }, - Address: "1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB", - } - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - _, err := b.WithdrawCryptocurrencyFunds(&withdrawCryptoRequest) - if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } - if areTestAPIKeysSet() && err != nil { - t.Errorf("Withdraw failed to be placed: %v", err) - } -} - -func TestWithdrawFiat(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - var withdrawFiatRequest = exchange.FiatWithdrawRequest{ - GenericWithdrawRequestInfo: exchange.GenericWithdrawRequestInfo{ - Amount: -1, - Currency: currency.USD, - Description: "WITHDRAW IT ALL", - }, - BankAccountName: "Satoshi Nakamoto", - BankAccountNumber: 12345, - BankAddress: "123 Fake St", - BankCity: "Tarry Town", - BankCountry: "Hyrule", - BankName: "Commonwealth Bank of Australia", - WireCurrency: currency.AUD.String(), - SwiftCode: "Taylor", - RequiresIntermediaryBank: false, - IsExpressWire: false, - } - - _, err := b.WithdrawFiatFunds(&withdrawFiatRequest) - if !areTestAPIKeysSet() && err == nil { - t.Error("Expecting an error when no keys are set") - } - if areTestAPIKeysSet() && err != nil { - t.Errorf("Withdraw failed to be placed: %v", err) - } -} - -func TestWithdrawInternationalBank(t *testing.T) { - b.SetDefaults() - TestSetup(t) - - if areTestAPIKeysSet() && !canManipulateRealOrders { - t.Skip("API keys set, canManipulateRealOrders false, skipping test") - } - - var withdrawFiatRequest = exchange.FiatWithdrawRequest{} - _, err := b.WithdrawFiatFundsToInternationalBank(&withdrawFiatRequest) - if err != common.ErrFunctionNotSupported { - t.Errorf("Expected '%v', received: '%v'", common.ErrFunctionNotSupported, err) - } -} - -func TestGetDepositAddress(t *testing.T) { - _, err := b.GetDepositAddress(currency.BTC, "") - if err == nil { - t.Error("GetDepositAddress() error cannot be nil") +func TestUpdateTicker(t *testing.T) { + t.Parallel() + cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.AUD.String(), "-") + _, err := b.UpdateTicker(cp, asset.Spot) + if err != nil { + t.Error(err) } } diff --git a/exchanges/btcmarkets/btcmarkets_types.go b/exchanges/btcmarkets/btcmarkets_types.go index 7cccb8b2..64730e9a 100644 --- a/exchanges/btcmarkets/btcmarkets_types.go +++ b/exchanges/btcmarkets/btcmarkets_types.go @@ -1,66 +1,74 @@ package btcmarkets -import ( - "time" - - "github.com/thrasher-corp/gocryptotrader/currency" -) - -// Response is the genralized response type -type Response struct { - Success bool `json:"success"` - ErrorCode int `json:"errorCode"` - ErrorMessage string `json:"errorMessage"` - ID int `json:"id"` - Responses []ResponseDetails `json:"responses"` - ClientRequestID string `json:"clientRequestId"` - Orders []Order `json:"orders"` - Status string `json:"status"` -} - -// ResponseDetails holds order status details -type ResponseDetails struct { - Success bool `json:"success"` - ErrorCode int `json:"errorCode"` - ErrorMessage string `json:"errorMessage"` - ID int64 `json:"id"` -} +import "time" // Market holds a tradable market instrument type Market struct { - Instrument string `json:"instrument"` - Currency string `json:"currency"` + MarketID string `json:"marketId"` + BaseAsset string `json:"baseAsset"` + QuoteAsset string `json:"quoteAsset"` + MinOrderAmount float64 `json:"minOrderAmount,string"` + MaxOrderAmount float64 `json:"maxOrderAmount,string"` + AmountDecimals int64 `json:"amountDecimals,string"` + PriceDecimals int64 `json:"priceDecimals,string"` } // Ticker holds ticker information type Ticker struct { - BestAsk float64 `json:"bestAsk"` - BestBid float64 `json:"bestBid"` - Currency currency.Code `json:"currency"` - High24h float64 `json:"high24h"` - Instrument currency.Pair `json:"instrument"` - LastPrice float64 `json:"lastPrice"` - Low24h float64 `json:"low24h"` - Price24h float64 `json:"price24h"` - Timestamp int64 `json:"timestamp"` - Volume24h float64 `json:"volume24h"` -} - -// Orderbook holds current orderbook information returned from the exchange -type Orderbook struct { - Currency string `json:"currency"` - Instrument string `json:"instrument"` - Timestamp int64 `json:"timestamp"` - Asks [][]float64 `json:"asks"` - Bids [][]float64 `json:"bids"` + MarketID string `json:"marketId"` + BestBID float64 `json:"bestBid,string"` + BestAsk float64 `json:"bestAsk,string"` + LastPrice float64 `json:"lastPrice,string"` + Volume float64 `json:"volume24h,string"` + Change24h float64 `json:"price24h,string"` + Low24h float64 `json:"low24h,string"` + High24h float64 `json:"high24h,string"` + Timestamp time.Time `json:"timestamp"` } // Trade holds trade information type Trade struct { - TradeID int64 `json:"tid"` - Amount float64 `json:"amount"` - Price float64 `json:"price"` - Date int64 `json:"date"` + TradeID string `json:"id"` + Amount float64 `json:"amount,string"` + Price float64 `json:"price,string"` + Timestamp time.Time `json:"timestamp"` +} + +// tempOrderbook stores orderbook data +type tempOrderbook struct { + MarketID string `json:"marketId"` + SnapshotID int64 `json:"snapshotId"` + Asks [][2]string `json:"asks"` + Bids [][2]string `json:"bids"` +} + +// OBData stores orderbook data +type OBData struct { + Price float64 + Volume float64 +} + +// Orderbook holds current orderbook information returned from the exchange +type Orderbook struct { + MarketID string + SnapshotID int64 + Asks []OBData + Bids []OBData +} + +// MarketCandle stores candle data for a given pair +type MarketCandle struct { + Time time.Time + Open float64 + Close float64 + Low float64 + High float64 + Volume float64 +} + +// TimeResp stores server time +type TimeResp struct { + Time time.Time `json:"timestamp"` } // TradingFee 30 day trade volume @@ -90,7 +98,7 @@ type Order struct { Instrument string `json:"instrument"` OrderSide string `json:"orderSide"` OrderType string `json:"ordertype"` - CreationTime float64 `json:"creationTime"` + CreationTime time.Time `json:"creationTime"` Status string `json:"status"` ErrorMessage string `json:"errorMessage"` Price float64 `json:"price"` @@ -102,19 +110,164 @@ type Order struct { // TradeResponse holds trade information type TradeResponse struct { - ID int64 `json:"id"` - CreationTime float64 `json:"creationTime"` - Description string `json:"description"` - Price float64 `json:"price"` - Volume float64 `json:"volume"` - Fee float64 `json:"fee"` + ID int64 `json:"id"` + CreationTime time.Time `json:"creationTime"` + Description string `json:"description"` + Price float64 `json:"price"` + Volume float64 `json:"volume"` + Fee float64 `json:"fee"` } -// AccountBalance holds account balance details -type AccountBalance struct { - Balance float64 `json:"balance"` - PendingFunds float64 `json:"pendingFunds"` - Currency string `json:"currency"` +// AccountData stores account data +type AccountData struct { + AssetName string `json:"assetName"` + Balance float64 `json:"balance,string"` + Available float64 `json:"available,string"` + Locked float64 `json:"locked,string"` +} + +// TradeHistoryData stores data of past trades +type TradeHistoryData struct { + ID string `json:"id"` + MarketID string `json:"marketId"` + Timestamp time.Time `json:"timestamp"` + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + Side string `json:"side"` + Fee float64 `json:"fee,string"` + OrderID string `json:"orderId"` + LiquidityType string `json:"liquidityType"` +} + +// OrderData stores data for new order created +type OrderData struct { + OrderID string `json:"orderId"` + MarketID string `json:"marketId"` + Side string `json:"side"` + Type string `json:"type"` + CreationTime time.Time `json:"creationTime"` + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + OpenAmount float64 `json:"openAmount,string"` + Status string `json:"status"` +} + +// CancelOrderResp stores data for cancelled orders +type CancelOrderResp struct { + OrderID string `json:"orderId"` + ClientOrderID string `json:"clientOrderId"` +} + +// PaymentDetails stores payment address +type PaymentDetails struct { + Address string `json:"address"` +} + +// TransferData stores data from asset transfers +type TransferData struct { + ID string `json:"id"` + AssetName string `json:"assetName"` + Amount float64 `json:"amount,string"` + RequestType string `json:"type"` + CreationTime time.Time `json:"creationTime"` + Status string `json:"status"` + Description string `json:"description"` + Fee float64 `json:"fee,string"` + LastUpdate string `json:"lastUpdate"` + PaymentDetails PaymentDetails `json:"paymentDetail"` +} + +// DepositAddress stores deposit address data +type DepositAddress struct { + Address string `json:"address"` + AssetName string `json:"assetName"` +} + +// WithdrawalFeeData stores data for fees +type WithdrawalFeeData struct { + AssetName string `json:"assetName"` + Fee float64 `json:"fee,string"` +} + +// AssetData stores data for given asset +type AssetData struct { + AssetName string `json:"assetName"` + MinDepositAmount float64 `json:"minDepositAmount,string"` + MaxDepositAmount float64 `json:"maxDepositAmount,string"` + DepositDecimals float64 `json:"depositDecimals,string"` + MinWithdrawalAmount float64 `json:"minWithdrawalAmount,string"` + MaxWithdrawalAmount float64 `json:"maxWithdrawalAmount,string"` + WithdrawalDecimals float64 `json:"withdrawalDecimals,string"` + WithdrawalFee float64 `json:"withdrawalFee,string"` + DepositFee float64 `json:"depositFee,string"` +} + +// TransactionData stores data from past transactions +type TransactionData struct { + ID string `json:"id"` + CreationTime time.Time `json:"creationTime"` + Description string `json:"description"` + AssetName string `json:"assetName"` + Amount float64 `json:"amount,string"` + Balance float64 `json:"balance,string"` + FeeType string `json:"type"` + RecordType string `json:"recordType"` + ReferrenceID string `json:"referrenceId"` +} + +// CreateReportResp stores data for created report +type CreateReportResp struct { + ReportID string `json:"reportId"` +} + +// ReportData gets data for a created report +type ReportData struct { + ID string `json:"id"` + ContentURL string `json:"contentUrl"` + CreationTime time.Time `json:"creationTime"` + ReportType string `json:"reportType"` + Status string `json:"status"` + Format string `json:"format"` +} + +// BatchPlaceData stores data for placed batch orders +type BatchPlaceData struct { + OrderID string `json:"orderId"` + MarketID string `json:"marketId"` + Side string `json:"side"` + Type string `json:"type"` + CreationTime time.Time `json:"creationTime"` + Price float64 `json:"price,string"` + Amount float64 `json:"amount,string"` + OpenAmount float64 `json:"openAmount,string"` + Status string `json:"status"` + ClientOrderID string `json:"clientOrderId"` +} + +// UnprocessedBatchResp stores data for unprocessed response +type UnprocessedBatchResp struct { + Code string `json:"code"` + Message string `json:"message"` + RequestID string `json:"requestId"` +} + +// BatchPlaceCancelResponse stores place and cancel batch data +type BatchPlaceCancelResponse struct { + PlacedOrders []BatchPlaceData `json:"placeOrders"` + CancelledOrders []CancelOrderResp `json:"cancelOrders"` + UnprocessedOrders []UnprocessedBatchResp `json:"unprocessedRequests"` +} + +// BatchTradeResponse stores the trades from batchtrades +type BatchTradeResponse struct { + Orders []BatchPlaceData `json:"orders"` + UnprocessedRequests []UnprocessedBatchResp `json:"unprocessedRequests"` +} + +// BatchCancelResponse stores the cancellation details from batch cancels +type BatchCancelResponse struct { + CancelOrders []CancelOrderResp `json:"cancelOrders"` + UnprocessedRequests []UnprocessedBatchResp `json:"unprocessedRequests"` } // WithdrawRequestCrypto is a generalized withdraw request type @@ -134,18 +287,48 @@ type WithdrawRequestAUD struct { BSBNumber string `json:"bsbNumber"` } -// WithdrawalFees the large list of predefined withdrawal fees -// Prone to change -var WithdrawalFees = map[currency.Code]float64{ - currency.AUD: 0, - currency.BTC: 0.001, - currency.ETH: 0.001, - currency.ETC: 0.001, - currency.LTC: 0.0001, - currency.XRP: 0.15, - currency.BCH: 0.0001, - currency.OMG: 0.15, - currency.POWR: 5, +// CancelBatch stores data for batch cancel request +type CancelBatch struct { + OrderID string `json:"orderId,omitempty"` + ClientOrderID string `json:"clientOrderId,omitempty"` +} + +// PlaceBatch stores data for place batch request +type PlaceBatch struct { + MarketID string `json:"marketId"` + Price float64 `json:"price"` + Amount float64 `json:"amount"` + OrderType string `json:"type"` + Side string `json:"side"` + TriggerPrice float64 `json:"triggerPrice,omitempty"` + TriggerAmount float64 `json:"triggerAmount,omitempty"` + TimeInForce string `json:"timeInForce,omitempty"` + PostOnly bool `json:"postOnly,omitempty"` + SelfTrade string `json:"selfTrade,omitempty"` + ClientOrderID string `json:"clientOrderId,omitempty"` +} + +// PlaceOrderMethod stores data for place request +type PlaceOrderMethod struct { + PlaceOrder PlaceBatch `json:"placeOrder,omitempty"` +} + +// CancelOrderMethod stores data for Cancel request +type CancelOrderMethod struct { + CancelOrder CancelBatch `json:"cancelOrder,omitempty"` +} + +// TradingFeeData stores trading fee data +type TradingFeeData struct { + MakerFeeRate float64 `json:"makerFeeRate,string"` + TakerFeeRate float64 `json:"takerFeeRate,string"` + MarketID string `json:"marketId"` +} + +// TradingFeeResponse stores trading fee data +type TradingFeeResponse struct { + MonthlyVolume float64 `json:"volume30Day,string"` + FeeByMarkets []TradingFeeData `json:"FeeByMarkets"` } // WsSubscribe message sent via ws to subscribe @@ -155,6 +338,16 @@ type WsSubscribe struct { MessageType string `json:"messageType"` } +// WsAuthSubscribe message sent via login to subscribe +type WsAuthSubscribe struct { + MarketIDs []string `json:"marketIds,omitempty"` + Channels []string `json:"channels"` + Key string `json:"key"` + Signature string `json:"signature"` + Timestamp string `json:"timestamp"` + MessageType string `json:"messageType"` +} + // WsMessageType message sent via ws to determine type type WsMessageType struct { MessageType string `json:"messageType"` @@ -193,7 +386,42 @@ type WsOrderbook struct { MessageType string `json:"messageType"` } -// WsError message received for orderbook errors +// WsFundTransfer stores fund transfer data for websocket +type WsFundTransfer struct { + FundTransferID int64 `json:"fundtransferId"` + TransferType string `json:"type"` + Status string `json:"status"` + Timestamp time.Time `json:"timestamp"` + Amount float64 `json:"amount,string"` + Currency string `json:"currency"` + Fee float64 `json:"fee,string"` + MessageType string `json:"messageType"` +} + +// WsTradeData stores trade data for websocket +type WsTradeData struct { + TradeID int64 `json:"tradeId"` + Price float64 `json:"price,string"` + Volume float64 `json:"volume,string"` + Fee float64 `json:"fee,string"` + LiquidityType string `json:"liquidityType"` +} + +// WsOrderChange stores order data +type WsOrderChange struct { + OrderID int64 `json:"orderId"` + MarketID string `json:"marketId"` + Side string `json:"side"` + OrderType string `json:"type"` + OpenVolume float64 `json:"openVolume,string"` + Status string `json:"status"` + TriggerStatus string `json:"triggerStatus"` + Trades []WsTradeData `json:"trades"` + Timestamp time.Time `json:"timestamp"` + MessageType string `json:"messageType"` +} + +// WsError stores websocket error data type WsError struct { MessageType string `json:"messageType"` Code int64 `json:"code"` diff --git a/exchanges/btcmarkets/btcmarkets_websocket.go b/exchanges/btcmarkets/btcmarkets_websocket.go index 7b5d3917..79cf815a 100644 --- a/exchanges/btcmarkets/btcmarkets_websocket.go +++ b/exchanges/btcmarkets/btcmarkets_websocket.go @@ -6,9 +6,13 @@ import ( "fmt" "net/http" "strconv" + "time" "github.com/gorilla/websocket" + "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/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" @@ -32,10 +36,15 @@ func (b *BTCMarkets) WsConnect() error { if b.Verbose { log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", b.Name) } - - b.generateDefaultSubscriptions() go b.WsHandleData() - + if b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + b.createChannels() + if err != nil { + b.Websocket.DataHandler <- err + b.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + } + b.generateDefaultSubscriptions() return nil } @@ -64,11 +73,11 @@ func (b *BTCMarkets) WsHandleData() { continue } switch wsResponse.MessageType { - case "heartbeat": + case heartbeat: if b.Verbose { log.Debugf(log.ExchangeSys, "%v - Websocket heartbeat received %s", b.Name, resp.Raw) } - case "orderbook": + case wsOB: var ob WsOrderbook err := json.Unmarshal(resp.Raw, &ob) if err != nil { @@ -129,7 +138,7 @@ func (b *BTCMarkets) WsHandleData() { Asset: asset.Spot, Exchange: b.Name, } - case "trade": + case trade: var trade WsTrade err := json.Unmarshal(resp.Raw, &trade) if err != nil { @@ -145,7 +154,7 @@ func (b *BTCMarkets) WsHandleData() { Price: trade.Price, Amount: trade.Volume, } - case "tick": + case tick: var tick WsTick err := json.Unmarshal(resp.Raw, &tick) if err != nil { @@ -166,6 +175,22 @@ func (b *BTCMarkets) WsHandleData() { AssetType: asset.Spot, Pair: p, } + case fundChange: + var transferData WsFundTransfer + err := json.Unmarshal(resp.Raw, &transferData) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + b.Websocket.DataHandler <- transferData + case orderChange: + var orderData WsOrderChange + err := json.Unmarshal(resp.Raw, &orderData) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + b.Websocket.DataHandler <- orderData case "error": var wsErr WsError err := json.Unmarshal(resp.Raw, &wsErr) @@ -182,7 +207,7 @@ func (b *BTCMarkets) WsHandleData() { } func (b *BTCMarkets) generateDefaultSubscriptions() { - var channels = []string{"tick", "trade", "orderbook"} + var channels = []string{tick, trade, wsOB} enabledCurrencies := b.GetEnabledPairs(asset.Spot) var subscriptions []wshandler.WebsocketChannelSubscription for i := range channels { @@ -198,10 +223,68 @@ func (b *BTCMarkets) generateDefaultSubscriptions() { // Subscribe sends a websocket message to receive data from the channel func (b *BTCMarkets) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { - req := WsSubscribe{ - MarketIDs: []string{channelToSubscribe.Currency.String()}, - Channels: []string{channelToSubscribe.Channel}, - MessageType: "subscribe", + unauthChannels := []string{tick, trade, wsOB} + authChannels := []string{fundChange, heartbeat, orderChange} + switch { + case common.StringDataCompare(unauthChannels, channelToSubscribe.Channel): + req := WsSubscribe{ + MarketIDs: []string{b.FormatExchangeCurrency(channelToSubscribe.Currency, asset.Spot).String()}, + Channels: []string{channelToSubscribe.Channel}, + MessageType: subscribe, + } + err := b.WebsocketConn.SendMessage(req) + if err != nil { + return err + } + case common.StringDataCompare(authChannels, channelToSubscribe.Channel): + message, ok := channelToSubscribe.Params["AuthSub"].(WsAuthSubscribe) + if !ok { + return errors.New("invalid params data") + } + tempAuthData := b.generateAuthSubscriptions() + message.Channels = append(message.Channels, channelToSubscribe.Channel, heartbeat) + message.Key = tempAuthData.Key + message.Signature = tempAuthData.Signature + message.Timestamp = tempAuthData.Timestamp + err := b.WebsocketConn.SendMessage(message) + if err != nil { + return err + } } - return b.WebsocketConn.SendMessage(req) + return nil +} + +// Login logs in allowing private ws events +func (b *BTCMarkets) generateAuthSubscriptions() WsAuthSubscribe { + var authSubInfo WsAuthSubscribe + signTime := strconv.FormatInt(time.Now().UTC().UnixNano()/1000000, 10) + strToSign := "/users/self/subscribe" + "\n" + signTime + tempSign := crypto.GetHMAC(crypto.HashSHA512, + []byte(strToSign), + []byte(b.API.Credentials.Secret)) + sign := crypto.Base64Encode(tempSign) + authSubInfo.Key = b.API.Credentials.Key + authSubInfo.Signature = sign + authSubInfo.Timestamp = signTime + return authSubInfo +} + +// createChannels creates channels that need to be +func (b *BTCMarkets) createChannels() { + tempChannels := []string{orderChange, fundChange} + var channels []wshandler.WebsocketChannelSubscription + pairArray := b.GetEnabledPairs(asset.Spot) + for y := range tempChannels { + for x := range pairArray { + var authSub WsAuthSubscribe + var channel wshandler.WebsocketChannelSubscription + channel.Params = make(map[string]interface{}) + channel.Channel = tempChannels[y] + authSub.MarketIDs = append(authSub.MarketIDs, b.FormatExchangeCurrency(pairArray[x], asset.Spot).String()) + authSub.MessageType = subscribe + channel.Params["AuthSub"] = authSub + channels = append(channels, channel) + } + } + b.Websocket.SubscribeToChannels(channels) } diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index b89f199b..7c59d882 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -3,7 +3,6 @@ package btcmarkets import ( "errors" "fmt" - "strconv" "strings" "sync" "time" @@ -62,6 +61,7 @@ func (b *BTCMarkets) SetDefaults() { }, UseGlobalFormat: true, RequestFormat: ¤cy.PairFormat{ + Delimiter: "-", Uppercase: true, }, ConfigFormat: ¤cy.PairFormat{ @@ -172,19 +172,31 @@ func (b *BTCMarkets) Start(wg *sync.WaitGroup) { // Run implements the BTC Markets wrapper func (b *BTCMarkets) Run() { if b.Verbose { + log.Debugf(log.ExchangeSys, + "%s Websocket: %s (url: %s).\n", + b.Name, + common.IsEnabled(b.Websocket.IsEnabled()), + btcMarketsWSURL) b.PrintEnabledPairs() } - forceUpdate := false if !common.StringDataContains(b.GetEnabledPairs(asset.Spot).Strings(), "-") || !common.StringDataContains(b.GetAvailablePairs(asset.Spot).Strings(), "-") { - enabledPairs := []string{"BTC-AUD"} log.Warnln(log.ExchangeSys, "Available pairs for BTC Markets reset due to config upgrade, please enable the pairs you would like again.") forceUpdate = true - - err := b.UpdatePairs(currency.NewPairsFromStrings(enabledPairs), asset.Spot, true, true) + } + if forceUpdate { + enabledPairs := currency.Pairs{currency.Pair{ + Base: currency.BTC.Lower(), + Quote: currency.AUD.Lower(), + Delimiter: "-", + }, + } + err := b.UpdatePairs(enabledPairs, asset.Spot, true, true) if err != nil { - log.Errorf(log.ExchangeSys, "%s failed to update currencies. Err: %s", b.Name, err) + log.Errorf(log.ExchangeSys, + "%s Failed to update enabled currencies.\n", + b.Name) } } @@ -194,12 +206,18 @@ func (b *BTCMarkets) Run() { err := b.UpdateTradablePairs(forceUpdate) if err != nil { - log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", b.Name, err) + log.Errorf(log.ExchangeSys, + "%s failed to update tradable pairs. Err: %s", + b.Name, + err) } } // FetchTradablePairs returns a list of the exchanges tradable pairs -func (b *BTCMarkets) FetchTradablePairs(asset asset.Item) ([]string, error) { +func (b *BTCMarkets) FetchTradablePairs(a asset.Item) ([]string, error) { + if a != asset.Spot { + return nil, fmt.Errorf("asset type of %s is not supported by %s", a, b.Name) + } markets, err := b.GetMarkets() if err != nil { return nil, err @@ -207,9 +225,8 @@ func (b *BTCMarkets) FetchTradablePairs(asset asset.Item) ([]string, error) { var pairs []string for x := range markets { - pairs = append(pairs, fmt.Sprintf("%v%v%v", markets[x].Instrument, b.GetPairFormat(asset, false).Delimiter, markets[x].Currency)) + pairs = append(pairs, markets[x].MarketID) } - return pairs, nil } @@ -226,28 +243,26 @@ func (b *BTCMarkets) UpdateTradablePairs(forceUpdate bool) error { // UpdateTicker updates and returns the ticker for a currency pair func (b *BTCMarkets) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { - var tickerPrice ticker.Price - tick, err := b.GetTicker(p.Base.String(), p.Quote.String()) - if err != nil { - return tickerPrice, err + var resp ticker.Price + allPairs := b.GetEnabledPairs(assetType) + for x := range allPairs { + tick, err := b.GetTicker(b.FormatExchangeCurrency(allPairs[x], assetType).String()) + if err != nil { + return resp, err + } + resp.Pair = allPairs[x] + resp.Last = tick.LastPrice + resp.High = tick.High24h + resp.Low = tick.Low24h + resp.Bid = tick.BestBID + resp.Ask = tick.BestAsk + resp.Volume = tick.Volume + resp.LastUpdated = time.Now() + err = ticker.ProcessTicker(b.Name, &resp, assetType) + if err != nil { + return resp, err + } } - - tickerPrice = ticker.Price{ - Last: tick.LastPrice, - High: tick.High24h, - Low: tick.Low24h, - Bid: tick.BestBid, - Ask: tick.BestAsk, - Volume: tick.Volume24h, - Pair: p, - LastUpdated: time.Unix(tick.Timestamp, 0), - } - - err = ticker.ProcessTicker(b.Name, &tickerPrice, assetType) - if err != nil { - return tickerPrice, err - } - return ticker.GetTicker(b.Name, p, assetType) } @@ -272,64 +287,50 @@ func (b *BTCMarkets) FetchOrderbook(p currency.Pair, assetType asset.Item) (orde // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *BTCMarkets) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := b.GetOrderbook(p.Base.String(), - p.Quote.String()) + tempResp, err := b.GetOrderbook(b.FormatExchangeCurrency(p, assetType).String(), 2) if err != nil { return orderBook, err } - - for x := range orderbookNew.Bids { + for x := range tempResp.Bids { orderBook.Bids = append(orderBook.Bids, orderbook.Item{ - Amount: orderbookNew.Bids[x][1], - Price: orderbookNew.Bids[x][0], - }) + Amount: tempResp.Bids[x].Volume, + Price: tempResp.Bids[x].Price}) } - - for x := range orderbookNew.Asks { + for y := range tempResp.Asks { orderBook.Asks = append(orderBook.Asks, orderbook.Item{ - Amount: orderbookNew.Asks[x][1], - Price: orderbookNew.Asks[x][0], - }) + Amount: tempResp.Asks[y].Volume, + Price: tempResp.Asks[y].Price}) } - orderBook.Pair = p orderBook.ExchangeName = b.Name orderBook.AssetType = assetType - err = orderBook.Process() if err != nil { return orderBook, err } - return orderbook.Get(b.Name, p, assetType) } -// GetAccountInfo retrieves balances for all enabled currencies for the -// BTCMarkets exchange +// GetAccountInfo retrieves balances for all enabled currencies func (b *BTCMarkets) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - response.Exchange = b.Name - - accountBalance, err := b.GetAccountBalance() + var resp exchange.AccountInfo + data, err := b.GetAccountBalance() if err != nil { - return response, err + return resp, err } - - var currencies []exchange.AccountCurrencyInfo - for i := 0; i < len(accountBalance); i++ { - var exchangeCurrency exchange.AccountCurrencyInfo - exchangeCurrency.CurrencyName = currency.NewCode(accountBalance[i].Currency) - exchangeCurrency.TotalValue = accountBalance[i].Balance - exchangeCurrency.Hold = accountBalance[i].PendingFunds - - currencies = append(currencies, exchangeCurrency) + var account exchange.Account + for key := range data { + c := currency.NewCode(data[key].AssetName) + hold := data[key].Locked + total := data[key].Balance + account.Currencies = append(account.Currencies, + exchange.AccountCurrencyInfo{CurrencyName: c, + TotalValue: total, + Hold: hold}) } - - response.Accounts = append(response.Accounts, exchange.Account{ - Currencies: currencies, - }) - - return response, nil + resp.Accounts = append(resp.Accounts, account) + resp.Exchange = b.Name + return resp, nil } // GetFundingHistory returns funding history, deposits and @@ -345,35 +346,35 @@ func (b *BTCMarkets) GetExchangeHistory(p currency.Pair, assetType asset.Item) ( // SubmitOrder submits a new order func (b *BTCMarkets) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { - var submitOrderResponse order.SubmitResponse + var resp order.SubmitResponse if err := s.Validate(); err != nil { - return submitOrderResponse, err + return resp, err } - if strings.EqualFold(s.OrderSide.String(), order.Sell.String()) { + if s.OrderSide == order.Sell { s.OrderSide = order.Ask } - if strings.EqualFold(s.OrderSide.String(), order.Buy.String()) { + if s.OrderSide == order.Buy { s.OrderSide = order.Bid } - response, err := b.NewOrder(s.Pair.Base.Upper().String(), - s.Pair.Quote.Upper().String(), + tempResp, err := b.NewOrder(b.FormatExchangeCurrency(s.Pair, asset.Spot).String(), s.Price, s.Amount, s.OrderSide.String(), s.OrderType.String(), + s.TriggerPrice, + s.TargetAmount, + "", + false, + "", s.ClientID) - - if response > 0 { - submitOrderResponse.OrderID = strconv.FormatInt(response, 10) + if err != nil { + return resp, err } - - if err == nil { - submitOrderResponse.IsOrderPlaced = true - } - - return submitOrderResponse, err + resp.IsOrderPlaced = true + resp.OrderID = tempResp.OrderID + return resp, nil } // ModifyOrder will allow of changing orderbook placement and limit to @@ -383,116 +384,130 @@ func (b *BTCMarkets) ModifyOrder(action *order.Modify) (string, error) { } // CancelOrder cancels an order by its corresponding ID number -func (b *BTCMarkets) CancelOrder(order *order.Cancel) error { - orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) - if err != nil { - return err - } - - _, err = b.CancelExistingOrder([]int64{orderIDInt}) +func (b *BTCMarkets) CancelOrder(o *order.Cancel) error { + _, err := b.RemoveOrder(o.OrderID) return err } // CancelAllOrders cancels all orders associated with a currency pair func (b *BTCMarkets) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) { - cancelAllOrdersResponse := order.CancelAllResponse{ - Status: make(map[string]string), - } - openOrders, err := b.GetOpenOrders() + var resp order.CancelAllResponse + tempMap := make(map[string]string) + var orderIDs []string + orders, err := b.GetOrders("", -1, -1, -1, "open") if err != nil { - return cancelAllOrdersResponse, err + return resp, err } - - var orderList []int64 - for i := range openOrders { - orderList = append(orderList, openOrders[i].ID) + for x := range orders { + orderIDs = append(orderIDs, orders[x].OrderID) } - - if len(orderList) > 0 { - orders, err := b.CancelExistingOrder(orderList) - if err != nil { - return cancelAllOrdersResponse, err - } - - for i := range orders { - if !orders[i].Success { - cancelAllOrdersResponse.Status[strconv.FormatInt(orders[i].ID, 10)] = orders[i].ErrorMessage - } - } + tempResp, err := b.CancelBatchOrders(orderIDs) + if err != nil { + return resp, err } - return cancelAllOrdersResponse, nil + for y := range tempResp.CancelOrders { + tempMap[tempResp.CancelOrders[y].OrderID] = "Success" + } + for z := range tempResp.UnprocessedRequests { + tempMap[tempResp.UnprocessedRequests[z].RequestID] = "Cancellation Failed" + } + return resp, nil } // GetOrderInfo returns information on a current open order func (b *BTCMarkets) GetOrderInfo(orderID string) (order.Detail, error) { - var OrderDetail order.Detail - - o, err := strconv.ParseInt(orderID, 10, 64) + var resp order.Detail + o, err := b.FetchOrder(orderID) if err != nil { - return OrderDetail, err + return resp, err } - - orders, err := b.GetOrderDetail([]int64{o}) - if err != nil { - return OrderDetail, err + resp.Exchange = b.Name + resp.ID = orderID + resp.CurrencyPair = currency.NewPairFromString(o.MarketID) + resp.Price = o.Price + resp.OrderDate = o.CreationTime + resp.ExecutedAmount = o.Amount - o.OpenAmount + resp.OrderSide = order.Bid + if o.Side == ask { + resp.OrderSide = order.Ask } - - if len(orders) > 1 { - return OrderDetail, errors.New("too many orders returned") + switch o.Type { + case limit: + resp.OrderType = order.Limit + case market: + resp.OrderType = order.Market + case stopLimit: + resp.OrderType = order.Stop + case stop: + resp.OrderType = order.Stop + case takeProfit: + resp.OrderType = order.ImmediateOrCancel + default: + resp.OrderType = order.Unknown } - - if len(orders) == 0 { - return OrderDetail, errors.New("no orders found") + resp.RemainingAmount = o.OpenAmount + switch o.Status { + case orderAccepted: + resp.Status = order.Active + case orderPlaced: + resp.Status = order.Active + case orderPartiallyMatched: + resp.Status = order.PartiallyFilled + case orderFullyMatched: + resp.Status = order.Filled + case orderCancelled: + resp.Status = order.Cancelled + case orderPartiallyCancelled: + resp.Status = order.PartiallyCancelled + case orderFailed: + resp.Status = order.Rejected + default: + resp.Status = order.UnknownStatus } - - for i := range orders { - var side order.Side - if strings.EqualFold(orders[i].OrderSide, order.Ask.String()) { - side = order.Sell - } else if strings.EqualFold(orders[i].OrderSide, order.Bid.String()) { - side = order.Buy - } - orderDate := time.Unix(int64(orders[i].CreationTime), 0) - orderType := order.Type(strings.ToUpper(orders[i].OrderType)) - - OrderDetail.Amount = orders[i].Volume - OrderDetail.OrderDate = orderDate - OrderDetail.Exchange = b.Name - OrderDetail.ID = strconv.FormatInt(orders[i].ID, 10) - OrderDetail.RemainingAmount = orders[i].OpenVolume - OrderDetail.OrderSide = side - OrderDetail.OrderType = orderType - OrderDetail.Price = orders[i].Price - OrderDetail.Status = order.Status(orders[i].Status) - OrderDetail.CurrencyPair = currency.NewPairWithDelimiter(orders[i].Instrument, - orders[i].Currency, - b.GetPairFormat(asset.Spot, false).Delimiter) - } - - return OrderDetail, nil + return resp, nil } // GetDepositAddress returns a deposit address for a specified currency func (b *BTCMarkets) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) { - return "", common.ErrFunctionNotSupported + temp, err := b.FetchDepositAddress(strings.ToUpper(cryptocurrency.String()), -1, -1, -1) + if err != nil { + return "", err + } + return temp.Address, nil } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is submitted func (b *BTCMarkets) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { - return b.WithdrawCrypto(withdrawRequest.Amount, withdrawRequest.Currency.String(), withdrawRequest.Address) + a, err := b.RequestWithdraw(withdrawRequest.Currency.String(), + withdrawRequest.Amount, + withdrawRequest.Address, + "", + "", + "", + "") + if err != nil { + return "", err + } + return a.Status, nil } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted func (b *BTCMarkets) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { if withdrawRequest.Currency != currency.AUD { - return "", errors.New("only AUD is supported for withdrawals") + return "", errors.New("only aud is supported for withdrawals") } - return b.WithdrawAUD(withdrawRequest.BankAccountName, - strconv.FormatFloat(withdrawRequest.BankAccountNumber, 'f', -1, 64), - withdrawRequest.BankName, - strconv.FormatFloat(withdrawRequest.BankCode, 'f', -1, 64), - withdrawRequest.Amount) + a, err := b.RequestWithdraw(withdrawRequest.GenericWithdrawRequestInfo.Currency.String(), + withdrawRequest.GenericWithdrawRequestInfo.Amount, + "", + withdrawRequest.BankAccountName, + withdrawRequest.BankAccountNumber, + withdrawRequest.BSB, + withdrawRequest.BankName) + if err != nil { + return "", err + } + return a.Status, nil } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a @@ -517,124 +532,116 @@ func (b *BTCMarkets) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, err // GetActiveOrders retrieves any orders that are active/open func (b *BTCMarkets) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) { - resp, err := b.GetOpenOrders() - if err != nil { - return nil, err + var resp []order.Detail + var tempResp order.Detail + var tempData []OrderData + if len(req.Currencies) == 0 { + allPairs := b.GetEnabledPairs(asset.Spot) + for a := range allPairs { + req.Currencies = append(req.Currencies, + allPairs[a]) + } } - - var orders []order.Detail - for i := range resp { - var side order.Side - if strings.EqualFold(resp[i].OrderSide, order.Ask.String()) { - side = order.Sell - } else if strings.EqualFold(resp[i].OrderSide, order.Bid.String()) { - side = order.Buy + var err error + for x := range req.Currencies { + tempData, err = b.GetOrders(b.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String(), -1, -1, -1, "") + if err != nil { + return resp, err } - orderDate := time.Unix(int64(resp[i].CreationTime), 0) - orderType := order.Type(strings.ToUpper(resp[i].OrderType)) - - openOrder := order.Detail{ - ID: strconv.FormatInt(resp[i].ID, 10), - Amount: resp[i].Volume, - Exchange: b.Name, - RemainingAmount: resp[i].OpenVolume, - OrderDate: orderDate, - OrderSide: side, - OrderType: orderType, - Price: resp[i].Price, - Status: order.Status(resp[i].Status), - CurrencyPair: currency.NewPairWithDelimiter(resp[i].Instrument, - resp[i].Currency, - b.GetPairFormat(asset.Spot, false).Delimiter), + for y := range tempData { + tempResp.Exchange = b.Name + tempResp.CurrencyPair = req.Currencies[x] + tempResp.OrderSide = order.Bid + if tempData[y].Side == ask { + tempResp.OrderSide = order.Ask + } + tempResp.OrderDate = tempData[y].CreationTime + switch tempData[y].Status { + case orderAccepted: + tempResp.Status = order.Active + case orderPlaced: + tempResp.Status = order.Active + case orderPartiallyMatched: + tempResp.Status = order.PartiallyFilled + case orderFullyMatched: + tempResp.Status = order.Filled + case orderCancelled: + tempResp.Status = order.Cancelled + case orderPartiallyCancelled: + tempResp.Status = order.PartiallyCancelled + case orderFailed: + tempResp.Status = order.Rejected + } + tempResp.Price = tempData[y].Price + tempResp.Amount = tempData[y].Amount + tempResp.ExecutedAmount = tempData[y].Amount - tempData[y].OpenAmount + tempResp.RemainingAmount = tempData[y].OpenAmount + resp = append(resp, tempResp) } - - for j := range resp[i].Trades { - tradeDate := time.Unix(int64(resp[i].Trades[j].CreationTime), 0) - openOrder.Trades = append(openOrder.Trades, order.TradeHistory{ - Amount: resp[i].Trades[j].Volume, - Exchange: b.Name, - Price: resp[i].Trades[j].Price, - TID: resp[i].Trades[j].ID, - Timestamp: tradeDate, - Fee: resp[i].Trades[j].Fee, - Description: resp[i].Trades[j].Description, - }) - } - - orders = append(orders, openOrder) } - - order.FilterOrdersByType(&orders, req.OrderType) - order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) - return orders, nil + order.FilterOrdersByType(&resp, req.OrderType) + order.FilterOrdersByTickRange(&resp, req.StartTicks, req.EndTicks) + order.FilterOrdersBySide(&resp, req.OrderSide) + return resp, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status func (b *BTCMarkets) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error) { + var resp []order.Detail + var tempResp order.Detail + var tempArray []string if len(req.Currencies) == 0 { - return nil, errors.New("requires at least one currency pair to retrieve history") - } - - var respOrders []Order - for i := range req.Currencies { - resp, err := b.GetOrders(req.Currencies[i].Base.String(), - req.Currencies[i].Quote.String(), - 200, - 0, - true) + orders, err := b.GetOrders("", -1, -1, -1, "") if err != nil { - return nil, err + return resp, err + } + for x := range orders { + tempArray = append(tempArray, orders[x].OrderID) } - respOrders = append(respOrders, resp...) } - - var orders []order.Detail - for i := range respOrders { - var side order.Side - if strings.EqualFold(respOrders[i].OrderSide, order.Ask.String()) { - side = order.Sell - } else if strings.EqualFold(respOrders[i].OrderSide, order.Bid.String()) { - side = order.Buy + for y := range req.Currencies { + orders, err := b.GetOrders(b.FormatExchangeCurrency(req.Currencies[y], asset.Spot).String(), -1, -1, -1, "") + if err != nil { + return resp, err } - orderDate := time.Unix(int64(respOrders[i].CreationTime), 0) - orderType := order.Type(strings.ToUpper(respOrders[i].OrderType)) - - openOrder := order.Detail{ - ID: strconv.FormatInt(respOrders[i].ID, 10), - Amount: respOrders[i].Volume, - Exchange: b.Name, - RemainingAmount: respOrders[i].OpenVolume, - OrderDate: orderDate, - OrderSide: side, - OrderType: orderType, - Price: respOrders[i].Price, - Status: order.Status(respOrders[i].Status), - CurrencyPair: currency.NewPairWithDelimiter(respOrders[i].Instrument, - respOrders[i].Currency, - b.GetPairFormat(asset.Spot, false).Delimiter), + for z := range orders { + tempArray = append(tempArray, orders[z].OrderID) } - - for j := range respOrders[i].Trades { - tradeDate := time.Unix(int64(respOrders[i].Trades[j].CreationTime), 0) - openOrder.Trades = append(openOrder.Trades, order.TradeHistory{ - Amount: respOrders[i].Trades[j].Volume, - Exchange: b.Name, - Price: respOrders[i].Trades[j].Price, - TID: respOrders[i].Trades[j].ID, - Timestamp: tradeDate, - Fee: respOrders[i].Trades[j].Fee, - Description: respOrders[i].Trades[j].Description, - }) - } - orders = append(orders, openOrder) } - - order.FilterOrdersByType(&orders, req.OrderType) - order.FilterOrdersByTickRange(&orders, req.StartTicks, req.EndTicks) - order.FilterOrdersBySide(&orders, req.OrderSide) - return orders, nil + tempData, err := b.GetBatchTrades(tempArray) + if err != nil { + return resp, err + } + for c := range tempData.Orders { + switch tempData.Orders[c].Status { + case orderFailed: + tempResp.Status = order.Rejected + case orderPartiallyCancelled: + tempResp.Status = order.PartiallyCancelled + case orderCancelled: + tempResp.Status = order.Cancelled + case orderFullyMatched: + tempResp.Status = order.Filled + case orderPartiallyMatched: + continue + case orderPlaced: + continue + case orderAccepted: + continue + } + tempResp.Exchange = b.Name + tempResp.CurrencyPair = currency.NewPairFromString(tempData.Orders[c].MarketID) + tempResp.OrderSide = order.Bid + if tempData.Orders[c].Side == ask { + tempResp.OrderSide = order.Ask + } + tempResp.OrderDate = tempData.Orders[c].CreationTime + tempResp.Price = tempData.Orders[c].Price + tempResp.ExecutedAmount = tempData.Orders[c].Amount + resp = append(resp, tempResp) + } + return resp, nil } // SubscribeToWebsocketChannels appends to ChannelsToSubscribe diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go index 344e8e6c..8b5e8839 100644 --- a/exchanges/exchange_types.go +++ b/exchanges/exchange_types.go @@ -193,12 +193,13 @@ type FiatWithdrawRequest struct { GenericWithdrawRequestInfo // FIAT related information BankAccountName string - BankAccountNumber float64 + BankAccountNumber string BankName string BankAddress string BankCity string BankCountry string BankPostalCode string + BSB string SwiftCode string IBAN string BankCode float64 diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 02eb6d8e..0a02e9ed 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -37,12 +37,14 @@ var ( // Submit contains the order submission data type Submit struct { - Pair currency.Pair - OrderType Type - OrderSide Side - Price float64 - Amount float64 - ClientID string + Pair currency.Pair + OrderType Type + OrderSide Side + TriggerPrice float64 + TargetAmount float64 + Price float64 + Amount float64 + ClientID string } // SubmitResponse is what is returned after submitting an order to an exchange @@ -161,17 +163,18 @@ type Status string // All order status types const ( - AnyStatus Status = "ANY" - New Status = "NEW" - Active Status = "ACTIVE" - PartiallyFilled Status = "PARTIALLY_FILLED" - Filled Status = "FILLED" - Cancelled Status = "CANCELED" - PendingCancel Status = "PENDING_CANCEL" - Rejected Status = "REJECTED" - Expired Status = "EXPIRED" - Hidden Status = "HIDDEN" - UnknownStatus Status = "UNKNOWN" + AnyStatus Status = "ANY" + New Status = "NEW" + Active Status = "ACTIVE" + PartiallyCancelled Status = "PARTIALLY_CANCELLED" + PartiallyFilled Status = "PARTIALLY_FILLED" + Filled Status = "FILLED" + Cancelled Status = "CANCELED" + PendingCancel Status = "PENDING_CANCEL" + Rejected Status = "REJECTED" + Expired Status = "EXPIRED" + Hidden Status = "HIDDEN" + UnknownStatus Status = "UNKNOWN" ) // ByPrice used for sorting orders by price