diff --git a/engine/routines.go b/engine/routines.go index 49deb313..ba396df2 100644 --- a/engine/routines.go +++ b/engine/routines.go @@ -9,6 +9,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/stats" @@ -323,6 +324,11 @@ func (bot *Engine) WebsocketDataHandler(exchName string, data interface{}) error } printOrderbookSummary(d, "websocket", bot, nil) case *order.Detail: + if bot.Settings.Verbose { + printOrderSummary(d) + } + // TODO: Dont check if exists this creates two locks, on conflict update + // else insert. if !bot.OrderManager.orderStore.exists(d) { err := bot.OrderManager.orderStore.Add(d) if err != nil { @@ -335,9 +341,11 @@ func (bot *Engine) WebsocketDataHandler(exchName string, data interface{}) error } od.UpdateOrderFromDetail(d) } - case *order.Cancel: - return bot.OrderManager.Cancel(d) case *order.Modify: + if bot.Settings.Verbose { + printOrderChangeSummary(d) + } + // TODO: On conflict update or insert if not found od, err := bot.OrderManager.orderStore.GetByExchangeAndID(d.Exchange, d.ID) if err != nil { return err @@ -347,6 +355,10 @@ func (bot *Engine) WebsocketDataHandler(exchName string, data interface{}) error return errors.New(d.Error()) case stream.UnhandledMessageWarning: log.Warn(log.WebsocketMgr, d.Message) + case account.Change: + if bot.Settings.Verbose { + printAccountHoldingsChangeSummary(d) + } default: if bot.Settings.Verbose { log.Warnf(log.WebsocketMgr, @@ -357,3 +369,59 @@ func (bot *Engine) WebsocketDataHandler(exchName string, data interface{}) error } return nil } + +// printOrderChangeSummary this function will be deprecated when a order manager +// update is done. +func printOrderChangeSummary(m *order.Modify) { + if m == nil { + return + } + log.Debugf(log.WebsocketMgr, + "Order Change: %s %s %s %s %s %s OrderID:%s ClientOrderID:%s Price:%f Amount:%f Executed Amount:%f Remaining Amount:%f", + m.Exchange, + m.AssetType, + m.Pair, + m.Status, + m.Type, + m.Side, + m.ID, + m.ClientOrderID, + m.Price, + m.Amount, + m.ExecutedAmount, + m.RemainingAmount) +} + +// printOrderSummary this function will be deprecated when a order manager +// update is done. +func printOrderSummary(m *order.Detail) { + if m == nil { + return + } + log.Debugf(log.WebsocketMgr, + "New Order: %s %s %s %s %s %s OrderID:%s ClientOrderID:%s Price:%f Amount:%f Executed Amount:%f Remaining Amount:%f", + m.Exchange, + m.AssetType, + m.Pair, + m.Status, + m.Type, + m.Side, + m.ID, + m.ClientOrderID, + m.Price, + m.Amount, + m.ExecutedAmount, + m.RemainingAmount) +} + +// printAccountHoldingsChangeSummary this function will be deprecated when a +// account holdings update is done. +func printAccountHoldingsChangeSummary(m account.Change) { + log.Debugf(log.WebsocketMgr, + "Account Holdings Balance Changed: %s %s %s has changed balance by %f for account: %s", + m.Exchange, + m.Asset, + m.Currency, + m.Amount, + m.Account) +} diff --git a/exchanges/account/account_types.go b/exchanges/account/account_types.go index 3416af37..7e1b2339 100644 --- a/exchanges/account/account_types.go +++ b/exchanges/account/account_types.go @@ -47,3 +47,12 @@ type Balance struct { TotalValue float64 Hold float64 } + +// Change defines incoming balance change on currency holdings +type Change struct { + Exchange string + Currency currency.Code + Asset asset.Item + Amount float64 + Account string +} diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index e4f73618..0e6c9255 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -423,7 +423,7 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error { Err: err, } } - b.Websocket.DataHandler <- &order.Cancel{ + b.Websocket.DataHandler <- &order.Modify{ Price: response.Data[x].Price, Amount: response.Data[x].OrderQuantity, Exchange: b.Name, diff --git a/exchanges/bittrex/bittrex_test.go b/exchanges/bittrex/bittrex_test.go index 11e089d2..5d707814 100644 --- a/exchanges/bittrex/bittrex_test.go +++ b/exchanges/bittrex/bittrex_test.go @@ -304,8 +304,8 @@ func TestGetFee(t *testing.T) { // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := b.GetFee(feeBuilder); resp != float64(0.00015) || err != nil { - t.Errorf("Expected: %f, Received: %f", float64(0.00015), resp) + if resp, err := b.GetFee(feeBuilder); resp != float64(0.0003) || err != nil { + t.Errorf("Expected: %f, Received: %f", float64(0.0003), resp) t.Error(err) } diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 5bc44077..552ba0da 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -238,6 +238,7 @@ const ( Open Status = "OPEN" AutoDeleverage Status = "ADL" Closed Status = "CLOSED" + Pending Status = "PENDING" ) // Type enforces a standard for order types across the code base diff --git a/exchanges/poloniex/currency_details.go b/exchanges/poloniex/currency_details.go new file mode 100644 index 00000000..7bc242f1 --- /dev/null +++ b/exchanges/poloniex/currency_details.go @@ -0,0 +1,235 @@ +package poloniex + +import ( + "errors" + "strconv" + "sync" + + "github.com/thrasher-corp/gocryptotrader/currency" +) + +// CurrencyDetails stores a map of currencies associated with their ID +type CurrencyDetails struct { + pairs map[float64]PairSummaryInfo + codes map[float64]CodeSummaryInfo + // Mutex used for future when we will periodically update this table every + // week or so in production + m sync.RWMutex +} + +// PairSummaryInfo defines currency pair information +type PairSummaryInfo struct { + Pair currency.Pair + IsFrozen bool + PostOnly bool +} + +// CodeSummaryInfo defines currency information +type CodeSummaryInfo struct { + Currency currency.Code + WithdrawalTXFee float64 + MinimumConfirmations int64 + DepositAddress string + WithdrawalDepositDisabled bool + Frozen bool +} + +var ( + errCannotLoadNoData = errors.New("cannot load websocket currency data as data is nil") + errNoDepositAddress = errors.New("no public deposit address for currency") + errPairMapIsNil = errors.New("cannot get currency pair, map is nil") + errCodeMapIsNil = errors.New("cannot get currency code, map is nil") + errCurrencyNotFoundInMap = errors.New("currency not found") +) + +// loadPairs loads currency pair associations with unique identifiers from +// ticker data map +func (w *CurrencyDetails) loadPairs(data map[string]Ticker) error { + if data == nil { + return errCannotLoadNoData + } + w.m.Lock() + defer w.m.Unlock() + for k, v := range data { + pair, err := currency.NewPairFromString(k) + if err != nil { + return err + } + + if w.pairs == nil { + w.pairs = make(map[float64]PairSummaryInfo) + } + w.pairs[v.ID] = PairSummaryInfo{ + Pair: pair, + IsFrozen: v.IsFrozen == 1, + PostOnly: v.PostOnly == 1, + } + } + return nil +} + +// loadCodes loads currency codes from currency map +func (w *CurrencyDetails) loadCodes(data map[string]Currencies) error { + if data == nil { + return errCannotLoadNoData + } + w.m.Lock() + defer w.m.Unlock() + for k, v := range data { + if v.Delisted == 1 { + continue + } + + if w.codes == nil { + w.codes = make(map[float64]CodeSummaryInfo) + } + + w.codes[v.ID] = CodeSummaryInfo{ + Currency: currency.NewCode(k), + WithdrawalTXFee: v.TxFee, + MinimumConfirmations: v.MinConfirmations, + DepositAddress: v.DepositAddress, + WithdrawalDepositDisabled: v.WithdrawalDepositDisabled == 1, + Frozen: v.Frozen == 1, + } + } + return nil +} + +// GetPair returns a currency pair by its ID +func (w *CurrencyDetails) GetPair(id float64) (currency.Pair, error) { + w.m.RLock() + defer w.m.RUnlock() + if w.pairs == nil { + return currency.Pair{}, errPairMapIsNil + } + + p, ok := w.pairs[id] + if ok { + return p.Pair, nil + } + + // This is here so we can still log an order with the ID as the currency + // pair which you can then cross reference later with the exchange ID list, + // rather than error out. + op, err := currency.NewPairFromString(strconv.FormatFloat(id, 'f', -1, 64)) + if err != nil { + return op, err + } + return op, errIDNotFoundInPairMap +} + +// GetCode returns a currency code by its ID +func (w *CurrencyDetails) GetCode(id float64) (currency.Code, error) { + w.m.RLock() + defer w.m.RUnlock() + if w.codes == nil { + return currency.Code{}, errCodeMapIsNil + } + c, ok := w.codes[id] + if ok { + return c.Currency, nil + } + return currency.Code{}, errIDNotFoundInCodeMap +} + +// GetWithdrawalTXFee returns withdrawal transaction fee for the currency +func (w *CurrencyDetails) GetWithdrawalTXFee(c currency.Code) (float64, error) { + w.m.RLock() + defer w.m.RUnlock() + if w.codes == nil { + return 0, errCodeMapIsNil + } + for _, v := range w.codes { + if v.Currency == c { + return v.WithdrawalTXFee, nil + } + } + return 0, errCurrencyNotFoundInMap +} + +// GetDepositAddress returns the public deposit address details for the currency +func (w *CurrencyDetails) GetDepositAddress(c currency.Code) (string, error) { + w.m.RLock() + defer w.m.RUnlock() + if w.codes == nil { + return "", errCodeMapIsNil + } + for _, v := range w.codes { + if v.Currency == c { + if v.DepositAddress == "" { + return "", errNoDepositAddress + } + return v.DepositAddress, nil + } + } + return "", errCurrencyNotFoundInMap +} + +// IsWithdrawAndDepositsEnabled returns if withdrawals or deposits are enabled +func (w *CurrencyDetails) IsWithdrawAndDepositsEnabled(c currency.Code) (bool, error) { + w.m.RLock() + defer w.m.RUnlock() + if w.codes == nil { + return false, errCodeMapIsNil + } + for _, v := range w.codes { + if v.Currency == c { + return !v.WithdrawalDepositDisabled, nil + } + } + return false, errCurrencyNotFoundInMap +} + +// IsTradingEnabledForCurrency returns if the currency is allowed to be traded +func (w *CurrencyDetails) IsTradingEnabledForCurrency(c currency.Code) (bool, error) { + w.m.RLock() + defer w.m.RUnlock() + if w.codes == nil { + return false, errCodeMapIsNil + } + for _, v := range w.codes { + if v.Currency == c { + return !v.Frozen, nil + } + } + return false, errCurrencyNotFoundInMap +} + +// IsTradingEnabledForPair returns if the currency pair is allowed to be traded +func (w *CurrencyDetails) IsTradingEnabledForPair(pair currency.Pair) (bool, error) { + w.m.RLock() + defer w.m.RUnlock() + if w.codes == nil { + return false, errCodeMapIsNil + } + for _, v := range w.pairs { + if v.Pair.Equal(pair) { + return !v.IsFrozen, nil + } + } + return false, errCurrencyNotFoundInMap +} + +// IsPostOnlyForPair returns if an order is allowed to take liquidity from the +// books or reduce positions +func (w *CurrencyDetails) IsPostOnlyForPair(pair currency.Pair) (bool, error) { + w.m.RLock() + defer w.m.RUnlock() + if w.codes == nil { + return false, errCodeMapIsNil + } + for _, v := range w.pairs { + if v.Pair.Equal(pair) { + return v.PostOnly, nil + } + } + return false, errCurrencyNotFoundInMap +} + +// isInitial checks state of maps to determine if they have been loaded or not +func (w *CurrencyDetails) isInitial() bool { + w.m.RLock() + defer w.m.RUnlock() + return w.codes == nil || w.pairs == nil +} diff --git a/exchanges/poloniex/currency_details_test.go b/exchanges/poloniex/currency_details_test.go new file mode 100644 index 00000000..ffbb8d98 --- /dev/null +++ b/exchanges/poloniex/currency_details_test.go @@ -0,0 +1,209 @@ +package poloniex + +import ( + "errors" + "testing" + + "github.com/thrasher-corp/gocryptotrader/currency" +) + +func TestWsCurrencyMap(t *testing.T) { + var m CurrencyDetails + + if !m.isInitial() { + t.Fatal("unexpected value") + } + + err := m.loadPairs(nil) + if !errors.Is(err, errCannotLoadNoData) { + t.Fatalf("expected: %v but received: %v", errCannotLoadNoData, err) + } + + err = m.loadCodes(nil) + if !errors.Is(err, errCannotLoadNoData) { + t.Fatalf("expected: %v but received: %v", errCannotLoadNoData, err) + } + + _, err = m.GetPair(1337) + if !errors.Is(err, errPairMapIsNil) { + t.Fatalf("expected: %v but received: %v", errPairMapIsNil, err) + } + + _, err = m.GetCode(1337) + if !errors.Is(err, errCodeMapIsNil) { + t.Fatalf("expected: %v but received: %v", errCodeMapIsNil, err) + } + + _, err = m.GetWithdrawalTXFee(currency.Code{}) + if !errors.Is(err, errCodeMapIsNil) { + t.Fatalf("expected: %v but received: %v", errCodeMapIsNil, err) + } + + _, err = m.GetDepositAddress(currency.Code{}) + if !errors.Is(err, errCodeMapIsNil) { + t.Fatalf("expected: %v but received: %v", errCodeMapIsNil, err) + } + + _, err = m.IsWithdrawAndDepositsEnabled(currency.Code{}) + if !errors.Is(err, errCodeMapIsNil) { + t.Fatalf("expected: %v but received: %v", errCodeMapIsNil, err) + } + + _, err = m.IsTradingEnabledForCurrency(currency.Code{}) + if !errors.Is(err, errCodeMapIsNil) { + t.Fatalf("expected: %v but received: %v", errCodeMapIsNil, err) + } + + _, err = m.IsTradingEnabledForPair(currency.Pair{}) + if !errors.Is(err, errCodeMapIsNil) { + t.Fatalf("expected: %v but received: %v", errCodeMapIsNil, err) + } + + _, err = m.IsPostOnlyForPair(currency.Pair{}) + if !errors.Is(err, errCodeMapIsNil) { + t.Fatalf("expected: %v but received: %v", errCodeMapIsNil, err) + } + + c, err := p.GetCurrencies() + if err != nil { + t.Fatal(err) + } + + err = m.loadCodes(c) + if err != nil { + t.Fatal(err) + } + + tick, err := p.GetTicker() + if err != nil { + t.Fatal(err) + } + + err = m.loadPairs(tick) + if err != nil { + t.Fatal(err) + } + + pTest, err := m.GetPair(1337) + if !errors.Is(err, errIDNotFoundInPairMap) { + t.Fatalf("expected: %v but received: %v", errIDNotFoundInPairMap, err) + } + + if pTest.String() != "1337" { + t.Fatal("unexpected value") + } + + _, err = m.GetCode(1337) + if !errors.Is(err, errIDNotFoundInCodeMap) { + t.Fatalf("expected: %v but received: %v", errIDNotFoundInCodeMap, err) + } + + btcusdt, err := m.GetPair(121) + if !errors.Is(err, nil) { + t.Fatalf("expected: %v but received: %v", nil, err) + } + + if btcusdt.String() != "USDT_BTC" { + t.Fatal("expecting USDT_BTC pair") + } + + maid, err := m.GetCode(127) + if !errors.Is(err, nil) { + t.Fatalf("expected: %v but received: %v", nil, err) + } + + if maid.String() != "MAID" { + t.Fatal("unexpected value") + } + + txFee, err := m.GetWithdrawalTXFee(maid) + if err != nil { + t.Fatal(err) + } + + if txFee != 80 { + t.Fatal("unexpected value") + } + + _, err = m.GetDepositAddress(maid) + if !errors.Is(err, errNoDepositAddress) { + t.Fatalf("expected: %v but received: %v", errNoDepositAddress, err) + } + + dAddr, err := m.GetDepositAddress(currency.NewCode("BCN")) + if !errors.Is(err, nil) { + t.Fatalf("expected: %v but received: %v", nil, err) + } + + if dAddr != "25cZNQYVAi3issDCoa6fWA2Aogd4FgPhYdpX3p8KLfhKC6sN8s6Q9WpcW4778TPwcUS5jEM25JrQvjD3XjsvXuNHSWhYUsu" { + t.Fatal("unexpected deposit address") + } + + wdEnabled, err := m.IsWithdrawAndDepositsEnabled(maid) + if !errors.Is(err, nil) { + t.Fatalf("expected: %v but received: %v", nil, err) + } + + if !wdEnabled { + t.Fatal("unexpected results") + } + + tEnabled, err := m.IsTradingEnabledForCurrency(maid) + if !errors.Is(err, nil) { + t.Fatalf("expected: %v but received: %v", nil, err) + } + + if !tEnabled { + t.Fatal("unexpected results") + } + + cp := currency.NewPair(currency.USDT, currency.BTC) + + tEnabled, err = m.IsTradingEnabledForPair(cp) + if !errors.Is(err, nil) { + t.Fatalf("expected: %v but received: %v", nil, err) + } + + if !tEnabled { + t.Fatal("unexpected results") + } + + postOnly, err := m.IsPostOnlyForPair(cp) + if !errors.Is(err, nil) { + t.Fatalf("expected: %v but received: %v", nil, err) + } + + if postOnly { + t.Fatal("unexpected results") + } + + _, err = m.GetWithdrawalTXFee(currency.Code{}) + if !errors.Is(err, errCurrencyNotFoundInMap) { + t.Fatalf("expected: %v but received: %v", errCurrencyNotFoundInMap, err) + } + + _, err = m.GetDepositAddress(currency.Code{}) + if !errors.Is(err, errCurrencyNotFoundInMap) { + t.Fatalf("expected: %v but received: %v", errCurrencyNotFoundInMap, err) + } + + _, err = m.IsWithdrawAndDepositsEnabled(currency.Code{}) + if !errors.Is(err, errCurrencyNotFoundInMap) { + t.Fatalf("expected: %v but received: %v", errCurrencyNotFoundInMap, err) + } + + _, err = m.IsTradingEnabledForCurrency(currency.Code{}) + if !errors.Is(err, errCurrencyNotFoundInMap) { + t.Fatalf("expected: %v but received: %v", errCurrencyNotFoundInMap, err) + } + + _, err = m.IsTradingEnabledForPair(currency.Pair{}) + if !errors.Is(err, errCurrencyNotFoundInMap) { + t.Fatalf("expected: %v but received: %v", errCurrencyNotFoundInMap, err) + } + + _, err = m.IsPostOnlyForPair(currency.Pair{}) + if !errors.Is(err, errCurrencyNotFoundInMap) { + t.Fatalf("expected: %v but received: %v", errCurrencyNotFoundInMap, err) + } +} diff --git a/exchanges/poloniex/poloniex.go b/exchanges/poloniex/poloniex.go index f5874baf..076a4304 100644 --- a/exchanges/poloniex/poloniex.go +++ b/exchanges/poloniex/poloniex.go @@ -55,6 +55,7 @@ const ( // Poloniex is the overarching type across the poloniex package type Poloniex struct { exchange.Base + details CurrencyDetails } // GetTicker returns current ticker information @@ -258,27 +259,15 @@ func (p *Poloniex) GetBalances() (Balance, error) { // GetCompleteBalances returns complete balances from your account. func (p *Poloniex) GetCompleteBalances() (CompleteBalances, error) { - var result interface{} - - err := p.SendAuthenticatedHTTPRequest(exchange.RestSpot, http.MethodPost, poloniexBalancesComplete, url.Values{}, &result) - if err != nil { - return CompleteBalances{}, err - } - - data := result.(map[string]interface{}) - balance := CompleteBalances{} - balance.Currency = make(map[string]CompleteBalance) - - for x, y := range data { - dataVals := y.(map[string]interface{}) - balancesData := CompleteBalance{} - balancesData.Available, _ = strconv.ParseFloat(dataVals["available"].(string), 64) - balancesData.OnOrders, _ = strconv.ParseFloat(dataVals["onOrders"].(string), 64) - balancesData.BTCValue, _ = strconv.ParseFloat(dataVals["btcValue"].(string), 64) - balance.Currency[x] = balancesData - } - - return balance, nil + var result CompleteBalances + vals := url.Values{} + vals.Set("account", "all") + err := p.SendAuthenticatedHTTPRequest(exchange.RestSpot, + http.MethodPost, + poloniexBalancesComplete, + vals, + &result) + return result, err } // GetDepositAddresses returns deposit addresses for all enabled cryptos. diff --git a/exchanges/poloniex/poloniex_test.go b/exchanges/poloniex/poloniex_test.go index 0fa716a5..6ea4e946 100644 --- a/exchanges/poloniex/poloniex_test.go +++ b/exchanges/poloniex/poloniex_test.go @@ -1,6 +1,7 @@ package poloniex import ( + "errors" "net/http" "strings" "testing" @@ -565,7 +566,7 @@ func TestWsSubAck(t *testing.T) { } func TestWsTicker(t *testing.T) { - err := p.getCurrencyIDMap() + err := p.loadCurrencyDetails() if err != nil { t.Error(err) } @@ -577,7 +578,7 @@ func TestWsTicker(t *testing.T) { } func TestWsExchangeVolume(t *testing.T) { - err := p.getCurrencyIDMap() + err := p.loadCurrencyDetails() if err != nil { t.Error(err) } @@ -589,7 +590,8 @@ func TestWsExchangeVolume(t *testing.T) { } func TestWsTrades(t *testing.T) { - err := p.getCurrencyIDMap() + p.SetSaveTradeDataStatus(true) + err := p.loadCurrencyDetails() if err != nil { t.Error(err) } @@ -601,7 +603,7 @@ func TestWsTrades(t *testing.T) { } func TestWsPriceAggregateOrderbook(t *testing.T) { - err := p.getCurrencyIDMap() + err := p.loadCurrencyDetails() if err != nil { t.Error(err) } @@ -617,25 +619,6 @@ func TestWsPriceAggregateOrderbook(t *testing.T) { t.Error(err) } } -func TestWsHandleAccountData(t *testing.T) { - t.Parallel() - err := p.getCurrencyIDMap() - if err != nil { - t.Error(err) - } - jsons := []string{ - `[1000,"",[["o",807230187,"0.00000000", "f"],["b",267,"e","0.10000000"]]]`, - `[1000,"",[["n",50,807230187,0,"1000.00000000","0.10000000","2018-11-07 16:42:42"],["b",267,"e","-0.10000000"]]]`, - `[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345"]]]`, - `[1000,"",[["k", 1337, ""]]]`, - } - for i := range jsons { - err := p.wsHandleData([]byte(jsons[i])) - if err != nil { - t.Error(err) - } - } -} func TestGetHistoricCandles(t *testing.T) { currencyPair, err := currency.NewPairFromString("BTC_LTC") @@ -712,3 +695,330 @@ func TestGetHistoricTrades(t *testing.T) { t.Error(err) } } + +func TestProcessAccountMarginPosition(t *testing.T) { + err := p.loadCurrencyDetails() + if err != nil { + t.Error(err) + } + + margin := []byte(`[1000,"",[["m", 23432933, 28, "-0.06000000"]]]`) + err = p.wsHandleData(margin) + if !errors.Is(err, errNotEnoughData) { + t.Fatalf("expected: %v but received: %v", errNotEnoughData, err) + } + + margin = []byte(`[1000,"",[["m", "23432933", 28, "-0.06000000", null]]]`) + err = p.wsHandleData(margin) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + margin = []byte(`[1000,"",[["m", 23432933, "28", "-0.06000000", null]]]`) + err = p.wsHandleData(margin) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + margin = []byte(`[1000,"",[["m", 23432933, 28, -0.06000000, null]]]`) + err = p.wsHandleData(margin) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + margin = []byte(`[1000,"",[["m", 23432933, 28, "-0.06000000", null]]]`) + err = p.wsHandleData(margin) + if err != nil { + t.Fatal(err) + } +} + +func TestProcessAccountPendingOrder(t *testing.T) { + err := p.loadCurrencyDetails() + if err != nil { + t.Error(err) + } + + pending := []byte(`[1000,"",[["p",431682155857,127,"1000.00000000","1.00000000","0"]]]`) + err = p.wsHandleData(pending) + if !errors.Is(err, errNotEnoughData) { + t.Fatalf("expected: %v but received: %v", errNotEnoughData, err) + } + + pending = []byte(`[1000,"",[["p","431682155857",127,"1000.00000000","1.00000000","0",null]]]`) + err = p.wsHandleData(pending) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + pending = []byte(`[1000,"",[["p",431682155857,"127","1000.00000000","1.00000000","0",null]]]`) + err = p.wsHandleData(pending) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + pending = []byte(`[1000,"",[["p",431682155857,127,1000.00000000,"1.00000000","0",null]]]`) + err = p.wsHandleData(pending) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + pending = []byte(`[1000,"",[["p",431682155857,127,"1000.00000000",1.00000000,"0",null]]]`) + err = p.wsHandleData(pending) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + pending = []byte(`[1000,"",[["p",431682155857,127,"1000.00000000","1.00000000",0,null]]]`) + err = p.wsHandleData(pending) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + pending = []byte(`[1000,"",[["p",431682155857,127,"1000.00000000","1.00000000","0",null]]]`) + err = p.wsHandleData(pending) + if err != nil { + t.Fatal(err) + } + + // Unmatched pair in system + pending = []byte(`[1000,"",[["p",431682155857,666,"1000.00000000","1.00000000","0",null]]]`) + err = p.wsHandleData(pending) + if err != nil { + t.Fatal(err) + } +} + +func TestProcessAccountOrderUpdate(t *testing.T) { + orderUpdate := []byte(`[1000,"",[["o",431682155857,"0.00000000","f"]]]`) + err := p.wsHandleData(orderUpdate) + if !errors.Is(err, errNotEnoughData) { + t.Fatalf("expected: %v but received: %v", errNotEnoughData, err) + } + + orderUpdate = []byte(`[1000,"",[["o","431682155857","0.00000000","f",null]]]`) + err = p.wsHandleData(orderUpdate) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + orderUpdate = []byte(`[1000,"",[["o",431682155857,0.00000000,"f",null]]]`) + err = p.wsHandleData(orderUpdate) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.00000000",123,null]]]`) + err = p.wsHandleData(orderUpdate) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.00000000","c",null]]]`) + err = p.wsHandleData(orderUpdate) + if !errors.Is(err, errNotEnoughData) { + t.Fatalf("expected: %v but received: %v", errNotEnoughData, err) + } + + orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.50000000","c",null,"0.50000000"]]]`) + err = p.wsHandleData(orderUpdate) + if err != nil { + t.Fatal(err) + } + + orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.00000000","c",null,"1.00000000"]]]`) + err = p.wsHandleData(orderUpdate) + if err != nil { + t.Fatal(err) + } + + orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.50000000","f",null]]]`) + err = p.wsHandleData(orderUpdate) + if err != nil { + t.Fatal(err) + } + + orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.00000000","s",null]]]`) + err = p.wsHandleData(orderUpdate) + if err != nil { + t.Fatal(err) + } +} + +func TestProcessAccountOrderLimit(t *testing.T) { + err := p.loadCurrencyDetails() + if err != nil { + t.Error(err) + } + + accountTrade := []byte(`[1000,"",[["n",127,431682155857,"0","1000.00000000","1.00000000","2021-04-13 07:19:56","1.00000000"]]]`) + err = p.wsHandleData(accountTrade) + if !errors.Is(err, errNotEnoughData) { + t.Fatalf("expected: %v but received: %v", errNotEnoughData, err) + } + + accountTrade = []byte(`[1000,"",[["n","127",431682155857,"0","1000.00000000","1.00000000","2021-04-13 07:19:56","1.00000000",null]]]`) + err = p.wsHandleData(accountTrade) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrade = []byte(`[1000,"",[["n",127,"431682155857","0","1000.00000000","1.00000000","2021-04-13 07:19:56","1.00000000",null]]]`) + err = p.wsHandleData(accountTrade) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrade = []byte(`[1000,"",[["n",127,431682155857,0,"1000.00000000","1.00000000","2021-04-13 07:19:56","1.00000000",null]]]`) + err = p.wsHandleData(accountTrade) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrade = []byte(`[1000,"",[["n",127,431682155857,"0",1000.00000000,"1.00000000","2021-04-13 07:19:56","1.00000000",null]]]`) + err = p.wsHandleData(accountTrade) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrade = []byte(`[1000,"",[["n",127,431682155857,"0","1000.00000000",1.00000000,"2021-04-13 07:19:56","1.00000000",null]]]`) + err = p.wsHandleData(accountTrade) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrade = []byte(`[1000,"",[["n",127,431682155857,"0","1000.00000000","1.00000000",1234,"1.00000000",null]]]`) + err = p.wsHandleData(accountTrade) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrade = []byte(`[1000,"",[["n",127,431682155857,"0","1000.00000000","1.00000000","2021-04-13 07:19:56",1.00000000,null]]]`) + err = p.wsHandleData(accountTrade) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrade = []byte(`[1000,"",[["n",127,431682155857,"0","1000.00000000","1.00000000","2021-04-13 07:19:56","1.00000000",null]]]`) + err = p.wsHandleData(accountTrade) + if err != nil { + t.Fatal(err) + } +} + +func TestProcessAccountBalanceUpdate(t *testing.T) { + err := p.loadCurrencyDetails() + if err != nil { + t.Error(err) + } + + balance := []byte(`[1000,"",[["b",243,"e"]]]`) + err = p.wsHandleData(balance) + if !errors.Is(err, errNotEnoughData) { + t.Fatalf("expected: %v but received: %v", errNotEnoughData, err) + } + + balance = []byte(`[1000,"",[["b","243","e","-1.00000000"]]]`) + err = p.wsHandleData(balance) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + balance = []byte(`[1000,"",[["b",243,1234,"-1.00000000"]]]`) + err = p.wsHandleData(balance) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + balance = []byte(`[1000,"",[["b",243,"e",-1.00000000]]]`) + err = p.wsHandleData(balance) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + balance = []byte(`[1000,"",[["b",243,"e","-1.00000000"]]]`) + err = p.wsHandleData(balance) + if err != nil { + t.Fatal(err) + } +} + +func TestProcessAccountTrades(t *testing.T) { + accountTrades := []byte(`[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345"]]]`) + err := p.wsHandleData(accountTrades) + if !errors.Is(err, errNotEnoughData) { + t.Fatalf("expected: %v but received: %v", errNotEnoughData, err) + } + + accountTrades = []byte(`[1000,"",[["t", "12345", "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345", "0.015"]]]`) + err = p.wsHandleData(accountTrades) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrades = []byte(`[1000,"",[["t", 12345, 0.03000000, "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345", "0.015"]]]`) + err = p.wsHandleData(accountTrades) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrades = []byte(`[1000,"",[["t", 12345, "0.03000000", 0.50000000, "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345", "0.015"]]]`) + err = p.wsHandleData(accountTrades) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrades = []byte(`[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, 0.00000375, "2018-09-08 05:54:09", "12345", "0.015"]]]`) + err = p.wsHandleData(accountTrades) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrades = []byte(`[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, 0.0000037, "2018-09-08 05:54:09", "12345", "0.015"]]]`) + err = p.wsHandleData(accountTrades) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrades = []byte(`[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", 12345, "12345", 0.015]]]`) + err = p.wsHandleData(accountTrades) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + accountTrades = []byte(`[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345", "0.015"]]]`) + err = p.wsHandleData(accountTrades) + if err != nil { + t.Fatal(err) + } +} + +func TestProcessAccountKilledOrder(t *testing.T) { + kill := []byte(`[1000,"",[["k", 1337]]]`) + err := p.wsHandleData(kill) + if !errors.Is(err, errNotEnoughData) { + t.Fatalf("expected: %v but received: %v", errNotEnoughData, err) + } + + kill = []byte(`[1000,"",[["k", "1337", null]]]`) + err = p.wsHandleData(kill) + if !errors.Is(err, errTypeAssertionFailure) { + t.Fatalf("expected: %v but received: %v", errTypeAssertionFailure, err) + } + + kill = []byte(`[1000,"",[["k", 1337, null]]]`) + err = p.wsHandleData(kill) + if err != nil { + t.Fatal(err) + } +} + +func TestGetCompleteBalances(t *testing.T) { + if !mockTests && !areTestAPIKeysSet() { + t.Skip("API keys not set, mockTests false, skipping test") + } + _, err := p.GetCompleteBalances() + if err != nil { + t.Fatal(err) + } +} diff --git a/exchanges/poloniex/poloniex_types.go b/exchanges/poloniex/poloniex_types.go index 00c2493d..a8dded76 100644 --- a/exchanges/poloniex/poloniex_types.go +++ b/exchanges/poloniex/poloniex_types.go @@ -16,9 +16,10 @@ type Ticker struct { PercentChange float64 `json:"percentChange,string"` BaseVolume float64 `json:"baseVolume,string"` QuoteVolume float64 `json:"quoteVolume,string"` - IsFrozen int64 `json:"isFrozen,string"` High24Hr float64 `json:"high24hr,string"` Low24Hr float64 `json:"low24hr,string"` + IsFrozen uint8 `json:"isFrozen,string"` + PostOnly uint8 `json:"postOnly,string"` } // OrderbookResponseAll holds the full response type orderbook @@ -27,9 +28,7 @@ type OrderbookResponseAll struct { } // CompleteBalances holds the full balance data -type CompleteBalances struct { - Currency map[string]CompleteBalance -} +type CompleteBalances map[string]CompleteBalance // OrderbookResponse is a sub-type for orderbooks type OrderbookResponse struct { @@ -116,15 +115,14 @@ type ChartData struct { // Currencies contains currency information type Currencies struct { - ID float64 `json:"id"` - Name string `json:"name"` - MaxDailyWithdrawal string `json:"maxDailyWithdrawal"` - TxFee float64 `json:"txFee,string"` - MinConfirmations int64 `json:"minConf"` - DepositAddresses interface{} `json:"depositAddress"` - Disabled int64 `json:"disabled"` - Delisted int64 `json:"delisted"` - Frozen int64 `json:"frozen"` + ID float64 `json:"id"` + Name string `json:"name"` + TxFee float64 `json:"txFee,string"` + MinConfirmations int64 `json:"minConf"` + DepositAddress string `json:"depositAddress"` + WithdrawalDepositDisabled uint8 `json:"disabled"` + Delisted uint8 `json:"delisted"` + Frozen uint8 `json:"frozen"` } // LoanOrder holds loan order information @@ -148,9 +146,9 @@ type Balance struct { // CompleteBalance contains the complete balance with a btcvalue type CompleteBalance struct { - Available float64 - OnOrders float64 - BTCValue float64 + Available float64 `json:"available,string"` + OnOrders float64 `json:"onOrders,string"` + BTCValue float64 `json:"btcValue,string"` } // DepositAddresses holds the full address per crypto-currency @@ -442,13 +440,6 @@ var WithdrawalFees = map[currency.Code]float64{ currency.ZEC: 0.001, } -// WsAccountBalanceUpdateResponse Authenticated Ws Account data -type WsAccountBalanceUpdateResponse struct { - currencyID float64 - wallet string - amount float64 -} - // WsOrderUpdateResponse Authenticated Ws Account data type WsOrderUpdateResponse struct { OrderNumber float64 diff --git a/exchanges/poloniex/poloniex_websocket.go b/exchanges/poloniex/poloniex_websocket.go index af26205e..aba4a50a 100644 --- a/exchanges/poloniex/poloniex_websocket.go +++ b/exchanges/poloniex/poloniex_websocket.go @@ -14,6 +14,7 @@ import ( "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/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" @@ -21,6 +22,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" + "github.com/thrasher-corp/gocryptotrader/log" ) const ( @@ -29,11 +31,25 @@ const ( wsTickerDataID = 1002 ws24HourExchangeVolumeID = 1003 wsHeartbeat = 1010 + + accountNotificationBalanceUpdate = "b" + accountNotificationOrderUpdate = "o" + accountNotificationPendingOrder = "p" + accountNotificationOrderLimitCreated = "n" + accountNotificationTrades = "t" + accountNotificationKilledOrder = "k" + accountNotificationMarginPosition = "m" + + orderbookInitial = "i" + orderbookUpdate = "o" + tradeUpdate = "t" ) var ( - // currencyIDMap stores a map of currencies associated with their ID - currencyIDMap map[float64]string + errNotEnoughData = errors.New("element length not adequate to process") + errTypeAssertionFailure = errors.New("type assertion failure") + errIDNotFoundInPairMap = errors.New("id not associated with currency pair map") + errIDNotFoundInCodeMap = errors.New("id not associated with currency code map") ) // WsConnect initiates a websocket connection @@ -47,7 +63,7 @@ func (p *Poloniex) WsConnect() error { return err } - err = p.getCurrencyIDMap() + err = p.loadCurrencyDetails() if err != nil { return err } @@ -57,31 +73,31 @@ func (p *Poloniex) WsConnect() error { return nil } -func (p *Poloniex) getCurrencyIDMap() error { - if currencyIDMap == nil { - currencyIDMap = make(map[float64]string) - resp, err := p.GetTicker() +// TODO: Create routine to refresh list every day/week(?) for production +func (p *Poloniex) loadCurrencyDetails() error { + if p.details.isInitial() { + ticks, err := p.GetTicker() + if err != nil { + return err + } + err = p.details.loadPairs(ticks) if err != nil { return err } - for k, v := range resp { - currencyIDMap[v.ID] = k + currs, err := p.GetCurrencies() + if err != nil { + return err + } + + err = p.details.loadCodes(currs) + if err != nil { + return err } } return nil } -func getWSDataType(data interface{}) string { - subData := data.([]interface{}) - dataType := subData[0].(string) - return dataType -} - -func checkSubscriptionSuccess(data []interface{}) bool { - return data[1].(float64) == 1 -} - // wsReadData handles data from the websocket connection func (p *Poloniex) wsReadData() { p.Websocket.Wg.Add(1) @@ -94,7 +110,7 @@ func (p *Poloniex) wsReadData() { } err := p.wsHandleData(resp.Raw) if err != nil { - p.Websocket.DataHandler <- err + p.Websocket.DataHandler <- fmt.Errorf("%s: %w", p.Name, err) } } } @@ -105,271 +121,149 @@ func (p *Poloniex) wsHandleData(respRaw []byte) error { if err != nil { return err } - if data, ok := result.([]interface{}); ok { - if len(data) == 0 { - return nil - } - if len(data) == 1 { - // heartbeat - return nil - } - if len(data) == 2 { - // subscription acknowledgement / heartbeat - return nil - } - if channelID, ok := data[0].(float64); ok { - switch channelID { - case ws24HourExchangeVolumeID: - return nil - case wsAccountNotificationID: - if notificationsArray, ok := data[2].([]interface{}); ok { - if _, ok := notificationsArray[0].([]interface{}); ok { - for i := 0; i < len(notificationsArray); i++ { - if notification, ok := (notificationsArray[i]).([]interface{}); ok { - switch notification[0].(string) { - case "o": - var amount float64 - amount, err = strconv.ParseFloat(notification[2].(string), 64) - if err != nil { - return err - } - var oStatus order.Status - var oType = notification[3].(string) - switch { - case amount > 0 && (oType == "f" || oType == "s"): - oStatus = order.PartiallyFilled - case amount == 0 && (oType == "f" || oType == "s"): - oStatus = order.Filled - case amount > 0 && oType == "c": - oStatus = order.PartiallyCancelled - case amount == 0 && oType == "c": - oStatus = order.Cancelled - } - response := &order.Modify{ - RemainingAmount: amount, - Exchange: p.Name, - ID: strconv.FormatFloat(notification[1].(float64), 'f', -1, 64), - Type: order.Limit, - Status: oStatus, - AssetType: asset.Spot, - } - p.Websocket.DataHandler <- response - case "n": - var timeParse time.Time - timeParse, err = time.Parse(common.SimpleTimeFormat, notification[6].(string)) - if err != nil { - return err - } - var rate, amount float64 - rate, err = strconv.ParseFloat(notification[4].(string), 64) - if err != nil { - return err - } - amount, err = strconv.ParseFloat(notification[5].(string), 64) - if err != nil { - return err - } - var buySell order.Side - switch notification[2].(float64) { - case 0: - buySell = order.Buy - case 1: - buySell = order.Sell - } - var currPair currency.Pair - if currPairFromMap, ok := currencyIDMap[notification[1].(float64)]; ok { - currPair, err = currency.NewPairFromString(currPairFromMap) - if err != nil { - return err - } - } else { - // It is better to still log an order which you can recheck later, rather than error out - p.Websocket.DataHandler <- fmt.Errorf(p.Name+ - " - Unknown currency pair ID. "+ - "Currency will appear as the pair ID: '%v'", - notification[1].(float64)) - currPair, err = currency.NewPairFromString(strconv.FormatFloat(notification[1].(float64), 'f', -1, 64)) - if err != nil { - return err - } - } - var a asset.Item - a, err = p.GetPairAssetType(currPair) - if err != nil { - return err - } - response := &order.Detail{ - Price: rate, - Amount: amount, - Exchange: p.Name, - ID: strconv.FormatFloat(notification[2].(float64), 'f', -1, 64), - Type: order.Limit, - Side: buySell, - Status: order.New, - AssetType: a, - Date: timeParse, - Pair: currPair, - } - p.Websocket.DataHandler <- response - case "b": - var amount float64 - amount, err = strconv.ParseFloat(notification[3].(string), 64) - if err != nil { - return err - } - response := WsAccountBalanceUpdateResponse{ - currencyID: notification[1].(float64), - wallet: notification[2].(string), - amount: amount, - } - p.Websocket.DataHandler <- response - case "t": - var timeParse time.Time - timeParse, err = time.Parse(common.SimpleTimeFormat, notification[8].(string)) - if err != nil { - return err - } - var rate, amount, totalFee float64 - rate, err = strconv.ParseFloat(notification[2].(string), 64) - if err != nil { - return err - } - amount, err = strconv.ParseFloat(notification[3].(string), 64) - if err != nil { - return err - } - totalFee, err = strconv.ParseFloat(notification[7].(string), 64) - if err != nil { - return err - } - var trades []order.TradeHistory - trades = append(trades, order.TradeHistory{ - Price: rate, - Amount: amount, - Fee: totalFee, - Exchange: p.Name, - TID: strconv.FormatFloat(notification[1].(float64), 'f', -1, 64), - Timestamp: timeParse, - }) - response := &order.Modify{ - ID: strconv.FormatFloat(notification[6].(float64), 'f', -1, 64), - Fee: totalFee, - Trades: trades, - } - p.Websocket.DataHandler <- response - case "k": - response := &order.Modify{ - Exchange: p.Name, - ID: strconv.FormatFloat(notification[1].(float64), 'f', -1, 64), - Status: order.Cancelled, - } - p.Websocket.DataHandler <- response - } - } - } - } + data, ok := result.([]interface{}) + if !ok { + return fmt.Errorf("%w data is not []interface{}", + errTypeAssertionFailure) + } + + if len(data) == 0 { + return nil + } + if len(data) == 2 { + // subscription acknowledgement + // TODO: Add in subscriber ack + return nil + } + + channelID, ok := data[0].(float64) + if !ok { + return fmt.Errorf("%w channel id is not of type float64", + errTypeAssertionFailure) + } + switch channelID { + case ws24HourExchangeVolumeID, wsHeartbeat: + return nil + case wsAccountNotificationID: + var notificationsArray []interface{} + notificationsArray, ok = data[2].([]interface{}) + if !ok { + return fmt.Errorf("%w account notification is not a []interface{}", + errTypeAssertionFailure) + } + for i := range notificationsArray { + var notification []interface{} + notification, ok = (notificationsArray[i]).([]interface{}) + if !ok { + return fmt.Errorf("%w notification array element is not a []interface{}", + errTypeAssertionFailure) + } + var updateType string + updateType, ok = notification[0].(string) + if !ok { + return fmt.Errorf("%w update type is not a string", + errTypeAssertionFailure) + } + + switch updateType { + case accountNotificationPendingOrder: + err = p.processAccountPendingOrder(notification) + if err != nil { + return fmt.Errorf("account notification pending order: %w", err) + } + case accountNotificationOrderUpdate: + err = p.processAccountOrderUpdate(notification) + if err != nil { + return fmt.Errorf("account notification order update: %w", err) + } + case accountNotificationOrderLimitCreated: + err = p.processAccountOrderLimit(notification) + if err != nil { + return fmt.Errorf("account notification limit order creation: %w", err) + } + case accountNotificationBalanceUpdate: + err = p.processAccountBalanceUpdate(notification) + if err != nil { + return fmt.Errorf("account notification balance update: %w", err) + } + case accountNotificationTrades: + err = p.processAccountTrades(notification) + if err != nil { + return fmt.Errorf("account notification trades: %w", err) + } + case accountNotificationKilledOrder: + err = p.processAccountKilledOrder(notification) + if err != nil { + return fmt.Errorf("account notification killed order: %w", err) + } + case accountNotificationMarginPosition: + err = p.processAccountMarginPosition(notification) + if err != nil { + return fmt.Errorf("account notification margin position: %w", err) } - case wsTickerDataID: - return p.wsHandleTickerData(data) default: - subData := data[2].([]interface{}) - for x := range subData { - dataL2 := subData[x] + return fmt.Errorf("unhandled account update: %s", string(respRaw)) + } + } + return nil + case wsTickerDataID: + err = p.wsHandleTickerData(data) + if err != nil { + return fmt.Errorf("websocket ticker process: %w", err) + } + return nil + } - switch getWSDataType(dataL2) { - case "i": - dataL3 := dataL2.([]interface{}) - dataL3map := dataL3[1].(map[string]interface{}) - currencyPair, ok := dataL3map["currencyPair"].(string) - if !ok { - return fmt.Errorf("%s websocket could not find currency pair in map", - p.Name) - } + priceAggBook, ok := data[2].([]interface{}) + if !ok { + return fmt.Errorf("%w price aggregated book not []interface{}", + errTypeAssertionFailure) + } - orderbookData, ok := dataL3map["orderBook"].([]interface{}) - if !ok { - return fmt.Errorf("%s websocket could not find orderbook data in map", - p.Name) - } + for x := range priceAggBook { + subData, ok := priceAggBook[x].([]interface{}) + if !ok { + return fmt.Errorf("%w price aggregated book element not []interface{}", + errTypeAssertionFailure) + } - err = p.WsProcessOrderbookSnapshot(orderbookData, - currencyPair) - if err != nil { - return err - } - case "o": - currencyPair := currencyIDMap[channelID] - dataL3 := dataL2.([]interface{}) - err = p.WsProcessOrderbookUpdate(int64(data[1].(float64)), - dataL3, - currencyPair) - if err != nil { - return err - } - case "t": - if !p.IsSaveTradeDataEnabled() { - return nil - } - currencyPair := currencyIDMap[channelID] - var t WsTrade - t.Symbol = currencyIDMap[channelID] - dataL3, ok := dataL2.([]interface{}) - if !ok { - return errors.New("websocket trade update error: type conversion failure") - } + updateIdent, ok := subData[0].(string) + if !ok { + return fmt.Errorf("%w update identifier not a string", + errTypeAssertionFailure) + } - if len(dataL3) != 6 { - return errors.New("websocket trade update error: incorrect data returned") - } - - // tradeID type intermittently changes - switch tradeIDData := dataL3[1].(type) { - case string: - t.TradeID, err = strconv.ParseInt(tradeIDData, 10, 64) - if err != nil { - return err - } - case float64: - t.TradeID = int64(tradeIDData) - default: - return fmt.Errorf("unhandled type for websocket trade update: %v", t) - } - - side := order.Buy - if dataL3[2].(float64) != 1 { - side = order.Sell - } - t.Volume, err = strconv.ParseFloat(dataL3[3].(string), 64) - if err != nil { - return err - } - t.Price, err = strconv.ParseFloat(dataL3[4].(string), 64) - if err != nil { - return err - } - t.Timestamp = int64(dataL3[5].(float64)) - - pair, err := currency.NewPairFromString(currencyPair) - if err != nil { - return err - } - - return p.AddTradesToBuffer(trade.Data{ - TID: strconv.FormatInt(t.TradeID, 10), - Exchange: p.Name, - CurrencyPair: pair, - AssetType: asset.Spot, - Side: side, - Price: t.Price, - Amount: t.Volume, - Timestamp: time.Unix(t.Timestamp, 0), - }) - default: - p.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: p.Name + stream.UnhandledMessage + string(respRaw)} - return nil - } - } + switch updateIdent { + case orderbookInitial: + err = p.WsProcessOrderbookSnapshot(subData) + if err != nil { + return fmt.Errorf("websocket process orderbook snapshot: %w", err) + } + case orderbookUpdate: + var pair currency.Pair + pair, err = p.details.GetPair(channelID) + if err != nil { + return err + } + var seqNo float64 + seqNo, ok = data[1].(float64) + if !ok { + return fmt.Errorf("%w sequence number is not a float64", + errTypeAssertionFailure) + } + err = p.WsProcessOrderbookUpdate(seqNo, subData, pair) + if err != nil { + return fmt.Errorf("websocket process orderbook update: %w", err) + } + case tradeUpdate: + err = p.processTrades(channelID, subData) + if err != nil { + return fmt.Errorf("websocket process trades update: %w", err) + } + default: + p.Websocket.DataHandler <- stream.UnhandledMessageWarning{ + Message: p.Name + stream.UnhandledMessage + string(respRaw), } } } @@ -377,10 +271,18 @@ func (p *Poloniex) wsHandleData(respRaw []byte) error { } func (p *Poloniex) wsHandleTickerData(data []interface{}) error { - tickerData := data[2].([]interface{}) - var t WsTicker - currencyPair, err := currency.NewPairDelimiter(currencyIDMap[tickerData[0].(float64)], - currency.UnderscoreDelimiter) + tickerData, ok := data[2].([]interface{}) + if !ok { + return fmt.Errorf("%w ticker data is not []interface{}", + errTypeAssertionFailure) + } + + currencyID, ok := tickerData[0].(float64) + if !ok { + return fmt.Errorf("%w currency ID not float64", errTypeAssertionFailure) + } + + pair, err := p.details.GetPair(currencyID) if err != nil { return err } @@ -390,104 +292,150 @@ func (p *Poloniex) wsHandleTickerData(data []interface{}) error { return err } - if !enabled.Contains(currencyPair, true) { - var avail currency.Pairs - avail, err = p.GetAvailablePairs(asset.Spot) - if err != nil { - return err - } - - if !avail.Contains(currencyPair, true) { - return fmt.Errorf("currency pair %s not found in available pair list", - currencyPair) - } + if !enabled.Contains(pair, true) { return nil } - t.LastPrice, err = strconv.ParseFloat(tickerData[1].(string), 64) + tlp, ok := tickerData[1].(string) + if !ok { + return fmt.Errorf("%w last price not string", errTypeAssertionFailure) + } + + lastPrice, err := strconv.ParseFloat(tlp, 64) if err != nil { return err } - t.LowestAsk, err = strconv.ParseFloat(tickerData[2].(string), 64) + la, ok := tickerData[2].(string) + if !ok { + return fmt.Errorf("%w lowest ask price not string", + errTypeAssertionFailure) + } + + lowestAsk, err := strconv.ParseFloat(la, 64) if err != nil { return err } - t.HighestBid, err = strconv.ParseFloat(tickerData[3].(string), 64) + hb, ok := tickerData[3].(string) + if !ok { + return fmt.Errorf("%w highest bid price not string", + errTypeAssertionFailure) + } + + highestBid, err := strconv.ParseFloat(hb, 64) if err != nil { return err } - t.PercentageChange, err = strconv.ParseFloat(tickerData[4].(string), 64) + bcv, ok := tickerData[5].(string) + if !ok { + return fmt.Errorf("%w base currency volume not string", + errTypeAssertionFailure) + } + + baseCurrencyVolume24H, err := strconv.ParseFloat(bcv, 64) if err != nil { return err } - t.BaseCurrencyVolume24H, err = strconv.ParseFloat(tickerData[5].(string), 64) + qcv, ok := tickerData[6].(string) + if !ok { + return fmt.Errorf("%w quote currency volume not string", + errTypeAssertionFailure) + } + + quoteCurrencyVolume24H, err := strconv.ParseFloat(qcv, 64) if err != nil { return err } - t.QuoteCurrencyVolume24H, err = strconv.ParseFloat(tickerData[6].(string), 64) - if err != nil { - return err - } - - t.IsFrozen = tickerData[7].(float64) == 1 - t.HighestTradeIn24H, err = strconv.ParseFloat(tickerData[8].(string), 64) - if err != nil { - return err - } - - t.LowestTradePrice24H, err = strconv.ParseFloat(tickerData[9].(string), 64) - if err != nil { - return err - } + // Unused variables below, can add later if needed: + // percentageChange, ok := tickerData[4].(string) + // Not integrating isFrozen with currency details as this will slow down + // the sync RW mutex (can use REST calls for now). + // isFrozen, ok := tickerData[7].(float64) // == 1 means it is frozen + // highestTradeIn24Hm, ok := tickerData[8].(string) + // lowestTradePrice24H, ok := tickerData[9].(string) p.Websocket.DataHandler <- &ticker.Price{ ExchangeName: p.Name, - Volume: t.BaseCurrencyVolume24H, - QuoteVolume: t.QuoteCurrencyVolume24H, - High: t.HighestBid, - Low: t.LowestAsk, - Bid: t.HighestBid, - Ask: t.LowestAsk, - Last: t.LastPrice, + Volume: baseCurrencyVolume24H, + QuoteVolume: quoteCurrencyVolume24H, + High: highestBid, + Low: lowestAsk, + Bid: highestBid, + Ask: lowestAsk, + Last: lastPrice, AssetType: asset.Spot, - Pair: currencyPair, + Pair: pair, } return nil } // WsProcessOrderbookSnapshot processes a new orderbook snapshot into a local // of orderbooks -func (p *Poloniex) WsProcessOrderbookSnapshot(ob []interface{}, symbol string) error { - if len(ob) != 2 { - return errors.New("incorrect orderbook data returned") - } - - askdata, ok := ob[0].(map[string]interface{}) +func (p *Poloniex) WsProcessOrderbookSnapshot(data []interface{}) error { + subDataMap, ok := data[1].(map[string]interface{}) if !ok { - return errors.New("assertion failed for ask data") + return fmt.Errorf("%w subData element is not map[string]interface{}", + errTypeAssertionFailure) } - var book orderbook.Base - for price, volume := range askdata { - p, err := strconv.ParseFloat(price, 64) - if err != nil { - return err - } - a, err := strconv.ParseFloat(volume.(string), 64) - if err != nil { - return err - } - book.Asks = append(book.Asks, orderbook.Item{Price: p, Amount: a}) + pMap, ok := subDataMap["currencyPair"] + if !ok { + return errors.New("could not find currency pair in map") + } + + pair, ok := pMap.(string) + if !ok { + return fmt.Errorf("%w subData element is not map[string]interface{}", + errTypeAssertionFailure) + } + + oMap, ok := subDataMap["orderBook"] + if !ok { + return errors.New("could not find orderbook data in map") + } + + ob, ok := oMap.([]interface{}) + if !ok { + return fmt.Errorf("%w orderbook data is not []interface{}", + errTypeAssertionFailure) + } + + if len(ob) != 2 { + return errNotEnoughData + } + + askData, ok := ob[0].(map[string]interface{}) + if !ok { + return fmt.Errorf("%w ask data is not map[string]interface{}", + errTypeAssertionFailure) } bidData, ok := ob[1].(map[string]interface{}) if !ok { - return errors.New("assertion failed for bid data") + return fmt.Errorf("%w bid data is not map[string]interface{}", + errTypeAssertionFailure) + } + + var book orderbook.Base + for price, volume := range askData { + p, err := strconv.ParseFloat(price, 64) + if err != nil { + return err + } + v, ok := volume.(string) + if !ok { + return fmt.Errorf("%w ask volume data not string", + errTypeAssertionFailure) + } + a, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + book.Asks = append(book.Asks, orderbook.Item{Price: p, Amount: a}) } for price, volume := range bidData { @@ -495,7 +443,12 @@ func (p *Poloniex) WsProcessOrderbookSnapshot(ob []interface{}, symbol string) e if err != nil { return err } - a, err := strconv.ParseFloat(volume.(string), 64) + v, ok := volume.(string) + if !ok { + return fmt.Errorf("%w bid volume data not string", + errTypeAssertionFailure) + } + a, err := strconv.ParseFloat(v, 64) if err != nil { return err } @@ -509,7 +462,7 @@ func (p *Poloniex) WsProcessOrderbookSnapshot(ob []interface{}, symbol string) e book.VerifyOrderbook = p.CanVerifyOrderbook var err error - book.Pair, err = currency.NewPairFromString(symbol) + book.Pair, err = currency.NewPairFromString(pair) if err != nil { return err } @@ -519,25 +472,37 @@ func (p *Poloniex) WsProcessOrderbookSnapshot(ob []interface{}, symbol string) e } // WsProcessOrderbookUpdate processes new orderbook updates -func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber int64, target []interface{}, symbol string) error { - cP, err := currency.NewPairFromString(symbol) +func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber float64, data []interface{}, pair currency.Pair) error { + if len(data) < 4 { + return errNotEnoughData + } + + ps, ok := data[2].(string) + if !ok { + return fmt.Errorf("%w price not string", errTypeAssertionFailure) + } + price, err := strconv.ParseFloat(ps, 64) if err != nil { return err } - price, err := strconv.ParseFloat(target[2].(string), 64) + vs, ok := data[3].(string) + if !ok { + return fmt.Errorf("%w volume not string", errTypeAssertionFailure) + } + volume, err := strconv.ParseFloat(vs, 64) if err != nil { return err } - volume, err := strconv.ParseFloat(target[3].(string), 64) - if err != nil { - return err + bs, ok := data[1].(float64) + if !ok { + return fmt.Errorf("%w buysell not float64", errTypeAssertionFailure) } update := &buffer.Update{ - Pair: cP, + Pair: pair, Asset: asset.Spot, - UpdateID: sequenceNumber, + UpdateID: int64(sequenceNumber), } - if target[1].(float64) == 1 { + if bs == 1 { update.Bids = []orderbook.Item{{Price: price, Amount: volume}} } else { update.Asks = []orderbook.Item{{Price: price, Amount: volume}} @@ -663,3 +628,512 @@ func (p *Poloniex) wsSendAuthorisedCommand(command string) error { } return p.Websocket.Conn.SendJSONMessage(request) } + +func (p *Poloniex) processAccountMarginPosition(notification []interface{}) error { + if len(notification) < 5 { + return errNotEnoughData + } + + orderID, ok := notification[1].(float64) + if !ok { + return fmt.Errorf("%w order id not float64", errTypeAssertionFailure) + } + + currencyID, ok := notification[2].(float64) + if !ok { + return fmt.Errorf("%w currency id not float64", errTypeAssertionFailure) + } + code, err := p.details.GetCode(currencyID) + if err != nil { + return err + } + + a, ok := notification[3].(string) + if !ok { + return fmt.Errorf("%w amount not string", errTypeAssertionFailure) + } + + amount, err := strconv.ParseFloat(a, 64) + if err != nil { + return err + } + + // null returned so ok check is not needed + clientOrderID, _ := notification[4].(string) + + // Temp struct for margin position changes + p.Websocket.DataHandler <- struct { + OrderID string + Code currency.Code + Amount float64 + ClientOrderID string + }{ + OrderID: strconv.FormatFloat(orderID, 'f', -1, 64), + Code: code, + Amount: amount, + ClientOrderID: clientOrderID, + } + + return nil +} + +func (p *Poloniex) processAccountPendingOrder(notification []interface{}) error { + if len(notification) < 7 { + return errNotEnoughData + } + + orderID, ok := notification[1].(float64) + if !ok { + return fmt.Errorf("%w order id not float64", errTypeAssertionFailure) + } + + currencyID, ok := notification[2].(float64) + if !ok { + return fmt.Errorf("%w currency id not float64", errTypeAssertionFailure) + } + pair, err := p.details.GetPair(currencyID) + if err != nil { + if !errors.Is(err, errIDNotFoundInPairMap) { + return err + } + log.Errorf(log.WebsocketMgr, + "%s - Unknown currency pair ID. Currency will appear as the pair ID: '%v'", + p.Name, + currencyID) + } + + price, ok := notification[3].(string) + if !ok { + return fmt.Errorf("%w price not string", errTypeAssertionFailure) + } + orderPrice, err := strconv.ParseFloat(price, 64) + if err != nil { + return err + } + amount, ok := notification[4].(string) + if !ok { + return fmt.Errorf("%w amount not string", errTypeAssertionFailure) + } + orderAmount, err := strconv.ParseFloat(amount, 64) + if err != nil { + return err + } + side, ok := notification[5].(string) + if !ok { + return fmt.Errorf("%w order type not string", errTypeAssertionFailure) + } + orderSide := order.Buy + if side == "0" { + orderSide = order.Sell + } + + // null returned so ok check is not needed + clientOrderID, _ := notification[6].(string) + + p.Websocket.DataHandler <- &order.Detail{ + Exchange: p.Name, + ID: strconv.FormatFloat(orderID, 'f', -1, 64), + Pair: pair, + AssetType: asset.Spot, + Side: orderSide, + Price: orderPrice, + Amount: orderAmount, + RemainingAmount: orderAmount, + ClientOrderID: clientOrderID, + Status: order.Pending, + } + return nil +} + +func (p *Poloniex) processAccountOrderUpdate(notification []interface{}) error { + if len(notification) < 5 { + return errNotEnoughData + } + + orderID, ok := notification[1].(float64) + if !ok { + return fmt.Errorf("%w order id not float64", errTypeAssertionFailure) + } + + a, ok := notification[2].(string) + if !ok { + return fmt.Errorf("%w amount not string", errTypeAssertionFailure) + } + amount, err := strconv.ParseFloat(a, 64) + if err != nil { + return err + } + + oType, ok := notification[3].(string) + if !ok { + return fmt.Errorf("%w order type not string", errTypeAssertionFailure) + } + + var oStatus order.Status + var cancelledAmount float64 + if oType == "c" { + if len(notification) < 6 { + return errNotEnoughData + } + cancel, ok := notification[5].(string) + if !ok { + return fmt.Errorf("%w cancel amount not string", errTypeAssertionFailure) + } + + cancelledAmount, err = strconv.ParseFloat(cancel, 64) + if err != nil { + return err + } + + if amount > 0 { + oStatus = order.PartiallyCancelled + } else { + oStatus = order.Cancelled + } + } else { + if amount > 0 { + oStatus = order.PartiallyFilled + } else { + oStatus = order.Filled + } + } + + // null returned so ok check is not needed + clientOrderID, _ := notification[4].(string) + + p.Websocket.DataHandler <- &order.Modify{ + Exchange: p.Name, + RemainingAmount: cancelledAmount, + Amount: amount + cancelledAmount, + ExecutedAmount: amount, + ID: strconv.FormatFloat(orderID, 'f', -1, 64), + Type: order.Limit, + Status: oStatus, + AssetType: asset.Spot, + ClientOrderID: clientOrderID, + } + return nil +} + +func (p *Poloniex) processAccountOrderLimit(notification []interface{}) error { + if len(notification) != 9 { + return errNotEnoughData + } + + currencyID, ok := notification[1].(float64) + if !ok { + return fmt.Errorf("%w currency ID not string", errTypeAssertionFailure) + } + pair, err := p.details.GetPair(currencyID) + if err != nil { + if !errors.Is(err, errIDNotFoundInPairMap) { + return err + } + log.Errorf(log.WebsocketMgr, + "%s - Unknown currency pair ID. Currency will appear as the pair ID: '%v'", + p.Name, + currencyID) + } + + orderID, ok := notification[2].(float64) + if !ok { + return fmt.Errorf("%w order ID not float64", errTypeAssertionFailure) + } + + side, ok := notification[3].(string) + if !ok { + return fmt.Errorf("%w order type not string", errTypeAssertionFailure) + } + orderSide := order.Buy + if side == "0" { + orderSide = order.Sell + } + + rate, ok := notification[4].(string) + if !ok { + return fmt.Errorf("%w rate not string", errTypeAssertionFailure) + } + orderPrice, err := strconv.ParseFloat(rate, 64) + if err != nil { + return err + } + amount, ok := notification[5].(string) + if !ok { + return fmt.Errorf("%w amount not string", errTypeAssertionFailure) + } + orderAmount, err := strconv.ParseFloat(amount, 64) + if err != nil { + return err + } + + ts, ok := notification[6].(string) + if !ok { + return fmt.Errorf("%w time not string", errTypeAssertionFailure) + } + + var timeParse time.Time + timeParse, err = time.Parse(common.SimpleTimeFormat, ts) + if err != nil { + return err + } + + origAmount, ok := notification[7].(string) + if !ok { + return fmt.Errorf("%w original amount not string", errTypeAssertionFailure) + } + origOrderAmount, err := strconv.ParseFloat(origAmount, 64) + if err != nil { + return err + } + + // null returned so ok check is not needed + clientOrderID, _ := notification[8].(string) + p.Websocket.DataHandler <- &order.Detail{ + Exchange: p.Name, + Price: orderPrice, + RemainingAmount: orderAmount, + ExecutedAmount: origOrderAmount - orderAmount, + Amount: origOrderAmount, + ID: strconv.FormatFloat(orderID, 'f', -1, 64), + Type: order.Limit, + Side: orderSide, + Status: order.New, + AssetType: asset.Spot, + Date: timeParse, + Pair: pair, + ClientOrderID: clientOrderID, + } + return nil +} + +func (p *Poloniex) processAccountBalanceUpdate(notification []interface{}) error { + if len(notification) < 4 { + return errNotEnoughData + } + + currencyID, ok := notification[1].(float64) + if !ok { + return fmt.Errorf("%w currency ID not float64", errTypeAssertionFailure) + } + code, err := p.details.GetCode(currencyID) + if err != nil { + return err + } + + walletType, ok := notification[2].(string) + if !ok { + return fmt.Errorf("%w wallet addr not string", errTypeAssertionFailure) + } + + a, ok := notification[3].(string) + if !ok { + return fmt.Errorf("%w amount not string", errTypeAssertionFailure) + } + amount, err := strconv.ParseFloat(a, 64) + if err != nil { + return err + } + + // TODO: Integrate with exchange account system + // NOTES: This will affect free amount, a rest call might be needed to get + // locked and total amounts periodically. + p.Websocket.DataHandler <- account.Change{ + Exchange: p.Name, + Currency: code, + Asset: asset.Spot, + Account: deriveWalletType(walletType), + Amount: amount, + } + return nil +} + +func deriveWalletType(s string) string { + switch s { + case "e": + return "exchange" + case "m": + return "margin" + case "l": + return "lending" + default: + return "unknown" + } +} + +func (p *Poloniex) processAccountTrades(notification []interface{}) error { + if len(notification) < 11 { + return errNotEnoughData + } + + tradeID, ok := notification[1].(float64) + if !ok { + return fmt.Errorf("%w tradeID not float64", errTypeAssertionFailure) + } + + r, ok := notification[2].(string) + if !ok { + return fmt.Errorf("%w rate not string", errTypeAssertionFailure) + } + rate, err := strconv.ParseFloat(r, 64) + if err != nil { + return err + } + + a, ok := notification[3].(string) + if !ok { + return fmt.Errorf("%w amount not string", errTypeAssertionFailure) + } + amount, err := strconv.ParseFloat(a, 64) + if err != nil { + return err + } + + // notification[4].(string) is the fee multiplier + // notification[5].(string) is the funding type 0 (exchange wallet), + // 1 (borrowed funds), 2 (margin funds), or 3 (lending funds) + + orderID, ok := notification[6].(float64) + if !ok { + return fmt.Errorf("%w orderID not float64", errTypeAssertionFailure) + } + + fee, ok := notification[7].(string) + if !ok { + return fmt.Errorf("%w fee not string", errTypeAssertionFailure) + } + totalFee, err := strconv.ParseFloat(fee, 64) + if err != nil { + return err + } + + t, ok := notification[8].(string) + if !ok { + return fmt.Errorf("%w time not string", errTypeAssertionFailure) + } + timeParse, err := time.Parse(common.SimpleTimeFormat, t) + if err != nil { + return err + } + + // null returned so ok check is not needed + clientOrderID, _ := notification[9].(string) + + tt, ok := notification[10].(string) + if !ok { + return fmt.Errorf("%w time not string", errTypeAssertionFailure) + } + tradeTotal, err := strconv.ParseFloat(tt, 64) + if err != nil { + return err + } + + p.Websocket.DataHandler <- &order.Modify{ + Exchange: p.Name, + ID: strconv.FormatFloat(orderID, 'f', -1, 64), + Fee: totalFee, + Trades: []order.TradeHistory{{ + Price: rate, + Amount: amount, + Fee: totalFee, + Exchange: p.Name, + TID: strconv.FormatFloat(tradeID, 'f', -1, 64), + Timestamp: timeParse, + Total: tradeTotal, + }}, + AssetType: asset.Spot, + ClientOrderID: clientOrderID, + } + return nil +} + +func (p *Poloniex) processAccountKilledOrder(notification []interface{}) error { + if len(notification) < 3 { + return errNotEnoughData + } + + orderID, ok := notification[1].(float64) + if !ok { + return fmt.Errorf("%w order ID not float64", errTypeAssertionFailure) + } + + // null returned so ok check is not needed + clientOrderID, _ := notification[2].(string) + + p.Websocket.DataHandler <- &order.Modify{ + Exchange: p.Name, + ID: strconv.FormatFloat(orderID, 'f', -1, 64), + Status: order.Cancelled, + AssetType: asset.Spot, + ClientOrderID: clientOrderID, + } + return nil +} + +func (p *Poloniex) processTrades(currencyID float64, subData []interface{}) error { + if !p.IsSaveTradeDataEnabled() { + return nil + } + pair, err := p.details.GetPair(currencyID) + if err != nil { + return err + } + + if len(subData) != 6 { + return errNotEnoughData + } + + var tradeID string + switch tradeIDData := subData[1].(type) { // tradeID type intermittently changes + case string: + tradeID = tradeIDData + case float64: + tradeID = strconv.FormatFloat(tradeIDData, 'f', -1, 64) + default: + return fmt.Errorf("unhandled type for websocket trade update: %v", + tradeIDData) + } + + orderSide, ok := subData[2].(float64) + if !ok { + return fmt.Errorf("%w order side not float64", + errTypeAssertionFailure) + } + + side := order.Buy + if orderSide != 1 { + side = order.Sell + } + + v, ok := subData[3].(string) + if !ok { + return fmt.Errorf("%w volume not string", + errTypeAssertionFailure) + } + volume, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + rate, ok := subData[4].(string) + if !ok { + return fmt.Errorf("%w rate not string", errTypeAssertionFailure) + } + price, err := strconv.ParseFloat(rate, 64) + if err != nil { + return err + } + timestamp, ok := subData[5].(float64) + if !ok { + return fmt.Errorf("%w time not float64", errTypeAssertionFailure) + } + + return p.AddTradesToBuffer(trade.Data{ + TID: tradeID, + Exchange: p.Name, + CurrencyPair: pair, + AssetType: asset.Spot, + Side: side, + Price: price, + Amount: volume, + Timestamp: time.Unix(int64(timestamp), 0), + }) +} diff --git a/testdata/http_mock/poloniex/poloniex.json b/testdata/http_mock/poloniex/poloniex.json index 543645c8..84610822 100644 --- a/testdata/http_mock/poloniex/poloniex.json +++ b/testdata/http_mock/poloniex/poloniex.json @@ -10120,19 +10120,19 @@ }, { "data": { - "result":{ - "6071071":{ - "status":"Open", - "rate":"0.40000000", - "amount":"1.00000000", - "currencyPair":"BTC_ETH", - "date":"2018-10-17 17:04:50", - "total":"0.40000000", - "type":"buy", - "startingAmount":"1.00000" + "result": { + "6071071": { + "status": "Open", + "rate": "0.40000000", + "amount": "1.00000000", + "currencyPair": "BTC_ETH", + "date": "2018-10-17 17:04:50", + "total": "0.40000000", + "type": "buy", + "startingAmount": "1.00000" } }, - "success":1 + "success": 1 }, "queryString": "", "bodyParams": "command=returnOrderStatus\u0026nonce=1594157624217368022\u0026orderNumber=96238912841", @@ -10150,7 +10150,7 @@ }, { "data": { - "result":{ + "result": { "error": "Order not found, or you are not the person who placed it." } }, @@ -10167,6 +10167,2408 @@ "db30a451e3277242dd56ba0d0f73e1ca58b6b6891afbc8a5ae13c388a95ffd6ae6f6e63d0b6915e481f35bfcc84b3c73395666f28e76912c7178a440d46de43c" ] } + }, + { + "data": { + "1CR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "AAVE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ABY": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "AC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ACH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ADABEAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ADABULL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ADD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ADEL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ADN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "AEON": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "AERO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "AIR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "AKITA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "AKRO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ALPHA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "AMP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "APH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "API3": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ARCH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ARDR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ATOM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "AUR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "AVA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "AXIS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BAC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BADGER": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BAL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BALLS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BAND": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BANK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BAS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BAT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BBL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BBR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BCC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BCH": { + "available": "0.00000005", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BCHA": { + "available": "0.00000005", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BCHABC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BCHBEAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BCHBULL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BCHC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BCHSV": { + "available": "0.00000005", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BCN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BCY": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BDC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BDG": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BDP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BEAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BELA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BID": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BITCNY": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BITS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BITUSD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BLK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BLOCK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BLU": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BLY": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BNB": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BNS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BNT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BOND": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BONES": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BOST": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BREE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BRG": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BSVBEAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BSVBULL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BTC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BTCD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BTCS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BTCST": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BTM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BTS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BTT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BULL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BURN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BURST": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BUSD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BVOL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "BZRX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "C2": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CACH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CAI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CCN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CGA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CHA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CHR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CINNI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CLAM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CNL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CNMT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CNOTE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "COMBO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "COMM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "COMP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CON": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CORG": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CORN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "COVER": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CREAM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CRT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CRV": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CRYPT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CUDOS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CURE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CUSDT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CVC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CVP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CVT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "CYC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DAI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DAO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DASH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DCR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DEC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DEXT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DGB": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DHT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DIA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DICE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DIEM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DIME": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DIS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DMG": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DNS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DOGE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DOS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DOT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DRKC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DRM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DSH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "DVK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "EAC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "EBT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ECC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "EFL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "EMC2": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "EMO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ENC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "EOS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "EOSBEAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "EOSBULL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ESD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ETC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ETH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ETHBEAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ETHBNT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ETHBULL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "EXE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "EXP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FAC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FARM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FCN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FCT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FCT2": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FIBRE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FIL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FLAP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FLDC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FLO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FLT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FOAM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FOX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FRAC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FRK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FRONT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FRQ": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FSW": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FTT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FUND": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FVZ": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FXC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FZ": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "FZN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GAME": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GAP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GAS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GDN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GEEQ": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GEMZ": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GEO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GHST": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GIAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GLB": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GLM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GML": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GNO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GNS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GNT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GOLD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GPC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GPUC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GRC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GRCX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GRIN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GRS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GRT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "GUE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "H2O": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "HEGIC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "HGET": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "HIRO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "HOT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "HUC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "HUGE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "HVC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "HYP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "HZ": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "IBVOL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "IFC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "INDEX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "INJ": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "IOC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ITC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "IXC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "JFI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "JLH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "JPC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "JST": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "JUG": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "KDC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "KEY": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "KNC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "KP3R": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "KTON": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LBC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LCL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LEAF": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LEND": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LGC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LINK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LINKBEAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LINKBULL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LIVE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LOL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LON": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LOOM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LOVE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LPT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LQD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LRC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LSK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LTBC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LTC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LTCBEAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LTCBULL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "LTCX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MAID": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MANA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MAST": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MATIC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MAX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MCB": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MCN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MDT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MEC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MEME": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "METH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MEXP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MIL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MIN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MINT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MKR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MMC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MMNXT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MMXIV": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MNTA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MON": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MPH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MRC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MRS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MTA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MTS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MUN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MYR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "MZC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "N5X": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NAS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NAUT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NAV": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NBT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NEO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NEOS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NMC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NMR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NOBL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NOTE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NOXT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NRS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NSR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NTX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NU": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NXC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NXT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "NXTI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "OCEAN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "OM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "OMG": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "OMNI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ONEINCH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "OPAL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "OPT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PAND": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PASC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PAWN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PAX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PBTC35A": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PEARL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PERX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PIGGY": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PINK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PLT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PLX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PMC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "POLS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "POLY": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "POT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PPC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PRC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PRQ": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PRT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "PTS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "Q2C": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "QBK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "QCN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "QORA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "QTL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "QTUM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "RADS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "RARI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "RBY": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "RDD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "REEF": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "REN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "REPV2": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "RFUEL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "RIC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "RING": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ROOK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "RSR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "RZR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SAL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SAND": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SBD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SBREE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SDC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SENSO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SFI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SHIB": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SHIBE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SHOPX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SILK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SJCX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SLR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SMC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SNT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SNX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SOC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SPA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SQL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SRCC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SRG": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SRM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SSD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "STAKE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "STEEM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "STORJ": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "STPT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "STR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "STRAT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SUM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SUN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SUSHI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SWAP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SWARM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SWFTC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SWINGBY": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SWRV": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SXC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SXP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SYNC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "SYS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TAC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TAI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TEND": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TOR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TORN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TRADE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TRB": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TRU": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TRUMPWIN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TRUST": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TRX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TRXBEAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TRXBULL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TUSD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "TWE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "UIS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ULTC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "UMA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "UNI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "UNITY": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "URO": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "USDC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "USDE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "USDJ": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "USDT": { + "available": "5.81871750", + "btcValue": "0.00010159", + "onOrders": "0.00000000" + }, + "UTC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "UTIL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "UVC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "VALUE": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "VIA": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "VOOT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "VOX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "VRC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "VSP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "VTC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "WBTC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "WC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "WDC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "WETH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "WIKI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "WIN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "WNXM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "WOLF": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "WRX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "X13": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XAI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XAP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XBC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XCH": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XCN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XCP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XCR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XDN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XDOT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XDP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XEM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XFLR": { + "available": "40.14000000", + "btcValue": "0.00022787", + "onOrders": "0.00000000" + }, + "XHC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XLB": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XLMBEAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XLMBULL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XMG": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XMR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XPB": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XPM": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XRP": { + "available": "40.14000000", + "btcValue": "0.00100470", + "onOrders": "0.00000000" + }, + "XRPBEAR": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XRPBULL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XSI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XST": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XSV": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XTZ": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XUSD": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XVC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "XXC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "YACC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "YANG": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "YC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "YFI": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "YFII": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "YFL": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "YFV": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "YIN": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ZAP": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ZEC": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ZKS": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ZLOT": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "ZRX": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + }, + "eTOK": { + "available": "0.00000000", + "btcValue": "0.00000000", + "onOrders": "0.00000000" + } + }, + "queryString": "", + "bodyParams": "account=all\u0026command=returnCompleteBalances\u0026nonce=1618799774231546400", + "headers": { + "Content-Type": [ + "application/x-www-form-urlencoded" + ], + "Key": [ + "" + ], + "Sign": [ + "7673324ee69afb1ac1b9b55c4fa22a097624353e2a975977e09e8958cae1b17bcccf8beffd7791a197a9ce3f7059eb727d50c86031f43f921034263cd6ba2439" + ] + } } ] }