From 4c5b0a4aa1bfbca750d8f8e561305a5ce31cf70c Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Tue, 24 Sep 2019 18:16:42 +1000 Subject: [PATCH] Coinut code impovements and websocket pair formatting fixes --- .../coinbasepro/coinbasepro_websocket.go | 12 +- exchanges/coinbasepro/coinbasepro_wrapper.go | 26 +- exchanges/coinut/coinut.go | 39 ++- exchanges/coinut/coinut_test.go | 101 +++++- exchanges/coinut/coinut_types.go | 87 ++++- exchanges/coinut/coinut_websocket.go | 99 +++--- exchanges/coinut/coinut_wrapper.go | 307 +++++++++++------- exchanges/okcoin/okcoin_wrapper.go | 32 +- 8 files changed, 491 insertions(+), 212 deletions(-) diff --git a/exchanges/coinbasepro/coinbasepro_websocket.go b/exchanges/coinbasepro/coinbasepro_websocket.go index c6471bcb..74c3b9d0 100644 --- a/exchanges/coinbasepro/coinbasepro_websocket.go +++ b/exchanges/coinbasepro/coinbasepro_websocket.go @@ -285,10 +285,10 @@ func (c *CoinbasePro) GenerateDefaultSubscriptions() { continue } for j := range enabledCurrencies { - enabledCurrencies[j].Delimiter = "-" subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ - Channel: channels[i], - Currency: enabledCurrencies[j], + Channel: channels[i], + Currency: c.FormatExchangeCurrency(enabledCurrencies[j], + asset.Spot), }) } } @@ -303,7 +303,8 @@ func (c *CoinbasePro) Subscribe(channelToSubscribe wshandler.WebsocketChannelSub { Name: channelToSubscribe.Channel, ProductIDs: []string{ - channelToSubscribe.Currency.String(), + c.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String(), }, }, }, @@ -329,7 +330,8 @@ func (c *CoinbasePro) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelS { Name: channelToSubscribe.Channel, ProductIDs: []string{ - channelToSubscribe.Currency.String(), + c.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String(), }, }, }, diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index 40064905..3c207972 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -159,14 +159,22 @@ func (c *CoinbasePro) Start(wg *sync.WaitGroup) { // Run implements the coinbasepro wrapper func (c *CoinbasePro) Run() { if c.Verbose { - log.Debugf(log.ExchangeSys, "%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket.IsEnabled()), coinbaseproWebsocketURL) + log.Debugf(log.ExchangeSys, "%s Websocket: %s. (url: %s).\n", + c.GetName(), + common.IsEnabled(c.Websocket.IsEnabled()), + coinbaseproWebsocketURL) c.PrintEnabledPairs() } forceUpdate := false - if !common.StringDataContains(c.GetEnabledPairs(asset.Spot).Strings(), c.GetPairFormat(asset.Spot, false).Delimiter) || - !common.StringDataContains(c.GetAvailablePairs(asset.Spot).Strings(), c.GetPairFormat(asset.Spot, false).Delimiter) { - enabledPairs := currency.NewPairsFromStrings([]string{fmt.Sprintf("BTC%vUSD", c.GetPairFormat(asset.Spot, false).Delimiter)}) + delim := c.GetPairFormat(asset.Spot, false).Delimiter + if !common.StringDataContains(c.CurrencyPairs.GetPairs(asset.Spot, + true).Strings(), delim) || + !common.StringDataContains(c.CurrencyPairs.GetPairs(asset.Spot, + false).Strings(), delim) { + enabledPairs := currency.NewPairsFromStrings( + []string{fmt.Sprintf("BTC%sUSD", delim)}, + ) log.Warn(log.ExchangeSys, "Enabled pairs for CoinbasePro reset due to config upgrade, please enable the ones you would like to use again") forceUpdate = true @@ -294,7 +302,8 @@ func (c *CoinbasePro) FetchOrderbook(p currency.Pair, assetType asset.Item) (ord // UpdateOrderbook updates and returns the orderbook for a currency pair func (c *CoinbasePro) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := c.GetOrderbook(c.FormatExchangeCurrency(p, assetType).String(), 2) + orderbookNew, err := c.GetOrderbook(c.FormatExchangeCurrency(p, + assetType).String(), 2) if err != nil { return orderBook, err } @@ -324,8 +333,7 @@ func (c *CoinbasePro) UpdateOrderbook(p currency.Pair, assetType asset.Item) (or // GetFundingHistory returns funding history, deposits and // withdrawals func (c *CoinbasePro) GetFundingHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, common.ErrFunctionNotSupported + return nil, common.ErrFunctionNotSupported } // GetExchangeHistory returns historic trade data since exchange opening. @@ -352,7 +360,7 @@ func (c *CoinbasePro) SubmitOrder(order *exchange.OrderSubmission) (exchange.Sub order.Amount, order.Amount, order.OrderSide.ToString(), - order.Pair.String(), + c.FormatExchangeCurrency(order.Pair, asset.Spot).String(), "") case exchange.LimitOrderType: response, err = c.PlaceLimitOrder("", @@ -361,7 +369,7 @@ func (c *CoinbasePro) SubmitOrder(order *exchange.OrderSubmission) (exchange.Sub order.OrderSide.ToString(), "", "", - order.Pair.String(), + c.FormatExchangeCurrency(order.Pair, asset.Spot).String(), "", false) default: diff --git a/exchanges/coinut/coinut.go b/exchanges/coinut/coinut.go index acd52481..faf70829 100644 --- a/exchanges/coinut/coinut.go +++ b/exchanges/coinut/coinut.go @@ -42,11 +42,29 @@ const ( coinutStatusOK = "OK" ) +var ( + errLookupInstrumentID = errors.New("unable to lookup instrument ID") + errLookupInstrumentCurrency = errors.New("unable to lookup instrument") +) + // COINUT is the overarching type across the coinut package type COINUT struct { exchange.Base WebsocketConn *wshandler.WebsocketConnection - InstrumentMap map[string]int + instrumentMap instrumentMap +} + +// SeedInstruments seeds the instrument map +func (c *COINUT) SeedInstruments() error { + i, err := c.GetInstruments() + if err != nil { + return err + } + + for _, y := range i.Instruments { + c.instrumentMap.Seed(y[0].Base+y[0].Quote, y[0].InstID) + } + return nil } // GetInstruments returns instruments @@ -54,21 +72,19 @@ func (c *COINUT) GetInstruments() (Instruments, error) { var result Instruments params := make(map[string]interface{}) params["sec_type"] = strings.ToUpper(asset.Spot.String()) - return result, c.SendHTTPRequest(coinutInstruments, params, false, &result) } // GetInstrumentTicker returns a ticker for a specific instrument -func (c *COINUT) GetInstrumentTicker(instrumentID int) (Ticker, error) { +func (c *COINUT) GetInstrumentTicker(instrumentID int64) (Ticker, error) { var result Ticker params := make(map[string]interface{}) params["inst_id"] = instrumentID - return result, c.SendHTTPRequest(coinutTicker, params, false, &result) } // GetInstrumentOrderbook returns the orderbooks for a specific instrument -func (c *COINUT) GetInstrumentOrderbook(instrumentID, limit int) (Orderbook, error) { +func (c *COINUT) GetInstrumentOrderbook(instrumentID, limit int64) (Orderbook, error) { var result Orderbook params := make(map[string]interface{}) params["inst_id"] = instrumentID @@ -96,7 +112,7 @@ func (c *COINUT) GetUserBalance() (UserBalance, error) { } // NewOrder places a new order on the exchange -func (c *COINUT) NewOrder(instrumentID int, quantity, price float64, buy bool, orderID uint32) (interface{}, error) { +func (c *COINUT) NewOrder(instrumentID int64, quantity, price float64, buy bool, orderID uint32) (interface{}, error) { var result interface{} params := make(map[string]interface{}) params["inst_id"] = instrumentID @@ -133,21 +149,20 @@ func (c *COINUT) NewOrders(orders []Order) ([]OrdersBase, error) { } // GetOpenOrders returns a list of open order and relevant information -func (c *COINUT) GetOpenOrders(instrumentID int) (GetOpenOrdersResponse, error) { +func (c *COINUT) GetOpenOrders(instrumentID int64) (GetOpenOrdersResponse, error) { var result GetOpenOrdersResponse params := make(map[string]interface{}) params["inst_id"] = instrumentID - return result, c.SendHTTPRequest(coinutOrdersOpen, params, true, &result) } // CancelExistingOrder cancels a specific order and returns if it was actioned -func (c *COINUT) CancelExistingOrder(instrumentID, orderID int) (bool, error) { +func (c *COINUT) CancelExistingOrder(instrumentID, orderID int64) (bool, error) { var result GenericResponse params := make(map[string]interface{}) type Request struct { - InstrumentID int `json:"inst_id"` - OrderID int `json:"order_id"` + InstrumentID int64 `json:"inst_id"` + OrderID int64 `json:"order_id"` } var entry = Request{ @@ -182,7 +197,7 @@ func (c *COINUT) CancelOrders(orders []CancelOrders) (CancelOrdersResponse, erro } // GetTradeHistory returns trade history for a specific instrument. -func (c *COINUT) GetTradeHistory(instrumentID, start, limit int) (TradeHistory, error) { +func (c *COINUT) GetTradeHistory(instrumentID, start, limit int64) (TradeHistory, error) { var result TradeHistory params := make(map[string]interface{}) params["inst_id"] = instrumentID diff --git a/exchanges/coinut/coinut_test.go b/exchanges/coinut/coinut_test.go index 7c1b8436..54706e55 100644 --- a/exchanges/coinut/coinut_test.go +++ b/exchanges/coinut/coinut_test.go @@ -76,9 +76,6 @@ func setupWSTestAuth(t *testing.T) { if err != nil { t.Error(err) } - - instrumentListByString = make(map[string]int64) - instrumentListByString[currency.NewPair(currency.LTC, currency.BTC).String()] = 1 wsSetupRan = true } @@ -89,6 +86,18 @@ func TestGetInstruments(t *testing.T) { } } +func TestSeedInstruments(t *testing.T) { + err := c.SeedInstruments() + if err != nil { + // No point checking the next condition + t.Fatal(err) + } + + if len(c.instrumentMap.GetInstrumentIDs()) == 0 { + t.Error("instrument map hasn't been seeded") + } +} + func setFeeBuilder() *exchange.FeeBuilder { return &exchange.FeeBuilder{ Amount: 1, @@ -543,3 +552,89 @@ func TestWsAuthGetOpenOrders(t *testing.T) { t.Error(err) } } + +func TestCurrencyMapIsLoaded(t *testing.T) { + t.Parallel() + var i instrumentMap + if l := i.IsLoaded(); l { + t.Error("unexpected result") + } + + i.Seed("BTCUSD", 1337) + if l := i.IsLoaded(); !l { + t.Error("unexpected result") + } +} + +func TestCurrencyMapSeed(t *testing.T) { + t.Parallel() + var i instrumentMap + + // Test non-seeded lookups + if id := i.LookupInstrument(1234); id != "" { + t.Error("unexpected result") + } + if id := i.LookupID("BLAH"); id != 0 { + t.Error("unexpected result") + } + + // Test seeded lookups + i.Seed("BTCUSD", 1337) + if id := i.LookupID("BTCUSD"); id != 1337 { + t.Error("unexpected result") + } + if id := i.LookupInstrument(1337); id != "BTCUSD" { + t.Error("unexpected result") + } + + // Test invalid lookups + if id := i.LookupInstrument(1234); id != "" { + t.Error("unexpected result") + } + if id := i.LookupID("BLAH"); id != 0 { + t.Error("unexpected result") + } + + // Test seeding existing item + i.Seed("BTCUSD", 1234) + if id := i.LookupID("BTCUSD"); id != 1337 { + t.Error("unexpected result") + } + if id := i.LookupInstrument(1337); id != "BTCUSD" { + t.Error("unexpected result") + } +} + +func TestCurrencyMapInstrumentIDs(t *testing.T) { + t.Parallel() + + var i instrumentMap + if r := i.GetInstrumentIDs(); len(r) > 0 { + t.Error("non initialised instrument map shouldn't return any ids") + } + + // Seed the instrument map + i.Seed("BTCUSD", 1234) + i.Seed("LTCUSD", 1337) + + f := func(ids []int64, target int64) bool { + for x := range ids { + if ids[x] == target { + return true + } + } + return false + } + + // Test 2 valid instruments and one invalid + ids := i.GetInstrumentIDs() + if r := f(ids, 1234); !r { + t.Error("unexpected result") + } + if r := f(ids, 1337); !r { + t.Error("unexpected result") + } + if r := f(ids, 4321); r { + t.Error("unexpected result") + } +} diff --git a/exchanges/coinut/coinut_types.go b/exchanges/coinut/coinut_types.go index 900e9fd0..1822bfed 100644 --- a/exchanges/coinut/coinut_types.go +++ b/exchanges/coinut/coinut_types.go @@ -1,6 +1,9 @@ package coinut import ( + "strings" + "sync" + "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" ) @@ -17,7 +20,7 @@ type GenericResponse struct { type InstrumentBase struct { Base string `json:"base"` DecimalPlaces int `json:"decimal_places"` - InstID int `json:"inst_id"` + InstID int64 `json:"inst_id"` Quote string `json:"quote"` } @@ -665,3 +668,85 @@ type WsGetAccountBalanceResponse struct { Status []string `json:"status"` TransID int64 `json:"trans_id"` } + +type instrumentMap struct { + Instruments map[string]int64 + Loaded bool + m sync.Mutex +} + +// IsLoaded returns whether or not the instrument map has been seeded +func (i *instrumentMap) IsLoaded() bool { + i.m.Lock() + defer i.m.Unlock() + return i.Loaded +} + +// Seed seeds the instrument map +func (i *instrumentMap) Seed(currency string, id int64) { + i.m.Lock() + defer i.m.Unlock() + + if !i.Loaded { + i.Instruments = make(map[string]int64) + } + + // check to see if the instrument already exists + _, ok := i.Instruments[currency] + if ok { + return + } + + i.Instruments[currency] = id + i.Loaded = true +} + +// LookupInstrument looks up an instrument based on an id +func (i *instrumentMap) LookupInstrument(id int64) string { + i.m.Lock() + defer i.m.Unlock() + + if !i.Loaded { + return "" + } + + for k, v := range i.Instruments { + if v == id { + return k + } + } + return "" +} + +// LookupID looks up an ID based on a string +func (i *instrumentMap) LookupID(currency string) int64 { + i.m.Lock() + defer i.m.Unlock() + + if !i.Loaded { + return 0 + } + + for k, v := range i.Instruments { + if strings.EqualFold(currency, k) { + return v + } + } + return 0 +} + +// GetInstrumentIDs returns a list of IDs +func (i *instrumentMap) GetInstrumentIDs() []int64 { + i.m.Lock() + defer i.m.Unlock() + + if !i.Loaded { + return nil + } + + var instruments []int64 + for _, x := range i.Instruments { + instruments = append(instruments, x) + } + return instruments +} diff --git a/exchanges/coinut/coinut_websocket.go b/exchanges/coinut/coinut_websocket.go index cd50a9e6..bae03986 100644 --- a/exchanges/coinut/coinut_websocket.go +++ b/exchanges/coinut/coinut_websocket.go @@ -18,14 +18,15 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" ) -const coinutWebsocketURL = "wss://wsapi.coinut.com" -const coinutWebsocketRateLimit = 30 +const ( + coinutWebsocketURL = "wss://wsapi.coinut.com" + coinutWebsocketRateLimit = 30 +) -var nNonce map[int64]string -var channels map[string]chan []byte -var instrumentListByString map[string]int64 -var instrumentListByCode map[int64]string -var populatedList bool +var ( + channels map[string]chan []byte + wsInstrumentMap instrumentMap +) // NOTE for speed considerations // wss://wsapi-as.coinut.com @@ -44,14 +45,11 @@ func (c *COINUT) WsConnect() error { } go c.WsHandleData() - if !populatedList { - instrumentListByString = make(map[string]int64) - instrumentListByCode = make(map[int64]string) + if !wsInstrumentMap.IsLoaded() { err = c.WsSetInstrumentList() if err != nil { return err } - populatedList = true } c.wsAuthenticate() c.GenerateDefaultSubscriptions() @@ -137,7 +135,7 @@ func (c *COINUT) wsProcessResponse(resp []byte) { c.Websocket.DataHandler <- err return } - currencyPair := instrumentListByCode[ticker.InstID] + currencyPair := wsInstrumentMap.LookupInstrument(ticker.InstID) c.Websocket.DataHandler <- wshandler.TickerData{ Exchange: c.Name, Volume: ticker.Volume, @@ -147,7 +145,9 @@ func (c *COINUT) wsProcessResponse(resp []byte) { Last: ticker.Last, Timestamp: time.Unix(0, ticker.Timestamp), AssetType: asset.Spot, - Pair: currency.NewPairFromString(currencyPair), + Pair: currency.NewPairFromFormattedPairs(currencyPair, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)), } case "inst_order_book": @@ -162,11 +162,13 @@ func (c *COINUT) wsProcessResponse(resp []byte) { c.Websocket.DataHandler <- err return } - currencyPair := instrumentListByCode[orderbooksnapshot.InstID] + currencyPair := wsInstrumentMap.LookupInstrument(orderbooksnapshot.InstID) c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: c.GetName(), Asset: asset.Spot, - Pair: currency.NewPairFromString(currencyPair), + Pair: currency.NewPairFromFormattedPairs(currencyPair, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)), } case "inst_order_book_update": var orderbookUpdate WsOrderbookUpdate @@ -180,11 +182,13 @@ func (c *COINUT) wsProcessResponse(resp []byte) { c.Websocket.DataHandler <- err return } - currencyPair := instrumentListByCode[orderbookUpdate.InstID] + currencyPair := wsInstrumentMap.LookupInstrument(orderbookUpdate.InstID) c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: c.GetName(), Asset: asset.Spot, - Pair: currency.NewPairFromString(currencyPair), + Pair: currency.NewPairFromFormattedPairs(currencyPair, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)), } case "inst_trade": var tradeSnap WsTradeSnapshot @@ -201,14 +205,16 @@ func (c *COINUT) wsProcessResponse(resp []byte) { c.Websocket.DataHandler <- err return } - currencyPair := instrumentListByCode[tradeUpdate.InstID] + currencyPair := wsInstrumentMap.LookupInstrument(tradeUpdate.InstID) c.Websocket.DataHandler <- wshandler.TradeData{ - Timestamp: time.Unix(tradeUpdate.Timestamp, 0), - CurrencyPair: currency.NewPairFromString(currencyPair), - AssetType: asset.Spot, - Exchange: c.GetName(), - Price: tradeUpdate.Price, - Side: tradeUpdate.Side, + Timestamp: time.Unix(tradeUpdate.Timestamp, 0), + CurrencyPair: currency.NewPairFromFormattedPairs(currencyPair, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)), + AssetType: asset.Spot, + Exchange: c.GetName(), + Price: tradeUpdate.Price, + Side: tradeUpdate.Side, } default: if incoming.Nonce > 0 { @@ -247,11 +253,10 @@ func (c *COINUT) WsSetInstrumentList() error { return err } for curr, data := range list.Spot { - instrumentListByString[curr] = data[0].InstID - instrumentListByCode[data[0].InstID] = curr + wsInstrumentMap.Seed(curr, data[0].InstID) } - if len(instrumentListByString) == 0 || len(instrumentListByCode) == 0 { - return errors.New("instrument lists failed to populate") + if len(wsInstrumentMap.GetInstrumentIDs()) == 0 { + return errors.New("instrument list failed to populate") } return nil } @@ -277,7 +282,11 @@ func (c *COINUT) WsProcessOrderbookSnapshot(ob *WsOrderbookSnapshot) error { var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids - newOrderBook.Pair = currency.NewPairFromString(instrumentListByCode[ob.InstID]) + newOrderBook.Pair = currency.NewPairFromFormattedPairs( + wsInstrumentMap.LookupInstrument(ob.InstID), + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true), + ) newOrderBook.AssetType = asset.Spot return c.Websocket.Orderbook.LoadSnapshot(&newOrderBook, false) @@ -285,7 +294,11 @@ func (c *COINUT) WsProcessOrderbookSnapshot(ob *WsOrderbookSnapshot) error { // WsProcessOrderbookUpdate process an orderbook update func (c *COINUT) WsProcessOrderbookUpdate(update *WsOrderbookUpdate) error { - p := currency.NewPairFromString(instrumentListByCode[update.InstID]) + p := currency.NewPairFromFormattedPairs( + wsInstrumentMap.LookupInstrument(update.InstID), + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true), + ) bufferUpdate := &wsorderbook.WebsocketOrderbookUpdate{ CurrencyPair: p, UpdateID: update.TransID, @@ -318,8 +331,9 @@ func (c *COINUT) GenerateDefaultSubscriptions() { // Subscribe sends a websocket message to receive data from the channel func (c *COINUT) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := wsRequest{ - Request: channelToSubscribe.Channel, - InstID: instrumentListByString[channelToSubscribe.Currency.String()], + Request: channelToSubscribe.Channel, + InstID: wsInstrumentMap.LookupID(c.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String()), Subscribe: true, Nonce: c.WebsocketConn.GenerateMessageID(false), } @@ -329,8 +343,9 @@ func (c *COINUT) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscrip // Unsubscribe sends a websocket message to stop receiving data from the channel func (c *COINUT) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := wsRequest{ - Request: channelToSubscribe.Channel, - InstID: instrumentListByString[channelToSubscribe.Currency.String()], + Request: channelToSubscribe.Channel, + InstID: wsInstrumentMap.LookupID(c.FormatExchangeCurrency(channelToSubscribe.Currency, + asset.Spot).String()), Subscribe: false, Nonce: c.WebsocketConn.GenerateMessageID(false), } @@ -419,7 +434,7 @@ func (c *COINUT) wsSubmitOrder(order *WsSubmitOrderParameters) (*WsStandardOrder var orderSubmissionRequest WsSubmitOrderRequest orderSubmissionRequest.Request = "new_order" orderSubmissionRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) - orderSubmissionRequest.InstID = instrumentListByString[curr] + orderSubmissionRequest.InstID = wsInstrumentMap.LookupID(curr) orderSubmissionRequest.Qty = order.Amount orderSubmissionRequest.Price = order.Price orderSubmissionRequest.Side = string(order.Side) @@ -530,7 +545,7 @@ func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) ([]WsStandardO Qty: orders[i].Amount, Price: orders[i].Price, Side: string(orders[i].Side), - InstID: instrumentListByString[curr], + InstID: wsInstrumentMap.LookupID(curr), ClientOrdID: i + 1, }) } @@ -567,7 +582,7 @@ func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) ([]WsStandardO if len(standardOrder.Reasons) > 0 && standardOrder.Reasons[0] != "" { errors = append(errors, fmt.Errorf("%v order submission failed for currency %v and orderID %v, message %v ", c.Name, - instrumentListByCode[standardOrder.InstID], + wsInstrumentMap.LookupInstrument(standardOrder.InstID), standardOrder.OrderID, standardOrder.Reasons[0])) @@ -587,7 +602,7 @@ func (c *COINUT) wsGetOpenOrders(p currency.Pair) error { var openOrdersRequest WsGetOpenOrdersRequest openOrdersRequest.Request = "user_open_orders" openOrdersRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) - openOrdersRequest.InstID = instrumentListByString[curr] + openOrdersRequest.InstID = wsInstrumentMap.LookupID(curr) resp, err := c.WebsocketConn.SendMessageReturnResponse(openOrdersRequest.Nonce, openOrdersRequest) if err != nil { @@ -613,7 +628,7 @@ func (c *COINUT) wsCancelOrder(cancellation WsCancelOrderParameters) error { currency := c.FormatExchangeCurrency(cancellation.Currency, asset.Spot).String() var cancellationRequest WsCancelOrderRequest cancellationRequest.Request = "cancel_order" - cancellationRequest.InstID = instrumentListByString[currency] + cancellationRequest.InstID = wsInstrumentMap.LookupID(currency) cancellationRequest.OrderID = cancellation.OrderID cancellationRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) @@ -645,7 +660,7 @@ func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) (*WsCan for i := range cancellations { currency := c.FormatExchangeCurrency(cancellations[i].Currency, asset.Spot).String() cancelOrderRequest.Entries = append(cancelOrderRequest.Entries, WsCancelOrdersRequestEntry{ - InstID: instrumentListByString[currency], + InstID: wsInstrumentMap.LookupID(currency), OrderID: cancellations[i].OrderID, }) } @@ -668,7 +683,7 @@ func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) (*WsCan if response.Results[i].Status != "OK" { errors = append(errors, fmt.Errorf("%v order cancellation failed for currency %v and orderID %v, message %v", c.Name, - instrumentListByCode[response.Results[i].InstID], + wsInstrumentMap.LookupInstrument(response.Results[i].InstID), response.Results[i].OrderID, response.Results[i].Status)) } @@ -683,7 +698,7 @@ func (c *COINUT) wsGetTradeHistory(p currency.Pair, start, limit int64) error { curr := c.FormatExchangeCurrency(p, asset.Spot).String() var request WsTradeHistoryRequest request.Request = "trade_history" - request.InstID = instrumentListByString[curr] + request.InstID = wsInstrumentMap.LookupID(curr) request.Nonce = c.WebsocketConn.GenerateMessageID(false) request.Start = start request.Limit = limit diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index cb59e59a..78e60142 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -1,6 +1,7 @@ package coinut import ( + "errors" "fmt" "strconv" "strings" @@ -60,6 +61,7 @@ func (c *COINUT) SetDefaults() { }, ConfigFormat: ¤cy.PairFormat{ Uppercase: true, + Delimiter: "-", }, } @@ -162,11 +164,30 @@ func (c *COINUT) Run() { c.PrintEnabledPairs() } - if !c.GetEnabledFeatures().AutoPairUpdates { + forceUpdate := false + delim := c.GetPairFormat(asset.Spot, false).Delimiter + if !common.StringDataContains(c.CurrencyPairs.GetPairs(asset.Spot, + true).Strings(), delim) || + !common.StringDataContains(c.CurrencyPairs.GetPairs(asset.Spot, + false).Strings(), delim) { + enabledPairs := currency.NewPairsFromStrings( + []string{fmt.Sprintf("LTC%sUSDT", delim)}, + ) + log.Warn(log.ExchangeSys, + "Enabled pairs for Coinut reset due to config upgrade, please enable the ones you would like to use again") + forceUpdate = true + + err := c.UpdatePairs(enabledPairs, asset.Spot, true, true) + if err != nil { + log.Errorf(log.ExchangeSys, "%s failed to update currencies. Err: %s\n", c.Name, err) + } + } + + if !c.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { return } - err := c.UpdateTradablePairs(false) + err := c.UpdateTradablePairs(forceUpdate) if err != nil { log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", c.Name, err) } @@ -180,10 +201,10 @@ func (c *COINUT) FetchTradablePairs(asset asset.Item) ([]string, error) { } var pairs []string - c.InstrumentMap = make(map[string]int) - for x, y := range i.Instruments { - c.InstrumentMap[x] = y[0].InstID - pairs = append(pairs, x) + for _, y := range i.Instruments { + c.instrumentMap.Seed(y[0].Base+y[0].Quote, y[0].InstID) + p := y[0].Base + c.GetPairFormat(asset, false).Delimiter + y[0].Quote + pairs = append(pairs, p) } return pairs, nil @@ -197,7 +218,8 @@ func (c *COINUT) UpdateTradablePairs(forceUpdate bool) error { return err } - return c.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate) + return c.UpdatePairs(currency.NewPairsFromStrings(pairs), + asset.Spot, false, forceUpdate) } // GetAccountInfo retrieves balances for all enabled currencies for the @@ -278,7 +300,21 @@ func (c *COINUT) GetAccountInfo() (exchange.AccountInfo, error) { // UpdateTicker updates and returns the ticker for a currency pair func (c *COINUT) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { var tickerPrice ticker.Price - tick, err := c.GetInstrumentTicker(c.InstrumentMap[c.FormatExchangeCurrency(p, assetType).String()]) + + if !c.instrumentMap.IsLoaded() { + err := c.SeedInstruments() + if err != nil { + return tickerPrice, err + } + } + + instID := c.instrumentMap.LookupID(c.FormatExchangeCurrency(p, + assetType).String()) + if instID == 0 { + return tickerPrice, errors.New("unable to lookup instrument ID") + } + + tick, err := c.GetInstrumentTicker(instID) if err != nil { return tickerPrice, err } @@ -320,7 +356,21 @@ func (c *COINUT) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderboo // UpdateOrderbook updates and returns the orderbook for a currency pair func (c *COINUT) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := c.GetInstrumentOrderbook(c.InstrumentMap[p.String()], 200) + + if !c.instrumentMap.IsLoaded() { + err := c.SeedInstruments() + if err != nil { + return orderBook, err + } + } + + instID := c.instrumentMap.LookupID(c.FormatExchangeCurrency(p, + assetType).String()) + if instID == 0 { + return orderBook, errLookupInstrumentID + } + + orderbookNew, err := c.GetInstrumentOrderbook(instID, 200) if err != nil { return orderBook, err } @@ -378,14 +428,18 @@ func (c *COINUT) SubmitOrder(order *exchange.OrderSubmission) (exchange.SubmitOr clientIDUint := uint32(clientIDInt) - // Need to get the ID of the currency sent - instruments, err := c.GetInstruments() - if err != nil { - return submitOrderResponse, err + if !c.instrumentMap.IsLoaded() { + err = c.SeedInstruments() + if err != nil { + return submitOrderResponse, err + } } - currencyArray := instruments.Instruments[order.Pair.String()] - currencyID := currencyArray[0].InstID + currencyID := c.instrumentMap.LookupID(c.FormatExchangeCurrency(order.Pair, + asset.Spot).String()) + if currencyID == 0 { + return submitOrderResponse, errLookupInstrumentID + } switch order.OrderType { case exchange.LimitOrderType: @@ -425,23 +479,25 @@ func (c *COINUT) ModifyOrder(action *exchange.ModifyOrder) (string, error) { // CancelOrder cancels an order by its corresponding ID number func (c *COINUT) CancelOrder(order *exchange.OrderCancellation) error { orderIDInt, err := strconv.ParseInt(order.OrderID, 10, 64) - if err != nil { return err } - // Need to get the ID of the currency sent - instruments, err := c.GetInstruments() - - if err != nil { - return err + if !c.instrumentMap.IsLoaded() { + err = c.SeedInstruments() + if err != nil { + return err + } } - currencyArray := instruments.Instruments[c.FormatExchangeCurrency(order.CurrencyPair, - order.AssetType).String()] - currencyID := currencyArray[0].InstID - _, err = c.CancelExistingOrder(currencyID, int(orderIDInt)) - + currencyID := c.instrumentMap.LookupID(c.FormatExchangeCurrency( + order.CurrencyPair, + asset.Spot).String(), + ) + if currencyID == 0 { + return errLookupInstrumentID + } + _, err = c.CancelExistingOrder(currencyID, orderIDInt) return err } @@ -454,22 +510,22 @@ func (c *COINUT) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cancel cancelAllOrdersResponse := exchange.CancelAllOrdersResponse{ OrderStatus: make(map[string]string), } - instruments, err := c.GetInstruments() - if err != nil { - return cancelAllOrdersResponse, err + + if !c.instrumentMap.IsLoaded() { + err := c.SeedInstruments() + if err != nil { + return cancelAllOrdersResponse, err + } } var allTheOrders []OrderResponse - for _, allInstrumentData := range instruments.Instruments { - for _, instrumentData := range allInstrumentData { - - openOrders, err := c.GetOpenOrders(instrumentData.InstID) - if err != nil { - return cancelAllOrdersResponse, err - } - - allTheOrders = append(allTheOrders, openOrders.Orders...) + ids := c.instrumentMap.GetInstrumentIDs() + for x := range ids { + openOrders, err := c.GetOpenOrders(ids[x]) + if err != nil { + return cancelAllOrdersResponse, err } + allTheOrders = append(allTheOrders, openOrders.Orders...) } var allTheOrdersToCancel []CancelOrders @@ -499,8 +555,7 @@ func (c *COINUT) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cancel // GetOrderInfo returns information on a current open order func (c *COINUT) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { - var orderDetail exchange.OrderDetail - return orderDetail, common.ErrNotYetImplemented + return exchange.OrderDetail{}, common.ErrNotYetImplemented } // GetDepositAddress returns a deposit address for a specified currency @@ -542,51 +597,51 @@ func (c *COINUT) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) // GetActiveOrders retrieves any orders that are active/open func (c *COINUT) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - instruments, err := c.GetInstruments() - if err != nil { - return nil, err - } - - var allTheOrders []OrderResponse - for instrument, allInstrumentData := range instruments.Instruments { - for _, instrumentData := range allInstrumentData { - for _, currency := range getOrdersRequest.Currencies { - currStr := fmt.Sprintf("%v%v%v", - currency.Base.String(), - c.CurrencyPairs.Get(asset.Spot).ConfigFormat.Delimiter, - currency.Quote.String()) - if strings.EqualFold(currStr, instrument) { - openOrders, err := c.GetOpenOrders(instrumentData.InstID) - if err != nil { - return nil, err - } - allTheOrders = append(allTheOrders, openOrders.Orders...) - - continue - } - } + if !c.instrumentMap.IsLoaded() { + err := c.SeedInstruments() + if err != nil { + return nil, err } } + var instrumentsToUse []int64 + if len(getOrdersRequest.Currencies) > 0 { + for x := range getOrdersRequest.Currencies { + currency := c.FormatExchangeCurrency(getOrdersRequest.Currencies[x], + asset.Spot).String() + instrumentsToUse = append(instrumentsToUse, + c.instrumentMap.LookupID(currency)) + } + } else { + instrumentsToUse = c.instrumentMap.GetInstrumentIDs() + } + + if len(instrumentsToUse) == 0 { + return nil, errors.New("no instrument IDs to use") + } + var orders []exchange.OrderDetail - for _, order := range allTheOrders { - for instrument, allInstrumentData := range instruments.Instruments { - for _, instrumentData := range allInstrumentData { - if instrumentData.InstID == int(order.InstrumentID) { - currPair := currency.NewPairDelimiter(instrument, "") - orderSide := exchange.OrderSide(strings.ToUpper(order.Side)) - orderDate := time.Unix(order.Timestamp, 0) - orders = append(orders, exchange.OrderDetail{ - ID: strconv.FormatInt(order.OrderID, 10), - Amount: order.Quantity, - Price: order.Price, - Exchange: c.Name, - OrderSide: orderSide, - OrderDate: orderDate, - CurrencyPair: currPair, - }) - } - } + for x := range instrumentsToUse { + openOrders, err := c.GetOpenOrders(instrumentsToUse[x]) + if err != nil { + return nil, err + } + for y := range openOrders.Orders { + curr := c.instrumentMap.LookupInstrument(instrumentsToUse[x]) + p := currency.NewPairFromFormattedPairs(curr, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)) + orderSide := exchange.OrderSide(strings.ToUpper(openOrders.Orders[y].Side)) + orderDate := time.Unix(openOrders.Orders[y].Timestamp, 0) + orders = append(orders, exchange.OrderDetail{ + ID: strconv.FormatInt(openOrders.Orders[y].OrderID, 10), + Amount: openOrders.Orders[y].Quantity, + Price: openOrders.Orders[y].Price, + Exchange: c.Name, + OrderSide: orderSide, + OrderDate: orderDate, + CurrencyPair: p, + }) } } @@ -598,56 +653,60 @@ func (c *COINUT) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([ // GetOrderHistory retrieves account order information // Can Limit response to specific order status func (c *COINUT) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { - instruments, err := c.GetInstruments() - if err != nil { - return nil, err - } - - var allTheOrders []OrderFilledResponse - for instrument, allInstrumentData := range instruments.Instruments { - for _, instrumentData := range allInstrumentData { - for _, currency := range getOrdersRequest.Currencies { - currStr := fmt.Sprintf("%v%v", - currency.Base.String(), currency.Quote.String()) - if strings.EqualFold(currStr, instrument) { - orders, err := c.GetTradeHistory(instrumentData.InstID, -1, -1) - if err != nil { - return nil, err - } - allTheOrders = append(allTheOrders, orders.Trades...) - - continue - } - } + if !c.instrumentMap.IsLoaded() { + err := c.SeedInstruments() + if err != nil { + return nil, err } } - var orders []exchange.OrderDetail - for i := range allTheOrders { - for instrument, allInstrumentData := range instruments.Instruments { - for j := range allInstrumentData { - if allInstrumentData[j].InstID == int(allTheOrders[i].Order.InstrumentID) { - currPair := currency.NewPairDelimiter(instrument, "") - orderSide := exchange.OrderSide(strings.ToUpper(allTheOrders[i].Order.Side)) - orderDate := time.Unix(allTheOrders[i].Order.Timestamp, 0) - orders = append(orders, exchange.OrderDetail{ - ID: strconv.FormatInt(allTheOrders[i].Order.OrderID, 10), - Amount: allTheOrders[i].Order.Quantity, - Price: allTheOrders[i].Order.Price, - Exchange: c.Name, - OrderSide: orderSide, - OrderDate: orderDate, - CurrencyPair: currPair, - }) - } - } + var instrumentsToUse []int64 + if len(getOrdersRequest.Currencies) > 0 { + for x := range getOrdersRequest.Currencies { + currency := c.FormatExchangeCurrency(getOrdersRequest.Currencies[x], + asset.Spot).String() + instrumentsToUse = append(instrumentsToUse, + c.instrumentMap.LookupID(currency)) } + } else { + instrumentsToUse = c.instrumentMap.GetInstrumentIDs() } - exchange.FilterOrdersByTickRange(&orders, getOrdersRequest.StartTicks, + if len(instrumentsToUse) == 0 { + return nil, errors.New("no instrument IDs to use") + } + + var allOrders []exchange.OrderDetail + for x := range instrumentsToUse { + orders, err := c.GetTradeHistory(instrumentsToUse[x], -1, -1) + if err != nil { + return nil, err + } + for y := range orders.Trades { + curr := c.instrumentMap.LookupInstrument(instrumentsToUse[x]) + p := currency.NewPairFromFormattedPairs(curr, + c.GetEnabledPairs(asset.Spot), + c.GetPairFormat(asset.Spot, true)) + orderSide := exchange.OrderSide( + strings.ToUpper(orders.Trades[y].Order.Side)) + orderDate := time.Unix(orders.Trades[y].Order.Timestamp, 0) + allOrders = append(allOrders, exchange.OrderDetail{ + ID: strconv.FormatInt(orders.Trades[y].Order.OrderID, 10), + Amount: orders.Trades[y].Order.Quantity, + Price: orders.Trades[y].Order.Price, + Exchange: c.Name, + OrderSide: orderSide, + OrderDate: orderDate, + CurrencyPair: p, + }) + } + + } + + exchange.FilterOrdersByTickRange(&allOrders, getOrdersRequest.StartTicks, getOrdersRequest.EndTicks) - exchange.FilterOrdersBySide(&orders, getOrdersRequest.OrderSide) - return orders, nil + exchange.FilterOrdersBySide(&allOrders, getOrdersRequest.OrderSide) + return allOrders, nil } // SubscribeToWebsocketChannels appends to ChannelsToSubscribe diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go index 10579e76..ef65153f 100644 --- a/exchanges/okcoin/okcoin_wrapper.go +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -124,20 +124,19 @@ func (o *OKCoin) Run() { log.Debugf(log.ExchangeSys, "%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket.IsEnabled()), o.WebsocketURL) } - if o.Config.CurrencyPairs.ConfigFormat.Delimiter != o.CurrencyPairs.ConfigFormat.Delimiter { - o.Config.CurrencyPairs.ConfigFormat.Delimiter = o.CurrencyPairs.ConfigFormat.Delimiter - } - if o.Config.CurrencyPairs.RequestFormat.Uppercase != o.CurrencyPairs.RequestFormat.Uppercase { - o.Config.CurrencyPairs.RequestFormat.Uppercase = true - } - if o.Config.CurrencyPairs.RequestFormat.Delimiter != o.CurrencyPairs.RequestFormat.Delimiter { - o.Config.CurrencyPairs.RequestFormat.Delimiter = o.CurrencyPairs.RequestFormat.Delimiter - } - - if !common.StringDataContains(o.Config.CurrencyPairs.Pairs[asset.Spot].Enabled.Strings(), o.CurrencyPairs.RequestFormat.Delimiter) { - enabledPairs := currency.NewPairsFromStrings([]string{"BTC-USD"}) + forceUpdate := false + delim := o.GetPairFormat(asset.Spot, false).Delimiter + if !common.StringDataContains(o.CurrencyPairs.GetPairs(asset.Spot, + true).Strings(), delim) || + !common.StringDataContains(o.CurrencyPairs.GetPairs(asset.Spot, + false).Strings(), delim) { + enabledPairs := currency.NewPairsFromStrings([]string{ + fmt.Sprintf("BTC%sUSD", delim), + }) log.Warnf(log.ExchangeSys, - "Enabled pairs for %v reset due to config upgrade, please enable the ones you would like again.", o.Name) + "Enabled pairs for %v reset due to config upgrade, please enable the ones you would like again.\n", + o.Name) + forceUpdate = true err := o.UpdatePairs(enabledPairs, asset.Spot, true, true) if err != nil { @@ -146,11 +145,11 @@ func (o *OKCoin) Run() { } } - if !o.GetEnabledFeatures().AutoPairUpdates { + if !o.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { return } - err := o.UpdateTradablePairs(false) + err := o.UpdateTradablePairs(forceUpdate) if err != nil { log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", o.Name, err) } @@ -165,7 +164,8 @@ func (o *OKCoin) FetchTradablePairs(asset asset.Item) ([]string, error) { var pairs []string for x := range prods { - pairs = append(pairs, fmt.Sprintf("%v%v%v", prods[x].BaseCurrency, o.GetPairFormat(asset, false).Delimiter, prods[x].QuoteCurrency)) + pairs = append(pairs, fmt.Sprintf("%v%v%v", prods[x].BaseCurrency, + o.GetPairFormat(asset, false).Delimiter, prods[x].QuoteCurrency)) } return pairs, nil