From 4ca3fd5b0027c96c9495ef33973943a24fb0c02a Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Thu, 13 Dec 2018 16:53:19 +1100 Subject: [PATCH] GetAccountInfo wrapper update (#220) * Added untested [cloudflare issue] changes to accountinfo for ANX * Add alphapoint comment for future implementation * Adds GetAccountInfo for Binance * Adds GetAccountInfo update for Bithumb * Updates GetAccountInfo for GateIO. Adds error handling feature for authenticated requests. * Updates GetAccountInfo function for Huobi. Adds function for getting account ID. * Updates GetAccountInfo function Adds GetAccountID function * Updates GetAccountInfo [un-tested, no access to keys at this time] * Updates GetAccountInfo for Kraken * Updates GetAccountInfo func for OKEX * Updates GetAccountInfo for exchange ZB * Updates GetAccountInfo func for Bitmex * Updates GetAccountInfo func for Coinut * Updates GetAccountInfo for ANX exchange * Fixes incorrect hold currency issue * Fixes type name * Fixes issue with unneeded code in wrapper for Bithumb * Change strings to type symbol string * Fixes nit for Gateio * Fixes GetAccountInfo issue Fixes SpotCancelOrder issue --- currency/symbol/symbol.go | 2 + exchanges/alphapoint/alphapoint_wrapper.go | 3 +- exchanges/anx/anx.go | 3 +- exchanges/anx/anx_test.go | 14 ++++ exchanges/anx/anx_types.go | 4 +- exchanges/anx/anx_wrapper.go | 42 +++++++++- exchanges/binance/binance_test.go | 17 +++- exchanges/binance/binance_wrapper.go | 40 +++++++-- exchanges/bithumb/bithumb.go | 73 ++++++++++++++-- exchanges/bithumb/bithumb_test.go | 36 ++++++-- exchanges/bithumb/bithumb_types.go | 25 +++--- exchanges/bithumb/bithumb_wrapper.go | 27 +++++- exchanges/bitmex/bitmex.go | 10 +++ exchanges/bitmex/bitmex_test.go | 14 ++++ exchanges/bitmex/bitmex_wrapper.go | 21 ++++- exchanges/coinut/coinut_test.go | 19 ++++- exchanges/coinut/coinut_types.go | 28 ++++--- exchanges/coinut/coinut_wrapper.go | 97 ++++++++++++++++++---- exchanges/gateio/gateio.go | 32 +++++-- exchanges/gateio/gateio_test.go | 14 ++++ exchanges/gateio/gateio_types.go | 6 +- exchanges/gateio/gateio_wrapper.go | 59 ++++++++++++- exchanges/huobi/huobi.go | 7 +- exchanges/huobi/huobi_test.go | 14 ++++ exchanges/huobi/huobi_wrapper.go | 71 +++++++++++++++- exchanges/huobihadax/huobihadax.go | 1 + exchanges/huobihadax/huobihadax_test.go | 14 ++++ exchanges/huobihadax/huobihadax_wrapper.go | 71 +++++++++++++++- exchanges/itbit/itbit.go | 29 ++++++- exchanges/itbit/itbit_test.go | 9 ++ exchanges/itbit/itbit_wrapper.go | 44 ++++++++-- exchanges/kraken/kraken_test.go | 14 ++++ exchanges/kraken/kraken_wrapper.go | 21 ++++- exchanges/okex/okex.go | 74 +++++++++++++++-- exchanges/okex/okex_test.go | 16 +++- exchanges/okex/okex_types.go | 17 ++++ exchanges/okex/okex_wrapper.go | 22 ++++- exchanges/zb/zb.go | 56 ++++++++++++- exchanges/zb/zb_test.go | 27 +++--- exchanges/zb/zb_wrapper.go | 31 ++++++- 40 files changed, 981 insertions(+), 143 deletions(-) diff --git a/currency/symbol/symbol.go b/currency/symbol/symbol.go index f9c97b89..408af0c6 100644 --- a/currency/symbol/symbol.go +++ b/currency/symbol/symbol.go @@ -1510,6 +1510,8 @@ const ( UAH = "UAH" // Ukrainian hryvnia JPY = "JPY" // Japanese yen ZJPY = "ZJPY" // Japanese yen, but with a Z in front of it + LCH = "LCH" + MYR = "MYR" ) // symbols map holds the currency name and symbol mappings diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index c9549c60..7aa1b836 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -95,7 +95,8 @@ func (a *Alphapoint) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orde // withdrawals func (a *Alphapoint) GetFundingHistory() ([]exchange.FundHistory, error) { var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + // https://alphapoint.github.io/slate/#generatetreasuryactivityreport + return fundHistory, common.ErrNotYetImplemented } // GetExchangeHistory returns historic trade data since exchange opening. diff --git a/exchanges/anx/anx.go b/exchanges/anx/anx.go index 1425b66a..975a7896 100644 --- a/exchanges/anx/anx.go +++ b/exchanges/anx/anx.go @@ -406,7 +406,6 @@ func (a *ANX) SendAuthenticatedHTTPRequest(path string, params map[string]interf } PayloadJSON, err := common.JSONEncode(request) - if err != nil { return errors.New("SendAuthenticatedHTTPRequest: Unable to JSON request") } @@ -471,7 +470,7 @@ func getInternationalBankWithdrawalFee(currency string, amount float64) float64 // GetAccountInformation retrieves details including API permissions func (a *ANX) GetAccountInformation() (AccountInformation, error) { var response AccountInformation - err := a.SendAuthenticatedHTTPRequest(anxOrderInfo, nil, &response) + err := a.SendAuthenticatedHTTPRequest(anxAccount, nil, &response) if err != nil { return response, err } diff --git a/exchanges/anx/anx_test.go b/exchanges/anx/anx_test.go index f420d71c..66c19378 100644 --- a/exchanges/anx/anx_test.go +++ b/exchanges/anx/anx_test.go @@ -283,3 +283,17 @@ func TestCancelExchangeOrder(t *testing.T) { t.Errorf("Test Failed - ANX CancelOrder() error: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + if testAPIKey != "" || testAPISecret != "" { + _, err := a.GetAccountInfo() + if err != nil { + t.Error("test failed - GetAccountInfo() error:", err) + } + } else { + _, err := a.GetAccountInfo() + if err == nil { + t.Error("test failed - GetAccountInfo() error") + } + } +} diff --git a/exchanges/anx/anx_types.go b/exchanges/anx/anx_types.go index a874f290..43bb12c4 100644 --- a/exchanges/anx/anx_types.go +++ b/exchanges/anx/anx_types.go @@ -63,12 +63,12 @@ type AccountInformation struct { UserUUID string `json:"userUuid"` Rights []string `json:"Rights"` ResultCode string `json:"resultCode"` - Wallets struct { + Wallets map[string]struct { Balance Amount `json:"Balance"` AvailableBalance Amount `json:"Available_Balance"` DailyWithdrawalLimit Amount `json:"Daily_Withdrawal_Limit"` MaxWithdraw Amount `json:"Max_Withdraw"` - } + } `json:"Wallets"` } // Amount basic storage of wallet details diff --git a/exchanges/anx/anx_wrapper.go b/exchanges/anx/anx_wrapper.go index ff03065e..68cafabc 100644 --- a/exchanges/anx/anx_wrapper.go +++ b/exchanges/anx/anx_wrapper.go @@ -162,11 +162,17 @@ func (a *ANX) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook. } for x := range orderbookNew.Data.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Price: orderbookNew.Data.Asks[x].Price, Amount: orderbookNew.Data.Asks[x].Amount}) + orderBook.Asks = append(orderBook.Asks, + orderbook.Item{ + Price: orderbookNew.Data.Asks[x].Price, + Amount: orderbookNew.Data.Asks[x].Amount}) } for x := range orderbookNew.Data.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Price: orderbookNew.Data.Bids[x].Price, Amount: orderbookNew.Data.Bids[x].Amount}) + orderBook.Bids = append(orderBook.Bids, + orderbook.Item{ + Price: orderbookNew.Data.Bids[x].Price, + Amount: orderbookNew.Data.Bids[x].Amount}) } orderbook.ProcessOrderbook(a.GetName(), p, orderBook, assetType) @@ -177,7 +183,25 @@ func (a *ANX) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook. // exchange func (a *ANX) GetAccountInfo() (exchange.AccountInfo, error) { var info exchange.AccountInfo - return info, common.ErrNotYetImplemented + + raw, err := a.GetAccountInformation() + if err != nil { + return info, err + } + + var balance []exchange.AccountCurrencyInfo + for currency, info := range raw.Wallets { + balance = append(balance, exchange.AccountCurrencyInfo{ + CurrencyName: currency, + TotalValue: info.AvailableBalance.Value, + Hold: info.Balance.Value, + }) + } + + info.ExchangeName = a.GetName() + info.Currencies = balance + + return info, nil } // GetFundingHistory returns funding history, deposits and @@ -209,7 +233,17 @@ func (a *ANX) SubmitOrder(p pair.CurrencyPair, side exchange.OrderSide, orderTyp limitPriceInSettlementCurrency = price } - response, err := a.NewOrder(orderType.ToString(), isBuying, p.FirstCurrency.String(), amount, p.SecondCurrency.String(), amount, limitPriceInSettlementCurrency, false, "", false) + response, err := a.NewOrder(orderType.ToString(), + isBuying, + p.FirstCurrency.String(), + amount, + p.SecondCurrency.String(), + amount, + limitPriceInSettlementCurrency, + false, + "", + false) + if response != "" { submitOrderResponse.OrderID = response } diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index 9cc7e264..17c62314 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -166,7 +166,7 @@ func TestCancelExistingOrder(t *testing.T) { } _, err := b.CancelExistingOrder("BTCUSDT", 82584683, "") - if err == nil { + if err != nil { t.Error("Test Failed - Binance CancelExistingOrder() error", err) } } @@ -179,7 +179,7 @@ func TestQueryOrder(t *testing.T) { } _, err := b.QueryOrder("BTCUSDT", "", 1337) - if err != nil { + if err == nil { t.Error("Test Failed - Binance QueryOrder() error", err) } } @@ -386,8 +386,19 @@ func TestCancelExchangeOrder(t *testing.T) { // Act err := b.CancelOrder(orderCancellation) - // Assert + // Assert if err != nil { t.Errorf("Could not cancel order: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + if testAPIKey == "" || testAPISecret == "" { + t.Skip() + } + + _, err := b.GetAccountInfo() + if err != nil { + t.Error("test failed - GetAccountInfo() error:", err) + } +} diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index cab9e370..6d357014 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -36,7 +36,8 @@ func (b *Binance) Run() { log.Printf("%s Failed to get exchange info.\n", b.GetName()) } else { forceUpgrade := false - if !common.StringDataContains(b.EnabledPairs, "-") || !common.StringDataContains(b.AvailablePairs, "-") { + if !common.StringDataContains(b.EnabledPairs, "-") || + !common.StringDataContains(b.AvailablePairs, "-") { forceUpgrade = true } @@ -110,11 +111,13 @@ func (b *Binance) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderb } for _, bids := range orderbookNew.Bids { - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: bids.Quantity, Price: bids.Price}) + orderBook.Bids = append(orderBook.Bids, + orderbook.Item{Amount: bids.Quantity, Price: bids.Price}) } for _, asks := range orderbookNew.Asks { - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: asks.Quantity, Price: asks.Price}) + orderBook.Asks = append(orderBook.Asks, + orderbook.Item{Amount: asks.Quantity, Price: asks.Price}) } orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType) @@ -124,8 +127,35 @@ func (b *Binance) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderb // GetAccountInfo retrieves balances for all enabled currencies for the // Bithumb exchange func (b *Binance) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - return response, common.ErrNotYetImplemented + var info exchange.AccountInfo + raw, err := b.GetAccount() + if err != nil { + return info, err + } + + var currencyBalance []exchange.AccountCurrencyInfo + for _, balance := range raw.Balances { + freeCurrency, err := strconv.ParseFloat(balance.Free, 64) + if err != nil { + return info, err + } + + lockedCurrency, err := strconv.ParseFloat(balance.Locked, 64) + if err != nil { + return info, err + } + + currencyBalance = append(currencyBalance, exchange.AccountCurrencyInfo{ + CurrencyName: balance.Asset, + TotalValue: freeCurrency + lockedCurrency, + Hold: freeCurrency, + }) + } + + info.ExchangeName = b.GetName() + info.Currencies = currencyBalance + + return info, nil } // GetFundingHistory returns funding history, deposits and diff --git a/exchanges/bithumb/bithumb.go b/exchanges/bithumb/bithumb.go index 4d4f802f..d816d8d3 100644 --- a/exchanges/bithumb/bithumb.go +++ b/exchanges/bithumb/bithumb.go @@ -238,11 +238,16 @@ func (b *Bithumb) GetTransactionHistory(symbol string) (TransactionHistory, erro return response, nil } -// GetAccountInformation returns account information -func (b *Bithumb) GetAccountInformation() (Account, error) { +// GetAccountInformation returns account information by singular currency +func (b *Bithumb) GetAccountInformation(currency string) (Account, error) { response := Account{} - err := b.SendAuthenticatedHTTPRequest(privateAccInfo, nil, &response) + val := url.Values{} + if currency != "" { + val.Set("currency", currency) + } + + err := b.SendAuthenticatedHTTPRequest(privateAccInfo, val, &response) if err != nil { return response, err } @@ -254,18 +259,68 @@ func (b *Bithumb) GetAccountInformation() (Account, error) { } // GetAccountBalance returns customer wallet information -func (b *Bithumb) GetAccountBalance() (Balance, error) { - response := Balance{} +func (b *Bithumb) GetAccountBalance(c string) (FullBalance, error) { + var response Balance + var fullBalance = FullBalance{ + make(map[string]float64), + make(map[string]float64), + make(map[string]float64), + make(map[string]float64), + make(map[string]float64), + } - err := b.SendAuthenticatedHTTPRequest(privateAccBalance, nil, &response) + vals := url.Values{} + if c != "" { + vals.Set("currency", c) + } + + err := b.SendAuthenticatedHTTPRequest(privateAccBalance, vals, &response) if err != nil { - return response, err + return fullBalance, err } if response.Status != noError { - return response, errors.New(response.Message) + return fullBalance, errors.New(response.Message) } - return response, nil + + // Added due to increasing of the usuable currencies on exchange, usually + // without notificatation, so we dont need to update structs later on + for tag, datum := range response.Data { + splitTag := common.SplitStrings(tag, "_") + c := splitTag[len(splitTag)-1] + var val float64 + if reflect.TypeOf(datum).String() != "float64" { + val, err = strconv.ParseFloat(datum.(string), 64) + if err != nil { + return fullBalance, err + } + } else { + val = datum.(float64) + } + + switch splitTag[0] { + case "available": + fullBalance.Available[c] = val + + case "in": + fullBalance.InUse[c] = val + + case "total": + fullBalance.Total[c] = val + + case "misu": + fullBalance.Misu[c] = val + + case "xcoin": + fullBalance.Xcoin[c] = val + + default: + return fullBalance, fmt.Errorf("GetAccountBalance error tag name %s unhandled", + splitTag) + } + } + + return fullBalance, nil } // GetWalletAddress returns customer wallet address diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index 9c10d6ce..a78eee01 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -77,23 +77,23 @@ func TestGetTransactionHistory(t *testing.T) { } } -func TestGetAccountInfo(t *testing.T) { - t.Parallel() - _, err := b.GetAccountInfo() - if err == nil { - t.Error("test failed - Bithumb GetAccountInfo() error", err) - } -} - func TestGetAccountBalance(t *testing.T) { t.Parallel() - _, err := b.GetAccountBalance() + if testAPIKey == "" || testAPISecret == "" { + t.Skip() + } + + _, err := b.GetAccountBalance("BTC") if err == nil { t.Error("test failed - Bithumb GetAccountBalance() error", err) } } func TestGetWalletAddress(t *testing.T) { + if testAPIKey == "" || testAPISecret == "" { + t.Skip() + } + t.Parallel() _, err := b.GetWalletAddress("") if err == nil { @@ -159,6 +159,9 @@ func TestWithdrawCrypto(t *testing.T) { func TestRequestKRWDepositDetails(t *testing.T) { t.Parallel() + if testAPIKey == "" || testAPISecret == "" { + t.Skip() + } _, err := b.RequestKRWDepositDetails() if err == nil { t.Error("test failed - Bithumb RequestKRWDepositDetails() error", err) @@ -341,3 +344,18 @@ func TestCancelExchangeOrder(t *testing.T) { t.Errorf("Could not cancel order: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + t.Parallel() + if testAPIKey != "" || testAPISecret != "" { + _, err := b.GetAccountInfo() + if err != nil { + t.Error("test failed - Bithumb GetAccountInfo() error", err) + } + } else { + _, err := b.GetAccountInfo() + if err == nil { + t.Error("test failed - Bithumb GetAccountInfo() error") + } + } +} diff --git a/exchanges/bithumb/bithumb_types.go b/exchanges/bithumb/bithumb_types.go index 110d8f00..625823c5 100644 --- a/exchanges/bithumb/bithumb_types.go +++ b/exchanges/bithumb/bithumb_types.go @@ -79,19 +79,9 @@ type Account struct { // Balance holds balance details type Balance struct { - Status string `json:"status"` - Data struct { - TotalBTC float64 `json:"total_btc,string"` - TotalKRW float64 `json:"total_krw"` - InUseBTC float64 `json:"in_use_btc,string"` - InUseKRW float64 `json:"in_use_krw"` - AvailableBTC float64 `json:"available_btc,string"` - AvailableKRW float64 `json:"available_krw"` - MisuKRW float64 `json:"misu_krw"` - MisuBTC float64 `json:"misu_btc,string"` - XcoinLast float64 `json:"xcoin_last,string"` - } `json:"data"` - Message string `json:"message"` + Status string `json:"status"` + Data map[string]interface{} `json:"data"` + Message string `json:"message"` } // WalletAddressRes contains wallet address information @@ -278,3 +268,12 @@ var WithdrawalFees = map[string]float64{ symbol.ENJ: 35, symbol.PST: 30, } + +// FullBalance defines a return type with full balance data +type FullBalance struct { + InUse map[string]float64 + Misu map[string]float64 + Total map[string]float64 + Xcoin map[string]float64 + Available map[string]float64 +} diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index 7eda7a44..b2f68892 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -1,7 +1,6 @@ package bithumb import ( - "errors" "fmt" "log" "sync" @@ -122,8 +121,30 @@ func (b *Bithumb) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderb // GetAccountInfo retrieves balances for all enabled currencies for the // Bithumb exchange func (b *Bithumb) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - return response, errors.New("not implemented") + var info exchange.AccountInfo + bal, err := b.GetAccountBalance("ALL") + if err != nil { + return info, err + } + + var exchangeBalances []exchange.AccountCurrencyInfo + for key, totalAmount := range bal.Total { + hold, ok := bal.InUse[key] + if !ok { + return info, fmt.Errorf("GetAccountInfo error - in use item not found for currency %s", + key) + } + + exchangeBalances = append(exchangeBalances, exchange.AccountCurrencyInfo{ + CurrencyName: key, + TotalValue: totalAmount, + Hold: hold, + }) + } + + info.Currencies = exchangeBalances + info.ExchangeName = b.GetName() + return info, nil } // GetFundingHistory returns funding history, deposits and diff --git a/exchanges/bitmex/bitmex.go b/exchanges/bitmex/bitmex.go index 1c8a7134..96e36e6d 100644 --- a/exchanges/bitmex/bitmex.go +++ b/exchanges/bitmex/bitmex.go @@ -744,6 +744,16 @@ func (b *Bitmex) GetUserMargin(currency string) (UserMargin, error) { &info) } +// GetAllUserMargin returns user margin information +func (b *Bitmex) GetAllUserMargin() ([]UserMargin, error) { + var info []UserMargin + + return info, b.SendAuthenticatedHTTPRequest("GET", + bitmexEndpointUserMargin, + UserCurrencyParams{Currency: "all"}, + &info) +} + // GetMinimumWithdrawalFee returns minimum withdrawal fee information func (b *Bitmex) GetMinimumWithdrawalFee(currency string) (MinWithdrawalFee, error) { var fee MinWithdrawalFee diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index f55cbec9..ed8535ab 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -521,3 +521,17 @@ func TestCancelExchangeOrder(t *testing.T) { t.Errorf("Could not cancel order: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + if testAPIKey != "" || testAPISecret != "" { + _, err := b.GetAccountInfo() + if err != nil { + t.Error("Test Failed - GetAccountInfo() error", err) + } + } else { + _, err := b.GetAccountInfo() + if err == nil { + t.Error("Test Failed - GetAccountInfo() error") + } + } +} diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 14e228f7..e22e2a10 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -125,8 +125,25 @@ func (b *Bitmex) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbo // GetAccountInfo retrieves balances for all enabled currencies for the // Bitmex exchange func (b *Bitmex) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - return response, common.ErrNotYetImplemented + var info exchange.AccountInfo + + bal, err := b.GetAllUserMargin() + if err != nil { + return info, err + } + + // Need to update to add Margin/Liquidity availibilty + var balances []exchange.AccountCurrencyInfo + for _, data := range bal { + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: data.Currency, + TotalValue: float64(data.WalletBalance), + }) + } + + info.ExchangeName = b.GetName() + info.Currencies = balances + return info, nil } // GetFundingHistory returns funding history, deposits and diff --git a/exchanges/coinut/coinut_test.go b/exchanges/coinut/coinut_test.go index 39417034..3a49bb27 100644 --- a/exchanges/coinut/coinut_test.go +++ b/exchanges/coinut/coinut_test.go @@ -15,7 +15,7 @@ var c COINUT // Please supply your own keys here to do better tests const ( apiKey = "" - apiSecret = "" + clientID = "" canManipulateRealOrders = false ) @@ -31,7 +31,8 @@ func TestSetup(t *testing.T) { t.Error("Test Failed - Coinut Setup() init error") } bConfig.AuthenticatedAPISupport = true - bConfig.APISecret = apiSecret + bConfig.APIKey = apiKey + bConfig.ClientID = clientID bConfig.Verbose = true c.Setup(bConfig) @@ -250,3 +251,17 @@ func TestCancelExchangeOrder(t *testing.T) { t.Errorf("Could not cancel order: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + if apiKey != "" || clientID != "" { + _, err := c.GetAccountInfo() + if err != nil { + t.Error("Test Failed - GetAccountInfo() error", err) + } + } else { + _, err := c.GetAccountInfo() + if err == nil { + t.Error("Test Failed - GetAccountInfo() error") + } + } +} diff --git a/exchanges/coinut/coinut_types.go b/exchanges/coinut/coinut_types.go index 410f03c1..f42b198e 100644 --- a/exchanges/coinut/coinut_types.go +++ b/exchanges/coinut/coinut_types.go @@ -68,16 +68,24 @@ type Trades struct { // UserBalance holds user balances on the exchange type UserBalance struct { - BTC float64 `json:"btc,string"` - ETC float64 `json:"etc,string"` - ETH float64 `json:"eth,string"` - LTC float64 `json:"ltc,string"` - Equity float64 `json:"equity,string,string"` - InitialMargin float64 `json:"initial_margin,string"` - MaintenanceMargin float64 `json:"maintenance_margin,string"` - RealizedPL float64 `json:"realized_pl,string"` - TransID int64 `json:"trans_id"` - UnrealizedPL float64 `json:"unrealized_pl,string"` + BCH float64 `json:"BCH,string"` + BTC float64 `json:"BTC,string"` + BTG float64 `json:"BTG,string"` + CAD float64 `json:"CAD,string"` + ETC float64 `json:"ETC,string"` + ETH float64 `json:"ETH,string"` + LCH float64 `json:"LCH,string"` + LTC float64 `json:"LTC,string"` + MYR float64 `json:"MYR,string"` + SGD float64 `json:"SGD,string"` + USD float64 `json:"USD,string"` + USDT float64 `json:"USDT,string"` + XMR float64 `json:"XMR,string"` + ZEC float64 `json:"ZEC,string"` + Nonce int64 `json:"nonce"` + Reply string `json:"reply"` + Status []string `json:"status"` + TransID int64 `json:"trans_id"` } // Order holds order information diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 5c085514..afbe0588 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -7,6 +7,8 @@ import ( "strconv" "sync" + "github.com/thrasher-/gocryptotrader/currency/symbol" + "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges" @@ -53,23 +55,86 @@ func (c *COINUT) Run() { // GetAccountInfo retrieves balances for all enabled currencies for the // COINUT exchange func (c *COINUT) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - /* - response.ExchangeName = e.GetName() - accountBalance, err := e.GetAccounts() - if err != nil { - return response, err - } - for i := 0; i < len(accountBalance); i++ { - var exchangeCurrency exchange.AccountCurrencyInfo - exchangeCurrency.CurrencyName = accountBalance[i].Currency - exchangeCurrency.TotalValue = accountBalance[i].Available - exchangeCurrency.Hold = accountBalance[i].Hold + var info exchange.AccountInfo + bal, err := c.GetUserBalance() + if err != nil { + return info, err + } - response.Currencies = append(response.Currencies, exchangeCurrency) - } - */ - return response, nil + var balances []exchange.AccountCurrencyInfo + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.BCH, + TotalValue: bal.BCH, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.BTC, + TotalValue: bal.BTC, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.BTG, + TotalValue: bal.BTG, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.CAD, + TotalValue: bal.CAD, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.ETC, + TotalValue: bal.ETC, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.ETH, + TotalValue: bal.ETH, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.LCH, + TotalValue: bal.LCH, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.LTC, + TotalValue: bal.LTC, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.MYR, + TotalValue: bal.MYR, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.SGD, + TotalValue: bal.SGD, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.USD, + TotalValue: bal.USD, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.USDT, + TotalValue: bal.USDT, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.XMR, + TotalValue: bal.XMR, + }) + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: symbol.ZEC, + TotalValue: bal.ZEC, + }) + + info.ExchangeName = c.GetName() + info.Currencies = balances + return info, nil } // UpdateTicker updates and returns the ticker for a currency pair diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index be12d531..b23761c7 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -322,15 +322,10 @@ func (g *Gateio) GetSpotKline(arg KlinesRequestParams) ([]*KLineResponse, error) // GetBalances obtains the users account balance func (g *Gateio) GetBalances() (BalancesResponse, error) { - var result BalancesResponse - err := g.SendAuthenticatedHTTPRequest("POST", gateioBalances, "", &result) - if err != nil { - return result, err - } - - return result, nil + return result, + g.SendAuthenticatedHTTPRequest("POST", gateioBalances, "", &result) } // SpotNewOrder places a new order @@ -402,7 +397,28 @@ func (g *Gateio) SendAuthenticatedHTTPRequest(method, endpoint, param string, re url := fmt.Sprintf("%s/%s/%s", g.APIUrl, gateioAPIVersion, endpoint) - return g.SendPayload(method, url, headers, strings.NewReader(param), result, true, g.Verbose) + var intermidiary json.RawMessage + + err := g.SendPayload(method, url, headers, strings.NewReader(param), &intermidiary, true, g.Verbose) + if err != nil { + return err + } + + errCap := struct { + Result bool `json:"result,string"` + Code int `json:"code"` + Message string `json:"message"` + }{} + + if err := common.JSONDecode(intermidiary, &errCap); err == nil { + if !errCap.Result { + return fmt.Errorf("GateIO auth request error, code: %d message: %s", + errCap.Code, + errCap.Message) + } + } + + return common.JSONDecode(intermidiary, result) } // GetFee returns an estimate of fee based on type of transaction diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 0134bd7e..5ea4da34 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -308,3 +308,17 @@ func TestCancelExchangeOrder(t *testing.T) { t.Errorf("Could not cancel order: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + if apiSecret == "" || apiKey == "" { + _, err := g.GetAccountInfo() + if err == nil { + t.Error("Test Failed - GetAccountInfo() error") + } + } else { + _, err := g.GetAccountInfo() + if err != nil { + t.Error("Test Failed - GetAccountInfo() error", err) + } + } +} diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index 0c4b9900..81ea3215 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -53,9 +53,9 @@ type MarketInfoPairsResponse struct { // BalancesResponse holds the user balances type BalancesResponse struct { - Result string `json:"result"` - Available map[string]string `json:"available"` - Locked map[string]string `json:"locked"` + Result string `json:"result"` + Available []map[string]string `json:"available"` + Locked []map[string]string `json:"locked"` } // KlinesRequestParams represents Klines request data. diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 01bbcfd0..0f3a7efc 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -1,7 +1,6 @@ package gateio import ( - "errors" "fmt" "log" "strconv" @@ -110,8 +109,62 @@ func (g *Gateio) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbo // GetAccountInfo retrieves balances for all enabled currencies for the // ZB exchange func (g *Gateio) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - return response, errors.New("not implemented") + var info exchange.AccountInfo + + balance, err := g.GetBalances() + if err != nil { + return info, err + } + + if len(balance.Available) == 0 && len(balance.Locked) == 0 { + return info, nil + } + + var balances []exchange.AccountCurrencyInfo + + for _, data := range balance.Locked { + for key, amountStr := range data { + lockedF, err := strconv.ParseFloat(amountStr, 64) + if err != nil { + return info, err + } + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: key, + Hold: lockedF, + }) + } + } + + for _, data := range balance.Available { + for key, amountStr := range data { + availAmount, err := strconv.ParseFloat(amountStr, 64) + if err != nil { + return info, err + } + + var updated bool + for i := range balances { + if balances[i].CurrencyName == key { + balances[i].TotalValue = balances[i].Hold + availAmount + updated = true + break + } + } + + if !updated { + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: key, + TotalValue: availAmount, + }) + } + } + } + + info.Currencies = balances + info.ExchangeName = g.GetName() + + return info, nil } // GetFundingHistory returns funding history, deposits and diff --git a/exchanges/huobi/huobi.go b/exchanges/huobi/huobi.go index feb2ab5f..7d08101d 100644 --- a/exchanges/huobi/huobi.go +++ b/exchanges/huobi/huobi.go @@ -63,6 +63,7 @@ const ( // HUOBI is the overarching type across this package type HUOBI struct { exchange.Base + AccountID string WebsocketConn *websocket.Conn } @@ -367,7 +368,11 @@ func (h *HUOBI) GetAccountBalance(accountID string) ([]AccountBalanceDetail, err var result response endpoint := fmt.Sprintf(huobiAccountBalance, accountID) - err := h.SendAuthenticatedHTTPRequest("GET", endpoint, url.Values{}, nil, &result) + + v := url.Values{} + v.Set("account-id", accountID) + + err := h.SendAuthenticatedHTTPRequest("GET", endpoint, v, nil, &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index 043309fa..5ba5e19a 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -456,3 +456,17 @@ func TestCancelExchangeOrder(t *testing.T) { t.Errorf("Could not cancel order: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + if apiKey == "" || apiSecret == "" { + _, err := h.GetAccountInfo() + if err == nil { + t.Error("Test Failed - GetAccountInfo() error") + } + } else { + _, err := h.GetAccountInfo() + if err != nil { + t.Error("Test Failed - GetAccountInfo() error", err) + } + } +} diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 63c34a47..d7a4f0d7 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -149,12 +149,77 @@ func (h *HUOBI) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderboo return orderbook.GetOrderbook(h.Name, p, assetType) } +var mtx sync.Mutex + +// GetAccountID returns the account ID for trades NOTE interim implementation +// does not account for multiple account IDs +func (h *HUOBI) GetAccountID() (string, error) { + mtx.Lock() + defer mtx.Unlock() + + if h.AccountID == "" { + acc, err := h.GetAccounts() + if err != nil { + return "", err + } + + if len(acc) > 0 { + return strconv.FormatInt(acc[0].ID, 10), nil + } + + return "", errors.New("no user ID fetched") + } + + return h.AccountID, nil +} + //GetAccountInfo retrieves balances for all enabled currencies for the // HUOBI exchange - to-do func (h *HUOBI) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - response.ExchangeName = h.GetName() - return response, nil + var info exchange.AccountInfo + info.ExchangeName = h.GetName() + + accID, err := h.GetAccountID() + if err != nil { + return info, err + } + + acc, err := h.GetAccountBalance(accID) + if err != nil { + return info, err + } + + type hold struct { + Avail float64 + Hold float64 + } + + var currencyData = make(map[string]*hold) + for _, data := range acc { + _, ok := currencyData[data.Currency] + if !ok { + currencyData[data.Currency] = &hold{} + } + + if data.Type == "trade" { + currencyData[data.Currency].Avail = data.Balance + } else { + currencyData[data.Currency].Hold = data.Balance + } + } + + var balances []exchange.AccountCurrencyInfo + + for key, data := range currencyData { + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: key, + TotalValue: data.Avail + data.Hold, + Hold: data.Hold, + }) + } + + info.Currencies = balances + return info, nil } // GetFundingHistory returns funding history, deposits and diff --git a/exchanges/huobihadax/huobihadax.go b/exchanges/huobihadax/huobihadax.go index 7e47ad31..6cb54d22 100644 --- a/exchanges/huobihadax/huobihadax.go +++ b/exchanges/huobihadax/huobihadax.go @@ -55,6 +55,7 @@ const ( // HUOBIHADAX is the overarching type across this package type HUOBIHADAX struct { + AccountID string exchange.Base } diff --git a/exchanges/huobihadax/huobihadax_test.go b/exchanges/huobihadax/huobihadax_test.go index 17eebe8f..4d8fc213 100644 --- a/exchanges/huobihadax/huobihadax_test.go +++ b/exchanges/huobihadax/huobihadax_test.go @@ -435,3 +435,17 @@ func TestCancelExchangeOrder(t *testing.T) { t.Errorf("Could not cancel order: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + if apiKey == "" || apiSecret == "" { + _, err := h.GetAccountInfo() + if err == nil { + t.Error("Test Failed - GetAccountInfo() error") + } + } else { + _, err := h.GetAccountInfo() + if err != nil { + t.Error("Test Failed - GetAccountInfo() error", err) + } + } +} diff --git a/exchanges/huobihadax/huobihadax_wrapper.go b/exchanges/huobihadax/huobihadax_wrapper.go index d13c2418..8c8551d8 100644 --- a/exchanges/huobihadax/huobihadax_wrapper.go +++ b/exchanges/huobihadax/huobihadax_wrapper.go @@ -114,12 +114,77 @@ func (h *HUOBIHADAX) UpdateOrderbook(p pair.CurrencyPair, assetType string) (ord return orderbook.GetOrderbook(h.Name, p, assetType) } +var mtx sync.Mutex + +// GetAccountID returns the account ID for trades NOTE interim implementation +// does not account for multiple account IDs +func (h *HUOBIHADAX) GetAccountID() (string, error) { + mtx.Lock() + defer mtx.Unlock() + + if h.AccountID == "" { + acc, err := h.GetAccounts() + if err != nil { + return "", err + } + + if len(acc) > 0 { + return strconv.FormatInt(acc[0].ID, 10), nil + } + + return "", errors.New("no account ID fetched") + } + + return h.AccountID, nil +} + //GetAccountInfo retrieves balances for all enabled currencies for the // HUOBIHADAX exchange - to-do func (h *HUOBIHADAX) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - response.ExchangeName = h.GetName() - return response, nil + var info exchange.AccountInfo + info.ExchangeName = h.GetName() + + accID, err := h.GetAccountID() + if err != nil { + return info, err + } + + acc, err := h.GetAccountBalance(accID) + if err != nil { + return info, err + } + + type hold struct { + Avail float64 + Hold float64 + } + + var currencyData = make(map[string]*hold) + for _, data := range acc { + _, ok := currencyData[data.Currency] + if !ok { + currencyData[data.Currency] = &hold{} + } + + if data.Type == "trade" { + currencyData[data.Currency].Avail = data.Balance + } else { + currencyData[data.Currency].Hold = data.Balance + } + } + + var balances []exchange.AccountCurrencyInfo + + for key, data := range currencyData { + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: key, + TotalValue: data.Avail + data.Hold, + Hold: data.Hold, + }) + } + + info.Currencies = balances + return info, nil } // GetFundingHistory returns funding history, deposits and diff --git a/exchanges/itbit/itbit.go b/exchanges/itbit/itbit.go index 16b69653..962387f7 100644 --- a/exchanges/itbit/itbit.go +++ b/exchanges/itbit/itbit.go @@ -2,6 +2,7 @@ package itbit import ( "bytes" + "encoding/json" "errors" "fmt" "log" @@ -326,6 +327,10 @@ func (i *ItBit) SendAuthenticatedHTTPRequest(method string, path string, params return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, i.Name) } + if i.ClientID == "" { + return errors.New("client ID not set") + } + request := make(map[string]interface{}) url := i.APIUrl + path @@ -368,7 +373,29 @@ func (i *ItBit) SendAuthenticatedHTTPRequest(method string, path string, params headers["X-Auth-Nonce"] = nonce headers["Content-Type"] = "application/json" - return i.SendPayload(method, url, headers, bytes.NewBuffer([]byte(PayloadJSON)), result, true, i.Verbose) + var intermediary json.RawMessage + + errCheck := struct { + Code int `json:"code"` + Description string `json:"description"` + RequestID string `json:"requestId"` + }{} + + err = i.SendPayload(method, url, headers, bytes.NewBuffer([]byte(PayloadJSON)), &intermediary, true, i.Verbose) + if err != nil { + return err + } + + err = common.JSONDecode(intermediary, &errCheck) + if err == nil { + if errCheck.Code != 0 || errCheck.Description != "" { + return fmt.Errorf("itbit.go SendAuthRequest error code: %d description: %s", + errCheck.Code, + errCheck.Description) + } + } + + return common.JSONDecode(intermediary, result) } // GetFee returns an estimate of fee based on type of transaction diff --git a/exchanges/itbit/itbit_test.go b/exchanges/itbit/itbit_test.go index b1a4b13b..4cc890cf 100644 --- a/exchanges/itbit/itbit_test.go +++ b/exchanges/itbit/itbit_test.go @@ -302,3 +302,12 @@ func TestCancelExchangeOrder(t *testing.T) { t.Errorf("Could not cancel order: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + if apiKey != "" || apiSecret != "" || clientID != "" { + _, err := i.GetAccountInfo() + if err == nil { + t.Error("Test Failed - GetAccountInfo() error") + } + } +} diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index a5144afe..6ccddd55 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -3,6 +3,7 @@ package itbit import ( "fmt" "log" + "net/url" "strconv" "sync" @@ -107,12 +108,45 @@ func (i *ItBit) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderboo return orderbook.GetOrderbook(i.Name, p, assetType) } -// GetAccountInfo retrieves balances for all enabled currencies for the -//ItBit exchange - to-do +// GetAccountInfo retrieves balances for all enabled currencies func (i *ItBit) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - response.ExchangeName = i.GetName() - return response, nil + var info exchange.AccountInfo + info.ExchangeName = i.GetName() + + wallets, err := i.GetWallets(url.Values{}) + if err != nil { + return info, err + } + + type balance struct { + TotalValue float64 + Hold float64 + } + + var amounts = make(map[string]*balance) + + for _, wallet := range wallets { + for _, cb := range wallet.Balances { + if _, ok := amounts[cb.Currency]; !ok { + amounts[cb.Currency] = &balance{} + } + + amounts[cb.Currency].TotalValue += cb.TotalBalance + amounts[cb.Currency].Hold += cb.TotalBalance - cb.AvailableBalance + } + } + + var fullBalance []exchange.AccountCurrencyInfo + + for key, data := range amounts { + fullBalance = append(fullBalance, exchange.AccountCurrencyInfo{ + CurrencyName: key, + TotalValue: data.TotalValue, + Hold: data.Hold, + }) + } + + return info, nil } // GetFundingHistory returns funding history, deposits and diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index 77a58bab..ca6697f8 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -388,3 +388,17 @@ func TestCancelExchangeOrder(t *testing.T) { t.Errorf("Could not cancel order: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + if apiKey != "" || apiSecret != "" || clientID != "" { + _, err := k.GetAccountInfo() + if err != nil { + t.Error("Test Failed - GetAccountInfo() error", err) + } + } else { + _, err := k.GetAccountInfo() + if err == nil { + t.Error("Test Failed - GetAccountInfo() error") + } + } +} diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index b1a68430..7c1b0d8e 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -141,9 +141,24 @@ func (k *Kraken) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbo // GetAccountInfo retrieves balances for all enabled currencies for the // Kraken exchange - to-do func (k *Kraken) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - response.ExchangeName = k.GetName() - return response, nil + var info exchange.AccountInfo + info.ExchangeName = k.GetName() + + bal, err := k.GetBalance() + if err != nil { + return info, err + } + + var balances []exchange.AccountCurrencyInfo + for key, data := range bal { + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: key, + TotalValue: data, + }) + } + + info.Currencies = balances + return info, nil } // GetFundingHistory returns funding history, deposits and diff --git a/exchanges/okex/okex.go b/exchanges/okex/okex.go index b7b2e4e8..c97938b2 100644 --- a/exchanges/okex/okex.go +++ b/exchanges/okex/okex.go @@ -1,6 +1,7 @@ package okex import ( + "encoding/json" "errors" "fmt" "log" @@ -70,6 +71,8 @@ const ( spotWithdrawInfo = "withdraw_info" spotAccountRecords = "account_records" + myWalletInfo = "wallet_info.do" + // just your average return type from okex returnTypeOne = "map[string]interface {}" @@ -684,23 +687,22 @@ func (o *OKEX) SpotNewOrder(arg SpotNewOrderRequestParams) (int64, error) { // orderID orderID // returns orderID or an error func (o *OKEX) SpotCancelOrder(symbol string, argOrderID int64) (int64, error) { - type response struct { + var res = struct { Result bool `json:"result"` OrderID string `json:"order_id"` ErrorCode int `json:"error_code"` - } - - var res response + }{} params := url.Values{} params.Set("symbol", symbol) params.Set("order_id", strconv.FormatInt(argOrderID, 10)) - err := o.SendAuthenticatedHTTPRequest(spotCancelTrade, params, &res) var returnOrderID int64 - if err != nil && res.ErrorCode != 0 { + err := o.SendAuthenticatedHTTPRequest(spotCancelTrade+".do", params, &res) + if err != nil { return returnOrderID, err } + if res.ErrorCode != 0 { return returnOrderID, fmt.Errorf("ErrCode:%d ErrMsg:%s", res.ErrorCode, o.ErrorCodes[strconv.Itoa(res.ErrorCode)]) } @@ -934,7 +936,27 @@ func (o *OKEX) SendAuthenticatedHTTPRequest(method string, values url.Values, re headers := make(map[string]string) headers["Content-Type"] = "application/x-www-form-urlencoded" - return o.SendPayload("POST", path, headers, strings.NewReader(encoded), result, true, o.Verbose) + var intermediary json.RawMessage + + errCap := struct { + Result bool `json:"result"` + Error int64 `json:"error_code"` + }{} + + err = o.SendPayload("POST", path, headers, strings.NewReader(encoded), &intermediary, true, o.Verbose) + if err != nil { + return err + } + + err = common.JSONDecode(intermediary, &errCap) + if err == nil { + if !errCap.Result { + return fmt.Errorf("SendAuthenticatedHTTPRequest error - %s", + o.ErrorCodes[strconv.FormatInt(errCap.Error, 10)]) + } + } + + return common.JSONDecode(intermediary, result) } // SetErrorDefaults sets the full error default list @@ -944,7 +966,7 @@ func (o *OKEX) SetErrorDefaults() { "10000": errors.New("Required field, can not be null"), "10001": errors.New("Request frequency too high to exceed the limit allowed"), "10002": errors.New("System error"), - "10004": errors.New("Request failed"), + "10004": errors.New("Request failed - Your API key might need to be recreated"), "10005": errors.New("'SecretKey' does not exist"), "10006": errors.New("'Api_key' does not exist"), "10007": errors.New("Signature does not match"), @@ -1153,3 +1175,39 @@ func calculateTradingFee(purchasePrice, amount float64, isMaker bool) (fee float func getWithdrawalFee(currency string) float64 { return WithdrawalFees[currency] } + +// GetBalance returns the full balance accross all wallets +func (o *OKEX) GetBalance() ([]FullBalance, error) { + var resp Balance + var balances []FullBalance + + err := o.SendAuthenticatedHTTPRequest(myWalletInfo, url.Values{}, &resp) + if err != nil { + return balances, err + } + + for key, available := range resp.Info.Funds.Free { + free, err := strconv.ParseFloat(available, 64) + if err != nil { + return balances, err + } + + inUse, ok := resp.Info.Funds.Holds[key] + if !ok { + return balances, fmt.Errorf("hold currency %s not found in map", key) + } + + hold, err := strconv.ParseFloat(inUse, 64) + if err != nil { + return balances, err + } + + balances = append(balances, FullBalance{ + Currency: key, + Available: free, + Hold: hold, + }) + } + + return balances, nil +} diff --git a/exchanges/okex/okex_test.go b/exchanges/okex/okex_test.go index d600b40a..47e0b166 100644 --- a/exchanges/okex/okex_test.go +++ b/exchanges/okex/okex_test.go @@ -411,7 +411,6 @@ func isRealOrderTestEnabled() bool { func TestSubmitOrder(t *testing.T) { o.SetDefaults() TestSetup(t) - o.Verbose = true if !isRealOrderTestEnabled() { t.Skip() @@ -437,7 +436,6 @@ func TestCancelExchangeOrder(t *testing.T) { t.Skip() } - o.Verbose = true currencyPair := pair.NewCurrencyPair(symbol.LTC, symbol.BTC) var orderCancellation = exchange.OrderCancellation{ @@ -455,3 +453,17 @@ func TestCancelExchangeOrder(t *testing.T) { t.Errorf("Could not cancel order: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + if apiKey != "" || apiSecret != "" { + _, err := o.GetAccountInfo() + if err != nil { + t.Error("Test Failed - GetAccountInfo() error", err) + } + } else { + _, err := o.GetAccountInfo() + if err == nil { + t.Error("Test Failed - GetAccountInfo() error") + } + } +} diff --git a/exchanges/okex/okex_types.go b/exchanges/okex/okex_types.go index 743573a3..40a91ea2 100644 --- a/exchanges/okex/okex_types.go +++ b/exchanges/okex/okex_types.go @@ -428,3 +428,20 @@ var WithdrawalFees = map[string]float64{ symbol.ZIL: 20, symbol.ZIP: 1000, } + +// FullBalance defines a structured return type with balance data +type FullBalance struct { + Available float64 + Currency string + Hold float64 +} + +// Balance defines returned balance data +type Balance struct { + Info struct { + Funds struct { + Free map[string]string `json:"free"` + Holds map[string]string `json:"holds"` + } `json:"funds"` + } `json:"info"` +} diff --git a/exchanges/okex/okex_wrapper.go b/exchanges/okex/okex_wrapper.go index cf3848a4..6c5d84b8 100644 --- a/exchanges/okex/okex_wrapper.go +++ b/exchanges/okex/okex_wrapper.go @@ -151,8 +151,24 @@ func (o *OKEX) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook // GetAccountInfo retrieves balances for all enabled currencies for the // OKEX exchange func (o *OKEX) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - return response, errors.New("not implemented") + var info exchange.AccountInfo + bal, err := o.GetBalance() + if err != nil { + return info, err + } + + var balances []exchange.AccountCurrencyInfo + for _, data := range bal { + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: data.Currency, + TotalValue: data.Available + data.Hold, + Hold: data.Hold, + }) + } + + info.ExchangeName = o.GetName() + info.Currencies = balances + return info, nil } // GetFundingHistory returns funding history, deposits and @@ -219,13 +235,11 @@ func (o *OKEX) ModifyOrder(orderID int64, action exchange.ModifyOrder) (int64, e // CancelOrder cancels an order by its corresponding ID number func (o *OKEX) CancelOrder(order exchange.OrderCancellation) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) - if err != nil { return err } _, err = o.SpotCancelOrder(exchange.FormatExchangeCurrency(o.Name, order.CurrencyPair).String(), orderIDInt) - return err } diff --git a/exchanges/zb/zb.go b/exchanges/zb/zb.go index f0fc7308..861a0e5d 100644 --- a/exchanges/zb/zb.go +++ b/exchanges/zb/zb.go @@ -336,13 +336,34 @@ func (z *ZB) SendAuthenticatedHTTPRequest(method, endpoint string, values url.Va endpoint, values.Encode()) - return z.SendPayload(method, + var intermediary json.RawMessage + + errCap := struct { + Code int64 `json:"code"` + Message string `json:"message"` + }{} + + err := z.SendPayload(method, url, nil, strings.NewReader(""), - result, + &intermediary, true, z.Verbose) + if err != nil { + return err + } + + err = common.JSONDecode(intermediary, &errCap) + if err == nil { + if errCap.Code != 0 { + return fmt.Errorf("SendAuthenticatedHTTPRequest error code: %d message %s", + errCap.Code, + errorCode[errCap.Code]) + } + } + + return common.JSONDecode(intermediary, result) } // GetFee returns an estimate of fee based on type of transaction @@ -369,3 +390,34 @@ func calculateTradingFee(purchasePrice, amount float64) (fee float64) { func getWithdrawalFee(currency string) float64 { return WithdrawalFees[currency] } + +var errorCode = map[int64]string{ + 1000: "Successful call", + 1001: "General error message", + 1002: "internal error", + 1003: "Verification failed", + 1004: "Financial security password lock", + 1005: "The fund security password is incorrect. Please confirm and re-enter.", + 1006: "Real-name certification is awaiting review or review", + 1009: "This interface is being maintained", + 1010: "Not open yet", + 1012: "Insufficient permissions", + 1013: "Can not trade, if you have any questions, please contact online customer service", + 1014: "Cannot be sold during the pre-sale period", + 2002: "Insufficient balance in Bitcoin account", + 2003: "Insufficient balance of Litecoin account", + 2005: "Insufficient balance in Ethereum account", + 2006: "Insufficient balance in ETC currency account", + 2007: "Insufficient balance of BTS currency account", + 2009: "Insufficient account balance", + 3001: "Pending order not found", + 3002: "Invalid amount", + 3003: "Invalid quantity", + 3004: "User does not exist", + 3005: "Invalid parameter", + 3006: "Invalid IP or inconsistent with the bound IP", + 3007: "Request time has expired", + 3008: "Transaction history not found", + 4001: "API interface is locked", + 4002: "Request too frequently", +} diff --git a/exchanges/zb/zb_test.go b/exchanges/zb/zb_test.go index db55a06c..b2e82534 100644 --- a/exchanges/zb/zb_test.go +++ b/exchanges/zb/zb_test.go @@ -112,19 +112,6 @@ func TestGetMarkets(t *testing.T) { } } -func TestGetAccountInfo(t *testing.T) { - t.Parallel() - - if z.APIKey == "" || z.APISecret == "" { - t.Skip() - } - - _, err := z.GetAccountInfo() - if err != nil { - t.Errorf("Test failed - ZB GetAccountInfo: %s", err) - } -} - func TestGetSpotKline(t *testing.T) { t.Parallel() @@ -300,3 +287,17 @@ func TestCancelExchangeOrder(t *testing.T) { t.Errorf("Could not cancel order: %s", err) } } + +func TestGetAccountInfo(t *testing.T) { + if apiKey != "" || apiSecret != "" { + _, err := z.GetAccountInfo() + if err != nil { + t.Error("Test Failed - GetAccountInfo() error", err) + } + } else { + _, err := z.GetAccountInfo() + if err == nil { + t.Error("Test Failed - GetAccountInfo() error") + } + } +} diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index 0255fd8d..b4e4a9e6 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -1,7 +1,6 @@ package zb import ( - "errors" "fmt" "log" "strconv" @@ -119,8 +118,34 @@ func (z *ZB) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.B // GetAccountInfo retrieves balances for all enabled currencies for the // ZB exchange func (z *ZB) GetAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - return response, errors.New("not implemented") + var info exchange.AccountInfo + bal, err := z.GetAccountInformation() + if err != nil { + return info, err + } + + var balances []exchange.AccountCurrencyInfo + for _, data := range bal.Result.Coins { + hold, err := strconv.ParseFloat(data.Freez, 64) + if err != nil { + return info, err + } + + avail, err := strconv.ParseFloat(data.Available, 64) + if err != nil { + return info, err + } + + balances = append(balances, exchange.AccountCurrencyInfo{ + CurrencyName: data.EnName, + TotalValue: hold + avail, + Hold: hold, + }) + } + + info.ExchangeName = z.GetName() + info.Currencies = balances + return info, nil } // GetFundingHistory returns funding history, deposits and