Engine QA (#367)

* Improved error message when no config is set on startup

* Change inccorect error wording

* bump Bitfinex websocket orderbook return length to max

* temporary fix of incorrect orderbook updates, limit to bid and ask len of 100, will be extended later if needed

* Fixed issue in binance websocket that appended 0 volume bid/ask items

* Fix panic when unmarshalling an empty pair from config

* Add get pair asset method for exchange base
Fix Bitmex orderbook stream
Unbuffer Bitmex orderbook stream

* force syncer to update ticker instead of fetch, which allows a stream

* Fix websocket last price for coinbasepro

* fix websocket ticker for coinut

* Fix websocket orderbook stream Huobi

* increase orderbook depth REST for Huobi

* Fix websocket support and ensure data integrity

* Fix time parsing issue after error checks

* check error, only process enabled currency pairs, signal websocket data processing

* expanded websocket functionality for okgroup

* Add logic to not process zero length slice for orderbooks

* fix websocket ticker only updating enabled and individual book updates

* ZB fixes to order submission/retrieval/cancellation w/ general fixes

* Quiet unnecessary warning

* updated config entry values for REST and websocket (initial hack until I come up with a better solution for asset types)

* Ch GetName function to field access modifyer & rm useless code

* Add in error I missed

* Nits addressed

* some more fixes

* Turned kraken default websocket to true and some small changes

* fixes linter issues

* Ensured okgroup books and sent update through to datahandler. Zb update as well.

* Add test case to get asset type from pair

* Add test for pairs unmarshal

* Add testing and addressed nits

* FIX linter issue

* Addressed Gees nits

* Thanks glorious spotter

* more nitorinos

* Addres even more nits

* Add stringerino 4000

* Fix for panic cause by sort slice out of range, also nits addressed

* fix linter issues

* Changed from function to field access

* Changed from function to field access

* fix for orderbook update panic, removes quick fix - caused by sync item fetching through same protocol

* Add new test and update random generator

* pass in invalid string to future ob fetching, due to futures contract expire and a http 400 error is returned
This commit is contained in:
Ryan O'Hara-Reid
2019-11-04 15:34:30 +11:00
committed by Adrian Gallagher
parent e2c349424f
commit 22ff33cd54
53 changed files with 1813 additions and 1074 deletions

View File

@@ -1433,7 +1433,8 @@ func GetFilePath(file string) (string, error) {
return newDirs[0], nil
}
return "", errors.New("config default file path error")
return "", fmt.Errorf("config.json file not found in %s, please follow README.md in root dir for config generation",
newDir)
}
// ReadConfig verifies and checks for encryption and verifies the unencrypted

View File

@@ -73,6 +73,11 @@ func (p *Pairs) UnmarshalJSON(d []byte) error {
return err
}
// If no pairs enabled in config just continue
if pairs == "" {
return nil
}
var allThePairs Pairs
for _, data := range strings.Split(pairs, ",") {
allThePairs = append(allThePairs, NewPairFromString(data))

View File

@@ -68,13 +68,28 @@ func TestPairsFormat(t *testing.T) {
func TestPairsUnmarshalJSON(t *testing.T) {
var unmarshalHere Pairs
configPairs := "btc_usd,btc_aud,btc_ltc"
configPairs := ""
encoded, err := common.JSONEncode(configPairs)
if err != nil {
t.Fatal("Pairs UnmarshalJSON() error", err)
}
err = common.JSONDecode([]byte{1, 3, 3, 7}, &unmarshalHere)
if err == nil {
t.Fatal("error cannot be nil")
}
err = common.JSONDecode(encoded, &unmarshalHere)
if err != nil {
t.Fatal("Pairs UnmarshalJSON() error", err)
}
configPairs = "btc_usd,btc_aud,btc_ltc"
encoded, err = common.JSONEncode(configPairs)
if err != nil {
t.Fatal("Pairs UnmarshalJSON() error", err)
}
err = common.JSONDecode(encoded, &unmarshalHere)
if err != nil {
t.Fatal("Pairs UnmarshalJSON() error", err)

View File

@@ -400,7 +400,7 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
result, err = Bot.Exchanges[x].FetchTicker(c.Pair, c.AssetType)
}
} else {
result, err = Bot.Exchanges[x].FetchTicker(c.Pair, c.AssetType)
result, err = Bot.Exchanges[x].UpdateTicker(c.Pair, c.AssetType)
}
printTickerSummary(&result, c.Pair, c.AssetType, exchangeName, err)
if err == nil {

View File

@@ -92,13 +92,13 @@ type DepthUpdateParams []struct {
// WebsocketDepthStream is the difference for the update depth stream
type WebsocketDepthStream struct {
Event string `json:"e"`
Timestamp int64 `json:"E"`
Pair string `json:"s"`
FirstUpdateID int64 `json:"U"`
LastUpdateID int64 `json:"u"`
UpdateBids []interface{} `json:"b"`
UpdateAsks []interface{} `json:"a"`
Event string `json:"e"`
Timestamp int64 `json:"E"`
Pair string `json:"s"`
FirstUpdateID int64 `json:"U"`
LastUpdateID int64 `json:"u"`
UpdateBids [][]interface{} `json:"b"`
UpdateAsks [][]interface{} `json:"a"`
}
// RecentTradeRequestParams represents Klines request data.

View File

@@ -236,12 +236,16 @@ func (b *Binance) SeedLocalCache(p currency.Pair) error {
}
for i := range orderbookNew.Bids {
newOrderBook.Bids = append(newOrderBook.Bids,
orderbook.Item{Amount: orderbookNew.Bids[i].Quantity, Price: orderbookNew.Bids[i].Price})
newOrderBook.Bids = append(newOrderBook.Bids, orderbook.Item{
Amount: orderbookNew.Bids[i].Quantity,
Price: orderbookNew.Bids[i].Price,
})
}
for i := range orderbookNew.Asks {
newOrderBook.Asks = append(newOrderBook.Asks,
orderbook.Item{Amount: orderbookNew.Asks[i].Quantity, Price: orderbookNew.Asks[i].Price})
newOrderBook.Asks = append(newOrderBook.Asks, orderbook.Item{
Amount: orderbookNew.Asks[i].Quantity,
Price: orderbookNew.Asks[i].Price,
})
}
newOrderBook.LastUpdated = time.Unix(orderbookNew.LastUpdateID, 0)
@@ -256,38 +260,38 @@ func (b *Binance) SeedLocalCache(p currency.Pair) error {
func (b *Binance) UpdateLocalCache(wsdp *WebsocketDepthStream) error {
var updateBid, updateAsk []orderbook.Item
for i := range wsdp.UpdateBids {
var priceToBeUpdated orderbook.Item
for i, bids := range wsdp.UpdateBids[i].([]interface{}) {
switch i {
case 0:
priceToBeUpdated.Price, _ = strconv.ParseFloat(bids.(string), 64)
case 1:
priceToBeUpdated.Amount, _ = strconv.ParseFloat(bids.(string), 64)
}
p, err := strconv.ParseFloat(wsdp.UpdateBids[i][0].(string), 64)
if err != nil {
return err
}
updateBid = append(updateBid, priceToBeUpdated)
a, err := strconv.ParseFloat(wsdp.UpdateBids[i][1].(string), 64)
if err != nil {
return err
}
updateBid = append(updateBid, orderbook.Item{Price: p, Amount: a})
}
for i := range wsdp.UpdateAsks {
var priceToBeUpdated orderbook.Item
for i, asks := range wsdp.UpdateAsks[i].([]interface{}) {
switch i {
case 0:
priceToBeUpdated.Price, _ = strconv.ParseFloat(asks.(string), 64)
case 1:
priceToBeUpdated.Amount, _ = strconv.ParseFloat(asks.(string), 64)
}
p, err := strconv.ParseFloat(wsdp.UpdateAsks[i][0].(string), 64)
if err != nil {
return err
}
updateAsk = append(updateAsk, priceToBeUpdated)
a, err := strconv.ParseFloat(wsdp.UpdateAsks[i][1].(string), 64)
if err != nil {
return err
}
updateAsk = append(updateAsk, orderbook.Item{Price: p, Amount: a})
}
currencyPair := currency.NewPairFromFormattedPairs(wsdp.Pair, b.GetEnabledPairs(asset.Spot),
b.GetPairFormat(asset.Spot, true))
return b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{
Bids: updateBid,
Asks: updateAsk,
CurrencyPair: currencyPair,
UpdateID: wsdp.LastUpdateID,
AssetType: asset.Spot,
Bids: updateBid,
Asks: updateAsk,
Pair: currencyPair,
UpdateID: wsdp.LastUpdateID,
Asset: asset.Spot,
})
}

View File

@@ -162,7 +162,7 @@ func (b *Binance) Setup(exch *config.ExchangeConfig) error {
b.Websocket.Orderbook.Setup(
exch.WebsocketOrderbookBufferLimit,
true,
false,
true,
true,
false,
@@ -202,7 +202,8 @@ func (b *Binance) Run() {
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update currencies. Err: %s\n",
b.Name, err)
b.Name,
err)
}
}
@@ -247,7 +248,10 @@ func (b *Binance) UpdateTradablePairs(forceUpdate bool) error {
return err
}
return b.UpdatePairs(currency.NewPairsFromStrings(pairs), asset.Spot, false, forceUpdate)
return b.UpdatePairs(currency.NewPairsFromStrings(pairs),
asset.Spot,
false,
forceUpdate)
}
// UpdateTicker updates and returns the ticker for a currency pair

View File

@@ -498,10 +498,10 @@ func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books
// orderbook sides
func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book []WebsocketBook) error {
orderbookUpdate := wsorderbook.WebsocketOrderbookUpdate{
Asks: []orderbook.Item{},
Bids: []orderbook.Item{},
AssetType: assetType,
CurrencyPair: p,
Asks: []orderbook.Item{},
Bids: []orderbook.Item{},
Asset: assetType,
Pair: p,
}
for i := 0; i < len(book); i++ {
@@ -548,6 +548,7 @@ func (b *Bitfinex) GenerateDefaultSubscriptions() {
params := make(map[string]interface{})
if channels[i] == "book" {
params["prec"] = "P0"
params["len"] = "100"
}
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: channels[i],

View File

@@ -206,8 +206,17 @@ func (b *Bitmex) wsHandleIncomingData() {
}
p := currency.NewPairFromString(orderbooks.Data[0].Symbol)
// TODO: update this to support multiple asset types
err = b.processOrderbook(orderbooks.Data, orderbooks.Action, p, "CONTRACT")
var a asset.Item
a, err = b.GetPairAssetType(p)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
err = b.processOrderbook(orderbooks.Data,
orderbooks.Action,
p,
a)
if err != nil {
b.Websocket.DataHandler <- err
continue
@@ -345,12 +354,14 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai
asks = append(asks, orderbook.Item{
Price: data[i].Price,
Amount: float64(data[i].Size),
ID: data[i].ID,
})
continue
}
bids = append(bids, orderbook.Item{
Price: data[i].Price,
Amount: float64(data[i].Size),
ID: data[i].ID,
})
}
@@ -379,24 +390,23 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai
for i := range data {
if strings.EqualFold(data[i].Side, "Sell") {
asks = append(asks, orderbook.Item{
Price: data[i].Price,
Amount: float64(data[i].Size),
ID: data[i].ID,
})
continue
}
bids = append(bids, orderbook.Item{
Price: data[i].Price,
Amount: float64(data[i].Size),
ID: data[i].ID,
})
}
err := b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: currencyPair,
UpdateTime: time.Now(),
AssetType: assetType,
Action: action,
Bids: bids,
Asks: asks,
Pair: currencyPair,
Asset: assetType,
Action: action,
})
if err != nil {
return err
@@ -413,7 +423,16 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitmex) GenerateDefaultSubscriptions() {
contracts := b.GetEnabledPairs(asset.PerpetualContract)
assets := b.GetAssetTypes()
var allPairs currency.Pairs
for x := range assets {
contracts := b.GetEnabledPairs(assets[x])
for y := range contracts {
allPairs = allPairs.Add(contracts[y])
}
}
channels := []string{bitmexWSOrderbookL2, bitmexWSTrade}
subscriptions := []wshandler.WebsocketChannelSubscription{
{
@@ -422,10 +441,10 @@ func (b *Bitmex) GenerateDefaultSubscriptions() {
}
for i := range channels {
for j := range contracts {
for j := range allPairs {
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: fmt.Sprintf("%v:%v", channels[i], contracts[j].String()),
Currency: contracts[j],
Channel: fmt.Sprintf("%v:%v", channels[i], allPairs[j].String()),
Currency: allPairs[j],
})
}
}

View File

@@ -188,7 +188,7 @@ func (b *Bitmex) Setup(exch *config.ExchangeConfig) error {
b.Websocket.Orderbook.Setup(
exch.WebsocketOrderbookBufferLimit,
true,
false,
false,
false,
true,

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
@@ -13,7 +14,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook"
log "github.com/thrasher-corp/gocryptotrader/logger"
)
@@ -32,7 +32,7 @@ func (b *Bitstamp) WsConnect() error {
return err
}
if b.Verbose {
log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", b.GetName())
log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", b.Name)
}
err = b.seedOrderBook()
@@ -76,7 +76,7 @@ func (b *Bitstamp) WsHandleData() {
switch wsResponse.Event {
case "bts:request_reconnect":
if b.Verbose {
log.Debugf(log.ExchangeSys, "%v - Websocket reconnection request received", b.GetName())
log.Debugf(log.ExchangeSys, "%v - Websocket reconnection request received", b.Name)
}
go b.Websocket.Shutdown() // Connection monitor will reconnect
@@ -89,7 +89,7 @@ func (b *Bitstamp) WsHandleData() {
}
currencyPair := strings.Split(wsResponse.Channel, "_")
p := currency.NewPairFromString(strings.ToUpper(currencyPair[3]))
p := currency.NewPairFromString(strings.ToUpper(currencyPair[2]))
err = b.wsUpdateOrderbook(wsOrderBookTemp.Data, p, asset.Spot)
if err != nil {
@@ -113,7 +113,7 @@ func (b *Bitstamp) WsHandleData() {
Price: wsTradeTemp.Data.Price,
Amount: wsTradeTemp.Data.Amount,
CurrencyPair: p,
Exchange: b.GetName(),
Exchange: b.Name,
AssetType: asset.Spot,
}
}
@@ -122,7 +122,7 @@ func (b *Bitstamp) WsHandleData() {
}
func (b *Bitstamp) generateDefaultSubscriptions() {
var channels = []string{"live_trades_", "diff_order_book_"}
var channels = []string{"live_trades_", "order_book_"}
enabledCurrencies := b.GetEnabledPairs(asset.Spot)
var subscriptions []wshandler.WebsocketChannelSubscription
for i := range channels {
@@ -163,47 +163,45 @@ func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair,
}
var asks, bids []orderbook.Item
if len(update.Asks) > 0 {
for i := range update.Asks {
target, err := strconv.ParseFloat(update.Asks[i][0], 64)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
amount, err := strconv.ParseFloat(update.Asks[i][1], 64)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
asks = append(asks, orderbook.Item{Price: target, Amount: amount})
for i := range update.Asks {
target, err := strconv.ParseFloat(update.Asks[i][0], 64)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
amount, err := strconv.ParseFloat(update.Asks[i][1], 64)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
asks = append(asks, orderbook.Item{Price: target, Amount: amount})
}
if len(update.Bids) > 0 {
for i := range update.Bids {
target, err := strconv.ParseFloat(update.Bids[i][0], 64)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
amount, err := strconv.ParseFloat(update.Bids[i][1], 64)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
bids = append(bids, orderbook.Item{Price: target, Amount: amount})
for i := range update.Bids {
target, err := strconv.ParseFloat(update.Bids[i][0], 64)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
amount, err := strconv.ParseFloat(update.Bids[i][1], 64)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
bids = append(bids, orderbook.Item{Price: target, Amount: amount})
}
err := b.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{
err := b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{
Bids: bids,
Asks: asks,
CurrencyPair: p,
UpdateID: update.Timestamp,
Pair: p,
LastUpdated: time.Unix(update.Timestamp, 0),
AssetType: asset.Spot,
ExchangeName: b.Name,
})
if err != nil {
return err
@@ -212,7 +210,7 @@ func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair,
b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: p,
Asset: assetType,
Exchange: b.GetName(),
Exchange: b.Name,
}
return nil
@@ -247,7 +245,7 @@ func (b *Bitstamp) seedOrderBook() error {
newOrderBook.Bids = bids
newOrderBook.Pair = p[x]
newOrderBook.AssetType = asset.Spot
newOrderBook.ExchangeName = b.GetName()
newOrderBook.ExchangeName = b.Name
err = b.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
if err != nil {
@@ -257,7 +255,7 @@ func (b *Bitstamp) seedOrderBook() error {
b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: p[x],
Asset: asset.Spot,
Exchange: b.GetName(),
Exchange: b.Name,
}
}
return nil

View File

@@ -159,13 +159,6 @@ func (b *Bitstamp) Setup(exch *config.ExchangeConfig) error {
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
b.Websocket.Orderbook.Setup(
exch.WebsocketOrderbookBufferLimit,
true,
true,
true,
false,
exch.Name)
return nil
}

View File

@@ -329,7 +329,7 @@ func (b *Bittrex) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) {
if s.OrderType != order.Limit {
return submitOrderResponse,
errors.New("limit order not supported on exchange")
errors.New("limit orders only supported on exchange")
}
var response UUID

View File

@@ -97,11 +97,10 @@ func (c *CoinbasePro) WsHandleData() {
Open: ticker.Open24H,
High: ticker.High24H,
Low: ticker.Low24H,
Close: ticker.Price,
Last: ticker.Price,
Volume: ticker.Volume24H,
Bid: ticker.BestBid,
Ask: ticker.BestAsk,
Last: ticker.LastSize,
}
case "snapshot":
@@ -258,11 +257,11 @@ func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error {
return err
}
err = c.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: p,
UpdateTime: timestamp,
AssetType: asset.Spot,
Bids: bids,
Asks: asks,
Pair: p,
UpdateTime: timestamp,
Asset: asset.Spot,
})
if err != nil {
return err

View File

@@ -31,15 +31,22 @@ type Instruments struct {
// Ticker holds ticker information
type Ticker struct {
HighestBuy float64 `json:"highest_buy,string"`
InstrumentID int `json:"inst_id"`
Last float64 `json:"last,string"`
LowestSell float64 `json:"lowest_sell,string"`
OpenInterest float64 `json:"open_interest,string"`
Timestamp int64 `json:"timestamp"`
TransID int64 `json:"trans_id"`
Volume float64 `json:"volume,string"`
Volume24 float64 `json:"volume24,string"`
High24 float64 `json:"high24,string"`
HighestBuy float64 `json:"highest_buy,string"`
InstrumentID int `json:"inst_id"`
Last float64 `json:"last,string"`
Low24 float64 `json:"low24,string"`
LowestSell float64 `json:"lowest_sell,string"`
PrevTransID int64 `json:"prev_trans_id"`
PriceChange24 float64 `json:"price_change_24,string"`
Reply string `json:"reply"`
OpenInterest float64 `json:"open_interest,string"`
Timestamp int64 `json:"timestamp"`
TransID int64 `json:"trans_id"`
Volume float64 `json:"volume,string"`
Volume24 float64 `json:"volume24,string"`
Volume24Quote float64 `json:"volume24_quote,string"`
VolumeQuote float64 `json:"volume_quote,string"`
}
// OrderbookBase is a sub-type holding price and quantity

View File

@@ -136,13 +136,16 @@ func (c *COINUT) wsProcessResponse(resp []byte) {
c.Websocket.DataHandler <- err
return
}
currencyPair := wsInstrumentMap.LookupInstrument(ticker.InstID)
c.Websocket.DataHandler <- wshandler.TickerData{
Exchange: c.Name,
Volume: ticker.Volume,
QuoteVolume: ticker.VolumeQuote,
High: ticker.HighestBuy,
Low: ticker.LowestSell,
Volume: ticker.Volume24,
QuoteVolume: ticker.Volume24Quote,
Bid: ticker.HighestBuy,
Ask: ticker.LowestSell,
High: ticker.High24,
Low: ticker.Low24,
Last: ticker.Last,
Timestamp: time.Unix(0, ticker.Timestamp),
AssetType: asset.Spot,
@@ -302,9 +305,9 @@ func (c *COINUT) WsProcessOrderbookUpdate(update *WsOrderbookUpdate) error {
c.GetPairFormat(asset.Spot, true),
)
bufferUpdate := &wsorderbook.WebsocketOrderbookUpdate{
CurrencyPair: p,
UpdateID: update.TransID,
AssetType: asset.Spot,
Pair: p,
UpdateID: update.TransID,
Asset: asset.Spot,
}
if strings.EqualFold(update.Side, order.Buy.Lower()) {
bufferUpdate.Bids = []orderbook.Item{{Price: update.Price, Amount: update.Volume}}

View File

@@ -342,8 +342,10 @@ func (c *COINUT) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Pri
}
tickerPrice = ticker.Price{
Last: tick.Last,
High: tick.HighestBuy,
Low: tick.LowestSell,
High: tick.High24,
Low: tick.Low24,
Bid: tick.HighestBuy,
Ask: tick.LowestSell,
Volume: tick.Volume24,
Pair: p,
LastUpdated: time.Unix(0, tick.Timestamp),

View File

@@ -230,6 +230,16 @@ func (e *Base) GetAssetTypes() asset.Items {
return e.CurrencyPairs.AssetTypes
}
// GetPairAssetType returns the associated asset type for the currency pair
func (e *Base) GetPairAssetType(c currency.Pair) (asset.Item, error) {
for i := range e.GetAssetTypes() {
if e.GetEnabledPairs(e.GetAssetTypes()[i]).Contains(c, true) {
return e.GetAssetTypes()[i], nil
}
}
return "", errors.New("asset type not associated with currency pair")
}
// GetClientBankAccounts returns banking details associated with
// a client for withdrawal purposes
func (e *Base) GetClientBankAccounts(exchangeName, withdrawalCurrency string) (config.BankAccount, error) {

View File

@@ -1357,3 +1357,29 @@ func TestGetBase(t *testing.T) {
t.Error("name should be rawr")
}
}
func TestGetAssetType(t *testing.T) {
var b Base
p := currency.NewPair(currency.BTC, currency.USD)
_, err := b.GetPairAssetType(p)
if err == nil {
t.Fatal("error cannot be nil")
}
b.CurrencyPairs.AssetTypes = asset.Items{asset.Spot}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Enabled: currency.Pairs{
currency.NewPair(currency.BTC, currency.USD),
},
ConfigFormat: &currency.PairFormat{Delimiter: "-"},
}
a, err := b.GetPairAssetType(p)
if err != nil {
t.Fatal(err)
}
if a != asset.Spot {
t.Error("should be spot but is", a)
}
}

View File

@@ -246,11 +246,11 @@ func (g *Gateio) WsHandleData() {
} else {
err = g.Websocket.Orderbook.Update(
&wsorderbook.WebsocketOrderbookUpdate{
Asks: asks,
Bids: bids,
CurrencyPair: currency.NewPairFromString(c),
UpdateTime: time.Now(),
AssetType: asset.Spot,
Asks: asks,
Bids: bids,
Pair: currency.NewPairFromString(c),
UpdateTime: time.Now(),
Asset: asset.Spot,
})
if err != nil {
g.Websocket.DataHandler <- err

View File

@@ -319,11 +319,11 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa
}
}
err := g.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{
Asks: asks,
Bids: bids,
CurrencyPair: pair,
UpdateTime: time.Unix(0, result.TimestampMS),
AssetType: asset.Spot,
Asks: asks,
Bids: bids,
Pair: pair,
UpdateTime: time.Unix(0, result.TimestampMS),
Asset: asset.Spot,
})
if err != nil {
g.Websocket.DataHandler <- fmt.Errorf("%v %v", g.Name, err)

View File

@@ -284,11 +284,11 @@ func (h *HitBTC) WsProcessOrderbookUpdate(update WsOrderbook) error {
p := currency.NewPairFromFormattedPairs(update.Params.Symbol,
h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true))
err := h.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{
Asks: asks,
Bids: bids,
CurrencyPair: p,
UpdateID: update.Params.Sequence,
AssetType: asset.Spot,
Asks: asks,
Bids: bids,
Pair: p,
UpdateID: update.Params.Sequence,
Asset: asset.Spot,
})
if err != nil {
return err

View File

@@ -320,10 +320,10 @@ type WsDepth struct {
Channel string `json:"ch"`
Timestamp int64 `json:"ts"`
Tick struct {
Bids []interface{} `json:"bids"`
Asks []interface{} `json:"asks"`
Timestamp int64 `json:"ts"`
Version int64 `json:"version"`
Bids [][]interface{} `json:"bids"`
Asks [][]interface{} `json:"asks"`
Timestamp int64 `json:"ts"`
Version int64 `json:"version"`
} `json:"tick"`
}

View File

@@ -233,8 +233,14 @@ func (h *HUOBI) wsHandleMarketData(resp WsMessage) {
h.Websocket.DataHandler <- err
return
}
data := strings.Split(depth.Channel, ".")
h.WsProcessOrderbook(&depth, data[1])
err = h.WsProcessOrderbook(&depth, data[1])
if err != nil {
h.Websocket.DataHandler <- err
return
}
case strings.Contains(init.Channel, "kline"):
var kline WsKline
err := common.JSONDecode(resp.Raw, &kline)
@@ -297,32 +303,41 @@ func (h *HUOBI) wsHandleMarketData(resp WsMessage) {
// WsProcessOrderbook processes new orderbook data
func (h *HUOBI) WsProcessOrderbook(update *WsDepth, symbol string) error {
p := currency.NewPairFromFormattedPairs(symbol,
h.GetEnabledPairs(asset.Spot), h.GetPairFormat(asset.Spot, true))
h.GetEnabledPairs(asset.Spot),
h.GetPairFormat(asset.Spot, true))
var bids, asks []orderbook.Item
for i := 0; i < len(update.Tick.Bids); i++ {
bidLevel := update.Tick.Bids[i].([]interface{})
bids = append(bids, orderbook.Item{Price: bidLevel[0].(float64),
Amount: bidLevel[0].(float64)})
for i := range update.Tick.Bids {
bids = append(bids, orderbook.Item{
Price: update.Tick.Bids[i][0].(float64),
Amount: update.Tick.Bids[i][1].(float64),
})
}
for i := 0; i < len(update.Tick.Asks); i++ {
askLevel := update.Tick.Asks[i].([]interface{})
asks = append(asks, orderbook.Item{Price: askLevel[0].(float64),
Amount: askLevel[0].(float64)})
for i := range update.Tick.Asks {
asks = append(asks, orderbook.Item{
Price: update.Tick.Asks[i][0].(float64),
Amount: update.Tick.Asks[i][1].(float64),
})
}
var newOrderBook orderbook.Base
newOrderBook.Asks = asks
newOrderBook.Bids = bids
newOrderBook.Pair = p
newOrderBook.AssetType = asset.Spot
newOrderBook.ExchangeName = h.Name
err := h.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
if err != nil {
return err
}
h.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: p,
Exchange: h.GetName(),
Asset: asset.Spot,
}
return nil
}

View File

@@ -343,7 +343,7 @@ func (h *HUOBI) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderboo
var orderBook orderbook.Base
orderbookNew, err := h.GetDepth(OrderBookDataRequestParams{
Symbol: h.FormatExchangeCurrency(p, assetType).String(),
Type: OrderBookDataRequestParamsTypeStep1,
Type: OrderBookDataRequestParamsTypeStep0,
})
if err != nil {
return orderBook, err

View File

@@ -143,31 +143,31 @@ func (k *Kraken) WsHandleDataResponse(response WebsocketDataResponse) {
log.Debugf(log.ExchangeSys, "%v Websocket ticker data received",
k.Name)
}
k.wsProcessTickers(&channelData, response[1])
k.wsProcessTickers(&channelData, response[1].(map[string]interface{}))
case krakenWsOHLC:
if k.Verbose {
log.Debugf(log.ExchangeSys, "%v Websocket OHLC data received",
k.Name)
}
k.wsProcessCandles(&channelData, response[1])
k.wsProcessCandles(&channelData, response[1].([]interface{}))
case krakenWsOrderbook:
if k.Verbose {
log.Debugf(log.ExchangeSys, "%v Websocket Orderbook data received",
k.Name)
}
k.wsProcessOrderBook(&channelData, response[1])
k.wsProcessOrderBook(&channelData, response[1].(map[string]interface{}))
case krakenWsSpread:
if k.Verbose {
log.Debugf(log.ExchangeSys, "%v Websocket Spread data received",
k.Name)
}
k.wsProcessSpread(&channelData, response[1])
k.wsProcessSpread(&channelData, response[1].([]interface{}))
case krakenWsTrade:
if k.Verbose {
log.Debugf(log.ExchangeSys, "%v Websocket Trade data received",
k.Name)
}
k.wsProcessTrades(&channelData, response[1])
k.wsProcessTrades(&channelData, response[1].([]interface{}))
default:
log.Errorf(log.ExchangeSys, "%v Unidentified websocket data received: %v",
k.Name,
@@ -238,22 +238,48 @@ func getSubscriptionChannelData(id int64) WebsocketChannelData {
}
// wsProcessTickers converts ticker data and sends it to the datahandler
func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data interface{}) {
tickerData := data.(map[string]interface{})
askData := tickerData["a"].([]interface{})
bidData := tickerData["b"].([]interface{})
closeData := tickerData["c"].([]interface{})
openData := tickerData["o"].([]interface{})
lowData := tickerData["l"].([]interface{})
highData := tickerData["h"].([]interface{})
volumeData := tickerData["v"].([]interface{})
closePrice, _ := strconv.ParseFloat(closeData[0].(string), 64)
openPrice, _ := strconv.ParseFloat(openData[0].(string), 64)
highPrice, _ := strconv.ParseFloat(highData[0].(string), 64)
lowPrice, _ := strconv.ParseFloat(lowData[0].(string), 64)
quantity, _ := strconv.ParseFloat(volumeData[0].(string), 64)
ask, _ := strconv.ParseFloat(askData[0].(string), 64)
bid, _ := strconv.ParseFloat(bidData[0].(string), 64)
func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data map[string]interface{}) {
closePrice, err := strconv.ParseFloat(data["c"].([]interface{})[0].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
openPrice, err := strconv.ParseFloat(data["o"].([]interface{})[0].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
highPrice, err := strconv.ParseFloat(data["h"].([]interface{})[0].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
lowPrice, err := strconv.ParseFloat(data["l"].([]interface{})[0].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
quantity, err := strconv.ParseFloat(data["v"].([]interface{})[0].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
ask, err := strconv.ParseFloat(data["a"].([]interface{})[0].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
bid, err := strconv.ParseFloat(data["b"].([]interface{})[0].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
k.Websocket.DataHandler <- wshandler.TickerData{
Exchange: k.Name,
@@ -271,13 +297,17 @@ func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data interf
}
// wsProcessTickers converts ticker data and sends it to the datahandler
func (k *Kraken) wsProcessSpread(channelData *WebsocketChannelData, data interface{}) {
spreadData := data.([]interface{})
bestBid := spreadData[0].(string)
bestAsk := spreadData[1].(string)
timeData, _ := strconv.ParseFloat(spreadData[2].(string), 64)
bidVolume := spreadData[3].(string)
askVolume := spreadData[4].(string)
func (k *Kraken) wsProcessSpread(channelData *WebsocketChannelData, data []interface{}) {
bestBid := data[0].(string)
bestAsk := data[1].(string)
timeData, err := strconv.ParseFloat(data[2].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
bidVolume := data[3].(string)
askVolume := data[4].(string)
sec, dec := math.Modf(timeData)
spreadTimestamp := time.Unix(int64(sec), int64(dec*(1e9)))
if k.Verbose {
@@ -294,14 +324,28 @@ func (k *Kraken) wsProcessSpread(channelData *WebsocketChannelData, data interfa
}
// wsProcessTrades converts trade data and sends it to the datahandler
func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data interface{}) {
tradeData := data.([]interface{})
for i := range tradeData {
trade := tradeData[i].([]interface{})
timeData, _ := strconv.ParseInt(trade[2].(string), 10, 64)
timeUnix := time.Unix(timeData, 0)
price, _ := strconv.ParseFloat(trade[0].(string), 64)
amount, _ := strconv.ParseFloat(trade[1].(string), 64)
func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data []interface{}) {
for i := range data {
trade := data[i].([]interface{})
timeData, err := strconv.ParseFloat(trade[2].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
sec, dec := math.Modf(timeData)
timeUnix := time.Unix(int64(sec), int64(dec*(1e9)))
price, err := strconv.ParseFloat(trade[0].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
amount, err := strconv.ParseFloat(trade[1].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
k.Websocket.DataHandler <- wshandler.TradeData{
AssetType: asset.Spot,
@@ -318,17 +362,17 @@ func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data interfa
// wsProcessOrderBook determines if the orderbook data is partial or update
// Then sends to appropriate fun
func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data interface{}) {
obData := data.(map[string]interface{})
if _, ok := obData["as"]; ok {
k.wsProcessOrderBookPartial(channelData, obData)
func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[string]interface{}) {
if fullAsk, ok := data["as"].([]interface{}); ok {
fullBids := data["as"].([]interface{})
k.wsProcessOrderBookPartial(channelData, fullAsk, fullBids)
} else {
_, asksExist := obData["a"]
_, bidsExist := obData["b"]
askData, asksExist := data["a"].([]interface{})
bidData, bidsExist := data["b"].([]interface{})
if asksExist || bidsExist {
k.wsRequestMtx.Lock()
defer k.wsRequestMtx.Unlock()
err := k.wsProcessOrderBookUpdate(channelData, obData)
err := k.wsProcessOrderBookUpdate(channelData, askData, bidData)
if err != nil {
subscriptionToRemove := wshandler.WebsocketChannelSubscription{
Channel: krakenWsOrderbook,
@@ -341,40 +385,64 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data inte
}
// wsProcessOrderBookPartial creates a new orderbook entry for a given currency pair
func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, obData map[string]interface{}) {
func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, askData, bidData []interface{}) {
base := orderbook.Base{
Pair: channelData.Pair,
AssetType: asset.Spot,
}
// Kraken ob data is timestamped per price, GCT orderbook data is timestamped per entry
// Using the highest last update time, we can attempt to respect both within a reasonable degree
// Kraken ob data is timestamped per price, GCT orderbook data is
// timestamped per entry using the highest last update time, we can attempt
// to respect both within a reasonable degree
var highestLastUpdate time.Time
askData := obData["as"].([]interface{})
for i := range askData {
asks := askData[i].([]interface{})
price, _ := strconv.ParseFloat(asks[0].(string), 64)
amount, _ := strconv.ParseFloat(asks[1].(string), 64)
price, err := strconv.ParseFloat(asks[0].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
amount, err := strconv.ParseFloat(asks[1].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
base.Asks = append(base.Asks, orderbook.Item{
Amount: amount,
Price: price,
})
timeData, _ := strconv.ParseFloat(asks[2].(string), 64)
timeData, err := strconv.ParseFloat(asks[2].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
sec, dec := math.Modf(timeData)
askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9)))
if highestLastUpdate.Before(askUpdatedTime) {
highestLastUpdate = askUpdatedTime
}
}
bidData := obData["bs"].([]interface{})
for i := range bidData {
bids := bidData[i].([]interface{})
price, _ := strconv.ParseFloat(bids[0].(string), 64)
amount, _ := strconv.ParseFloat(bids[1].(string), 64)
price, err := strconv.ParseFloat(bids[0].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
amount, err := strconv.ParseFloat(bids[1].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
base.Bids = append(base.Bids, orderbook.Item{
Amount: amount,
Price: price,
})
timeData, _ := strconv.ParseFloat(bids[2].(string), 64)
timeData, err := strconv.ParseFloat(bids[2].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
sec, dec := math.Modf(timeData)
bidUpdateTime := time.Unix(int64(sec), int64(dec*(1e9)))
if highestLastUpdate.Before(bidUpdateTime) {
@@ -382,6 +450,7 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, ob
}
}
base.LastUpdated = highestLastUpdate
base.ExchangeName = k.Name
err := k.Websocket.Orderbook.LoadSnapshot(&base)
if err != nil {
k.Websocket.DataHandler <- err
@@ -395,48 +464,68 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, ob
}
// wsProcessOrderBookUpdate updates an orderbook entry for a given currency pair
func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, obData map[string]interface{}) error {
func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, askData, bidData []interface{}) error {
update := wsorderbook.WebsocketOrderbookUpdate{
AssetType: asset.Spot,
CurrencyPair: channelData.Pair,
Asset: asset.Spot,
Pair: channelData.Pair,
}
var highestLastUpdate time.Time
// Ask data is not always sent
if _, ok := obData["a"]; ok {
askData := obData["a"].([]interface{})
for i := range askData {
asks := askData[i].([]interface{})
price, _ := strconv.ParseFloat(asks[0].(string), 64)
amount, _ := strconv.ParseFloat(asks[1].(string), 64)
update.Asks = append(update.Asks, orderbook.Item{
Amount: amount,
Price: price,
})
timeData, _ := strconv.ParseFloat(asks[2].(string), 64)
sec, dec := math.Modf(timeData)
askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9)))
if highestLastUpdate.Before(askUpdatedTime) {
highestLastUpdate = askUpdatedTime
}
for i := range askData {
asks := askData[i].([]interface{})
price, err := strconv.ParseFloat(asks[0].(string), 64)
if err != nil {
return err
}
amount, err := strconv.ParseFloat(asks[1].(string), 64)
if err != nil {
return err
}
update.Asks = append(update.Asks, orderbook.Item{
Amount: amount,
Price: price,
})
timeData, err := strconv.ParseFloat(asks[2].(string), 64)
if err != nil {
return err
}
sec, dec := math.Modf(timeData)
askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9)))
if highestLastUpdate.Before(askUpdatedTime) {
highestLastUpdate = askUpdatedTime
}
}
// Bid data is not always sent
if _, ok := obData["b"]; ok {
bidData := obData["b"].([]interface{})
for i := range bidData {
bids := bidData[i].([]interface{})
price, _ := strconv.ParseFloat(bids[0].(string), 64)
amount, _ := strconv.ParseFloat(bids[1].(string), 64)
update.Bids = append(update.Bids, orderbook.Item{
Amount: amount,
Price: price,
})
timeData, _ := strconv.ParseFloat(bids[2].(string), 64)
sec, dec := math.Modf(timeData)
bidUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9)))
if highestLastUpdate.Before(bidUpdatedTime) {
highestLastUpdate = bidUpdatedTime
}
for i := range bidData {
bids := bidData[i].([]interface{})
price, err := strconv.ParseFloat(bids[0].(string), 64)
if err != nil {
return err
}
amount, err := strconv.ParseFloat(bids[1].(string), 64)
if err != nil {
return err
}
update.Bids = append(update.Bids, orderbook.Item{
Amount: amount,
Price: price,
})
timeData, err := strconv.ParseFloat(bids[2].(string), 64)
if err != nil {
return err
}
sec, dec := math.Modf(timeData)
bidUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9)))
if highestLastUpdate.Before(bidUpdatedTime) {
highestLastUpdate = bidUpdatedTime
}
}
update.UpdateTime = highestLastUpdate
@@ -454,17 +543,52 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, obD
}
// wsProcessCandles converts candle data and sends it to the data handler
func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data interface{}) {
candleData := data.([]interface{})
startTimeData, _ := strconv.ParseInt(candleData[0].(string), 10, 64)
startTimeUnix := time.Unix(startTimeData, 0)
endTimeData, _ := strconv.ParseInt(candleData[1].(string), 10, 64)
endTimeUnix := time.Unix(endTimeData, 0)
openPrice, _ := strconv.ParseFloat(candleData[2].(string), 64)
highPrice, _ := strconv.ParseFloat(candleData[3].(string), 64)
lowPrice, _ := strconv.ParseFloat(candleData[4].(string), 64)
closePrice, _ := strconv.ParseFloat(candleData[5].(string), 64)
volume, _ := strconv.ParseFloat(candleData[7].(string), 64)
func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data []interface{}) {
startTime, err := strconv.ParseFloat(data[0].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
sec, dec := math.Modf(startTime)
startTimeUnix := time.Unix(int64(sec), int64(dec*(1e9)))
endTime, err := strconv.ParseFloat(data[1].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
sec, dec = math.Modf(endTime)
endTimeUnix := time.Unix(int64(sec), int64(dec*(1e9)))
openPrice, err := strconv.ParseFloat(data[2].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
highPrice, err := strconv.ParseFloat(data[3].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
lowPrice, err := strconv.ParseFloat(data[4].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
closePrice, err := strconv.ParseFloat(data[5].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
volume, err := strconv.ParseFloat(data[7].(string), 64)
if err != nil {
k.Websocket.DataHandler <- err
return
}
k.Websocket.DataHandler <- wshandler.KlineData{
AssetType: asset.Spot,
@@ -501,11 +625,17 @@ func (k *Kraken) GenerateDefaultSubscriptions() {
// Subscribe sends a websocket message to receive data from the channel
func (k *Kraken) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
var depth int64
if channelToSubscribe.Channel == "book" {
depth = 1000
}
resp := WebsocketSubscriptionEventRequest{
Event: krakenWsSubscribe,
Pairs: []string{channelToSubscribe.Currency.String()},
Subscription: WebsocketSubscriptionData{
Name: channelToSubscribe.Channel,
Name: channelToSubscribe.Channel,
Depth: depth,
},
RequestID: k.WebsocketConn.GenerateMessageID(false),
}

View File

@@ -74,7 +74,7 @@ func (k *Kraken) SetDefaults() {
k.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: false,
Websocket: true,
RESTCapabilities: protocol.Features{
TickerBatching: true,
TickerFetching: true,

View File

@@ -45,7 +45,8 @@ func (l *LakeBTC) GetTicker() (map[string]Ticker, error) {
response := make(map[string]TickerResponse)
path := fmt.Sprintf("%s/%s", l.API.Endpoints.URL, lakeBTCTicker)
if err := l.SendHTTPRequest(path, &response); err != nil {
err := l.SendHTTPRequest(path, &response)
if err != nil {
return nil, err
}
@@ -55,22 +56,40 @@ func (l *LakeBTC) GetTicker() (map[string]Ticker, error) {
var tick Ticker
key := strings.ToUpper(k)
if v.Ask != nil {
tick.Ask, _ = strconv.ParseFloat(v.Ask.(string), 64)
tick.Ask, err = strconv.ParseFloat(v.Ask.(string), 64)
if err != nil {
return nil, err
}
}
if v.Bid != nil {
tick.Bid, _ = strconv.ParseFloat(v.Bid.(string), 64)
tick.Bid, err = strconv.ParseFloat(v.Bid.(string), 64)
if err != nil {
return nil, err
}
}
if v.High != nil {
tick.High, _ = strconv.ParseFloat(v.High.(string), 64)
tick.High, err = strconv.ParseFloat(v.High.(string), 64)
if err != nil {
return nil, err
}
}
if v.Last != nil {
tick.Last, _ = strconv.ParseFloat(v.Last.(string), 64)
tick.Last, err = strconv.ParseFloat(v.Last.(string), 64)
if err != nil {
return nil, err
}
}
if v.Low != nil {
tick.Low, _ = strconv.ParseFloat(v.Low.(string), 64)
tick.Low, err = strconv.ParseFloat(v.Low.(string), 64)
if err != nil {
return nil, err
}
}
if v.Volume != nil {
tick.Volume, _ = strconv.ParseFloat(v.Volume.(string), 64)
tick.Volume, err = strconv.ParseFloat(v.Volume.(string), 64)
if err != nil {
return nil, err
}
}
result[key] = tick
}

View File

@@ -74,9 +74,14 @@ func (l *LakeBTC) listenToEndpoints() error {
func (l *LakeBTC) GenerateDefaultSubscriptions() {
var subscriptions []wshandler.WebsocketChannelSubscription
enabledCurrencies := l.GetEnabledPairs(asset.Spot)
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = ""
channel := fmt.Sprintf("%v%v%v", marketSubstring, enabledCurrencies[j].Lower(), globalSubstring)
channel := fmt.Sprintf("%v%v%v",
marketSubstring,
enabledCurrencies[j].Lower(),
globalSubstring)
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: channel,
Currency: enabledCurrencies[j],
@@ -164,8 +169,11 @@ func (l *LakeBTC) processOrderbook(obUpdate, channel string) error {
if err != nil {
return err
}
p := l.getCurrencyFromChannel(channel)
book := orderbook.Base{
Pair: l.getCurrencyFromChannel(channel),
Pair: p,
LastUpdated: time.Now(),
AssetType: asset.Spot,
ExchangeName: l.Name,
@@ -188,6 +196,7 @@ func (l *LakeBTC) processOrderbook(obUpdate, channel string) error {
Price: price,
})
}
for i := 0; i < len(update.Bids); i++ {
var amount, price float64
amount, err = strconv.ParseFloat(update.Bids[i][1], 64)
@@ -205,7 +214,19 @@ func (l *LakeBTC) processOrderbook(obUpdate, channel string) error {
Price: price,
})
}
return l.Websocket.Orderbook.LoadSnapshot(&book)
err = l.Websocket.Orderbook.LoadSnapshot(&book)
if err != nil {
return err
}
l.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: p,
Asset: asset.Spot,
Exchange: l.Name,
}
return nil
}
func (l *LakeBTC) getCurrencyFromChannel(channel string) currency.Pair {
@@ -221,7 +242,14 @@ func (l *LakeBTC) processTicker(ticker string) error {
l.Websocket.DataHandler <- err
return err
}
enabled := l.GetEnabledPairs(asset.Spot)
for k, v := range tUpdate {
returnCurrency := currency.NewPairFromString(k)
if !enabled.Contains(returnCurrency, true) {
continue
}
tickerData := v.(map[string]interface{})
processTickerItem := func(tick map[string]interface{}, item string) float64 {
if tick[item] == nil {
@@ -230,7 +258,10 @@ func (l *LakeBTC) processTicker(ticker string) error {
p, err := strconv.ParseFloat(tick[item].(string), 64)
if err != nil {
l.Websocket.DataHandler <- fmt.Errorf("%s error parsing ticker data '%s' %v", l.Name, item, tickerData)
l.Websocket.DataHandler <- fmt.Errorf("%s error parsing ticker data '%s' %v",
l.Name,
item,
tickerData)
return 0
}
@@ -246,7 +277,7 @@ func (l *LakeBTC) processTicker(ticker string) error {
Ask: processTickerItem(tickerData, tickerSellString),
Volume: processTickerItem(tickerData, tickerVolumeString),
AssetType: asset.Spot,
Pair: currency.NewPairFromString(k),
Pair: returnCurrency,
}
}
return nil

View File

@@ -214,20 +214,21 @@ func (l *LakeBTC) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Pr
pairs := l.GetEnabledPairs(assetType)
for i := range pairs {
currency := l.FormatExchangeCurrency(pairs[i], assetType).String()
if _, ok := ticks[currency]; !ok {
c, ok := ticks[l.FormatExchangeCurrency(pairs[i], assetType).String()]
if !ok {
continue
}
var tickerPrice ticker.Price
tickerPrice.Pair = pairs[i]
tickerPrice.Ask = ticks[currency].Ask
tickerPrice.Bid = ticks[currency].Bid
tickerPrice.Volume = ticks[currency].Volume
tickerPrice.High = ticks[currency].High
tickerPrice.Low = ticks[currency].Low
tickerPrice.Last = ticks[currency].Last
tickerPrice.Ask = c.Ask
tickerPrice.Bid = c.Bid
tickerPrice.Volume = c.Volume
tickerPrice.High = c.High
tickerPrice.Low = c.Low
tickerPrice.Last = c.Last
err = ticker.ProcessTicker(l.GetName(), &tickerPrice, assetType)
err = ticker.ProcessTicker(l.Name, &tickerPrice, assetType)
if err != nil {
log.Error(log.Ticker, err)
}
@@ -237,7 +238,7 @@ func (l *LakeBTC) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Pr
// FetchTicker returns the ticker for a currency pair
func (l *LakeBTC) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) {
tickerNew, err := ticker.GetTicker(l.GetName(), p, assetType)
tickerNew, err := ticker.GetTicker(l.Name, p, assetType)
if err != nil {
return l.UpdateTicker(p, assetType)
}
@@ -246,7 +247,7 @@ func (l *LakeBTC) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Pri
// FetchOrderbook returns orderbook base on the currency pair
func (l *LakeBTC) FetchOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) {
ob, err := orderbook.Get(l.GetName(), p, assetType)
ob, err := orderbook.Get(l.Name, p, assetType)
if err != nil {
return l.UpdateOrderbook(p, assetType)
}
@@ -270,7 +271,7 @@ func (l *LakeBTC) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderb
}
orderBook.Pair = p
orderBook.ExchangeName = l.GetName()
orderBook.ExchangeName = l.Name
orderBook.AssetType = assetType
err = orderBook.Process()
@@ -285,7 +286,7 @@ func (l *LakeBTC) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderb
// LakeBTC exchange
func (l *LakeBTC) GetAccountInfo() (exchange.AccountInfo, error) {
var response exchange.AccountInfo
response.Exchange = l.GetName()
response.Exchange = l.Name
accountInfo, err := l.GetAccountInformation()
if err != nil {
return response, err

View File

@@ -488,18 +488,6 @@ func TestGetSpotTokenPairDetails(t *testing.T) {
}
}
// TestGetSpotOrderBook API endpoint test
func TestGetSpotOrderBook(t *testing.T) {
TestSetDefaults(t)
request := okgroup.GetSpotOrderBookRequest{
InstrumentID: spotCurrency,
}
_, err := o.GetSpotOrderBook(request)
if err != nil {
t.Error(err)
}
}
// TestGetSpotAllTokenPairsInformation API endpoint test
func TestGetSpotAllTokenPairsInformation(t *testing.T) {
TestSetDefaults(t)

View File

@@ -142,7 +142,11 @@ func (o *OKCoin) Start(wg *sync.WaitGroup) {
// Run implements the OKEX wrapper
func (o *OKCoin) Run() {
if o.Verbose {
log.Debugf(log.ExchangeSys, "%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket.IsEnabled()), o.WebsocketURL)
log.Debugf(log.ExchangeSys,
"%s Websocket: %s. (url: %s).\n",
o.Name,
common.IsEnabled(o.Websocket.IsEnabled()),
o.WebsocketURL)
}
forceUpdate := false
@@ -161,7 +165,9 @@ func (o *OKCoin) Run() {
err := o.UpdatePairs(enabledPairs, asset.Spot, true, true)
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to update currencies.\n", o.GetName())
log.Errorf(log.ExchangeSys,
"%s failed to update currencies.\n",
o.Name)
return
}
}
@@ -172,7 +178,10 @@ func (o *OKCoin) Run() {
err := o.UpdateTradablePairs(forceUpdate)
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", o.Name, err)
log.Errorf(log.ExchangeSys,
"%s failed to update tradable pairs. Err: %s",
o.Name,
err)
}
}
@@ -237,12 +246,12 @@ func (o *OKCoin) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Pri
}
}
}
return ticker.GetTicker(o.GetName(), p, assetType)
return ticker.GetTicker(o.Name, p, assetType)
}
// FetchTicker returns the ticker for a currency pair
func (o *OKCoin) FetchTicker(p currency.Pair, assetType asset.Item) (tickerData ticker.Price, err error) {
tickerData, err = ticker.GetTicker(o.GetName(), p, assetType)
tickerData, err = ticker.GetTicker(o.Name, p, assetType)
if err != nil {
return o.UpdateTicker(p, assetType)
}

View File

@@ -3,8 +3,6 @@ package okex
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/exchanges/okgroup"
@@ -18,7 +16,7 @@ const (
okExAPIVersion = "/v3/"
okExExchangeName = "OKEX"
// OkExWebsocketURL WebsocketURL
OkExWebsocketURL = "wss://real.okex.com:10442/ws/v3"
OkExWebsocketURL = "wss://real.okex.com:8443/ws/v3"
// API subsections
okGroupFuturesSubsection = "futures"
okGroupSwapSubsection = "swap"
@@ -141,69 +139,6 @@ func (o *OKEX) GetFuturesContractInformation() (resp []okgroup.GetFuturesContrac
return resp, o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, okgroup.OKGroupInstruments, nil, &resp, false)
}
// GetFuturesOrderBook List all contracts. This request does not support pagination. The full list will be returned for a request.
func (o *OKEX) GetFuturesOrderBook(request okgroup.GetFuturesOrderBookRequest) (resp okgroup.GetFuturesOrderBookResponse, err error) {
requestURL := fmt.Sprintf("%v/%v/%v%v", okgroup.OKGroupInstruments, request.InstrumentID, okgroup.OKGroupGetSpotOrderBook, okgroup.FormatParameters(request))
type tempOB struct {
Bids [][]string `json:"bids"`
Asks [][]string `json:"asks"`
Timestamp time.Time `json:"timestamp"`
}
var tmpOB tempOB
err = o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, requestURL, nil, &tmpOB, false)
if err != nil {
return resp, err
}
processOB := func(ob [][]string) ([]okgroup.FuturesOrderbookItem, error) {
var processedOB []okgroup.FuturesOrderbookItem
for x := range ob {
price, convErr := strconv.ParseFloat(ob[x][0], 64)
if err != nil {
return nil, convErr
}
size, convErr := strconv.ParseInt(ob[x][1], 10, 64)
if err != nil {
return nil, convErr
}
liqOrders, convErr := strconv.ParseInt(ob[x][2], 10, 64)
if err != nil {
return nil, convErr
}
numOrders, convErr := strconv.ParseInt(ob[x][3], 10, 64)
if err != nil {
return nil, convErr
}
processedOB = append(processedOB, okgroup.FuturesOrderbookItem{
Price: price,
Size: size,
ForceLiquidatedOrders: liqOrders,
NumberOrders: numOrders,
})
}
return processedOB, nil
}
resp.Bids, err = processOB(tmpOB.Bids)
if err != nil {
return
}
resp.Asks, err = processOB(tmpOB.Asks)
if err != nil {
return
}
resp.Timestamp = tmpOB.Timestamp
return resp, nil
}
// GetAllFuturesTokenInfo Get the last traded price, best bid/ask price, 24 hour trading volume and more info of all contracts.
func (o *OKEX) GetAllFuturesTokenInfo() (resp []okgroup.GetFuturesTokenInfoResponse, _ error) {
requestURL := fmt.Sprintf("%v/%v", okgroup.OKGroupInstruments, okgroup.OKGroupTicker)
@@ -373,12 +308,6 @@ func (o *OKEX) GetSwapContractInformation() (resp []okgroup.GetSwapContractInfor
return resp, o.SendHTTPRequest(http.MethodGet, okGroupSwapSubsection, okgroup.OKGroupInstruments, nil, &resp, false)
}
// GetSwapOrderBook Get the charts of the trading pairs.
func (o *OKEX) GetSwapOrderBook(request okgroup.GetSwapOrderBookRequest) (resp okgroup.GetSwapOrderBookResponse, _ error) {
requestURL := fmt.Sprintf("%v/%v/%v%v", okgroup.OKGroupInstruments, request.InstrumentID, okGroupDepth, okgroup.FormatParameters(request))
return resp, o.SendHTTPRequest(http.MethodGet, okGroupSwapSubsection, requestURL, nil, &resp, false)
}
// GetAllSwapTokensInformation Get the last traded price, best bid/ask price, 24 hour trading volume and more info of all contracts.
func (o *OKEX) GetAllSwapTokensInformation() (resp []okgroup.GetAllSwapTokensInformationResponse, _ error) {
requestURL := fmt.Sprintf("%v/%v", okgroup.OKGroupInstruments, okgroup.OKGroupTicker)

View File

@@ -522,19 +522,6 @@ func TestGetSpotTokenPairDetails(t *testing.T) {
}
}
// TestGetSpotOrderBook API endpoint test
func TestGetSpotOrderBook(t *testing.T) {
TestSetDefaults(t)
t.Parallel()
request := okgroup.GetSpotOrderBookRequest{
InstrumentID: spotCurrency,
}
_, err := o.GetSpotOrderBook(request)
if err != nil {
t.Error(err)
}
}
// TestGetSpotAllTokenPairsInformation API endpoint test
func TestGetSpotAllTokenPairsInformation(t *testing.T) {
TestSetDefaults(t)
@@ -1035,18 +1022,6 @@ func TestGetFuturesContractInformation(t *testing.T) {
}
}
// TestGetFuturesContractInformation API endpoint test
func TestGetFuturesOrderBook(t *testing.T) {
TestSetDefaults(t)
_, err := o.GetFuturesOrderBook(okgroup.GetFuturesOrderBookRequest{
InstrumentID: getFutureInstrumentID(),
Size: 10,
})
if err != nil {
t.Error(err)
}
}
// TestGetAllFuturesTokenInfo API endpoint test
func TestGetAllFuturesTokenInfo(t *testing.T) {
TestSetDefaults(t)
@@ -1315,19 +1290,6 @@ func TestGetSwapContractInformation(t *testing.T) {
}
}
// TestGetSwapOrderBook API endpoint test
func TestGetSwapOrderBook(t *testing.T) {
TestSetDefaults(t)
t.Parallel()
_, err := o.GetSwapOrderBook(okgroup.GetSwapOrderBookRequest{
InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD),
Size: 200,
})
if err != nil {
t.Error(err)
}
}
// TestGetAllSwapTokensInformation API endpoint test
func TestGetAllSwapTokensInformation(t *testing.T) {
TestSetDefaults(t)

View File

@@ -1,7 +1,9 @@
package okex
import (
"errors"
"fmt"
"strings"
"sync"
"time"
@@ -17,6 +19,11 @@ import (
log "github.com/thrasher-corp/gocryptotrader/logger"
)
const (
delimiterDash = "-"
delimiterUnderscore = "_"
)
// GetDefaultConfig returns a default exchange config
func (o *OKEX) GetDefaultConfig() (*config.ExchangeConfig, error) {
o.SetDefaults()
@@ -64,28 +71,38 @@ func (o *OKEX) SetDefaults() {
fmt1 := currency.PairStore{
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "-",
Delimiter: delimiterDash,
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "_",
Delimiter: delimiterUnderscore,
},
}
o.CurrencyPairs.Store(asset.PerpetualSwap, fmt1)
o.CurrencyPairs.Store(asset.Futures, fmt1)
fmt2 := currency.PairStore{
index := currency.PairStore{
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "-",
Delimiter: delimiterDash,
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "-",
},
}
o.CurrencyPairs.Store(asset.Spot, fmt2)
o.CurrencyPairs.Store(asset.Index, fmt2)
spot := currency.PairStore{
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: delimiterDash,
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: delimiterDash,
},
}
o.CurrencyPairs.Store(asset.Spot, spot)
o.CurrencyPairs.Store(asset.Index, index)
o.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
@@ -159,19 +176,25 @@ func (o *OKEX) Start(wg *sync.WaitGroup) {
// Run implements the OKEX wrapper
func (o *OKEX) Run() {
if o.Verbose {
log.Debugf(log.ExchangeSys, "%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket.IsEnabled()), o.API.Endpoints.WebsocketURL)
log.Debugf(log.ExchangeSys,
"%s Websocket: %s. (url: %s).\n",
o.Name,
common.IsEnabled(o.Websocket.IsEnabled()),
o.API.Endpoints.WebsocketURL)
}
if o.Config.CurrencyPairs.Pairs[asset.Spot].ConfigFormat == nil || o.Config.CurrencyPairs.Pairs[asset.Spot].RequestFormat == nil ||
o.Config.CurrencyPairs.Pairs[asset.Index].ConfigFormat == nil || o.Config.CurrencyPairs.Pairs[asset.Index].RequestFormat == nil {
if o.Config.CurrencyPairs.Pairs[asset.Spot].ConfigFormat == nil ||
o.Config.CurrencyPairs.Pairs[asset.Spot].RequestFormat == nil ||
o.Config.CurrencyPairs.Pairs[asset.Index].ConfigFormat == nil ||
o.Config.CurrencyPairs.Pairs[asset.Index].RequestFormat == nil {
currFmt := currency.PairStore{
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "-",
Delimiter: delimiterDash,
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "-",
Delimiter: delimiterDash,
},
}
o.CurrencyPairs.Store(asset.Spot, currFmt)
@@ -180,16 +203,18 @@ func (o *OKEX) Run() {
o.Config.CurrencyPairs.Store(asset.Index, currFmt)
}
if o.Config.CurrencyPairs.Pairs[asset.Futures].ConfigFormat == nil || o.Config.CurrencyPairs.Pairs[asset.Futures].RequestFormat == nil ||
o.Config.CurrencyPairs.Pairs[asset.PerpetualSwap].ConfigFormat == nil || o.Config.CurrencyPairs.Pairs[asset.PerpetualSwap].RequestFormat == nil {
if o.Config.CurrencyPairs.Pairs[asset.Futures].ConfigFormat == nil ||
o.Config.CurrencyPairs.Pairs[asset.Futures].RequestFormat == nil ||
o.Config.CurrencyPairs.Pairs[asset.PerpetualSwap].ConfigFormat == nil ||
o.Config.CurrencyPairs.Pairs[asset.PerpetualSwap].RequestFormat == nil {
currFmt := currency.PairStore{
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "-",
Delimiter: delimiterDash,
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "_",
Delimiter: delimiterUnderscore,
},
}
o.CurrencyPairs.Store(asset.Futures, currFmt)
@@ -198,14 +223,18 @@ func (o *OKEX) Run() {
o.Config.CurrencyPairs.Store(asset.PerpetualSwap, currFmt)
}
if !common.StringDataContains(o.Config.CurrencyPairs.Pairs[asset.Spot].Enabled.Strings(), o.CurrencyPairs.Pairs[asset.Spot].RequestFormat.Delimiter) {
if !common.StringDataContains(o.Config.CurrencyPairs.Pairs[asset.Spot].Enabled.Strings(),
o.CurrencyPairs.Pairs[asset.Spot].RequestFormat.Delimiter) {
enabledPairs := currency.NewPairsFromStrings([]string{"EOS-USDT"})
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.",
o.Name)
err := o.UpdatePairs(enabledPairs, asset.Spot, true, true)
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to update currencies.\n", o.GetName())
log.Errorf(log.ExchangeSys,
"%s failed to update currencies.\n",
o.Name)
return
}
}
@@ -216,7 +245,10 @@ func (o *OKEX) Run() {
err := o.UpdateTradablePairs(false)
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", o.Name, err)
log.Errorf(log.ExchangeSys,
"%s failed to update tradable pairs. Err: %s",
o.Name,
err)
}
}
@@ -231,7 +263,10 @@ func (o *OKEX) FetchTradablePairs(i asset.Item) ([]string, error) {
}
for x := range prods {
pairs = append(pairs, fmt.Sprintf("%v%v%v", prods[x].BaseCurrency, o.GetPairFormat(i, false).Delimiter, prods[x].QuoteCurrency))
pairs = append(pairs,
currency.NewPairWithDelimiter(prods[x].BaseCurrency,
prods[x].QuoteCurrency,
o.GetPairFormat(i, false).Delimiter).String())
}
return pairs, nil
case asset.Futures:
@@ -240,9 +275,10 @@ func (o *OKEX) FetchTradablePairs(i asset.Item) ([]string, error) {
return nil, err
}
var pairs []string
for x := range prods {
pairs = append(pairs, fmt.Sprintf("%v%v%v", prods[x].UnderlyingIndex+prods[x].QuoteCurrency, o.GetPairFormat(i, false).Delimiter, prods[x].Delivery))
p := strings.Split(prods[x].InstrumentID, delimiterDash)
pairs = append(pairs,
p[0]+delimiterDash+p[1]+o.GetPairFormat(i, false).Delimiter+p[2])
}
return pairs, nil
@@ -252,13 +288,18 @@ func (o *OKEX) FetchTradablePairs(i asset.Item) ([]string, error) {
return nil, err
}
var pairs []string
for x := range prods {
pairs = append(pairs, fmt.Sprintf("%v%v%v%vSWAP", prods[x].UnderlyingIndex, o.GetPairFormat(i, false).Delimiter, prods[x].QuoteCurrency, o.GetPairFormat(i, false).Delimiter))
pairs = append(pairs,
prods[x].UnderlyingIndex+
delimiterDash+
prods[x].QuoteCurrency+
o.GetPairFormat(i, false).Delimiter+
"SWAP")
}
return pairs, nil
case asset.Index:
return []string{fmt.Sprintf("BTC%vUSD", o.GetPairFormat(i, false).Delimiter)}, nil
// This is updated in futures index
return nil, errors.New("index updated in futures")
}
return nil, fmt.Errorf("%s invalid asset type", o.Name)
@@ -268,13 +309,33 @@ func (o *OKEX) FetchTradablePairs(i asset.Item) ([]string, error) {
// them in the exchanges config
func (o *OKEX) UpdateTradablePairs(forceUpdate bool) error {
for x := range o.CurrencyPairs.AssetTypes {
a := o.CurrencyPairs.AssetTypes[x]
pairs, err := o.FetchTradablePairs(a)
if o.CurrencyPairs.AssetTypes[x] == asset.Index {
// Update from futures
continue
}
pairs, err := o.FetchTradablePairs(o.CurrencyPairs.AssetTypes[x])
if err != nil {
return err
}
err = o.UpdatePairs(currency.NewPairsFromStrings(pairs), a, false, forceUpdate)
if o.CurrencyPairs.AssetTypes[x] == asset.Futures {
var indexPairs []string
for i := range pairs {
indexPairs = append(indexPairs,
strings.Split(pairs[i], delimiterUnderscore)[0])
}
err = o.UpdatePairs(currency.NewPairsFromStrings(indexPairs),
asset.Index,
false,
forceUpdate)
if err != nil {
return err
}
}
err = o.UpdatePairs(currency.NewPairsFromStrings(pairs),
o.CurrencyPairs.AssetTypes[x], false, forceUpdate)
if err != nil {
return err
}
@@ -291,92 +352,98 @@ func (o *OKEX) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price
if err != nil {
return tickerData, err
}
pairs := o.GetEnabledPairs(assetType)
for i := range pairs {
for j := range resp {
if !pairs[i].Equal(resp[j].InstrumentID) {
continue
}
tickerData = ticker.Price{
Last: resp[j].Last,
High: resp[j].High24h,
Low: resp[j].Low24h,
Bid: resp[j].BestBid,
Ask: resp[j].BestAsk,
Volume: resp[j].BaseVolume24h,
QuoteVolume: resp[j].QuoteVolume24h,
Open: resp[j].Open24h,
Pair: pairs[i],
LastUpdated: resp[j].Timestamp,
}
err = ticker.ProcessTicker(o.Name, &tickerData, assetType)
if err != nil {
log.Error(log.Ticker, err)
}
for j := range resp {
if !o.GetEnabledPairs(assetType).Contains(resp[j].InstrumentID, true) {
continue
}
tickerData = ticker.Price{
Last: resp[j].Last,
High: resp[j].High24h,
Low: resp[j].Low24h,
Bid: resp[j].BestBid,
Ask: resp[j].BestAsk,
Volume: resp[j].BaseVolume24h,
QuoteVolume: resp[j].QuoteVolume24h,
Open: resp[j].Open24h,
Pair: resp[j].InstrumentID,
LastUpdated: resp[j].Timestamp,
}
err = ticker.ProcessTicker(o.Name, &tickerData, assetType)
if err != nil {
log.Error(log.Ticker, err)
}
}
case asset.PerpetualSwap:
resp, err := o.GetAllSwapTokensInformation()
if err != nil {
return tickerData, err
}
pairs := o.GetEnabledPairs(assetType)
for i := range pairs {
for j := range resp {
if !pairs[i].Equal(resp[j].InstrumentID) {
continue
}
tickerData = ticker.Price{
Last: resp[j].Last,
High: resp[j].High24H,
Low: resp[j].Low24H,
Bid: resp[j].BestBid,
Ask: resp[j].BestAsk,
Volume: resp[j].Volume24H,
Pair: resp[j].InstrumentID,
LastUpdated: resp[j].Timestamp,
}
err = ticker.ProcessTicker(o.Name, &tickerData, assetType)
if err != nil {
log.Error(log.Ticker, err)
}
for j := range resp {
p := strings.Split(resp[j].InstrumentID, delimiterDash)
nC := currency.NewPairWithDelimiter(p[0]+delimiterDash+p[1],
p[2],
delimiterUnderscore)
if !o.GetEnabledPairs(assetType).Contains(nC, true) {
continue
}
tickerData = ticker.Price{
Last: resp[j].Last,
High: resp[j].High24H,
Low: resp[j].Low24H,
Bid: resp[j].BestBid,
Ask: resp[j].BestAsk,
Volume: resp[j].Volume24H,
Pair: nC,
LastUpdated: resp[j].Timestamp,
}
err = ticker.ProcessTicker(o.Name, &tickerData, assetType)
if err != nil {
log.Error(log.Ticker, err)
}
}
case asset.Futures:
resp, err := o.GetAllFuturesTokenInfo()
if err != nil {
return tickerData, err
}
pairs := o.GetEnabledPairs(assetType)
for i := range pairs {
for j := range resp {
if !pairs[i].Equal(resp[j].InstrumentID) {
continue
}
tickerData = ticker.Price{
Last: resp[j].Last,
High: resp[j].High24h,
Low: resp[j].Low24h,
Bid: resp[j].BestBid,
Ask: resp[j].BestAsk,
Volume: resp[j].Volume24h,
Pair: resp[j].InstrumentID,
LastUpdated: resp[j].Timestamp,
}
err = ticker.ProcessTicker(o.Name, &tickerData, assetType)
if err != nil {
log.Error(log.Ticker, err)
}
for j := range resp {
p := strings.Split(resp[j].InstrumentID, delimiterDash)
nC := currency.NewPairWithDelimiter(p[0]+delimiterDash+p[1],
p[2],
delimiterUnderscore)
if !o.GetEnabledPairs(assetType).Contains(nC, true) {
continue
}
tickerData = ticker.Price{
Last: resp[j].Last,
High: resp[j].High24h,
Low: resp[j].Low24h,
Bid: resp[j].BestBid,
Ask: resp[j].BestAsk,
Volume: resp[j].Volume24h,
Pair: nC,
LastUpdated: resp[j].Timestamp,
}
err = ticker.ProcessTicker(o.Name, &tickerData, assetType)
if err != nil {
log.Error(log.Ticker, err)
}
}
}
return ticker.GetTicker(o.GetName(), p, assetType)
return ticker.GetTicker(o.Name, p, assetType)
}
// FetchTicker returns the ticker for a currency pair
func (o *OKEX) FetchTicker(p currency.Pair, assetType asset.Item) (tickerData ticker.Price, err error) {
tickerData, err = ticker.GetTicker(o.GetName(), p, assetType)
if assetType == asset.Index {
return tickerData, errors.New("ticker fetching not supported for index")
}
tickerData, err = ticker.GetTicker(o.Name, p, assetType)
if err != nil {
return o.UpdateTicker(p, assetType)
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
log "github.com/thrasher-corp/gocryptotrader/logger"
)
@@ -307,11 +308,35 @@ func (o *OKGroup) GetSpotTokenPairDetails() (resp []GetSpotTokenPairDetailsRespo
return resp, o.SendHTTPRequest(http.MethodGet, okGroupTokenSubsection, OKGroupInstruments, nil, &resp, false)
}
// GetSpotOrderBook Getting the order book of a trading pair. Pagination is not supported here.
// The whole book will be returned for one request. Websocket is recommended here.
func (o *OKGroup) GetSpotOrderBook(request GetSpotOrderBookRequest) (resp GetSpotOrderBookResponse, _ error) {
requestURL := fmt.Sprintf("%v/%v/%v%v", OKGroupInstruments, request.InstrumentID, OKGroupGetSpotOrderBook, FormatParameters(request))
return resp, o.SendHTTPRequest(http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, false)
// GetOrderBook Getting the order book of a trading pair. Pagination is not
// supported here. The whole book will be returned for one request. Websocket is
// recommended here.
func (o *OKGroup) GetOrderBook(request GetOrderBookRequest, a asset.Item) (resp GetOrderBookResponse, _ error) {
var requestType, endpoint string
switch a {
case asset.Spot:
endpoint = OKGroupGetSpotOrderBook
requestType = okGroupTokenSubsection
case asset.Futures:
endpoint = OKGroupGetSpotOrderBook
requestType = "futures"
case asset.PerpetualSwap:
endpoint = "depth"
requestType = "swap"
default:
return resp, errors.New("unhandled asset type")
}
requestURL := fmt.Sprintf("%v/%v/%v/%v",
OKGroupInstruments,
request.InstrumentID,
endpoint,
FormatParameters(request))
return resp, o.SendHTTPRequest(http.MethodGet,
requestType,
requestURL,
nil,
&resp,
false)
}
// GetSpotAllTokenPairsInformation Get the last traded price, best bid/ask price, 24 hour trading volume and more info of all trading pairs.

View File

@@ -0,0 +1,78 @@
package okgroup
import (
"log"
"os"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
)
const (
apiKey = ""
apiSecret = ""
testAPIURL = "https://www.okex.com/api/"
testAPIVersion = "/v3/"
)
var o OKGroup
func TestMain(m *testing.M) {
cfg := config.GetConfig()
err := cfg.LoadConfig("../../testdata/configtest.json", true)
if err != nil {
log.Fatal("okgroup load config error", err)
}
okgroup, err := cfg.GetExchangeConfig("Okex")
if err != nil {
log.Fatal("okgroup Setup() init error", err)
}
okgroup.API.AuthenticatedSupport = true
okgroup.API.Credentials.Key = apiKey
okgroup.API.Credentials.Secret = apiSecret
o.API.Endpoints.URL = testAPIURL
o.APIVersion = testAPIVersion
o.Requester = request.New("okgroup_test_things",
request.NewRateLimit(time.Second, 10),
request.NewRateLimit(time.Second, 10),
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
)
o.Websocket = wshandler.New()
err = o.Setup(okgroup)
if err != nil {
log.Fatal("okgroup setup error", err)
}
os.Exit(m.Run())
}
func TestGetOrderbook(t *testing.T) {
t.Parallel()
_, err := o.GetOrderBook(GetOrderBookRequest{InstrumentID: "BTC-USDT"},
asset.Spot)
if err != nil {
t.Error(err)
}
// futures expire and break test, will need to mock this in the future
_, err = o.GetOrderBook(GetOrderBookRequest{InstrumentID: "Payload"},
asset.Futures)
if err == nil {
t.Error("error cannot be nil")
}
_, err = o.GetOrderBook(GetOrderBookRequest{InstrumentID: "BTC-USD-SWAP"},
asset.PerpetualSwap)
if err != nil {
t.Error(err)
}
}

View File

@@ -274,15 +274,15 @@ type GetSpotTokenPairDetailsResponse struct {
TickSize string `json:"tick_size"`
}
// GetSpotOrderBookRequest request data for GetSpotOrderBook
type GetSpotOrderBookRequest struct {
// GetOrderBookRequest request data for GetOrderBook
type GetOrderBookRequest struct {
Size int64 `url:"size,string,omitempty"` // [optional] number of results per request. Maximum 200
Depth float64 `url:"depth,string,omitempty"` // [optional] the aggregation of the book. e.g . 0.1,0.001
InstrumentID string `url:"-"` // [required] trading pairs
}
// GetSpotOrderBookResponse response data for GetSpotOrderBook
type GetSpotOrderBookResponse struct {
// GetOrderBookResponse response data
type GetOrderBookResponse struct {
Timestamp time.Time `json:"timestamp"`
Asks [][]string `json:"asks"` // [[0]: "Price", [1]: "Size", [2]: "Num_orders"], ...
Bids [][]string `json:"bids"` // [[0]: "Price", [1]: "Size", [2]: "Num_orders"], ...
@@ -678,37 +678,16 @@ type GetFuturesContractInformationResponse struct {
UnderlyingIndex string `json:"underlying_index"`
}
// GetFuturesOrderBookRequest request data for GetFuturesOrderBook
type GetFuturesOrderBookRequest struct {
InstrumentID string `url:"-"` // [required] Contract ID, e.g. "BTC-USD-180213"
Size int64 `url:"size,omitempty"` // [optional] The size of the price range (max: 200)
}
// FuturesOrderbookItem stores an individual futures orderbook item
type FuturesOrderbookItem struct {
Price float64
Size int64
ForceLiquidatedOrders int64 // Number of force liquidated orders
NumberOrders int64 // Number of orders on the price
}
// GetFuturesOrderBookResponse response data for GetFuturesOrderBook
type GetFuturesOrderBookResponse struct {
Asks []FuturesOrderbookItem
Bids []FuturesOrderbookItem
Timestamp time.Time
}
// GetFuturesTokenInfoResponse response data for GetFuturesOrderBook
type GetFuturesTokenInfoResponse struct {
BestAsk float64 `json:"best_ask,string"`
BestBid float64 `json:"best_bid,string"`
High24h float64 `json:"high_24h,string"`
InstrumentID currency.Pair `json:"instrument_id"`
Last float64 `json:"last,string"`
Low24h float64 `json:"low_24h,string"`
Timestamp time.Time `json:"timestamp"`
Volume24h float64 `json:"volume_24h,string"`
BestAsk float64 `json:"best_ask,string"`
BestBid float64 `json:"best_bid,string"`
High24h float64 `json:"high_24h,string"`
InstrumentID string `json:"instrument_id"`
Last float64 `json:"last,string"`
Low24h float64 `json:"low_24h,string"`
Timestamp time.Time `json:"timestamp"`
Volume24h float64 `json:"volume_24h,string"`
}
// GetFuturesFilledOrderRequest request data for GetFuturesFilledOrder
@@ -1059,14 +1038,14 @@ type GetSwapOrderBookResponse struct {
// GetAllSwapTokensInformationResponse response data for GetAllSwapTokensInformation
type GetAllSwapTokensInformationResponse struct {
InstrumentID currency.Pair `json:"instrument_id"`
Last float64 `json:"last,string"`
High24H float64 `json:"high_24h,string"`
Low24H float64 `json:"low_24h,string"`
BestBid float64 `json:"best_bid,string"`
BestAsk float64 `json:"best_ask,string"`
Volume24H float64 `json:"volume_24h,string"`
Timestamp time.Time `json:"timestamp"`
InstrumentID string `json:"instrument_id"`
Last float64 `json:"last,string"`
High24H float64 `json:"high_24h,string"`
Low24H float64 `json:"low_24h,string"`
BestBid float64 `json:"best_bid,string"`
BestAsk float64 `json:"best_ask,string"`
Volume24H float64 `json:"volume_24h,string"`
Timestamp time.Time `json:"timestamp"`
}
// GetSwapFilledOrdersDataRequest request data for GetSwapFilledOrdersData

View File

@@ -140,11 +140,36 @@ const (
okGroupWsFuturesOrder = okGroupWsFuturesSubsection + okGroupWsOrder
okGroupWsRateLimit = 30
allowableIterations = 25
delimiterColon = ":"
delimiterDash = "-"
delimiterUnderscore = "_"
)
// orderbookMutex Ensures if two entries arrive at once, only one can be processed at a time
// orderbookMutex Ensures if two entries arrive at once, only one can be
// processed at a time
var orderbookMutex sync.Mutex
var defaultSubscribedChannels = []string{okGroupWsSpotDepth, okGroupWsSpotCandle300s, okGroupWsSpotTicker, okGroupWsSpotTrade}
var defaultSpotSubscribedChannels = []string{okGroupWsSpotDepth,
okGroupWsSpotCandle300s,
okGroupWsSpotTicker,
okGroupWsSpotTrade}
var defaultFuturesSubscribedChannels = []string{okGroupWsFuturesDepth,
okGroupWsFuturesCandle300s,
okGroupWsFuturesTicker,
okGroupWsFuturesTrade}
var defaultIndexSubscribedChannels = []string{okGroupWsIndexCandle300s,
okGroupWsIndexTicker}
var defaultSwapSubscribedChannels = []string{okGroupWsSwapDepth,
okGroupWsSwapCandle300s,
okGroupWsSwapTicker,
okGroupWsSwapTrade,
okGroupWsSwapFundingRate,
okGroupWsSwapMarkPrice}
// WsConnect initiates a websocket connection
func (o *OKGroup) WsConnect() error {
@@ -161,13 +186,15 @@ func (o *OKGroup) WsConnect() error {
o.Websocket.GetWebsocketURL())
}
wg := sync.WaitGroup{}
wg.Add(2)
wg.Add(1)
go o.WsHandleData(&wg)
go o.wsPingHandler(&wg)
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
err = o.WsLogin()
if err != nil {
log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", o.Name, err)
log.Errorf(log.ExchangeSys,
"%v - authentication failed: %v\n",
o.Name,
err)
}
}
@@ -177,36 +204,6 @@ func (o *OKGroup) WsConnect() error {
return nil
}
// wsPingHandler sends a message "ping" every 27 to maintain the connection to the websocket
func (o *OKGroup) wsPingHandler(wg *sync.WaitGroup) {
o.Websocket.Wg.Add(1)
defer o.Websocket.Wg.Done()
ticker := time.NewTicker(time.Second * 27)
defer ticker.Stop()
wg.Done()
for {
select {
case <-o.Websocket.ShutdownC:
return
case <-ticker.C:
if !o.Websocket.IsConnected() {
continue
}
err := o.WebsocketConn.Connection.WriteMessage(websocket.TextMessage, []byte("ping"))
if o.Verbose {
log.Debugf(log.ExchangeSys, "%v sending ping", o.GetName())
}
if err != nil {
o.Websocket.DataHandler <- err
}
}
}
}
// WsHandleData handles the read data from the websocket connection
func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) {
o.Websocket.Wg.Add(1)
@@ -240,7 +237,11 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) {
err = common.JSONDecode(resp.Raw, &errorResponse)
if err == nil && errorResponse.ErrorCode > 0 {
if o.Verbose {
log.Debugf(log.ExchangeSys, "WS Error Event: %v Message: %v", errorResponse.Event, errorResponse.Message)
log.Debugf(log.ExchangeSys,
"WS Error Event: %v Message: %v for %s",
errorResponse.Event,
errorResponse.Message,
o.Name)
}
o.WsHandleErrorResponse(errorResponse)
continue
@@ -252,10 +253,12 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) {
o.Websocket.SetCanUseAuthenticatedEndpoints(eventResponse.Success)
}
if o.Verbose {
log.Debugf(log.ExchangeSys, "WS Event: %v on Channel: %v", eventResponse.Event, eventResponse.Channel)
log.Debugf(log.ExchangeSys,
"WS Event: %v on Channel: %v for %s",
eventResponse.Event,
eventResponse.Channel,
o.Name)
}
o.Websocket.DataHandler <- eventResponse
continue
}
}
}
@@ -273,7 +276,10 @@ func (o *OKGroup) WsLogin() error {
base64 := crypto.Base64Encode(hmac)
request := WebsocketEventRequest{
Operation: "login",
Arguments: []string{o.API.Credentials.Key, o.API.Credentials.ClientID, fmt.Sprintf("%v", unixTime), base64},
Arguments: []string{o.API.Credentials.Key,
o.API.Credentials.ClientID,
fmt.Sprintf("%v", unixTime),
base64},
}
err := o.WebsocketConn.SendMessage(request)
if err != nil {
@@ -286,7 +292,9 @@ func (o *OKGroup) WsLogin() error {
// WsHandleErrorResponse sends an error message to ws handler
func (o *OKGroup) WsHandleErrorResponse(event WebsocketErrorResponse) {
errorMessage := fmt.Sprintf("%v error - %v message: %s ",
o.GetName(), event.ErrorCode, event.Message)
o.Name,
event.ErrorCode,
event.Message)
if o.Verbose {
log.Error(log.ExchangeSys, errorMessage)
}
@@ -314,28 +322,54 @@ func (o *OKGroup) GetWsChannelWithoutOrderType(table string) string {
// eg "spot/ticker:BTCUSD" results in "SPOT"
func (o *OKGroup) GetAssetTypeFromTableName(table string) asset.Item {
assetIndex := strings.Index(table, "/")
return asset.Item(table[:assetIndex])
switch table[:assetIndex] {
case asset.Futures.String():
return asset.Futures
case asset.Spot.String():
return asset.Spot
case "swap":
return asset.PerpetualSwap
case asset.Index.String():
return asset.Index
default:
log.Warnf(log.ExchangeSys, "%s unhandled asset type %s",
o.Name,
table[:assetIndex])
return asset.Item(table[:assetIndex])
}
}
// WsHandleDataResponse classifies the WS response and sends to appropriate handler
func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) {
switch o.GetWsChannelWithoutOrderType(response.Table) {
case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s, okGroupWsCandle900s,
okGroupWsCandle1800s, okGroupWsCandle3600s, okGroupWsCandle7200s, okGroupWsCandle14400s,
okGroupWsCandle21600s, okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s:
case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s,
okGroupWsCandle900s, okGroupWsCandle1800s, okGroupWsCandle3600s,
okGroupWsCandle7200s, okGroupWsCandle14400s, okGroupWsCandle21600s,
okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s:
o.wsProcessCandles(response)
case okGroupWsDepth, okGroupWsDepth5:
// Locking, orderbooks cannot be processed out of order
orderbookMutex.Lock()
err := o.WsProcessOrderBook(response)
if err != nil {
pair := currency.NewPairDelimiter(response.Data[0].InstrumentID, "-")
channelToResubscribe := wshandler.WebsocketChannelSubscription{
Channel: response.Table,
Currency: pair,
for i := range response.Data {
a := o.GetAssetTypeFromTableName(response.Table)
var c currency.Pair
switch a {
case asset.Futures, asset.PerpetualSwap:
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterDash)
default:
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
}
channelToResubscribe := wshandler.WebsocketChannelSubscription{
Channel: response.Table,
Currency: c,
}
o.Websocket.ResubscribeToChannel(channelToResubscribe)
}
o.Websocket.ResubscribeToChannel(channelToResubscribe)
}
orderbookMutex.Unlock()
case okGroupWsTicker:
@@ -343,26 +377,37 @@ func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) {
case okGroupWsTrade:
o.wsProcessTrades(response)
default:
logDataResponse(response)
logDataResponse(response, o.Name)
}
}
// logDataResponse will log the details of any websocket data event
// where there is no websocket datahandler for it
func logDataResponse(response *WebsocketDataResponse) {
func logDataResponse(response *WebsocketDataResponse, exchangeName string) {
for i := range response.Data {
log.Errorf(log.ExchangeSys, "Unhandled channel: '%v'. Instrument '%v' Timestamp '%v', Data '%v",
log.Warnf(log.ExchangeSys,
"%s Unhandled channel: '%v'. Instrument '%v' Timestamp '%v'",
exchangeName,
response.Table,
response.Data[i].InstrumentID,
response.Data[i].Timestamp,
response.Data[i])
response.Data[i].Timestamp)
}
}
// wsProcessTickers converts ticker data and sends it to the datahandler
func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) {
for i := range response.Data {
instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-")
a := o.GetAssetTypeFromTableName(response.Table)
var c currency.Pair
switch a {
case asset.Futures, asset.PerpetualSwap:
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore)
default:
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
}
o.Websocket.DataHandler <- wshandler.TickerData{
Exchange: o.Name,
Open: response.Data[i].Open24h,
@@ -376,7 +421,7 @@ func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) {
Last: response.Data[i].Last,
Timestamp: response.Data[i].Timestamp,
AssetType: o.GetAssetTypeFromTableName(response.Table),
Pair: instrument,
Pair: c,
}
}
}
@@ -384,11 +429,21 @@ func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) {
// wsProcessTrades converts trade data and sends it to the datahandler
func (o *OKGroup) wsProcessTrades(response *WebsocketDataResponse) {
for i := range response.Data {
instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-")
a := o.GetAssetTypeFromTableName(response.Table)
var c currency.Pair
switch a {
case asset.Futures, asset.PerpetualSwap:
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore)
default:
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
}
o.Websocket.DataHandler <- wshandler.TradeData{
Amount: response.Data[i].Size,
AssetType: o.GetAssetTypeFromTableName(response.Table),
CurrencyPair: instrument,
CurrencyPair: c,
EventTime: time.Now().Unix(),
Exchange: o.GetName(),
Price: response.Data[i].WebsocketTradeResponse.Price,
@@ -401,10 +456,24 @@ func (o *OKGroup) wsProcessTrades(response *WebsocketDataResponse) {
// wsProcessCandles converts candle data and sends it to the data handler
func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) {
for i := range response.Data {
instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-")
timeData, err := time.Parse(time.RFC3339Nano, response.Data[i].WebsocketCandleResponse.Candle[0])
a := o.GetAssetTypeFromTableName(response.Table)
var c currency.Pair
switch a {
case asset.Futures, asset.PerpetualSwap:
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore)
default:
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
}
timeData, err := time.Parse(time.RFC3339Nano,
response.Data[i].WebsocketCandleResponse.Candle[0])
if err != nil {
log.Warnf(log.ExchangeSys, "%v Time data could not be parsed: %v", o.GetName(), response.Data[i].Candle[0])
log.Warnf(log.ExchangeSys,
"%v Time data could not be parsed: %v",
o.Name,
response.Data[i].Candle[0])
}
candleIndex := strings.LastIndex(response.Table, okGroupWsCandle)
@@ -416,16 +485,36 @@ func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) {
klineData := wshandler.KlineData{
AssetType: o.GetAssetTypeFromTableName(response.Table),
Pair: instrument,
Pair: c,
Exchange: o.GetName(),
Timestamp: timeData,
Interval: candleInterval,
}
klineData.OpenPrice, _ = strconv.ParseFloat(response.Data[i].Candle[1], 64)
klineData.HighPrice, _ = strconv.ParseFloat(response.Data[i].Candle[2], 64)
klineData.LowPrice, _ = strconv.ParseFloat(response.Data[i].Candle[3], 64)
klineData.ClosePrice, _ = strconv.ParseFloat(response.Data[i].Candle[4], 64)
klineData.Volume, _ = strconv.ParseFloat(response.Data[i].Candle[5], 64)
klineData.OpenPrice, err = strconv.ParseFloat(response.Data[i].Candle[1], 64)
if err != nil {
o.Websocket.DataHandler <- err
continue
}
klineData.HighPrice, err = strconv.ParseFloat(response.Data[i].Candle[2], 64)
if err != nil {
o.Websocket.DataHandler <- err
continue
}
klineData.LowPrice, err = strconv.ParseFloat(response.Data[i].Candle[3], 64)
if err != nil {
o.Websocket.DataHandler <- err
continue
}
klineData.ClosePrice, err = strconv.ParseFloat(response.Data[i].Candle[4], 64)
if err != nil {
o.Websocket.DataHandler <- err
continue
}
klineData.Volume, err = strconv.ParseFloat(response.Data[i].Candle[5], 64)
if err != nil {
o.Websocket.DataHandler <- err
continue
}
o.Websocket.DataHandler <- klineData
}
@@ -434,57 +523,96 @@ func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) {
// WsProcessOrderBook Validates the checksum and updates internal orderbook values
func (o *OKGroup) WsProcessOrderBook(response *WebsocketDataResponse) (err error) {
for i := range response.Data {
instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-")
a := o.GetAssetTypeFromTableName(response.Table)
var c currency.Pair
switch a {
case asset.Futures, asset.PerpetualSwap:
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore)
default:
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
}
if response.Action == okGroupWsOrderbookPartial {
err = o.WsProcessPartialOrderBook(&response.Data[i], instrument, response.Table)
err = o.WsProcessPartialOrderBook(&response.Data[i], c, a)
if err != nil {
return
}
} else if response.Action == okGroupWsOrderbookUpdate {
err = o.WsProcessUpdateOrderbook(&response.Data[i], instrument, response.Table)
if len(response.Data[i].Asks) == 0 && len(response.Data[i].Bids) == 0 {
continue
}
err = o.WsProcessUpdateOrderbook(&response.Data[i], c, a)
if err != nil {
return
}
}
}
return
}
// AppendWsOrderbookItems adds websocket orderbook data bid/asks into an orderbook item array
func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) (orderbookItems []orderbook.Item) {
func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) ([]orderbook.Item, error) {
var items []orderbook.Item
for j := range entries {
amount, _ := strconv.ParseFloat(entries[j][1].(string), 64)
price, _ := strconv.ParseFloat(entries[j][0].(string), 64)
orderbookItems = append(orderbookItems, orderbook.Item{
Amount: amount,
Price: price,
})
amount, err := strconv.ParseFloat(entries[j][1].(string), 64)
if err != nil {
return nil, err
}
price, err := strconv.ParseFloat(entries[j][0].(string), 64)
if err != nil {
return nil, err
}
items = append(items, orderbook.Item{Amount: amount, Price: price})
}
return
return items, nil
}
// WsProcessPartialOrderBook takes websocket orderbook data and creates an orderbook
// Calculates checksum to ensure it is valid
func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, tableName string) error {
func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, a asset.Item) error {
signedChecksum := o.CalculatePartialOrderbookChecksum(wsEventData)
if signedChecksum != wsEventData.Checksum {
return fmt.Errorf("channel: %v. Orderbook partial for %v checksum invalid", tableName, instrument)
return fmt.Errorf("%s channel: %s. Orderbook partial for %v checksum invalid",
o.Name,
a,
instrument)
}
if o.Verbose {
log.Debug(log.ExchangeSys, "Passed checksum!")
log.Debugf(log.ExchangeSys,
"%s passed checksum for instrument %s",
o.Name,
instrument)
}
asks := o.AppendWsOrderbookItems(wsEventData.Asks)
bids := o.AppendWsOrderbookItems(wsEventData.Bids)
asks, err := o.AppendWsOrderbookItems(wsEventData.Asks)
if err != nil {
return err
}
bids, err := o.AppendWsOrderbookItems(wsEventData.Bids)
if err != nil {
return err
}
newOrderBook := orderbook.Base{
Asks: asks,
Bids: bids,
AssetType: o.GetAssetTypeFromTableName(tableName),
AssetType: a,
LastUpdated: wsEventData.Timestamp,
Pair: instrument,
ExchangeName: o.GetName(),
}
err := o.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
err = o.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
if err != nil {
return err
}
o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Exchange: o.GetName(),
Asset: o.GetAssetTypeFromTableName(tableName),
Asset: a,
Pair: instrument,
}
return nil
@@ -492,123 +620,209 @@ func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketDataWrapper, i
// WsProcessUpdateOrderbook updates an existing orderbook using websocket data
// After merging WS data, it will sort, validate and finally update the existing orderbook
func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, tableName string) error {
func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, a asset.Item) error {
update := wsorderbook.WebsocketOrderbookUpdate{
AssetType: asset.Spot,
CurrencyPair: instrument,
UpdateTime: wsEventData.Timestamp,
Asset: a,
Pair: instrument,
UpdateTime: wsEventData.Timestamp,
}
update.Asks = o.AppendWsOrderbookItems(wsEventData.Asks)
update.Bids = o.AppendWsOrderbookItems(wsEventData.Bids)
err := o.Websocket.Orderbook.Update(&update)
if err != nil {
log.Error(log.ExchangeSys, err)
}
updatedOb := o.Websocket.Orderbook.GetOrderbook(instrument, asset.Spot)
checksum := o.CalculateUpdateOrderbookChecksum(updatedOb)
if checksum == wsEventData.Checksum {
if o.Verbose {
log.Debug(log.ExchangeSys, "Orderbook valid")
}
o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Exchange: o.GetName(),
Asset: o.GetAssetTypeFromTableName(tableName),
Pair: instrument,
}
} else {
if o.Verbose {
log.Warnln(log.ExchangeSys, "Orderbook invalid")
}
return fmt.Errorf("channel: %v. Orderbook update for %v checksum invalid. Received %v Calculated %v", tableName, instrument, wsEventData.Checksum, checksum)
var err error
update.Asks, err = o.AppendWsOrderbookItems(wsEventData.Asks)
if err != nil {
return err
}
update.Bids, err = o.AppendWsOrderbookItems(wsEventData.Bids)
if err != nil {
return err
}
err = o.Websocket.Orderbook.Update(&update)
if err != nil {
return err
}
updatedOb := o.Websocket.Orderbook.GetOrderbook(instrument, a)
checksum := o.CalculateUpdateOrderbookChecksum(updatedOb)
if checksum != wsEventData.Checksum {
// re-sub
log.Warnf(log.ExchangeSys, "%s checksum failure for item %s",
o.Name,
wsEventData.InstrumentID)
return errors.New("checksum failed")
}
o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Exchange: o.GetName(),
Asset: a,
Pair: instrument,
}
return nil
}
// CalculatePartialOrderbookChecksum alternates over the first 25 bid and ask entries from websocket data
// The checksum is made up of the price and the quantity with a semicolon (:) deliminating them
// This will also work when there are less than 25 entries (for whatever reason)
// CalculatePartialOrderbookChecksum alternates over the first 25 bid and ask
// entries from websocket data. The checksum is made up of the price and the
// quantity with a semicolon (:) deliminating them. This will also work when
// there are less than 25 entries (for whatever reason)
// eg Bid:Ask:Bid:Ask:Ask:Ask
func (o *OKGroup) CalculatePartialOrderbookChecksum(orderbookData *WebsocketDataWrapper) int32 {
var checksum string
iterations := 25
for i := 0; i < iterations; i++ {
bidsMessage := ""
askMessage := ""
for i := 0; i < allowableIterations; i++ {
if len(orderbookData.Bids)-1 >= i {
bidsMessage = fmt.Sprintf("%v:%v:", orderbookData.Bids[i][0], orderbookData.Bids[i][1])
checksum += orderbookData.Bids[i][0].(string) +
delimiterColon +
orderbookData.Bids[i][1].(string) +
delimiterColon
}
if len(orderbookData.Asks)-1 >= i {
askMessage = fmt.Sprintf("%v:%v:", orderbookData.Asks[i][0], orderbookData.Asks[i][1])
}
if checksum == "" {
checksum = fmt.Sprintf("%v%v", bidsMessage, askMessage)
} else {
checksum = fmt.Sprintf("%v%v%v", checksum, bidsMessage, askMessage)
checksum += orderbookData.Asks[i][0].(string) +
delimiterColon +
orderbookData.Asks[i][1].(string) +
delimiterColon
}
}
checksum = strings.TrimSuffix(checksum, ":")
checksum = strings.TrimSuffix(checksum, delimiterColon)
return int32(crc32.ChecksumIEEE([]byte(checksum)))
}
// CalculateUpdateOrderbookChecksum alternates over the first 25 bid and ask entries of a merged orderbook
// The checksum is made up of the price and the quantity with a semicolon (:) deliminating them
// This will also work when there are less than 25 entries (for whatever reason)
// CalculateUpdateOrderbookChecksum alternates over the first 25 bid and ask
// entries of a merged orderbook. The checksum is made up of the price and the
// quantity with a semicolon (:) deliminating them. This will also work when
// there are less than 25 entries (for whatever reason)
// eg Bid:Ask:Bid:Ask:Ask:Ask
func (o *OKGroup) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base) int32 {
var checksum string
iterations := 25
for i := 0; i < iterations; i++ {
bidsMessage := ""
askMessage := ""
for i := 0; i < allowableIterations; i++ {
if len(orderbookData.Bids)-1 >= i {
price := strconv.FormatFloat(orderbookData.Bids[i].Price, 'f', -1, 64)
amount := strconv.FormatFloat(orderbookData.Bids[i].Amount, 'f', -1, 64)
bidsMessage = fmt.Sprintf("%v:%v:", price, amount)
checksum += price + delimiterColon + amount + delimiterColon
}
if len(orderbookData.Asks)-1 >= i {
price := strconv.FormatFloat(orderbookData.Asks[i].Price, 'f', -1, 64)
amount := strconv.FormatFloat(orderbookData.Asks[i].Amount, 'f', -1, 64)
askMessage = fmt.Sprintf("%v:%v:", price, amount)
}
if checksum == "" {
checksum = fmt.Sprintf("%v%v", bidsMessage, askMessage)
} else {
checksum = fmt.Sprintf("%v%v%v", checksum, bidsMessage, askMessage)
checksum += price + delimiterColon + amount + delimiterColon
}
}
checksum = strings.TrimSuffix(checksum, ":")
checksum = strings.TrimSuffix(checksum, delimiterColon)
return int32(crc32.ChecksumIEEE([]byte(checksum)))
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be
// handled by ManageSubscriptions()
func (o *OKGroup) GenerateDefaultSubscriptions() {
enabledCurrencies := o.GetEnabledPairs(asset.Spot)
var subscriptions []wshandler.WebsocketChannelSubscription
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
defaultSubscribedChannels = append(defaultSubscribedChannels, okGroupWsSpotMarginAccount, okGroupWsSpotAccount, okGroupWsSpotOrder)
}
for i := range defaultSubscribedChannels {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = "-"
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: defaultSubscribedChannels[i],
Currency: enabledCurrencies[j],
})
assets := o.GetAssetTypes()
for x := range assets {
enabledCurrencies := o.GetEnabledPairs(assets[x])
if len(enabledCurrencies) == 0 {
continue
}
switch assets[x] {
case asset.Spot:
for i := range enabledCurrencies {
for y := range defaultSpotSubscribedChannels {
subscriptions = append(subscriptions,
wshandler.WebsocketChannelSubscription{
Channel: defaultSpotSubscribedChannels[y],
Currency: o.FormatExchangeCurrency(enabledCurrencies[i],
asset.Spot),
})
}
}
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
subscriptions = append(subscriptions,
wshandler.WebsocketChannelSubscription{
Channel: okGroupWsSpotMarginAccount,
},
wshandler.WebsocketChannelSubscription{
Channel: okGroupWsSpotAccount,
},
wshandler.WebsocketChannelSubscription{
Channel: okGroupWsSpotOrder,
})
}
case asset.Futures:
for i := range enabledCurrencies {
for y := range defaultFuturesSubscribedChannels {
subscriptions = append(subscriptions,
wshandler.WebsocketChannelSubscription{
Channel: defaultFuturesSubscribedChannels[y],
Currency: o.FormatExchangeCurrency(enabledCurrencies[i],
asset.Futures),
})
}
}
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
subscriptions = append(subscriptions,
wshandler.WebsocketChannelSubscription{
Channel: okGroupWsFuturesAccount,
},
wshandler.WebsocketChannelSubscription{
Channel: okGroupWsFuturesPosition,
},
wshandler.WebsocketChannelSubscription{
Channel: okGroupWsFuturesOrder,
})
}
case asset.PerpetualSwap:
for i := range enabledCurrencies {
for y := range defaultSwapSubscribedChannels {
subscriptions = append(subscriptions,
wshandler.WebsocketChannelSubscription{
Channel: defaultSwapSubscribedChannels[y],
Currency: o.FormatExchangeCurrency(enabledCurrencies[i],
asset.PerpetualSwap),
})
}
}
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
subscriptions = append(subscriptions,
wshandler.WebsocketChannelSubscription{
Channel: okGroupWsSwapAccount,
},
wshandler.WebsocketChannelSubscription{
Channel: okGroupWsSwapPosition,
},
wshandler.WebsocketChannelSubscription{
Channel: okGroupWsSwapOrder,
})
}
case asset.Index:
for i := range enabledCurrencies {
for y := range defaultIndexSubscribedChannels {
subscriptions = append(subscriptions,
wshandler.WebsocketChannelSubscription{
Channel: defaultIndexSubscribedChannels[y],
Currency: o.FormatExchangeCurrency(enabledCurrencies[i], asset.Index),
})
}
}
default:
o.Websocket.DataHandler <- errors.New("unhandled asset type")
}
}
o.Websocket.SubscribeToChannels(subscriptions)
}
// Subscribe sends a websocket message to receive data from the channel
func (o *OKGroup) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
c := channelToSubscribe.Currency.String()
request := WebsocketEventRequest{
Operation: "subscribe",
Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())},
Arguments: []string{channelToSubscribe.Channel + delimiterColon + c},
}
if strings.EqualFold(channelToSubscribe.Channel, okGroupWsSpotAccount) {
request.Arguments = []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.Base.String())}
request.Arguments = []string{channelToSubscribe.Channel +
delimiterColon +
channelToSubscribe.Currency.Base.String()}
}
return o.WebsocketConn.SendMessage(request)
@@ -618,7 +832,9 @@ func (o *OKGroup) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscri
func (o *OKGroup) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
request := WebsocketEventRequest{
Operation: "unsubscribe",
Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())},
Arguments: []string{channelToSubscribe.Channel +
delimiterColon +
channelToSubscribe.Currency.String()},
}
return o.WebsocketConn.SendMessage(request)
}

View File

@@ -1,6 +1,7 @@
package okgroup
import (
"errors"
"fmt"
"strconv"
"strings"
@@ -79,62 +80,93 @@ func (o *OKGroup) FetchOrderbook(p currency.Pair, assetType asset.Item) (resp or
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (o *OKGroup) UpdateOrderbook(p currency.Pair, assetType asset.Item) (resp orderbook.Base, err error) {
orderbookNew, err := o.GetSpotOrderBook(GetSpotOrderBookRequest{
InstrumentID: o.FormatExchangeCurrency(p, assetType).String(),
})
func (o *OKGroup) UpdateOrderbook(p currency.Pair, a asset.Item) (orderbook.Base, error) {
var resp orderbook.Base
if a == asset.Index {
return resp, errors.New("no orderbooks for index")
}
orderbookNew, err := o.GetOrderBook(GetOrderBookRequest{
InstrumentID: o.FormatExchangeCurrency(p, a).String(),
}, a)
if err != nil {
return
return resp, err
}
for x := range orderbookNew.Bids {
amount, convErr := strconv.ParseFloat(orderbookNew.Bids[x][1], 64)
if convErr != nil {
log.Errorf(log.ExchangeSys,
"Could not convert %v to float64",
orderbookNew.Bids[x][1])
return resp, err
}
price, convErr := strconv.ParseFloat(orderbookNew.Bids[x][0], 64)
if convErr != nil {
log.Errorf(log.ExchangeSys,
"Could not convert %v to float64",
orderbookNew.Bids[x][0])
return resp, err
}
var liquidationOrders, orderCount int64
// Contract specific variables
if len(orderbookNew.Bids[x]) == 4 {
liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Bids[x][2], 10, 64)
if convErr != nil {
return resp, err
}
orderCount, convErr = strconv.ParseInt(orderbookNew.Bids[x][3], 10, 64)
if convErr != nil {
return resp, err
}
}
resp.Bids = append(resp.Bids, orderbook.Item{
Amount: amount,
Price: price,
Amount: amount,
Price: price,
LiquidationOrders: liquidationOrders,
OrderCount: orderCount,
})
}
for x := range orderbookNew.Asks {
amount, convErr := strconv.ParseFloat(orderbookNew.Asks[x][1], 64)
if convErr != nil {
log.Errorf(log.ExchangeSys,
"Could not convert %v to float64",
orderbookNew.Asks[x][1])
return resp, err
}
price, convErr := strconv.ParseFloat(orderbookNew.Asks[x][0], 64)
if convErr != nil {
log.Errorf(log.ExchangeSys,
"Could not convert %v to float64",
orderbookNew.Asks[x][0])
return resp, err
}
var liquidationOrders, orderCount int64
// Contract specific variables
if len(orderbookNew.Asks[x]) == 4 {
liquidationOrders, convErr = strconv.ParseInt(orderbookNew.Asks[x][2], 10, 64)
if convErr != nil {
return resp, err
}
orderCount, convErr = strconv.ParseInt(orderbookNew.Asks[x][3], 10, 64)
if convErr != nil {
return resp, err
}
}
resp.Asks = append(resp.Asks, orderbook.Item{
Amount: amount,
Price: price,
Amount: amount,
Price: price,
LiquidationOrders: liquidationOrders,
OrderCount: orderCount,
})
}
resp.Pair = p
resp.AssetType = assetType
resp.AssetType = a
resp.ExchangeName = o.Name
err = resp.Process()
if err != nil {
return
return resp, err
}
return orderbook.Get(o.Name, p, assetType)
return orderbook.Get(o.Name, p, a)
}
// GetAccountInfo retrieves balances for all enabled currencies

View File

@@ -117,7 +117,17 @@ func (s *Service) SetNewData(b *Base) error {
return err
}
s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType] = &Book{b: b,
// Below instigates orderbook item separation so we can ensure, in the event
// of a simultaneous update via websocket/rest/fix, we don't affect package
// scoped orderbook data which could result in a potential panic
cpyBook := *b
cpyBook.Bids = make([]Item, len(b.Bids))
copy(cpyBook.Bids, b.Bids)
cpyBook.Asks = make([]Item, len(b.Asks))
copy(cpyBook.Asks, b.Asks)
s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType] = &Book{
b: &cpyBook,
Main: singleID,
Assoc: ids}
return nil

View File

@@ -50,6 +50,10 @@ type Item struct {
Amount float64
Price float64
ID int64
// Contract variables
LiquidationOrders int64
OrderCount int64
}
// Base holds the fields for the orderbook base

View File

@@ -256,7 +256,7 @@ func TestSubmitOrder(t *testing.T) {
var orderSubmission = &order.Submit{
Pair: currency.Pair{
Delimiter: "_",
Delimiter: delimiterUnderscore,
Base: currency.BTC,
Quote: currency.LTC,
},

View File

@@ -26,6 +26,7 @@ const (
wsTickerDataID = 1002
ws24HourExchangeVolumeID = 1003
wsHeartbeat = 1010
delimiterUnderscore = "_"
)
var (
@@ -107,10 +108,15 @@ func (p *Poloniex) WsHandleData() {
if len(data) == 2 && chanID != wsHeartbeat {
if checkSubscriptionSuccess(data) {
if p.Verbose {
log.Debugf(log.ExchangeSys, "poloniex websocket subscribed to channel successfully. %d", chanID)
log.Debugf(log.ExchangeSys,
"%s websocket subscribed to channel successfully. %d",
p.Name,
chanID)
}
} else {
p.Websocket.DataHandler <- fmt.Errorf("poloniex websocket subscription to channel failed. %d", chanID)
p.Websocket.DataHandler <- fmt.Errorf("%s websocket subscription to channel failed. %d",
p.Name,
chanID)
}
continue
}
@@ -135,17 +141,20 @@ func (p *Poloniex) WsHandleData() {
dataL3map := dataL3[1].(map[string]interface{})
currencyPair, ok := dataL3map["currencyPair"].(string)
if !ok {
p.Websocket.DataHandler <- errors.New("poloniex.go error - could not find currency pair in map")
p.Websocket.DataHandler <- fmt.Errorf("%s websocket could not find currency pair in map",
p.Name)
continue
}
orderbookData, ok := dataL3map["orderBook"].([]interface{})
if !ok {
p.Websocket.DataHandler <- errors.New("poloniex.go error - could not find orderbook data in map")
p.Websocket.DataHandler <- fmt.Errorf("%s websocket could not find orderbook data in map",
p.Name)
continue
}
err := p.WsProcessOrderbookSnapshot(orderbookData, currencyPair)
err = p.WsProcessOrderbookSnapshot(orderbookData,
currencyPair)
if err != nil {
p.Websocket.DataHandler <- err
continue
@@ -158,7 +167,9 @@ func (p *Poloniex) WsHandleData() {
}
case "o":
currencyPair := currencyIDMap[chanID]
err := p.WsProcessOrderbookUpdate(int64(data[1].(float64)), dataL3, currencyPair)
err = p.WsProcessOrderbookUpdate(int64(data[1].(float64)),
dataL3,
currencyPair)
if err != nil {
p.Websocket.DataHandler <- err
continue
@@ -180,8 +191,16 @@ func (p *Poloniex) WsHandleData() {
side = "sell"
}
trade.Side = side
trade.Volume, _ = strconv.ParseFloat(dataL3[3].(string), 64)
trade.Price, _ = strconv.ParseFloat(dataL3[4].(string), 64)
trade.Volume, err = strconv.ParseFloat(dataL3[3].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
continue
}
trade.Price, err = strconv.ParseFloat(dataL3[4].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
continue
}
trade.Timestamp = int64(dataL3[5].(float64))
p.Websocket.DataHandler <- wshandler.TradeData{
@@ -203,16 +222,60 @@ func (p *Poloniex) WsHandleData() {
func (p *Poloniex) wsHandleTickerData(data []interface{}) {
tickerData := data[2].([]interface{})
var t WsTicker
currencyPair := currencyIDMap[int(tickerData[0].(float64))]
t.LastPrice, _ = strconv.ParseFloat(tickerData[1].(string), 64)
t.LowestAsk, _ = strconv.ParseFloat(tickerData[2].(string), 64)
t.HighestBid, _ = strconv.ParseFloat(tickerData[3].(string), 64)
t.PercentageChange, _ = strconv.ParseFloat(tickerData[4].(string), 64)
t.BaseCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[5].(string), 64)
t.QuoteCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[6].(string), 64)
currencyPair := currency.NewPairDelimiter(currencyIDMap[int(tickerData[0].(float64))], delimiterUnderscore)
if !p.GetEnabledPairs(asset.Spot).Contains(currencyPair, true) {
return
}
var err error
t.LastPrice, err = strconv.ParseFloat(tickerData[1].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
t.LowestAsk, err = strconv.ParseFloat(tickerData[2].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
t.HighestBid, err = strconv.ParseFloat(tickerData[3].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
t.PercentageChange, err = strconv.ParseFloat(tickerData[4].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
t.BaseCurrencyVolume24H, err = strconv.ParseFloat(tickerData[5].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
t.QuoteCurrencyVolume24H, err = strconv.ParseFloat(tickerData[6].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
t.IsFrozen = tickerData[7].(float64) == 1
t.HighestTradeIn24H, _ = strconv.ParseFloat(tickerData[8].(string), 64)
t.LowestTradePrice24H, _ = strconv.ParseFloat(tickerData[9].(string), 64)
t.HighestTradeIn24H, err = strconv.ParseFloat(tickerData[8].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
t.LowestTradePrice24H, err = strconv.ParseFloat(tickerData[9].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
p.Websocket.DataHandler <- wshandler.TickerData{
Exchange: p.Name,
@@ -225,7 +288,7 @@ func (p *Poloniex) wsHandleTickerData(data []interface{}) {
Last: t.LastPrice,
Timestamp: time.Now(),
AssetType: asset.Spot,
Pair: currency.NewPairDelimiter(currencyPair, "_"),
Pair: currencyPair,
}
}
@@ -234,7 +297,12 @@ func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) {
for i := range accountData {
switch accountData[i][0].(string) {
case "b":
amount, _ := strconv.ParseFloat(accountData[i][3].(string), 64)
amount, err := strconv.ParseFloat(accountData[i][3].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
response := WsAccountBalanceUpdateResponse{
currencyID: accountData[i][1].(float64),
wallet: accountData[i][2].(string),
@@ -242,9 +310,23 @@ func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) {
}
p.Websocket.DataHandler <- response
case "n":
timeParse, _ := time.Parse("2006-01-02 15:04:05", accountData[i][6].(string))
rate, _ := strconv.ParseFloat(accountData[i][4].(string), 64)
amount, _ := strconv.ParseFloat(accountData[i][5].(string), 64)
timeParse, err := time.Parse("2006-01-02 15:04:05", accountData[i][6].(string))
if err != nil {
p.Websocket.DataHandler <- err
return
}
rate, err := strconv.ParseFloat(accountData[i][4].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
amount, err := strconv.ParseFloat(accountData[i][5].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
response := WsNewLimitOrderResponse{
currencyID: accountData[i][1].(float64),
@@ -262,11 +344,35 @@ func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) {
}
p.Websocket.DataHandler <- response
case "t":
timeParse, _ := time.Parse("2006-01-02 15:04:05", accountData[i][8].(string))
rate, _ := strconv.ParseFloat(accountData[i][2].(string), 64)
amount, _ := strconv.ParseFloat(accountData[i][3].(string), 64)
feeMultiplier, _ := strconv.ParseFloat(accountData[i][4].(string), 64)
totalFee, _ := strconv.ParseFloat(accountData[i][7].(string), 64)
timeParse, err := time.Parse("2006-01-02 15:04:05", accountData[i][8].(string))
if err != nil {
p.Websocket.DataHandler <- err
return
}
rate, err := strconv.ParseFloat(accountData[i][2].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
amount, err := strconv.ParseFloat(accountData[i][3].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
feeMultiplier, err := strconv.ParseFloat(accountData[i][4].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
totalFee, err := strconv.ParseFloat(accountData[i][7].(string), 64)
if err != nil {
p.Websocket.DataHandler <- err
return
}
response := WsTradeNotificationResponse{
TradeID: accountData[i][1].(float64),
@@ -336,7 +442,6 @@ 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 {
sideCheck := target[1].(float64)
cP := currency.NewPairFromString(symbol)
price, err := strconv.ParseFloat(target[2].(string), 64)
if err != nil {
@@ -347,11 +452,11 @@ func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber int64, target []inter
return err
}
update := &wsorderbook.WebsocketOrderbookUpdate{
CurrencyPair: cP,
AssetType: asset.Spot,
UpdateID: sequenceNumber,
Pair: cP,
Asset: asset.Spot,
UpdateID: sequenceNumber,
}
if sideCheck == 0 {
if target[1].(float64) == 1 {
update.Bids = []orderbook.Item{{Price: price, Amount: volume}}
} else {
update.Asks = []orderbook.Item{{Price: price, Amount: volume}}
@@ -374,7 +479,7 @@ func (p *Poloniex) GenerateDefaultSubscriptions() {
enabledCurrencies := p.GetEnabledPairs(asset.Spot)
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = "_"
enabledCurrencies[j].Delimiter = delimiterUnderscore
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: "orderbook",
Currency: enabledCurrencies[j],

View File

@@ -58,11 +58,11 @@ func (p *Poloniex) SetDefaults() {
},
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Delimiter: "_",
Delimiter: delimiterUnderscore,
Uppercase: true,
},
ConfigFormat: &currency.PairFormat{
Delimiter: "_",
Delimiter: delimiterUnderscore,
Uppercase: true,
},
}
@@ -163,7 +163,7 @@ func (p *Poloniex) Setup(exch *config.ExchangeConfig) error {
p.Websocket.Orderbook.Setup(
exch.WebsocketOrderbookBufferLimit,
true,
false,
true,
true,
false,

View File

@@ -277,8 +277,13 @@ func (r *Requester) DoRequest(req *http.Request, path string, body io.Reader, re
reader = resp.Body
default:
log.Warnf(log.ExchangeSys, "%s request response content type differs from JSON; received %v [path: %s]\n",
r.Name, resp.Header.Get("Content-Type"), path)
if verbose {
log.Warnf(log.ExchangeSys,
"%s request response content type differs from JSON; received %v [path: %s]\n",
r.Name,
resp.Header.Get("Content-Type"),
path)
}
reader = resp.Body
}
}

View File

@@ -25,27 +25,28 @@ func (w *WebsocketOrderbookLocal) Setup(obBufferLimit int, bufferEnabled, sortBu
// Volume == 0; deletion at price target
// Price target not found; append of price target
// Price target found; amend volume of price target
func (w *WebsocketOrderbookLocal) Update(orderbookUpdate *WebsocketOrderbookUpdate) error {
if (orderbookUpdate.Bids == nil && orderbookUpdate.Asks == nil) ||
(len(orderbookUpdate.Bids) == 0 && len(orderbookUpdate.Asks) == 0) {
return fmt.Errorf("%v cannot have bids and ask targets both nil", w.exchangeName)
func (w *WebsocketOrderbookLocal) Update(u *WebsocketOrderbookUpdate) error {
if (u.Bids == nil && u.Asks == nil) || (len(u.Bids) == 0 && len(u.Asks) == 0) {
return fmt.Errorf("%v cannot have bids and ask targets both nil",
w.exchangeName)
}
w.m.Lock()
defer w.m.Unlock()
obLookup, ok := w.ob[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType]
obLookup, ok := w.ob[u.Pair][u.Asset]
if !ok {
return fmt.Errorf("ob.Base could not be found for Exchange %s CurrencyPair: %s AssetType: %s",
w.exchangeName,
orderbookUpdate.CurrencyPair.String(),
orderbookUpdate.AssetType)
u.Pair,
u.Asset)
}
if w.bufferEnabled {
overBufferLimit := w.processBufferUpdate(obLookup, orderbookUpdate)
overBufferLimit := w.processBufferUpdate(obLookup, u)
if !overBufferLimit {
return nil
}
} else {
w.processObUpdate(obLookup, orderbookUpdate)
w.processObUpdate(obLookup, u)
}
err := obLookup.Process()
if err != nil {
@@ -53,23 +54,23 @@ func (w *WebsocketOrderbookLocal) Update(orderbookUpdate *WebsocketOrderbookUpda
}
if w.bufferEnabled {
// Reset the buffer
w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType] = nil
w.buffer[u.Pair][u.Asset] = nil
}
return nil
}
func (w *WebsocketOrderbookLocal) processBufferUpdate(o *orderbook.Base, orderbookUpdate *WebsocketOrderbookUpdate) bool {
func (w *WebsocketOrderbookLocal) processBufferUpdate(o *orderbook.Base, u *WebsocketOrderbookUpdate) bool {
if w.buffer == nil {
w.buffer = make(map[currency.Pair]map[asset.Item][]*WebsocketOrderbookUpdate)
}
if w.buffer[orderbookUpdate.CurrencyPair] == nil {
w.buffer[orderbookUpdate.CurrencyPair] = make(map[asset.Item][]*WebsocketOrderbookUpdate)
if w.buffer[u.Pair] == nil {
w.buffer[u.Pair] = make(map[asset.Item][]*WebsocketOrderbookUpdate)
}
bufferLookup := w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType]
bufferLookup := w.buffer[u.Pair][u.Asset]
if len(bufferLookup) <= w.obBufferLimit {
bufferLookup = append(bufferLookup, orderbookUpdate)
bufferLookup = append(bufferLookup, u)
if len(bufferLookup) < w.obBufferLimit {
w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType] = bufferLookup
w.buffer[u.Pair][u.Asset] = bufferLookup
return false
}
}
@@ -88,61 +89,59 @@ func (w *WebsocketOrderbookLocal) processBufferUpdate(o *orderbook.Base, orderbo
for i := 0; i < len(bufferLookup); i++ {
w.processObUpdate(o, bufferLookup[i])
}
w.buffer[orderbookUpdate.CurrencyPair][orderbookUpdate.AssetType] = bufferLookup
w.buffer[u.Pair][u.Asset] = bufferLookup
return true
}
func (w *WebsocketOrderbookLocal) processObUpdate(o *orderbook.Base, orderbookUpdate *WebsocketOrderbookUpdate) {
func (w *WebsocketOrderbookLocal) processObUpdate(o *orderbook.Base, u *WebsocketOrderbookUpdate) {
if w.updateEntriesByID {
w.updateByIDAndAction(o, orderbookUpdate)
w.updateByIDAndAction(o, u)
} else {
w.updateAsksByPrice(o, orderbookUpdate)
w.updateBidsByPrice(o, orderbookUpdate)
w.updateAsksByPrice(o, u)
w.updateBidsByPrice(o, u)
}
}
func (w *WebsocketOrderbookLocal) updateAsksByPrice(o *orderbook.Base, base *WebsocketOrderbookUpdate) {
for j := 0; j < len(base.Asks); j++ {
found := false
for k := 0; k < len(o.Asks); k++ {
if o.Asks[k].Price == base.Asks[j].Price {
found = true
if base.Asks[j].Amount == 0 {
o.Asks = append(o.Asks[:k],
o.Asks[k+1:]...)
break
func (w *WebsocketOrderbookLocal) updateAsksByPrice(o *orderbook.Base, u *WebsocketOrderbookUpdate) {
updates:
for j := range u.Asks {
for k := range o.Asks {
if o.Asks[k].Price == u.Asks[j].Price {
if u.Asks[j].Amount <= 0 {
o.Asks = append(o.Asks[:k], o.Asks[k+1:]...)
continue updates
}
o.Asks[k].Amount = base.Asks[j].Amount
break
o.Asks[k].Amount = u.Asks[j].Amount
continue updates
}
}
if !found {
o.Asks = append(o.Asks, base.Asks[j])
if u.Asks[j].Amount == 0 {
continue
}
o.Asks = append(o.Asks, u.Asks[j])
}
sort.Slice(o.Asks, func(i, j int) bool {
return o.Asks[i].Price < o.Asks[j].Price
})
}
func (w *WebsocketOrderbookLocal) updateBidsByPrice(o *orderbook.Base, base *WebsocketOrderbookUpdate) {
for j := 0; j < len(base.Bids); j++ {
found := false
for k := 0; k < len(o.Bids); k++ {
if o.Bids[k].Price == base.Bids[j].Price {
found = true
if base.Bids[j].Amount == 0 {
o.Bids = append(o.Bids[:k],
o.Bids[k+1:]...)
break
func (w *WebsocketOrderbookLocal) updateBidsByPrice(o *orderbook.Base, u *WebsocketOrderbookUpdate) {
updates:
for j := range u.Bids {
for k := range o.Bids {
if o.Bids[k].Price == u.Bids[j].Price {
if u.Bids[j].Amount <= 0 {
o.Bids = append(o.Bids[:k], o.Bids[k+1:]...)
continue updates
}
o.Bids[k].Amount = base.Bids[j].Amount
break
o.Bids[k].Amount = u.Bids[j].Amount
continue updates
}
}
if !found {
o.Bids = append(o.Bids, base.Bids[j])
if u.Bids[j].Amount == 0 {
continue
}
o.Bids = append(o.Bids, u.Bids[j])
}
sort.Slice(o.Bids, func(i, j int) bool {
return o.Bids[i].Price > o.Bids[j].Price
@@ -151,49 +150,52 @@ func (w *WebsocketOrderbookLocal) updateBidsByPrice(o *orderbook.Base, base *Web
// updateByIDAndAction will receive an action to execute against the orderbook
// it will then match by IDs instead of price to perform the action
func (w *WebsocketOrderbookLocal) updateByIDAndAction(o *orderbook.Base, orderbookUpdate *WebsocketOrderbookUpdate) {
switch orderbookUpdate.Action {
func (w *WebsocketOrderbookLocal) updateByIDAndAction(o *orderbook.Base, u *WebsocketOrderbookUpdate) {
switch u.Action {
case "update":
for _, target := range orderbookUpdate.Bids {
for i := range o.Bids {
if o.Bids[i].ID == target.ID {
o.Bids[i].Amount = target.Amount
for x := range u.Bids {
for y := range o.Bids {
if o.Bids[y].ID == u.Bids[x].ID {
o.Bids[y].Amount = u.Bids[x].Amount
break
}
}
}
for _, target := range orderbookUpdate.Asks {
for i := range o.Asks {
if o.Asks[i].ID == target.ID {
o.Asks[i].Amount = target.Amount
for x := range u.Asks {
for y := range o.Asks {
if o.Asks[y].ID == u.Asks[x].ID {
o.Asks[y].Amount = u.Asks[x].Amount
break
}
}
}
case "delete":
for _, target := range orderbookUpdate.Bids {
for i := 0; i < len(o.Bids); i++ {
if o.Bids[i].ID == target.ID {
o.Bids = append(o.Bids[:i],
o.Bids[i+1:]...)
i--
for x := range u.Bids {
for y := 0; y < len(o.Bids); y++ {
if o.Bids[y].ID == u.Bids[x].ID {
o.Bids = append(o.Bids[:y], o.Bids[y+1:]...)
break
}
}
}
for _, target := range orderbookUpdate.Asks {
for i := 0; i < len(o.Asks); i++ {
if o.Asks[i].ID == target.ID {
o.Asks = append(o.Asks[:i],
o.Asks[i+1:]...)
i--
for x := range u.Asks {
for y := 0; y < len(o.Asks); y++ {
if o.Asks[y].ID == u.Asks[x].ID {
o.Asks = append(o.Asks[:y], o.Asks[y+1:]...)
break
}
}
}
case "insert":
o.Bids = append(o.Bids, orderbookUpdate.Bids...)
o.Asks = append(o.Asks, orderbookUpdate.Asks...)
o.Bids = append(o.Bids, u.Bids...)
sort.Slice(o.Bids, func(i, j int) bool {
return o.Bids[i].Price > o.Bids[j].Price
})
o.Asks = append(o.Asks, u.Asks...)
sort.Slice(o.Asks, func(i, j int) bool {
return o.Asks[i].Price < o.Asks[j].Price
})
}
}
@@ -225,22 +227,17 @@ func (w *WebsocketOrderbookLocal) LoadSnapshot(newOrderbook *orderbook.Base) err
if w.ob[newOrderbook.Pair] == nil {
w.ob[newOrderbook.Pair] = make(map[asset.Item]*orderbook.Base)
}
fullObLookup := w.ob[newOrderbook.Pair][newOrderbook.AssetType]
if fullObLookup != nil &&
(len(fullObLookup.Asks) > 0 ||
len(fullObLookup.Bids) > 0) {
fullObLookup = newOrderbook
return newOrderbook.Process()
}
w.ob[newOrderbook.Pair][newOrderbook.AssetType] = newOrderbook
return newOrderbook.Process()
}
// GetOrderbook use sparingly. Modifying anything here will ruin hash calculation and cause problems
func (w *WebsocketOrderbookLocal) GetOrderbook(p currency.Pair, assetType asset.Item) *orderbook.Base {
// GetOrderbook use sparingly. Modifying anything here will ruin hash
// calculation and cause problems
func (w *WebsocketOrderbookLocal) GetOrderbook(p currency.Pair, a asset.Item) *orderbook.Base {
w.m.Lock()
defer w.m.Unlock()
return w.ob[p][assetType]
return w.ob[p][a]
}
// FlushCache flushes w.ob data to be garbage collected and refreshed when a

View File

@@ -20,14 +20,15 @@ var itemArray = [][]orderbook.Item{
{{Price: 5000, Amount: 1, ID: 5}},
}
var cp = currency.NewPairFromString("BTCUSD")
const (
exchangeName = "exchangeTest"
)
func createSnapshot() (obl *WebsocketOrderbookLocal, curr currency.Pair, asks, bids []orderbook.Item, err error) {
func createSnapshot() (obl *WebsocketOrderbookLocal, asks, bids []orderbook.Item, err error) {
var snapShot1 orderbook.Base
snapShot1.ExchangeName = exchangeName
curr = currency.NewPairFromString("BTCUSD")
asks = []orderbook.Item{
{Price: 4000, Amount: 1, ID: 6},
}
@@ -37,7 +38,7 @@ func createSnapshot() (obl *WebsocketOrderbookLocal, curr currency.Pair, asks, b
snapShot1.Asks = asks
snapShot1.Bids = bids
snapShot1.AssetType = asset.Spot
snapShot1.Pair = curr
snapShot1.Pair = cp
obl = &WebsocketOrderbookLocal{exchangeName: exchangeName}
err = obl.LoadSnapshot(&snapShot1)
return
@@ -52,7 +53,7 @@ func bidAskGenerator() []orderbook.Item {
price = 1
}
response = append(response, orderbook.Item{
Amount: float64(rand.Intn(1)),
Amount: float64(rand.Intn(10)),
Price: price,
ID: int64(i),
})
@@ -61,7 +62,7 @@ func bidAskGenerator() []orderbook.Item {
}
func BenchmarkUpdateBidsByPrice(b *testing.B) {
ob, curr, _, _, err := createSnapshot()
ob, _, _, err := createSnapshot()
if err != nil {
b.Error(err)
}
@@ -69,18 +70,18 @@ func BenchmarkUpdateBidsByPrice(b *testing.B) {
for i := 0; i < b.N; i++ {
bidAsks := bidAskGenerator()
update := &WebsocketOrderbookUpdate{
Bids: bidAsks,
Asks: bidAsks,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Bids: bidAsks,
Asks: bidAsks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
}
ob.updateBidsByPrice(ob.ob[curr][asset.Spot], update)
ob.updateBidsByPrice(ob.ob[cp][asset.Spot], update)
}
}
func BenchmarkUpdateAsksByPrice(b *testing.B) {
ob, curr, _, _, err := createSnapshot()
ob, _, _, err := createSnapshot()
if err != nil {
b.Error(err)
}
@@ -88,25 +89,24 @@ func BenchmarkUpdateAsksByPrice(b *testing.B) {
for i := 0; i < b.N; i++ {
bidAsks := bidAskGenerator()
update := &WebsocketOrderbookUpdate{
Bids: bidAsks,
Asks: bidAsks,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Bids: bidAsks,
Asks: bidAsks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
}
ob.updateAsksByPrice(ob.ob[curr][asset.Spot], update)
ob.updateAsksByPrice(ob.ob[cp][asset.Spot], update)
}
}
// BenchmarkBufferPerformance demonstrates buffer more performant than multi
// process calls
func BenchmarkBufferPerformance(b *testing.B) {
obl, curr, asks, bids, err := createSnapshot()
obl, asks, bids, err := createSnapshot()
if err != nil {
b.Fatal(err)
}
obl.bufferEnabled = true
cp := currency.NewPairFromString("BTCUSD")
// This is to ensure we do not send in zero orderbook info to our main book
// in orderbook.go, orderbooks should not be zero even after an update.
dummyItem := orderbook.Item{
@@ -116,11 +116,11 @@ func BenchmarkBufferPerformance(b *testing.B) {
}
obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem)
update := &WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Bids: bids,
Asks: asks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -136,13 +136,12 @@ func BenchmarkBufferPerformance(b *testing.B) {
// BenchmarkBufferSortingPerformance benchmark
func BenchmarkBufferSortingPerformance(b *testing.B) {
obl, curr, asks, bids, err := createSnapshot()
obl, asks, bids, err := createSnapshot()
if err != nil {
b.Fatal(err)
}
obl.bufferEnabled = true
obl.sortBuffer = true
cp := currency.NewPairFromString("BTCUSD")
// This is to ensure we do not send in zero orderbook info to our main book
// in orderbook.go, orderbooks should not be zero even after an update.
dummyItem := orderbook.Item{
@@ -152,11 +151,11 @@ func BenchmarkBufferSortingPerformance(b *testing.B) {
}
obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem)
update := &WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Bids: bids,
Asks: asks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -172,14 +171,13 @@ func BenchmarkBufferSortingPerformance(b *testing.B) {
// BenchmarkBufferSortingPerformance benchmark
func BenchmarkBufferSortingByIDPerformance(b *testing.B) {
obl, curr, asks, bids, err := createSnapshot()
obl, asks, bids, err := createSnapshot()
if err != nil {
b.Fatal(err)
}
obl.bufferEnabled = true
obl.sortBuffer = true
obl.sortBufferByUpdateIDs = true
cp := currency.NewPairFromString("BTCUSD")
// This is to ensure we do not send in zero orderbook info to our main book
// in orderbook.go, orderbooks should not be zero even after an update.
dummyItem := orderbook.Item{
@@ -189,11 +187,11 @@ func BenchmarkBufferSortingByIDPerformance(b *testing.B) {
}
obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem)
update := &WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Bids: bids,
Asks: asks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -210,11 +208,10 @@ func BenchmarkBufferSortingByIDPerformance(b *testing.B) {
// BenchmarkNoBufferPerformance demonstrates orderbook process less performant
// than buffer
func BenchmarkNoBufferPerformance(b *testing.B) {
obl, curr, asks, bids, err := createSnapshot()
obl, asks, bids, err := createSnapshot()
if err != nil {
b.Fatal(err)
}
cp := currency.NewPairFromString("BTCUSD")
// This is to ensure we do not send in zero orderbook info to our main book
// in orderbook.go, orderbooks should not be zero even after an update.
dummyItem := orderbook.Item{
@@ -224,11 +221,11 @@ func BenchmarkNoBufferPerformance(b *testing.B) {
}
obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem)
update := &WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Bids: bids,
Asks: asks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -243,41 +240,41 @@ func BenchmarkNoBufferPerformance(b *testing.B) {
}
func TestUpdates(t *testing.T) {
obl, curr, _, _, err := createSnapshot()
obl, _, _, err := createSnapshot()
if err != nil {
t.Error(err)
}
obl.updateAsksByPrice(obl.ob[curr][asset.Spot], &WebsocketOrderbookUpdate{
Bids: itemArray[5],
Asks: itemArray[5],
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &WebsocketOrderbookUpdate{
Bids: itemArray[5],
Asks: itemArray[5],
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
})
if err != nil {
t.Error(err)
}
obl.updateAsksByPrice(obl.ob[curr][asset.Spot], &WebsocketOrderbookUpdate{
Bids: itemArray[0],
Asks: itemArray[0],
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &WebsocketOrderbookUpdate{
Bids: itemArray[0],
Asks: itemArray[0],
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
})
if err != nil {
t.Error(err)
}
if len(obl.ob[curr][asset.Spot].Asks) != 3 {
if len(obl.ob[cp][asset.Spot].Asks) != 3 {
t.Error("Did not update")
}
}
// TestHittingTheBuffer logic test
func TestHittingTheBuffer(t *testing.T) {
obl, curr, _, _, err := createSnapshot()
obl, _, _, err := createSnapshot()
if err != nil {
t.Fatal(err)
}
@@ -287,30 +284,30 @@ func TestHittingTheBuffer(t *testing.T) {
asks := itemArray[i]
bids := itemArray[i]
err = obl.Update(&WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Bids: bids,
Asks: asks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
})
if err != nil {
t.Fatal(err)
}
}
if len(obl.ob[curr][asset.Spot].Asks) != 3 {
t.Log(obl.ob[curr][asset.Spot])
if len(obl.ob[cp][asset.Spot].Asks) != 3 {
t.Log(obl.ob[cp][asset.Spot])
t.Errorf("expected 3 entries, received: %v",
len(obl.ob[curr][asset.Spot].Asks))
len(obl.ob[cp][asset.Spot].Asks))
}
if len(obl.ob[curr][asset.Spot].Bids) != 3 {
if len(obl.ob[cp][asset.Spot].Bids) != 3 {
t.Errorf("expected 3 entries, received: %v",
len(obl.ob[curr][asset.Spot].Bids))
len(obl.ob[cp][asset.Spot].Bids))
}
}
// TestInsertWithIDs logic test
func TestInsertWithIDs(t *testing.T) {
obl, curr, _, _, err := createSnapshot()
obl, _, _, err := createSnapshot()
if err != nil {
t.Fatal(err)
}
@@ -321,30 +318,30 @@ func TestInsertWithIDs(t *testing.T) {
asks := itemArray[i]
bids := itemArray[i]
err = obl.Update(&WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Action: "insert",
Bids: bids,
Asks: asks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
Action: "insert",
})
if err != nil {
t.Fatal(err)
}
}
if len(obl.ob[curr][asset.Spot].Asks) != 6 {
if len(obl.ob[cp][asset.Spot].Asks) != 6 {
t.Errorf("expected 6 entries, received: %v",
len(obl.ob[curr][asset.Spot].Asks))
len(obl.ob[cp][asset.Spot].Asks))
}
if len(obl.ob[curr][asset.Spot].Bids) != 6 {
if len(obl.ob[cp][asset.Spot].Bids) != 6 {
t.Errorf("expected 6 entries, received: %v",
len(obl.ob[curr][asset.Spot].Bids))
len(obl.ob[cp][asset.Spot].Bids))
}
}
// TestSortIDs logic test
func TestSortIDs(t *testing.T) {
obl, curr, _, _, err := createSnapshot()
obl, _, _, err := createSnapshot()
if err != nil {
t.Fatal(err)
}
@@ -356,34 +353,33 @@ func TestSortIDs(t *testing.T) {
asks := itemArray[i]
bids := itemArray[i]
err = obl.Update(&WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: curr,
UpdateID: int64(i),
AssetType: asset.Spot,
Bids: bids,
Asks: asks,
Pair: cp,
UpdateID: int64(i),
Asset: asset.Spot,
})
if err != nil {
t.Fatal(err)
}
}
if len(obl.ob[curr][asset.Spot].Asks) != 3 {
if len(obl.ob[cp][asset.Spot].Asks) != 3 {
t.Errorf("expected 3 entries, received: %v",
len(obl.ob[curr][asset.Spot].Asks))
len(obl.ob[cp][asset.Spot].Asks))
}
if len(obl.ob[curr][asset.Spot].Bids) != 3 {
if len(obl.ob[cp][asset.Spot].Bids) != 3 {
t.Errorf("expected 3 entries, received: %v",
len(obl.ob[curr][asset.Spot].Bids))
len(obl.ob[cp][asset.Spot].Bids))
}
}
// TestDeleteWithIDs logic test
func TestDeleteWithIDs(t *testing.T) {
obl, curr, _, _, err := createSnapshot()
obl, _, _, err := createSnapshot()
if err != nil {
t.Fatal(err)
}
cp := currency.NewPairFromString("BTCUSD")
// This is to ensure we do not send in zero orderbook info to our main book
// in orderbook.go, orderbooks should not be zero even after an update.
dummyItem := orderbook.Item{
@@ -392,35 +388,41 @@ func TestDeleteWithIDs(t *testing.T) {
ID: 1337,
}
obl.ob[cp][asset.Spot].Bids = append(obl.ob[cp][asset.Spot].Bids, dummyItem)
obl.ob[cp][asset.Spot].Asks = append(obl.ob[cp][asset.Spot].Asks,
itemArray[2][0])
obl.ob[cp][asset.Spot].Asks = append(obl.ob[cp][asset.Spot].Asks,
itemArray[1][0])
obl.updateEntriesByID = true
for i := 0; i < len(itemArray); i++ {
asks := itemArray[i]
bids := itemArray[i]
err = obl.Update(&WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Action: "delete",
Bids: bids,
Asks: asks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
Action: "delete",
})
if err != nil {
t.Fatal(err)
}
}
if len(obl.ob[curr][asset.Spot].Asks) != 0 {
if len(obl.ob[cp][asset.Spot].Asks) != 0 {
t.Errorf("expected 0 entries, received: %v",
len(obl.ob[curr][asset.Spot].Asks))
len(obl.ob[cp][asset.Spot].Asks))
}
if len(obl.ob[curr][asset.Spot].Bids) != 1 {
if len(obl.ob[cp][asset.Spot].Bids) != 1 {
t.Errorf("expected 1 entries, received: %v",
len(obl.ob[curr][asset.Spot].Bids))
len(obl.ob[cp][asset.Spot].Bids))
}
}
// TestUpdateWithIDs logic test
func TestUpdateWithIDs(t *testing.T) {
obl, curr, _, _, err := createSnapshot()
obl, _, _, err := createSnapshot()
if err != nil {
t.Fatal(err)
}
@@ -429,35 +431,38 @@ func TestUpdateWithIDs(t *testing.T) {
asks := itemArray[i]
bids := itemArray[i]
err = obl.Update(&WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Action: "update",
Bids: bids,
Asks: asks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
Action: "update",
})
if err != nil {
t.Fatal(err)
}
}
if len(obl.ob[curr][asset.Spot].Asks) != 1 {
t.Log(obl.ob[curr][asset.Spot])
t.Errorf("expected 1 entries, received: %v", len(obl.ob[curr][asset.Spot].Asks))
if len(obl.ob[cp][asset.Spot].Asks) != 1 {
t.Log(obl.ob[cp][asset.Spot])
t.Errorf("expected 1 entries, received: %v",
len(obl.ob[cp][asset.Spot].Asks))
}
if len(obl.ob[curr][asset.Spot].Bids) != 1 {
t.Errorf("expected 1 entries, received: %v", len(obl.ob[curr][asset.Spot].Bids))
if len(obl.ob[cp][asset.Spot].Bids) != 1 {
t.Errorf("expected 1 entries, received: %v",
len(obl.ob[cp][asset.Spot].Bids))
}
}
// TestOutOfOrderIDs logic test
func TestOutOfOrderIDs(t *testing.T) {
obl, curr, _, _, err := createSnapshot()
obl, _, _, err := createSnapshot()
if err != nil {
t.Fatal(err)
}
outOFOrderIDs := []int64{2, 1, 5, 3, 4, 6, 7}
if itemArray[0][0].Price != 1000 {
t.Errorf("expected sorted price to be 3000, received: %v", itemArray[1][0].Price)
t.Errorf("expected sorted price to be 3000, received: %v",
itemArray[1][0].Price)
}
obl.bufferEnabled = true
obl.sortBuffer = true
@@ -465,18 +470,19 @@ func TestOutOfOrderIDs(t *testing.T) {
for i := 0; i < len(itemArray); i++ {
asks := itemArray[i]
err = obl.Update(&WebsocketOrderbookUpdate{
Asks: asks,
CurrencyPair: curr,
UpdateID: outOFOrderIDs[i],
AssetType: asset.Spot,
Asks: asks,
Pair: cp,
UpdateID: outOFOrderIDs[i],
Asset: asset.Spot,
})
if err != nil {
t.Fatal(err)
}
}
// Index 1 since index 0 is price 7000
if obl.ob[curr][asset.Spot].Asks[1].Price != 2000 {
t.Errorf("expected sorted price to be 3000, received: %v", obl.ob[curr][asset.Spot].Asks[1].Price)
if obl.ob[cp][asset.Spot].Asks[1].Price != 2000 {
t.Errorf("expected sorted price to be 3000, received: %v",
obl.ob[cp][asset.Spot].Asks[1].Price)
}
}
@@ -484,7 +490,6 @@ func TestOutOfOrderIDs(t *testing.T) {
func TestRunUpdateWithoutSnapshot(t *testing.T) {
var obl WebsocketOrderbookLocal
var snapShot1 orderbook.Base
curr := currency.NewPairFromString("BTCUSD")
asks := []orderbook.Item{
{Price: 4000, Amount: 1, ID: 8},
}
@@ -495,14 +500,14 @@ func TestRunUpdateWithoutSnapshot(t *testing.T) {
snapShot1.Asks = asks
snapShot1.Bids = bids
snapShot1.AssetType = asset.Spot
snapShot1.Pair = curr
snapShot1.Pair = cp
obl.exchangeName = exchangeName
err := obl.Update(&WebsocketOrderbookUpdate{
Bids: bids,
Asks: asks,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Bids: bids,
Asks: asks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
})
if err == nil {
t.Fatal("expected an error running update with no snapshot loaded")
@@ -516,18 +521,17 @@ func TestRunUpdateWithoutSnapshot(t *testing.T) {
func TestRunUpdateWithoutAnyUpdates(t *testing.T) {
var obl WebsocketOrderbookLocal
var snapShot1 orderbook.Base
curr := currency.NewPairFromString("BTCUSD")
snapShot1.Asks = []orderbook.Item{}
snapShot1.Bids = []orderbook.Item{}
snapShot1.AssetType = asset.Spot
snapShot1.Pair = curr
snapShot1.Pair = cp
obl.exchangeName = exchangeName
err := obl.Update(&WebsocketOrderbookUpdate{
Bids: snapShot1.Asks,
Asks: snapShot1.Bids,
CurrencyPair: curr,
UpdateTime: time.Now(),
AssetType: asset.Spot,
Bids: snapShot1.Asks,
Asks: snapShot1.Bids,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
})
if err == nil {
t.Fatal("expected an error running update with no snapshot loaded")
@@ -542,11 +546,10 @@ func TestRunUpdateWithoutAnyUpdates(t *testing.T) {
func TestRunSnapshotWithNoData(t *testing.T) {
var obl WebsocketOrderbookLocal
var snapShot1 orderbook.Base
curr := currency.NewPairFromString("BTCUSD")
snapShot1.Asks = []orderbook.Item{}
snapShot1.Bids = []orderbook.Item{}
snapShot1.AssetType = asset.Spot
snapShot1.Pair = curr
snapShot1.Pair = cp
snapShot1.ExchangeName = "test"
obl.exchangeName = "test"
err := obl.LoadSnapshot(&snapShot1)
@@ -563,7 +566,6 @@ func TestLoadSnapshot(t *testing.T) {
var obl WebsocketOrderbookLocal
var snapShot1 orderbook.Base
snapShot1.ExchangeName = "SnapshotWithOverride"
curr := currency.NewPairFromString("BTCUSD")
asks := []orderbook.Item{
{Price: 4000, Amount: 1, ID: 8},
}
@@ -573,7 +575,7 @@ func TestLoadSnapshot(t *testing.T) {
snapShot1.Asks = asks
snapShot1.Bids = bids
snapShot1.AssetType = asset.Spot
snapShot1.Pair = curr
snapShot1.Pair = cp
err := obl.LoadSnapshot(&snapShot1)
if err != nil {
t.Error(err)
@@ -582,15 +584,15 @@ func TestLoadSnapshot(t *testing.T) {
// TestFlushCache logic test
func TestFlushCache(t *testing.T) {
obl, curr, _, _, err := createSnapshot()
obl, _, _, err := createSnapshot()
if err != nil {
t.Fatal(err)
}
if obl.ob[curr][asset.Spot] == nil {
if obl.ob[cp][asset.Spot] == nil {
t.Error("expected ob to have ask entries")
}
obl.FlushCache()
if obl.ob[curr][asset.Spot] != nil {
if obl.ob[cp][asset.Spot] != nil {
t.Error("expected ob be flushed")
}
@@ -632,7 +634,7 @@ func TestInsertingSnapShots(t *testing.T) {
snapShot1.Asks = asks
snapShot1.Bids = bids
snapShot1.AssetType = asset.Spot
snapShot1.Pair = currency.NewPairFromString("BTCUSD")
snapShot1.Pair = cp
err := obl.LoadSnapshot(&snapShot1)
if err != nil {
t.Fatal(err)
@@ -714,23 +716,29 @@ func TestInsertingSnapShots(t *testing.T) {
t.Fatal(err)
}
if obl.ob[snapShot1.Pair][snapShot1.AssetType].Asks[0] != snapShot1.Asks[0] {
t.Errorf("loaded data mismatch. Expected %v, received %v", snapShot1.Asks[0], obl.ob[snapShot1.Pair][snapShot1.AssetType].Asks[0])
t.Errorf("loaded data mismatch. Expected %v, received %v",
snapShot1.Asks[0],
obl.ob[snapShot1.Pair][snapShot1.AssetType].Asks[0])
}
if obl.ob[snapShot2.Pair][snapShot2.AssetType].Asks[0] != snapShot2.Asks[0] {
t.Errorf("loaded data mismatch. Expected %v, received %v", snapShot2.Asks[0], obl.ob[snapShot2.Pair][snapShot2.AssetType].Asks[0])
t.Errorf("loaded data mismatch. Expected %v, received %v",
snapShot2.Asks[0],
obl.ob[snapShot2.Pair][snapShot2.AssetType].Asks[0])
}
if obl.ob[snapShot3.Pair][snapShot3.AssetType].Asks[0] != snapShot3.Asks[0] {
t.Errorf("loaded data mismatch. Expected %v, received %v", snapShot3.Asks[0], obl.ob[snapShot3.Pair][snapShot3.AssetType].Asks[0])
t.Errorf("loaded data mismatch. Expected %v, received %v",
snapShot3.Asks[0],
obl.ob[snapShot3.Pair][snapShot3.AssetType].Asks[0])
}
}
func TestGetOrderbook(t *testing.T) {
obl, curr, _, _, err := createSnapshot()
obl, _, _, err := createSnapshot()
if err != nil {
t.Fatal(err)
}
ob := obl.GetOrderbook(curr, asset.Spot)
if obl.ob[curr][asset.Spot] != ob {
ob := obl.GetOrderbook(cp, asset.Spot)
if obl.ob[cp][asset.Spot] != ob {
t.Error("Failed to get orderbook")
}
}
@@ -738,7 +746,35 @@ func TestGetOrderbook(t *testing.T) {
func TestSetup(t *testing.T) {
w := WebsocketOrderbookLocal{}
w.Setup(1, true, true, true, true, "hi")
if w.obBufferLimit != 1 || !w.bufferEnabled || !w.sortBuffer || !w.sortBufferByUpdateIDs || !w.updateEntriesByID || w.exchangeName != "hi" {
if w.obBufferLimit != 1 ||
!w.bufferEnabled ||
!w.sortBuffer ||
!w.sortBufferByUpdateIDs ||
!w.updateEntriesByID ||
w.exchangeName != "hi" {
t.Errorf("Setup incorrectly loaded %s", w.exchangeName)
}
}
func TestEnsureMultipleUpdatesViaPrice(t *testing.T) {
obl, _, _, err := createSnapshot()
if err != nil {
t.Error(err)
}
asks := bidAskGenerator()
obl.updateAsksByPrice(obl.ob[cp][asset.Spot], &WebsocketOrderbookUpdate{
Bids: asks,
Asks: asks,
Pair: cp,
UpdateTime: time.Now(),
Asset: asset.Spot,
})
if err != nil {
t.Error(err)
}
if len(obl.ob[cp][asset.Spot].Asks) <= 3 {
t.Errorf("Insufficient updates")
}
}

View File

@@ -25,11 +25,11 @@ type WebsocketOrderbookLocal struct {
// WebsocketOrderbookUpdate stores orderbook updates and dictates what features to use when processing
type WebsocketOrderbookUpdate struct {
UpdateID int64 // Used when no time is provided
UpdateTime time.Time
AssetType asset.Item
Action string // Used in conjunction with UpdateEntriesByID
Bids []orderbook.Item
Asks []orderbook.Item
CurrencyPair currency.Pair
UpdateID int64 // Used when no time is provided
UpdateTime time.Time
Asset asset.Item
Action string // Used in conjunction with UpdateEntriesByID
Bids []orderbook.Item
Asks []orderbook.Item
Pair currency.Pair
}

View File

@@ -287,8 +287,8 @@ func TestGetActiveOrders(t *testing.T) {
var getOrdersRequest = order.GetOrdersRequest{
OrderType: order.AnyType,
Currencies: []currency.Pair{currency.NewPair(currency.LTC,
currency.BTC)},
Currencies: []currency.Pair{currency.NewPair(currency.XRP,
currency.USDT)},
}
_, err := z.GetActiveOrders(&getOrdersRequest)
@@ -327,6 +327,7 @@ func areTestAPIKeysSet() bool {
func TestSubmitOrder(t *testing.T) {
z.SetDefaults()
TestSetup(t)
if areTestAPIKeysSet() && !canManipulateRealOrders {
t.Skip(fmt.Sprintf("ApiKey: %s. Can place orders: %v",
z.API.Credentials.Key,
@@ -336,8 +337,8 @@ func TestSubmitOrder(t *testing.T) {
var orderSubmission = &order.Submit{
Pair: currency.Pair{
Delimiter: "_",
Base: currency.QTUM,
Quote: currency.USD,
Base: currency.XRP,
Quote: currency.USDT,
},
OrderSide: order.Buy,
OrderType: order.Limit,
@@ -361,7 +362,7 @@ func TestCancelExchangeOrder(t *testing.T) {
t.Skip("API keys set, canManipulateRealOrders false, skipping test")
}
currencyPair := currency.NewPair(currency.LTC, currency.BTC)
currencyPair := currency.NewPair(currency.XRP, currency.USDT)
var orderCancellation = &order.Cancel{
OrderID: "1",
@@ -387,7 +388,7 @@ func TestCancelAllExchangeOrders(t *testing.T) {
t.Skip("API keys set, canManipulateRealOrders false, skipping test")
}
currencyPair := currency.NewPair(currency.LTC, currency.BTC)
currencyPair := currency.NewPair(currency.XRP, currency.USDT)
var orderCancellation = &order.Cancel{
OrderID: "1",

View File

@@ -104,7 +104,7 @@ func (z *ZB) WsHandleData() {
Last: ticker.Data.Last,
Bid: ticker.Data.Buy,
Ask: ticker.Data.Sell,
Timestamp: time.Unix(0, ticker.Date),
Timestamp: time.Unix(0, ticker.Date*int64(time.Millisecond)),
AssetType: asset.Spot,
Pair: currency.NewPairFromString(cPair[0]),
}
@@ -119,19 +119,17 @@ func (z *ZB) WsHandleData() {
var asks []orderbook.Item
for i := range depth.Asks {
ask := depth.Asks[i].([]interface{})
asks = append(asks, orderbook.Item{
Amount: ask[1].(float64),
Price: ask[0].(float64),
Amount: depth.Asks[i][1].(float64),
Price: depth.Asks[i][0].(float64),
})
}
var bids []orderbook.Item
for i := range depth.Bids {
bid := depth.Bids[i].([]interface{})
bids = append(bids, orderbook.Item{
Amount: bid[1].(float64),
Price: bid[0].(float64),
Amount: depth.Bids[i][1].(float64),
Price: depth.Bids[i][0].(float64),
})
}
@@ -172,7 +170,7 @@ func (z *ZB) WsHandleData() {
channelInfo := strings.Split(result.Channel, "_")
cPair := currency.NewPairFromString(channelInfo[0])
z.Websocket.DataHandler <- wshandler.TradeData{
Timestamp: time.Unix(0, t.Date),
Timestamp: time.Unix(0, t.Date*int64(time.Millisecond)),
CurrencyPair: cPair,
AssetType: asset.Spot,
Exchange: z.GetName(),

View File

@@ -43,20 +43,20 @@ type WsTicker struct {
// WsDepth defines websocket orderbook data
type WsDepth struct {
Timestamp int64 `json:"timestamp"`
Asks []interface{} `json:"asks"`
Bids []interface{} `json:"bids"`
Timestamp int64 `json:"timestamp"`
Asks [][]interface{} `json:"asks"`
Bids [][]interface{} `json:"bids"`
}
// WsTrades defines websocket trade data
type WsTrades struct {
Data []struct {
Amount float64 `json:"amount,string"`
Price float64 `json:"price,string"`
TID int64 `json:"tid"`
Date int64 `json:"date"`
Type string `json:"type"`
TradeType string `json:"trade_type"`
Amount float64 `json:"amount,string"`
Price float64 `json:"price,string"`
TID interface{} `json:"tid"`
Date int64 `json:"date"`
Type string `json:"type"`
TradeType string `json:"trade_type"`
} `json:"data"`
}

View File

@@ -2,8 +2,8 @@ package zb
import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"
@@ -221,15 +221,16 @@ func (z *ZB) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price,
return tickerPrice, err
}
for _, x := range z.GetEnabledPairs(assetType) {
enabledPairs := z.GetEnabledPairs(assetType)
for x := range enabledPairs {
// We can't use either pair format here, so format it to lower-
// case and without any delimiter
curr := x.Format("", false).String()
curr := enabledPairs[x].Format("", false).String()
if _, ok := result[curr]; !ok {
continue
}
var tp ticker.Price
tp.Pair = x
tp.Pair = enabledPairs[x]
tp.High = result[curr].High
tp.Last = result[curr].Last
tp.Ask = result[curr].Sell
@@ -276,12 +277,14 @@ func (z *ZB) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.B
for x := range orderbookNew.Bids {
data := orderbookNew.Bids[x]
orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: data[1], Price: data[0]})
orderBook.Bids = append(orderBook.Bids,
orderbook.Item{Amount: data[1], Price: data[0]})
}
for x := range orderbookNew.Asks {
data := orderbookNew.Asks[x]
orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: data[1], Price: data[0]})
orderBook.Asks = append(orderBook.Asks,
orderbook.Item{Amount: data[1], Price: data[0]})
}
orderBook.Pair = p
@@ -366,7 +369,7 @@ func (z *ZB) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) {
}
response, err := z.SpotNewOrder(params)
if response > 0 {
submitOrderResponse.OrderID = fmt.Sprintf("%v", response)
submitOrderResponse.OrderID = strconv.FormatInt(response, 10)
}
if err == nil {
submitOrderResponse.IsOrderPlaced = true
@@ -398,13 +401,13 @@ func (z *ZB) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) {
var allOpenOrders []Order
enabledPairs := z.GetEnabledPairs(asset.Spot)
for x := range enabledPairs {
// Limiting to 10 pages
for pageNumber := int64(0); pageNumber < 11; pageNumber++ {
fCurr := z.FormatExchangeCurrency(enabledPairs[x], asset.Spot).String()
openOrders, err := z.GetUnfinishedOrdersIgnoreTradeType(fCurr,
pageNumber,
10)
fPair := z.FormatExchangeCurrency(enabledPairs[x], asset.Spot).String()
for y := int64(1); ; y++ {
openOrders, err := z.GetUnfinishedOrdersIgnoreTradeType(fPair, y, 10)
if err != nil {
if strings.Contains(err.Error(), "3001") {
break
}
return cancelAllOrdersResponse, err
}
@@ -413,6 +416,10 @@ func (z *ZB) CancelAllOrders(_ *order.Cancel) (order.CancelAllResponse, error) {
}
allOpenOrders = append(allOpenOrders, openOrders...)
if len(openOrders) != 10 {
break
}
}
}
@@ -481,20 +488,25 @@ func (z *ZB) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) {
func (z *ZB) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error) {
var allOrders []Order
for x := range req.Currencies {
// Limiting to 10 pages
for pageNumber := int64(0); pageNumber < 11; pageNumber++ {
fCurr := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String()
resp, err := z.GetUnfinishedOrdersIgnoreTradeType(fCurr,
pageNumber,
10)
for i := int64(1); ; i++ {
fPair := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String()
resp, err := z.GetUnfinishedOrdersIgnoreTradeType(fPair, i, 10)
if err != nil {
if strings.Contains(err.Error(), "3001") {
break
}
return nil, err
}
if len(resp) == 0 {
break
}
allOrders = append(allOrders, resp...)
if len(resp) != 10 {
break
}
}
}
@@ -505,7 +517,7 @@ func (z *ZB) GetActiveOrders(req *order.GetOrdersRequest) ([]order.Detail, error
orderDate := time.Unix(int64(allOrders[i].TradeDate), 0)
orderSide := orderSideMap[allOrders[i].Type]
orders = append(orders, order.Detail{
ID: fmt.Sprintf("%d", allOrders[i].ID),
ID: strconv.FormatInt(allOrders[i].ID, 10),
Amount: allOrders[i].TotalAmount,
Exchange: z.Name,
OrderDate: orderDate,
@@ -536,10 +548,9 @@ func (z *ZB) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error
}
for x := range req.Currencies {
// Limiting to 10 pages
for pageNumber := int64(0); pageNumber < 11; pageNumber++ {
fCurr := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String()
resp, err := z.GetOrders(fCurr, pageNumber, side)
for y := int64(1); ; y++ {
fPair := z.FormatExchangeCurrency(req.Currencies[x], asset.Spot).String()
resp, err := z.GetOrders(fPair, y, side)
if err != nil {
return nil, err
}
@@ -549,6 +560,10 @@ func (z *ZB) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error
}
allOrders = append(allOrders, resp...)
if len(resp) != 10 {
break
}
}
}
@@ -559,7 +574,7 @@ func (z *ZB) GetOrderHistory(req *order.GetOrdersRequest) ([]order.Detail, error
orderDate := time.Unix(int64(allOrders[i].TradeDate), 0)
orderSide := orderSideMap[allOrders[i].Type]
orders = append(orders, order.Detail{
ID: fmt.Sprintf("%d", allOrders[i].ID),
ID: strconv.FormatInt(allOrders[i].ID, 10),
Amount: allOrders[i].TotalAmount,
Exchange: z.Name,
OrderDate: orderDate,